Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automate turn based card draw. #116

Merged
merged 17 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ...")

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather we kept this logic (which is useful for debugging), but simply ignored it by supplying a custom setLoading function that ignores it.

Copy link
Collaborator Author

@ultraviolet10 ultraviolet10 Feb 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something as simple as:

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

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should handle the case where it is set to null to make the toast go away.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toast disappears in a couple of seconds as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to make the call we need to dismiss it — as if it overlaps with the card being highlighted, which is really "eh".

Also, we should "pop up" the hand (as if it was hovered by the cursor, even if it isn't) at least for the time of the card highlight. I would make the card highlight time a tad longer as well (maybe 3s instead of 2.5?)

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cf. above comment

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 = () => {
ultraviolet10 marked this conversation as resolved.
Show resolved Hide resolved
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