518 lines
17 KiB
TypeScript
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>
|
|
);
|
|
}
|