144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Bell, HelpCircle, Search, Menu } from "lucide-react";
|
|
import Sidebar from "@/components/Sidebar";
|
|
import { api, clearToken } from "@/lib/api";
|
|
import { useRef } from "react";
|
|
import { LogOut } from "lucide-react";
|
|
|
|
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
const router = useRouter();
|
|
const [me, setMe] = useState<any>(null);
|
|
const [org, setOrg] = useState<any>(null);
|
|
const [ready, setReady] = useState(false);
|
|
const [navOpen, setNavOpen] = useState(false);
|
|
const [profileOpen, setProfileOpen] = useState(false);
|
|
const profileRef = useRef<HTMLDivElement>(null);
|
|
function logout() {
|
|
clearToken();
|
|
router.replace("/login");
|
|
}
|
|
|
|
useEffect(() => {
|
|
function handleClick(e: MouseEvent) {
|
|
if (
|
|
profileRef.current &&
|
|
!profileRef.current.contains(e.target as Node)
|
|
) {
|
|
setProfileOpen(false);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("mousedown", handleClick);
|
|
|
|
return () =>
|
|
document.removeEventListener("mousedown", handleClick);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
api("/me")
|
|
.then((m) => {
|
|
setMe(m);
|
|
// Product Admins don't belong here — send them to the Super Admin Panel.
|
|
if (m.is_product_admin) {
|
|
router.replace("/admin");
|
|
return;
|
|
}
|
|
if (!m.has_org) {
|
|
router.replace("/onboarding");
|
|
return;
|
|
}
|
|
api("/organization").then(setOrg).catch(() => {});
|
|
setReady(true);
|
|
})
|
|
.catch((e) => {
|
|
clearToken();
|
|
router.replace("/login");
|
|
});
|
|
}, [router]);
|
|
|
|
if (!ready)
|
|
return <div className="min-h-screen grid place-items-center text-slate-400">Loading…</div>;
|
|
|
|
const initial = (me?.name?.[0] ?? "U").toUpperCase();
|
|
|
|
return (
|
|
<div className="flex min-h-screen bg-slate-50">
|
|
<Sidebar org={org} open={navOpen} onClose={() => setNavOpen(false)} />
|
|
<div className="flex-1 min-w-0">
|
|
<header className="sticky top-0 z-10 bg-slate-50/80 backdrop-blur px-4 md:px-8 py-4 flex items-center gap-4">
|
|
<button onClick={() => setNavOpen(true)} className="md:hidden h-10 w-10 grid place-items-center rounded-full bg-white border border-slate-200">
|
|
<Menu className="h-4 w-4 text-slate-600" />
|
|
</button>
|
|
<div className="flex-1">
|
|
<h1 className="text-lg font-bold text-slate-900">
|
|
Welcome back, {me?.name ?? "there"}! 👋
|
|
</h1>
|
|
<p className="text-sm text-slate-500">
|
|
Here's what's happening with your products today.
|
|
</p>
|
|
</div>
|
|
<div className="relative hidden md:block">
|
|
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
|
|
<input placeholder="Search anything..."
|
|
className="pl-9 pr-4 py-2 w-64 rounded-full border border-slate-200 bg-white text-sm
|
|
focus:outline-none focus:ring-2 focus:ring-brand-500/30" />
|
|
</div>
|
|
<button className="relative h-10 w-10 grid place-items-center rounded-full bg-white border border-slate-200">
|
|
<Bell className="h-4 w-4 text-slate-600" />
|
|
</button>
|
|
<button className="h-10 w-10 grid place-items-center rounded-full bg-white border border-slate-200">
|
|
<HelpCircle className="h-4 w-4 text-slate-600" />
|
|
</button>
|
|
<div className="relative" ref={profileRef}>
|
|
<button
|
|
onClick={() => setProfileOpen(!profileOpen)}
|
|
className="flex items-center gap-2 pl-2 rounded-lg hover:bg-slate-100 px-2 py-1"
|
|
>
|
|
<div className="h-9 w-9 rounded-full bg-brand-500 text-white grid place-items-center font-semibold text-sm">
|
|
{initial}
|
|
</div>
|
|
|
|
<div className="text-sm leading-tight hidden sm:block text-left">
|
|
<div className="font-semibold text-slate-800">
|
|
{me?.name}
|
|
</div>
|
|
|
|
<div className="text-[11px] text-slate-400 capitalize">
|
|
{me?.client_role}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{profileOpen && (
|
|
<div className="absolute right-0 mt-2 w-72 bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden z-50">
|
|
<div className="px-4 py-4">
|
|
<div className="font-semibold text-slate-900">
|
|
{me?.name}
|
|
</div>
|
|
|
|
<div className="text-sm text-slate-500 mt-1">
|
|
{me?.email}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t" />
|
|
|
|
<button
|
|
onClick={logout}
|
|
className="w-full flex items-center gap-2 px-4 py-3 hover:bg-red-50 text-red-600 font-medium transition-colors"
|
|
>
|
|
<LogOut className="h-4 w-4" />
|
|
Logout
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
<main className="px-4 md:px-8 pb-10">{children}</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|