Compare commits

...

6 Commits

Author SHA1 Message Date
semantic-release-bot
9a9958a659 chore(release): 2.16.4 [skip ci]
## [2.16.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.16.3...v2.16.4) (2025-10-09)

### Bug Fixes

* correct Docker CMD to use root-level server.js ([48b47e9](48b47e9bdb))
2025-10-09 23:45:37 +00:00
Thomas Hallock
48b47e9bdb fix: correct Docker CMD to use root-level server.js
The Next.js standalone build outputs server.js to /app/server.js, not
/app/apps/web/server.js. This was causing the container to crash on
startup with MODULE_NOT_FOUND errors, resulting in 404s for abaci.one.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 18:44:34 -05:00
semantic-release-bot
41aa205d04 chore(release): 2.16.3 [skip ci]
## [2.16.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.16.2...v2.16.3) (2025-10-09)

### Bug Fixes

* use game state playerMetadata instead of GameModeContext in UI components ([388c254](388c25451d))
2025-10-09 23:08:44 +00:00
Thomas Hallock
388c25451d fix: use game state playerMetadata instead of GameModeContext in UI components
Replace useGameMode() calls with state.playerMetadata in PlayerStatusBar and
MemoryGrid to ensure only players in the current game are displayed.

Before: UI components used GameModeContext which includes all room members'
players, causing remote players to appear in local-only games.

After: UI components use state.playerMetadata and state.activePlayers from
MemoryPairsContext, which only contains players actually in the current game.

Changes:
- PlayerStatusBar: Get players from state.playerMetadata, not GameModeContext
- MemoryGrid: Check player.userId === viewerId instead of isLocal flag
- Remove useGameMode imports from display components

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 18:07:49 -05:00
semantic-release-bot
fa827ac792 chore(release): 2.16.2 [skip ci]
## [2.16.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.16.1...v2.16.2) (2025-10-09)

### Bug Fixes

* use only local user's players in LocalMemoryPairsProvider ([c26138f](c26138ffb5))
2025-10-09 23:05:31 +00:00
Thomas Hallock
c26138ffb5 fix: use only local user's players in LocalMemoryPairsProvider
Replace useGameMode() with direct useUserPlayers() call to prevent
room members' players from appearing in local-only games at /arcade/matching.

Before: LocalMemoryPairsProvider used GameModeContext which includes ALL
players from room members when user is in a room, showing remote players
in turn indicator for local games.

After: Direct useUserPlayers() call ensures only the current user's players
appear in local games, regardless of room membership.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 18:04:43 -05:00
6 changed files with 66 additions and 30 deletions

View File

@@ -1,3 +1,24 @@
## [2.16.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.16.3...v2.16.4) (2025-10-09)
### Bug Fixes
* correct Docker CMD to use root-level server.js ([48b47e9](https://github.com/antialias/soroban-abacus-flashcards/commit/48b47e9bdb0da44746282cd7cf7599a69bf5130d))
## [2.16.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.16.2...v2.16.3) (2025-10-09)
### Bug Fixes
* use game state playerMetadata instead of GameModeContext in UI components ([388c254](https://github.com/antialias/soroban-abacus-flashcards/commit/388c25451d11b85236c1f7682fe2f7a62a15d5eb))
## [2.16.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.16.1...v2.16.2) (2025-10-09)
### Bug Fixes
* use only local user's players in LocalMemoryPairsProvider ([c26138f](https://github.com/antialias/soroban-abacus-flashcards/commit/c26138ffb55a237a99cb6ff399c8a2ac54a22b51))
## [2.16.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.16.0...v2.16.1) (2025-10-09)

View File

@@ -50,4 +50,4 @@ ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
# Start the application
CMD ["node", "apps/web/server.js"]
CMD ["node", "server.js"]

View File

@@ -3,7 +3,7 @@
import { useSpring, animated } from '@react-spring/web'
import { useEffect, useMemo, useRef, useState } from 'react'
import { css } from '../../../../../styled-system/css'
import { useGameMode } from '../../../../contexts/GameModeContext'
import { useViewerId } from '@/hooks/useViewerId'
import { useMemoryPairs } from '../context/MemoryPairsContext'
import { getGridConfiguration } from '../utils/cardGeneration'
import { GameCard } from './GameCard'
@@ -173,7 +173,7 @@ function HoverAvatar({
export function MemoryGrid() {
const { state, flipCard, hoverCard, gameMode } = useMemoryPairs()
const { players: playerMap } = useGameMode()
const { data: viewerId } = useViewerId()
// Track card element refs for positioning hover avatars
const cardRefs = useRef<Map<string, HTMLElement>>(new Map())
@@ -182,9 +182,11 @@ export function MemoryGrid() {
const isMyTurn = useMemo(() => {
if (gameMode === 'single') return true // Always your turn in single player
const currentPlayerData = playerMap.get(state.currentPlayer)
return currentPlayerData?.isLocal === true
}, [state.currentPlayer, playerMap, gameMode])
// In local games, all players belong to current user, so always their turn
// In room games, check if current player belongs to this user
const currentPlayerMetadata = state.playerMetadata?.[state.currentPlayer]
return currentPlayerMetadata?.userId === viewerId
}, [state.currentPlayer, state.playerMetadata, viewerId, gameMode])
// Hooks must be called before early return
const gridConfig = useMemo(() => getGridConfiguration(state.difficulty), [state.difficulty])
@@ -200,16 +202,8 @@ export function MemoryGrid() {
// Get player metadata for hover avatars
const getPlayerHoverInfo = (playerId: string) => {
// Check playerMetadata first (from room members)
if (state.playerMetadata && state.playerMetadata[playerId]) {
return {
emoji: state.playerMetadata[playerId].emoji,
name: state.playerMetadata[playerId].name,
color: state.playerMetadata[playerId].color,
}
}
// Fall back to local player map
const player = playerMap.get(playerId)
// Get player info from game state metadata
const player = state.playerMetadata?.[playerId]
return player
? {
emoji: player.emoji,
@@ -387,8 +381,10 @@ export function MemoryGrid() {
Object.entries(state.playerHovers)
.filter(([playerId]) => {
// Don't show your own hover avatar (only show remote players)
const player = playerMap.get(playerId)
return player?.isLocal !== true
// In local games, all players belong to this user
// In room games, check if player belongs to different user
const player = state.playerMetadata?.[playerId]
return player?.userId !== viewerId
})
.map(([playerId, cardId]) => {
const playerInfo = getPlayerHoverInfo(playerId)

View File

@@ -1,7 +1,6 @@
'use client'
import { css } from '../../../../../styled-system/css'
import { useGameMode } from '../../../../contexts/GameModeContext'
import { gamePlurals } from '../../../../utils/pluralization'
import { useMemoryPairs } from '../context/MemoryPairsContext'
@@ -10,12 +9,12 @@ interface PlayerStatusBarProps {
}
export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
const { state } = useMemoryPairs()
// Get active players array
const activePlayersData = Array.from(activePlayerIds)
.map((id) => playerMap.get(id))
// Get active players from game state (not GameModeContext)
// This ensures we only show players actually in this game
const activePlayersData = state.activePlayers
.map((id) => state.playerMetadata?.[id])
.filter((p): p is NonNullable<typeof p> => p !== undefined)
// Map active players to display data with scores
@@ -26,7 +25,8 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
displayEmoji: player.emoji,
score: state.scores[player.id] || 0,
consecutiveMatches: state.consecutiveMatches?.[player.id] || 0,
isLocalPlayer: player.isLocal !== false, // Local if not explicitly marked as remote
// In local games all players are local, in room games check metadata
isLocalPlayer: state.gameMode === 'single' || state.gameMode === 'multiplayer',
}))
// Check if current player is local (your turn) or remote (waiting)

View File

@@ -4,7 +4,7 @@ import { type ReactNode, useCallback, useEffect, useMemo, useReducer } from 'rea
import { useRouter } from 'next/navigation'
import { useArcadeRedirect } from '@/hooks/useArcadeRedirect'
import { useViewerId } from '@/hooks/useViewerId'
import { useGameMode } from '../../../../contexts/GameModeContext'
import { useUserPlayers } from '@/hooks/useUserPlayers'
import { generateGameCards } from '../utils/cardGeneration'
import { validateMatch } from '../utils/matchValidation'
import { MemoryPairsContext } from './MemoryPairsContext'
@@ -283,16 +283,35 @@ function localMemoryPairsReducer(state: MemoryPairsState, action: LocalAction):
export function LocalMemoryPairsProvider({ children }: { children: ReactNode }) {
const router = useRouter()
const { data: viewerId } = useViewerId()
const { activePlayerCount, activePlayers: activePlayerIds, players } = useGameMode()
// LOCAL-ONLY: Get only the current user's players (no room members)
const { data: userPlayers = [] } = useUserPlayers()
// Use arcade redirect to determine button visibility
const { canModifyPlayers } = useArcadeRedirect({ currentGame: 'matching' })
// Get active player IDs as array
const activePlayers = Array.from(activePlayerIds)
// Build players map from current user's players only
const players = useMemo(() => {
const map = new Map()
userPlayers.forEach((player) => {
map.set(player.id, {
id: player.id,
name: player.name,
emoji: player.emoji,
color: player.color,
isLocal: true,
})
})
return map
}, [userPlayers])
// Get active player IDs from current user's players only
const activePlayers = useMemo(() => {
return userPlayers.filter((p) => p.isActive).map((p) => p.id)
}, [userPlayers])
// Derive game mode from active player count
const gameMode = activePlayerCount > 1 ? 'multiplayer' : 'single'
const gameMode = activePlayers.length > 1 ? 'multiplayer' : 'single'
// Pure client-side state with useReducer
const [state, dispatch] = useReducer(localMemoryPairsReducer, initialState)

View File

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