Files
hope-events/frontend/src/components/layout/Sidebar.tsx
T
joshuaandClaude Sonnet 5 8e6cb542d9 Full site redesign, help system, and dashboard stats fixes
Multi-phase visual facelift (design tokens, dashboards, sidebar/navbar
shell, per-page help guides, and a layout/content pass across every
remaining page) plus backend fixes to the dashboard KPI stats:

- Admin/Supervisor dashboard KPIs (revenue, donations, registrations,
  tickets sold) now use a rolling trailing-month window (today back one
  calendar month, e.g. 9 May - 8 June if today is 8 June) instead of
  calendar month-to-date, which under-counted for most of the month.
  The comparison window shifts the same way, so like is still compared
  with like.
- Reports deep-links from those stat tiles now match the same window
  (range=trailing_month, replacing range=this_month).
- Design tokens (brand-* Tailwind scale + shadcn CSS variables), a
  site-wide contextual help button, fixed dashboard sidebar/navbar,
  Admin/Supervisor/Staff/User dashboard rebuilds backed by a new
  GET /api/stats/overview endpoint, a dedicated Contact page, Site
  Settings restyle with WhatsApp config folded in, and an Account
  activity feed backed by a new SecurityEvent model.
- Every remaining page (home, events, registration flow, auth, legal,
  payment results, and every Admin/Supervisor/Staff/User tool page)
  restyled onto the same design tokens, several with real layout
  upgrades (home hero, events list/detail, donate page, auth pages).
- 20+ new dedicated help guides so the whole site has page-specific
  help content instead of falling back to a generic guide.
- Assorted fixes surfaced along the way: donation-leg double-counting
  in payment stats, donations not counting toward revenue, refund
  netting in per-method report breakdowns, and donation
  over-allocation after a refund.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:00:10 +02:00

101 lines
3.9 KiB
TypeScript

"use client";
import React, { useState } from "react";
import Link from "next/link";
import { Calendar, User, LayoutDashboard, ShieldCheck, Globe, Menu, type LucideIcon } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { RoleBadge, type Role } from "@/components/shared/RoleBadge";
import { UserAvatar } from "@/components/shared/UserAvatar";
import { usePathname } from "next/navigation";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
const navLinks: { href: string; label: string; icon: LucideIcon; roles?: string[] }[] = [
{ href: "/dashboard/user", label: "My Events", icon: Calendar, roles: ["user", "staff", "supervisor", "admin"] },
{ href: "/dashboard/user/profile", label: "Profile & Security", icon: User, roles: ["user", "staff", "supervisor", "admin"] },
{ href: "/dashboard/staff", label: "Staff", icon: LayoutDashboard, roles: ["staff"] },
{ href: "/dashboard/supervisor", label: "Supervisor", icon: LayoutDashboard, roles: ["supervisor"] },
{ href: "/dashboard/admin", label: "Admin", icon: ShieldCheck, roles: ["admin"] },
{ href: "/dashboard/admin/settings", label: "Site Settings", icon: Globe, roles: ["admin"] },
];
function SidebarNavContent({ onNavigate }: { onNavigate?: () => void }) {
const { user } = useAuth();
const role = (user?.role || "user") as Role;
const pathname = usePathname();
const links = navLinks.filter(l => !l.roles || l.roles.includes(role));
return (
<div className="flex flex-col h-full">
{user && (
<div className="mb-6 flex items-center gap-3">
<UserAvatar name={user.name} />
<div className="min-w-0">
<div className="text-sm font-semibold text-gray-900 truncate">{user.name}</div>
<RoleBadge role={role} className="mt-0.5" />
</div>
</div>
)}
<nav className="space-y-1">
{links.map(l => {
const Icon = l.icon;
const active = pathname === l.href;
return (
<Link
key={l.href}
href={l.href}
onClick={onNavigate}
className={
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors " +
(active ? "bg-brand-50 text-brand-700 font-medium" : "text-gray-600 hover:bg-gray-50")
}
>
<Icon className="w-4 h-4 shrink-0" />
{l.label}
</Link>
);
})}
</nav>
</div>
);
}
/** Desktop sidebar — fixed/pinned, does not scroll with page content. */
export function Sidebar() {
return (
<aside className="hidden md:flex md:flex-col w-64 shrink-0 h-full overflow-y-auto border-r bg-white p-4">
<SidebarNavContent />
</aside>
);
}
/** Mobile equivalent — a slim top strip with a hamburger that opens the same nav in a drawer. */
export function MobileSidebar() {
const { user } = useAuth();
const role = (user?.role || "user") as Role;
const [open, setOpen] = useState(false);
return (
<div className="md:hidden border-b bg-white px-4 py-3 shadow-sm flex items-center justify-between shrink-0">
{user && (
<div className="flex items-center gap-2 min-w-0">
<UserAvatar name={user.name} className="w-8 h-8 text-xs" />
<div className="min-w-0">
<div className="text-sm font-semibold text-gray-900 truncate">{user.name}</div>
<RoleBadge role={role} />
</div>
</div>
)}
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<button type="button" aria-label="Open navigation" className="p-2 rounded-md hover:bg-gray-100 shrink-0">
<Menu className="w-5 h-5" />
</button>
</SheetTrigger>
<SheetContent side="left" className="w-72 p-4">
<SidebarNavContent onNavigate={() => setOpen(false)} />
</SheetContent>
</Sheet>
</div>
);
}