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