diff --git a/bun.lock b/bun.lock index 502ee82..9f84c4d 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "tanstack-router", "dependencies": { "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", @@ -17,6 +18,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@supabase/supabase-js": "^2.55.0", "@tailwindcss/vite": "^4.1.12", "@tanstack/react-devtools": "^0.2.2", @@ -38,6 +40,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.12", + "vaul": "^1.1.2", "zod": "^4.0.17", }, "devDependencies": { @@ -210,6 +213,8 @@ "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], @@ -266,6 +271,8 @@ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], @@ -850,6 +857,8 @@ "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], + "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], diff --git a/package.json b/package.json index d5b89ef..97a4fb3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-context-menu": "^2.2.16", "@radix-ui/react-dialog": "^1.1.15", @@ -23,6 +24,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@supabase/supabase-js": "^2.55.0", "@tailwindcss/vite": "^4.1.12", "@tanstack/react-devtools": "^0.2.2", @@ -44,6 +46,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.12", + "vaul": "^1.1.2", "zod": "^4.0.17" }, "devDependencies": { diff --git a/src/components/asignaturas/EditAsignaturaButton.tsx b/src/components/asignaturas/EditAsignaturaButton.tsx new file mode 100644 index 0000000..60838b4 --- /dev/null +++ b/src/components/asignaturas/EditAsignaturaButton.tsx @@ -0,0 +1,107 @@ +import { useState } from "react" +import { supabase } from "@/auth/supabase" +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select" +import { typeStyle } from "./typeStyle" +import type { Asignatura } from "@/types/asignatura" +import { Field } from "./Field" + +export function EditAsignaturaButton({ asignatura, onUpdate }: { + asignatura: Asignatura; onUpdate: (a: Asignatura) => void +}) { + const [open, setOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [form, setForm] = useState>({}) + + const openAndFill = () => { setForm(asignatura); setOpen(true) } + + async function save() { + setSaving(true) + const payload = { + nombre: form.nombre ?? asignatura.nombre, + clave: form.clave ?? asignatura.clave, + tipo: form.tipo ?? asignatura.tipo, + semestre: form.semestre ?? asignatura.semestre, + creditos: form.creditos ?? asignatura.creditos, + horas_teoricas: form.horas_teoricas ?? asignatura.horas_teoricas, + horas_practicas: form.horas_practicas ?? asignatura.horas_practicas, + } + const { data, error } = await supabase + .from("asignaturas") + .update(payload) + .eq("id", asignatura.id) + .select() + .maybeSingle() + setSaving(false) + if (!error && data) { onUpdate(data as Asignatura); setOpen(false) } + else alert(error?.message ?? "Error al guardar") + } + + return ( + <> + + + + + Editar asignatura + Actualiza campos básicos. + + +
+ + setForm((s: Partial) => ({ ...s, nombre: e.target.value }))} /> + + + setForm((s: Partial) => ({ ...s, clave: e.target.value }))} /> + + + + + + setForm((s: Partial) => ({ ...s, semestre: Number(e.target.value) || null }))} /> + + + setForm((s: Partial) => ({ ...s, creditos: Number(e.target.value) || null }))} /> + + + setForm((s: Partial) => ({ ...s, horas_teoricas: Number(e.target.value) || null }))} /> + + + setForm((s: Partial) => ({ ...s, horas_practicas: Number(e.target.value) || null }))} /> + +
+ + + + + +
+
+ + ) +} \ No newline at end of file diff --git a/src/components/asignaturas/EditBibliografíaButton.tsx b/src/components/asignaturas/EditBibliografíaButton.tsx new file mode 100644 index 0000000..4f9eef4 --- /dev/null +++ b/src/components/asignaturas/EditBibliografíaButton.tsx @@ -0,0 +1,228 @@ +import { useEffect, useMemo, useRef, useState } from "react" +import { supabase } from "@/auth/supabase" +import { Button } from "../ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog" +import { Textarea } from "../ui/textarea" +import { Separator } from "../ui/separator" +import { ScrollArea } from "../ui/scroll-area" +import { Badge } from "../ui/badge" +import * as Icons from "lucide-react" +import { toast } from "sonner" + +/** + * EditBibliografiaButton v3 (simple y aireado) + * - Layout limpio: una sola columna + barra mínima. + * - Acciones esenciales: Recortar, Dedupe, A–Z, Importar, Copiar. + * - Toasts con sonner: toast.success / toast.error. + */ + +export function EditBibliografiaButton({ + asignaturaId, + value, + onSaved, +}: { + asignaturaId: string + value: string[] + onSaved: (refs: string[]) => void +}) { + const [open, setOpen] = useState(false) + const [saving, setSaving] = useState(false) + const [text, setText] = useState("") + + const initialTextRef = useRef("") + const lines = useMemo(() => parseLines(text), [text]) + const dirty = useMemo(() => initialTextRef.current !== text, [text]) + + function openEditor() { + const start = (value ?? []).join("\n") + setText(start) + initialTextRef.current = start + setOpen(true) + } + + async function save() { + try { + setSaving(true) + const refs = parseLines(text) + const { data, error } = await supabase + .from("asignaturas") + .update({ bibliografia: refs }) + .eq("id", asignaturaId) + .select() + .maybeSingle() + + if (error) throw error + + onSaved((data as any)?.bibliografia ?? refs) + initialTextRef.current = refs.join("\n") + toast.success(`${refs.length} referencia(s) guardada(s).`) + setOpen(false) + } catch (e: any) { + toast.error(e?.message ?? "No se pudo guardar") + } finally { + setSaving(false) + } + } + + // Acciones + function actionTrim() { + const next = parseLines(text).map((s) => s.replace(/\s+/g, " ").trim()) + setText(next.join("\n")) + } + function actionDedupe() { + const seen = new Set() + const next: string[] = [] + for (const l of parseLines(text)) { + const k = l.toLowerCase() + if (!seen.has(k)) { seen.add(k); next.push(l) } + } + setText(next.join("\n")) + } + function actionSort() { + const next = [...parseLines(text)].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" })) + setText(next.join("\n")) + } + async function actionImportClipboard() { + try { + const clip = await navigator.clipboard.readText() + if (!clip) { toast("Portapapeles vacío"); return } + const next = [...parseLines(text), ...parseLines(clip)] + setText(next.join("\n")) + toast.success("Texto importado") + } catch (e: any) { + toast.error(e?.message ?? "No se pudo leer el portapapeles") + } + } + async function actionExportClipboard() { + try { + await navigator.clipboard.writeText(parseLines(text).join("\n")) + toast.success("Copiado al portapapeles") + } catch (e: any) { + toast.error(e?.message ?? "No se pudo copiar") + } + } + + // Atajo guardar + useEffect(() => { + function onKey(e: KeyboardEvent) { + if (!open) return + if (e.key === "s" && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + if (!saving && dirty) void save() + } + } + window.addEventListener("keydown", onKey) + return () => window.removeEventListener("keydown", onKey) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open, saving, dirty, text]) + + return ( + <> + + + + + +
+
+ Editar bibliografía + + Una referencia por línea. Guarda con Ctrl/⌘+S. + +
+
+ {lines.length} + {dirty ? Sin guardar : OK} +
+
+
+ + {/* Barra de acciones (compacta) */} +
+
+ + + + + +
+
+ + + + {/* Editor */} +
+