Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | 'use client' import { useCallback, useEffect, useRef, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' import { roomKeys, useCreateRoom, useLeaveRoom, useSetRoomGame, type RoomData } from './useRoomData' export interface UseGameBreakRoomOptions { studentName: string enabled: boolean /** * Pre-configured game settings to apply when selecting a game. * Nested by game name: { 'memory-quiz': { selectedCount: 5 } } */ gameConfig?: Record<string, Record<string, unknown>> onRoomReady?: (room: RoomData) => void onError?: (error: Error) => void } export interface UseGameBreakRoomResult { room: RoomData | null isCreating: boolean isSettingGame: boolean error: Error | null /** * Select a game for this room. * If gameConfig was provided in options and contains settings for this game, * those settings will be applied. */ selectGame: (gameName: string, configOverride?: Record<string, unknown>) => Promise<void> cleanup: () => Promise<void> } export function useGameBreakRoom({ studentName, enabled, gameConfig, onRoomReady, onError, }: UseGameBreakRoomOptions): UseGameBreakRoomResult { const queryClient = useQueryClient() const [room, setRoom] = useState<RoomData | null>(null) const [error, setError] = useState<Error | null>(null) const roomIdRef = useRef<string | null>(null) const isCleaningUpRef = useRef(false) const hasStartedRef = useRef(false) const onRoomReadyRef = useRef(onRoomReady) const onErrorRef = useRef(onError) onRoomReadyRef.current = onRoomReady onErrorRef.current = onError const createRoom = useCreateRoom() const leaveRoom = useLeaveRoom() const setRoomGame = useSetRoomGame() const createRoomRef = useRef(createRoom) const leaveRoomRef = useRef(leaveRoom) createRoomRef.current = createRoom leaveRoomRef.current = leaveRoom // Track if room creation is in progress to prevent duplicates const isCreatingRef = useRef(false) useEffect(() => { if (!enabled) { hasStartedRef.current = false return } // Skip if already started or already have a room if ( hasStartedRef.current || roomIdRef.current || isCleaningUpRef.current || isCreatingRef.current ) { return } hasStartedRef.current = true isCreatingRef.current = true async function initRoom() { try { const result = await createRoomRef.current.mutateAsync({ name: `${studentName}'s Game Break`, gameName: null, accessMode: 'open', }) const newRoom = result.room // Always set the room - don't check mounted flag during Strict Mode // The cleanup() function handles proper teardown when truly unmounting if (!roomIdRef.current) { roomIdRef.current = newRoom.id setRoom(newRoom) onRoomReadyRef.current?.(newRoom) } else { // Another room was already set (shouldn't happen with guards), leave this one console.log('[useGameBreakRoom] Room already exists, leaving duplicate') await leaveRoomRef.current.mutateAsync(newRoom.id).catch(() => {}) } } catch (err) { const error = err instanceof Error ? err : new Error(String(err)) setError(error) onErrorRef.current?.(error) } finally { isCreatingRef.current = false } } initRoom() // No cleanup needed here - cleanup() handles room teardown }, [enabled, studentName]) const selectGame = useCallback( async (gameName: string, configOverride?: Record<string, unknown>) => { if (!room) { throw new Error('No room available') } // Merge config sources: gameConfig[gameName] from options + configOverride const baseConfig = gameConfig?.[gameName] const mergedConfig = baseConfig || configOverride ? { ...baseConfig, ...configOverride } : undefined await setRoomGame.mutateAsync({ roomId: room.id, gameName, gameConfig: mergedConfig ? { [gameName]: mergedConfig } : undefined, }) setRoom((prev) => prev ? { ...prev, gameName, gameConfig: mergedConfig ? { [gameName]: mergedConfig } : prev.gameConfig, } : null ) }, [room, setRoomGame, gameConfig] ) const cleanup = useCallback(async () => { if (isCleaningUpRef.current) return isCleaningUpRef.current = true const roomId = roomIdRef.current if (roomId) { try { await leaveRoomRef.current.mutateAsync(roomId) } catch { // Intentionally swallow cleanup errors } roomIdRef.current = null queryClient.setQueryData(roomKeys.current(), null) } setRoom(null) hasStartedRef.current = false isCleaningUpRef.current = false }, [queryClient]) return { room, isCreating: createRoom.isPending, isSettingGame: setRoomGame.isPending, error, selectGame, cleanup, } } |