Files

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>
);
}