Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4d8032d02 | ||
|
|
01ff114258 | ||
|
|
d173a178bc | ||
|
|
431668729c | ||
|
|
b5d0bee120 | ||
|
|
9dac431c1f | ||
|
|
5bbb212da9 | ||
|
|
c30f585810 | ||
|
|
0a768c65fb | ||
|
|
5ed2ab21ca | ||
|
|
1cb175982a | ||
|
|
ee6094d59d |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,3 +1,43 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* resolve race condition in /arcade/room redirect ([5ed2ab2](https://github.com/antialias/soroban-abacus-flashcards/commit/5ed2ab21cab408147081a493c8dd6b1de48b2d01))
|
||||
|
||||
## [2.7.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.6.0...v2.7.0) (2025-10-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* extend GameModeContext to support room-based multiplayer ([ee6094d](https://github.com/antialias/soroban-abacus-flashcards/commit/ee6094d59d26a9e80ba5d023ca6dc13143bea308))
|
||||
|
||||
## [2.6.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.5.0...v2.6.0) (2025-10-08)
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"Bash(npm run pre-commit:*)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(git pull:*)",
|
||||
"Bash(git stash:*)"
|
||||
"Bash(git stash:*)",
|
||||
"Bash(members of the room\" requirement for room-based gameplay.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\nEOF\n)\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
@@ -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
|
||||
@@ -18,7 +18,6 @@ export default function RoomPage() {
|
||||
// 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])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
useUpdatePlayer,
|
||||
useUserPlayers,
|
||||
} from '@/hooks/useUserPlayers'
|
||||
import { useRoomData } from '@/hooks/useRoomData'
|
||||
import { useViewerId } from '@/hooks/useViewerId'
|
||||
import { getNextPlayerColor } from '../types/player'
|
||||
|
||||
// Client-side Player type (compatible with old type)
|
||||
@@ -66,28 +68,72 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
|
||||
const { mutate: createPlayer } = useCreatePlayer()
|
||||
const { mutate: updatePlayerMutation } = useUpdatePlayer()
|
||||
const { mutate: deletePlayer } = useDeletePlayer()
|
||||
const { roomData } = useRoomData()
|
||||
const { data: viewerId } = useViewerId()
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
|
||||
// Convert DB players to Map
|
||||
const players = useMemo(() => {
|
||||
// Convert DB players to Map (local players)
|
||||
const localPlayers = useMemo(() => {
|
||||
const map = new Map<string, Player>()
|
||||
dbPlayers.forEach((dbPlayer) => {
|
||||
map.set(dbPlayer.id, toClientPlayer(dbPlayer))
|
||||
map.set(dbPlayer.id, {
|
||||
...toClientPlayer(dbPlayer),
|
||||
isLocal: true,
|
||||
})
|
||||
})
|
||||
return map
|
||||
}, [dbPlayers])
|
||||
|
||||
// Track active players from DB isActive status
|
||||
// When in a room, merge all players from all room members
|
||||
const players = useMemo(() => {
|
||||
const map = new Map<string, Player>(localPlayers)
|
||||
|
||||
if (roomData) {
|
||||
// Add players from other room members (marked as remote)
|
||||
Object.entries(roomData.memberPlayers).forEach(([userId, memberPlayers]) => {
|
||||
// Skip the current user's players (already in localPlayers)
|
||||
if (userId === viewerId) return
|
||||
|
||||
memberPlayers.forEach((roomPlayer) => {
|
||||
map.set(roomPlayer.id, {
|
||||
id: roomPlayer.id,
|
||||
name: roomPlayer.name,
|
||||
emoji: roomPlayer.emoji,
|
||||
color: roomPlayer.color,
|
||||
createdAt: Date.now(),
|
||||
isActive: true, // Players in memberPlayers are active
|
||||
isLocal: false, // Remote player
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return map
|
||||
}, [localPlayers, roomData, viewerId])
|
||||
|
||||
// Track active players (local + room members when in a room)
|
||||
const activePlayers = useMemo(() => {
|
||||
const set = new Set<string>()
|
||||
dbPlayers.forEach((player) => {
|
||||
if (player.isActive) {
|
||||
set.add(player.id)
|
||||
}
|
||||
})
|
||||
|
||||
if (roomData) {
|
||||
// In room mode: all players from all members are active
|
||||
Object.values(roomData.memberPlayers).forEach((memberPlayers) => {
|
||||
memberPlayers.forEach((player) => {
|
||||
set.add(player.id)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Solo mode: only local active players
|
||||
dbPlayers.forEach((player) => {
|
||||
if (player.isActive) {
|
||||
set.add(player.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return set
|
||||
}, [dbPlayers])
|
||||
}, [dbPlayers, roomData])
|
||||
|
||||
// Initialize with default players if none exist
|
||||
useEffect(() => {
|
||||
@@ -125,15 +171,33 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
const updatePlayer = (id: string, updates: Partial<Player>) => {
|
||||
updatePlayerMutation({ id, updates })
|
||||
const player = players.get(id)
|
||||
// Only allow updating local players
|
||||
if (player?.isLocal) {
|
||||
updatePlayerMutation({ id, updates })
|
||||
} else {
|
||||
console.warn('[GameModeContext] Cannot update remote player:', id)
|
||||
}
|
||||
}
|
||||
|
||||
const removePlayer = (id: string) => {
|
||||
deletePlayer(id)
|
||||
const player = players.get(id)
|
||||
// Only allow removing local players
|
||||
if (player?.isLocal) {
|
||||
deletePlayer(id)
|
||||
} else {
|
||||
console.warn('[GameModeContext] Cannot remove remote player:', id)
|
||||
}
|
||||
}
|
||||
|
||||
const setActive = (id: string, active: boolean) => {
|
||||
updatePlayerMutation({ id, updates: { isActive: active } })
|
||||
const player = players.get(id)
|
||||
// Only allow changing active status of local players
|
||||
if (player?.isLocal) {
|
||||
updatePlayerMutation({ id, updates: { isActive: active } })
|
||||
} else {
|
||||
console.warn('[GameModeContext] Cannot change active status of remote player:', id)
|
||||
}
|
||||
}
|
||||
|
||||
const getActivePlayers = (): Player[] => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -31,19 +31,22 @@ export interface RoomData {
|
||||
* Returns null if user is not in any room
|
||||
*/
|
||||
export function useRoomData() {
|
||||
const { data: userId } = useViewerId()
|
||||
const { data: userId, isPending: isUserIdPending } = useViewerId()
|
||||
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) {
|
||||
setRoomData(null)
|
||||
setHasAttemptedFetch(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setHasAttemptedFetch(false)
|
||||
|
||||
// Fetch current room data
|
||||
fetch('/api/arcade/rooms/current')
|
||||
@@ -53,23 +56,26 @@ export function useRoomData() {
|
||||
})
|
||||
.then((data) => {
|
||||
if (data.room) {
|
||||
setRoomData({
|
||||
const roomData = {
|
||||
id: data.room.id,
|
||||
name: data.room.name,
|
||||
code: data.room.code,
|
||||
gameName: data.room.gameName,
|
||||
members: data.members || [],
|
||||
memberPlayers: data.memberPlayers || {},
|
||||
})
|
||||
}
|
||||
setRoomData(roomData)
|
||||
} else {
|
||||
setRoomData(null)
|
||||
}
|
||||
setIsLoading(false)
|
||||
setHasAttemptedFetch(true)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch room data:', error)
|
||||
console.error('[useRoomData] Failed to fetch room data:', error)
|
||||
setRoomData(null)
|
||||
setIsLoading(false)
|
||||
setHasAttemptedFetch(true)
|
||||
})
|
||||
}, [userId])
|
||||
|
||||
@@ -86,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)
|
||||
@@ -115,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
|
||||
@@ -134,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
|
||||
@@ -153,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
|
||||
@@ -170,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
|
||||
@@ -197,7 +198,8 @@ export function useRoomData() {
|
||||
|
||||
return {
|
||||
roomData,
|
||||
isLoading,
|
||||
// Loading if: userId is pending, currently fetching, or have userId but haven't tried fetching yet
|
||||
isLoading: isUserIdPending || isLoading || (!!userId && !hasAttemptedFetch),
|
||||
isInRoom: !!roomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.4",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user