218 lines
7.4 KiB
TypeScript
218 lines
7.4 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useCallback } from "react"
|
|
import { motion, AnimatePresence } from "framer-motion"
|
|
import { GatoMascota } from "@/components/child/gato-mascota"
|
|
import { CuisenaireRod, rodColors } from "@/components/child/cuisenaire-rod"
|
|
import TemporizadorVisual from "@/components/child/temporizador-visual"
|
|
import { speak } from "@/lib/speech"
|
|
import { getRods } from "@/lib/pedagogia/cuisenaire"
|
|
import { createCPAState, advanceStage } from "@/lib/pedagogia/cpa-progression"
|
|
import { generateCPAItems, speakCPAInstruction } from "@/components/child/cpa-exercise"
|
|
|
|
type CPAStage = "concrete" | "pictoric" | "abstract"
|
|
|
|
export default function NumerosPage() {
|
|
const [currentValue, setCurrentValue] = useState(1)
|
|
const [stage, setStage] = useState<CPAStage>("concrete")
|
|
const [running, setRunning] = useState(true)
|
|
const [sessionComplete, setSessionComplete] = useState(false)
|
|
const [score, setScore] = useState(0)
|
|
const [tappedItems, setTappedItems] = useState<number[]>([])
|
|
|
|
const rods = getRods(10)
|
|
const cpaItems = generateCPAItems(currentValue, stage)
|
|
|
|
const startValue = useCallback(
|
|
(value: number) => {
|
|
setCurrentValue(value)
|
|
const newStage = value <= 3 ? "concrete" : value <= 6 ? "pictoric" : "abstract"
|
|
setStage(newStage)
|
|
setTappedItems([])
|
|
speakCPAInstruction(value, newStage)
|
|
},
|
|
[],
|
|
)
|
|
|
|
const handleItemTap = useCallback(
|
|
(id: number) => {
|
|
setTappedItems((prev) => {
|
|
const next = [...prev, id]
|
|
if (next.length === currentValue) {
|
|
setScore((s) => s + 1)
|
|
if (currentValue >= 5) {
|
|
setSessionComplete(true)
|
|
setRunning(false)
|
|
} else {
|
|
const nextValue = currentValue + 1
|
|
setCurrentValue(nextValue)
|
|
const newStage: CPAStage = nextValue <= 3 ? "concrete" : nextValue <= 6 ? "pictoric" : "abstract"
|
|
setStage(newStage)
|
|
setTappedItems([])
|
|
setTimeout(() => speakCPAInstruction(nextValue, newStage), 500)
|
|
}
|
|
}
|
|
return next
|
|
})
|
|
},
|
|
[currentValue],
|
|
)
|
|
|
|
if (sessionComplete) {
|
|
return (
|
|
<div className="flex-1 flex flex-col items-center justify-center p-8 gap-6">
|
|
<GatoMascota mood="celebrate" message="¡Muy bien con los números!" size="lg" />
|
|
<p className="text-lg font-display">Puntuación: {score}/5</p>
|
|
<button
|
|
onClick={() => {
|
|
setCurrentValue(1)
|
|
setStage("concrete")
|
|
setScore(0)
|
|
setTappedItems([])
|
|
setSessionComplete(false)
|
|
}}
|
|
className="bg-accent text-accent-foreground touch-target-lg rounded-2xl px-8 py-4 text-xl font-display font-bold"
|
|
>
|
|
Jugar de nuevo
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex-1 flex flex-col items-center justify-center p-6 gap-6">
|
|
<TemporizadorVisual
|
|
durationMinutes={10}
|
|
onComplete={() => {
|
|
setRunning(false)
|
|
setSessionComplete(true)
|
|
}}
|
|
running={running}
|
|
/>
|
|
|
|
<GatoMascota
|
|
mood="coach"
|
|
message={
|
|
stage === "concrete"
|
|
? `Tocá ${currentValue} cosas`
|
|
: stage === "pictoric"
|
|
? "¿Cuántos hay?"
|
|
: "Señalá el número"
|
|
}
|
|
size="sm"
|
|
/>
|
|
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={`${currentValue}-${stage}`}
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
className="flex flex-col items-center gap-4"
|
|
>
|
|
{/* Level indicator */}
|
|
<div className="flex gap-3 items-center">
|
|
{[1, 2, 3, 4, 5].map((n) => (
|
|
<button
|
|
key={n}
|
|
onClick={() => startValue(n)}
|
|
className={`w-12 h-12 rounded-full text-lg font-display font-bold ${
|
|
n <= currentValue ? "bg-primary text-white" : "bg-muted text-muted-foreground"
|
|
}`}
|
|
>
|
|
{n}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* CPA area */}
|
|
<div className="w-full max-w-sm min-h-[200px] flex flex-wrap items-center justify-center gap-4 p-4 bg-white rounded-3xl shadow-md">
|
|
{stage === "concrete" &&
|
|
cpaItems.map((item) => (
|
|
<motion.button
|
|
key={item.id}
|
|
onClick={() => handleItemTap(item.id)}
|
|
className="text-4xl touch-target active:scale-110 transition-transform"
|
|
whileTap={{ scale: 1.2 }}
|
|
animate={
|
|
tappedItems.includes(item.id)
|
|
? { scale: 0.8, opacity: 0.5 }
|
|
: { scale: 1, opacity: 1 }
|
|
}
|
|
>
|
|
{item.emoji}
|
|
</motion.button>
|
|
))}
|
|
|
|
{stage === "pictoric" && (
|
|
<div className="flex flex-col items-center gap-2">
|
|
<div className="flex flex-wrap justify-center gap-2">
|
|
{cpaItems.map((item, i) => (
|
|
<motion.span
|
|
key={item.id}
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: i * 0.1 }}
|
|
className="text-3xl"
|
|
>
|
|
{item.emoji}
|
|
</motion.span>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-3 mt-4">
|
|
{[1, 2, 3, 4, 5].map((n) => (
|
|
<button
|
|
key={n}
|
|
onClick={() => {
|
|
if (n === currentValue) {
|
|
handleItemTap(n)
|
|
}
|
|
}}
|
|
className="w-14 h-14 rounded-2xl bg-muted text-2xl font-display font-bold active:scale-90 transition-transform"
|
|
>
|
|
{n}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{stage === "abstract" && (
|
|
<div className="flex flex-col items-center gap-4">
|
|
<span className="text-6xl font-display font-bold">{currentValue}</span>
|
|
<p className="text-muted-foreground">Tocá el número que ves arriba</p>
|
|
<div className="flex flex-wrap gap-3 justify-center">
|
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((n) => (
|
|
<button
|
|
key={n}
|
|
onClick={() => {
|
|
if (n === currentValue) {
|
|
handleItemTap(n)
|
|
}
|
|
}}
|
|
className="w-16 h-16 rounded-2xl bg-muted text-2xl font-display font-bold active:scale-90 transition-transform"
|
|
>
|
|
{n}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Cuisenaire reference */}
|
|
<div className="flex flex-wrap justify-center gap-1 mt-2">
|
|
{rods.slice(0, currentValue).map((rod) => (
|
|
<CuisenaireRod
|
|
key={rod.value}
|
|
value={rod.value}
|
|
color={rod.color}
|
|
length={rod.lengthPx}
|
|
/>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
)
|
|
}
|