create banner

This commit is contained in:
amirhosein.ashourloo
2026-08-17 10:29:46 +03:30
parent 1272977923
commit e5f040d3eb
27 changed files with 832 additions and 603 deletions
+264 -82
View File
@@ -1,7 +1,9 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { categories } from "@/data/sample";
import { createPortal } from "react-dom";
import type { Category, CategoryGroup } from "@/lib/meshkee";
import { EmptyState } from "@/components/ui/EmptyState";
const navLinks = [
{ label: "فروش ویژه", href: "#" },
@@ -10,31 +12,202 @@ const navLinks = [
{ label: "تماس با ما", href: "#" },
];
const megaColumns = [
{
title: "سرمایش و گرمایش",
en: "Cooling & Heating",
items: ["یخچال و فریزر", "کولر گازی", "آبگرمکن", "بخاری"],
},
{
title: "شستشو",
en: "Laundry & Care",
items: ["ماشین لباسشویی", "ماشین ظرفشویی", "خشک‌کن", "اتو بخار"],
},
{
title: "صوتی و تصویری",
en: "AV & Display",
items: ["تلویزیون", "ساندبار", "سینمای خانگی", "پروژکتور"],
},
{
title: "آشپزخانه",
en: "Kitchen",
items: ["مایکروویو", "جاروبرقی", "هود", "اجاق گاز"],
},
];
function Logo({ large = false }: { large?: boolean }) {
return (
<a href="/" className="flex shrink-0 items-center">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/images/logo.png"
alt="سانی‌هوم"
className={
large
? "h-[42px] w-auto object-contain md:h-[46px]"
: "h-8 w-auto md:h-9"
}
/>
</a>
);
}
export function Header() {
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 SearchBar() {
return (
<div className="flex overflow-hidden rounded-xl border border-border bg-background">
<input
type="search"
placeholder="جستجوی محصول، برند یا دسته‌بندی..."
className="flex-1 bg-transparent px-4 py-2.5 text-sm outline-none placeholder:text-muted"
/>
<button
type="button"
className="bg-red px-5 text-sm font-medium text-white transition-colors hover:bg-red-dark"
>
جستجو
</button>
</div>
);
}
function MobileDrawer({
open,
onClose,
categoryGroups,
}: {
open: boolean;
onClose: () => void;
categoryGroups: CategoryGroup[];
}) {
const [categoriesOpen, setCategoriesOpen] = useState(false);
const [mounted, setMounted] = useState(false);
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 />
<button
type="button"
onClick={onClose}
aria-label="بستن منو"
className="flex h-9 w-9 items-center justify-center rounded-lg text-xl text-muted transition-colors hover:bg-red-light hover:text-red"
>
✕
</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">
{categoryGroups.length === 0 ? (
<div className="bg-background px-5 pb-4">
<EmptyState message="هنوز دسته‌بندی‌ای ثبت نشده است." />
</div>
) : (
<div className="space-y-4 bg-background px-5 pb-4">
{categoryGroups.map((group) => (
<div key={group.id}>
<p className="mb-1.5 pt-3 text-xs font-bold text-red-dark">
{group.title}
</p>
<ul className="space-y-1">
{group.items.map((item) => (
<li key={item.name}>
<a
href="#"
className="block rounded-lg px-3 py-1.5 text-muted transition-colors hover:bg-red-light hover:text-red"
>
{item.name}
</a>
</li>
))}
</ul>
</div>
))}
</div>
)}
</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>
))}
<button
type="button"
className="flex items-center gap-2 px-5 py-4 text-right font-medium transition-colors hover:bg-red-light/40 hover:text-red"
>
<span aria-hidden>👤</span>
ورود
</button>
</nav>
</div>
</div>,
document.body,
);
}
export function Header({
categoryGroups,
categories,
}: {
categoryGroups: CategoryGroup[];
categories: Category[];
}) {
const [menuOpen, setMenuOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@@ -50,37 +223,31 @@ export function Header() {
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="flex items-center justify-between gap-4 py-4">
<div className="flex items-center gap-2">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-red text-lg font-bold text-white">
S
</div>
<div>
<p className="font-display text-xl font-bold tracking-tight">
<span className="text-red">SANI</span>
<span className="text-foreground">HOME</span>
</p>
<p className="text-xs text-muted">فروشگاه لوازم خانگی</p>
<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 text-xl transition-colors hover:border-red hover:text-red min-[1200px]:hidden"
>
<span aria-hidden>☰</span>
</button>
<div className="hidden min-[1200px]:block">
<Logo />
</div>
</div>
<div className="hidden flex-1 px-6 md:block">
<div className="flex overflow-hidden rounded-xl border border-border bg-background">
<input
type="search"
placeholder="جستجوی محصول، برند یا دسته‌بندی..."
className="flex-1 bg-transparent px-4 py-2.5 text-sm outline-none placeholder:text-muted"
/>
<button
type="button"
className="bg-red px-5 text-sm font-medium text-white transition-colors hover:bg-red-dark"
>
جستجو
</button>
<div className="flex items-center justify-center min-[1200px]:flex-1 min-[1200px]:px-6">
<div className="min-[1200px]:hidden">
<Logo large />
</div>
<div className="hidden w-full min-[1200px]:block">
<SearchBar />
</div>
</div>
<div className="flex items-center gap-3 text-sm">
<div className="flex items-center justify-end gap-3 text-sm">
<button
type="button"
className="hidden items-center gap-1.5 text-muted transition-colors hover:text-red sm:flex"
@@ -101,7 +268,11 @@ export function Header() {
</div>
</div>
<nav className="relative flex items-center gap-6 overflow-visible border-t border-border py-3 text-sm">
<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"
@@ -133,40 +304,45 @@ export function Header() {
</a>
</div>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{megaColumns.map((column) => (
<div key={column.title}>
<p className="mb-0.5 text-sm font-bold text-red-dark">{column.title}</p>
<p className="font-display mb-3 text-[10px] tracking-wider text-muted uppercase">
{column.en}
</p>
<ul className="space-y-2">
{column.items.map((item) => (
<li key={item}>
<a
href="#"
className="text-sm text-muted transition-colors hover:text-red"
>
{item}
</a>
</li>
))}
</ul>
{categoryGroups.length === 0 ? (
<EmptyState message="هنوز دسته‌بندی‌ای ثبت نشده است." />
) : (
<>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{categoryGroups.map((column) => (
<div key={column.id}>
<p className="mb-3 text-sm font-bold text-red-dark">{column.title}</p>
<ul className="space-y-2">
{column.items.map((item) => (
<li key={item.name}>
<a
href="#"
className="text-sm text-muted transition-colors hover:text-red"
>
{item.name}
</a>
</li>
))}
</ul>
</div>
))}
</div>
))}
</div>
<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="#"
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>
{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="#"
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>
@@ -182,6 +358,12 @@ export function Header() {
))}
</nav>
</div>
<MobileDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
categoryGroups={categoryGroups}
/>
</header>
);
}