Files
sanihome/src/components/home/Header.tsx
T

387 lines
14 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { Category, CategoryNode, CategoryTreeGroup } from "@/lib/meshkee";
import { EmptyState } from "@/components/ui/EmptyState";
import { CartButton } from "@/components/cart/CartButton";
import { ChevronLeftIcon, CloseIcon, MenuIcon, UserIcon } from "@/components/ui/icons";
import { SearchBar } from "./SearchBar";
const navLinks = [
{ label: "فروشگاه", href: "/store-items" },
{ label: "فروش ویژه", href: "#" },
{ label: "درباره ما", href: "#" },
{ label: "سوالات متداول", href: "#" },
{ label: "تماس با ما", href: "/contact" },
];
const LOGIN_URL = "https://customer.sanihome.ir";
function Logo({ src, large = false }: { src?: string | null; large?: boolean }) {
return (
<a href="/" className="flex shrink-0 items-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src || "/images/logo.png"}
alt="سانی‌هوم"
className={
large
? "h-[65px] w-auto object-contain"
: "h-[70px] w-auto object-contain md:h-[86px]"
}
/>
</a>
);
}
function ChevronDownIcon({ className = "" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden>
<path
d="m6 9 6 6 6-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function flattenLevel2(categoryTree: CategoryTreeGroup[]): CategoryNode[] {
return categoryTree.flatMap((group) => group.items);
}
function MobileDrawer({
open,
onClose,
categoryTree,
logoUrl,
}: {
open: boolean;
onClose: () => void;
categoryTree: CategoryTreeGroup[];
logoUrl?: string | null;
}) {
const [categoriesOpen, setCategoriesOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const level2 = flattenLevel2(categoryTree);
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "";
return () => {
document.body.style.overflow = "";
};
}, [open]);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") onClose();
};
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [onClose]);
if (!mounted) return null;
return createPortal(
<div className="min-[1200px]:hidden" aria-hidden={!open}>
<div
onClick={onClose}
className={`fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm transition-opacity duration-300 ${
open ? "opacity-100" : "pointer-events-none opacity-0"
}`}
/>
<div
className={`fixed top-0 right-0 z-[110] h-dvh w-[85%] max-w-sm overflow-y-auto bg-surface shadow-2xl transition-transform duration-300 ease-out ${
open ? "translate-x-0" : "translate-x-full"
}`}
>
<div className="flex items-center justify-between border-b border-border p-5">
<Logo src={logoUrl} />
<button
type="button"
onClick={onClose}
aria-label="بستن منو"
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted transition-colors hover:bg-red-light hover:text-red"
>
<CloseIcon className="h-5 w-5" />
</button>
</div>
<nav className="flex flex-col text-sm">
<a href="/" className="border-b border-border px-5 py-4 transition-colors hover:bg-red-light/40 hover:text-red">
خانه
</a>
<div className="border-b border-border">
<button
type="button"
onClick={() => setCategoriesOpen((v) => !v)}
aria-expanded={categoriesOpen}
className="flex w-full items-center justify-between px-5 py-4 text-right transition-colors hover:bg-red-light/40 hover:text-red"
>
دسته‌بندی‌ها
<ChevronDownIcon
className={`h-4 w-4 shrink-0 transition-transform duration-200 ${
categoriesOpen ? "rotate-180" : ""
}`}
/>
</button>
<div
className={`grid transition-all duration-300 ease-out ${
categoriesOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
}`}
>
<div className="overflow-hidden">
{level2.length === 0 ? (
<div className="bg-background px-5 pb-4">
<EmptyState message="هنوز دسته‌بندی‌ای ثبت نشده است." />
</div>
) : (
<ul className="space-y-1 bg-background px-5 pb-4">
{level2.map((node) => (
<li key={node.id}>
<a
href={`/products?categoryId=${node.id}`}
className="block rounded-lg px-3 py-1.5 font-medium text-foreground transition-colors hover:bg-red-light hover:text-red"
>
{node.nameFa}
</a>
{node.children.length > 0 && (
<ul className="mr-3 space-y-1 border-r border-border pr-3">
{node.children.map((child) => (
<li key={child.id}>
<a
href={`/products?categoryId=${child.id}`}
className="block rounded-lg px-3 py-1 text-muted transition-colors hover:bg-red-light hover:text-red"
>
{child.nameFa}
</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
)}
</div>
</div>
</div>
{navLinks.map((link) => (
<a
key={link.label}
href={link.href}
className="border-b border-border px-5 py-4 transition-colors hover:bg-red-light/40 hover:text-red"
>
{link.label}
</a>
))}
<a
href={LOGIN_URL}
className="flex items-center gap-2 px-5 py-4 text-right font-medium transition-colors hover:bg-red-light/40 hover:text-red"
>
<UserIcon className="h-4 w-4" />
ورود
</a>
</nav>
</div>
</div>,
document.body,
);
}
export function Header({
categoryTree,
categories,
logoUrl,
}: {
categoryTree: CategoryTreeGroup[];
categories: Category[];
logoUrl?: string | null;
}) {
const [menuOpen, setMenuOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const level2 = flattenLevel2(categoryTree);
useEffect(() => {
const onClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setMenuOpen(false);
}
};
document.addEventListener("mousedown", onClickOutside);
return () => document.removeEventListener("mousedown", onClickOutside);
}, []);
return (
<header className="sticky top-0 z-50 border-b border-border bg-surface/95 backdrop-blur-sm">
<div className="mx-auto max-w-7xl px-4">
<div className="grid grid-cols-3 items-center gap-4 py-4 min-[1200px]:flex min-[1200px]:justify-between">
<div className="flex items-center justify-start">
<button
type="button"
onClick={() => setDrawerOpen(true)}
aria-label="منو"
className="flex h-10 w-10 items-center justify-center rounded-lg border border-border transition-colors hover:border-red hover:text-red min-[1200px]:hidden"
>
<MenuIcon className="h-5 w-5" />
</button>
<div className="hidden min-[1200px]:block">
<Logo src={logoUrl} />
</div>
</div>
<div className="flex items-center justify-center min-[1200px]:flex-1 min-[1200px]:px-6">
<div className="min-[1200px]:hidden">
<Logo src={logoUrl} large />
</div>
<div className="hidden w-full min-[1200px]:block">
<SearchBar />
</div>
</div>
<div className="flex items-center justify-end gap-3 text-sm">
<div className="hidden items-center gap-2 rounded-2xl bg-surface p-1.5 sm:flex">
<CartButton />
<span className="h-8 w-px bg-border" aria-hidden />
<a
href={LOGIN_URL}
className="flex items-center gap-2 rounded-xl py-1 pr-1 transition-opacity hover:opacity-80"
>
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-red text-white">
<UserIcon className="h-5 w-5" />
</span>
<span className="text-right leading-tight">
<span className="block text-sm font-bold whitespace-nowrap text-foreground">
حساب کاربری
</span>
<span className="block text-xs whitespace-nowrap text-muted">ورود/ثبت‌نام</span>
</span>
</a>
</div>
<div className="sm:hidden">
<CartButton />
</div>
</div>
</div>
<div className="border-t border-border py-3 min-[1200px]:hidden">
<SearchBar />
</div>
<nav className="relative hidden items-center gap-6 overflow-visible border-t border-border py-3 text-sm min-[1200px]:flex">
<div
ref={menuRef}
className="relative"
onMouseEnter={() => setMenuOpen(true)}
onMouseLeave={() => setMenuOpen(false)}
>
<button
type="button"
onClick={() => setMenuOpen((open) => !open)}
aria-expanded={menuOpen}
aria-haspopup="true"
className="flex shrink-0 items-center gap-2 rounded-lg bg-red px-4 py-2 font-medium text-white transition-colors hover:bg-red-dark"
>
<MenuIcon className="h-4 w-4" />
دسته‌بندی‌ها
</button>
{menuOpen && (
<div className="absolute top-full right-0 z-50 mt-0 max-h-[70vh] w-[min(92vw,900px)] animate-fade-up overflow-y-auto rounded-b-2xl border border-t-0 border-border bg-surface p-5 shadow-xl">
<div className="mb-4 flex items-end justify-between gap-3 border-b border-border pb-3">
<div>
<p className="font-bold text-red-dark">همه دسته‌بندی‌ها</p>
<p className="font-display text-[11px] tracking-wide text-muted uppercase">
Shop by Category
</p>
</div>
<a
href="/products"
className="flex items-center gap-1 text-xs font-medium text-red hover:text-red-dark"
>
مشاهده همه
<ChevronLeftIcon className="h-3.5 w-3.5" />
</a>
</div>
{level2.length === 0 ? (
<EmptyState message="هنوز دسته‌بندی‌ای ثبت نشده است." />
) : (
<div className="columns-1 gap-6 sm:columns-2 lg:columns-4">
{level2.map((node) => (
<div key={node.id} className="mb-4 break-inside-avoid">
<a
href={`/products?categoryId=${node.id}`}
className="text-sm font-bold text-red-dark transition-colors hover:text-red"
>
{node.nameFa}
</a>
{node.children.length > 0 && (
<ul className="mt-1.5 space-y-1.5">
{node.children.map((child) => (
<li key={child.id}>
<a
href={`/products?categoryId=${child.id}`}
className="text-sm text-muted transition-colors hover:text-red"
>
{child.nameFa}
</a>
</li>
))}
</ul>
)}
</div>
))}
</div>
)}
{categories.length > 0 && (
<div className="mt-5 grid grid-cols-2 gap-2 border-t border-border pt-4 sm:grid-cols-4">
{categories.slice(0, 4).map((cat) => (
<a
key={cat.id}
href={`/products?categoryId=${cat.id}`}
className="rounded-lg bg-red-light/50 px-3 py-2 text-center text-xs font-medium text-red-dark transition-colors hover:bg-red-light"
>
{cat.name}
</a>
))}
</div>
)}
</div>
)}
</div>
{navLinks.map((link) => (
<a
key={link.label}
href={link.href}
className="shrink-0 text-muted transition-colors hover:text-red"
>
{link.label}
</a>
))}
</nav>
</div>
<MobileDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
categoryTree={categoryTree}
logoUrl={logoUrl}
/>
</header>
);
}