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:
parent
9ad35e65d3
commit
76d6f19d51
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue