feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
|
||||
export default function LoginPage() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
const { error: err } = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (err) {
|
||||
setError(err.message || "Error al iniciar sesión")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
router.push("/parent")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh flex items-center justify-center p-6">
|
||||
<form onSubmit={handleLogin} className="w-full max-w-sm flex flex-col gap-6">
|
||||
<h1 className="text-2xl font-display font-bold text-foreground text-center">
|
||||
EduEasy · Mamá
|
||||
</h1>
|
||||
|
||||
{error && (
|
||||
<p className="text-destructive text-sm text-center">{error}</p>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full rounded-xl border border-border bg-white px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Contraseña"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full rounded-xl border border-border bg-white px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
required
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-primary text-primary-foreground touch-target rounded-xl text-lg font-display font-semibold disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Entrando..." : "Iniciar sesión"}
|
||||
</button>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
¿No tenés cuenta?{" "}
|
||||
<a href="/parent/auth/register" className="text-primary underline">
|
||||
Crear cuenta
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { GatoMascota } from "@/components/child/gato-mascota"
|
||||
|
||||
interface ChildSummary {
|
||||
id: string
|
||||
name: string | null
|
||||
}
|
||||
|
||||
export default function PairingPage() {
|
||||
const [code, setCode] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [paired, setPaired] = useState(false)
|
||||
const [children, setChildren] = useState<ChildSummary[]>([])
|
||||
const [selectedChildId, setSelectedChildId] = useState("")
|
||||
const { data: session } = authClient.useSession()
|
||||
const router = useRouter()
|
||||
|
||||
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(() => {})
|
||||
}, [])
|
||||
|
||||
const handlePairing = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
if (!selectedChildId) {
|
||||
setError("Seleccioná un niño")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/pairing", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
pairingCode: code.toUpperCase(),
|
||||
childId: selectedChildId,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
|
||||
if (data.error) {
|
||||
setError(data.error)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setPaired(true)
|
||||
} catch {
|
||||
setError("Error de conexión")
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center p-6 gap-4">
|
||||
<p className="text-muted-foreground">Iniciá sesión primero para emparejar</p>
|
||||
<button
|
||||
onClick={() => router.push("/parent/auth/login")}
|
||||
className="bg-primary text-primary-foreground touch-target rounded-xl px-6 py-3 font-display"
|
||||
>
|
||||
Ir a login
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center p-6 gap-6">
|
||||
<h1 className="text-2xl font-display font-bold">Emparejar iPad</h1>
|
||||
|
||||
{paired ? (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<GatoMascota mood="celebrate" message="iPad emparejado con éxito" size="lg" />
|
||||
<button
|
||||
onClick={() => router.push("/parent")}
|
||||
className="bg-primary text-primary-foreground touch-target rounded-xl px-6 py-3 font-display"
|
||||
>
|
||||
Volver al dashboard
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handlePairing} className="flex flex-col gap-4 w-full max-w-sm">
|
||||
<p className="text-muted-foreground text-center">
|
||||
Ingresá el código de 8 caracteres que aparece en el iPad
|
||||
</p>
|
||||
|
||||
{children.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
{children.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
onClick={() => setSelectedChildId(c.id)}
|
||||
className={`rounded-xl px-4 py-2 font-display text-sm transition-colors ${
|
||||
c.id === selectedChildId
|
||||
? "bg-secondary text-secondary-foreground"
|
||||
: "bg-muted text-foreground"
|
||||
}`}
|
||||
>
|
||||
{c.name || "Sin nombre"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
||||
placeholder="Código de 8 letras"
|
||||
maxLength={8}
|
||||
className="w-full rounded-xl border border-border bg-white px-4 py-3 text-center text-2xl tracking-widest uppercase focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
autoFocus
|
||||
/>
|
||||
{error && <p className="text-destructive text-sm text-center">{error}</p>}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || code.length < 8}
|
||||
className="bg-secondary text-secondary-foreground touch-target rounded-xl text-lg font-display font-semibold disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Emparejando..." : "Emparejar"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [email, setEmail] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [name, setName] = useState("")
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const router = useRouter()
|
||||
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
const { error: err } = await authClient.signUp.email({
|
||||
email,
|
||||
password,
|
||||
name: name || "",
|
||||
})
|
||||
|
||||
if (err) {
|
||||
setError(err.message || "Error al registrarse")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
router.push("/parent")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh flex items-center justify-center p-6">
|
||||
<form onSubmit={handleRegister} className="w-full max-w-sm flex flex-col gap-6">
|
||||
<h1 className="text-2xl font-display font-bold text-foreground text-center">
|
||||
Crear cuenta · Mamá
|
||||
</h1>
|
||||
|
||||
{error && <p className="text-destructive text-sm text-center">{error}</p>}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tu nombre"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded-xl border border-border bg-white px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full rounded-xl border border-border bg-white px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Contraseña (mín. 8 caracteres)"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
minLength={8}
|
||||
className="w-full rounded-xl border border-border bg-white px-4 py-3 text-base focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
required
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-primary text-primary-foreground touch-target rounded-xl text-lg font-display font-semibold disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Creando cuenta..." : "Crear cuenta"}
|
||||
</button>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
¿Ya tenés cuenta?{" "}
|
||||
<a href="/parent/auth/login" className="text-primary underline">
|
||||
Iniciá sesión
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export default function ConfigPage() {
|
||||
const { data: session } = authClient.useSession()
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh p-6">
|
||||
<h1 className="text-2xl font-display font-bold mb-6">Configuración</h1>
|
||||
|
||||
<div className="flex flex-col gap-4 max-w-md">
|
||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
||||
<p className="text-sm text-muted-foreground">Email</p>
|
||||
<p className="font-medium">{session?.user?.email || "—"}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => router.push("/parent/auth/pairing")}
|
||||
className="bg-secondary text-secondary-foreground touch-target rounded-xl text-lg font-display font-semibold"
|
||||
>
|
||||
Emparejar nuevo iPad
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => router.push("/parent")}
|
||||
className="bg-muted text-foreground touch-target rounded-xl font-display"
|
||||
>
|
||||
Volver al dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const navItems = [
|
||||
{ href: "/parent/dashboard", label: "Dashboard", emoji: "📊" },
|
||||
{ href: "/parent/configuracion", label: "Configuración", emoji: "⚙️" },
|
||||
]
|
||||
|
||||
export default function ParentLayout({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname()
|
||||
const { data: session } = authClient.useSession()
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
if (
|
||||
pathname.startsWith("/parent/auth")
|
||||
) {
|
||||
return <div className="min-h-dvh bg-background">{children}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-background flex flex-col">
|
||||
<header className="sticky top-0 z-10 bg-background/95 backdrop-blur border-b border-border px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="text-foreground touch-target rounded-xl flex items-center justify-center"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M3 12h18M3 6h18M3 18h18" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 className="text-lg font-display font-bold">EduEasy</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-muted-foreground hidden sm:block">
|
||||
{session?.user?.name || "Mamá"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => authClient.signOut()}
|
||||
className="text-sm text-muted-foreground underline"
|
||||
>
|
||||
Salir
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{sidebarOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/20 z-20"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<nav
|
||||
className="fixed left-0 top-0 bottom-0 w-64 bg-white shadow-lg p-4 pt-16"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-xl px-4 py-3 font-display font-medium transition-colors",
|
||||
pathname.startsWith(item.href)
|
||||
? "bg-primary/10 text-primary"
|
||||
: "text-foreground hover:bg-muted",
|
||||
)}
|
||||
>
|
||||
<span>{item.emoji}</span>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { authClient } from "@/lib/auth-client"
|
||||
|
||||
export default function ParentPage() {
|
||||
const { data: session, isPending } = authClient.useSession()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (isPending) return
|
||||
if (session) {
|
||||
router.replace("/parent/dashboard")
|
||||
} else {
|
||||
router.replace("/parent/auth/login")
|
||||
}
|
||||
}, [session, isPending, router])
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user