feat: add optimistic updates and remove dead code
Phase 4 & 5: Cleanup + Optimistic Updates - Removed dead localStorage types from player.ts - Added optimistic updates to all player mutations - Added optimistic updates to stats mutations - Instant UI feedback with automatic rollback on error Breaking changes: None 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fe01a1fe18
commit
b62cf26fb6
|
|
@ -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<Player[]>(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<Player[]>(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<Player[]>(playerKeys.list())
|
||||
|
||||
// Optimistically update
|
||||
if (previousPlayers) {
|
||||
const optimisticPlayers = previousPlayers.map(player =>
|
||||
player.id === id ? { ...player, ...updates } : player
|
||||
)
|
||||
queryClient.setQueryData<Player[]>(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<Player[]>(playerKeys.list())
|
||||
|
||||
// Optimistically remove from list
|
||||
if (previousPlayers) {
|
||||
const optimisticPlayers = previousPlayers.filter(player => player.id !== id)
|
||||
queryClient.setQueryData<Player[]>(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() })
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<UserStats>(statsKeys.detail())
|
||||
|
||||
// Optimistically update
|
||||
if (previousStats) {
|
||||
const optimisticStats = { ...previousStats, ...updates }
|
||||
queryClient.setQueryData<UserStats>(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)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, Player>
|
||||
|
||||
/** IDs of currently active players (order matters) */
|
||||
activePlayerIds: string[]
|
||||
|
||||
/** Activation order mapping for sorting */
|
||||
activationOrder: Record<string, number>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue