feat: add canvas-confetti integration and AuroraButton component; enhance UI with animations and improve button interactions

This commit is contained in:
2025-08-26 15:58:30 -06:00
parent 56b0dc8a62
commit 196aea5df9
8 changed files with 143 additions and 41 deletions

View File

@@ -6,6 +6,8 @@ import { supabase } from "@/auth/supabase"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import confetti from "canvas-confetti"
import {
Accordion, AccordionContent, AccordionItem, AccordionTrigger,
} from "@/components/ui/accordion"
@@ -514,6 +516,11 @@ function MejorarAIButton({ asignaturaId, onApply }: {
}
const nuevo = await res.json()
onApply(nuevo as Asignatura)
confetti({
particleCount: 120,
spread: 80,
origin: { y: 0.6 },
})
setOpen(false)
} catch (e: any) {
alert(e?.message ?? "Error al mejorar la asignatura")
@@ -547,7 +554,24 @@ function MejorarAIButton({ asignaturaId, onApply }: {
</label>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
<Button onClick={apply} disabled={!prompt.trim() || loading}>{loading ? "Aplicando…" : "Aplicar ajuste"}</Button>
<Button
onClick={apply}
disabled={!prompt.trim() || loading}
className={
loading
? "relative overflow-hidden text-white shadow-md"
: ""
}
>
{loading ? (
<span className="relative z-10">Pensando</span>
) : (
"Aplicar ajuste"
)}
{loading && (
<span className="absolute inset-0 animate-aurora" />
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -291,7 +291,7 @@ function RouteComponent() {
// NEW: acciones carrito
function addToCart(a: Asignatura) {
setCart(prev => prev.find(x => x.id === a.id) ? prev : [...prev, a])
toast.success('Asignatura añadida al carrito')
toast.success('Asignatura añadida al carrito de asignaturas')
}
function removeFromCart(id: string) {
setCart(prev => prev.filter(x => x.id !== id))
@@ -355,7 +355,7 @@ function RouteComponent() {
className="relative"
>
<Icons.ShoppingCart className="w-4 h-4 mr-2" />
Carrito
Carrito de asignaturas
{cart.length > 0 && (
<span className="ml-2 inline-flex h-5 min-w-[1.25rem] items-center justify-center rounded-full bg-white/90 text-[11px] text-neutral-900 px-1">
{cart.length}
@@ -369,36 +369,51 @@ function RouteComponent() {
</div>
{/* Filtros */}
<div className="grid gap-2 sm:grid-cols-[1fr,140px,180px,150px]">
<Input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Buscar por nombre, clave, plan, carrera, facultad…"
className="w-full"
/>
<Select value={sem} onValueChange={setSem}>
<SelectTrigger><SelectValue placeholder="Semestre" /></SelectTrigger>
<SelectContent>
<SelectItem value="todos">Todos</SelectItem>
{semestres.map(s => <SelectItem key={s} value={s}>Semestre {s}</SelectItem>)}
</SelectContent>
</Select>
<Select value={tipo} onValueChange={setTipo}>
<SelectTrigger><SelectValue placeholder="Tipo" /></SelectTrigger>
<SelectContent className="max-h-64">
<SelectItem value="todos">Todos</SelectItem>
{tipos.map(t => <SelectItem key={t} value={t}>{t}</SelectItem>)}
</SelectContent>
</Select>
<Select value={groupBy} onValueChange={(v) => setGroupBy(v as any)}>
<SelectTrigger><SelectValue placeholder="Agrupar por" /></SelectTrigger>
<SelectContent>
<SelectItem value="semestre">Agrupar por semestre</SelectItem>
<SelectItem value="ninguno">Sin agrupación</SelectItem>
</SelectContent>
</Select>
<div className="grid gap-4 sm:grid-cols-4">
<div>
<Label>Búsqueda</Label>
<Input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Nombre, clave, plan, carrera, facultad…"
/>
</div>
<div>
<Label>Semestre</Label>
<Select value={sem} onValueChange={setSem}>
<SelectTrigger><SelectValue placeholder="Todos" /></SelectTrigger>
<SelectContent>
<SelectItem value="todos">Todos</SelectItem>
{semestres.map(s => <SelectItem key={s} value={s}>Semestre {s}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div>
<Label>Tipo</Label>
<Select value={tipo} onValueChange={setTipo}>
<SelectTrigger><SelectValue placeholder="Todos" /></SelectTrigger>
<SelectContent className="max-h-64">
<SelectItem value="todos">Todos</SelectItem>
{tipos.map(t => <SelectItem key={t} value={t}>{t}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div>
<Label>Agrupación</Label>
<Select value={groupBy} onValueChange={(v) => setGroupBy(v as any)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="semestre">Por semestre</SelectItem>
<SelectItem value="ninguno">Sin agrupación</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Chips de salud */}
<div className="flex flex-wrap items-center gap-2">
<HealthChip
@@ -621,7 +636,7 @@ function RouteComponent() {
</div>
<div className="flex items-center justify-between">
<Button variant="ghost" onClick={clearCart}><Icons.Trash2 className="w-4 h-4 mr-1" /> Vaciar carrito</Button>
<Button variant="ghost" onClick={clearCart}><Icons.Trash2 className="w-4 h-4 mr-1" /> Vaciar carrito de Asignaturas</Button>
<div className="space-x-2">
<Button variant="outline" onClick={() => setBulkOpen(false)}>Cerrar</Button>
<Button onClick={cloneBulk}><Icons.CopyPlus className="w-4 h-4 mr-1" /> Clonar en lote</Button>
@@ -686,7 +701,7 @@ function AsignaturaCard({ a, onClone, onAddToCart }: { a: Asignatura; onClone: (
<Icons.Copy className="w-4 h-4" /> Clonar
</DropdownMenuItem>
<DropdownMenuItem className="gap-2" onClick={onAddToCart}>
<Icons.ShoppingCart className="w-4 h-4" /> Añadir al carrito
<Icons.ShoppingCart className="w-4 h-4" /> Añadir al carrito de asignaturas
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -171,10 +171,10 @@ function RouteComponent() {
{/* Métricas principales */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Metric to={`/_authenticated/carreras?facultadId=${facultad.id}`} label="Carreras" value={counts.carreras} Icon={Icons.GraduationCap} />
<Metric to={`/_authenticated/planes?facultadId=${facultad.id}`} label="Planes de estudio" value={counts.planes} Icon={Icons.ScrollText} />
<Metric to={`/_authenticated/asignaturas?facultadId=${facultad.id}`} label="Asignaturas" value={counts.asignaturas} Icon={Icons.BookOpen} />
<Metric to={`/_authenticated/criterios?facultadId=${facultad.id}`} label="Criterios de carrera" value={counts.criterios} Icon={Icons.CheckCircle2} />
<Metric to={`/carreras?facultadId=${facultad.id}`} label="Carreras" value={counts.carreras} Icon={Icons.GraduationCap} />
<Metric to={`/planes?facultadId=${facultad.id}`} label="Planes de estudio" value={counts.planes} Icon={Icons.ScrollText} />
<Metric to={`/asignaturas?facultadId=${facultad.id}`} label="Asignaturas" value={counts.asignaturas} Icon={Icons.BookOpen} />
<Metric to={`/criterios?facultadId=${facultad.id}`} label="Criterios de carrera" value={counts.criterios} Icon={Icons.CheckCircle2} />
</div>
{/* Calidad + Salud */}

View File

@@ -13,6 +13,8 @@ import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { AcademicSections } from '@/components/planes/academic-sections'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@radix-ui/react-tabs'
import confetti from 'canvas-confetti'
import { AuroraButton } from '@/components/effect/aurora-button'
gsap.registerPlugin(ScrollTrigger)
@@ -407,6 +409,8 @@ function AdjustAIButton({ plan }: { plan: PlanFull }) {
body: JSON.stringify({ prompt, plan_id: plan.id }),
}).catch(() => { })
setLoading(false)
confetti({ particleCount: 120, spread: 80, origin: { y: 0.6 } })
setOpen(false)
}
@@ -423,9 +427,9 @@ function AdjustAIButton({ plan }: { plan: PlanFull }) {
</DialogHeader>
<Textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Ej.: Enfatiza ciberseguridad y proyectos prácticos…" className="min-h-[120px]" />
<DialogFooter>
<Button onClick={apply} disabled={!prompt.trim() || loading}>
<AuroraButton onClick={apply} disabled={!prompt.trim() || loading}>
{loading ? 'Aplicando…' : 'Aplicar ajuste'}
</Button>
</AuroraButton>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -519,6 +523,8 @@ function AddAsignaturaButton({
}),
})
if (!res.ok) throw new Error(await res.text())
confetti({ particleCount: 120, spread: 80, origin: { y: 0.6 } })
setOpen(false); onAdded?.()
} catch (e: any) {
alert(e?.message ?? "Error al generar la asignatura")
@@ -616,11 +622,13 @@ function AddAsignaturaButton({
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
<Button onClick={submit} disabled={saving || !canSubmit}>
<Button >
</Button>
<AuroraButton onClick={submit} disabled={saving || !canSubmit}>
{saving
? (mode === "manual" ? "Guardando…" : "Generando…")
: (mode === "manual" ? "Crear" : "Generar e insertar")}
</Button>
</AuroraButton>
</DialogFooter>
</DialogContent>
</Dialog>