153 lines
6.4 KiB
TypeScript
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>;
|
|
}
|