diff --git a/apps/web/src/hooks/useUserPlayers.ts b/apps/web/src/hooks/useUserPlayers.ts index 5881c7ff..3d9b6422 100644 --- a/apps/web/src/hooks/useUserPlayers.ts +++ b/apps/web/src/hooks/useUserPlayers.ts @@ -88,8 +88,34 @@ export function useCreatePlayer() { return useMutation({ mutationFn: createPlayer, - onSuccess: () => { - // Invalidate and refetch players list + onMutate: async (newPlayer) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: playerKeys.lists() }) + + // Snapshot previous value + const previousPlayers = queryClient.getQueryData(playerKeys.list()) + + // Optimistically update to new value + if (previousPlayers) { + const optimisticPlayer: Player = { + id: `temp-${Date.now()}`, // Temporary ID + ...newPlayer, + createdAt: new Date(), + isActive: newPlayer.isActive ?? false, + } + queryClient.setQueryData(playerKeys.list(), [...previousPlayers, optimisticPlayer]) + } + + return { previousPlayers } + }, + onError: (_err, _newPlayer, context) => { + // Rollback on error + if (context?.previousPlayers) { + queryClient.setQueryData(playerKeys.list(), context.previousPlayers) + } + }, + onSettled: () => { + // Always refetch after error or success queryClient.invalidateQueries({ queryKey: playerKeys.lists() }) }, }) @@ -103,11 +129,35 @@ export function useUpdatePlayer() { return useMutation({ mutationFn: updatePlayer, - onSuccess: (updatedPlayer) => { - // Invalidate players list + onMutate: async ({ id, updates }) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: playerKeys.lists() }) + + // Snapshot previous value + const previousPlayers = queryClient.getQueryData(playerKeys.list()) + + // Optimistically update + if (previousPlayers) { + const optimisticPlayers = previousPlayers.map(player => + player.id === id ? { ...player, ...updates } : player + ) + queryClient.setQueryData(playerKeys.list(), optimisticPlayers) + } + + return { previousPlayers } + }, + onError: (_err, _variables, context) => { + // Rollback on error + if (context?.previousPlayers) { + queryClient.setQueryData(playerKeys.list(), context.previousPlayers) + } + }, + onSettled: (_data, _error, { id }) => { + // Refetch after error or success queryClient.invalidateQueries({ queryKey: playerKeys.lists() }) - // Update detail cache if it exists - queryClient.setQueryData(playerKeys.detail(updatedPlayer.id), updatedPlayer) + if (_data) { + queryClient.setQueryData(playerKeys.detail(id), _data) + } }, }) } @@ -120,8 +170,29 @@ export function useDeletePlayer() { return useMutation({ mutationFn: deletePlayer, - onSuccess: () => { - // Invalidate players list + onMutate: async (id) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: playerKeys.lists() }) + + // Snapshot previous value + const previousPlayers = queryClient.getQueryData(playerKeys.list()) + + // Optimistically remove from list + if (previousPlayers) { + const optimisticPlayers = previousPlayers.filter(player => player.id !== id) + queryClient.setQueryData(playerKeys.list(), optimisticPlayers) + } + + return { previousPlayers } + }, + onError: (_err, _id, context) => { + // Rollback on error + if (context?.previousPlayers) { + queryClient.setQueryData(playerKeys.list(), context.previousPlayers) + } + }, + onSettled: () => { + // Refetch after error or success queryClient.invalidateQueries({ queryKey: playerKeys.lists() }) }, }) diff --git a/apps/web/src/hooks/useUserStats.ts b/apps/web/src/hooks/useUserStats.ts index 6add06d7..c1bb391b 100644 --- a/apps/web/src/hooks/useUserStats.ts +++ b/apps/web/src/hooks/useUserStats.ts @@ -56,9 +56,32 @@ export function useUpdateUserStats() { return useMutation({ mutationFn: updateUserStats, - onSuccess: (updatedStats) => { - // Update cache with new stats - queryClient.setQueryData(statsKeys.detail(), updatedStats) + onMutate: async (updates) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: statsKeys.detail() }) + + // Snapshot previous value + const previousStats = queryClient.getQueryData(statsKeys.detail()) + + // Optimistically update + if (previousStats) { + const optimisticStats = { ...previousStats, ...updates } + queryClient.setQueryData(statsKeys.detail(), optimisticStats) + } + + return { previousStats } + }, + onError: (_err, _updates, context) => { + // Rollback on error + if (context?.previousStats) { + queryClient.setQueryData(statsKeys.detail(), context.previousStats) + } + }, + onSettled: (updatedStats) => { + // Update with server data on success + if (updatedStats) { + queryClient.setQueryData(statsKeys.detail(), updatedStats) + } }, }) } diff --git a/apps/web/src/types/player.ts b/apps/web/src/types/player.ts index a49c996f..e7a37045 100644 --- a/apps/web/src/types/player.ts +++ b/apps/web/src/types/player.ts @@ -1,84 +1,5 @@ /** - * Player types for the new UUID-based player system - * This replaces the old index-based (1-4) player system - */ - -export interface Player { - /** Globally unique identifier (nanoid) */ - id: string - - /** Display name */ - name: string - - /** Player emoji/avatar */ - emoji: string - - /** Player color (for UI theming) */ - color: string - - /** Creation timestamp */ - createdAt: number - - /** Optional: Device ID for multi-device sync */ - deviceId?: string - - /** Optional: Last sync timestamp */ - syncedAt?: number - - /** Optional: Whether this is a local or remote player */ - isLocal?: boolean -} - -/** - * Storage format V2 - * Replaces the old UserProfile format with indexed players - */ -export interface PlayerStorageV2 { - version: 2 - - /** All known players, keyed by ID */ - players: Record - - /** IDs of currently active players (order matters) */ - activePlayerIds: string[] - - /** Activation order mapping for sorting */ - activationOrder: Record -} - -/** - * Legacy storage format (V1) - * Used for migration from old system - */ -export interface UserProfileV1 { - player1Emoji: string - player2Emoji: string - player3Emoji: string - player4Emoji: string - player1Name: string - player2Name: string - player3Name: string - player4Name: string - gamesPlayed: number - totalWins: number - favoriteGameType: 'abacus-numeral' | 'complement-pairs' | null - bestTime: number | null - highestAccuracy: number -} - -/** - * New stats-only profile (player data moved to PlayerStorageV2) - */ -export interface UserStatsProfile { - gamesPlayed: number - totalWins: number - favoriteGameType: 'abacus-numeral' | 'complement-pairs' | null - bestTime: number | null - highestAccuracy: number -} - -/** - * Default player colors (used during migration and player creation) + * Default player colors (used during player creation) */ export const DEFAULT_PLAYER_COLORS = [ '#3b82f6', // Blue @@ -93,6 +14,20 @@ export const DEFAULT_PLAYER_COLORS = [ '#84cc16', // Lime ] +/** + * Client-side Player type (for contexts/components) + * Matches database Player type but with flexible createdAt + */ +export interface Player { + id: string + name: string + emoji: string + color: string + createdAt: Date | number + isActive?: boolean + isLocal?: boolean +} + /** * Get a color for a new player (cycles through defaults) */