Compare commits

...

8 Commits

Author SHA1 Message Date
semantic-release-bot
c4d8032d02 chore(release): 2.7.4 [skip ci]
## [2.7.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.7.3...v2.7.4) (2025-10-08)

### Bug Fixes

* respect enabled flag in useArcadeGuard WebSocket redirects ([01ff114](01ff114258))

### Code Refactoring

* move room management pages to /arcade-rooms ([4316687](431668729c))
2025-10-08 16:06:05 +00:00
Thomas Hallock
01ff114258 fix: respect enabled flag in useArcadeGuard WebSocket redirects
The useArcadeGuard hook was ignoring the enabled flag for WebSocket
redirects, causing unwanted navigation from /arcade-rooms to /arcade/room.

When enabled=false (used by PageWithNav), the hook should only track
session state without triggering redirects.

Changes:
- Check enabled flag before redirecting in WebSocket onSessionState
- Check enabled flag before redirecting in HTTP checkSession
- Allows room management pages to use PageWithNav without redirects

Fixes the issue where visiting /arcade-rooms with an active game
session would immediately redirect to /arcade/room.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 11:05:07 -05:00
Thomas Hallock
d173a178bc chore: remove debug logging from room data fetching
Clean up console.log statements that were added for debugging the
room navigation race condition. Keeping only error logging.

Files cleaned:
- src/hooks/useRoomData.ts - removed trace logs for fetch/socket events
- src/app/arcade/room/page.tsx - removed state logging
- src/app/api/arcade/rooms/current/route.ts - removed request logging

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 11:01:47 -05:00
Thomas Hallock
431668729c refactor: move room management pages to /arcade-rooms
Move room browser and detail pages from /arcade/rooms to /arcade-rooms
to eliminate conflicts with arcade session redirect logic. This allows
users to navigate to room management pages even when they have an
active game session.

Changes:
- Move /arcade/rooms/page.tsx → /arcade-rooms/page.tsx (room browser)
- Move /arcade/rooms/[roomId]/page.tsx → /arcade-rooms/[roomId]/page.tsx (room detail)
- Update all router.push() calls to use /arcade-rooms
- Fix styled-system import paths for new location
- Delete old /arcade/rooms directory

Benefits:
- Room management pages are now outside /arcade namespace
- No exceptions needed in useArcadeRedirect hook
- Users can access room pages during active gameplay
- Cleaner separation between gameplay and room management

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 11:00:08 -05:00
semantic-release-bot
b5d0bee120 chore(release): 2.7.3 [skip ci]
## [2.7.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.7.2...v2.7.3) (2025-10-08)

### Bug Fixes

* set room sessions to use /arcade/room URL ([9dac431](9dac431c1f))
2025-10-08 15:44:46 +00:00
Thomas Hallock
9dac431c1f fix: set room sessions to use /arcade/room URL
When creating an arcade session for a room-based game, the gameUrl was
hardcoded to '/arcade/matching'. This caused useArcadeRedirect to
redirect users from /arcade/room to /arcade/matching, breaking the
simplified room addressing model.

Fix:
- Changed gameUrl to '/arcade/room' for room-based sessions
- Now users stay on /arcade/room for the duration of room gameplay
- Solo sessions still use game-specific URLs like /arcade/matching

This ensures the user experience matches the intended design:
- /arcade/room - room-based multiplayer (regardless of game type)
- /arcade/[game] - solo/local multiplayer for specific games

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 10:43:48 -05:00
semantic-release-bot
5bbb212da9 chore(release): 2.7.2 [skip ci]
## [2.7.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.7.1...v2.7.2) (2025-10-08)

### Bug Fixes

* add hasAttemptedFetch flag to prevent premature redirect ([c30f585](c30f585810))
2025-10-08 15:40:52 +00:00
Thomas Hallock
c30f585810 fix: add hasAttemptedFetch flag to prevent premature redirect
The previous fix didn't fully resolve the race condition. When userId
finished loading, there was a brief moment where:
- isUserIdPending = false (userId loaded)
- isLoading = false (fetch hasn't started yet)
- roomData = null

This triggered the redirect before the room fetch even began.

Solution:
- Added hasAttemptedFetch flag to track fetch attempt state
- Updated isLoading to include: isUserIdPending || isLoading || (!!userId && !hasAttemptedFetch)
- Now the page stays in loading state until we've both loaded userId AND attempted the room fetch

This ensures we never redirect while a fetch is pending.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 10:39:54 -05:00
12 changed files with 50 additions and 109 deletions

View File

@@ -1,3 +1,29 @@
## [2.7.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.7.3...v2.7.4) (2025-10-08)
### Bug Fixes
* respect enabled flag in useArcadeGuard WebSocket redirects ([01ff114](https://github.com/antialias/soroban-abacus-flashcards/commit/01ff114258ff7ab43ef2bd79b41c7035fe02ac70))
### Code Refactoring
* move room management pages to /arcade-rooms ([4316687](https://github.com/antialias/soroban-abacus-flashcards/commit/431668729cfb145d6e0c13947de2a82f27fa400d))
## [2.7.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.7.2...v2.7.3) (2025-10-08)
### Bug Fixes
* set room sessions to use /arcade/room URL ([9dac431](https://github.com/antialias/soroban-abacus-flashcards/commit/9dac431c1f91c246f67a059cda3cff6cbef40a43))
## [2.7.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.7.1...v2.7.2) (2025-10-08)
### Bug Fixes
* add hasAttemptedFetch flag to prevent premature redirect ([c30f585](https://github.com/antialias/soroban-abacus-flashcards/commit/c30f58581028878350282cad5231d614590d9f2b))
## [2.7.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.7.0...v2.7.1) (2025-10-08)

View File

@@ -142,7 +142,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
await createArcadeSession({
userId: data.userId,
gameName: 'matching',
gameUrl: '/arcade/matching',
gameUrl: '/arcade/room', // Room-based sessions use /arcade/room
initialState,
activePlayers,
roomId: room.id,

View File

@@ -12,14 +12,11 @@ import { getViewerId } from '@/lib/viewer'
export async function GET() {
try {
const userId = await getViewerId()
console.log('[Current Room API] Fetching for user:', userId)
// Get all rooms user is in (should be at most 1 due to modal room enforcement)
const roomIds = await getUserRooms(userId)
console.log('[Current Room API] User rooms:', roomIds)
if (roomIds.length === 0) {
console.log('[Current Room API] User is not in any room')
return NextResponse.json({ room: null }, { status: 200 })
}
@@ -28,7 +25,6 @@ export async function GET() {
// Get room data
const room = await getRoomById(roomId)
if (!room) {
console.log('[Current Room API] Room not found:', roomId)
return NextResponse.json({ error: 'Room not found' }, { status: 404 })
}
@@ -44,12 +40,6 @@ export async function GET() {
memberPlayersObj[uid] = players
}
console.log('[Current Room API] Returning room:', {
roomId: room.id,
roomName: room.name,
memberCount: members.length,
})
return NextResponse.json({
room,
members,

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { io, type Socket } from 'socket.io-client'
import { css } from '../../../../../styled-system/css'
import { css } from '../../../../styled-system/css'
import { PageWithNav } from '@/components/PageWithNav'
import { useViewerId } from '@/hooks/useViewerId'
@@ -154,8 +154,8 @@ export default function RoomDetailPage() {
const startGame = () => {
if (!room) return
// Navigate to the game with the room ID
router.push(`/arcade/rooms/${roomId}/${room.gameName}`)
// Navigate to the room game page
router.push('/arcade/room')
}
const joinRoom = async () => {
@@ -264,7 +264,7 @@ export default function RoomDetailPage() {
{error || 'Room not found'}
</p>
<button
onClick={() => router.push('/arcade/rooms')}
onClick={() => router.push('/arcade-rooms')}
className={css({
px: '6',
py: '3',
@@ -325,7 +325,7 @@ export default function RoomDetailPage() {
>
<div className={css({ mb: '4' })}>
<button
onClick={() => router.push('/arcade/rooms')}
onClick={() => router.push('/arcade-rooms')}
className={css({
display: 'inline-flex',
alignItems: 'center',
@@ -621,7 +621,7 @@ export default function RoomDetailPage() {
) : (
<>
<button
onClick={() => router.push('/arcade/rooms')}
onClick={() => router.push('/arcade-rooms')}
className={css({
flex: 1,
px: '6',

View File

@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { css } from '../../../../styled-system/css'
import { css } from '../../../styled-system/css'
import { PageWithNav } from '@/components/PageWithNav'
interface Room {
@@ -66,7 +66,7 @@ export default function RoomBrowserPage() {
}
const data = await response.json()
router.push(`/arcade/rooms/${data.room.id}`)
router.push(`/arcade-rooms/${data.room.id}`)
} catch (err) {
console.error('Failed to create room:', err)
alert('Failed to create room')
@@ -103,7 +103,7 @@ export default function RoomBrowserPage() {
// Could show a toast notification here in the future
}
router.push(`/arcade/rooms/${roomId}`)
router.push(`/arcade-rooms/${roomId}`)
} catch (err) {
console.error('Failed to join room:', err)
alert('Failed to join room')
@@ -219,7 +219,7 @@ export default function RoomBrowserPage() {
})}
>
<div
onClick={() => router.push(`/arcade/rooms/${room.id}`)}
onClick={() => router.push(`/arcade-rooms/${room.id}`)}
className={css({ flex: 1, cursor: 'pointer' })}
>
<div

View File

@@ -15,15 +15,9 @@ export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
// Debug logging
useEffect(() => {
console.log('[RoomPage] State:', { isLoading, hasRoomData: !!roomData, roomData })
}, [isLoading, roomData])
// Redirect to arcade if no room
useEffect(() => {
if (!isLoading && !roomData) {
console.log('[RoomPage] No active room, redirecting to /arcade')
router.push('/arcade')
}
}, [isLoading, roomData, router])

View File

@@ -1,24 +0,0 @@
'use client'
import { useParams } from 'next/navigation'
import { PageWithNav } from '@/components/PageWithNav'
import { ComplementRaceGame } from '@/app/arcade/complement-race/components/ComplementRaceGame'
import { ComplementRaceProvider } from '@/app/arcade/complement-race/context/ComplementRaceContext'
export default function RoomComplementRacePage() {
const params = useParams()
const roomId = params.roomId as string
// TODO Phase 4: Integrate room context with game state
// - Connect to room socket events
// - Sync game state across players
// - Handle multiplayer race dynamics
return (
<PageWithNav navTitle="Speed Complement Race" navEmoji="🏁">
<ComplementRaceProvider>
<ComplementRaceGame />
</ComplementRaceProvider>
</PageWithNav>
)
}

View File

@@ -1,24 +0,0 @@
'use client'
import { useParams } from 'next/navigation'
import { ArcadeGuardedPage } from '@/components/ArcadeGuardedPage'
import { MemoryPairsGame } from '@/app/arcade/matching/components/MemoryPairsGame'
import { ArcadeMemoryPairsProvider } from '@/app/arcade/matching/context/ArcadeMemoryPairsContext'
export default function RoomMatchingPage() {
const params = useParams()
const roomId = params.roomId as string
// TODO Phase 4: Integrate room context with game state
// - Connect to room socket events
// - Sync game state across players
// - Handle multiplayer moves
return (
<ArcadeGuardedPage>
<ArcadeMemoryPairsProvider>
<MemoryPairsGame />
</ArcadeMemoryPairsProvider>
</ArcadeGuardedPage>
)
}

View File

@@ -1,16 +0,0 @@
'use client'
import { useParams } from 'next/navigation'
// Temporarily redirect to solo arcade version
// TODO Phase 4: Implement room-aware memory quiz with multiplayer sync
export default function RoomMemoryQuizPage() {
const params = useParams()
const roomId = params.roomId as string
// Import and use the arcade version for now
// This prevents 404s while we work on full multiplayer integration
const MemoryQuizGame = require('@/app/arcade/memory-quiz/page').default
return <MemoryQuizGame />
}

View File

@@ -73,8 +73,8 @@ export function useArcadeGuard(options: UseArcadeGuardOptions = {}): UseArcadeGu
currentGame: data.currentGame,
})
// Redirect if we're not already on the active game page
if (pathname !== data.gameUrl) {
// Redirect if we're not already on the active game page (only if enabled)
if (enabled && pathname !== data.gameUrl) {
console.log('[ArcadeGuard] Redirecting to active session:', data.gameUrl)
onRedirect?.(data.gameUrl)
router.push(data.gameUrl)
@@ -126,8 +126,8 @@ export function useArcadeGuard(options: UseArcadeGuardOptions = {}): UseArcadeGu
currentGame: session.currentGame,
})
// Redirect if we're not already on the active game page
if (pathname !== session.gameUrl) {
// Redirect if we're not already on the active game page (only if enabled)
if (enabled && pathname !== session.gameUrl) {
console.log('[ArcadeGuard] Redirecting to active session:', session.gameUrl)
onRedirect?.(session.gameUrl)
router.push(session.gameUrl)

View File

@@ -35,27 +35,26 @@ export function useRoomData() {
const [socket, setSocket] = useState<Socket | null>(null)
const [roomData, setRoomData] = useState<RoomData | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [hasAttemptedFetch, setHasAttemptedFetch] = useState(false)
// Fetch the user's current room
useEffect(() => {
if (!userId) {
console.log('[useRoomData] No userId, clearing room data')
setRoomData(null)
setHasAttemptedFetch(false)
return
}
console.log('[useRoomData] Fetching current room for user:', userId)
setIsLoading(true)
setHasAttemptedFetch(false)
// Fetch current room data
fetch('/api/arcade/rooms/current')
.then((res) => {
console.log('[useRoomData] API response status:', res.status)
if (!res.ok) throw new Error('Failed to fetch current room')
return res.json()
})
.then((data) => {
console.log('[useRoomData] API response data:', data)
if (data.room) {
const roomData = {
id: data.room.id,
@@ -65,18 +64,18 @@ export function useRoomData() {
members: data.members || [],
memberPlayers: data.memberPlayers || {},
}
console.log('[useRoomData] Setting room data:', roomData)
setRoomData(roomData)
} else {
console.log('[useRoomData] No room in response, clearing room data')
setRoomData(null)
}
setIsLoading(false)
setHasAttemptedFetch(true)
})
.catch((error) => {
console.error('[useRoomData] Failed to fetch room data:', error)
setRoomData(null)
setIsLoading(false)
setHasAttemptedFetch(true)
})
}, [userId])
@@ -93,13 +92,12 @@ export function useRoomData() {
const sock = io({ path: '/api/socket' })
sock.on('connect', () => {
console.log('[useRoomData] Socket connected, joining room:', roomData.id)
// Join the room to receive updates
sock.emit('join-room', { roomId: roomData.id, userId })
})
sock.on('disconnect', () => {
console.log('[useRoomData] Socket disconnected')
// Socket disconnected
})
setSocket(sock)
@@ -122,7 +120,6 @@ export function useRoomData() {
members: RoomMember[]
memberPlayers: Record<string, RoomPlayer[]>
}) => {
console.log('[useRoomData] Received room-joined event:', data)
if (data.roomId === roomData.id) {
setRoomData((prev) => {
if (!prev) return null
@@ -141,7 +138,6 @@ export function useRoomData() {
members: RoomMember[]
memberPlayers: Record<string, RoomPlayer[]>
}) => {
console.log('[useRoomData] Received member-joined event:', data)
if (data.roomId === roomData.id) {
setRoomData((prev) => {
if (!prev) return null
@@ -160,7 +156,6 @@ export function useRoomData() {
members: RoomMember[]
memberPlayers: Record<string, RoomPlayer[]>
}) => {
console.log('[useRoomData] Received member-left event:', data)
if (data.roomId === roomData.id) {
setRoomData((prev) => {
if (!prev) return null
@@ -177,7 +172,6 @@ export function useRoomData() {
roomId: string
memberPlayers: Record<string, RoomPlayer[]>
}) => {
console.log('[useRoomData] Received room-players-updated event:', data)
if (data.roomId === roomData.id) {
setRoomData((prev) => {
if (!prev) return null
@@ -204,7 +198,8 @@ export function useRoomData() {
return {
roomData,
isLoading: isLoading || isUserIdPending, // Wait for both userId and room data
// Loading if: userId is pending, currently fetching, or have userId but haven't tried fetching yet
isLoading: isUserIdPending || isLoading || (!!userId && !hasAttemptedFetch),
isInRoom: !!roomData,
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "2.7.1",
"version": "2.7.4",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [