feat: enhance RouteComponent with facultades color and improve StatCard UI
This commit is contained in:
@@ -165,6 +165,7 @@ function RouteComponent() {
|
|||||||
return () => ctx.revert()
|
return () => ctx.revert()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
const facColor = plan.carreras?.facultades?.color ?? null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative p-6 space-y-6">
|
<div className="relative p-6 space-y-6">
|
||||||
@@ -217,13 +218,22 @@ function RouteComponent() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{/* stats */}
|
{/* stats */}
|
||||||
<CardContent ref={statsRef} className="relative z-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<CardContent
|
||||||
<KV className="kv" label="Nivel" value={plan.nivel} />
|
ref={statsRef}
|
||||||
<KV className="kv" label="Duración" value={plan.duracion} />
|
className="relative z-10 grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]"
|
||||||
<KV className="kv" label="Créditos" value={plan.total_creditos} />
|
>
|
||||||
<KV className="kv" label="Asignaturas" value={asignaturasCount} />
|
<StatCard label="Nivel" value={plan.nivel ?? "—"} Icon={Icons.GraduationCap} accent={facColor} />
|
||||||
<KV className="kv" label="Creado" value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : '—'} />
|
<StatCard label="Duración" value={plan.duracion ?? "—"} Icon={Icons.Clock} accent={facColor} />
|
||||||
|
<StatCard label="Créditos" value={fmt(plan.total_creditos)} Icon={Icons.Coins} accent={facColor} />
|
||||||
|
<StatCard label="Asignaturas" value={fmt(asignaturasCount)} Icon={Icons.BookOpen} accent={facColor} />
|
||||||
|
<StatCard
|
||||||
|
label="Creado"
|
||||||
|
value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : "—"}
|
||||||
|
Icon={Icons.CalendarDays}
|
||||||
|
accent={facColor}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
|
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
|
||||||
@@ -231,12 +241,58 @@ function RouteComponent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hexToRgbA(hex?: string | null, a = .25) {
|
||||||
|
if (!hex) return `rgba(37,99,235,${a})`
|
||||||
|
const h = hex.replace("#", "")
|
||||||
|
const v = h.length === 3 ? h.split("").map(c => c + c).join("") : h
|
||||||
|
const n = parseInt(v, 16)
|
||||||
|
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255
|
||||||
|
return `rgba(${r},${g},${b},${a})`
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmt = (n?: number | null) => (n !== null && n !== undefined) ? Intl.NumberFormat().format(n) : "—"
|
||||||
/* ===== UI bits ===== */
|
/* ===== UI bits ===== */
|
||||||
function KV({ label, value, className = '' }: { label: string; value?: string | number | null; className?: string }) {
|
type StatProps = {
|
||||||
|
label: string
|
||||||
|
value?: React.ReactNode
|
||||||
|
Icon?: React.ComponentType<React.SVGProps<SVGSVGElement>>
|
||||||
|
accent?: string | null // color de facultad (hex) opcional
|
||||||
|
className?: string
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
function StatCard({ label, value = "—", Icon = Icons.Info, accent, className = "", title }: StatProps) {
|
||||||
|
const border = hexToRgbA(accent, .28)
|
||||||
|
const chipBg = hexToRgbA(accent, .08)
|
||||||
|
const glow = hexToRgbA(accent, .14)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-xl border p-4 shadow-sm hover:shadow-md transition-shadow ${className}`}>
|
<div
|
||||||
<div className="text-xs text-neutral-500">{label}</div>
|
className={`group relative overflow-hidden rounded-2xl border p-4 sm:p-5
|
||||||
<div className="text-base font-medium">{value ?? '—'}</div>
|
bg-white/70 dark:bg-neutral-900/60 backdrop-blur
|
||||||
|
shadow-sm hover:shadow-md transition-all ${className}`}
|
||||||
|
style={{ borderColor: border }}
|
||||||
|
title={title ?? (typeof value === "string" ? value : undefined)}
|
||||||
|
aria-label={`${label}: ${typeof value === "string" ? value : ""}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="text-xs text-neutral-500">{label}</div>
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center justify-center rounded-xl px-2.5 py-2 border"
|
||||||
|
style={{ borderColor: border, background: chipBg }}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4 opacity-80" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-1 text-2xl font-semibold tabular-nums tracking-tight truncate">
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* glow sutil en hover */}
|
||||||
|
<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%, ${glow}, transparent 60%)` }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user