Skip to content

Commit

Permalink
Automate turn based card draw. (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
norswap authored Feb 26, 2024
2 parents 7820324 + 2e4d0fe commit 8e28481
Show file tree
Hide file tree
Showing 17 changed files with 261 additions and 83 deletions.
2 changes: 2 additions & 0 deletions packages/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
"lodash": "^4.17.21",
"lucide-react": "^0.309.0",
"next": "^13.5.6",
"next-themes": "^0.2.1",
"next-transpile-modules": "^10.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.11.0",
"snarkjs": "^0.7.1",
"sonner": "^1.4.0",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"viem": "^1.16.6",
Expand Down
8 changes: 6 additions & 2 deletions packages/webapp/src/actions/drawCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export async function drawCard(args: DrawCardArgs): Promise<boolean> {
}
}

/** Intentionally left blank to ignore any loading message.
* This function accepts a message parameter but does nothing with it. */
function setLoading(_message: string|null|undefined) {}

async function drawCardImpl(args: DrawCardArgs): Promise<boolean> {

const gameID = getGameID()
Expand Down Expand Up @@ -92,7 +96,7 @@ async function drawCardImpl(args: DrawCardArgs): Promise<boolean> {
const cards = getCards()!
console.log(`drew card ${cards[selectedCard]}`)

args.setLoading("Generating draw proof ...")
setLoading("Generating draw proof ...")

const tmpHandSize = privateInfo.handIndexes.indexOf(255)
const initialHandSize = tmpHandSize < 0
Expand Down Expand Up @@ -138,7 +142,7 @@ async function drawCardImpl(args: DrawCardArgs): Promise<boolean> {
proof.proof_b,
proof.proof_c
],
setLoading: args.setLoading
setLoading: setLoading
})))

// TODO: this should be put in an optimistic store, before proof generation
Expand Down
3 changes: 3 additions & 0 deletions packages/webapp/src/components/cards/cardContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ interface BaseCardProps {
className?: string
handHovered?: boolean
placement: CardPlacement
cardGlow?: boolean
}

const CardContainer: React.FC<BaseCardProps> = ({
id,
handHovered,
placement,
cardGlow
}) => {
const {
attributes,
Expand Down Expand Up @@ -46,6 +48,7 @@ const CardContainer: React.FC<BaseCardProps> = ({
handHovered={handHovered}
isDragging={isDragging}
ref={setNodeRef}
cardGlow={cardGlow}
/>
)
case CardPlacement.BOARD:
Expand Down
5 changes: 3 additions & 2 deletions packages/webapp/src/components/cards/handCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ interface HandCardProps {
id: number
handHovered?: boolean
isDragging: boolean
cardGlow?: boolean
}

const HandCard = forwardRef<HTMLDivElement, HandCardProps>(
({ id, isDragging, handHovered }, ref) => {
({ id, isDragging, handHovered, cardGlow }, ref) => {
const [ cardHover, setCardHover ] = useState<boolean>(false)
const [ isDetailsVisible, setIsDetailsVisible ] = useState<boolean>(false)
const showingDetails = isDetailsVisible && !isDragging
Expand Down Expand Up @@ -50,7 +51,7 @@ const HandCard = forwardRef<HTMLDivElement, HandCardProps>(
className="pointer-events-none rounded-xl border select-none"
style={{
boxShadow:
cardHover && !isDetailsVisible ? "0 0 10px 2px gold" : "none", // Adds golden glow when hovered
(cardHover && !isDetailsVisible) || cardGlow ? "0 0 10px 2px gold" : "none", // Adds golden glow when hovered
}}
/>
{showingDetails && (
Expand Down
20 changes: 14 additions & 6 deletions packages/webapp/src/components/hand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ const Hand = ({
setLoading: (label: string | null) => void
cancellationHandler: CancellationHandler
}) => {
const [ isFocused, setIsFocused ] = useState<boolean>(false)
const scrollWrapperRef = useRef<any>()
const { showLeftArrow, scrollLeft, showRightArrow, scrollRight } =
useScrollBox(scrollWrapperRef)
const [isFocused, setIsFocused] = useState<boolean>(false)
const scrollWrapperRef = useRef<HTMLDivElement>(null)
const {
showLeftArrow,
scrollLeft,
showRightArrow,
scrollRight,
isLastCardGlowing,
} = useScrollBox(scrollWrapperRef, cards)

const { setNodeRef } = useSortable({
id: CardPlacement.HAND,
Expand All @@ -44,7 +49,9 @@ const Hand = ({

return (
<div
className={`${className} flex flex-row items-center justify-evenly bottom-0 w-[95%] space-x-2`}
className={`${className} ${
isLastCardGlowing ? "translate-y-0" : null
} py-4 flex flex-row items-center justify-evenly bottom-0 w-[95%] space-x-2`}
ref={setNodeRef}
onMouseEnter={() => {
setIsFocused(true)
Expand Down Expand Up @@ -74,6 +81,7 @@ const Hand = ({
<CardContainer
id={convertedCards[index - 1]}
placement={CardPlacement.HAND}
cardGlow={isLastCardGlowing && index === range.length}
/>
</div>
))}
Expand All @@ -94,4 +102,4 @@ const Hand = ({
)
}

export default Hand
export default Hand
8 changes: 7 additions & 1 deletion packages/webapp/src/components/modals/globalErrorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DialogTitle,
} from "../ui/dialog"
import { Button } from "src/components/ui/button"
import { useEffect, useState } from "react"

/**
* A modal displayed globally (setup in _app.tsx) whenever the errorConfig state is set to non-null.
Expand All @@ -16,9 +17,14 @@ export const GlobalErrorModal = ({ config }: { config: ErrorConfig }) => {
// UI. This is good practice as it lets the user figure out what happened. Really not a priority
// at the moment, and the error should be systematically logged to the console instead, for
// debugging purposes.
const [ open, setOpen ] = useState<boolean>(false)
useEffect(() => {
if(config !== null && !open) setOpen(true)
else setOpen(false)
}, [config, open])

return (
<Dialog>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTitle>{config.title}</DialogTitle>
<DialogContent>
{config.message !== "" && (
Expand Down
2 changes: 0 additions & 2 deletions packages/webapp/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client"

import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
Expand Down
31 changes: 31 additions & 0 deletions packages/webapp/src/components/ui/sonner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

// ref: https://ui.shadcn.com/docs/components/sonner
// docs: https://sonner.emilkowal.ski/
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()

return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}

export { Toaster }
72 changes: 61 additions & 11 deletions packages/webapp/src/hooks/useScrollBox.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import { useState, useEffect, useCallback } from "react"
import { useState, useEffect, useCallback, RefObject } from "react"
import throttle from "lodash/throttle"
import { toast } from "sonner"

const timing = (1 / 60) * 1000
const decay = (v: any) => -0.1 * ((1 / timing) ^ 4) + v

function useScrollBox(scrollRef: any) {
const [lastScrollX, setLastScrollX] = useState(0)
const [showLeftArrow, setShowLeftArrow] = useState<boolean>(false)
const [showRightArrow, setShowRightArrow] = useState<boolean>(false)
function useScrollBox(scrollRef: RefObject<HTMLDivElement>, cards: readonly bigint[] | null) {
// Stores the last horizontal scroll position.
const [ lastScrollX, setLastScrollX ] = useState(0)

// Determines the visibility of navigation arrows based on scroll position.
const [ showLeftArrow, setShowLeftArrow ] = useState<boolean>(false)
const [ showRightArrow, setShowRightArrow ] = useState<boolean>(false)

const [ isLastCardGlowing, setIsLastCardGlowing ] = useState<boolean>(false)

const scrollWrapperCurrent = scrollRef.current

const cardWidth = 200 // width of card when not in focus
const scrollAmount = 2 * cardWidth
const duration = 300

/** Checks and updates the arrow visibility states based on the scroll position. */
const checkArrowsVisibility = () => {
if (!scrollRef.current) return
const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current
setShowLeftArrow(scrollLeft > 0)
setShowRightArrow(scrollLeft < scrollWidth - clientWidth)
}

const smoothScroll = (target: number) => {
/** Performs a smooth scrolling animation to a specified target position.
* Accepts a target scroll position and an optional callback to execute after completion. */
const smoothScroll = useCallback((target: number, callback?: () => void) => {
if (!scrollRef.current) return

const start = scrollRef.current.scrollLeft
Expand All @@ -32,15 +40,20 @@ function useScrollBox(scrollRef: any) {
const now = Date.now()
const time = Math.min(1, (now - startTime) / duration)

scrollRef.current.scrollLeft = start + time * (target - start)
scrollRef.current!.scrollLeft = start + time * (target - start)

if (time < 1) requestAnimationFrame(animateScroll)
else checkArrowsVisibility()
if (time < 1) {
requestAnimationFrame(animateScroll)
} else {
checkArrowsVisibility()
if (callback) callback() // Execute callback after the scroll animation completes
}
}

requestAnimationFrame(animateScroll)
}
}, [])

/** Scrolls the container a fixed distance to the left or right with animation. */
const scrollLeft = () => {
if (!scrollRef.current) return
const target = Math.max(0, scrollRef.current.scrollLeft - scrollAmount)
Expand All @@ -58,25 +71,50 @@ function useScrollBox(scrollRef: any) {
smoothScroll(target)
}

/** Throttled function to update the last horizontal scroll position, minimizing performance impact. */
const handleLastScrollX = useCallback(
throttle((screenX) => {
setLastScrollX(screenX)
}, timing),
[]
)

/** Handles the wheel event to adjust the scrollLeft property, enabling horizontal scrolling. */
const handleScroll = (e: WheelEvent) => {
if (scrollRef.current) {
// Adjust the scrollLeft property based on the deltaY value
scrollRef.current.scrollLeft += e.deltaY
}
}

/** Responds to window resize events to update arrow visibility states. */
const handleResize = () => {
setShowLeftArrow(true)
setShowRightArrow(true)
}

/** Smoothly scrolls to the rightmost end of the container,
* triggers a glow in the last card added. */
const smoothScrollToRightThenLeft = useCallback(() => {
const element = scrollRef.current
if (!element) return

const targetRight = element.scrollWidth - element.clientWidth
smoothScroll(targetRight, () => {
triggerLastCardGlow()
})
}, [scrollRef])

const triggerLastCardGlow = useCallback(() => {
setIsLastCardGlowing(true)
// dismiss the toast displaying draw status
toast.dismiss("DRAW_CARD_TOAST")
setTimeout(() => {
setIsLastCardGlowing(false)
}, 2500)
}, [])

/** Sets up and cleans up event listeners for resize, scroll, and wheel events. */
useEffect(() => {
if (scrollRef.current) {
checkArrowsVisibility()
Expand All @@ -101,11 +139,23 @@ function useScrollBox(scrollRef: any) {
}
}, [scrollWrapperCurrent, handleLastScrollX, lastScrollX])

// Detects changes in the `cards` array to trigger the pop-up effect and initiate smooth scrolling to highlight new content.
useEffect(() => {
if (cards && cards.length > 0) {
const timer = setTimeout(() => {
smoothScrollToRightThenLeft()
}, 3000)

return () => clearTimeout(timer)
}
}, [cards, smoothScrollToRightThenLeft])

return {
showLeftArrow,
scrollLeft,
showRightArrow,
scrollRight,
isLastCardGlowing,
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/webapp/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import "src/styles/globals.css"
import { useRouter } from "next/router"
import { ComponentType, useEffect } from "react"
import { Deck } from "src/store/types"
import { Toaster } from "src/components/ui/sonner"

// =================================================================================================

Expand Down Expand Up @@ -50,6 +51,7 @@ const MyApp: AppType = ({ Component, pageProps }) => {
<ConnectKitProvider>
{jotaiDebug()}
<ComponentWrapper Component={Component} pageProps={pageProps} />
<Toaster expand={true} />
</ConnectKitProvider>
</WagmiConfig>
</>
Expand Down
Loading

0 comments on commit 8e28481

Please sign in to comment.