feat: EduEasy checkpoint - plan, scaffolding, pedagogia modules, components child+parent, API routes, Prisma schema, Docker infra, Better Auth
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useCallback } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { GatoMascota } from "@/components/child/gato-mascota"
|
||||
import VocalCard from "@/components/child/vocal-card"
|
||||
import TrazoLetra from "@/components/child/trazo-letra"
|
||||
import TemporizadorVisual from "@/components/child/temporizador-visual"
|
||||
import { speak } from "@/lib/speech"
|
||||
import { vocales } from "@/lib/pedagogia/borel-maisonny"
|
||||
import { getInitialState, evaluateResponse, isMastered } from "@/lib/pedagogia/errorless"
|
||||
|
||||
type Step = "gesto" | "escuchar" | "trazar" | "elegir" | "completo"
|
||||
|
||||
export default function LecturaSesion() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [step, setStep] = useState<Step>("gesto")
|
||||
const [promptState, setPromptState] = useState(getInitialState())
|
||||
const [sessionComplete, setSessionComplete] = useState(false)
|
||||
const [running, setRunning] = useState(true)
|
||||
const [score, setScore] = useState(0)
|
||||
|
||||
const vocal = vocales[currentIndex]
|
||||
|
||||
const handleGestoComplete = useCallback(() => {
|
||||
speak(`Escuchá. Esta es la letra ${vocal.letra}. ${vocal.palabra}. ${vocal.fonema} ${vocal.fonema} ${vocal.fonema}`)
|
||||
setStep("escuchar")
|
||||
}, [vocal])
|
||||
|
||||
const handleEscucharComplete = useCallback(() => {
|
||||
speak(`Ahora trazala con tu dedo`)
|
||||
setStep("trazar")
|
||||
}, [])
|
||||
|
||||
const handleTrazoComplete = useCallback(() => {
|
||||
setScore((s) => s + 1)
|
||||
const newState = evaluateResponse(promptState, true)
|
||||
setPromptState(newState)
|
||||
|
||||
if (isMastered(newState.level)) {
|
||||
speak(`¡Muy bien! Pasemos a la siguiente`)
|
||||
if (currentIndex < vocales.length - 1) {
|
||||
setCurrentIndex((i) => i + 1)
|
||||
setStep("gesto")
|
||||
setPromptState(getInitialState())
|
||||
} else {
|
||||
setSessionComplete(true)
|
||||
setRunning(false)
|
||||
}
|
||||
} else {
|
||||
setStep("elegir")
|
||||
}
|
||||
}, [promptState, currentIndex])
|
||||
|
||||
const handleChoice = useCallback(
|
||||
(correct: boolean) => {
|
||||
const newState = evaluateResponse(promptState, correct)
|
||||
setPromptState(newState)
|
||||
|
||||
if (correct) {
|
||||
setScore((s) => s + 1)
|
||||
}
|
||||
|
||||
if (isMastered(newState.level)) {
|
||||
if (currentIndex < vocales.length - 1) {
|
||||
setCurrentIndex((i) => i + 1)
|
||||
setStep("gesto")
|
||||
setPromptState(getInitialState())
|
||||
} else {
|
||||
setSessionComplete(true)
|
||||
setRunning(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[promptState, currentIndex],
|
||||
)
|
||||
|
||||
if (sessionComplete) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-8 gap-6">
|
||||
<GatoMascota mood="celebrate" message="¡Terminaste todas las vocales!" size="lg" />
|
||||
<p className="text-lg font-display">Puntuación: {score}/{vocales.length * 2}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCurrentIndex(0)
|
||||
setStep("gesto")
|
||||
setPromptState(getInitialState())
|
||||
setSessionComplete(false)
|
||||
setRunning(true)
|
||||
setScore(0)
|
||||
}}
|
||||
className="bg-primary text-primary-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={step === "gesto" ? "thinking" : step === "completo" ? "celebrate" : "coach"}
|
||||
message={
|
||||
step === "gesto"
|
||||
? `Hacé así: ${vocal.gesto}`
|
||||
: step === "escuchar"
|
||||
? "Escuchá bien"
|
||||
: step === "trazar"
|
||||
? "Traza con tu dedo"
|
||||
: ""
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={`${vocal.letra}-${step}`}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{step === "gesto" && (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<span className="text-8xl">{vocal.imageEmoji}</span>
|
||||
<p className="text-lg text-foreground/60 italic max-w-xs text-center">
|
||||
{vocal.gesto}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleGestoComplete}
|
||||
className="bg-primary text-primary-foreground touch-target-lg rounded-2xl px-8 py-4 text-xl font-display font-bold"
|
||||
>
|
||||
¡Lo hice!
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "escuchar" && (
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<VocalCard
|
||||
letra={vocal.letra}
|
||||
fonema={vocal.fonema}
|
||||
palabra={vocal.palabra}
|
||||
imageEmoji={vocal.imageEmoji}
|
||||
color={vocal.color}
|
||||
onClick={() => {
|
||||
speak(`${vocal.fonema}... de ${vocal.palabra}`)
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleEscucharComplete}
|
||||
className="bg-secondary text-secondary-foreground touch-target-lg rounded-2xl px-8 py-4 text-xl font-display font-bold"
|
||||
>
|
||||
Siguiente
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "trazar" && (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<TrazoLetra
|
||||
letra={vocal.letra}
|
||||
fonema={vocal.fonema}
|
||||
color={vocal.color}
|
||||
onComplete={handleTrazoComplete}
|
||||
promptLevel={promptState.level}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === "elegir" && (
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<p className="text-lg font-display">¿Cuál es la {vocal.letra.toUpperCase()}?</p>
|
||||
<div className="flex flex-wrap gap-4 justify-center">
|
||||
{[...vocales].sort(() => Math.random() - 0.5).slice(0, 3).map((v) => (
|
||||
<button
|
||||
key={v.letra}
|
||||
onClick={() => handleChoice(v.letra === vocal.letra)}
|
||||
className={`touch-target-lg rounded-2xl text-2xl font-display font-bold px-8 py-6 shadow-md active:scale-95 transition-transform`}
|
||||
style={{ backgroundColor: v.color }}
|
||||
>
|
||||
{v.letra.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user