Compare commits

..

8 Commits

Author SHA1 Message Date
semantic-release-bot
0abec1a3bb chore(release): 2.8.7 [skip ci]
## [2.8.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.6...v2.8.7) (2025-10-09)

### Bug Fixes

* enable real-time player name updates across room members ([5171be3](5171be3d37))
2025-10-09 12:39:34 +00:00
Thomas Hallock
5171be3d37 fix: enable real-time player name updates across room members
When a player's name (or other properties) is updated, emit a
'players-updated' socket event to notify all room members. This triggers
the server to broadcast the updated player data via 'room-players-updated',
which all room members receive and use to update their local state.

Changes:
- Add notifyRoomOfPlayerUpdate() function to useRoomData hook
- Call it after successful player mutations (create, update, delete, setActive)
- This ensures all room members see player changes immediately without page reload

Fixes real-time synchronization of player names in the mini app nav.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 07:38:43 -05:00
semantic-release-bot
fc9eb253ad chore(release): 2.8.6 [skip ci]
## [2.8.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.5...v2.8.6) (2025-10-09)

### Bug Fixes

* prevent duplicate display of network avatars in nav ([d474ef0](d474ef07d6))
2025-10-09 12:27:57 +00:00
Thomas Hallock
d474ef07d6 fix: prevent duplicate display of network avatars in nav
Filter out remote players (isLocal: false) from active/inactive player
lists in PageWithNav to prevent them from appearing twice:
1. Once in the main player list (incorrect)
2. Once in the network players section (correct)

Now:
- Active/inactive player lists show only local players
- Network players section shows remote players separately

This provides a clear visual distinction between "your avatars" and
"network avatars" in the mini app game nav.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 07:27:03 -05:00
semantic-release-bot
3cdc0695f4 chore(release): 2.8.5 [skip ci]
## [2.8.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.4...v2.8.5) (2025-10-09)

### Bug Fixes

* remove redirect loop by not redirecting from room page ([10cf715](10cf71527f))
2025-10-09 01:17:27 +00:00
Thomas Hallock
10cf71527f fix: remove redirect loop by not redirecting from room page
The infinite redirect loop was caused by:
1. /arcade/room redirecting to /arcade when roomData is null
2. /arcade (via useArcadeRedirect) redirecting back to /arcade/room for active session
3. Loop repeats

Fix: Remove the redirect from room page entirely. Instead:
- Show loading state while fetching roomData
- Show error message with link if no room found (no automatic redirect)
- Let useArcadeRedirect on /arcade handle active session redirects

This prevents the redirect conflict and allows proper navigation.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 20:16:27 -05:00
semantic-release-bot
678f4423b6 chore(release): 2.8.4 [skip ci]
## [2.8.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.3...v2.8.4) (2025-10-09)

### Bug Fixes

* prevent redirect loops by checking if already at target URL ([c5268b7](c5268b79de))
2025-10-09 01:11:33 +00:00
Thomas Hallock
c5268b79de fix: prevent redirect loops by checking if already at target URL
Both useArcadeRedirect and useArcadeGuard were causing infinite loops:
1. User navigates to /arcade/room
2. Room page redirects to /arcade (if roomData null during loading)
3. /arcade sees active session at /arcade/room, redirects back
4. Loop repeats

Fix: Check if pathname already matches target URL before redirecting.
If already at target, skip the redirect and log that we're already there.

This fixes the infinite redirect loop when navigating to /arcade/room.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 20:10:40 -05:00
8 changed files with 119 additions and 23 deletions

View File

@@ -1,3 +1,31 @@
## [2.8.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.6...v2.8.7) (2025-10-09)
### Bug Fixes
* enable real-time player name updates across room members ([5171be3](https://github.com/antialias/soroban-abacus-flashcards/commit/5171be3d37980eb1c98aa0d1e1d6e06f589763d1))
## [2.8.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.5...v2.8.6) (2025-10-09)
### Bug Fixes
* prevent duplicate display of network avatars in nav ([d474ef0](https://github.com/antialias/soroban-abacus-flashcards/commit/d474ef07d69cf0b4f5dedd404616e3bbee7289fe))
## [2.8.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.4...v2.8.5) (2025-10-09)
### Bug Fixes
* remove redirect loop by not redirecting from room page ([10cf715](https://github.com/antialias/soroban-abacus-flashcards/commit/10cf71527f7cede7fd93e502dbfc59df99b5a524))
## [2.8.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.3...v2.8.4) (2025-10-09)
### Bug Fixes
* prevent redirect loops by checking if already at target URL ([c5268b7](https://github.com/antialias/soroban-abacus-flashcards/commit/c5268b79dee66aa02e14e2024fe1c6242a172ed3))
## [2.8.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.8.2...v2.8.3) (2025-10-08)

View File

@@ -1,7 +1,5 @@
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useRoomData } from '@/hooks/useRoomData'
import { MemoryPairsGame } from '../matching/components/MemoryPairsGame'
import { ArcadeMemoryPairsProvider } from '../matching/context/ArcadeMemoryPairsContext'
@@ -9,18 +7,14 @@ import { ArcadeMemoryPairsProvider } from '../matching/context/ArcadeMemoryPairs
/**
* /arcade/room - Renders the game for the user's current room
* Since users can only be in one room at a time, this is a simple singular route
*
* Note: We don't redirect to /arcade if no room exists because:
* - It would conflict with arcade session redirects and create loops
* - useArcadeRedirect on /arcade page handles redirecting to active sessions
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
// Redirect to arcade if no room
useEffect(() => {
if (!isLoading && !roomData) {
router.push('/arcade')
}
}, [isLoading, roomData, router])
// Show loading state
if (isLoading) {
return (
@@ -39,9 +33,33 @@ export default function RoomPage() {
)
}
// Show nothing while redirecting
// Show error if no room (instead of redirecting)
if (!roomData) {
return null
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
gap: '1rem',
}}
>
<div>No active room found</div>
<a
href="/arcade"
style={{
color: '#3b82f6',
textDecoration: 'underline',
}}
>
Go to Champion Arena
</a>
</div>
)
}
// Render the appropriate game based on room's gameName

View File

@@ -58,13 +58,14 @@ export function PageWithNav({
}
// Get active and inactive players as arrays
// Only show LOCAL players in the active/inactive lists (remote players shown separately in networkPlayers)
const activePlayerList = Array.from(activePlayers)
.map((id) => players.get(id))
.filter((p) => p !== undefined)
.filter((p) => p !== undefined && p.isLocal !== false) // Filter out remote players
.map((p) => ({ id: p.id, name: p.name, emoji: p.emoji }))
const inactivePlayerList = Array.from(players.values())
.filter((p) => !activePlayers.has(p.id))
.filter((p) => !activePlayers.has(p.id) && p.isLocal !== false) // Filter out remote players
.map((p) => ({ id: p.id, name: p.name, emoji: p.emoji }))
// Compute game mode from active player count

View File

@@ -68,7 +68,7 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
const { mutate: createPlayer } = useCreatePlayer()
const { mutate: updatePlayerMutation } = useUpdatePlayer()
const { mutate: deletePlayer } = useDeletePlayer()
const { roomData } = useRoomData()
const { roomData, notifyRoomOfPlayerUpdate } = useRoomData()
const { data: viewerId } = useViewerId()
const [isInitialized, setIsInitialized] = useState(false)
@@ -167,14 +167,27 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
isActive: playerData?.isActive ?? false,
}
createPlayer(newPlayer)
createPlayer(newPlayer, {
onSuccess: () => {
// Notify room members if in a room
notifyRoomOfPlayerUpdate()
},
})
}
const updatePlayer = (id: string, updates: Partial<Player>) => {
const player = players.get(id)
// Only allow updating local players
if (player?.isLocal) {
updatePlayerMutation({ id, updates })
updatePlayerMutation(
{ id, updates },
{
onSuccess: () => {
// Notify room members if in a room
notifyRoomOfPlayerUpdate()
},
}
)
} else {
console.warn('[GameModeContext] Cannot update remote player:', id)
}
@@ -184,7 +197,12 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
const player = players.get(id)
// Only allow removing local players
if (player?.isLocal) {
deletePlayer(id)
deletePlayer(id, {
onSuccess: () => {
// Notify room members if in a room
notifyRoomOfPlayerUpdate()
},
})
} else {
console.warn('[GameModeContext] Cannot remove remote player:', id)
}
@@ -194,7 +212,15 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
const player = players.get(id)
// Only allow changing active status of local players
if (player?.isLocal) {
updatePlayerMutation({ id, updates: { isActive: active } })
updatePlayerMutation(
{ id, updates: { isActive: active } },
{
onSuccess: () => {
// Notify room members if in a room
notifyRoomOfPlayerUpdate()
},
}
)
} else {
console.warn('[GameModeContext] Cannot change active status of remote player:', id)
}
@@ -227,6 +253,11 @@ export function GameModeProvider({ children }: { children: ReactNode }) {
isActive: index === 0,
})
})
// Notify room members after reset (slight delay to ensure mutations complete)
setTimeout(() => {
notifyRoomOfPlayerUpdate()
}, 100)
}
const activePlayerCount = activePlayers.size

View File

@@ -74,10 +74,13 @@ export function useArcadeGuard(options: UseArcadeGuardOptions = {}): UseArcadeGu
})
// Redirect if we're not already on the active game page (only if enabled)
if (enabled && pathname !== data.gameUrl) {
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')
}
},
@@ -127,10 +130,13 @@ export function useArcadeGuard(options: UseArcadeGuardOptions = {}): UseArcadeGu
})
// Redirect if we're not already on the active game page (only if enabled)
if (enabled && pathname !== session.gameUrl) {
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

View File

@@ -71,10 +71,13 @@ export function useArcadeRedirect(options: UseArcadeRedirectOptions = {}): UseAr
// 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) {
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')
}
},

View File

@@ -196,10 +196,19 @@ export function useRoomData() {
}
}, [socket, roomData?.id])
// Function to notify room members of player updates
const notifyRoomOfPlayerUpdate = () => {
if (socket && roomData?.id && userId) {
console.log('[useRoomData] Notifying room of player update')
socket.emit('players-updated', { roomId: roomData.id, userId })
}
}
return {
roomData,
// Loading if: userId is pending, currently fetching, or have userId but haven't tried fetching yet
isLoading: isUserIdPending || isLoading || (!!userId && !hasAttemptedFetch),
isInRoom: !!roomData,
notifyRoomOfPlayerUpdate,
}
}

View File

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