Files
eventsphere/apps/web/src/components/admin/AdminShell.tsx
2026-04-25 21:57:48 +01:00

153 lines
6.4 KiB
TypeScript

"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { BarChart3, CalendarDays, CheckCircle2, CreditCard, LayoutDashboard, Mail, QrCode, Settings, Users, Workflow, MessageCircle, Handshake, Contact, Plug } from "lucide-react";
import { clsx } from "clsx";
import { useEffect, useState } from "react";
import { apiFetch, clearAccessToken } from "@/lib/api";
import Image from "next/image";
const nav = [
["Dashboard", "/dashboard", LayoutDashboard, "dashboard"],
["Events", "/events", CalendarDays, "events"],
["Attendees", "/attendees", Users, "attendees"],
["Invitees", "/invitees", Mail, "invitees"],
["RSVPs", "/rsvps", CheckCircle2, "rsvps"],
["Registrations", "/registrations", Users, "registrations"],
["Check-in (Live)", "/check-in", QrCode, "checkins"],
["QR Codes", "/qr-codes", QrCode, "qrcodes"],
["Forms & Workflows", "/forms-workflows", Workflow, "workflows"],
["Calendar", "/calendar", CalendarDays, "calendar"],
["Communications", "/communications", MessageCircle, "communications"],
["Email Campaigns", "/email-campaigns", Mail, "communications"],
["WhatsApp Campaigns", "/whatsapp-campaigns", MessageCircle, "communications"],
["Payments", "/payments", CreditCard, "payments"],
["CRM Pipeline", "/crm-pipeline", Handshake, "crm"],
["Contacts / Leads", "/contacts-leads", Contact, "crm"],
["Reports", "/reports", BarChart3, "reports"],
["Settings", "/settings", Settings, "settings"],
["Integrations", "/integrations", Plug, "integrations"]
] as const;
export function AdminShell({ title, children }: { title: string; children: React.ReactNode }) {
const pathname = usePathname();
const [appName, setAppName] = useState<string>("EventSphere");
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [modules, setModules] = useState<Record<string, boolean> | null>(null);
const [me, setMe] = useState<{ fullName: string; email: string; avatarUrl?: string | null; mustChangePassword?: boolean } | null>(null);
useEffect(() => {
const load = async () => {
try {
const settings = await apiFetch<{ appName: string | null; logoUrl: string | null; modules: Record<string, boolean> | null }>("/settings");
setAppName(settings.appName ?? "EventSphere");
setLogoUrl(settings.logoUrl ?? null);
setModules(settings.modules ?? null);
} catch {
setAppName("EventSphere");
setLogoUrl(null);
setModules(null);
}
};
void load();
}, []);
useEffect(() => {
const load = async () => {
try {
const payload = await apiFetch<{ user: any }>("/auth/me");
const u = payload.user ?? null;
if (!u) return;
setMe({ fullName: String(u.fullName ?? "User"), email: String(u.email ?? ""), avatarUrl: u.avatarUrl ?? null, mustChangePassword: Boolean(u.mustChangePassword) });
} catch {
setMe(null);
}
};
void load();
}, []);
useEffect(() => {
if (me?.mustChangePassword && typeof window !== "undefined" && window.location.pathname !== "/change-password") {
window.location.href = "/change-password";
}
}, [me]);
useEffect(() => {
if (!modules) return;
const entry = nav.find((n) => n[1] === pathname);
if (!entry) return;
const key = entry[3];
if (key && modules[key] === false) {
window.location.href = "/dashboard";
}
}, [modules, pathname]);
return <div className="min-h-screen bg-surface">
<aside className="fixed inset-y-0 left-0 w-72 bg-[#06264A] p-5 text-white">
<div className="mb-8 flex items-center gap-3">
{logoUrl ? (
<Image src={logoUrl} alt={appName} width={44} height={44} className="h-11 w-11 rounded-xl object-cover" />
) : (
<div className="grid h-11 w-11 place-items-center rounded-xl bg-accent font-black">E</div>
)}
<div><div className="text-xl font-black">{appName}</div><div className="text-xs text-blue-100">Enterprise Event Management Platform</div></div>
</div>
<nav className="space-y-1">
{nav
.filter(([, , , key]) => (modules ? modules[key] !== false : true))
.map(([label, href, Icon]) => (
<Link
key={href}
href={href}
className={clsx(
"flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-semibold",
pathname === href ? "bg-accent text-white" : "text-blue-50 hover:bg-white/10"
)}
>
<Icon size={18} /> {label}
</Link>
))}
</nav>
<div className="absolute bottom-5 left-5 right-5 rounded-2xl border border-white/15 bg-white/5 p-4">
<div className="text-sm font-bold">EventSphere Team</div>
<div className="text-xs text-blue-100">Enterprise Plan</div>
<button className="mt-3 w-full rounded-xl bg-accent py-2 text-sm font-bold">View Plan</button>
</div>
</aside>
<main className="ml-72">
<header className="sticky top-0 z-10 flex h-20 items-center justify-between border-b border-line bg-white/90 px-8 backdrop-blur">
<div className="text-lg font-bold">{title}</div>
<div className="flex items-center gap-4">
<input className="w-72 rounded-xl border border-line px-4 py-2 text-sm" placeholder="Search anything..." />
<Link href="/profile" className="flex items-center gap-3 rounded-xl px-3 py-2 hover:bg-slate-100">
{me?.avatarUrl ? (
<Image src={me.avatarUrl} alt={me.fullName} width={34} height={34} className="h-9 w-9 rounded-xl object-cover" />
) : (
<div className="grid h-9 w-9 place-items-center rounded-xl bg-slate-200 text-xs font-black text-slate-700">SA</div>
)}
<div className="text-sm">
<b>{me?.fullName ?? "Super Admin"}</b>
<div className="text-xs text-slate-500">{me?.email ?? "Administrator"}</div>
</div>
</Link>
<button
className="rounded-xl border border-line px-3 py-2 text-sm font-semibold"
onClick={async () => {
try {
await apiFetch("/auth/logout", { method: "POST", body: JSON.stringify({}) });
} finally {
clearAccessToken();
window.location.href = "/login";
}
}}
>
Logout
</button>
</div>
</header>
<section className="p-8">{children}</section>
</main>
</div>;
}