feat(card-sorting): add updateCardPositions action to Provider

Add updateCardPositions action and optimistic update handler:
- Add updateCardPositions callback that sends UPDATE_CARD_POSITIONS moves
- Add optimistic update case for position updates
- Fix card shuffle bug: shuffle on client before sending to server
- Remove double-shuffle in optimistic START_GAME handler
- Clean up unnecessary dependencies in useCallback hooks

This enables throttled real-time position updates during drag operations
that sync across all connected browser windows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-23 14:32:58 -05:00
parent 656f5a7838
commit f6ed4a27a2

View File

@@ -1,14 +1,6 @@
'use client' 'use client'
import { import { type ReactNode, useCallback, useMemo, createContext, useContext, useState } from 'react'
type ReactNode,
useCallback,
useMemo,
createContext,
useContext,
useState,
useEffect,
} from 'react'
import { useArcadeSession } from '@/hooks/useArcadeSession' import { useArcadeSession } from '@/hooks/useArcadeSession'
import { useRoomData, useUpdateGameConfig } from '@/hooks/useRoomData' import { useRoomData, useUpdateGameConfig } from '@/hooks/useRoomData'
import { useViewerId } from '@/hooks/useViewerId' import { useViewerId } from '@/hooks/useViewerId'
@@ -16,7 +8,13 @@ import { buildPlayerMetadata as buildPlayerMetadataUtil } from '@/lib/arcade/pla
import type { GameMove } from '@/lib/arcade/validation' import type { GameMove } from '@/lib/arcade/validation'
import { useGameMode } from '@/contexts/GameModeContext' import { useGameMode } from '@/contexts/GameModeContext'
import { generateRandomCards, shuffleCards } from './utils/cardGeneration' import { generateRandomCards, shuffleCards } from './utils/cardGeneration'
import type { CardSortingState, CardSortingMove, SortingCard, CardSortingConfig } from './types' import type {
CardSortingState,
CardSortingMove,
SortingCard,
CardSortingConfig,
CardPosition,
} from './types'
// Context value interface // Context value interface
interface CardSortingContextValue { interface CardSortingContextValue {
@@ -31,6 +29,7 @@ interface CardSortingContextValue {
goToSetup: () => void goToSetup: () => void
resumeGame: () => void resumeGame: () => void
setConfig: (field: 'cardCount' | 'showNumbers' | 'timeLimit', value: unknown) => void setConfig: (field: 'cardCount' | 'showNumbers' | 'timeLimit', value: unknown) => void
updateCardPositions: (positions: CardPosition[]) => void
exitSession: () => void exitSession: () => void
// Computed // Computed
canCheckSolution: boolean canCheckSolution: boolean
@@ -68,6 +67,7 @@ const createInitialState = (config: Partial<CardSortingConfig>): CardSortingStat
correctOrder: [], correctOrder: [],
availableCards: [], availableCards: [],
placedCards: new Array(config.cardCount ?? 8).fill(null), placedCards: new Array(config.cardCount ?? 8).fill(null),
cardPositions: [],
selectedCardId: null, selectedCardId: null,
numbersRevealed: false, numbersRevealed: false,
scoreBreakdown: null, scoreBreakdown: null,
@@ -92,7 +92,8 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
gameStartTime: Date.now(), gameStartTime: Date.now(),
selectedCards, selectedCards,
correctOrder, correctOrder,
availableCards: shuffleCards(selectedCards), // Use cards in the order they were sent (already shuffled by initiating client)
availableCards: selectedCards,
placedCards: new Array(state.cardCount).fill(null), placedCards: new Array(state.cardCount).fill(null),
numbersRevealed: false, numbersRevealed: false,
// Save original config for pause/resume // Save original config for pause/resume
@@ -282,6 +283,13 @@ function applyMoveOptimistically(state: CardSortingState, move: GameMove): CardS
} }
} }
case 'UPDATE_CARD_POSITIONS': {
return {
...state,
cardPositions: typedMove.data.positions,
}
}
default: default:
return state return state
} }
@@ -386,7 +394,7 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
} }
const playerMetadata = buildPlayerMetadata() const playerMetadata = buildPlayerMetadata()
const selectedCards = generateRandomCards(state.cardCount) const selectedCards = shuffleCards(generateRandomCards(state.cardCount))
sendMove({ sendMove({
type: 'START_GAME', type: 'START_GAME',
@@ -397,7 +405,7 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
selectedCards, selectedCards,
}, },
}) })
}, [localPlayerId, state.cardCount, buildPlayerMetadata, sendMove, viewerId, state.gamePhase]) }, [localPlayerId, state.cardCount, buildPlayerMetadata, sendMove, viewerId])
const placeCard = useCallback( const placeCard = useCallback(
(cardId: string, position: number) => { (cardId: string, position: number) => {
@@ -465,7 +473,7 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
}, },
}) })
}, },
[localPlayerId, canCheckSolution, sendMove, viewerId, state.cardCount, state.gamePhase] [localPlayerId, canCheckSolution, sendMove, viewerId]
) )
const revealNumbers = useCallback(() => { const revealNumbers = useCallback(() => {
@@ -538,6 +546,20 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
[localPlayerId, sendMove, viewerId, roomData, updateGameConfig] [localPlayerId, sendMove, viewerId, roomData, updateGameConfig]
) )
const updateCardPositions = useCallback(
(positions: CardPosition[]) => {
if (!localPlayerId) return
sendMove({
type: 'UPDATE_CARD_POSITIONS',
playerId: localPlayerId,
userId: viewerId || '',
data: { positions },
})
},
[localPlayerId, sendMove, viewerId]
)
const contextValue: CardSortingContextValue = { const contextValue: CardSortingContextValue = {
state, state,
// Actions // Actions
@@ -550,6 +572,7 @@ export function CardSortingProvider({ children }: { children: ReactNode }) {
goToSetup, goToSetup,
resumeGame, resumeGame,
setConfig, setConfig,
updateCardPositions,
exitSession, exitSession,
// Computed // Computed
canCheckSolution, canCheckSolution,