feat(homepage): add interactive learning panels with animated mini-tutorials

Transform "What You'll Learn" section into an interactive experience where
users can click skill panels to see corresponding tutorials with animated
abacus demonstrations.

Changes:
- Make skill panels clickable to switch between different tutorials
- Replace static emojis with animated MiniAbacus components for each skill
- Create skill-specific tutorials: basic numbers, friends of 5,
  multiplication, and mental calculation
- Add visual indication for selected panel (yellow glow effect)
- Update MiniAbacus component to cycle through custom value sequences
- Default to "Friends techniques" panel (Friends of 5 tutorial)

🤖 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 17:21:59 -05:00
parent 9ad35e65d3
commit 76d6f19d51
1 changed files with 164 additions and 101 deletions

View File

@ -13,20 +13,28 @@ import { css } from '../../styled-system/css'
import { container, grid, hstack, stack } from '../../styled-system/patterns' import { container, grid, hstack, stack } from '../../styled-system/patterns'
import { token } from '../../styled-system/tokens' import { token } from '../../styled-system/tokens'
// Mini abacus that cycles through random 3-digit numbers // Mini abacus that cycles through a sequence of values
function MiniAbacus() { function MiniAbacus({
const [currentValue, setCurrentValue] = useState(123) values,
columns = 3,
interval = 2500,
}: {
values: number[]
columns?: number
interval?: number
}) {
const [currentIndex, setCurrentIndex] = useState(0)
const appConfig = useAbacusConfig() const appConfig = useAbacusConfig()
useEffect(() => { useEffect(() => {
// Cycle through random 3-digit numbers every 2.5 seconds if (values.length === 0) return
const interval = setInterval(() => {
const randomNum = Math.floor(Math.random() * 1000) // 0-999
setCurrentValue(randomNum)
}, 2500)
return () => clearInterval(interval) const timer = setInterval(() => {
}, []) setCurrentIndex((prev) => (prev + 1) % values.length)
}, interval)
return () => clearInterval(timer)
}, [values, interval])
// Dark theme styles for the abacus // Dark theme styles for the abacus
const darkStyles = { const darkStyles = {
@ -54,8 +62,8 @@ function MiniAbacus() {
> >
<div className={css({ transform: 'scale(0.6)', transformOrigin: 'center center' })}> <div className={css({ transform: 'scale(0.6)', transformOrigin: 'center center' })}>
<AbacusReact <AbacusReact
value={currentValue} value={values[currentIndex] || 0}
columns={3} columns={columns}
beadShape={appConfig.beadShape} beadShape={appConfig.beadShape}
customStyles={darkStyles} customStyles={darkStyles}
/> />
@ -65,15 +73,46 @@ function MiniAbacus() {
} }
export default function HomePage() { export default function HomePage() {
// Extract just the "Friends of 5" step (2+3=5) for homepage demo const [selectedSkillIndex, setSelectedSkillIndex] = useState(1) // Default to "Friends techniques"
const fullTutorial = getTutorialForEditor() const fullTutorial = getTutorialForEditor()
const friendsOf5Tutorial = {
...fullTutorial, // Create different tutorials for each skill level
id: 'friends-of-5-demo', const skillTutorials = [
title: 'Friends of 5', // Skill 0: Read and set numbers (0-9999)
description: 'Learn the "Friends of 5" technique: adding 3 to make 5', {
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'), ...fullTutorial,
} id: 'read-numbers-demo',
title: 'Read and Set Numbers',
description: 'Master abacus number representation from zero to thousands',
steps: fullTutorial.steps.filter((step) => step.id.startsWith('basic-')),
},
// Skill 1: Friends techniques (5 = 2+3)
{
...fullTutorial,
id: 'friends-of-5-demo',
title: 'Friends of 5',
description: 'Add and subtract using complement pairs: 5 = 2+3',
steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
},
// Skill 2: Multiply & divide (12×34)
{
...fullTutorial,
id: 'multiply-demo',
title: 'Multiplication',
description: 'Fluent multi-digit calculations with advanced techniques',
steps: fullTutorial.steps.filter((step) => step.id.includes('complement')).slice(0, 3),
},
// Skill 3: Mental calculation (Speed math)
{
...fullTutorial,
id: 'mental-calc-demo',
title: 'Mental Calculation',
description: 'Visualize and compute without the physical tool (Anzan)',
steps: fullTutorial.steps.slice(-3),
},
]
const selectedTutorial = skillTutorials[selectedSkillIndex]
return ( return (
<HomeHeroProvider> <HomeHeroProvider>
@ -126,7 +165,8 @@ export default function HomePage() {
{/* Tutorial on the left */} {/* Tutorial on the left */}
<div className={css({ flex: '1' })}> <div className={css({ flex: '1' })}>
<TutorialPlayer <TutorialPlayer
tutorial={friendsOf5Tutorial} key={selectedTutorial.id}
tutorial={selectedTutorial}
isDebugMode={false} isDebugMode={false}
showDebugPanel={false} showDebugPanel={false}
hideNavigation={true} hideNavigation={true}
@ -158,122 +198,145 @@ export default function HomePage() {
<div className={stack({ gap: '5' })}> <div className={stack({ gap: '5' })}>
{[ {[
{ {
icon: '🔢',
title: 'Read and set numbers', title: 'Read and set numbers',
desc: 'Master abacus number representation from zero to thousands', desc: 'Master abacus number representation from zero to thousands',
example: '0-9999', example: '0-9999',
badge: 'Foundation', badge: 'Foundation',
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
columns: 3,
}, },
{ {
icon: '🤝',
title: 'Friends techniques', title: 'Friends techniques',
desc: 'Add and subtract using complement pairs and mental shortcuts', desc: 'Add and subtract using complement pairs and mental shortcuts',
example: '5 = 2+3', example: '5 = 2+3',
badge: 'Core', badge: 'Core',
values: [2, 5, 3],
columns: 1,
}, },
{ {
icon: '✖️➗',
title: 'Multiply & divide', title: 'Multiply & divide',
desc: 'Fluent multi-digit calculations with advanced techniques', desc: 'Fluent multi-digit calculations with advanced techniques',
example: '12×34', example: '12×34',
badge: 'Advanced', badge: 'Advanced',
values: [12, 24, 36, 48],
columns: 2,
}, },
{ {
icon: '🧠',
title: 'Mental calculation', title: 'Mental calculation',
desc: 'Visualize and compute without the physical tool (Anzan)', desc: 'Visualize and compute without the physical tool (Anzan)',
example: 'Speed math', example: 'Speed math',
badge: 'Expert', badge: 'Expert',
values: [7, 14, 21, 28, 35],
columns: 2,
}, },
].map((skill, i) => ( ].map((skill, i) => {
<div const isSelected = i === selectedSkillIndex
key={i} return (
className={css({ <div
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))', key={i}
borderRadius: 'xl', onClick={() => setSelectedSkillIndex(i)}
p: '4', className={css({
border: '1px solid', bg: isSelected
borderColor: 'rgba(255, 255, 255, 0.15)', ? 'linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(250, 204, 21, 0.08))'
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', : 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
transition: 'all 0.2s', borderRadius: 'xl',
_hover: { p: '4',
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))', border: '1px solid',
borderColor: 'rgba(255, 255, 255, 0.25)', borderColor: isSelected
transform: 'translateY(-2px)', ? 'rgba(250, 204, 21, 0.4)'
boxShadow: '0 6px 16px rgba(0, 0, 0, 0.4)', : 'rgba(255, 255, 255, 0.15)',
}, boxShadow: isSelected
})} ? '0 6px 16px rgba(250, 204, 21, 0.2)'
> : '0 4px 12px rgba(0, 0, 0, 0.3)',
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}> transition: 'all 0.2s',
<div cursor: 'pointer',
className={css({ _hover: {
fontSize: '3xl', bg: isSelected
width: '75px', ? 'linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(250, 204, 21, 0.12))'
height: '115px', : 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
display: 'flex', borderColor: isSelected
alignItems: 'center', ? 'rgba(250, 204, 21, 0.5)'
justifyContent: 'center', : 'rgba(255, 255, 255, 0.25)',
textAlign: 'center', transform: 'translateY(-2px)',
bg: 'rgba(255, 255, 255, 0.1)', boxShadow: isSelected
borderRadius: 'lg', ? '0 8px 20px rgba(250, 204, 21, 0.3)'
})} : '0 6px 16px rgba(0, 0, 0, 0.4)',
> },
{i === 0 ? <MiniAbacus /> : skill.icon} })}
</div> >
<div className={stack({ gap: '2', flex: '1' })}> <div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
<div className={hstack({ gap: '2', alignItems: 'center' })}> <div
<div className={css({
className={css({ fontSize: '3xl',
color: 'white', width: '75px',
fontSize: 'md', height: '115px',
fontWeight: 'bold', display: 'flex',
})} alignItems: 'center',
> justifyContent: 'center',
{skill.title} textAlign: 'center',
bg: isSelected
? 'rgba(250, 204, 21, 0.15)'
: 'rgba(255, 255, 255, 0.1)',
borderRadius: 'lg',
})}
>
<MiniAbacus values={skill.values} columns={skill.columns} />
</div>
<div className={stack({ gap: '2', flex: '1' })}>
<div className={hstack({ gap: '2', alignItems: 'center' })}>
<div
className={css({
color: 'white',
fontSize: 'md',
fontWeight: 'bold',
})}
>
{skill.title}
</div>
<div
className={css({
bg: 'rgba(250, 204, 21, 0.2)',
color: 'yellow.400',
fontSize: '2xs',
fontWeight: 'semibold',
px: '2',
py: '0.5',
borderRadius: 'md',
})}
>
{skill.badge}
</div>
</div>
<div
className={css({
color: 'gray.300',
fontSize: 'xs',
lineHeight: '1.5',
})}
>
{skill.desc}
</div> </div>
<div <div
className={css({ className={css({
bg: 'rgba(250, 204, 21, 0.2)',
color: 'yellow.400', color: 'yellow.400',
fontSize: '2xs', fontSize: 'xs',
fontFamily: 'mono',
fontWeight: 'semibold', fontWeight: 'semibold',
mt: '1',
bg: 'rgba(250, 204, 21, 0.1)',
px: '2', px: '2',
py: '0.5', py: '1',
borderRadius: 'md', borderRadius: 'md',
w: 'fit-content',
})} })}
> >
{skill.badge} {skill.example}
</div> </div>
</div> </div>
<div
className={css({
color: 'gray.300',
fontSize: 'xs',
lineHeight: '1.5',
})}
>
{skill.desc}
</div>
<div
className={css({
color: 'yellow.400',
fontSize: 'xs',
fontFamily: 'mono',
fontWeight: 'semibold',
mt: '1',
bg: 'rgba(250, 204, 21, 0.1)',
px: '2',
py: '1',
borderRadius: 'md',
w: 'fit-content',
})}
>
{skill.example}
</div>
</div> </div>
</div> </div>
</div> )
))} })}
</div> </div>
</div> </div>
</div> </div>