v1.5 - history and timeline as well as many datastructure updates added, polished, fixes

This commit is contained in:
Zaldimmar 2026-09-25 02:38:51 -05:00
parent 1f0aa3078f
commit 1d84400aef
63 changed files with 7927 additions and 208 deletions

View file

@ -0,0 +1,182 @@
/* ═══════════════════════════════════════════════════════════════
ADMIN HOME
Where login lands. Two columns: the cards are the navigation —
the header deliberately drops its tab row here so the same links
aren't drawn twice — and a standing info panel on the right.
The cards come from adminNav.js, the same list the CMS header
reads, so a new entity shows up here the moment it's registered.
Forms sit in their own block below: they're submissions coming
in rather than content going out, and there'll be more of them
than the feedback queue eventually. The Panel block below that
only exists for superadmins.
The right-hand panel is deliberately inert. Nothing here
fetches, so the landing page can't be slow or half-broken on
arrival; anything live (open feedback count, last-edited
record) wants to be a separate component that fails on its own.
═══════════════════════════════════════════════════════════════ */
import { Link } from "react-router-dom";
import { useAuth } from "../../lib/auth.tsx";
import { SITE_VERSION } from "../../lib/version.ts";
import { ROLE_LABELS, isSuper } from "../../lib/roles.ts";
import { CMS_NAV, FORMS_NAV, PANEL_NAV, target } from "./adminNav.js";
/* One card. The title link is stretched over the whole card with
`after:absolute`, which makes the card clickable without nesting
an anchor inside an anchor; the sub-links sit above it on z-10 so
they stay separately clickable. */
function NavCard({ item }) {
const to = target(item);
// Drop the child that just repeats the card's own destination —
// "All organizations" under Organizations.
const extras = (item.children ?? []).filter((child) => child.to !== to);
return (
<div className="group relative flex flex-col rounded-2xl border border-[#138ba0]/20 bg-white p-5 transition-all hover:border-[#138ba0]/60 hover:shadow-sm">
<div className="flex items-baseline gap-3">
<h3 className="text-base font-semibold text-[#138ba0]">
<Link
to={to}
className="after:absolute after:inset-0 after:rounded-2xl after:content-['']"
>
{item.label}
</Link>
</h3>
<span
aria-hidden="true"
className="ml-auto text-[#138ba0] opacity-0 transition-opacity group-hover:opacity-100"
>
→
</span>
</div>
{item.blurb && (
<p className="mt-1.5 text-sm leading-relaxed text-[#4a6b72]">{item.blurb}</p>
)}
{extras.length > 0 && (
<div className="relative z-10 mt-4 flex flex-wrap gap-x-4 gap-y-1 border-t border-[#138ba0]/10 pt-3 text-sm">
{extras.map((child) => (
<Link
key={child.to}
to={child.to}
className="text-[#4a6b72] underline-offset-4 transition-colors hover:text-[#138ba0] hover:underline"
>
{child.label}
</Link>
))}
</div>
)}
</div>
);
}
function CardBlock({ title, blurb, children }) {
return (
<div className="mt-10">
<div className="flex items-baseline gap-3">
<h2 className="text-lg font-semibold text-[#138ba0]">{title}</h2>
{blurb && <p className="text-sm text-[#4a6b72]">{blurb}</p>}
</div>
<div className="mt-4 grid gap-4 sm:grid-cols-2">{children}</div>
</div>
);
}
function PanelSection({ title, children }) {
return (
<section className="border-b border-[#138ba0]/10 px-5 py-4 last:border-b-0">
<h2 className="text-xs font-semibold uppercase tracking-wider text-[#4a6b72]/70">
{title}
</h2>
<div className="mt-2.5 text-sm text-[#4a6b72]">{children}</div>
</section>
);
}
const QUICK_ADD = [
{ to: "/admin/events/new", label: "New event" },
{ to: "/admin/people/new", label: "New person" },
{ to: "/admin/organizations/new", label: "New organization" },
{ to: "/admin/timeline/new", label: "New timeline entry" },
];
export default function AdminHome() {
const { user } = useAuth();
const role = ROLE_LABELS[user?.role] ?? user?.role;
return (
<div className="grid gap-8 lg:grid-cols-[1fr_17rem] lg:items-start">
<div>
<h1 className="text-2xl font-bold text-[#138ba0]">
{user?.name ? `Welcome back, ${user.name.split(" ")[0]}.` : "Welcome back."}
</h1>
<p className="mt-1 text-[#4a6b72]">Pick a section to work in.</p>
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{CMS_NAV.map((item) => (
<NavCard key={item.label} item={item} />
))}
</div>
<CardBlock title={FORMS_NAV.label} blurb={FORMS_NAV.blurb}>
{FORMS_NAV.children.map((form) => (
<NavCard key={form.to} item={form} />
))}
</CardBlock>
{isSuper(user) && (
<CardBlock title="Superadmin" blurb="Only superadmins see this block.">
<NavCard item={PANEL_NAV} />
</CardBlock>
)}
</div>
{/* Sticky so it stays put once the card column outgrows it. */}
<aside className="divide-y divide-[#138ba0]/10 rounded-2xl border border-[#138ba0]/20 bg-white lg:sticky lg:top-6">
<PanelSection title="Signed in">
<p className="font-medium text-[#0f2f36]">{user?.name || user?.email}</p>
{user?.name && user?.email && (
<p className="mt-0.5 break-all text-xs text-[#4a6b72]/80">{user.email}</p>
)}
{role && (
<span className="mt-2 inline-block rounded-full bg-[#eef9fb] px-2.5 py-0.5 text-xs font-medium text-[#138ba0]">
{role}
</span>
)}
</PanelSection>
<PanelSection title="Start something">
<ul className="space-y-1.5">
{QUICK_ADD.map((link) => (
<li key={link.to}>
<Link
to={link.to}
className="underline-offset-4 transition-colors hover:text-[#138ba0] hover:underline"
>
{link.label}
</Link>
</li>
))}
</ul>
</PanelSection>
<PanelSection title="Site">
<p className="font-mono text-xs text-[#4a6b72]/80">{SITE_VERSION}</p>
<a
href="/"
target="_blank"
rel="noreferrer"
className="mt-2 inline-block underline-offset-4 transition-colors hover:text-[#138ba0] hover:underline"
>
View the public site ↗
</a>
</PanelSection>
</aside>
</div>
);
}