refactor: remove old arcade guard system

Remove deprecated arcade guard hooks and components:
- useArcadeGuard.ts
- useArcadeRedirect.ts
- ArcadeGuardedPage.tsx
- Related tests

These have been replaced by the new room-based system with
proper moderation, invitations, and real-time updates.

🤖 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-13 11:24:35 -05:00
parent 087652f9e7
commit 2e6469bed4
5 changed files with 0 additions and 1072 deletions

View File

@@ -1,312 +0,0 @@
import { render, screen, waitFor } from '@testing-library/react'
import * as nextNavigation from 'next/navigation'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as arcadeGuard from '@/hooks/useArcadeGuard'
import * as roomData from '@/hooks/useRoomData'
import * as viewerId from '@/hooks/useViewerId'
// Mock Next.js navigation
vi.mock('next/navigation', () => ({
useRouter: vi.fn(),
usePathname: vi.fn(),
useParams: vi.fn(),
}))
// Mock hooks
vi.mock('@/hooks/useArcadeGuard')
vi.mock('@/hooks/useRoomData')
vi.mock('@/hooks/useViewerId')
vi.mock('@/hooks/useUserPlayers', () => ({
useUserPlayers: () => ({ data: [], isLoading: false }),
useCreatePlayer: () => ({ mutate: vi.fn() }),
useUpdatePlayer: () => ({ mutate: vi.fn() }),
useDeletePlayer: () => ({ mutate: vi.fn() }),
}))
vi.mock('@/hooks/useArcadeSocket', () => ({
useArcadeSocket: () => ({
connected: false,
joinSession: vi.fn(),
socket: null,
sendMove: vi.fn(),
exitSession: vi.fn(),
pingSession: vi.fn(),
}),
}))
// Mock styled-system
vi.mock('../../../../styled-system/css', () => ({
css: () => '',
}))
// Mock components
vi.mock('@/components/PageWithNav', () => ({
PageWithNav: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
// Import pages after mocks
import RoomBrowserPage from '../page'
describe('Room Navigation with Active Sessions', () => {
const mockRouter = {
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
vi.spyOn(nextNavigation, 'useRouter').mockReturnValue(mockRouter as any)
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
data: 'test-user',
isLoading: false,
isPending: false,
error: null,
} as any)
global.fetch = vi.fn()
})
describe('RoomBrowserPage', () => {
it('should render room browser without redirecting when user has active game session', async () => {
// User has an active game session
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
hasActiveSession: true,
loading: false,
activeSession: {
gameUrl: '/arcade/room',
currentGame: 'matching',
},
})
// User is in a room
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
roomData: {
id: 'room-1',
name: 'Test Room',
code: 'ABC123',
gameName: 'matching',
members: [],
memberPlayers: {},
},
isLoading: false,
isInRoom: true,
notifyRoomOfPlayerUpdate: vi.fn(),
})
// Mock rooms API
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => ({
rooms: [
{
id: 'room-1',
code: 'ABC123',
name: 'Test Room',
gameName: 'matching',
status: 'lobby',
createdAt: new Date(),
creatorName: 'Test User',
isLocked: false,
},
],
}),
})
render(<RoomBrowserPage />)
// Should render the page
await waitFor(() => {
expect(screen.getByText('🎮 Multiplayer Rooms')).toBeInTheDocument()
})
// Should NOT redirect to /arcade/room
expect(mockRouter.push).not.toHaveBeenCalled()
})
it('should NOT redirect when PageWithNav uses arcade guard with enabled=false', async () => {
// Simulate PageWithNav calling useArcadeGuard with enabled=false
const arcadeGuardSpy = vi.spyOn(arcadeGuard, 'useArcadeGuard')
// User has an active game session
arcadeGuardSpy.mockReturnValue({
hasActiveSession: true,
loading: false,
activeSession: {
gameUrl: '/arcade/room',
currentGame: 'matching',
},
})
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
roomData: null,
isLoading: false,
isInRoom: false,
notifyRoomOfPlayerUpdate: vi.fn(),
})
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => ({ rooms: [] }),
})
render(<RoomBrowserPage />)
await waitFor(() => {
expect(screen.getByText('🎮 Multiplayer Rooms')).toBeInTheDocument()
})
// PageWithNav should have called useArcadeGuard with enabled=false
// This is tested in PageWithNav's own tests, but we verify no redirect happened
expect(mockRouter.push).not.toHaveBeenCalled()
})
it('should allow navigation to room detail even with active session', async () => {
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
hasActiveSession: true,
loading: false,
activeSession: {
gameUrl: '/arcade/room',
currentGame: 'matching',
},
})
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
roomData: null,
isLoading: false,
isInRoom: false,
notifyRoomOfPlayerUpdate: vi.fn(),
})
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => ({
rooms: [
{
id: 'room-1',
code: 'ABC123',
name: 'Test Room',
gameName: 'matching',
status: 'lobby',
createdAt: new Date(),
creatorName: 'Test User',
isLocked: false,
isMember: true,
},
],
}),
})
render(<RoomBrowserPage />)
await waitFor(() => {
expect(screen.getByText('Test Room')).toBeInTheDocument()
})
// Click on the room card
const roomCard = screen.getByText('Test Room').parentElement
roomCard?.click()
// Should navigate to room detail, not to /arcade/room
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/arcade-rooms/room-1')
})
})
})
describe('Room navigation edge cases', () => {
it('should handle rapid navigation between room pages without redirect loops', async () => {
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
hasActiveSession: true,
loading: false,
activeSession: {
gameUrl: '/arcade/room',
currentGame: 'matching',
},
})
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
roomData: null,
isLoading: false,
isInRoom: false,
notifyRoomOfPlayerUpdate: vi.fn(),
})
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => ({ rooms: [] }),
})
const { rerender } = render(<RoomBrowserPage />)
await waitFor(() => {
expect(screen.getByText('🎮 Multiplayer Rooms')).toBeInTheDocument()
})
// Simulate pathname changes (navigating between room pages)
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms/room-1')
rerender(<RoomBrowserPage />)
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
rerender(<RoomBrowserPage />)
// Should never redirect to game page
expect(mockRouter.push).not.toHaveBeenCalledWith('/arcade/room')
})
it('should allow user to leave room and browse other rooms during active game', async () => {
// User is in a room with an active game
vi.spyOn(arcadeGuard, 'useArcadeGuard').mockReturnValue({
hasActiveSession: true,
loading: false,
activeSession: {
gameUrl: '/arcade/room',
currentGame: 'matching',
},
})
vi.spyOn(roomData, 'useRoomData').mockReturnValue({
roomData: {
id: 'room-1',
name: 'Current Room',
code: 'ABC123',
gameName: 'matching',
members: [],
memberPlayers: {},
},
isLoading: false,
isInRoom: true,
notifyRoomOfPlayerUpdate: vi.fn(),
})
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => ({
rooms: [
{
id: 'room-1',
name: 'Current Room',
code: 'ABC123',
gameName: 'matching',
status: 'playing',
isMember: true,
},
{
id: 'room-2',
name: 'Other Room',
code: 'DEF456',
gameName: 'memory-quiz',
status: 'lobby',
isMember: false,
},
],
}),
})
render(<RoomBrowserPage />)
await waitFor(() => {
expect(screen.getByText('Current Room')).toBeInTheDocument()
expect(screen.getByText('Other Room')).toBeInTheDocument()
})
// Should be able to view both rooms without redirect
expect(mockRouter.push).not.toHaveBeenCalled()
})
})
})

View File

@@ -1,43 +0,0 @@
'use client'
import type { ReactNode } from 'react'
import { useArcadeGuard } from '@/hooks/useArcadeGuard'
export interface ArcadeGuardedPageProps {
children: ReactNode
/**
* Loading component to show while checking for active session
*/
loadingComponent?: ReactNode
}
/**
* Wrapper component that applies the arcade session guard
*
* This component:
* - Checks for active arcade sessions
* - Redirects to active session if user navigates to a different game
* - Shows loading state while checking
*
* @example
* ```tsx
* export default function MatchingPage() {
* return (
* <ArcadeGuardedPage>
* <MemoryPairsProvider>
* <MemoryPairsGame />
* </MemoryPairsProvider>
* </ArcadeGuardedPage>
* )
* }
* ```
*/
export function ArcadeGuardedPage({ children, loadingComponent }: ArcadeGuardedPageProps) {
const { loading } = useArcadeGuard()
if (loading && loadingComponent) {
return <>{loadingComponent}</>
}
return <>{children}</>
}

View File

@@ -1,427 +0,0 @@
import { renderHook, waitFor } from '@testing-library/react'
import * as nextNavigation from 'next/navigation'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useArcadeGuard } from '../useArcadeGuard'
import * as arcadeSocket from '../useArcadeSocket'
import * as viewerId from '../useViewerId'
// Mock Next.js navigation
vi.mock('next/navigation', () => ({
useRouter: vi.fn(),
usePathname: vi.fn(),
}))
// Mock useArcadeSocket
vi.mock('../useArcadeSocket', () => ({
useArcadeSocket: vi.fn(),
}))
// Mock useViewerId
vi.mock('../useViewerId', () => ({
useViewerId: vi.fn(),
}))
describe('useArcadeGuard', () => {
const mockRouter = {
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}
const mockUseArcadeSocket = {
connected: true,
joinSession: vi.fn(),
socket: null,
sendMove: vi.fn(),
exitSession: vi.fn(),
pingSession: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
vi.spyOn(nextNavigation, 'useRouter').mockReturnValue(mockRouter as any)
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockReturnValue(mockUseArcadeSocket)
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
data: 'test-user',
isLoading: false,
error: null,
} as any)
global.fetch = vi.fn()
})
it('should initialize with loading state', () => {
;(global.fetch as any).mockResolvedValue({
ok: false,
status: 404,
})
const { result } = renderHook(() => useArcadeGuard())
expect(result.current.loading).toBe(true)
expect(result.current.hasActiveSession).toBe(false)
expect(result.current.activeSession).toBe(null)
})
it('should fetch active session on mount', async () => {
const mockSession = {
session: {
gameUrl: '/arcade/matching',
currentGame: 'matching',
gameState: {},
},
}
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => mockSession,
})
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(global.fetch).toHaveBeenCalledWith('/api/arcade-session?userId=test-user')
expect(result.current.hasActiveSession).toBe(true)
expect(result.current.activeSession).toEqual({
gameUrl: '/arcade/matching',
currentGame: 'matching',
})
})
it('should redirect to active session if on different page', async () => {
const mockSession = {
session: {
gameUrl: '/arcade/memory-quiz',
currentGame: 'memory-quiz',
gameState: {},
},
}
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => mockSession,
})
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/arcade/memory-quiz')
})
})
it('should NOT redirect if already on active session page', async () => {
const mockSession = {
session: {
gameUrl: '/arcade/matching',
currentGame: 'matching',
gameState: {},
},
}
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => mockSession,
})
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(global.fetch).toHaveBeenCalled()
})
expect(mockRouter.push).not.toHaveBeenCalled()
})
it('should handle no active session (404)', async () => {
;(global.fetch as any).mockResolvedValue({
ok: false,
status: 404,
})
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.hasActiveSession).toBe(false)
expect(result.current.activeSession).toBe(null)
})
it('should call onRedirect callback when redirecting', async () => {
const onRedirect = vi.fn()
const mockSession = {
session: {
gameUrl: '/arcade/memory-quiz',
currentGame: 'memory-quiz',
gameState: {},
},
}
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => mockSession,
})
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
renderHook(() => useArcadeGuard({ onRedirect }))
await waitFor(() => {
expect(onRedirect).toHaveBeenCalledWith('/arcade/memory-quiz')
})
})
it('should not fetch session when disabled', () => {
renderHook(() => useArcadeGuard({ enabled: false }))
expect(global.fetch).not.toHaveBeenCalled()
})
it('should not fetch session when viewerId is null', () => {
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
data: null,
isLoading: false,
error: null,
} as any)
renderHook(() => useArcadeGuard())
expect(global.fetch).not.toHaveBeenCalled()
})
it('should join WebSocket room when connected', async () => {
;(global.fetch as any).mockResolvedValue({
ok: false,
status: 404,
})
renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(mockUseArcadeSocket.joinSession).toHaveBeenCalledWith('test-user')
})
})
it('should handle session-state event from WebSocket', async () => {
let onSessionStateCallback: ((data: any) => void) | null = null
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
onSessionStateCallback = events?.onSessionState || null
return mockUseArcadeSocket
})
;(global.fetch as any).mockResolvedValue({
ok: false,
status: 404,
})
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
// Simulate session-state event from WebSocket
onSessionStateCallback?.({
gameUrl: '/arcade/complement-race',
currentGame: 'complement-race',
gameState: {},
activePlayers: [1],
version: 1,
})
await waitFor(() => {
expect(result.current.hasActiveSession).toBe(true)
expect(result.current.activeSession).toEqual({
gameUrl: '/arcade/complement-race',
currentGame: 'complement-race',
})
})
})
it('should handle session-ended event from WebSocket', async () => {
let onSessionEndedCallback: (() => void) | null = null
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
onSessionEndedCallback = events?.onSessionEnded || null
return mockUseArcadeSocket
})
const mockSession = {
session: {
gameUrl: '/arcade/matching',
currentGame: 'matching',
gameState: {},
},
}
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => mockSession,
})
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.hasActiveSession).toBe(true)
})
// Simulate session-ended event
onSessionEndedCallback?.()
await waitFor(() => {
expect(result.current.hasActiveSession).toBe(false)
expect(result.current.activeSession).toBe(null)
})
})
it('should handle fetch errors gracefully', async () => {
;(global.fetch as any).mockRejectedValue(new Error('Network error'))
const { result } = renderHook(() => useArcadeGuard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
// Should not crash, just set loading to false
expect(result.current.hasActiveSession).toBe(false)
})
describe('enabled flag behavior', () => {
it('should NOT redirect from HTTP check when enabled=false', async () => {
const mockSession = {
session: {
gameUrl: '/arcade/memory-quiz',
currentGame: 'memory-quiz',
gameState: {},
},
}
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => mockSession,
})
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
renderHook(() => useArcadeGuard({ enabled: false }))
await waitFor(() => {
expect(global.fetch).not.toHaveBeenCalled()
})
// Should NOT redirect
expect(mockRouter.push).not.toHaveBeenCalled()
})
it('should NOT redirect from WebSocket when enabled=false', async () => {
let onSessionStateCallback: ((data: any) => void) | null = null
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
onSessionStateCallback = events?.onSessionState || null
return mockUseArcadeSocket
})
;(global.fetch as any).mockResolvedValue({
ok: false,
status: 404,
})
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
const { result } = renderHook(() => useArcadeGuard({ enabled: false }))
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
// Simulate session-state event from WebSocket
onSessionStateCallback?.({
gameUrl: '/arcade/room',
currentGame: 'matching',
gameState: {},
activePlayers: [1],
version: 1,
})
await waitFor(() => {
// Should track the session
expect(result.current.hasActiveSession).toBe(true)
expect(result.current.activeSession).toEqual({
gameUrl: '/arcade/room',
currentGame: 'matching',
})
})
// But should NOT redirect since enabled=false
expect(mockRouter.push).not.toHaveBeenCalled()
})
it('should STILL redirect from WebSocket when enabled=true', async () => {
let onSessionStateCallback: ((data: any) => void) | null = null
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
onSessionStateCallback = events?.onSessionState || null
return mockUseArcadeSocket
})
;(global.fetch as any).mockResolvedValue({
ok: false,
status: 404,
})
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade-rooms')
renderHook(() => useArcadeGuard({ enabled: true }))
await waitFor(() => {
expect(mockUseArcadeSocket.joinSession).toHaveBeenCalled()
})
// Simulate session-state event from WebSocket
onSessionStateCallback?.({
gameUrl: '/arcade/room',
currentGame: 'matching',
gameState: {},
activePlayers: [1],
version: 1,
})
// Should redirect when enabled=true
await waitFor(() => {
expect(mockRouter.push).toHaveBeenCalledWith('/arcade/room')
})
})
it('should track session state even when enabled=false', async () => {
const mockSession = {
session: {
gameUrl: '/arcade/room',
currentGame: 'matching',
gameState: {},
},
}
;(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => mockSession,
})
const { result } = renderHook(() => useArcadeGuard({ enabled: false }))
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
// Should still provide session info even without redirects
expect(result.current.hasActiveSession).toBe(false) // No fetch happened
expect(result.current.activeSession).toBe(null)
})
})
})

View File

@@ -1,168 +0,0 @@
import { usePathname, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import type { ArcadeSessionResponse } from '@/app/api/arcade-session/types'
import { useArcadeSocket } from './useArcadeSocket'
import { useViewerId } from './useViewerId'
export interface UseArcadeGuardOptions {
/**
* Whether to enable the guard
* @default true
*/
enabled?: boolean
/**
* Callback when redirecting to active session
*/
onRedirect?: (gameUrl: string) => void
}
export interface UseArcadeGuardReturn {
/**
* Whether there's an active arcade session
*/
hasActiveSession: boolean
/**
* Whether currently loading session state
*/
loading: boolean
/**
* Active session details if exists
*/
activeSession: {
gameUrl: string
currentGame: string
} | null
}
/**
* Hook for guarding arcade navigation and auto-resuming sessions
*
* Automatically redirects users to their active arcade session if they
* navigate to a different page while a session is active.
*
* @example
* ```tsx
* // In arcade game pages:
* const { hasActiveSession, loading } = useArcadeGuard()
*
* if (loading) return <LoadingSpinner />
* ```
*/
export function useArcadeGuard(options: UseArcadeGuardOptions = {}): UseArcadeGuardReturn {
const { enabled = true, onRedirect } = options
const router = useRouter()
const pathname = usePathname()
const { data: userId, isLoading: isLoadingUserId } = useViewerId()
const [hasActiveSession, setHasActiveSession] = useState(false)
const [loading, setLoading] = useState(true)
const [activeSession, setActiveSession] = useState<{
gameUrl: string
currentGame: string
} | null>(null)
// WebSocket connection to listen for session changes
const { connected, joinSession } = useArcadeSocket({
onSessionState: (data) => {
setHasActiveSession(true)
setActiveSession({
gameUrl: data.gameUrl,
currentGame: data.currentGame,
})
// Redirect if we're not already on the active game page (only if enabled)
const isAlreadyAtTarget = pathname === data.gameUrl
if (enabled && !isAlreadyAtTarget) {
console.log('[ArcadeGuard] Redirecting to active session:', data.gameUrl)
onRedirect?.(data.gameUrl)
router.push(data.gameUrl)
} else if (isAlreadyAtTarget) {
console.log('[ArcadeGuard] Already at target URL, no redirect needed')
}
},
onNoActiveSession: () => {
setHasActiveSession(false)
setActiveSession(null)
setLoading(false)
},
onSessionEnded: () => {
console.log('[ArcadeGuard] Session ended, clearing active session')
setHasActiveSession(false)
setActiveSession(null)
},
})
// Check for active session on mount and when userId changes
useEffect(() => {
if (!enabled) {
setLoading(false)
return
}
if (isLoadingUserId) {
setLoading(true)
return
}
if (!userId) {
setLoading(false)
return
}
const checkSession = async () => {
try {
setLoading(true)
const response = await fetch(`/api/arcade-session?userId=${userId}`)
if (response.ok) {
const data: ArcadeSessionResponse = await response.json()
const session = data.session // API wraps response in { session: {...} }
setHasActiveSession(true)
setActiveSession({
gameUrl: session.gameUrl,
currentGame: session.currentGame,
})
// Redirect if we're not already on the active game page (only if enabled)
const isAlreadyAtTarget = pathname === session.gameUrl
if (enabled && !isAlreadyAtTarget) {
console.log('[ArcadeGuard] Redirecting to active session:', session.gameUrl)
onRedirect?.(session.gameUrl)
router.push(session.gameUrl)
} else if (isAlreadyAtTarget) {
console.log('[ArcadeGuard] Already at target URL, no redirect needed')
}
} else if (response.status === 404) {
// No active session
setHasActiveSession(false)
setActiveSession(null)
}
} catch (error) {
console.error('[ArcadeGuard] Failed to check session:', error)
} finally {
setLoading(false)
}
}
checkSession()
}, [userId, enabled, pathname, router, onRedirect, isLoadingUserId])
// Join WebSocket room when connected
useEffect(() => {
if (connected && userId) {
joinSession(userId)
}
}, [connected, userId, joinSession])
return {
hasActiveSession,
loading,
activeSession,
}
}

View File

@@ -1,122 +0,0 @@
import { usePathname, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useArcadeSocket } from './useArcadeSocket'
import { useViewerId } from './useViewerId'
export interface UseArcadeRedirectOptions {
/**
* The current game this page represents (e.g., 'matching', 'memory-quiz')
* If null, this is the arcade lobby
*/
currentGame?: string | null
}
export interface UseArcadeRedirectReturn {
/**
* Whether we're checking for an active session
*/
isChecking: boolean
/**
* Whether user has an active session
*/
hasActiveSession: boolean
/**
* The URL of the active game (if any)
*/
activeGameUrl: string | null
/**
* Whether players can be modified (only true in arcade lobby with no active session)
*/
canModifyPlayers: boolean
}
/**
* Hook to handle arcade session redirects
*
* - If on /arcade and user has active session → redirect to that game
* - If on a game page and user has different active session → redirect to that game
* - If on a game page that matches active session → allow (locked in)
*
* @example
* ```tsx
* // In /arcade page
* const { canModifyPlayers } = useArcadeRedirect({ currentGame: null })
*
* // In /arcade/matching page
* const { canModifyPlayers } = useArcadeRedirect({ currentGame: 'matching' })
* ```
*/
export function useArcadeRedirect(options: UseArcadeRedirectOptions = {}): UseArcadeRedirectReturn {
const { currentGame } = options
const router = useRouter()
const _pathname = usePathname()
const { data: viewerId } = useViewerId()
const [isChecking, setIsChecking] = useState(true)
const [hasActiveSession, setHasActiveSession] = useState(false)
const [activeGameUrl, setActiveGameUrl] = useState<string | null>(null)
const [_activeGameName, setActiveGameName] = useState<string | null>(null)
const { connected, joinSession } = useArcadeSocket({
onSessionState: (data) => {
console.log('[ArcadeRedirect] Got session state:', data)
setIsChecking(false)
setHasActiveSession(true)
setActiveGameUrl(data.gameUrl)
setActiveGameName(data.currentGame)
// Determine if we need to redirect
const isArcadeLobby = currentGame === null || currentGame === undefined
const isWrongGame = currentGame && currentGame !== data.currentGame
const isAlreadyAtTarget = _pathname === data.gameUrl
if ((isArcadeLobby || isWrongGame) && !isAlreadyAtTarget) {
console.log('[ArcadeRedirect] Redirecting to active game:', data.gameUrl)
router.push(data.gameUrl)
} else if (isAlreadyAtTarget) {
console.log('[ArcadeRedirect] Already at target URL, no redirect needed')
}
},
onNoActiveSession: () => {
console.log('[ArcadeRedirect] No active session')
setIsChecking(false)
setHasActiveSession(false)
setActiveGameUrl(null)
setActiveGameName(null)
// No redirect needed - user can navigate to any game page to start a new session
// Only redirect when they have an active session for a different game
},
onSessionEnded: () => {
console.log('[ArcadeRedirect] Session ended')
setHasActiveSession(false)
setActiveGameUrl(null)
setActiveGameName(null)
},
})
// Check for active session when connected
useEffect(() => {
if (connected && viewerId) {
console.log('[ArcadeRedirect] Checking for active session')
setIsChecking(true)
joinSession(viewerId)
}
}, [connected, viewerId, joinSession])
// Can modify players whenever there's no active session
// (applies to arcade lobby and game setup pages)
const canModifyPlayers = !hasActiveSession && !isChecking
return {
isChecking,
hasActiveSession,
activeGameUrl,
canModifyPlayers,
}
}