feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes

This commit is contained in:
renato97
2026-07-21 15:31:46 -03:00
commit fa8e0c58ce
120 changed files with 16013 additions and 0 deletions
+293
View File
@@ -0,0 +1,293 @@
"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { authClient } from "@/lib/auth-client"
import { MetricCard } from "@/components/parent/metric-card"
import { WeeklyActivityChart } from "@/components/parent/weekly-activity-chart"
import { SkillMasteryChart } from "@/components/parent/skill-mastery-chart"
import { MilestonesList } from "@/components/parent/milestones-list"
import ErrorPatternsChart from "@/components/parent/error-patterns-chart"
interface ChildSummary {
id: string
name: string | null
}
interface DashboardData {
totalSessions: number
todaySessions: number
todayMinutes: number
masteryPercent: number
masteredSkills: number
totalSkills: number
milestones: number
milestoneData: { skillCode: string; reachedAt: string }[]
streak: number
weeklyData: { date: string; minutes: number }[]
skillBreakdown: { skillCode: string; stability: number; reps: number }[]
stageProgress: { topic: string; stage: number; completedAt: string | null }[]
errorTypeCount: Record<string, number>
totalErrors: number
lastSession: {
startedAt: string
durationSec: number
skillsPracticed: string
correctCount: number
totalAttempts: number
} | null
}
export default function ParentDashboard() {
const { data: session, isPending } = authClient.useSession()
const router = useRouter()
const [children, setChildren] = useState<ChildSummary[]>([])
const [selectedChildId, setSelectedChildId] = useState<string>("")
const [data, setData] = useState<DashboardData | null>(null)
const [loading, setLoading] = useState(true)
const [creating, setCreating] = useState(false)
const [newChildName, setNewChildName] = useState("")
const [pairingCode, setPairingCode] = useState("")
useEffect(() => {
fetch("/api/children")
.then((r) => r.json())
.then((list) => {
if (Array.isArray(list)) {
setChildren(list)
if (list.length > 0) setSelectedChildId(list[0].id)
}
})
.catch(() => {})
}, [])
useEffect(() => {
if (!selectedChildId) {
setLoading(false)
return
}
setLoading(true)
fetch(`/api/dashboard/summary?childId=${selectedChildId}`)
.then((r) => r.json())
.then(setData)
.catch(() => {})
.finally(() => setLoading(false))
}, [selectedChildId])
const handleCreateChild = async () => {
if (!newChildName.trim()) return
setCreating(true)
const res = await fetch("/api/children", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newChildName.trim() }),
})
if (res.ok) {
const child = await res.json()
setChildren((prev) => [...prev, child])
setSelectedChildId(child.id)
setNewChildName("")
}
setCreating(false)
}
const handleGeneratePairingCode = async () => {
if (!selectedChildId) return
const res = await fetch("/api/pairing", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "generate", childId: selectedChildId }),
})
const data = await res.json()
if (data.pairingCode) {
setPairingCode(data.pairingCode)
}
}
useEffect(() => {
if (!isPending && !session) {
router.push("/parent/auth/login")
}
}, [session, isPending, router])
if (isPending || !session) {
return (
<div className="p-4 text-center text-muted-foreground">Cargando...</div>
)
}
const selectedChild = children.find((c) => c.id === selectedChildId)
return (
<div className="p-4 sm:p-6 max-w-2xl mx-auto">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-6">
<div>
<h2 className="text-2xl font-display font-bold">Dashboard</h2>
<p className="text-sm text-muted-foreground">
Bienvenida, {session.user?.name || "Mamá"}
</p>
</div>
</div>
{children.length === 0 ? (
<div className="bg-white rounded-2xl p-6 shadow-sm border border-border">
<h3 className="text-lg font-display font-semibold mb-4">Agregar un niño</h3>
<div className="flex gap-2">
<input
type="text"
value={newChildName}
onChange={(e) => setNewChildName(e.target.value)}
placeholder="Nombre de la nena"
className="flex-1 rounded-xl border border-border bg-white px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-primary"
/>
<button
onClick={handleCreateChild}
disabled={creating || !newChildName.trim()}
className="bg-primary text-primary-foreground touch-target rounded-xl px-6 font-display font-semibold disabled:opacity-50"
>
{creating ? "..." : "Agregar"}
</button>
</div>
</div>
) : (
<>
<div className="flex flex-wrap gap-2 mb-6">
{children.map((c) => (
<button
key={c.id}
onClick={() => setSelectedChildId(c.id)}
className={`rounded-xl px-4 py-2 font-display font-medium text-sm transition-colors ${
c.id === selectedChildId
? "bg-primary text-primary-foreground"
: "bg-muted text-foreground"
}`}
>
{c.name || "Sin nombre"}
</button>
))}
</div>
{loading ? (
<div className="text-center py-12 text-muted-foreground">Cargando...</div>
) : data ? (
<div className="flex flex-col gap-4">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<MetricCard title="Racha" value={`${data.streak} días`} variant="primary" />
<MetricCard
title="Hoy"
value={`${data.todayMinutes} min`}
subtitle={data.todaySessions > 0 ? `${data.todaySessions} sesiones` : undefined}
variant="success"
/>
<MetricCard
title="Maestría"
value={`${data.masteryPercent}%`}
subtitle={`${data.masteredSkills}/${data.totalSkills} skills`}
variant="accent"
/>
<MetricCard title="Logros" value={data.milestones} variant="default" />
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm border border-border">
<h3 className="text-sm font-display font-semibold mb-3">
Actividad semanal
</h3>
<WeeklyActivityChart data={data.weeklyData} />
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm border border-border">
<h3 className="text-sm font-display font-semibold mb-3">
Maestría por skill
</h3>
<SkillMasteryChart data={data.skillBreakdown} />
</div>
<div className="bg-white rounded-2xl p-4 shadow-sm border border-border">
<h3 className="text-sm font-display font-semibold mb-3">Logros</h3>
<MilestonesList data={data.milestoneData} />
</div>
{data.stageProgress.length > 0 && (
<div className="bg-white rounded-2xl p-4 shadow-sm border border-border">
<h3 className="text-sm font-display font-semibold mb-3">Progreso curricular</h3>
<div className="space-y-2 text-sm">
{data.stageProgress.map((sp) => (
<div key={`${sp.topic}-${sp.stage}`} className="flex justify-between items-center">
<span className="font-medium">
{sp.topic === "lectura" ? "Lectura" : "Números"} Etapa {sp.stage}
</span>
<span className={sp.completedAt ? "text-green-600" : "text-yellow-600"}>
{sp.completedAt ? "✅ Completada" : "🔵 En curso"}
</span>
</div>
))}
</div>
</div>
)}
{data.totalErrors > 0 && (
<div className="bg-white rounded-2xl p-4 shadow-sm border border-border">
<h3 className="text-sm font-display font-semibold mb-3">Patrones de error</h3>
<ErrorPatternsChart data={data.errorTypeCount} total={data.totalErrors} />
</div>
)}
{data.lastSession && (
<div className="bg-white rounded-2xl p-4 shadow-sm border border-border">
<h3 className="text-sm font-display font-semibold mb-2">Última sesión</h3>
<div className="text-sm text-muted-foreground">
<p>{new Date(data.lastSession.startedAt).toLocaleDateString("es-AR")}</p>
<p>
{Math.round((data.lastSession.durationSec ?? 0) / 60)} min ·{" "}
{data.lastSession.correctCount}/{data.lastSession.totalAttempts} correctas
</p>
<p className="text-xs mt-1">Skills: {data.lastSession.skillsPracticed || "—"}</p>
</div>
</div>
)}
<div className="flex flex-col sm:flex-row gap-3 mt-2">
<button
onClick={handleGeneratePairingCode}
className="bg-secondary text-secondary-foreground touch-target rounded-xl px-6 font-display font-semibold"
>
Emparejar nuevo iPad
</button>
<button
onClick={() => router.push("/parent/auth/pairing")}
className="bg-muted text-foreground touch-target rounded-xl font-display"
>
Ingresar código
</button>
</div>
</div>
) : (
<div className="text-center py-12 text-muted-foreground">
Seleccioná un niño para ver su progreso
</div>
)}
</>
)}
{pairingCode && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="bg-white rounded-2xl p-6 shadow-xl max-w-sm w-full text-center space-y-4">
<h3 className="text-lg font-display font-semibold">Código de emparejamiento</h3>
<p className="text-muted-foreground text-sm">
Ingresá este código en el iPad de la nena
</p>
<p className="text-3xl tracking-[0.3em] font-bold font-display text-primary select-all">
{pairingCode}
</p>
<button
onClick={() => setPairingCode("")}
className="bg-primary text-primary-foreground touch-target rounded-xl px-6 py-3 font-display font-semibold"
>
Listo
</button>
</div>
</div>
)}
</div>
)
}