Files
website/components/Header.tsx
T

518 lines
17 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { megaMenuTabs, navLinks, type MegaTab } from "@/data/home";
import {
fetchCurrentUser,
firstNameFor,
getAdminDashboardUrl,
getBusinessDashboardUrl,
getCustomerDashboardUrl,
type WebsiteAuthUser,
} from "@/lib/auth-user";
import { searchMeshkeeCategories } from "@/lib/meshkee-categories";
import { buildMegaMenuTabs } from "@/lib/mega-menu";
import { clearTokens, getCustomerLoginUrl } from "@/lib/session";
import MegaMenu from "./MegaMenu";
import styles from "./Header.module.css";
const languages = [
{ code: "en", label: "en", dir: "ltr" as const },
{ code: "ar", label: "ar", dir: "rtl" as const },
{ code: "fa", label: "fa", dir: "rtl" as const },
];
function pickDefaultTabId(tabs: MegaTab[]) {
return (
tabs.find((tab) => tab.id === "stores")?.id ??
tabs[0]?.id ??
megaMenuTabs[0].id
);
}
export default function Header() {
const [open, setOpen] = useState(false);
const [megaOpen, setMegaOpen] = useState(false);
const [langOpen, setLangOpen] = useState(false);
const [accountOpen, setAccountOpen] = useState(false);
const [language, setLanguage] = useState(languages[0]);
const [tabs, setTabs] = useState<MegaTab[]>(megaMenuTabs);
const [menuImages, setMenuImages] = useState<string[]>([]);
const [activeTabId, setActiveTabId] = useState(pickDefaultTabId(megaMenuTabs));
const [user, setUser] = useState<WebsiteAuthUser | null>(null);
const [authReady, setAuthReady] = useState(false);
const closeTimer = useRef<number | undefined>(undefined);
const headerRef = useRef<HTMLElement>(null);
const langRef = useRef<HTMLDivElement>(null);
const accountRef = useRef<HTMLDivElement>(null);
const activeTab =
tabs.find((tab) => tab.id === activeTabId) ?? tabs[0] ?? megaMenuTabs[0];
const customerUrl = getCustomerDashboardUrl();
const adminUrl = getAdminDashboardUrl();
const businessUrl = getBusinessDashboardUrl();
const firstName = firstNameFor(user);
const isSuperAdmin = user?.dashboard === "super_admin";
const ownsBusiness = Boolean(
user?.businesses.some((item) => item.isOwner) ||
user?.dashboard === "business",
);
const showMega = () => {
window.clearTimeout(closeTimer.current);
setMegaOpen(true);
};
const hideMega = () => {
closeTimer.current = window.setTimeout(() => setMegaOpen(false), 160);
};
const closeMega = () => {
window.clearTimeout(closeTimer.current);
setMegaOpen(false);
};
const selectLanguage = (code: string) => {
const next = languages.find((item) => item.code === code) ?? languages[0];
setLanguage(next);
setLangOpen(false);
document.documentElement.lang = next.code;
document.documentElement.dir = next.dir;
};
useEffect(() => {
const controller = new AbortController();
void (async () => {
try {
const categories = await searchMeshkeeCategories("", controller.signal);
if (controller.signal.aborted) return;
const nextTabs = buildMegaMenuTabs(categories);
if (!nextTabs.length) return;
setTabs(nextTabs);
setActiveTabId((current) =>
nextTabs.some((tab) => tab.id === current)
? current
: pickDefaultTabId(nextTabs),
);
} catch {
/* keep static fallback */
}
})();
return () => controller.abort();
}, []);
useEffect(() => {
const controller = new AbortController();
const api =
process.env.NEXT_PUBLIC_MARKETPLACE_API_URL?.replace(/\/$/, "") ||
"http://localhost:3001/api/v1";
void (async () => {
try {
const response = await fetch(`${api}/website/home-media`, {
signal: controller.signal,
cache: "no-store",
});
if (!response.ok || controller.signal.aborted) return;
const data = (await response.json()) as {
menuImages?: { imageUrl?: string }[];
};
if (controller.signal.aborted) return;
setMenuImages(
(data.menuImages ?? []).map((item) => item.imageUrl?.trim() ?? ""),
);
} catch {
/* keep built-in menu photos */
}
})();
return () => controller.abort();
}, []);
useEffect(() => {
const controller = new AbortController();
void (async () => {
try {
const next = await fetchCurrentUser(controller.signal);
if (!controller.signal.aborted) setUser(next);
} catch {
if (!controller.signal.aborted) setUser(null);
} finally {
if (!controller.signal.aborted) setAuthReady(true);
}
})();
return () => controller.abort();
}, []);
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeMega();
setLangOpen(false);
setAccountOpen(false);
}
};
const onPointerDown = (event: PointerEvent) => {
const target = event.target as Node;
if (!headerRef.current?.contains(target)) {
setMegaOpen(false);
}
if (!langRef.current?.contains(target)) {
setLangOpen(false);
}
if (!accountRef.current?.contains(target)) {
setAccountOpen(false);
}
};
window.addEventListener("keydown", onKey);
window.addEventListener("pointerdown", onPointerDown);
return () => {
window.removeEventListener("keydown", onKey);
window.removeEventListener("pointerdown", onPointerDown);
};
}, []);
function logout() {
clearTokens();
setUser(null);
setAccountOpen(false);
}
return (
<header
ref={headerRef}
className={`${styles.header} ${megaOpen ? styles.headerMega : ""}`}
onMouseLeave={hideMega}
>
<div className={styles.inner}>
<a href="/" className={styles.brand} onMouseEnter={closeMega}>
<img
className={styles.logo}
src="/images/icons/logo.png"
alt="IranBay"
width={40}
height={40}
/>
<span className={styles.brandText}>
<span className={styles.brandName}>IranBay</span>
<span className={styles.brandTag}>A gate to iran market</span>
</span>
</a>
<nav className={styles.nav} aria-label="Primary">
{navLinks.map((link) =>
"hasMenu" in link && link.hasMenu ? (
<div className={styles.dropdown} key={link.label}>
<button
className={`${styles.menuTrigger} ${megaOpen ? styles.menuTriggerOpen : ""}`}
type="button"
aria-expanded={megaOpen}
aria-controls="categories-mega"
onMouseEnter={showMega}
onFocus={showMega}
onClick={showMega}
>
{link.label}
<img
className={styles.chevron}
src="/images/icons/chevron.svg"
alt=""
width={7}
height={12}
/>
</button>
</div>
) : (
<a
key={link.label}
href={link.href}
className={styles.navLink}
onMouseEnter={closeMega}
onFocus={closeMega}
>
{link.label}
</a>
),
)}
</nav>
<div className={styles.actions} onMouseEnter={closeMega}>
<a href="/register" className={styles.iconBtn} aria-label="Join">
<img src="/images/icons/add.svg" alt="" width={30} height={30} />
</a>
<button className={styles.iconBtn} type="button" aria-label="Search">
<img src="/images/icons/search.svg" alt="" width={30} height={30} />
</button>
{authReady && user ? (
<div className={styles.account} ref={accountRef}>
<button
type="button"
className={styles.accountName}
aria-haspopup="menu"
aria-expanded={accountOpen}
onClick={() => setAccountOpen((value) => !value)}
>
{firstName}
<img
className={`${styles.accountChevron} ${accountOpen ? styles.accountChevronOpen : ""}`}
src="/images/icons/chevron.svg"
alt=""
width={7}
height={12}
/>
</button>
{accountOpen ? (
<div className={styles.accountMenu} role="menu">
<a
role="menuitem"
href={customerUrl}
className={styles.accountItem}
onClick={() => setAccountOpen(false)}
>
Customer dashboard
</a>
<a
role="menuitem"
href={`${customerUrl}/profile`}
className={styles.accountItem}
onClick={() => setAccountOpen(false)}
>
My profile
</a>
<a
role="menuitem"
href={`${customerUrl}/forgot-password`}
className={styles.accountItem}
onClick={() => setAccountOpen(false)}
>
Change password
</a>
{ownsBusiness ? (
<a
role="menuitem"
href={businessUrl}
className={styles.accountItem}
onClick={() => setAccountOpen(false)}
>
Business dashboard
</a>
) : null}
{isSuperAdmin ? (
<a
role="menuitem"
href={adminUrl}
className={styles.accountItem}
onClick={() => setAccountOpen(false)}
>
Super admin
</a>
) : null}
<button
type="button"
role="menuitem"
className={`${styles.accountItem} ${styles.accountItemDanger}`}
onClick={logout}
>
Log out
</button>
</div>
) : null}
</div>
) : (
<a
href={customerUrl}
className={styles.iconBtn}
aria-label="Customer dashboard"
title="Customer dashboard"
>
<img
className={styles.userIcon}
src="/images/icons/user.svg"
alt=""
width={16}
height={16}
/>
</a>
)}
<span className={styles.actionsDivider} aria-hidden />
<div className={styles.lang} ref={langRef}>
<button
className={styles.langTrigger}
type="button"
aria-haspopup="listbox"
aria-expanded={langOpen}
aria-label="Select language"
onClick={() => setLangOpen((value) => !value)}
>
{language.label}
<img
className={`${styles.langChevron} ${langOpen ? styles.langChevronOpen : ""}`}
src="/images/icons/chevron.svg"
alt=""
width={7}
height={12}
/>
</button>
{langOpen ? (
<ul className={styles.langMenu} role="listbox" aria-label="Languages">
{languages.map((item) => (
<li
key={item.code}
role="option"
aria-selected={item.code === language.code}
>
<button
type="button"
className={
item.code === language.code
? styles.langOptionActive
: styles.langOption
}
onClick={() => selectLanguage(item.code)}
>
{item.label}
</button>
</li>
))}
</ul>
) : null}
</div>
</div>
<button
className={styles.burger}
type="button"
aria-label="Open menu"
aria-expanded={open}
onClick={() => setOpen((value) => !value)}
>
<img src="/images/icons/menu.svg" alt="" width={22} height={22} />
</button>
</div>
{megaOpen ? (
<div className={styles.megaHost} onMouseEnter={showMega}>
<MegaMenu
tabs={tabs}
tab={activeTab}
menuImages={menuImages}
onTabHover={setActiveTabId}
/>
</div>
) : null}
{open ? (
<nav className={styles.mobileNav} aria-label="Mobile">
<a href="/" onClick={() => setOpen(false)}>
Home
</a>
{navLinks.map((link) => (
<a key={link.label} href={link.href} onClick={() => setOpen(false)}>
{link.label}
</a>
))}
<div className={styles.mobileMega}>
{tabs.map((tab) => (
<div key={tab.id}>
<p>{tab.label}</p>
{tab.groups.map((group) => (
<div key={`${tab.id}-${group.title}`}>
<p>{group.title}</p>
{group.links.map((item) => (
<a
href={item.href}
key={`${tab.id}-${group.title}-${item.label}-${item.href}`}
onClick={() => setOpen(false)}
>
{item.label}
</a>
))}
</div>
))}
</div>
))}
</div>
<div className={styles.mobileLang}>
<span>Language</span>
<div className={styles.mobileLangOptions}>
{languages.map((item) => (
<button
key={item.code}
type="button"
className={
item.code === language.code
? styles.langOptionActive
: styles.langOption
}
onClick={() => {
selectLanguage(item.code);
setOpen(false);
}}
>
{item.label}
</button>
))}
</div>
</div>
<div className={styles.mobileAccount}>
{user ? (
<>
<p className={styles.mobileAccountName}>{firstName}</p>
<a href={customerUrl} onClick={() => setOpen(false)}>
Customer dashboard
</a>
<a
href={`${customerUrl}/profile`}
onClick={() => setOpen(false)}
>
My profile
</a>
<a
href={`${customerUrl}/forgot-password`}
onClick={() => setOpen(false)}
>
Change password
</a>
{ownsBusiness ? (
<a href={businessUrl} onClick={() => setOpen(false)}>
Business dashboard
</a>
) : null}
{isSuperAdmin ? (
<a href={adminUrl} onClick={() => setOpen(false)}>
Super admin
</a>
) : null}
<button
type="button"
onClick={() => {
logout();
setOpen(false);
}}
>
Log out
</button>
</>
) : (
<a
href={getCustomerLoginUrl("/")}
onClick={() => setOpen(false)}
>
Sign in
</a>
)}
</div>
<div className={styles.mobileActions}>
<a href="/register" className={styles.iconBtn} aria-label="Join">
<img src="/images/icons/add.svg" alt="" width={30} height={30} />
</a>
<button className={styles.iconBtn} type="button" aria-label="Search">
<img src="/images/icons/search.svg" alt="" width={30} height={30} />
</button>
</div>
</nav>
) : null}
</header>
);
}