Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
321d9aea10 | ||
|
|
92e1e62132 | ||
|
|
84d980bb24 | ||
|
|
892b377eb3 | ||
|
|
bc21095fa1 | ||
|
|
eb3b100056 | ||
|
|
276f6f0744 | ||
|
|
6d734f1d51 | ||
|
|
03b9b1228b | ||
|
|
10978e890b |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,3 +1,38 @@
|
||||
## [4.32.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.31.1...v4.32.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add dark mode styling and responsive scaling to abacus ([92e1e62](https://github.com/antialias/soroban-abacus-flashcards/commit/92e1e621321039206f65b3605f5797bbdc6beafc))
|
||||
|
||||
## [4.31.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.31.0...v4.31.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** use correct AbacusReact API with direct props ([892b377](https://github.com/antialias/soroban-abacus-flashcards/commit/892b377eb3bbd555dd2566bf58e946e9faa7b9f6))
|
||||
|
||||
## [4.31.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.30.0...v4.31.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** implement interactive slider for exploring kyu & dan ranks ([eb3b100](https://github.com/antialias/soroban-abacus-flashcards/commit/eb3b1000563536d4143ba1f4ec04e59e8dd2e608))
|
||||
|
||||
## [4.30.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.29.0...v4.30.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** create true horizontal slider with abacus visualizations ([6d734f1](https://github.com/antialias/soroban-abacus-flashcards/commit/6d734f1d51f5ba1367f55923e58bd977413d754e))
|
||||
|
||||
## [4.29.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.28.0...v4.29.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** replace kyu grid with interactive slider and abacus visualizations ([10978e8](https://github.com/antialias/soroban-abacus-flashcards/commit/10978e890beee65dea78ddcce52cfe5315d58063))
|
||||
|
||||
## [4.28.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.27.0...v4.28.0) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
@@ -121,6 +121,31 @@ className="bg-blue-200 border-gray-300 text-brand-600"
|
||||
|
||||
See `.claude/GAME_THEMES.md` for standardized color theme usage in arcade games.
|
||||
|
||||
## Abacus Visualizations
|
||||
|
||||
**CRITICAL: This project uses @soroban/abacus-react for all abacus visualizations.**
|
||||
|
||||
- All abacus displays MUST use components from `@soroban/abacus-react`
|
||||
- Package location: `packages/abacus-react`
|
||||
- Main components: `AbacusReact`, `useAbacusConfig`, `useAbacusDisplay`
|
||||
- DO NOT create custom abacus visualizations
|
||||
- DO NOT manually draw abacus columns, beads, or bars
|
||||
|
||||
**Common Mistakes to Avoid:**
|
||||
- ❌ Don't create custom abacus components or SVGs
|
||||
- ❌ Don't manually render abacus beads or columns
|
||||
- ✅ Always use `AbacusReact` from `@soroban/abacus-react`
|
||||
- ✅ Use `useAbacusConfig` for abacus configuration
|
||||
- ✅ Use `useAbacusDisplay` for reading abacus state
|
||||
|
||||
**Example Usage:**
|
||||
```typescript
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
|
||||
const config = useAbacusConfig({ columns: 5 })
|
||||
<AbacusReact config={config} initialNumber={123} />
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
### @soroban/abacus-react TypeScript Module Resolution
|
||||
|
||||
@@ -1,179 +1,135 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { container, grid, stack } from '../../../styled-system/patterns'
|
||||
import { container, stack } from '../../../styled-system/patterns'
|
||||
|
||||
// Kyu level data from the Japan Abacus Federation
|
||||
const kyuLevels = [
|
||||
// Combine all levels into one array for the slider
|
||||
const allLevels = [
|
||||
{ level: '10th Kyu', emoji: '🧒', color: 'green', digits: 2, type: 'kyu' as const },
|
||||
{ level: '9th Kyu', emoji: '🧒', color: 'green', digits: 2, type: 'kyu' as const },
|
||||
{ level: '8th Kyu', emoji: '🧒', color: 'green', digits: 3, type: 'kyu' as const },
|
||||
{ level: '7th Kyu', emoji: '🧒', color: 'green', digits: 4, type: 'kyu' as const },
|
||||
{ level: '6th Kyu', emoji: '🧑', color: 'blue', digits: 5, type: 'kyu' as const },
|
||||
{ level: '5th Kyu', emoji: '🧑', color: 'blue', digits: 6, type: 'kyu' as const },
|
||||
{ level: '4th Kyu', emoji: '🧑', color: 'blue', digits: 7, type: 'kyu' as const },
|
||||
{ level: '3rd Kyu', emoji: '🧔', color: 'violet', digits: 8, type: 'kyu' as const },
|
||||
{ level: '2nd Kyu', emoji: '🧔', color: 'violet', digits: 9, type: 'kyu' as const },
|
||||
{ level: '1st Kyu', emoji: '🧔', color: 'violet', digits: 10, type: 'kyu' as const },
|
||||
{
|
||||
level: '10th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '30%',
|
||||
points: '60/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '2-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '2-digit', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'No division at this level',
|
||||
level: 'Pre-1st Dan',
|
||||
name: 'Jun-Shodan',
|
||||
minScore: 90,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '9th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '60%',
|
||||
points: '120/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '2-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '2-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '1×1', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Introduces multiplication',
|
||||
level: '1st Dan',
|
||||
name: 'Shodan',
|
||||
minScore: 100,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '8th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '60%',
|
||||
points: '120/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '3-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '3-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '2×1', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '2÷1', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Introduces division',
|
||||
level: '2nd Dan',
|
||||
name: 'Nidan',
|
||||
minScore: 120,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '7th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '60%',
|
||||
points: '120/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '4-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '4-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '3×1', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '3÷1', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'All four operations',
|
||||
level: '3rd Dan',
|
||||
name: 'Sandan',
|
||||
minScore: 140,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '6th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
duration: '30 min',
|
||||
passThreshold: '70%',
|
||||
points: '210/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '5-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '5-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '4×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '5÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Longer exam time',
|
||||
level: '4th Dan',
|
||||
name: 'Yondan',
|
||||
minScore: 160,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '5th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
duration: '30 min',
|
||||
passThreshold: '70%',
|
||||
points: '210/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '6-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '6-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '5×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '6÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Mid-level proficiency',
|
||||
level: '5th Dan',
|
||||
name: 'Godan',
|
||||
minScore: 180,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '4th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
duration: '30 min',
|
||||
passThreshold: '70%',
|
||||
points: '210/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '7-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '7-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '6×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '7÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Advanced intermediate',
|
||||
level: '6th Dan',
|
||||
name: 'Rokudan',
|
||||
minScore: 200,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '3rd Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
duration: '30 min',
|
||||
passThreshold: '80%',
|
||||
points: '240/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '8-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '8-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '7×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '8÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Higher pass threshold (80%)',
|
||||
level: '7th Dan',
|
||||
name: 'Nanadan',
|
||||
minScore: 220,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '2nd Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
duration: '30 min',
|
||||
passThreshold: '80%',
|
||||
points: '240/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '9-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '9-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '8×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '9÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Near-mastery level',
|
||||
level: '8th Dan',
|
||||
name: 'Hachidan',
|
||||
minScore: 250,
|
||||
emoji: '🧝♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '1st Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
duration: '30 min',
|
||||
passThreshold: '80%',
|
||||
points: '240/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '10-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '10-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '9×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '10÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Highest Kyu level before Dan',
|
||||
level: '9th Dan',
|
||||
name: 'Kudan',
|
||||
minScore: 270,
|
||||
emoji: '🧝♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '10th Dan',
|
||||
name: 'Judan',
|
||||
minScore: 290,
|
||||
emoji: '👑',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
] as const
|
||||
|
||||
// Dan level data - score-based ranking system
|
||||
const danLevels = [
|
||||
{ level: 'Pre-1st Dan', name: 'Jun-Shodan', minScore: 90, emoji: '🧙' },
|
||||
{ level: '1st Dan', name: 'Shodan', minScore: 100, emoji: '🧙' },
|
||||
{ level: '2nd Dan', name: 'Nidan', minScore: 120, emoji: '🧙♂️' },
|
||||
{ level: '3rd Dan', name: 'Sandan', minScore: 140, emoji: '🧙♂️' },
|
||||
{ level: '4th Dan', name: 'Yondan', minScore: 160, emoji: '🧙♀️' },
|
||||
{ level: '5th Dan', name: 'Godan', minScore: 180, emoji: '🧙♀️' },
|
||||
{ level: '6th Dan', name: 'Rokudan', minScore: 200, emoji: '🧝' },
|
||||
{ level: '7th Dan', name: 'Nanadan', minScore: 220, emoji: '🧝' },
|
||||
{ level: '8th Dan', name: 'Hachidan', minScore: 250, emoji: '🧝♂️' },
|
||||
{ level: '9th Dan', name: 'Kudan', minScore: 270, emoji: '🧝♀️' },
|
||||
{ level: '10th Dan', name: 'Judan', minScore: 290, emoji: '👑' },
|
||||
] as const
|
||||
|
||||
export default function LevelsPage() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const currentLevel = allLevels[currentIndex]
|
||||
|
||||
// Calculate scale factor based on number of columns to fit the page
|
||||
// Smaller scale for more columns (Dan levels with 30 columns)
|
||||
const scaleFactor = Math.min(2.5, 20 / currentLevel.digits)
|
||||
|
||||
return (
|
||||
<PageWithNav navTitle="Kyu & Dan Levels" navEmoji="📊">
|
||||
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
|
||||
<div className={css({ bg: 'gray.900', minHeight: '100vh', pb: '12' })}>
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className={css({
|
||||
@@ -185,7 +141,6 @@ export default function LevelsPage() {
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
@@ -199,7 +154,6 @@ export default function LevelsPage() {
|
||||
|
||||
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
|
||||
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
|
||||
{/* Main headline */}
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '3xl', md: '5xl', lg: '6xl' },
|
||||
@@ -214,7 +168,6 @@ export default function LevelsPage() {
|
||||
Understanding Kyu & Dan Levels
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'lg', md: 'xl' },
|
||||
@@ -225,370 +178,194 @@ export default function LevelsPage() {
|
||||
lineHeight: '1.6',
|
||||
})}
|
||||
>
|
||||
Learn about the official Japanese soroban ranking system used by the Japan Abacus
|
||||
Federation
|
||||
Slide through the complete progression from beginner to master
|
||||
</p>
|
||||
|
||||
{/* Journey progression emojis */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '4',
|
||||
mb: '8',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
{[
|
||||
{ emoji: '🧒', label: '10 Kyu' },
|
||||
{ emoji: '→', label: '', isArrow: true },
|
||||
{ emoji: '🧑', label: '5 Kyu' },
|
||||
{ emoji: '→', label: '', isArrow: true },
|
||||
{ emoji: '🧔', label: '1 Kyu' },
|
||||
{ emoji: '→', label: '', isArrow: true },
|
||||
{ emoji: '🧙', label: 'Dan' },
|
||||
].map((step, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
opacity: step.isArrow ? 0.5 : 1,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: step.isArrow ? 'xl' : '4xl',
|
||||
color: step.isArrow ? 'gray.500' : 'yellow.400',
|
||||
})}
|
||||
>
|
||||
{step.emoji}
|
||||
</div>
|
||||
{step.label && (
|
||||
<div className={css({ fontSize: 'xs', color: 'gray.400' })}>{step.label}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content container */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
|
||||
{/* Kyu Levels Section */}
|
||||
{/* Main content */}
|
||||
<div className={container({ maxW: '6xl', 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: '4',
|
||||
})}
|
||||
>
|
||||
Kyu Levels (10th to 1st)
|
||||
</h2>
|
||||
<p className={css({ color: 'gray.400', fontSize: 'md', mb: '8' })}>
|
||||
Progress from beginner to advanced mastery
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Kyu Level Cards */}
|
||||
<div className={grid({ columns: { base: 1, md: 2, lg: 3 }, gap: '6' })}>
|
||||
{kyuLevels.map((kyu, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '1px solid',
|
||||
borderColor:
|
||||
kyu.color === 'green'
|
||||
? 'green.700'
|
||||
: kyu.color === 'blue'
|
||||
? 'blue.700'
|
||||
: 'violet.700',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
bg: 'rgba(0, 0, 0, 0.5)',
|
||||
borderColor:
|
||||
kyu.color === 'green'
|
||||
? 'green.500'
|
||||
: kyu.color === 'blue'
|
||||
? 'blue.500'
|
||||
: 'violet.500',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Card Header */}
|
||||
<div
|
||||
className={css({ display: 'flex', alignItems: 'center', gap: '3', mb: '4' })}
|
||||
>
|
||||
<div className={css({ fontSize: '3xl' })}>{kyu.emoji}</div>
|
||||
<div>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
kyu.color === 'green'
|
||||
? 'green.400'
|
||||
: kyu.color === 'blue'
|
||||
? 'blue.400'
|
||||
: 'violet.400',
|
||||
})}
|
||||
>
|
||||
{kyu.level}
|
||||
</h3>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.400' })}>{kyu.notes}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exam Details */}
|
||||
<div className={stack({ gap: '3' })}>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.400' })}>Duration:</span>
|
||||
<span className={css({ color: 'white', fontWeight: '500' })}>
|
||||
{kyu.duration}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.400' })}>Pass Threshold:</span>
|
||||
<span className={css({ color: 'amber.400', fontWeight: '600' })}>
|
||||
{kyu.passThreshold}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.400' })}>Points Needed:</span>
|
||||
<span className={css({ color: 'white', fontWeight: '500' })}>
|
||||
{kyu.points}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sections */}
|
||||
<div
|
||||
className={css({
|
||||
mt: '4',
|
||||
pt: '4',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
mb: '2',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
})}
|
||||
>
|
||||
Problem Types
|
||||
</div>
|
||||
{kyu.sections.map((section, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
py: '1',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.300' })}>{section.name}</span>
|
||||
<span className={css({ color: 'gray.400', fontSize: 'xs' })}>
|
||||
{section.digits}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dan Levels Section */}
|
||||
<section className={stack({ gap: '8', mt: '16' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
Dan Levels (Master Ranks)
|
||||
</h2>
|
||||
<p className={css({ color: 'gray.400', fontSize: 'md', mb: '8' })}>
|
||||
Score-based ranking system for master-level practitioners
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Exam Requirements Box */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(251, 191, 36, 0.1)',
|
||||
border: '1px solid',
|
||||
borderColor: 'amber.700',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
mb: '6',
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: 'amber.400',
|
||||
mb: '3',
|
||||
})}
|
||||
>
|
||||
Dan Exam Requirements
|
||||
</h3>
|
||||
<div className={stack({ gap: '2' })}>
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
<strong>Duration:</strong> 30 minutes
|
||||
</div>
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
<strong>Problem Complexity:</strong> 3× that of 1st Kyu
|
||||
</div>
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.300', mt: '2' })}>
|
||||
• Addition/Subtraction: 30-digit numbers
|
||||
</div>
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
• Multiplication: 27×6 digits
|
||||
</div>
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
• Division: 30÷6 digits
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dan Ladder Visualization */}
|
||||
{/* Current Level Display */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '1px solid',
|
||||
borderColor: 'amber.700',
|
||||
border: '2px solid',
|
||||
borderColor:
|
||||
currentLevel.color === 'green'
|
||||
? 'green.500'
|
||||
: currentLevel.color === 'blue'
|
||||
? 'blue.500'
|
||||
: currentLevel.color === 'violet'
|
||||
? 'violet.500'
|
||||
: 'amber.500',
|
||||
rounded: 'xl',
|
||||
p: { base: '6', md: '8' },
|
||||
position: 'relative',
|
||||
})}
|
||||
>
|
||||
<div className={stack({ gap: '0' })}>
|
||||
{danLevels
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((dan, index) => {
|
||||
const isTop = index === 0
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4',
|
||||
p: '3',
|
||||
borderBottom: !isTop ? '1px solid' : 'none',
|
||||
borderColor: 'gray.700',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
bg: isTop ? 'rgba(251, 191, 36, 0.1)' : 'rgba(251, 191, 36, 0.05)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Emoji */}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
minW: '12',
|
||||
textAlign: 'center',
|
||||
})}
|
||||
>
|
||||
{dan.emoji}
|
||||
</div>
|
||||
|
||||
{/* Level Info */}
|
||||
<div className={css({ flex: '1' })}>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'bold',
|
||||
color: isTop ? 'amber.300' : 'amber.400',
|
||||
})}
|
||||
>
|
||||
{dan.level}
|
||||
</div>
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.400' })}>
|
||||
{dan.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score */}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: { base: 'xl', md: '2xl' },
|
||||
fontWeight: 'bold',
|
||||
color: isTop ? 'amber.300' : 'white',
|
||||
minW: { base: '20', md: '24' },
|
||||
textAlign: 'right',
|
||||
})}
|
||||
>
|
||||
{dan.minScore}
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.400', ml: '1' })}>
|
||||
pts
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
{/* Level Info */}
|
||||
<div className={css({ textAlign: 'center', mb: '6' })}>
|
||||
<div className={css({ fontSize: '5xl', mb: '3' })}>{currentLevel.emoji}</div>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
currentLevel.color === 'green'
|
||||
? 'green.400'
|
||||
: currentLevel.color === 'blue'
|
||||
? 'blue.400'
|
||||
: currentLevel.color === 'violet'
|
||||
? 'violet.400'
|
||||
: 'amber.400',
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
{currentLevel.level}
|
||||
</h2>
|
||||
{'name' in currentLevel && (
|
||||
<div className={css({ fontSize: 'md', color: 'gray.400', mb: '1' })}>
|
||||
{currentLevel.name}
|
||||
</div>
|
||||
)}
|
||||
{'minScore' in currentLevel && (
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.500' })}>
|
||||
Minimum Score: {currentLevel.minScore} points
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
{/* Abacus Display */}
|
||||
<div
|
||||
className={css({
|
||||
mt: '6',
|
||||
pt: '6',
|
||||
borderTop: '1px solid',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mb: '6',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
fontSize: 'sm',
|
||||
color: 'gray.400',
|
||||
textAlign: 'center',
|
||||
overflowX: 'auto',
|
||||
})}
|
||||
>
|
||||
Ranks are awarded based on total score achieved on the Dan-level exam
|
||||
<AbacusReact
|
||||
value={0}
|
||||
columns={currentLevel.digits}
|
||||
scaleFactor={scaleFactor}
|
||||
showNumbers={true}
|
||||
customStyles={{
|
||||
reckoningBar: { stroke: '#9ca3af', strokeWidth: 3 },
|
||||
columnPosts: { stroke: '#6b7280' },
|
||||
numerals: { color: '#d1d5db', fontSize: '14px', fontWeight: 'normal' },
|
||||
heavenBeads: { fill: '#60a5fa' },
|
||||
earthBeads: { fill: '#34d399' },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Digit Count */}
|
||||
<div className={css({ textAlign: 'center', color: 'gray.400', fontSize: 'sm' })}>
|
||||
Requires mastery of <strong>{currentLevel.digits}-digit</strong> calculations
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Additional Information Section */}
|
||||
<section className={stack({ gap: '8', mt: '16', pb: '12' })}>
|
||||
{/* Slider Control */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
})}
|
||||
>
|
||||
<div className={css({ mb: '4', textAlign: 'center' })}>
|
||||
<h3
|
||||
className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}
|
||||
>
|
||||
Explore All Levels
|
||||
</h3>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.400' })}>
|
||||
Drag the slider to see each rank
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Range Slider */}
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={allLevels.length - 1}
|
||||
value={currentIndex}
|
||||
onChange={(e) => setCurrentIndex(Number(e.target.value))}
|
||||
className={css({
|
||||
w: '100%',
|
||||
h: '2',
|
||||
bg: 'gray.700',
|
||||
rounded: 'full',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Level Markers */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
mt: '4',
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
})}
|
||||
>
|
||||
<span>10th Kyu</span>
|
||||
<span>1st Kyu</span>
|
||||
<span>10th Dan</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6',
|
||||
justifyContent: 'center',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'green.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Beginner (10-7 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'blue.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Intermediate (6-4 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'violet.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Advanced (3-1 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'amber.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Master (Dan ranks)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
@@ -616,26 +393,11 @@ export default function LevelsPage() {
|
||||
(10th Dan), used internationally for soroban proficiency assessment.
|
||||
</p>
|
||||
<p className={css({ color: 'gray.300', lineHeight: '1.6' })}>
|
||||
The system is designed to gradually increase in difficulty, ensuring students
|
||||
build a solid foundation before advancing. Each level requires mastery of
|
||||
increasingly complex calculations, from simple 2-digit operations at 10th Kyu to
|
||||
30-digit calculations at Dan level.
|
||||
The system is designed to gradually increase in difficulty. Kyu levels progress
|
||||
from 2-digit calculations at 10th Kyu to 10-digit calculations at 1st Kyu. Dan
|
||||
levels all require mastery of 30-digit calculations, with ranks awarded based on
|
||||
exam scores.
|
||||
</p>
|
||||
<div
|
||||
className={css({
|
||||
mt: '4',
|
||||
pt: '4',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
fontSize: 'sm',
|
||||
color: 'gray.400',
|
||||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
Note: This page provides information about the official Japanese ranking system
|
||||
for educational purposes. This application does not administer official
|
||||
examinations or certifications.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.28.0",
|
||||
"version": "4.32.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user