82 lines
4.0 KiB
TypeScript
82 lines
4.0 KiB
TypeScript
import { Link } from "@tanstack/react-router"
|
|
import * as Icons from "lucide-react"
|
|
import { useSuspenseQuery } from "@tanstack/react-query"
|
|
import { useMemo } from "react"
|
|
import { asignaturaExtraOptions } from "./planQueries"
|
|
import { SmallStat } from "./SmallStat"
|
|
|
|
export function AsignaturaPreviewCard({ asignatura }: { asignatura: { id: string; nombre: string; semestre: number | null; creditos: number | null } }) {
|
|
const { data: extra } = useSuspenseQuery(asignaturaExtraOptions(asignatura.id))
|
|
|
|
const horasT = extra?.horas_teoricas ?? null
|
|
const horasP = extra?.horas_practicas ?? null
|
|
const horasTot = (horasT ?? 0) + (horasP ?? 0)
|
|
|
|
const resumenContenidos = useMemo(() => {
|
|
const c = extra?.contenidos
|
|
if (!c) return { unidades: 0, temas: 0 }
|
|
const unidades = Object.keys(c).length
|
|
const temas = Object.values(c).reduce((acc, temasObj) => acc + Object.keys(temasObj || {}).length, 0)
|
|
return { unidades, temas }
|
|
}, [extra?.contenidos])
|
|
|
|
const tipo = (extra?.tipo ?? "").toLowerCase()
|
|
const tipoChip =
|
|
tipo.includes("oblig") ? "bg-emerald-50 text-emerald-700 border-emerald-200" :
|
|
tipo.includes("opt") ? "bg-amber-50 text-amber-800 border-amber-200" :
|
|
tipo.includes("taller") ? "bg-indigo-50 text-indigo-700 border-indigo-200" :
|
|
tipo.includes("lab") ? "bg-sky-50 text-sky-700 border-sky-200" :
|
|
"bg-neutral-100 text-neutral-700 border-neutral-200"
|
|
|
|
return (
|
|
<article className="group relative overflow-hidden rounded-2xl border bg-white/70 dark:bg-neutral-900/60 backdrop-blur p-4 shadow-sm hover:shadow-md transition-all hover:-translate-y-0.5" role="region" aria-label={asignatura.nombre}>
|
|
<div className="flex items-start gap-3">
|
|
<div className="h-9 w-9 rounded-xl grid place-items-center border bg-white/80">
|
|
<Icons.BookOpen className="h-4 w-4" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="font-medium truncate" title={asignatura.nombre}>{asignatura.nombre}</div>
|
|
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
|
|
{asignatura.semestre != null && (
|
|
<span className="inline-flex items-center gap-1 rounded-full border px-2 py-0.5">
|
|
<Icons.Calendar className="h-3 w-3" /> S{asignatura.semestre}
|
|
</span>
|
|
)}
|
|
{asignatura.creditos != null && (
|
|
<span className="inline-flex items-center gap-1 rounded-full border px-2 py-0.5">
|
|
<Icons.Coins className="h-3 w-3" /> {asignatura.creditos} cr
|
|
</span>
|
|
)}
|
|
{extra?.tipo && (
|
|
<span className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 ${tipoChip}`}>
|
|
<Icons.Tag className="h-3 w-3" /> {extra.tipo}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-3 grid grid-cols-3 gap-2 text-[11px]">
|
|
<SmallStat icon={Icons.Clock} label="Horas" value={horasTot || "—"} />
|
|
<SmallStat icon={Icons.BookMarked} label="Unidades" value={resumenContenidos.unidades || "—"} />
|
|
<SmallStat icon={Icons.ListTree} label="Temas" value={resumenContenidos.temas || "—"} />
|
|
</div>
|
|
|
|
<div className="mt-3 flex items-center justify-between">
|
|
<div className="text-[11px] text-neutral-500">
|
|
{horasT != null || horasP != null ? (
|
|
<>H T/P: {horasT ?? "—"}/{horasP ?? "—"}</>
|
|
) : (
|
|
<span className="opacity-70">Resumen listo</span>
|
|
)}
|
|
</div>
|
|
<Link to="/asignatura/$asignaturaId" params={{ asignaturaId: asignatura.id }} className="inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs hover:bg-neutral-50" title="Ver detalle">
|
|
Ver detalle <Icons.ArrowRight className="h-3.5 w-3.5" />
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="pointer-events-none absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity" style={{ background: "radial-gradient(600px 120px at 20% -10%, rgba(0,0,0,.06), transparent 60%)" }} />
|
|
</article>
|
|
)
|
|
}
|