feat(levels): add kyu level details display with English translations

Add comprehensive exam requirements for each Kyu level displayed on
the left side of the abacus pane:

- Create kyuLevelDetails data file with English translations
- Display level details only for Kyu levels (hidden for Dan)
- Implement responsive font sizing based on abacus size
- Center abacus for Dan levels, right-align for Kyu
- Format details in clean, readable layout with bullet points

Details include:
- Addition/Subtraction requirements (rows, characters)
- Multiplication/Division requirements (digit counts, problem counts)
- Exam time limits and passing scores

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-10-20 11:14:03 -05:00
parent 28834e8a3e
commit c650ffa193
2 changed files with 227 additions and 15 deletions

View File

@@ -7,6 +7,7 @@ import { AbacusReact, StandaloneBead } from '@soroban/abacus-react'
import { PageWithNav } from '@/components/PageWithNav'
import { css } from '../../../styled-system/css'
import { container, stack } from '../../../styled-system/patterns'
import { kyuLevelDetails } from '@/data/kyuLevelDetails'
// Combine all levels into one array for the slider
const allLevels = [
@@ -181,6 +182,16 @@ const allLevels = [
},
] as const
// Helper function to map level names to kyuLevelDetails keys
function getLevelDetailsKey(levelName: string): string | null {
// Convert "10th Kyu" → "10-kyu", "3rd Kyu" → "3-kyu", etc.
const match = levelName.match(/^(\d+)(?:st|nd|rd|th)\s+Kyu$/)
if (match) {
return `${match[1]}-kyu`
}
return null
}
export default function LevelsPage() {
const [currentIndex, setCurrentIndex] = useState(0)
const [isHovering, setIsHovering] = useState(false)
@@ -576,12 +587,11 @@ export default function LevelsPage() {
</div>
</div>
{/* Abacus Display */}
{/* Abacus Display with Level Details */}
<div
className={css({
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
gap: '4',
p: '6',
bg: 'rgba(0, 0, 0, 0.3)',
rounded: 'lg',
@@ -591,19 +601,74 @@ export default function LevelsPage() {
flex: 1,
})}
>
<animated.div
style={{
transform: animatedProps.scaleFactor.to((s) => `scale(${s / scaleFactor})`),
}}
{/* Level Details (only for Kyu levels) */}
{currentLevel.type === 'kyu' &&
(() => {
const detailsKey = getLevelDetailsKey(currentLevel.level)
const detailsText = detailsKey
? kyuLevelDetails[detailsKey as keyof typeof kyuLevelDetails]
: null
// Calculate responsive font size based on digits
// More digits = larger abacus = less space for details
const getFontSize = () => {
if (currentLevel.digits <= 3) return 'sm' // 10th-8th Kyu
if (currentLevel.digits <= 6) return 'xs' // 7th-5th Kyu
return '2xs' // 4th-1st Kyu
}
return detailsText ? (
<div
className={css({
flex: '0 0 auto',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
pr: '4',
borderRight: '1px solid',
borderColor: 'gray.600',
maxW: '280px',
})}
>
<pre
className={css({
fontFamily: 'mono',
fontSize: getFontSize(),
color: 'gray.300',
lineHeight: '1.5',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
})}
>
{detailsText}
</pre>
</div>
) : null
})()}
{/* Abacus (right-aligned for Kyu, centered for Dan) */}
<div
className={css({
display: 'flex',
justifyContent: currentLevel.type === 'kyu' ? 'flex-end' : 'center',
alignItems: 'center',
flex: 1,
})}
>
<AbacusReact
value={displayValue}
columns={currentLevel.digits}
scaleFactor={scaleFactor}
showNumbers={true}
customStyles={darkStyles}
/>
</animated.div>
<animated.div
style={{
transform: animatedProps.scaleFactor.to((s) => `scale(${s / scaleFactor})`),
}}
>
<AbacusReact
value={displayValue}
columns={currentLevel.digits}
scaleFactor={scaleFactor}
showNumbers={true}
customStyles={darkStyles}
/>
</animated.div>
</div>
</div>
{/* Digit Count */}

View File

@@ -0,0 +1,147 @@
/**
* Detailed requirements for each Kyu level in the Soroban certification system
* Source: shuzan.jp
*/
export const kyuLevelDetails = {
'10-kyu': `+ / :
• 2-digit, 5 rows, 10 chars
×:
• 3 digits total (20 problems)
Exam: 20 min
Pass: ≥60/200 points`,
'9-kyu': `+ / :
• 2-digit, 5 rows, 10 chars
×:
• 3 digits total (20 problems)
Exam: 20 min
Pass: ≥120/200 points`,
'8-kyu': `+ / :
• 2-digit, 8 rows, 16 chars
×:
• 4 digits total (10 problems)
÷:
• 3 digits total (10 problems)
Exam: 20 min | Pass: ≥120/200`,
'7-kyu': `+ / :
• 2-digit, 10 rows, 20 chars
×:
• 4 digits total (10 problems)
÷:
• 4 digits total (10 problems)
Exam: 20 min | Pass: ≥120/200`,
'6-kyu': `+ / :
• 10 rows, 30 chars
×:
• 5 digits total (20 problems)
÷:
• 4 digits total (20 problems)
Exam: 30 min | Pass: ≥210/300`,
'5-kyu': `+ / :
• 10 rows, 40 chars
×:
• 6 digits total (20 problems)
÷:
• 5 digits total (20 problems)
Exam: 30 min | Pass: ≥210/300`,
'4-kyu': `+ / :
• 10 rows, 50 chars
×:
• 7 digits total (20 problems)
÷:
• 6 digits total (20 problems)
Exam: 30 min | Pass: ≥210/300`,
'Pre-3-kyu': `+ / :
• 10 rows, 50-60 chars (10 problems)
×:
• 7 digits total (20 problems)
÷:
• 6 digits total (20 problems)
Exam: 30 min | Pass: ≥240/300`,
'3-kyu': `+ / :
• 10 rows, 60 chars
×:
• 7 digits total (20 problems)
÷:
• 6 digits total (20 problems)
Exam: 30 min | Pass: ≥240/300`,
'Pre-2-kyu': `+ / :
• 10 rows, 70 chars
×:
• 8 digits total (20 problems)
÷:
• 7 digits total (20 problems)
Exam: 30 min | Pass: ≥240/300`,
'2-kyu': `+ / :
• 10 rows, 80 chars
×:
• 9 digits total (20 problems)
÷:
• 8 digits total (20 problems)
Exam: 30 min | Pass: ≥240/300`,
'Pre-1-kyu': `+ / :
• 10 rows, 90 chars
×:
• 10 digits total (20 problems)
÷:
• 9 digits total (20 problems)
Exam: 30 min | Pass: ≥240/300`,
'1-kyu': `+ / :
• 10 rows, 100 chars
×:
• 11 digits total (20 problems)
÷:
• 10 digits total (20 problems)
Exam: 30 min | Pass: ≥240/300`,
} as const
export type KyuLevel = keyof typeof kyuLevelDetails