212 lines
7.0 KiB
TypeScript
212 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import AccountMenu from "@/components/AccountMenu";
|
|
import { useAuth } from "@/components/AuthProvider";
|
|
import { navItems, type NavItem } from "@/data/home";
|
|
import { getDisplayName } from "@/lib/userName";
|
|
import styles from "./DashboardShell.module.css";
|
|
|
|
const websiteUrl =
|
|
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, "") ||
|
|
"http://localhost:3000";
|
|
|
|
function hrefMatches(href: string, pathname: string) {
|
|
return pathname === href || pathname.startsWith(`${href}/`);
|
|
}
|
|
|
|
function activeChildHref(
|
|
children: { href: string }[],
|
|
pathname: string,
|
|
) {
|
|
let best = "";
|
|
for (const child of children) {
|
|
if (!hrefMatches(child.href, pathname)) continue;
|
|
if (child.href.length > best.length) best = child.href;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function groupActive(item: Extract<NavItem, { type: "group" }>, pathname: string) {
|
|
return activeChildHref(item.children, pathname) !== "";
|
|
}
|
|
|
|
export default function DashboardShell({
|
|
children,
|
|
}: {
|
|
children: React.ReactNode;
|
|
}) {
|
|
const pathname = usePathname();
|
|
const router = useRouter();
|
|
const { user, logout } = useAuth();
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const [openGroup, setOpenGroup] = useState<string | null>(() => {
|
|
const active = navItems.find(
|
|
(item) => item.type === "group" && groupActive(item, pathname),
|
|
);
|
|
return active?.type === "group" ? active.label : null;
|
|
});
|
|
const displayName = getDisplayName(user);
|
|
|
|
useEffect(() => {
|
|
const active = navItems.find(
|
|
(item) => item.type === "group" && groupActive(item, pathname),
|
|
);
|
|
if (active?.type === "group") setOpenGroup(active.label);
|
|
}, [pathname]);
|
|
|
|
return (
|
|
<div className={styles.shell}>
|
|
<aside className={`${styles.sidebar} ${menuOpen ? styles.sidebarOpen : ""}`}>
|
|
<div className={styles.brand}>
|
|
<img src="/images/icons/logo.png" alt="" width={48} height={48} />
|
|
<div className={styles.brandText}>
|
|
<span>IranBay</span>
|
|
<strong>Dashboard</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className={styles.nav} aria-label="Dashboard">
|
|
{navItems.map((item) => {
|
|
if (item.type === "group") {
|
|
const active = groupActive(item, pathname);
|
|
const expanded = openGroup === item.label;
|
|
return (
|
|
<div key={item.label} className={styles.navGroup}>
|
|
<button
|
|
type="button"
|
|
className={`${styles.navItem} ${styles.navGroupBtn} ${
|
|
active ? styles.navGroupActive : ""
|
|
}`}
|
|
aria-expanded={expanded}
|
|
onClick={() =>
|
|
setOpenGroup((current) =>
|
|
current === item.label ? null : item.label,
|
|
)
|
|
}
|
|
>
|
|
<img src={item.icon} alt="" width={18} height={18} />
|
|
<span>{item.label}</span>
|
|
<img
|
|
className={`${styles.navChevron} ${
|
|
expanded ? styles.navChevronOpen : ""
|
|
}`}
|
|
src="/images/icons/chevron.svg"
|
|
alt=""
|
|
width={5}
|
|
height={9}
|
|
/>
|
|
</button>
|
|
{expanded ? (
|
|
<div className={styles.navSub}>
|
|
{item.children.map((child) => {
|
|
const childActive =
|
|
child.href ===
|
|
activeChildHref(item.children, pathname);
|
|
return (
|
|
<Link
|
|
key={child.href}
|
|
href={child.href}
|
|
prefetch
|
|
className={`${styles.navSubItem} ${
|
|
childActive ? styles.navSubItemActive : ""
|
|
}`}
|
|
onClick={(event) => {
|
|
setMenuOpen(false);
|
|
// Ensure navigation even if soft-route is flaky.
|
|
if (
|
|
event.metaKey ||
|
|
event.ctrlKey ||
|
|
event.shiftKey ||
|
|
event.altKey ||
|
|
event.button !== 0
|
|
) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
router.push(child.href);
|
|
}}
|
|
>
|
|
{child.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const active =
|
|
item.href === "/"
|
|
? pathname === "/"
|
|
: pathname === item.href ||
|
|
pathname.startsWith(`${item.href}/`);
|
|
return (
|
|
<Link
|
|
key={item.label}
|
|
href={item.href}
|
|
className={`${styles.navItem} ${active ? styles.navItemActive : ""}`}
|
|
onClick={() => setMenuOpen(false)}
|
|
>
|
|
<img src={item.icon} alt="" width={18} height={18} />
|
|
<span>{item.label}</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
<div className={styles.sidebarFoot}>
|
|
<p className={styles.userLabel}>{displayName}</p>
|
|
<a href={websiteUrl} className={styles.siteLink}>
|
|
View website
|
|
</a>
|
|
<button
|
|
type="button"
|
|
className={`outline-btn ${styles.logoutBtn}`}
|
|
onClick={() => {
|
|
logout();
|
|
router.replace("/login");
|
|
}}
|
|
>
|
|
Log out
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{menuOpen ? (
|
|
<button
|
|
type="button"
|
|
className={styles.backdrop}
|
|
aria-label="Close menu"
|
|
onClick={() => setMenuOpen(false)}
|
|
/>
|
|
) : null}
|
|
|
|
<div className={styles.main}>
|
|
<header className={styles.topbar}>
|
|
<button
|
|
type="button"
|
|
className={styles.menuBtn}
|
|
aria-label="Open menu"
|
|
onClick={() => setMenuOpen(true)}
|
|
>
|
|
<img src="/images/icons/menu.svg" alt="" width={22} height={22} />
|
|
</button>
|
|
<div className={styles.topbarCopy}>
|
|
<p className={styles.eyebrow}>Admin workspace</p>
|
|
<p className={styles.topbarHint}>Marketplace control</p>
|
|
</div>
|
|
<div className={styles.topbarActions}>
|
|
<AccountMenu />
|
|
</div>
|
|
</header>
|
|
|
|
<div className={styles.content}>{children}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|