Compare commits
7 Commits
abacus-rea
...
abacus-rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c0bf7b0f7 | ||
|
|
ae4b71b986 | ||
|
|
bf046c999b | ||
|
|
183706dade | ||
|
|
54ff20c755 | ||
|
|
7a3e34b4fa | ||
|
|
a935e5aed8 |
@@ -164,7 +164,11 @@
|
||||
"Bash(open http://localhost:3003/arcade/matching)",
|
||||
"Bash(open http://localhost:3000)",
|
||||
"Bash(open http://localhost:3003/games/memory-quiz)",
|
||||
"Bash(open http://localhost:3001)"
|
||||
"Bash(open http://localhost:3001)",
|
||||
"Bash(open http://localhost:3001/arcade)",
|
||||
"Bash(open http://localhost:6006)",
|
||||
"Bash(open http://localhost:3002/games/matching)",
|
||||
"Bash(open http://localhost:3002/create)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
4
.github/workflows/publish-abacus-react.yml
vendored
4
.github/workflows/publish-abacus-react.yml
vendored
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Configure npm for GitHub Packages
|
||||
working-directory: packages/abacus-react
|
||||
run: |
|
||||
echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" > .npmrc
|
||||
echo "//npm.pkg.github.com/:_authToken=${{ secrets.NPM_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" > .npmrc
|
||||
echo "@soroban:registry=https://npm.pkg.github.com" >> .npmrc
|
||||
echo "registry=https://npm.pkg.github.com" >> .npmrc
|
||||
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
|
||||
# Set authentication and registry for GitHub Packages
|
||||
echo "Publishing with explicit authentication..."
|
||||
NPM_CONFIG_USERCONFIG=.npmrc NODE_AUTH_TOKEN="${{ secrets.GITHUB_TOKEN }}" npm publish --registry=https://npm.pkg.github.com
|
||||
NPM_CONFIG_USERCONFIG=.npmrc NODE_AUTH_TOKEN="${{ secrets.NPM_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}" npm publish --registry=https://npm.pkg.github.com
|
||||
else
|
||||
echo "No new abacus-react version tag found, skipping publish"
|
||||
fi
|
||||
@@ -48,6 +48,7 @@
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"emojibase-data": "^16.0.3",
|
||||
"lucide-react": "^0.294.0",
|
||||
"make-plural": "^7.4.0",
|
||||
"next": "^14.2.32",
|
||||
"python-bridge": "^1.1.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function CreateNav() {
|
||||
return null
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function DefaultNav() {
|
||||
return null // No navigation content for routes without specific @nav slots
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export default function MatchingNav() {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧩 Memory Pairs
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export default function MemoryQuizNav() {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧠 Memory Lightning
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function GamesNav() {
|
||||
return null
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function GuideNav() {
|
||||
return null
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function HomeNav() {
|
||||
return null
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { container, stack, hstack, grid } from '../../../styled-system/patterns'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import Link from 'next/link'
|
||||
import { ConfigurationForm } from '@/components/ConfigurationForm'
|
||||
import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationFormWithoutGenerate'
|
||||
@@ -185,7 +186,8 @@ export default function CreatePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
|
||||
<PageWithNav navTitle="Create Flashcards" navEmoji="✨">
|
||||
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
|
||||
@@ -378,6 +380,7 @@ export default function CreatePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useGameMode } from '../../../../contexts/GameModeContext'
|
||||
import { MemoryGrid } from './MemoryGrid'
|
||||
import { PlayerStatusBar } from './PlayerStatusBar'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { pluralizeWord } from '../../../../utils/pluralization'
|
||||
|
||||
export function GamePhase() {
|
||||
const { state, resetGame, activePlayers } = useMemoryPairs()
|
||||
@@ -54,7 +55,7 @@ export function GamePhase() {
|
||||
{state.gameMode === 'multiplayer' && (
|
||||
<>
|
||||
<span className={css({ color: 'gray.400' })}>•</span>
|
||||
<span>⚔️ {activePlayers.length}P</span>
|
||||
<span>⚔️ {activePlayers.length}{pluralizeWord(activePlayers.length, 'P')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useMemoryPairs } from '../context/MemoryPairsContext'
|
||||
import { GameCard } from './GameCard'
|
||||
import { getGridConfiguration } from '../utils/cardGeneration'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { gamePlurals } from '../../../../utils/pluralization'
|
||||
|
||||
// Helper function to calculate optimal grid dimensions
|
||||
function calculateOptimalGrid(cards: number, aspectRatio: number, config: any) {
|
||||
@@ -102,37 +103,6 @@ export function MemoryGrid() {
|
||||
gap: { base: '12px', sm: '16px', md: '20px' }
|
||||
})}>
|
||||
|
||||
{/* Compact Game Progress */}
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: { base: '12px', sm: '16px', md: '24px' },
|
||||
padding: { base: '8px 12px', sm: '10px 16px', md: '12px 20px' },
|
||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.95), rgba(248,250,252,0.95))',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
|
||||
border: '1px solid rgba(255,255,255,0.9)',
|
||||
fontSize: { base: '14px', sm: '15px', md: '16px' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.700'
|
||||
})}>
|
||||
<span className={css({ color: 'blue.600' })}>
|
||||
{state.matchedPairs} matched
|
||||
</span>
|
||||
<span className={css({ color: 'gray.400' })}>•</span>
|
||||
<span className={css({ color: 'purple.600' })}>
|
||||
{state.moves} moves
|
||||
</span>
|
||||
{state.gameMode === 'single' && (
|
||||
<>
|
||||
<span className={css({ color: 'gray.400' })}>•</span>
|
||||
<span className={css({ color: 'green.600' })}>
|
||||
{Math.round((state.matchedPairs / state.totalPairs) * 100)}% complete
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cards Grid - Consistent r×c Layout */}
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { css } from '../../../../../styled-system/css'
|
||||
import { gamePlurals } from '../../../../utils/pluralization'
|
||||
|
||||
// Inject the celebration animations for Storybook
|
||||
const celebrationAnimations = `
|
||||
@@ -267,7 +268,7 @@ const MockPlayerCard = ({
|
||||
color: isCurrentPlayer ? playerColor : 'gray.500',
|
||||
fontWeight: isCurrentPlayer ? 'black' : 'semibold'
|
||||
})}>
|
||||
{score} pairs
|
||||
{gamePlurals.pair(score)}
|
||||
{isCurrentPlayer && (
|
||||
<span className={css({
|
||||
color: 'red.600',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css } from '../../../../../styled-system/css'
|
||||
import { useGameMode } from '../../../../contexts/GameModeContext'
|
||||
import { useUserProfile } from '../../../../contexts/UserProfileContext'
|
||||
import { useMemoryPairs } from '../context/MemoryPairsContext'
|
||||
import { gamePlurals } from '../../../../utils/pluralization'
|
||||
|
||||
interface PlayerStatusBarProps {
|
||||
className?: string
|
||||
@@ -38,82 +39,44 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
|
||||
}
|
||||
|
||||
if (activePlayers.length <= 1) {
|
||||
// Epic single player mode
|
||||
// Simple single player indicator
|
||||
return (
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea, #764ba2)',
|
||||
rounded: '2xl',
|
||||
p: { base: '4', md: '6' },
|
||||
border: '3px solid',
|
||||
borderColor: 'purple.300',
|
||||
mb: { base: '3', md: '4' },
|
||||
boxShadow: '0 0 0 2px white, 0 0 0 6px rgba(102, 126, 234, 0.4), 0 12px 32px rgba(0,0,0,0.2)',
|
||||
animation: 'gentle-pulse 3s ease-in-out infinite',
|
||||
position: 'relative'
|
||||
}, className)}>
|
||||
{/* Subtle glow effect */}
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(45deg, transparent 40%, rgba(255,255,255,0.1) 50%, transparent 60%)',
|
||||
pointerEvents: 'none'
|
||||
})} />
|
||||
|
||||
background: 'white',
|
||||
rounded: 'lg',
|
||||
p: { base: '2', md: '3' },
|
||||
border: '2px solid',
|
||||
borderColor: 'blue.200',
|
||||
mb: { base: '2', md: '3' },
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
})}
|
||||
className={className}>
|
||||
<div className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: { base: '4', md: '6' },
|
||||
position: 'relative',
|
||||
zIndex: 2
|
||||
gap: { base: '2', md: '3' }
|
||||
})}>
|
||||
<div className={css({
|
||||
fontSize: { base: '3xl', md: '5xl' },
|
||||
animation: 'gentle-sway 2s ease-in-out infinite',
|
||||
textShadow: '0 0 20px currentColor',
|
||||
transform: 'scale(1.2)'
|
||||
fontSize: { base: 'xl', md: '2xl' }
|
||||
})}>
|
||||
{activePlayers[0]?.displayEmoji || '🚀'}
|
||||
</div>
|
||||
<div>
|
||||
<div className={css({
|
||||
fontSize: { base: 'lg', md: 'xl' },
|
||||
fontWeight: 'black',
|
||||
color: 'white',
|
||||
color: 'white',
|
||||
textShadow: '0 0 15px rgba(255,255,255,0.8)'
|
||||
})}>
|
||||
{activePlayers[0]?.displayName || 'Player 1'}
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: { base: 'sm', md: 'md' },
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontWeight: 'bold',
|
||||
color: 'rgba(255,255,255,0.9)'
|
||||
})}>
|
||||
Solo Challenge • {state.moves} moves
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Epic progress indicator */}
|
||||
<div className={css({
|
||||
background: 'linear-gradient(135deg, #ff6b6b, #ee5a24)',
|
||||
color: 'white',
|
||||
px: { base: '3', md: '4' },
|
||||
py: { base: '2', md: '3' },
|
||||
rounded: 'xl',
|
||||
fontSize: { base: 'md', md: 'lg' },
|
||||
fontWeight: 'black',
|
||||
boxShadow: '0 4px 15px rgba(238, 90, 36, 0.4)',
|
||||
animation: 'gentle-bounce 2s ease-in-out infinite',
|
||||
textShadow: '0 0 10px rgba(255,255,255,0.8)'
|
||||
fontSize: { base: 'sm', md: 'md' },
|
||||
fontWeight: 'bold',
|
||||
color: 'gray.700'
|
||||
})}>
|
||||
⚡{state.matchedPairs}/{state.totalPairs}⚡
|
||||
{activePlayers[0]?.displayName || 'Player 1'}
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: { base: 'xs', md: 'sm' },
|
||||
color: 'blue.600',
|
||||
fontWeight: 'medium'
|
||||
})}>
|
||||
{gamePlurals.pair(state.matchedPairs)} of {state.totalPairs} • {gamePlurals.move(state.moves)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,7 +92,8 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.200',
|
||||
mb: { base: '3', md: '4' }
|
||||
}, className)}>
|
||||
})}
|
||||
className={className}>
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: activePlayers.length <= 2
|
||||
@@ -151,8 +115,8 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: { base: '3', md: '4' },
|
||||
p: isCurrentPlayer ? { base: '4', md: '6' } : { base: '2', md: '3' },
|
||||
gap: { base: '2', md: '3' },
|
||||
p: isCurrentPlayer ? { base: '3', md: '4' } : { base: '2', md: '2' },
|
||||
rounded: isCurrentPlayer ? '2xl' : 'lg',
|
||||
background: isCurrentPlayer
|
||||
? `linear-gradient(135deg, ${player.color || '#3b82f6'}15, ${player.color || '#3b82f6'}25, ${player.color || '#3b82f6'}15)`
|
||||
@@ -216,7 +180,7 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
|
||||
|
||||
{/* Living, breathing player emoji */}
|
||||
<div className={css({
|
||||
fontSize: isCurrentPlayer ? { base: '3xl', md: '5xl' } : { base: 'lg', md: 'xl' },
|
||||
fontSize: isCurrentPlayer ? { base: '2xl', md: '3xl' } : { base: 'lg', md: 'xl' },
|
||||
flexShrink: 0,
|
||||
animation: isCurrentPlayer
|
||||
? 'float 3s ease-in-out infinite'
|
||||
@@ -253,7 +217,7 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
|
||||
fontWeight: isCurrentPlayer ? 'black' : 'semibold',
|
||||
animation: 'none'
|
||||
})}>
|
||||
{player.score} pairs
|
||||
{gamePlurals.pair(player.score)}
|
||||
{isCurrentPlayer && (
|
||||
<span className={css({
|
||||
color: 'red.600',
|
||||
@@ -281,44 +245,24 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Epic score display for current player */}
|
||||
{/* Simple score display for current player */}
|
||||
{isCurrentPlayer && (
|
||||
<div className={css({
|
||||
background: 'linear-gradient(135deg, #ff6b6b, #ee5a24)',
|
||||
background: 'blue.500',
|
||||
color: 'white',
|
||||
px: { base: '3', md: '4' },
|
||||
py: { base: '2', md: '3' },
|
||||
rounded: 'xl',
|
||||
fontSize: { base: 'lg', md: 'xl' },
|
||||
fontWeight: 'black',
|
||||
boxShadow: '0 4px 15px rgba(238, 90, 36, 0.4)',
|
||||
animation: 'super-bounce 1.5s ease-in-out infinite',
|
||||
textShadow: '0 0 10px rgba(255,255,255,0.8)'
|
||||
px: { base: '2', md: '3' },
|
||||
py: { base: '1', md: '2' },
|
||||
rounded: 'md',
|
||||
fontSize: { base: 'sm', md: 'md' },
|
||||
fontWeight: 'bold'
|
||||
})}>
|
||||
⚡{player.score}⚡
|
||||
{player.score}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Game progress */}
|
||||
<div className={css({
|
||||
mt: '3',
|
||||
pt: '2',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
<div className={css({
|
||||
fontSize: { base: 'xs', md: 'sm' },
|
||||
color: 'gray.600',
|
||||
fontWeight: 'medium'
|
||||
})}>
|
||||
{state.matchedPairs} of {state.totalPairs} pairs found • {state.moves} total moves
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { MemoryPairsProvider } from './context/MemoryPairsContext'
|
||||
import { MemoryPairsGame } from './components/MemoryPairsGame'
|
||||
|
||||
export default function MatchingPage() {
|
||||
return (
|
||||
<MemoryPairsProvider>
|
||||
<MemoryPairsGame />
|
||||
</MemoryPairsProvider>
|
||||
<PageWithNav navTitle="Memory Pairs" navEmoji="🧩">
|
||||
<MemoryPairsProvider>
|
||||
<MemoryPairsGame />
|
||||
</MemoryPairsProvider>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { css } from '../../../../styled-system/css'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { isPrefix } from '../../../lib/memory-quiz-utils'
|
||||
import { StandardGameLayout } from '../../../components/StandardGameLayout'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
|
||||
|
||||
interface QuizCard {
|
||||
@@ -1733,7 +1733,7 @@ export default function MemoryQuizPage() {
|
||||
}, [state.prefixAcceptanceTimeout])
|
||||
|
||||
return (
|
||||
<StandardGameLayout>
|
||||
<PageWithNav navTitle="Memory Lightning" navEmoji="🧠">
|
||||
<style dangerouslySetInnerHTML={{ __html: globalAnimations }} />
|
||||
|
||||
<div
|
||||
@@ -1741,14 +1741,16 @@ export default function MemoryQuizPage() {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto'
|
||||
overflow: 'auto',
|
||||
padding: '20px 8px',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #f8fafc, #e2e8f0)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
margin: '0 auto',
|
||||
padding: '0 8px',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
@@ -1800,6 +1802,6 @@ export default function MemoryQuizPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StandardGameLayout>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { grid } from '../../../styled-system/patterns'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { useUserProfile } from '../../contexts/UserProfileContext'
|
||||
import { useGameMode } from '../../contexts/GameModeContext'
|
||||
import { FullscreenProvider, useFullscreen } from '../../contexts/FullscreenContext'
|
||||
@@ -1158,7 +1159,11 @@ const globalAnimations = `
|
||||
`
|
||||
|
||||
export default function GamesPage() {
|
||||
return <GamesPageContent />
|
||||
return (
|
||||
<PageWithNav navTitle="Soroban Arcade" navEmoji="🕹️">
|
||||
<GamesPageContent />
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
|
||||
// Inject refined animations into the page
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { container, stack, hstack, grid } from '../../../styled-system/patterns'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { TypstSoroban } from '@/components/TypstSoroban'
|
||||
import { InteractiveAbacus } from '@/components/InteractiveAbacus'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
@@ -16,7 +17,8 @@ type TabType = 'reading' | 'arithmetic'
|
||||
export default function GuidePage() {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('reading')
|
||||
return (
|
||||
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
|
||||
<PageWithNav navTitle="Interactive Guide" navEmoji="📖">
|
||||
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className={css({
|
||||
@@ -1281,6 +1283,7 @@ function ArithmeticOperationsGuide() {
|
||||
Practice Arithmetic Operations →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Metadata, Viewport } from 'next'
|
||||
import './globals.css'
|
||||
import { ClientProviders } from '@/components/ClientProviders'
|
||||
import { AppNav } from '@/components/AppNav'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Soroban Flashcard Generator',
|
||||
@@ -17,16 +16,13 @@ export const viewport: Viewport = {
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
nav,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
nav: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<ClientProviders>
|
||||
<AppNav>{nav}</AppNav>
|
||||
{children}
|
||||
</ClientProviders>
|
||||
</body>
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, stack, hstack } from '../../styled-system/patterns'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className={css({ minHeight: '100vh', bg: 'gradient-to-br from-brand.50 to-brand.100' })}>
|
||||
<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' })}>
|
||||
@@ -109,7 +111,8 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react'
|
||||
import { headers } from 'next/headers'
|
||||
import { AppNavBar } from './AppNavBar'
|
||||
|
||||
interface AppNavProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function getNavContentForPath(pathname: string): React.ReactNode {
|
||||
// Route-based nav content - no lazy loading needed
|
||||
if (pathname === '/games/matching' || pathname.startsWith('/arcade') && pathname.includes('matching')) {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧩 Memory Pairs
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
|
||||
if (pathname === '/games/memory-quiz' || pathname.startsWith('/arcade') && pathname.includes('memory-quiz')) {
|
||||
return (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
🧠 Memory Lightning
|
||||
</h1>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function AppNav({ children }: AppNavProps) {
|
||||
const headersList = headers()
|
||||
const pathname = headersList.get('x-pathname') || ''
|
||||
|
||||
// Use @nav slot content if available, otherwise fall back to route-based detection
|
||||
const navContent = children || getNavContentForPath(pathname)
|
||||
|
||||
return <AppNavBar navSlot={navContent} />
|
||||
}
|
||||
33
apps/web/src/components/PageWithNav.tsx
Normal file
33
apps/web/src/components/PageWithNav.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import { AppNavBar } from './AppNavBar'
|
||||
|
||||
interface PageWithNavProps {
|
||||
navTitle?: string
|
||||
navEmoji?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageWithNav({ navTitle, navEmoji, children }: PageWithNavProps) {
|
||||
// Create nav content if title is provided
|
||||
const navContent = navTitle ? (
|
||||
<h1 style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
margin: 0
|
||||
}}>
|
||||
{navEmoji && `${navEmoji} `}{navTitle}
|
||||
</h1>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppNavBar navSlot={navContent} />
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
44
apps/web/src/utils/pluralization.ts
Normal file
44
apps/web/src/utils/pluralization.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { en } from 'make-plural'
|
||||
|
||||
// Use English pluralization rules from make-plural
|
||||
const plural = en
|
||||
|
||||
/**
|
||||
* Pluralize a word based on count using make-plural library
|
||||
* @param count - The number to base pluralization on
|
||||
* @param singular - The singular form of the word
|
||||
* @param plural - The plural form of the word (optional, will add 's' by default)
|
||||
* @returns The properly pluralized word
|
||||
*/
|
||||
export function pluralizeWord(count: number, singular: string, pluralForm?: string): string {
|
||||
const category = plural(count)
|
||||
if (category === 'one') {
|
||||
return singular
|
||||
}
|
||||
return pluralForm || (singular + 's')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a formatted string with count and properly pluralized word
|
||||
* @param count - The number to display
|
||||
* @param singular - The singular form of the word
|
||||
* @param plural - The plural form of the word (optional)
|
||||
* @returns Formatted string like "1 pair" or "3 pairs"
|
||||
*/
|
||||
export function pluralizeCount(count: number, singular: string, pluralForm?: string): string {
|
||||
return `${count} ${pluralizeWord(count, singular, pluralForm)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Common game-specific pluralization helpers using make-plural
|
||||
*/
|
||||
export const gamePlurals = {
|
||||
pair: (count: number) => pluralizeCount(count, 'pair'),
|
||||
pairs: (count: number) => pluralizeCount(count, 'pair'),
|
||||
move: (count: number) => pluralizeCount(count, 'move'),
|
||||
moves: (count: number) => pluralizeCount(count, 'move'),
|
||||
match: (count: number) => pluralizeCount(count, 'match', 'matches'),
|
||||
matches: (count: number) => pluralizeCount(count, 'match', 'matches'),
|
||||
player: (count: number) => pluralizeCount(count, 'player'),
|
||||
players: (count: number) => pluralizeCount(count, 'player'),
|
||||
} as const
|
||||
@@ -1,3 +1,13 @@
|
||||
## [1.5.1](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.5.0...abacus-react-v1.5.1) (2025-09-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* resolve JSX parsing error with emoji in guide page ([bf046c9](https://github.com/antialias/soroban-abacus-flashcards/commit/bf046c999b51ba422284a139ebadde2c35187ac7))
|
||||
* resolve TypeScript errors in PlayerStatusBar component ([a935e5a](https://github.com/antialias/soroban-abacus-flashcards/commit/a935e5aed8c4584d21c8fc4359453b7dec494464))
|
||||
* restore navigation to all pages using PageWithNav ([183706d](https://github.com/antialias/soroban-abacus-flashcards/commit/183706dade12080a748b0c074d0bd71fb0471d7e))
|
||||
* update workflow to support Personal Access Token for GitHub Packages publishing auth ([ae4b71b](https://github.com/antialias/soroban-abacus-flashcards/commit/ae4b71b98655364887a729ef9d2b67b6a753d6e9))
|
||||
|
||||
# [1.5.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.4.0...abacus-react-v1.5.0) (2025-09-29)
|
||||
|
||||
|
||||
|
||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -139,6 +139,9 @@ importers:
|
||||
lucide-react:
|
||||
specifier: ^0.294.0
|
||||
version: 0.294.0(react@18.2.0)
|
||||
make-plural:
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0
|
||||
next:
|
||||
specifier: ^14.2.32
|
||||
version: 14.2.32(@babel/core@7.28.4)(@playwright/test@1.55.1)(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -12298,6 +12301,10 @@ packages:
|
||||
semver: 6.3.1
|
||||
dev: true
|
||||
|
||||
/make-plural@7.4.0:
|
||||
resolution: {integrity: sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==}
|
||||
dev: false
|
||||
|
||||
/makeerror@1.0.12:
|
||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user