208 lines
6.4 KiB
TypeScript
208 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import AccountMenu from "@/components/AccountMenu";
|
|
import { useAuth } from "@/components/AuthProvider";
|
|
import { businessNavItems } from "@/data/business-nav";
|
|
import { en } from "@/lib/i18n/en";
|
|
import {
|
|
getAdminDashboardOrigin,
|
|
getCustomerDashboardOrigin,
|
|
} from "@/lib/host";
|
|
import { customerDashboardHref } from "@/lib/session-handoff";
|
|
import { getDisplayName } from "@/lib/userName";
|
|
import { getBusinessProfile } from "@/services/meshkee-businesses";
|
|
import styles from "./DashboardShell.module.css";
|
|
|
|
const websiteUrl =
|
|
process.env.NEXT_PUBLIC_WEBSITE_URL?.replace(/\/$/, "") ||
|
|
"http://localhost:3000";
|
|
|
|
export default function BusinessShell({
|
|
children,
|
|
manageMeshkeeId,
|
|
}: {
|
|
children: React.ReactNode;
|
|
/** When set, nav stays on admin host under /manage/[id]/… */
|
|
manageMeshkeeId?: string;
|
|
}) {
|
|
const pathname = usePathname();
|
|
const { user, logout, isSuperAdmin, activeBusinessId } = useAuth();
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const [businessName, setBusinessName] = useState("");
|
|
const [businessTitle, setBusinessTitle] = useState("");
|
|
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
|
const displayName = getDisplayName(user);
|
|
const linkedBusiness =
|
|
user?.businesses.find((item) => item.isOwner) ?? user?.businesses[0];
|
|
|
|
const meshkeeId = manageMeshkeeId || activeBusinessId || null;
|
|
|
|
const manageBase = manageMeshkeeId
|
|
? `/manage/${encodeURIComponent(manageMeshkeeId)}`
|
|
: null;
|
|
|
|
const publicPath = manageBase
|
|
? pathname.replace(manageBase, "") || "/profile"
|
|
: pathname.startsWith("/business")
|
|
? pathname.replace(/^\/business/, "") || "/"
|
|
: pathname;
|
|
|
|
useEffect(() => {
|
|
if (!meshkeeId) {
|
|
setBusinessName(linkedBusiness?.name?.trim() || "");
|
|
setBusinessTitle("");
|
|
setLogoUrl(null);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
void (async () => {
|
|
try {
|
|
const profile = await getBusinessProfile(meshkeeId);
|
|
if (cancelled) return;
|
|
const name =
|
|
profile.profile.nameEn?.trim() ||
|
|
profile.profile.nameFa?.trim() ||
|
|
linkedBusiness?.name?.trim() ||
|
|
"";
|
|
setBusinessName(name);
|
|
setBusinessTitle(profile.profile.title?.trim() || "");
|
|
setLogoUrl(profile.profile.logoUrl?.trim() || null);
|
|
} catch {
|
|
if (cancelled) return;
|
|
setBusinessName(linkedBusiness?.name?.trim() || "");
|
|
setBusinessTitle("");
|
|
setLogoUrl(null);
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [meshkeeId, linkedBusiness?.name]);
|
|
|
|
function navHref(itemHref: string) {
|
|
if (!manageBase) return itemHref;
|
|
return `${manageBase}${itemHref === "/" ? "/profile" : itemHref}`;
|
|
}
|
|
|
|
const brandName = businessName || linkedBusiness?.name || "Business";
|
|
|
|
return (
|
|
<div className={styles.shell}>
|
|
<aside className={`${styles.sidebar} ${menuOpen ? styles.sidebarOpen : ""}`}>
|
|
<div className={styles.brand}>
|
|
{logoUrl ? (
|
|
<img
|
|
className={styles.brandLogo}
|
|
src={logoUrl}
|
|
alt=""
|
|
width={48}
|
|
height={48}
|
|
/>
|
|
) : null}
|
|
<div className={styles.brandText}>
|
|
<strong className={styles.brandBusiness} dir="auto" title={brandName}>
|
|
{brandName}
|
|
</strong>
|
|
{businessTitle ? (
|
|
<span
|
|
className={styles.brandTitle}
|
|
dir="auto"
|
|
title={businessTitle}
|
|
>
|
|
{businessTitle}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<nav className={styles.nav} aria-label="Business">
|
|
{businessNavItems.map((item) => {
|
|
const href = navHref(item.href);
|
|
const active =
|
|
publicPath === item.href ||
|
|
publicPath.startsWith(`${item.href}/`);
|
|
return (
|
|
<Link
|
|
key={item.label}
|
|
href={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>
|
|
{isSuperAdmin || manageMeshkeeId ? (
|
|
<Link href="/businesses" className={styles.siteLink}>
|
|
Admin dashboard
|
|
</Link>
|
|
) : (
|
|
<a href={customerDashboardHref("/")} className={styles.siteLink}>
|
|
{en.openCustomerDashboard}
|
|
</a>
|
|
)}
|
|
<a href={websiteUrl} className={styles.siteLink}>
|
|
{en.viewWebsite}
|
|
</a>
|
|
<button
|
|
type="button"
|
|
className={`outline-btn ${styles.logoutBtn}`}
|
|
onClick={() => {
|
|
logout();
|
|
window.location.href =
|
|
isSuperAdmin || manageMeshkeeId
|
|
? `${getAdminDashboardOrigin()}/login`
|
|
: `${getCustomerDashboardOrigin()}/login`;
|
|
}}
|
|
>
|
|
{en.logOut}
|
|
</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}>
|
|
{manageMeshkeeId ? "Admin · managing business" : en.workspaceEyebrow}
|
|
</p>
|
|
<p className={styles.topbarHint}>{en.workspaceHint}</p>
|
|
</div>
|
|
<div className={styles.topbarActions}>
|
|
<AccountMenu />
|
|
</div>
|
|
</header>
|
|
|
|
<div className={styles.content}>{children}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|