feat: update dialog titles to use font-mono style for consistency across components
This commit is contained in:
@@ -206,7 +206,7 @@ function DetailDialog({ row, onClose }: { row: RefRow | null; onClose: () => voi
|
||||
<Dialog open={!!row} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{row?.titulo_archivo ?? "(Sin título)"}</DialogTitle>
|
||||
<DialogTitle className="font-mono" >{row?.titulo_archivo ?? "(Sin título)"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{row?.descripcion || "Sin descripción"}
|
||||
</DialogDescription>
|
||||
@@ -333,7 +333,7 @@ function UploadDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nuevo archivo de referencia</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Nuevo archivo de referencia</DialogTitle>
|
||||
<DialogDescription>
|
||||
Sube un documento y escribe instrucciones para su procesamiento. Se guardará en la base y se marcará como
|
||||
<em> procesado </em> cuando termine el flujo.
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
// routes/_authenticated/asignatura/$asignaturaId.tsx
|
||||
import { createFileRoute, Link, useRouter } from "@tanstack/react-router"
|
||||
import * as Icons from "lucide-react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { supabase } from "@/auth/supabase"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectValue
|
||||
} from "@/components/ui/select"
|
||||
import confetti from "canvas-confetti"
|
||||
|
||||
import {
|
||||
@@ -16,6 +23,8 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
|
||||
/* ================== Tipos ================== */
|
||||
type Asignatura = {
|
||||
@@ -456,7 +465,7 @@ function EditAsignaturaButton({ asignatura, onUpdate }: {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar asignatura</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Editar asignatura</DialogTitle>
|
||||
<DialogDescription>Actualiza campos básicos.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -468,7 +477,28 @@ function EditAsignaturaButton({ asignatura, onUpdate }: {
|
||||
<Input value={form.clave ?? ""} onChange={e => setForm(s => ({ ...s, clave: e.target.value }))} />
|
||||
</Field>
|
||||
<Field label="Tipo">
|
||||
<Input value={form.tipo ?? ""} onChange={e => setForm(s => ({ ...s, tipo: e.target.value }))} />
|
||||
<Select value={form.tipo ?? ""} onValueChange={v => setForm(s => ({ ...s, tipo: v }))}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Selecciona tipo…" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Obligatoria">
|
||||
<span className={typeStyle("Obligatoria").chip}>Obligatoria</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="Optativa">
|
||||
<span className={typeStyle("Optativa").chip}>Optativa</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="Taller">
|
||||
<span className={typeStyle("Taller").chip}>Taller</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="Laboratorio">
|
||||
<span className={typeStyle("Laboratorio").chip}>Laboratorio</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="Otro">
|
||||
<span className={typeStyle("Otro").chip}>Otro</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="Semestre">
|
||||
<Input value={String(form.semestre ?? "")} onChange={e => setForm(s => ({ ...s, semestre: Number(e.target.value) || null }))} />
|
||||
@@ -537,7 +567,7 @@ function MejorarAIButton({ asignaturaId, onApply }: {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Mejorar asignatura con IA</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Mejorar asignatura con IA</DialogTitle>
|
||||
<DialogDescription>
|
||||
Describe el ajuste que deseas (p. ej. “refuerza contenidos prácticos y añade bibliografía reciente”).
|
||||
</DialogDescription>
|
||||
@@ -589,7 +619,9 @@ function Field({ label, children }: { label: string; children: React.ReactNode }
|
||||
}
|
||||
|
||||
|
||||
function EditContenidosButton({
|
||||
type UnitDraft = { title: string; temas: string[] }
|
||||
|
||||
export function EditContenidosButton({
|
||||
asignaturaId,
|
||||
value,
|
||||
onSaved,
|
||||
@@ -600,137 +632,264 @@ function EditContenidosButton({
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
type UnitDraft = { title: string; temas: string[] }
|
||||
const [units, setUnits] = useState<UnitDraft[]>([])
|
||||
const [initialUnits, setInitialUnits] = useState<UnitDraft[]>([])
|
||||
|
||||
// Normaliza el JSON (acepta estructuras flexibles)
|
||||
function normalize(v: any): UnitDraft[] {
|
||||
// --- Normaliza entrada flexible a estructura estable
|
||||
const normalize = useCallback((v: any): UnitDraft[] => {
|
||||
try {
|
||||
const entries = Object.entries(v ?? {})
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([key, val]) => {
|
||||
const obj = val as any
|
||||
// soporta: { titulo, subtemas:{ "1":"t1" } } | { "1":"t1" } | ["t1","t2"]
|
||||
const title = (typeof obj?.titulo === 'string' && obj.titulo.trim()) ? obj.titulo.trim() : `Unidad ${key}`
|
||||
const title =
|
||||
typeof obj?.titulo === "string" && obj.titulo.trim()
|
||||
? obj.titulo.trim()
|
||||
: `Unidad ${key}`
|
||||
let temas: string[] = []
|
||||
if (Array.isArray(obj?.subtemas)) temas = obj.subtemas.map(String)
|
||||
else if (obj?.subtemas && typeof obj.subtemas === 'object') {
|
||||
temas = Object.entries(obj.subtemas).sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
|
||||
else if (obj?.subtemas && typeof obj.subtemas === "object") {
|
||||
temas = Object.entries(obj.subtemas)
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([, t]) => String(t))
|
||||
} else if (Array.isArray(obj)) {
|
||||
temas = obj.map(String)
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
const nums = Object.entries(obj).filter(([k, v]) => /^\d+$/.test(String(k)) && (typeof v === "string" || typeof v === "number"))
|
||||
if (nums.length) temas = nums.sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
|
||||
} else if (obj && typeof obj === "object") {
|
||||
const nums = Object.entries(obj).filter(
|
||||
([k, v]) => /^\d+$/.test(String(k)) && (typeof v === "string" || typeof v === "number"),
|
||||
)
|
||||
if (nums.length)
|
||||
temas = nums.sort(([a], [b]) => Number(a) - Number(b)).map(([, t]) => String(t))
|
||||
}
|
||||
return { title, temas }
|
||||
})
|
||||
return entries.length ? entries : [{ title: 'Unidad 1', temas: [] }]
|
||||
return entries.length ? entries : [{ title: "Unidad 1", temas: [] }]
|
||||
} catch {
|
||||
return [{ title: 'Unidad 1', temas: [] }]
|
||||
return [{ title: "Unidad 1", temas: [] }]
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Construye un JSON estable para guardar
|
||||
function buildPayload(us: UnitDraft[]) {
|
||||
const out: any = {}
|
||||
// --- Construye payload consistente { "1": { titulo, subtemas:{ "1": "t1" } } }
|
||||
const buildPayload = useCallback((us: UnitDraft[]) => {
|
||||
const out: Record<string, any> = {}
|
||||
us.forEach((u, idx) => {
|
||||
const k = String(idx + 1)
|
||||
const sub: any = {}
|
||||
u.temas.filter(t => t.trim()).forEach((t, i) => { sub[String(i + 1)] = t.trim() })
|
||||
out[k] = { titulo: u.title.trim() || `Unidad ${k}`, subtemas: sub }
|
||||
const sub: Record<string, string> = {}
|
||||
u.temas
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((t, i) => {
|
||||
sub[String(i + 1)] = t
|
||||
})
|
||||
out[k] = { titulo: (u.title || "").trim() || `Unidad ${k}`, subtemas: sub }
|
||||
})
|
||||
return out
|
||||
}, [])
|
||||
|
||||
// --- Limpia UI: recorta espacios, elimina líneas vacías/duplicadas (case-insensitive)
|
||||
const cleanUnits = useCallback((us: UnitDraft[]) => {
|
||||
return us.map((u, idx) => {
|
||||
const seen = new Set<string>()
|
||||
const temas = u.temas
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => {
|
||||
if (!t) return false
|
||||
const key = t.toLowerCase()
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
})
|
||||
return {
|
||||
title: (u.title || "").trim() || `Unidad ${idx + 1}`,
|
||||
temas,
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
const openEditor = () => {
|
||||
const base = normalize(value)
|
||||
setUnits(cleanUnits(base))
|
||||
setInitialUnits(cleanUnits(base))
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
function openEditor() {
|
||||
setUnits(normalize(value))
|
||||
setOpen(true)
|
||||
const hasChanges = useMemo(
|
||||
() => JSON.stringify(cleanUnits(units)) !== JSON.stringify(cleanUnits(initialUnits)),
|
||||
[units, initialUnits, cleanUnits],
|
||||
)
|
||||
|
||||
// --- Atajos: Guardar con Ctrl/Cmd + Enter
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
const ctrlOrCmd = e.ctrlKey || e.metaKey
|
||||
if (ctrlOrCmd && e.key.toLowerCase() === "enter") {
|
||||
e.preventDefault()
|
||||
void save()
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", handler)
|
||||
return () => window.removeEventListener("keydown", handler)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, units, saving])
|
||||
|
||||
// --- Acciones por unidad
|
||||
const removeUnit = (idx: number) => {
|
||||
if (!confirm("¿Eliminar esta unidad?")) return
|
||||
setUnits((prev) => prev.filter((_, i) => i !== idx))
|
||||
}
|
||||
const moveUnit = (idx: number, dir: -1 | 1) => {
|
||||
setUnits((prev) => {
|
||||
const next = [...prev]
|
||||
const j = idx + dir
|
||||
if (j < 0 || j >= next.length) return prev
|
||||
;[next[idx], next[j]] = [next[j], next[idx]]
|
||||
return next
|
||||
})
|
||||
}
|
||||
const duplicateUnit = (idx: number) => {
|
||||
setUnits((prev) => {
|
||||
const next = [...prev]
|
||||
next.splice(idx + 1, 0, {
|
||||
title: `${prev[idx].title} (copia)`,
|
||||
temas: [...prev[idx].temas],
|
||||
})
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
async function save() {
|
||||
setSaving(true)
|
||||
const contenidos = buildPayload(units)
|
||||
const cleaned = cleanUnits(units)
|
||||
const contenidos = buildPayload(cleaned)
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.from("asignaturas")
|
||||
.update({ contenidos })
|
||||
.eq('id', asignaturaId)
|
||||
.eq("id", asignaturaId)
|
||||
.select()
|
||||
.maybeSingle()
|
||||
setSaving(false)
|
||||
if (error) { alert(error.message || 'No se pudo guardar'); return }
|
||||
if (error) {
|
||||
alert(error.message || "No se pudo guardar")
|
||||
return
|
||||
}
|
||||
setInitialUnits(cleaned)
|
||||
onSaved((data as any)?.contenidos ?? contenidos)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
if (hasChanges && !confirm("Hay cambios sin guardar. ¿Cerrar de todos modos?")) return
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={openEditor}>
|
||||
<Icons.Pencil className="h-4 w-4 mr-2" /> Editar contenidos
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar contenidos</DialogTitle>
|
||||
<DialogDescription>
|
||||
Títulos de unidad y temas (un tema por línea). Se guardará en un formato consistente con <code>titulo</code> y <code>subtemas</code>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 max-h-[60vh] overflow-auto pr-1">
|
||||
{units.map((u, i) => (
|
||||
<div key={i} className="rounded-2xl border p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="font-medium text-sm">Unidad {i + 1}</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
title="Eliminar unidad"
|
||||
onClick={() => setUnits(prev => prev.filter((_, idx) => idx !== i))}
|
||||
>
|
||||
<Icons.Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label>Título</Label>
|
||||
<Input
|
||||
value={u.title}
|
||||
onChange={(e) => setUnits(prev => prev.map((uu, idx) => idx === i ? { ...uu, title: e.target.value } : uu))}
|
||||
placeholder={`Unidad ${i + 1}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Temas (uno por línea)</Label>
|
||||
<Textarea
|
||||
className="min-h-[120px]"
|
||||
value={u.temas.join('\n')}
|
||||
onChange={(e) => {
|
||||
const lines = e.target.value.split('\n')
|
||||
setUnits(prev => prev.map((uu, idx) => idx === i ? { ...uu, temas: lines } : uu))
|
||||
}}
|
||||
placeholder={`Tema 1\nTema 2\n…`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setUnits(prev => [...prev, { title: `Unidad ${prev.length + 1}`, temas: [] }])}
|
||||
>
|
||||
<Icons.Plus className="w-4 h-4 mr-2" /> Agregar unidad
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={(o) => (o ? openEditor() : cancel())}>
|
||||
<DialogContent className="max-w-4xl p-0 overflow-hidden">
|
||||
{/* Header sticky */}
|
||||
<div className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
|
||||
<DialogHeader className="px-6 pt-5 pb-3">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Icons.BookOpen className="h-5 w-5" />
|
||||
Editar contenidos
|
||||
{hasChanges && (
|
||||
<Badge variant="secondary" className="ml-1">Cambios sin guardar</Badge>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Títulos de unidad y temas (uno por línea). Se guardará con <code>titulo</code> y <code>subtemas</code>.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="px-6 pb-3 flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{units.length} unidad(es)</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Icons.Keyboard className="h-3 w-3" /> Atajo: <kbd className="px-1 border rounded">Ctrl/⌘</kbd>+<kbd className="px-1 border rounded">Enter</kbd>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
|
||||
<Button onClick={save} disabled={saving}>{saving ? 'Guardando…' : 'Guardar'}</Button>
|
||||
<ScrollArea className="max-h-[65vh] px-6">
|
||||
<div className="space-y-4 py-4">
|
||||
{units.map((u, i) => (
|
||||
<div key={i} className="rounded-2xl border p-4">
|
||||
<div className="flex items-center justify-between mb-3 gap-2">
|
||||
<div className="font-medium text-sm">Unidad {i + 1}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button size="icon" variant="ghost" title="Subir" onClick={() => moveUnit(i, -1)} disabled={i === 0}>
|
||||
<Icons.ArrowUp className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" title="Bajar" onClick={() => moveUnit(i, 1)} disabled={i === units.length - 1}>
|
||||
<Icons.ArrowDown className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" title="Duplicar unidad" onClick={() => duplicateUnit(i)}>
|
||||
<Icons.Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="mx-1 h-5" />
|
||||
<Button size="icon" variant="ghost" title="Eliminar unidad" onClick={() => removeUnit(i)}>
|
||||
<Icons.Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label>Título</Label>
|
||||
<Input
|
||||
value={u.title}
|
||||
onChange={(e) =>
|
||||
setUnits((prev) => prev.map((uu, idx) => (idx === i ? { ...uu, title: e.target.value } : uu)))
|
||||
}
|
||||
placeholder={`Unidad ${i + 1}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 md:col-span-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Temas (uno por línea)</Label>
|
||||
<Badge variant="outline">{u.temas.filter((t) => t.trim()).length} tema(s)</Badge>
|
||||
</div>
|
||||
<Textarea
|
||||
className="min-h-[140px]"
|
||||
value={u.temas.join("\n")}
|
||||
onChange={(e) => {
|
||||
const lines = e.target.value.split("\n")
|
||||
setUnits((prev) => prev.map((uu, idx) => (idx === i ? { ...uu, temas: lines } : uu)))
|
||||
}}
|
||||
placeholder={`Tema 1\nTema 2\n…`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
setUnits((prev) => [...prev, { title: `Unidad ${prev.length + 1}`, temas: [] }])
|
||||
}
|
||||
>
|
||||
<Icons.Plus className="w-4 h-4 mr-2" /> Agregar unidad
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="px-6 pb-5">
|
||||
<Button variant="outline" onClick={cancel}>Cancelar</Button>
|
||||
<Button onClick={save} disabled={saving}>
|
||||
{saving ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Icons.Loader2 className="h-4 w-4 animate-spin" /> Guardando…
|
||||
</span>
|
||||
) : (
|
||||
"Guardar"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -738,6 +897,7 @@ function EditContenidosButton({
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function EditBibliografiaButton({
|
||||
asignaturaId,
|
||||
value,
|
||||
@@ -779,7 +939,7 @@ function EditBibliografiaButton({
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar bibliografía</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Editar bibliografía</DialogTitle>
|
||||
<DialogDescription>Escribe una referencia por línea.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
|
||||
@@ -493,7 +493,7 @@ function RouteComponent() {
|
||||
<Dialog open={cloneOpen} onOpenChange={setCloneOpen}>
|
||||
<DialogContent className="w-[min(92vw,720px)] sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Clonar asignatura</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Clonar asignatura</DialogTitle>
|
||||
</DialogHeader>
|
||||
{cloneTarget && (
|
||||
<div className="grid gap-3">
|
||||
@@ -584,7 +584,7 @@ function RouteComponent() {
|
||||
<Dialog open={bulkOpen} onOpenChange={setBulkOpen}>
|
||||
<DialogContent className="w-[min(92vw,840px)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Carrito de asignaturas ({cart.length})</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Carrito de asignaturas ({cart.length})</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{cart.length === 0 ? (
|
||||
|
||||
@@ -182,7 +182,7 @@ function Page() {
|
||||
Plan
|
||||
</div>
|
||||
<DialogHeader className="p-0">
|
||||
<DialogTitle className="truncate text-xl sm:text-2xl">{planNombre}</DialogTitle>
|
||||
<DialogTitle className="font-mono" className="truncate text-xl sm:text-2xl">{planNombre}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
|
||||
<KpiChip icon={Icons.BookOpen} label="Asignaturas" value={kpis.total} />
|
||||
|
||||
@@ -199,7 +199,7 @@ function RouteComponent() {
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nueva facultad</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Nueva facultad</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<FormFields form={form} setForm={setForm} />
|
||||
@@ -223,7 +223,7 @@ function RouteComponent() {
|
||||
<Dialog open={editOpen} onOpenChange={(o) => { setEditOpen(o); if (!o) setEditing(null) }}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar facultad</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Editar facultad</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<FormFields form={form} setForm={setForm} />
|
||||
|
||||
@@ -236,7 +236,7 @@ function EditPlanButton({ plan }: { plan: PlanFull }) {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar plan</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Editar plan</DialogTitle>
|
||||
<DialogDescription>Actualiza datos básicos.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3">
|
||||
@@ -287,7 +287,7 @@ function AdjustAIButton({ plan }: { plan: PlanFull }) {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ajustar con IA</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Ajustar con IA</DialogTitle>
|
||||
<DialogDescription>Describe cómo quieres modificar el plan actual.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} placeholder="Ej.: Enfatiza ciberseguridad y proyectos prácticos…" className="min-h-[120px]" />
|
||||
@@ -489,7 +489,7 @@ function AddAsignaturaButton({ planId, onAdded }: { planId: string; onAdded?: ()
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="w-[min(92vw,760px)]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nueva asignatura</DialogTitle>
|
||||
<DialogTitle className="font-mono" >Nueva asignatura</DialogTitle>
|
||||
<DialogDescription>Elige cómo crearla: manual o generada por IA.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
@@ -367,7 +367,7 @@ function RouteComponent() {
|
||||
{/* Dialog de edición */}
|
||||
<Dialog open={!!editing} onOpenChange={(o) => { if (!o) setEditing(null) }}>
|
||||
<DialogContent className="w-[min(92vw,720px)] sm:max-w-xl md:max-w-2xl">
|
||||
<DialogHeader><DialogTitle>Editar usuario</DialogTitle></DialogHeader>
|
||||
<DialogHeader><DialogTitle className="font-mono" >Editar usuario</DialogTitle></DialogHeader>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-1"><Label>Nombre</Label><Input value={form.nombre ?? ""} onChange={(e) => setForm((s) => ({ ...s, nombre: e.target.value }))} /></div>
|
||||
<div className="space-y-1"><Label>Apellidos</Label><Input value={form.apellidos ?? ""} onChange={(e) => setForm((s) => ({ ...s, apellidos: e.target.value }))} /></div>
|
||||
@@ -452,7 +452,7 @@ function RouteComponent() {
|
||||
{/* Modal: Nuevo usuario */}
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogContent className="w-[min(92vw,720px)] sm:max-w-xl md:max-w-2xl">
|
||||
<DialogHeader><DialogTitle>Nuevo usuario</DialogTitle></DialogHeader>
|
||||
<DialogHeader><DialogTitle className="font-mono" >Nuevo usuario</DialogTitle></DialogHeader>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-1 md:col-span-2">
|
||||
|
||||
Reference in New Issue
Block a user