From a487a8c293535d5d03c49c107357ab2ee3b2d859 Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Fri, 29 Aug 2025 11:14:34 -0600 Subject: [PATCH] feat: add AdjustAIButton, EditPlanButton, and AsignaturaPreviewCard components - Implemented AdjustAIButton for AI-driven plan adjustments with a confetti effect on success. - Created EditPlanButton to allow editing of plan details with form validation and optimistic updates. - Added AsignaturaPreviewCard to display course previews with relevant statistics and details. - Introduced Field component for consistent form field labeling. - Developed GradientMesh for dynamic background effects based on color input. - Added Pulse component for skeleton loading states. - Created SmallStat and StatCard components for displaying statistical information in a card format. - Implemented utility functions in planHelpers for color manipulation and formatting. - Established planQueries for fetching plan and course data from the database. - Updated the plan detail route to utilize new components and queries for improved user experience. --- .vscode/mcp.json | 19 ++ src/components/planes/AddAsignaturaButton.tsx | 132 ++++++++++ src/components/planes/AdjustAIButton.tsx | 47 ++++ .../planes/AsignaturaPreviewCard.tsx | 81 +++++++ src/components/planes/EditPlanButton.tsx | 73 ++++++ src/components/planes/Field.tsx | 11 + src/components/planes/GradientMesh.tsx | 43 ++++ src/components/planes/Pulse.tsx | 25 ++ src/components/planes/SmallStat.tsx | 12 + src/components/planes/StatCard.tsx | 33 +++ src/components/planes/planHelpers.ts | 25 ++ src/components/planes/planQueries.ts | 93 +++++++ src/routes/_authenticated/plan/$planId.tsx | 229 ++---------------- 13 files changed, 609 insertions(+), 214 deletions(-) create mode 100644 .vscode/mcp.json create mode 100644 src/components/planes/AddAsignaturaButton.tsx create mode 100644 src/components/planes/AdjustAIButton.tsx create mode 100644 src/components/planes/AsignaturaPreviewCard.tsx create mode 100644 src/components/planes/EditPlanButton.tsx create mode 100644 src/components/planes/Field.tsx create mode 100644 src/components/planes/GradientMesh.tsx create mode 100644 src/components/planes/Pulse.tsx create mode 100644 src/components/planes/SmallStat.tsx create mode 100644 src/components/planes/StatCard.tsx create mode 100644 src/components/planes/planHelpers.ts create mode 100644 src/components/planes/planQueries.ts diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..c9788b9 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,19 @@ +{ + "inputs": [ + { + "type": "promptString", + "id": "supabase-access-token", + "description": "Supabase personal access token", + "password": true + } + ], + "servers": { + "supabase": { + "command": "cmd", + "args": ["/c", "npx", "-y", "@supabase/mcp-server-supabase@latest", "--read-only", "--project-ref=exdkssurzmjnnhgtiama"], + "env": { + "SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}" + } + } + } +} \ No newline at end of file diff --git a/src/components/planes/AddAsignaturaButton.tsx b/src/components/planes/AddAsignaturaButton.tsx new file mode 100644 index 0000000..4457399 --- /dev/null +++ b/src/components/planes/AddAsignaturaButton.tsx @@ -0,0 +1,132 @@ +import { useState } from "react" +import { Button } from "@/components/ui/button" +import * as Icons from "lucide-react" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { AuroraButton } from "@/components/effect/aurora-button" +import confetti from "canvas-confetti" +import { useQueryClient } from "@tanstack/react-query" +import { supabase } from "@/auth/supabase" +import { Field } from "./Field" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs" +import { asignaturaKeys } from "./planQueries" + +export function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: () => void }) { + const qc = useQueryClient() + const [open, setOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [mode, setMode] = useState<"manual" | "ia">("manual") + + const [f, setF] = useState({ nombre: "", clave: "", tipo: "", semestre: "", creditos: "", horas_teoricas: "", horas_practicas: "", objetivos: "" }) + const [iaPrompt, setIaPrompt] = useState("") + const [iaSemestre, setIaSemestre] = useState("") + + const toNull = (s: string) => s.trim() ? s : null + const toNum = (s: string) => s.trim() ? Number(s) || null : null + + const canManual = f.nombre.trim().length > 0 + const canIA = iaPrompt.trim().length > 0 + const canSubmit = mode === "manual" ? canManual : canIA + + async function createManual() { + if (!canManual) return + setSaving(true) + const payload = { + plan_id: planId, + nombre: f.nombre.trim(), + clave: toNull(f.clave), + tipo: toNull(f.tipo), + semestre: toNum(f.semestre), + creditos: toNum(f.creditos), + horas_teoricas: toNum(f.horas_teoricas), + horas_practicas: toNum(f.horas_practicas), + objetivos: toNull(f.objetivos), + contenidos: {}, bibliografia: [], criterios_evaluacion: null, + } + const { error } = await supabase.from("asignaturas").insert([payload]) + setSaving(false) + if (error) { alert(error.message); return } + setOpen(false) + onAdded?.() + qc.invalidateQueries({ queryKey: asignaturaKeys.preview(planId) }) + qc.invalidateQueries({ queryKey: asignaturaKeys.count(planId) }) + } + + async function createWithAI() { + if (!canIA) return + setSaving(true) + try { + const res = await fetch("https://genesis-engine.apps.lci.ulsa.mx/api/generar/asignatura", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ planEstudiosId: planId, prompt: iaPrompt, semestre: iaSemestre.trim() ? Number(iaSemestre) : undefined, insert: true }), + }) + if (!res.ok) throw new Error(await res.text()) + confetti({ particleCount: 120, spread: 80, origin: { y: 0.6 } }) + setOpen(false) + onAdded?.() + qc.invalidateQueries({ queryKey: asignaturaKeys.preview(planId) }) + qc.invalidateQueries({ queryKey: asignaturaKeys.count(planId) }) + } catch (e: any) { + alert(e?.message ?? "Error al generar la asignatura") + } finally { setSaving(false) } + } + + const submit = () => (mode === "manual" ? createManual() : createWithAI()) + + return ( + <> + + + + + + Nueva asignatura + Elige cómo crearla: manual o generada por IA. + + + setMode(v as "manual" | "ia")} className="w-full"> + + + Manual + + + Generado por IA + + + + +
+ setF(s => ({ ...s, nombre: e.target.value }))} /> + setF(s => ({ ...s, clave: e.target.value }))} /> + setF(s => ({ ...s, tipo: e.target.value }))} placeholder="Obligatoria / Optativa / Taller…" /> + setF(s => ({ ...s, semestre: e.target.value }))} placeholder="1–10" /> + setF(s => ({ ...s, creditos: e.target.value }))} /> + setF(s => ({ ...s, horas_teoricas: e.target.value }))} /> + setF(s => ({ ...s, horas_practicas: e.target.value }))} /> +