Compare commits

...

22 Commits

Author SHA1 Message Date
semantic-release-bot
be39401716 chore(release): 4.13.4 [skip ci]
## [4.13.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.3...v4.13.4) (2025-10-18)

### Bug Fixes

* **card-sorting:** increase card tile sizes to contain abacuses ([d2a3b7a](d2a3b7ae2e))
2025-10-18 22:56:13 +00:00
Thomas Hallock
d2a3b7ae2e fix(card-sorting): increase card tile sizes to contain abacuses
Make card tiles larger to properly contain AbacusReact SVGs without overflow.

**Issue:**
Card tiles were too small (90px × 90px and 90px × 110px) to contain the
AbacusReact SVGs, causing abacuses to overflow their containers.

**Fix:**
- **Available cards**: Increased from 90px × 90px to 140px × 140px
- **Position slots**: Increased from 90px × 110px to 140px × 160px

**Result:**
Abacus SVGs now fit comfortably within their card containers without
overflowing, while maintaining the overflow: hidden constraint as a
safety measure.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:283-284,419-420

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:55:15 -05:00
semantic-release-bot
39ab605279 chore(release): 4.13.3 [skip ci]
## [4.13.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.2...v4.13.3) (2025-10-18)

### Bug Fixes

* **card-sorting:** increase SVG size to fill card containers ([cf9d893](cf9d893f3f))
2025-10-18 22:53:10 +00:00
Thomas Hallock
cf9d893f3f fix(card-sorting): increase SVG size to fill card containers
Make AbacusReact SVGs larger by allowing them to fill their containers.

**Issue:**
SVG containers were set to fixed small sizes (74px × 74px and 70px × 70px),
making the abacuses appear too small within their cards.

**Fix:**
- **Available cards**: Changed SVG container from 74px × 74px to 100% × 100%
  to fill the entire 90px card (accounting for 8px padding)
- **Position slots**: Changed SVG container from 70px × 70px to flex: 1,
  width: 100% to fill available space while leaving room for label

**Result:**
Abacuses now appear larger and fill their card containers better while
still being constrained by overflow: hidden to prevent breaking out.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:314-329,456-473

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:51:57 -05:00
semantic-release-bot
e6d0bd4953 chore(release): 4.13.2 [skip ci]
## [4.13.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.1...v4.13.2) (2025-10-18)

### Bug Fixes

* show initial value and improve numeral contrast ([1b57f6d](1b57f6ddec)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
2025-10-18 22:51:19 +00:00
Thomas Hallock
1b57f6ddec fix: show initial value and improve numeral contrast
- Changed back to controlled value (with state update) to display initial 1234567
- Added customStyles for numerals: golden color (#fbbf24), bold, larger font
- Numerals now have excellent contrast against dark background

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:50:12 -05:00
semantic-release-bot
d38ea312a7 chore(release): 4.13.1 [skip ci]
## [4.13.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.0...v4.13.1) (2025-10-18)

### Bug Fixes

* use defaultValue for interactive abacus control ([06aca98](06aca986ac))
2025-10-18 22:47:40 +00:00
Thomas Hallock
06aca986ac fix: use defaultValue for interactive abacus control
Changed from controlled (value) to uncontrolled (defaultValue) pattern
to allow AbacusReact to fully manage its own state and be interactive.
Still tracks value changes via onValueChange callback for display.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:46:32 -05:00
semantic-release-bot
a126466037 chore(release): 4.13.0 [skip ci]
## [4.13.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.12.0...v4.13.0) (2025-10-18)

### Features

* make home page abacus interactive with audio ([9a53d7e](9a53d7e5db))
2025-10-18 22:45:39 +00:00
Thomas Hallock
9a53d7e5db feat: make home page abacus interactive with audio
Enhanced the hero abacus to be a fully interactive showcase:
- Increased size from default to 2.2x scale factor
- Enabled interactive mode (click beads to change value)
- Enabled smooth animations for bead movements
- Enabled audio feedback with volume at 0.4
- Added live value display below abacus in large golden text
- Added instructional text "Click the beads to interact!"
- Shows place value numbers on each column

The abacus now serves as a "try it now" demo right on the landing page.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:44:40 -05:00
semantic-release-bot
d2d8f7740f chore(release): 4.12.0 [skip ci]
## [4.12.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.1...v4.12.0) (2025-10-18)

### Features

* redesign home page with component showcase ([29af265](29af265958))
2025-10-18 22:44:06 +00:00
Thomas Hallock
29af265958 feat: redesign home page with component showcase
Replaced outdated landing page with component-based "storybook" design:
- Large featured AbacusReact component (1,234,567 with place-value colors)
- Color scheme showcase with 4 actual AbacusReact instances
- Dark, fancy theme with purple gradients and golden accents
- All game links now point to /games instead of /arcade
- Hover animations and visual polish throughout
- Dot pattern background texture

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:42:55 -05:00
semantic-release-bot
291bcc581d chore(release): 4.11.1 [skip ci]
## [4.11.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.0...v4.11.1) (2025-10-18)

### Bug Fixes

* **card-sorting:** center AbacusReact SVGs in card tiles ([26edec1](26edec1bbf))
2025-10-18 22:41:08 +00:00
Thomas Hallock
26edec1bbf fix(card-sorting): center AbacusReact SVGs in card tiles
Improve centering of abacus SVGs within both available cards and position slots.

**Issue:**
AbacusReact SVGs were not properly centered within their card containers,
appearing off-center or misaligned.

**Fix:**
- **Available cards**: Added maxHeight: '100%' constraint and display: 'block'
  with margin: '0 auto' to SVG styling
- **Position slots**: Changed container to use flex: 1 and proper flex centering,
  constrained SVG to maxWidth: '70px' with centering styles

**Changes:**
- Available card SVG container: Added display flex with center alignment
- Available card SVG: maxHeight: '100%', display: 'block', margin: '0 auto'
- Position slot SVG container: width: '100%', flex: 1, flex centering
- Position slot SVG: maxWidth: '70px', maxHeight: '100%', display: 'block', margin: '0 auto'

Now AbacusReact SVGs render centered within their card tiles regardless of
the actual SVG dimensions.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:314-330,457-475

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:40:03 -05:00
semantic-release-bot
da4fdc90e0 chore(release): 4.11.0 [skip ci]
## [4.11.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.6...v4.11.0) (2025-10-18)

### Features

* **home:** redesign home page to showcase complete platform ([ee6c4f2](ee6c4f2f4f))
2025-10-18 22:34:07 +00:00
Thomas Hallock
ee6c4f2f4f feat(home): redesign home page to showcase complete platform
Replace outdated "flashcard generator" landing page with comprehensive
platform showcase highlighting all three pillars: arcade games,
interactive learning, and flashcard creation.

**New Home Page Structure:**
- Compact hero with 3 CTAs: Play Games, Learn, Create
- 4 arcade game cards with player counts and mode tags
- Two-column feature sections for Learning & Flashcards
- Multiplayer features grid (4 cards)
- Stats banner: 4 games, 8 max players, 3 learning modes, 4+ formats

**Visual Design:**
- Smaller, denser components to fit more content
- Information-rich showcase vs marketing fluff
- Purple gradient hero matching guide branding
- Responsive grid layouts for all screen sizes

**Result:**
Home page now accurately represents the full platform:
multiplayer arcade games + interactive tutorials + flashcard tools.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:33:01 -05:00
semantic-release-bot
9b9f0cdbcb chore(release): 4.10.6 [skip ci]
## [4.10.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.5...v4.10.6) (2025-10-18)

### Bug Fixes

* **card-sorting:** position slots flow horizontally with wrap ([e14ffe4](e14ffe44d6))
2025-10-18 22:24:03 +00:00
Thomas Hallock
e14ffe44d6 fix(card-sorting): position slots flow horizontally with wrap
Fix position slots to match Python original - they should flow horizontally
and wrap, not stack vertically.

**Before:**
- Container: display: flex, flexDirection: 'column' (vertical stack)
- Each slot + insert button pair wrapped in a div
- One slot per line with insert button below it

**After (matching Python lines 3101-3111):**
- Container: display: flex, flexWrap: 'wrap' (horizontal flow)
- Slots and insert buttons flow together as siblings
- Wraps naturally based on container width
- Dashed border container with semi-transparent background

**Changes:**
- Position slots container: flex-wrap instead of flex-direction: column
- Removed wrapper div around slot+button pairs
- Added React keys to slots and buttons
- Added container styling (padding, background, border)

Now matches the Python original where all slots and + buttons flow
together horizontally and wrap as needed.

src/arcade-games/card-sorting/components/PlayingPhase.tsx:362-524

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:22:56 -05:00
semantic-release-bot
d5bc0bb27c chore(release): 4.10.5 [skip ci]
## [4.10.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.4...v4.10.5) (2025-10-18)

### Code Refactoring

* **arcade:** merge /arcade/room into /arcade route ([0790074](0790074ffc))
2025-10-18 22:11:17 +00:00
Thomas Hallock
0790074ffc refactor(arcade): merge /arcade/room into /arcade route
Simplify arcade routing by moving room page to main arcade route.
The /arcade route now handles both game selection and gameplay,
eliminating the need for a separate /arcade/room route.

**Changes:**
- Move /arcade/room/page.tsx → /arcade/page.tsx
- Update import paths for styled-system (room/ → arcade/)
- Remove legacy GAMES_CONFIG handling (now registry-only)
- Delete obsolete EnhancedChampionArena component

**Result:**
- Users navigate directly to /arcade for all arcade features
- Game selection UI shows when no game selected in room
- Selected game renders when room has gameName set
- Simpler, more intuitive URL structure

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 17:10:10 -05:00
semantic-release-bot
1a44daf2ce chore(release): 4.10.4 [skip ci]
## [4.10.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.3...v4.10.4) (2025-10-18)

### Bug Fixes

* **card-sorting:** match Python card layout with flex wrap ([9679d68](9679d68154))
2025-10-18 21:35:53 +00:00
Thomas Hallock
9679d68154 fix(card-sorting): match Python card layout with flex wrap
Fix available cards layout to match the Python original implementation.

**Before:**
- Grid layout with responsive columns (1/2/3 cols)
- Cards stretched to fill grid cells
- Each card on its own line on mobile

**After (matching Python web_generator.py lines 3077-3087):**
- Flex container with wrap
- Fixed 90px x 90px card size
- Cards flow naturally and wrap based on container width
- Dashed border container with semi-transparent background
- Revealed numbers positioned absolutely in top-right corner
- Matching hover/selection animations from original

**Visual Changes:**
- Container: flex wrap, centered, dashed border, rgba background
- Cards: 90px x 90px fixed size
- Revealed numbers: absolute positioned badge (top-right)
- Selection state: scale(1.1), blue border, blue background
- Hover: translateY(-5px) lift effect

src/arcade-games/card-sorting/components/PlayingPhase.tsx:265-348

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 16:34:50 -05:00
14 changed files with 960 additions and 603 deletions

View File

@@ -1,3 +1,80 @@
## [4.13.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.3...v4.13.4) (2025-10-18)
### Bug Fixes
* **card-sorting:** increase card tile sizes to contain abacuses ([d2a3b7a](https://github.com/antialias/soroban-abacus-flashcards/commit/d2a3b7ae2e3f6819b8d9ace32be22f04f748d1bc))
## [4.13.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.2...v4.13.3) (2025-10-18)
### Bug Fixes
* **card-sorting:** increase SVG size to fill card containers ([cf9d893](https://github.com/antialias/soroban-abacus-flashcards/commit/cf9d893f3fdbef6e91cd0ba283d602b9215569f1))
## [4.13.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.1...v4.13.2) (2025-10-18)
### Bug Fixes
* show initial value and improve numeral contrast ([1b57f6d](https://github.com/antialias/soroban-abacus-flashcards/commit/1b57f6ddecf3a118f2e4fadd1a91be1256f5a034)), closes [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
## [4.13.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.13.0...v4.13.1) (2025-10-18)
### Bug Fixes
* use defaultValue for interactive abacus control ([06aca98](https://github.com/antialias/soroban-abacus-flashcards/commit/06aca986ace4d76b70f2fd2f5e57f66758185b38))
## [4.13.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.12.0...v4.13.0) (2025-10-18)
### Features
* make home page abacus interactive with audio ([9a53d7e](https://github.com/antialias/soroban-abacus-flashcards/commit/9a53d7e5db18853aca4e2e0c7abc799217feaecf))
## [4.12.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.1...v4.12.0) (2025-10-18)
### Features
* redesign home page with component showcase ([29af265](https://github.com/antialias/soroban-abacus-flashcards/commit/29af265958f9fdab0253b92e153c01575840454d))
## [4.11.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.11.0...v4.11.1) (2025-10-18)
### Bug Fixes
* **card-sorting:** center AbacusReact SVGs in card tiles ([26edec1](https://github.com/antialias/soroban-abacus-flashcards/commit/26edec1bbf038264405ec9d161edcd18f67a6fc6))
## [4.11.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.6...v4.11.0) (2025-10-18)
### Features
* **home:** redesign home page to showcase complete platform ([ee6c4f2](https://github.com/antialias/soroban-abacus-flashcards/commit/ee6c4f2f4f39e3b30f59c54866c3857c218fb80f))
## [4.10.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.5...v4.10.6) (2025-10-18)
### Bug Fixes
* **card-sorting:** position slots flow horizontally with wrap ([e14ffe4](https://github.com/antialias/soroban-abacus-flashcards/commit/e14ffe44d66d0c97bc0cc4e0c255698e88ce723a))
## [4.10.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.4...v4.10.5) (2025-10-18)
### Code Refactoring
* **arcade:** merge /arcade/room into /arcade route ([0790074](https://github.com/antialias/soroban-abacus-flashcards/commit/0790074ffc5008bce9a162fe0ddbd1d5c214c4f7))
## [4.10.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.3...v4.10.4) (2025-10-18)
### Bug Fixes
* **card-sorting:** match Python card layout with flex wrap ([9679d68](https://github.com/antialias/soroban-abacus-flashcards/commit/9679d68154ac8b6a2f905ec7d17a34b39bc00237))
## [4.10.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.10.2...v4.10.3) (2025-10-18)

View File

@@ -97,7 +97,8 @@
"Bash(pnpm exec turbo build --filter=@soroban/web)",
"Bash(do gh run list --limit 1 --json conclusion,status,name,databaseId --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')",
"Bash(do gh run list --limit 1 --json conclusion,status,name --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\") - \\(.name)\"\"')",
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')"
"Bash(do gh run list --limit 1 --workflow=\"Build and Deploy\" --json conclusion,status --jq '.[0] | \"\"\\(.status) - \\(.conclusion // \"\"running\"\")\"\"')",
"WebFetch(domain:abaci.one)"
],
"deny": [],
"ask": []

View File

@@ -158,7 +158,7 @@ export default function RoomDetailPage() {
const startGame = () => {
if (!room) return
// Navigate to the room game page
router.push('/arcade/room')
router.push('/arcade')
}
const joinRoom = async () => {

View File

@@ -1,128 +1,315 @@
'use client'
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../styled-system/css'
import { EnhancedChampionArena } from '../../components/EnhancedChampionArena'
import { FullscreenProvider, useFullscreen } from '../../contexts/FullscreenContext'
import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
function ArcadeContent() {
const { setFullscreenElement } = useFullscreen()
const arcadeRef = useRef<HTMLDivElement>(null)
/**
* /arcade - 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
*
* Shows game selection when no game is set, then shows the game itself once selected.
* URL never changes - it's always /arcade regardless of selection, setup, or gameplay.
*
* Note: We show a friendly message with a link if no room exists to avoid navigation loops.
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { mutate: setRoomGame } = useSetRoomGame()
useEffect(() => {
// Register this component's main div as the fullscreen element
if (arcadeRef.current) {
setFullscreenElement(arcadeRef.current)
}
}, [setFullscreenElement])
return (
<div
ref={arcadeRef}
className={css({
minHeight: 'calc(100vh - 80px)', // Account for mini nav height
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
position: 'relative',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
py: { base: '4', md: '6' },
})}
>
{/* Animated background elements */}
// Show loading state
if (isLoading) {
return (
<div
className={css({
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: `
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.3) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.2) 0%, transparent 50%)
`,
animation: 'arcadeFloat 20s ease-in-out infinite',
})}
/>
{/* Main Champion Arena - takes remaining space */}
<div
className={css({
flex: 1,
style={{
display: 'flex',
px: { base: '2', md: '4' },
position: 'relative',
zIndex: 1,
minHeight: 0, // Important for flex children
})}
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
<EnhancedChampionArena
onConfigurePlayer={() => {}}
Loading room...
</div>
)
}
// Show error if no room (instead of redirecting)
if (!roomData) {
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>
)
}
// Show game selection if no game is set
if (!roomData.gameName) {
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// All games are now in the registry
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
if (!gameDef?.manifest.available) {
console.log('[RoomPage] Registry game not available, blocking selection')
return
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType,
})
return
}
console.log('[RoomPage] Unknown game type:', gameType)
}
return (
<PageWithNav
navTitle="Choose Game"
navEmoji="🎮"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
className={css({
width: '100%',
height: '100%',
background: 'rgba(255, 255, 255, 0.05)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(255, 255, 255, 0.1)',
boxShadow: '0 20px 40px rgba(0, 0, 0, 0.3)',
minHeight: '100vh',
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4',
})}
/>
</div>
</div>
)
}
>
<h1
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
textAlign: 'center',
})}
>
Choose a Game
</h1>
function ArcadePageWithRedirect() {
return (
<PageWithNav navTitle="Champion Arena" navEmoji="🏟️" emphasizePlayerSelection={true}>
<ArcadeContent />
</PageWithNav>
)
}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)' },
gap: '4',
maxWidth: '800px',
width: '100%',
})}
>
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]: [string, any]) => {
const isAvailable = !('available' in config) || config.available !== false
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{config.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{config.name}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{config.description}
</p>
</button>
)
})}
export default function ArcadePage() {
return (
<FullscreenProvider>
<ArcadePageWithRedirect />
</FullscreenProvider>
)
}
// Arcade-specific animations
const arcadeAnimations = `
@keyframes arcadeFloat {
0%, 100% {
transform: translateY(0px) rotate(0deg);
opacity: 0.7;
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{gameDef.manifest.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{gameDef.manifest.displayName}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{gameDef.manifest.description}
</p>
</button>
)
})}
</div>
</div>
</PageWithNav>
)
}
33% {
transform: translateY(-20px) rotate(1deg);
opacity: 1;
// Check if this is a registry game first
if (hasGame(roomData.gameName)) {
const gameDef = getGame(roomData.gameName)
if (!gameDef) {
return (
<PageWithNav
navTitle="Game Not Found"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not found in registry
</div>
</PageWithNav>
)
}
// Render registry game dynamically
const { Provider, GameComponent } = gameDef
return (
<Provider>
<GameComponent />
</Provider>
)
}
66% {
transform: translateY(-10px) rotate(-1deg);
opacity: 0.8;
// Render legacy games based on room's gameName
switch (roomData.gameName) {
// TODO: Add other legacy games (complement-race, etc.) once migrated
default:
return (
<PageWithNav
navTitle="Game Not Available"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not yet supported
</div>
</PageWithNav>
)
}
}
@keyframes arcadePulse {
0%, 100% {
box-shadow: 0 0 20px rgba(96, 165, 250, 0.3);
}
50% {
box-shadow: 0 0 40px rgba(96, 165, 250, 0.6);
}
}
`
// Inject arcade animations
if (typeof document !== 'undefined' && !document.getElementById('arcade-animations')) {
const style = document.createElement('style')
style.id = 'arcade-animations'
style.textContent = arcadeAnimations
document.head.appendChild(style)
}

View File

@@ -1,358 +0,0 @@
'use client'
import { useRouter } from 'next/navigation'
import { useRoomData, useSetRoomGame } from '@/hooks/useRoomData'
import { GAMES_CONFIG } from '@/components/GameSelector'
import type { GameType } from '@/components/GameSelector'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../../styled-system/css'
import { getAllGames, getGame, hasGame } from '@/lib/arcade/game-registry'
// Map GameType keys to internal game names
// Note: "battle-arena" removed - now handled by game registry as "matching"
const GAME_TYPE_TO_NAME: Record<GameType, string> = {
'complement-race': 'complement-race',
'master-organizer': 'master-organizer',
}
/**
* /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
*
* Shows game selection when no game is set, then shows the game itself once selected.
* URL never changes - it's always /arcade/room regardless of selection, setup, or gameplay.
*
* Note: We don't redirect to /arcade if no room exists to avoid navigation loops.
* Instead, we show a friendly message with a link back to the Champion Arena.
*
* Note: ModerationNotifications is handled by PageWithNav inside each game component,
* so we don't need to render it here.
*/
export default function RoomPage() {
const router = useRouter()
const { roomData, isLoading } = useRoomData()
const { mutate: setRoomGame } = useSetRoomGame()
// Show loading state
if (isLoading) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Loading room...
</div>
)
}
// Show error if no room (instead of redirecting)
if (!roomData) {
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>
)
}
// Show game selection if no game is set
if (!roomData.gameName) {
const handleGameSelect = (gameType: GameType) => {
console.log('[RoomPage] handleGameSelect called with gameType:', gameType)
// Check if it's a registry game first
if (hasGame(gameType)) {
const gameDef = getGame(gameType)
if (!gameDef?.manifest.available) {
console.log('[RoomPage] Registry game not available, blocking selection')
return
}
console.log('[RoomPage] Selecting registry game:', gameType)
setRoomGame({
roomId: roomData.id,
gameName: gameType, // Use the game name directly for registry games
})
return
}
// Legacy game handling
const gameConfig = GAMES_CONFIG[gameType as keyof typeof GAMES_CONFIG]
if (!gameConfig) {
console.log('[RoomPage] Unknown game type:', gameType)
return
}
console.log('[RoomPage] Game config:', {
name: gameConfig.name,
available: 'available' in gameConfig ? gameConfig.available : true,
})
if ('available' in gameConfig && gameConfig.available === false) {
console.log('[RoomPage] Game not available, blocking selection')
return // Don't allow selecting unavailable games
}
// Map GameType to internal game name
const internalGameName = GAME_TYPE_TO_NAME[gameType]
console.log('[RoomPage] Mapping:', {
gameType,
internalGameName,
mappingExists: !!internalGameName,
})
console.log('[RoomPage] Calling setRoomGame with:', {
roomId: roomData.id,
gameName: internalGameName,
preservingGameConfig: true,
})
// Don't pass gameConfig - we want to preserve existing settings for all games
setRoomGame({
roomId: roomData.id,
gameName: internalGameName,
})
}
return (
<PageWithNav
navTitle="Choose Game"
navEmoji="🎮"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
className={css({
minHeight: '100vh',
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a3a 50%, #2d1b69 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '4',
})}
>
<h1
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '8',
textAlign: 'center',
})}
>
Choose a Game
</h1>
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1fr', md: 'repeat(2, 1fr)' },
gap: '4',
maxWidth: '800px',
width: '100%',
})}
>
{/* Legacy games */}
{Object.entries(GAMES_CONFIG).map(([gameType, config]) => {
const isAvailable = !('available' in config) || config.available !== false
return (
<button
key={gameType}
onClick={() => handleGameSelect(gameType as GameType)}
disabled={!isAvailable}
className={css({
background: config.gradient,
border: '2px solid',
borderColor: config.borderColor || 'blue.200',
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{config.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{config.name}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{config.description}
</p>
</button>
)
})}
{/* Registry games */}
{getAllGames().map((gameDef) => {
const isAvailable = gameDef.manifest.available
return (
<button
key={gameDef.manifest.name}
onClick={() => handleGameSelect(gameDef.manifest.name)}
disabled={!isAvailable}
className={css({
background: gameDef.manifest.gradient,
border: '2px solid',
borderColor: gameDef.manifest.borderColor,
borderRadius: '2xl',
padding: '6',
cursor: !isAvailable ? 'not-allowed' : 'pointer',
opacity: !isAvailable ? 0.5 : 1,
transition: 'all 0.3s ease',
_hover: !isAvailable
? {}
: {
transform: 'translateY(-4px) scale(1.02)',
boxShadow: '0 20px 40px rgba(59, 130, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '2',
})}
>
{gameDef.manifest.icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontWeight: 'bold',
color: 'gray.900',
mb: '2',
})}
>
{gameDef.manifest.displayName}
</h3>
<p
className={css({
fontSize: 'sm',
color: 'gray.600',
})}
>
{gameDef.manifest.description}
</p>
</button>
)
})}
</div>
</div>
</PageWithNav>
)
}
// Check if this is a registry game first
if (hasGame(roomData.gameName)) {
const gameDef = getGame(roomData.gameName)
if (!gameDef) {
return (
<PageWithNav
navTitle="Game Not Found"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not found in registry
</div>
</PageWithNav>
)
}
// Render registry game dynamically
const { Provider, GameComponent } = gameDef
return (
<Provider>
<GameComponent />
</Provider>
)
}
// Render legacy games based on room's gameName
switch (roomData.gameName) {
// TODO: Add other legacy games (complement-race, etc.) once migrated
default:
return (
<PageWithNav
navTitle="Game Not Available"
navEmoji="⚠️"
emphasizePlayerSelection={true}
onExitSession={() => router.push('/arcade')}
>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100vh',
fontSize: '18px',
color: '#666',
}}
>
Game "{roomData.gameName}" not yet supported
</div>
</PageWithNav>
)
}
}

View File

@@ -231,7 +231,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
password: roomPassword,
})
// Navigate to the game
router.push('/arcade/room')
router.push('/arcade')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to join room')
setIsJoining(false)
@@ -261,7 +261,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
// If user is already in this exact room, just navigate to game
if (roomData && roomData.id === room.id) {
router.push('/arcade/room')
router.push('/arcade')
return
}
@@ -313,7 +313,7 @@ export default function JoinRoomPage({ params }: { params: { code: string } }) {
}
const handleCancel = () => {
router.push('/arcade/room') // Stay in current room
router.push('/arcade') // Stay in current room
}
const handlePasswordSubmit = () => {

View File

@@ -1,185 +1,594 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { PageWithNav } from '@/components/PageWithNav'
import { AbacusReact } from '@soroban/abacus-react'
import { css } from '../../styled-system/css'
import { container, hstack, stack } from '../../styled-system/patterns'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
export default function HomePage() {
const [abacusValue, setAbacusValue] = useState(1234567)
return (
<PageWithNav navTitle="Soroban Flashcards" navEmoji="🧮">
<div
className={css({
minHeight: '100vh',
bg: 'gradient-to-br from-brand.50 to-brand.100',
})}
>
{/* Hero Section */}
<main className={container({ maxW: '6xl', px: '4' })}>
<PageWithNav navTitle="Soroban Mastery Platform" navEmoji="🧮">
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
{/* Hero with Large Abacus */}
<div
className={css({
background:
'linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(88, 28, 135, 0.3) 50%, rgba(17, 24, 39, 1) 100%)',
color: 'white',
py: { base: '12', md: '20' },
position: 'relative',
overflow: 'hidden',
})}
>
<div
className={stack({
gap: '12',
py: '16',
align: 'center',
textAlign: 'center',
className={css({
position: 'absolute',
inset: 0,
opacity: 0.1,
backgroundImage:
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
backgroundSize: '40px 40px',
})}
>
{/* Hero Content */}
<div className={stack({ gap: '6', maxW: '4xl' })}>
/>
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
<h1
className={css({
fontSize: { base: '4xl', md: '6xl' },
fontSize: { base: '3xl', md: '5xl', lg: '6xl' },
fontWeight: 'bold',
color: 'gray.900',
mb: '6',
lineHeight: 'tight',
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
backgroundClip: 'text',
color: 'transparent',
})}
>
Beautiful Soroban <span className={css({ color: 'brand.600' })}>Flashcards</span>
Master the Soroban
</h1>
{/* Large Featured Abacus */}
<div
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4',
my: '10',
p: '8',
bg: 'rgba(0, 0, 0, 0.4)',
borderRadius: '2xl',
border: '2px solid',
borderColor: 'purple.500/30',
boxShadow: '0 25px 50px -12px rgba(139, 92, 246, 0.25)',
})}
>
<div
className={css({
fontSize: 'lg',
fontWeight: 'semibold',
color: 'gray.300',
mb: '2',
})}
>
Click the beads to interact!
</div>
<AbacusReact
value={abacusValue}
columns={7}
beadShape="diamond"
colorScheme="place-value"
hideInactiveBeads={false}
interactive={true}
animated={true}
soundEnabled={true}
soundVolume={0.4}
scaleFactor={2.2}
showNumbers={true}
customStyles={{
numerals: {
fill: '#fbbf24',
fontWeight: 'bold',
fontSize: '18px',
},
}}
callbacks={{
onValueChange: (newValue: number) => setAbacusValue(newValue),
}}
/>
<div
className={css({
fontSize: '4xl',
fontWeight: 'bold',
color: 'yellow.400',
fontFamily: 'mono',
letterSpacing: 'wide',
})}
>
{abacusValue.toLocaleString()}
</div>
</div>
<p
className={css({
fontSize: { base: 'lg', md: 'xl' },
color: 'gray.600',
maxW: '2xl',
color: 'gray.300',
mb: '8',
maxW: '3xl',
mx: 'auto',
})}
>
Create stunning, educational flashcards with authentic Japanese abacus
representations. Perfect for teachers, students, and mental math enthusiasts.
Interactive tutorials, multiplayer games, and beautiful flashcardsyour complete
soroban learning ecosystem
</p>
<div className={hstack({ gap: '4', justify: 'center', mt: '8' })}>
<div className={hstack({ gap: '4', justify: 'center', flexWrap: 'wrap' })}>
<Link
href="/create"
href="/games"
className={css({
px: '8',
py: '4',
bg: 'brand.600',
color: 'white',
bg: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
color: 'gray.900',
fontWeight: 'bold',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
transition: 'all',
shadow: '0 10px 40px rgba(251, 191, 36, 0.3)',
_hover: {
bg: 'brand.700',
transform: 'translateY(-2px)',
shadow: 'modal',
shadow: '0 20px 50px rgba(251, 191, 36, 0.4)',
},
transition: 'all 0.3s ease',
})}
>
Start Creating
🎮 Play Games
</Link>
<Link
href="/guide"
className={css({
px: '8',
py: '4',
bg: 'white',
color: 'brand.700',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'white',
fontWeight: 'bold',
fontSize: 'lg',
fontWeight: 'semibold',
rounded: 'xl',
shadow: 'card',
border: '2px solid',
borderColor: 'brand.200',
transition: 'all',
borderColor: 'purple.500',
_hover: {
borderColor: 'brand.400',
bg: 'rgba(139, 92, 246, 0.3)',
transform: 'translateY(-2px)',
},
transition: 'all 0.3s ease',
})}
>
📚 Learn Soroban
📚 Learn
</Link>
<Link
href="/create"
className={css({
px: '8',
py: '4',
bg: 'rgba(139, 92, 246, 0.2)',
color: 'white',
fontWeight: 'bold',
fontSize: 'lg',
rounded: 'xl',
border: '2px solid',
borderColor: 'purple.500',
_hover: {
bg: 'rgba(139, 92, 246, 0.3)',
transform: 'translateY(-2px)',
},
transition: 'all 0.3s ease',
})}
>
🎨 Create
</Link>
</div>
</div>
</div>
</div>
{/* Features Grid */}
<div
className={css({
display: 'grid',
gridTemplateColumns: { base: '1', md: '3' },
gap: '8',
mt: '16',
w: 'full',
})}
>
<FeatureCard
icon="🎨"
title="Beautiful Design"
description="Vector graphics, color schemes, authentic bead positioning"
{/* Color Scheme Showcase */}
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
<section className={stack({ gap: '8' })}>
<div className={css({ textAlign: 'center' })}>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '3',
})}
>
Beautiful Color Schemes
</h2>
<p className={css({ color: 'gray.400', fontSize: 'lg' })}>
Choose from multiple visual styles to enhance learning
</p>
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '6' })}>
<ColorSchemeCard
title="Monochrome"
description="Classic, minimalist design"
colorScheme="monochrome"
value={42}
beadShape="diamond"
/>
<FeatureCard
icon=""
title="Instant Generation"
description="Create PDFs, interactive HTML, PNGs, and SVGs in seconds"
<ColorSchemeCard
title="Place Value"
description="Each column has its own color"
colorScheme="place-value"
value={789}
beadShape="circle"
/>
<FeatureCard
icon="🎯"
title="Educational Focus"
description="Perfect for teachers, students, and soroban enthusiasts"
<ColorSchemeCard
title="Heaven & Earth"
description="Distinct heaven and earth beads"
colorScheme="heaven-earth"
value={156}
beadShape="square"
/>
<ColorSchemeCard
title="Alternating"
description="Alternating column colors"
colorScheme="alternating"
value={234}
beadShape="diamond"
/>
</div>
</section>
{/* Arcade Games Section */}
<section className={stack({ gap: '6', mt: '16' })}>
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
<div>
<h2
className={css({
fontSize: { base: '2xl', md: '3xl' },
fontWeight: 'bold',
color: 'white',
mb: '2',
})}
>
🕹 Multiplayer Arcade
</h2>
<p className={css({ color: 'gray.400', fontSize: 'md' })}>
Compete with friends in real-time soroban games
</p>
</div>
<Link
href="/games"
className={css({
fontSize: 'md',
color: 'yellow.400',
fontWeight: 'semibold',
_hover: { color: 'yellow.300' },
display: { base: 'none', md: 'block' },
})}
>
View All
</Link>
</div>
<div className={grid({ columns: { base: 1, sm: 2, lg: 4 }, gap: '5' })}>
<GameCard
icon="🧠"
title="Memory Lightning"
description="Memorize soroban numbers"
players="1-8 players"
tags={['Co-op', 'Competitive']}
gradient="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
href="/games"
/>
<GameCard
icon="⚔️"
title="Matching Pairs"
description="Turn-based card battles"
players="1-4 players"
tags={['Pattern Recognition']}
gradient="linear-gradient(135deg, #f093fb 0%, #f5576c 100%)"
href="/games"
/>
<GameCard
icon="🏁"
title="Speed Race"
description="Race AI with complements"
players="1-4 players + AI"
tags={['Practice', 'Sprint', 'Survival']}
gradient="linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)"
href="/games"
/>
<GameCard
icon="🔢"
title="Card Sorting"
description="Arrange cards visually"
players="Solo challenge"
tags={['Visual Literacy']}
gradient="linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)"
href="/games"
/>
</div>
</section>
{/* Interactive Learning & Flashcard Creator */}
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '8', mt: '16' })}>
<FeaturePanel
icon="📚"
title="Interactive Learning"
description="Master soroban through hands-on guided tutorials"
features={[
'Visual tutorials on reading bead positions',
'Step-by-step arithmetic operations',
'Interactive exercises with instant feedback',
]}
ctaText="Start Learning →"
ctaHref="/guide"
accentColor="purple"
/>
<FeaturePanel
icon="🎨"
title="Flashcard Creator"
description="Design beautiful soroban flashcards for any purpose"
features={[
'Multiple export formats: PDF, PNG, SVG, HTML',
'Custom bead shapes, colors, and layouts',
'All paper sizes: A3, A4, A5, US Letter',
]}
ctaText="Create Flashcards →"
ctaHref="/create"
accentColor="blue"
/>
</div>
</main>
{/* Stats Banner */}
<section
className={css({
bg: 'linear-gradient(135deg, rgba(139, 92, 246, 0.2) 0%, rgba(59, 130, 246, 0.2) 100%)',
rounded: '2xl',
p: '10',
mt: '16',
border: '2px solid',
borderColor: 'purple.500/20',
})}
>
<h2
className={css({
fontSize: { base: 'xl', md: '2xl' },
fontWeight: 'bold',
mb: '8',
textAlign: 'center',
color: 'white',
})}
>
Complete Soroban Learning Platform
</h2>
<div className={grid({ columns: { base: 2, md: 4 }, gap: '8', textAlign: 'center' })}>
<StatItem number="4" label="Arcade Games" />
<StatItem number="8" label="Max Players" />
<StatItem number="3" label="Learning Modes" />
<StatItem number="4+" label="Export Formats" />
</div>
</section>
</div>
</div>
</PageWithNav>
)
}
function FeatureCard({
icon,
function ColorSchemeCard({
title,
description,
colorScheme,
value,
beadShape,
}: {
icon: string
title: string
description: string
colorScheme: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
value: number
beadShape: 'diamond' | 'circle' | 'square'
}) {
return (
<div
className={css({
p: '8',
bg: 'white',
rounded: '2xl',
shadow: 'card',
textAlign: 'center',
transition: 'all',
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '6',
border: '2px solid',
borderColor: 'gray.700',
transition: 'all 0.3s ease',
_hover: {
borderColor: 'purple.500',
transform: 'translateY(-4px)',
shadow: 'modal',
boxShadow: '0 20px 40px rgba(139, 92, 246, 0.2)',
},
})}
>
<div
className={css({
fontSize: '4xl',
mb: '4',
})}
>
{icon}
</div>
<h3
className={css({
fontSize: 'xl',
fontSize: 'lg',
fontWeight: 'bold',
color: 'gray.900',
mb: '3',
color: 'white',
mb: '2',
})}
>
{title}
</h3>
<p
<p className={css({ fontSize: 'sm', color: 'gray.400', mb: '4' })}>{description}</p>
<div
className={css({
color: 'gray.600',
lineHeight: 'relaxed',
display: 'flex',
justifyContent: 'center',
p: '4',
bg: 'rgba(255, 255, 255, 0.05)',
rounded: 'lg',
})}
>
{description}
</p>
<AbacusReact
value={value}
columns={3}
beadShape={beadShape}
colorScheme={colorScheme}
hideInactiveBeads={false}
/>
</div>
</div>
)
}
function GameCard({
icon,
title,
description,
players,
tags,
gradient,
href,
}: {
icon: string
title: string
description: string
players: string
tags: string[]
gradient: string
href: string
}) {
return (
<Link href={href}>
<div
className={css({
background: gradient,
rounded: 'xl',
p: '6',
shadow: 'lg',
transition: 'all 0.3s ease',
cursor: 'pointer',
_hover: {
transform: 'translateY(-6px) scale(1.02)',
shadow: '0 25px 50px rgba(0, 0, 0, 0.3)',
},
})}
>
<div className={css({ fontSize: '3xl', mb: '3' })}>{icon}</div>
<h3 className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}>
{title}
</h3>
<p className={css({ fontSize: 'sm', color: 'rgba(255, 255, 255, 0.9)', mb: '2' })}>
{description}
</p>
<p className={css({ fontSize: 'xs', color: 'rgba(255, 255, 255, 0.7)', mb: '3' })}>
{players}
</p>
<div className={hstack({ gap: '2', flexWrap: 'wrap' })}>
{tags.map((tag) => (
<span
key={tag}
className={css({
fontSize: 'xs',
px: '2',
py: '1',
bg: 'rgba(255, 255, 255, 0.2)',
color: 'white',
rounded: 'full',
fontWeight: 'semibold',
})}
>
{tag}
</span>
))}
</div>
</div>
</Link>
)
}
function FeaturePanel({
icon,
title,
description,
features,
ctaText,
ctaHref,
accentColor,
}: {
icon: string
title: string
description: string
features: string[]
ctaText: string
ctaHref: string
accentColor: 'purple' | 'blue'
}) {
const borderColor = accentColor === 'purple' ? 'purple.500/30' : 'blue.500/30'
const bgColor = accentColor === 'purple' ? 'purple.500/10' : 'blue.500/10'
const hoverBg = accentColor === 'purple' ? 'purple.500/20' : 'blue.500/20'
return (
<div
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
rounded: 'xl',
p: '8',
border: '2px solid',
borderColor,
})}
>
<div className={hstack({ gap: '3', mb: '4' })}>
<span className={css({ fontSize: '3xl' })}>{icon}</span>
<h2 className={css({ fontSize: '2xl', fontWeight: 'bold', color: 'white' })}>{title}</h2>
</div>
<p className={css({ fontSize: 'md', color: 'gray.300', mb: '6' })}>{description}</p>
<div className={stack({ gap: '3', mb: '6' })}>
{features.map((feature, i) => (
<div key={i} className={hstack({ gap: '3' })}>
<span className={css({ color: 'yellow.400', fontSize: 'lg' })}></span>
<span className={css({ color: 'gray.300', fontSize: 'sm' })}>{feature}</span>
</div>
))}
</div>
<Link
href={ctaHref}
className={css({
display: 'block',
textAlign: 'center',
py: '3',
px: '6',
bg: bgColor,
color: 'white',
fontWeight: 'bold',
rounded: 'lg',
border: '2px solid',
borderColor,
_hover: { bg: hoverBg },
transition: 'all 0.2s ease',
})}
>
{ctaText}
</Link>
</div>
)
}
function StatItem({ number, label }: { number: string; label: string }) {
return (
<div>
<div
className={css({
fontSize: { base: '3xl', md: '4xl' },
fontWeight: 'bold',
mb: '2',
background: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
backgroundClip: 'text',
color: 'transparent',
})}
>
{number}
</div>
<div className={css({ fontSize: 'sm', color: 'gray.300' })}>{label}</div>
</div>
)
}

View File

@@ -264,13 +264,15 @@ export function PlayingPhase() {
</h3>
<div
className={css({
display: 'grid',
gridTemplateColumns: {
base: '1',
sm: '2',
md: '3',
},
gap: '0.75rem',
display: 'flex',
flexWrap: 'wrap',
gap: '10px',
justifyContent: 'center',
padding: '15px',
background: 'rgba(255,255,255,0.5)',
borderRadius: '8px',
minHeight: '120px',
border: '2px dashed #2c5f76',
})}
>
{state.availableCards.map((card) => (
@@ -278,39 +280,65 @@ export function PlayingPhase() {
key={card.id}
onClick={() => handleCardClick(card.id)}
className={css({
padding: '0.5rem',
width: '140px',
height: '140px',
padding: '8px',
border: '2px solid',
borderColor: selectedCardId === card.id ? 'blue.500' : 'gray.300',
borderRadius: '0.5rem',
background: selectedCardId === card.id ? 'blue.50' : 'white',
borderColor: selectedCardId === card.id ? '#1976d2' : 'transparent',
borderRadius: '8px',
background: selectedCardId === card.id ? '#e3f2fd' : 'white',
cursor: 'pointer',
transition: 'all 0.2s',
transform: selectedCardId === card.id ? 'scale(1.05)' : 'scale(1)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
userSelect: 'none',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
_hover: {
transform: 'scale(1.05)',
borderColor: 'blue.500',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
transform: 'translateY(-5px)',
boxShadow: '0 8px 16px rgba(0,0,0,0.2)',
borderColor: '#2c5f76',
},
})}
style={
selectedCardId === card.id
? {
transform: 'scale(1.1)',
boxShadow: '0 6px 20px rgba(25, 118, 210, 0.3)',
}
: undefined
}
>
<div
dangerouslySetInnerHTML={{ __html: card.svgContent }}
className={css({
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
'& svg': {
width: '100%',
height: 'auto',
height: '100%',
display: 'block',
},
})}
/>
{state.numbersRevealed && (
<div
className={css({
textAlign: 'center',
marginTop: '0.5rem',
fontSize: 'lg',
position: 'absolute',
top: '5px',
right: '5px',
background: '#ffc107',
color: '#333',
borderRadius: '4px',
padding: '2px 8px',
fontSize: '14px',
fontWeight: 'bold',
color: 'gray.700',
})}
>
{card.number}
@@ -336,8 +364,14 @@ export function PlayingPhase() {
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '0.25rem',
flexWrap: 'wrap',
gap: '8px',
justifyContent: 'center',
alignItems: 'center',
padding: '15px',
background: 'rgba(255,255,255,0.7)',
borderRadius: '8px',
border: '2px dashed #2c5f76',
})}
>
{/* Insert button before first position */}
@@ -376,13 +410,14 @@ export function PlayingPhase() {
const isEmpty = card === null
return (
<div key={index}>
<>
{/* Position slot */}
<div
key={`slot-${index}`}
onClick={() => handleSlotClick(index)}
className={css({
width: '90px',
height: '110px',
width: '140px',
height: '160px',
padding: '0.5rem',
borderRadius: '8px',
border: '2px solid',
@@ -423,10 +458,16 @@ export function PlayingPhase() {
__html: card.svgContent,
}}
className={css({
width: '70px',
flex: 1,
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
'& svg': {
width: '100%',
height: 'auto',
height: '100%',
display: 'block',
},
})}
/>
@@ -457,6 +498,7 @@ export function PlayingPhase() {
{/* Insert button after this position */}
<button
key={`insert-${index + 1}`}
type="button"
onClick={() => handleInsertClick(index + 1)}
disabled={!selectedCardId}
@@ -472,7 +514,6 @@ export function PlayingPhase() {
cursor: selectedCardId ? 'pointer' : 'default',
opacity: selectedCardId ? 1 : 0.3,
transition: 'all 0.2s',
marginTop: '0.25rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -485,7 +526,7 @@ export function PlayingPhase() {
>
+
</button>
</div>
</>
)
})}
</div>

View File

@@ -31,7 +31,7 @@ function getAllGameConfigs() {
maxPlayers: gameDef.manifest.maxPlayers,
description: gameDef.manifest.description,
longDescription: gameDef.manifest.longDescription,
url: '/arcade/room', // Room page handles game selection through UI
url: '/arcade', // Arcade page handles game selection through UI
icon: gameDef.manifest.icon,
chips: gameDef.manifest.chips,
color: gameDef.manifest.color,

View File

@@ -80,7 +80,7 @@ export function AddPlayerButton({
})
// Close popover and navigate to room to choose game
setShowPopover(false)
router.push('/arcade/room')
router.push('/arcade')
},
onError: (error) => {
console.error('Failed to create room:', error)
@@ -111,7 +111,7 @@ export function AddPlayerButton({
}
// Close popover and navigate to room
setShowPopover(false)
router.push('/arcade/room')
router.push('/arcade')
},
}
)

View File

@@ -833,7 +833,7 @@ export function ModerationNotifications({
// Close the modal
onClose()
// Navigate to the room
router.push('/arcade/room')
router.push('/arcade')
} catch (error) {
console.error('Failed to join room:', error)
showError(

View File

@@ -68,7 +68,7 @@ export function PendingInvitations({ onInvitationChange, currentRoomId }: Pendin
// Join the room
await joinRoom({ roomId: invitation.roomId })
// Navigate to the room
router.push('/arcade/room')
router.push('/arcade')
// Refresh invitations
await fetchInvitations()
onInvitationChange?.()

View File

@@ -81,7 +81,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
session = await createArcadeSession({
userId,
gameName: room.gameName as GameName,
gameUrl: '/arcade/room',
gameUrl: '/arcade',
initialState,
activePlayers: roomPlayerIds, // Include all room members' active players
roomId: room.id,
@@ -173,7 +173,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
await createArcadeSession({
userId: data.userId,
gameName: 'matching',
gameUrl: '/arcade/room', // Room-based sessions use /arcade/room
gameUrl: '/arcade', // Room-based sessions use /arcade
initialState,
activePlayers,
roomId: room.id,

View File

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