Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c14012b97 | ||
|
|
c650ffa193 | ||
|
|
28834e8a3e | ||
|
|
8681b17340 | ||
|
|
d52cc608eb | ||
|
|
6f89d9e274 | ||
|
|
a8cc2bc0f0 | ||
|
|
9dff3e7b7b | ||
|
|
92fedb698d | ||
|
|
1e90d6c620 | ||
|
|
5fcb7925eb | ||
|
|
41eaed24fc |
42
CHANGELOG.md
42
CHANGELOG.md
@@ -1,3 +1,45 @@
|
||||
## [4.42.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.41.0...v4.42.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add kyu level details display with English translations ([c650ffa](https://github.com/antialias/soroban-abacus-flashcards/commit/c650ffa1935fe370d37190b2843c0deecdcce8e7))
|
||||
|
||||
## [4.41.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.40.1...v4.41.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** right-align abacus display ([8681b17](https://github.com/antialias/soroban-abacus-flashcards/commit/8681b17340e757cf04d17f884a780a251645bb33))
|
||||
|
||||
## [4.40.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.40.0...v4.40.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** increase animation speed to 10ms for 10th Dan ([6f89d9e](https://github.com/antialias/soroban-abacus-flashcards/commit/6f89d9e274082908fc090a9c0ba310f2cb06f014))
|
||||
|
||||
## [4.40.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.39.1...v4.40.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** progressive animation speed for Dan levels ([9dff3e7](https://github.com/antialias/soroban-abacus-flashcards/commit/9dff3e7b7b1ca46ea7f19a48135124b80c5182c0))
|
||||
|
||||
## [4.39.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.39.0...v4.39.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** improve slider tick spacing to use full width ([1e90d6c](https://github.com/antialias/soroban-abacus-flashcards/commit/1e90d6c6207f29084a8dc96ccfbb1013a1a62271))
|
||||
|
||||
## [4.39.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.38.1...v4.39.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add auto-advance slider with hover pause ([41eaed2](https://github.com/antialias/soroban-abacus-flashcards/commit/41eaed24fce510bab7fd03fa2e39e829b33a7346))
|
||||
|
||||
## [4.38.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.38.0...v4.38.1) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
@@ -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,9 +182,20 @@ 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)
|
||||
const [isPaneHovered, setIsPaneHovered] = useState(false)
|
||||
const currentLevel = allLevels[currentIndex]
|
||||
|
||||
// State for animated abacus digits
|
||||
@@ -197,8 +209,26 @@ export default function LevelsPage() {
|
||||
setAnimatedDigits(generateRandomDigits(currentLevel.digits))
|
||||
}, [currentLevel.digits])
|
||||
|
||||
// Animate abacus calculations every 0.5 seconds
|
||||
// Animate abacus calculations - speed increases with Dan level
|
||||
useEffect(() => {
|
||||
// Calculate animation speed based on level
|
||||
// Kyu levels: 500ms
|
||||
// Pre-1st Dan: 500ms
|
||||
// 1st-10th Dan: interpolate from 500ms to 10ms
|
||||
const getAnimationInterval = () => {
|
||||
if (currentIndex < 11) {
|
||||
// Kyu levels and Pre-1st Dan: constant 500ms
|
||||
return 500
|
||||
}
|
||||
// 1st Dan through 10th Dan: speed up from 500ms to 10ms
|
||||
// Index 11 (1st Dan) → 500ms
|
||||
// Index 20 (10th Dan) → 10ms
|
||||
const danProgress = (currentIndex - 11) / 9 // 0.0 to 1.0
|
||||
return 500 - danProgress * 490 // 500ms down to 10ms
|
||||
}
|
||||
|
||||
const intervalMs = getAnimationInterval()
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setAnimatedDigits((prev) => {
|
||||
const digits = prev.split('').map(Number)
|
||||
@@ -215,10 +245,24 @@ export default function LevelsPage() {
|
||||
|
||||
return digits.join('')
|
||||
})
|
||||
}, 500)
|
||||
}, intervalMs)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
}, [currentIndex])
|
||||
|
||||
// Auto-advance slider position every 3 seconds (unless pane is hovered)
|
||||
useEffect(() => {
|
||||
if (isPaneHovered) return // Don't auto-advance when mouse is over the pane
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentIndex((prev) => {
|
||||
// Cycle back to 0 when reaching the end
|
||||
return prev >= allLevels.length - 1 ? 0 : prev + 1
|
||||
})
|
||||
}, 3000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [isPaneHovered])
|
||||
|
||||
// Handle hover on slider track
|
||||
const handleSliderHover = (e: React.MouseEvent<HTMLSpanElement>) => {
|
||||
@@ -338,6 +382,8 @@ export default function LevelsPage() {
|
||||
<section className={stack({ gap: '8' })}>
|
||||
{/* Current Level Display */}
|
||||
<div
|
||||
onMouseEnter={() => setIsPaneHovered(true)}
|
||||
onMouseLeave={() => setIsPaneHovered(false)}
|
||||
className={css({
|
||||
bg: 'transparent',
|
||||
border: '2px solid',
|
||||
@@ -370,7 +416,7 @@ export default function LevelsPage() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'none',
|
||||
px: '90px', // Half of bead width (180px / 2)
|
||||
px: '0', // Use full width for tick spacing
|
||||
})}
|
||||
>
|
||||
<div
|
||||
@@ -541,12 +587,11 @@ export default function LevelsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Abacus Display */}
|
||||
{/* Abacus Display with Level Details */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '4',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
@@ -556,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 */}
|
||||
|
||||
147
apps/web/src/data/kyuLevelDetails.ts
Normal file
147
apps/web/src/data/kyuLevelDetails.ts
Normal 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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.38.1",
|
||||
"version": "4.42.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user