feat: add EditAsignaturaButton and EditBibliografiaButton components for managing asignaturas
- Introduced EditAsignaturaButton for editing asignatura details with a dialog interface. - Added EditBibliografiaButton for managing bibliographic references with various utility actions (trim, dedupe, sort, import, export). - Created reusable Field, Section, and Stat components for better UI structure. - Implemented typeStyle utility for styling based on asignatura type. - Integrated new components into the existing asignatura route. - Updated package.json to include new dependencies for alert dialogs and tooltips. - Defined Asignatura type in a new types file for better type safety.
This commit is contained in:
@@ -25,6 +25,10 @@ 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"
|
||||
import { typeStyle } from "@/components/asignaturas/typeStyle"
|
||||
import { Stat } from "@/components/asignaturas/Stat"
|
||||
import { Section } from "@/components/asignaturas/Section"
|
||||
import { EditBibliografiaButton } from "@/components/asignaturas/EditBibliografíaButton"
|
||||
|
||||
/* ================== Tipos ================== */
|
||||
type Asignatura = {
|
||||
@@ -57,42 +61,7 @@ export const Route = createFileRoute("/_authenticated/asignatura/$asignaturaId")
|
||||
})
|
||||
|
||||
/* ================== Helpers UI ================== */
|
||||
function typeStyle(tipo?: string | null) {
|
||||
const t = (tipo ?? "").toLowerCase()
|
||||
if (t.includes("oblig")) return { chip: "bg-emerald-50 text-emerald-700 border-emerald-200", halo: "from-emerald-100/60" }
|
||||
if (t.includes("opt")) return { chip: "bg-amber-50 text-amber-800 border-amber-200", halo: "from-amber-100/60" }
|
||||
if (t.includes("taller")) return { chip: "bg-indigo-50 text-indigo-700 border-indigo-200", halo: "from-indigo-100/60" }
|
||||
if (t.includes("lab")) return { chip: "bg-sky-50 text-sky-700 border-sky-200", halo: "from-sky-100/60" }
|
||||
return { chip: "bg-neutral-100 text-neutral-700 border-neutral-200", halo: "from-primary/10" }
|
||||
}
|
||||
|
||||
function Stat({ icon: Icon, label, value }: { icon: any; label: string; value: string | number }) {
|
||||
return (
|
||||
<div className="rounded-2xl border bg-white/70 dark:bg-neutral-900/60 p-3 flex items-center gap-3">
|
||||
<div className="h-9 w-9 rounded-xl grid place-items-center border bg-white/80">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-neutral-500">{label}</div>
|
||||
<div className="text-lg font-semibold tabular-nums">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({ id, title, icon: Icon, children }: { id: string; title: string; icon: any; children: React.ReactNode }) {
|
||||
return (
|
||||
<section id={id} className="rounded-2xl border bg-white/70 dark:bg-neutral-900/60 p-4 scroll-mt-24">
|
||||
<header className="flex items-center gap-2 mb-2">
|
||||
<div className="h-8 w-8 rounded-lg grid place-items-center border bg-white/80"><Icon className="h-4 w-4" /></div>
|
||||
<h3 className="text-sm font-semibold">{title}</h3>
|
||||
</header>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/* ================== Página ================== */
|
||||
function Page() {
|
||||
const router = useRouter()
|
||||
const { a: aFromLoader, plan } = Route.useLoaderData() as { a: Asignatura; plan: PlanMini | null }
|
||||
@@ -256,7 +225,7 @@ function Page() {
|
||||
const t = (u.temas || []).find(([k]: any[]) => String(k).toLowerCase() === "titulo")?.[1]
|
||||
if (typeof t === "string" && t.trim()) return t
|
||||
return /^\s*\d+/.test(String(u.key))
|
||||
? (u.title && u.title !== u.key ? u.title : `Unidad ${u.key && Number(u.key) ? Number(u.key) + 1 : 1}`)
|
||||
? (u.title && u.title !== u.key ? u.title : `Unidad ${u.key && Number(u.key) ? Number(u.key) : 1}`)
|
||||
: (u.title || String(u.key))
|
||||
}
|
||||
const temasOf = (u: any): string[] => {
|
||||
@@ -294,7 +263,7 @@ function Page() {
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span className="font-medium">
|
||||
{/^\s*\d+/.test(String(u.key))
|
||||
? `Unidad ${u.key && Number(u.key) ? Number(u.key) + 1 : 1}${u.__title ? `: ${u.__title}` : ""}`
|
||||
? `Unidad ${u.key && Number(u.key) ? Number(u.key) : 1}${u.__title ? `: ${u.__title}` : ""}`
|
||||
: u.__title}
|
||||
</span>
|
||||
<span className="text-[11px] text-neutral-500">{u.__temas.length} tema(s)</span>
|
||||
@@ -895,65 +864,4 @@ export function EditContenidosButton({
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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('')
|
||||
|
||||
function openEditor() {
|
||||
setText((value ?? []).join('\n'))
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
async function save() {
|
||||
setSaving(true)
|
||||
const refs = text.split('\n').map(s => s.trim()).filter(Boolean)
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.update({ bibliografia: refs })
|
||||
.eq('id', asignaturaId)
|
||||
.select()
|
||||
.maybeSingle()
|
||||
setSaving(false)
|
||||
if (error) { alert(error.message || 'No se pudo guardar'); return }
|
||||
onSaved((data as any)?.bibliografia ?? refs)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="outline" onClick={openEditor}>
|
||||
<Icons.Pencil className="h-4 w-4 mr-2" /> Editar bibliografía
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-mono" >Editar bibliografía</DialogTitle>
|
||||
<DialogDescription>Escribe una referencia por línea.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
className="min-h-[260px]"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder={`Autor, Título, Editorial, Año\nDOI/URL\n…`}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Cancelar</Button>
|
||||
<Button onClick={save} disabled={saving}>{saving ? 'Guardando…' : 'Guardar'}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user