Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user