Files
reui-blocks/blocks/ecommerce-navbar2.tsx
T

1014 lines
28 KiB
TypeScript
Raw Normal View History

"use client";
import {
Bell,
Bookmark,
Bug,
ChevronDown,
CreditCard,
FileQuestion,
Heart,
HeartHandshake,
HelpCircle,
History,
LayoutGrid,
LifeBuoy,
LogOut,
type LucideIcon,
Menu,
MessageCircleHeart,
Search,
Settings,
ShoppingCart,
Star,
ThumbsUp,
User,
} from "lucide-react";
import {
type CSSProperties,
Fragment,
useEffect,
useMemo,
useState,
} from "react";
import {
Logo,
LogoImage,
LogoTextDesktop,
} from "@/components/shadcnblocks/logo";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTrigger,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
type HomeLink = {
href: string;
logo: {
src: string;
alt: string;
};
};
type Link = {
label: string;
href: string;
};
interface SideMenuItem extends Link {
icon: LucideIcon;
}
type HelpfullLinkGroup = {
group: string;
links: Link[];
};
type SideMenuType = Array<SideMenuItem[]>;
type MegaMenuSectionType = {
label: string;
href: string;
id: string;
items: {
label: string;
href: string;
}[];
};
type MenuItemType = {
label: string;
href?: string;
id?: string;
sections?: MegaMenuSectionType[];
};
type MenuType = MenuItemType[];
interface HelpfullLinksDropdownProps {
groups?: HelpfullLinkGroup[];
}
interface SecondaryMenuProps {
menu: MenuType;
}
interface SecondaryMenuMobileProps {
menu: MenuType;
}
type DesktopMenuDropdownItemProps = MenuItemType;
interface MobileMenuProps {
menu: SideMenuType;
homeLink: HomeLink;
}
interface EcommerceNavbar2Props {
home: HomeLink;
helpfullLinks?: HelpfullLinkGroup[];
menu: MenuType;
sideMenu: SideMenuType;
className?: string;
}
const HOME = {
href: "https://ShadcnBlocks",
logo: {
src: "/block-media/shadcnblocks/block/logos/ShadcnBlockscom-icon-white.svg",
alt: "لوگو",
},
};
const HELPFUL_LINKS = [
{
group: "پشتیبانی",
links: [
{ label: "پشتیبانی مشتریان", href: "#" },
{ label: "تماس با ما", href: "#" },
{ label: "سؤالات متداول", href: "#" },
{ label: "راهنمای محصول", href: "#" },
{ label: "گارانتی و تعمیر", href: "#" },
],
},
{
group: "سفارش و ارسال",
links: [
{ label: "پیگیری سفارش", href: "#" },
{ label: "اطلاعات ارسال", href: "#" },
{ label: "مرجوعی و بازپرداخت", href: "#" },
{ label: "لغو سفارش", href: "#" },
],
},
{
group: "ابزارهای خرید",
links: [
{ label: "یافتن فروشگاه", href: "#" },
{ label: "کارت هدیه", href: "#" },
{ label: "تخفیف دانشجویی", href: "#" },
{ label: "پیشنهادها و تخفیف‌ها", href: "#" },
],
},
{
group: "شرکت و قوانین",
links: [
{ label: "درباره ما", href: "#" },
{ label: "فرصت‌های شغلی", href: "#" },
{ label: "حریم خصوصی", href: "#" },
{ label: "شرایط و ضوابط", href: "#" },
{ label: "دسترسی‌پذیری", href: "#" },
],
},
];
const MENU = [
{
id: "shop",
label: "فروشگاه",
href: "#",
sections: [
{
label: "صورت",
id: "face",
href: "#",
items: [
"پاک‌کننده‌ها",
"پاک‌کننده‌های روغنی",
"پاک‌کننده‌های فومی",
"تونرها",
"اسنس‌ها",
"سرم‌ها",
"مرطوب‌کننده‌ها",
"کرم دور چشم",
"روغن‌های صورت",
"ماسک صورت",
"لایه‌بردارها",
"درمان لک",
].map((label) => ({ label, href: "#" })),
},
{
label: "بدن",
id: "body",
href: "#",
items: [
"شوینده بدن",
"اسکراب بدن",
"لوسیون بدن",
"کرم بدن",
"روغن بدن",
"کرم دست",
"مراقبت پا",
"درمان سفت‌کننده",
"ملزومات حمام",
"مراقبت پس از آفتاب",
].map((label) => ({ label, href: "#" })),
},
{
label: "محافظت در برابر آفتاب",
id: "sun",
href: "#",
items: [
"SPF روزانه",
"ضدآفتاب معدنی",
"ضدآفتاب شیمیایی",
"SPF رنگی",
"استیک SPF",
"ضدآفتاب صورت",
"ضدآفتاب بدن",
"SPF لب",
"مراقبت پس از آفتاب",
"اسپری ضدآفتاب",
].map((label) => ({ label, href: "#" })),
},
{
label: "پاکسازی",
id: "cleansing",
href: "#",
items: [
"میسلار واتر",
"پاک‌کننده ژلی",
"پاک‌کننده کرمی",
"مرهم پاک‌کننده",
"روغن پاک‌کننده",
"پاک‌کننده آرایش",
"پاک‌کننده لایه‌بردار",
"پاک‌کننده پوست حساس",
"سایز مسافرتی",
"بسته‌های شارژ",
].map((label) => ({ label, href: "#" })),
},
{
label: "درمان‌ها",
id: "treatments",
href: "#",
items: [
"درمان آکنه",
"درمان روشن‌کننده",
"درمان ضدپیری",
"تقویت‌کننده آبرسانی",
"پیلینگ",
"ماسک شبانه",
"آمپول",
"سرم اصلاحی",
"درمان آرام‌بخش",
"درمان ترمیمی",
].map((label) => ({ label, href: "#" })),
},
{
label: "ابزار و دستگاه",
id: "tools",
href: "#",
items: [
"غلتک صورت",
"گua شا",
"ماسک LED",
"برس پاکسازی",
"ماساژور صورت",
"غلتک یخی",
"ابزار منافذ",
"دستگاه میکروجریان",
"سرهای یدکی",
"ابزار مسافرتی",
].map((label) => ({ label, href: "#" })),
},
{
label: "ماسک‌ها",
id: "masks",
href: "#",
items: [
"ماسک ورقه‌ای",
"ماسک رس",
"ماسک خواب",
"ماسک لایه‌ای",
"ماسک آبرسان",
"ماسک پاک‌کننده",
"ماسک آرام‌بخش",
"ماسک روشن‌کننده",
"ماسک لایه‌بردار",
"ست مولتی‌ماسک",
].map((label) => ({ label, href: "#" })),
},
{
label: "مراقبت چشم و لب",
id: "eye-lip",
href: "#",
items: [
"کرم دور چشم",
"سرم دور چشم",
"ماسک چشم",
"مرهم لب",
"ماسک لب",
"اسکراب لب",
"درمان پف چشم",
"مراقبت تیرگی دور چشم",
"SPF لب",
"مراقبت شبانه لب",
].map((label) => ({ label, href: "#" })),
},
{
label: "مو و پوست سر",
id: "hair",
href: "#",
items: [
"درمان پوست سر",
"اسکراب پوست سر",
"سرم مو",
"روغن مو",
"ماسک مو",
"ضدآفتاب مو",
"مراقبت شوره",
"درمان رشد مو",
"مراقبت بدون آبکشی",
"ابزار مو",
].map((label) => ({ label, href: "#" })),
},
{
label: "حمام و سلامتی",
id: "wellness",
href: "#",
items: [
"روغن حمام",
"نمک حمام",
"محلول‌های حمام",
"رایحه‌درمانی",
"آرام‌بخش بدن",
"کاهش استرس",
"مراقبت خواب",
"روغن ماساژ",
"ست مراقبت شخصی",
"هدایای سلامتی",
].map((label) => ({ label, href: "#" })),
},
{
label: "مراقبت آقایان",
id: "men",
href: "#",
items: [
"شوینده صورت",
"مرطوب‌کننده",
"پس از اصلاح",
"مراقبت ریش",
"کرم دور چشم",
"ضدپیری",
"کنترل چربی",
"مراقبت بدن",
"کیت مسافرتی",
"ست اصلاح",
].map((label) => ({ label, href: "#" })),
},
{
label: "مراقبت پوست نوجوان",
id: "teen",
href: "#",
items: [
"پاک‌کننده ملایم",
"مراقبت آکنه",
"کنترل چربی",
"مرطوب‌کننده آرام‌بخش",
"درمان لک",
"SPF",
"کیت شروع",
"پوست حساس",
"سایز مسافرتی",
"ملزومات روزانه",
].map((label) => ({ label, href: "#" })),
},
{
label: "سایز مسافرتی",
id: "travel",
href: "#",
items: [
"پاک‌کننده کوچک",
"سرم کوچک",
"مرطوب‌کننده کوچک",
"ماسک کوچک",
"کیت مسافرتی",
"ظرف قابل شارژ",
"مناسب کابین",
"ست نمونه",
"کیت آخر هفته",
"مراقبت در مسیر",
].map((label) => ({ label, href: "#" })),
},
{
label: "هدیه و ست‌ها",
id: "gifts",
href: "#",
items: [
"ست هدیه",
"کیت شروع",
"بسته روتین",
"پرفروش‌ترین‌ها",
"ست مناسبتی",
"نسخه محدود",
"ست کوچک",
"هدایای لوکس",
"زیر ۵۰ ریال",
"زیر ۱۰۰ ریال",
].map((label) => ({ label, href: "#" })),
},
],
},
{
id: "collections",
label: "مجموعه‌ها",
href: "#",
sections: [
{
label: "بر اساس مشکل پوست",
id: "concerns",
href: "#",
items: [
"آکنه",
"خشکی",
"حساسیت",
"خطوط ریز",
"چین و چروک",
"لک",
"قرمزی",
"کدری",
"بافت ناهمگن",
"منافذ باز",
].map((label) => ({ label, href: "#" })),
},
{
label: "بر اساس نوع پوست",
id: "skin-type",
href: "#",
items: [
"نرمال",
"خشک",
"چرب",
"مختلط",
"حساس",
"بالغ",
"آکنه‌ای",
"کم‌آب",
"واکنش‌پذیر",
"همه انواع پوست",
].map((label) => ({ label, href: "#" })),
},
{
label: "بر اساس ماده فعال",
id: "ingredients",
href: "#",
items: [
"هیالورونیک اسید",
"ویتامین C",
"رتینول",
"نیاسینامید",
"سرامید",
"پپتید",
"سالیسیلیک اسید",
"AHA و BHA",
"باکوچیول",
"سنتلا آسیاتica",
].map((label) => ({ label, href: "#" })),
},
{
label: "بر اساس روتین",
id: "routine",
href: "#",
items: [
"روتین صبح",
"روتین شب",
"روتین مینیمال",
"روتین پیشرفته",
"درمان‌های هفتگی",
"روتین مسافرتی",
"مراقبت پس از درمان",
"مراقبت فصلی",
"آیین‌های مراقبت شخصی",
"الهام‌گرفته از حرفه‌ای‌ها",
].map((label) => ({ label, href: "#" })),
},
{
label: "زیبایی پاک و مسئولانه",
id: "clean",
href: "#",
items: [
"زیبایی پاک",
"وگان",
"بدون تست حیوانی",
"بدون بو",
"تست‌شده توسط متخصص پوست",
"مناسب پوست حساس",
"بسته‌بندی زیست‌محیطی",
"قابل بازیافت",
"قابل شارژ",
"برندهای پایدار",
].map((label) => ({ label, href: "#" })),
},
],
},
{ label: "آزمون پوست", href: "#" },
{ label: "درباره ما", href: "#" },
{ label: "وبلاگ", href: "#" },
{ label: "حراج", href: "#" },
];
const SIDE_MENU = [
[
{
label: "حساب کاربری",
href: "#",
icon: User,
},
{
label: "سوابق خرید",
href: "#",
icon: History,
},
{
label: "روش‌های پرداخت",
href: "#",
icon: CreditCard,
},
{
label: "تنظیمات حساب",
href: "#",
icon: Settings,
},
{
label: "خروج",
href: "#",
icon: LogOut,
},
],
[
{
label: "راهنما",
href: "#",
icon: HeartHandshake,
},
{
label: "راهنمای کاربران",
href: "#",
icon: HelpCircle,
},
{
label: "سؤالات متداول",
href: "#",
icon: FileQuestion,
},
{
label: "تیکت پشتیبانی",
href: "#",
icon: LifeBuoy,
},
],
[
{
label: "علاقه‌مندی‌ها",
href: "#",
icon: Heart,
},
{
label: "ذخیره‌شده‌ها",
href: "#",
icon: Bookmark,
},
{
label: "هشدار موجود شدن",
href: "#",
icon: Bell,
},
{
label: "اخیراً مشاهده‌شده",
href: "#",
icon: Star,
},
],
[
{
label: "بازخورد",
href: "#",
icon: MessageCircleHeart,
},
{
label: "امتیازدهی به تجربه",
href: "#",
icon: ThumbsUp,
},
{
label: "گزارش مشکل",
href: "#",
icon: Bug,
},
],
];
const EcommerceNavbar2 = ({
home = HOME,
helpfullLinks = HELPFUL_LINKS,
menu = MENU,
sideMenu = SIDE_MENU,
className,
}: EcommerceNavbar2Props) => {
useEffect(() => {
document.documentElement.style.setProperty(
"--primary-nav-height",
"5.25rem",
);
document.documentElement.style.setProperty(
"--secondary-nav-height",
"3.25rem",
);
}, []);
return (
<header
dir="rtl"
lang="fa"
className={cn("fixed inset-x-0 top-0 isolate z-50 text-start", className)}
>
<div className="flex h-[var(--primary-nav-height)] items-center gap-3 bg-primary px-6 py-4 text-primary-foreground">
<Logo url={home.href} className="shrink-0 p-2">
<LogoImage
src={home.logo.src}
alt={home.logo.alt}
className="size-8"
/>
<LogoTextDesktop className="font-medium text-primary-foreground">
ری یو آی
</LogoTextDesktop>
</Logo>
<div className="ms-auto flex items-center gap-3">
<div className="hidden lg:contents">
<HelpfullLinksDropdown groups={helpfullLinks} />
<Button
variant="ghost"
className="text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground"
>
<Heart />
علاقه‌مندی‌ها
</Button>
<Button
variant="ghost"
className="text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground"
>
<User />
حساب کاربری
</Button>
</div>
<SearchForm />
<div className="relative size-fit">
<Button
size="icon"
variant="ghost"
className="text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground"
aria-label="سبد خرید"
>
<ShoppingCart />
</Button>
<Badge className="absolute top-0 end-0 translate-x-1/2 -translate-y-1/2 bg-amber-500 text-[0.625rem] font-medium text-foreground">
۰
</Badge>
</div>
<div className="contents lg:hidden">
<MobileMenu menu={sideMenu} homeLink={home} />
</div>
</div>
</div>
<div className="relative h-[var(--secondary-nav-height)] overflow-visible">
<SecondaryMenu menu={menu} />
</div>
</header>
);
};
const SearchForm = () => {
return (
<Button
size="icon"
variant="ghost"
className="text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground"
aria-label="جستجو"
>
<Search />
</Button>
);
};
const HelpfullLinksDropdown = ({ groups }: HelpfullLinksDropdownProps) => {
if (!groups) return;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground data-[state=open]:bg-primary-foreground/10 data-[state=open]:text-primary-foreground"
>
<HeartHandshake className="size-4" />
راهنما و پشتیبانی
<ChevronDown className="rtl:rotate-180" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 !border-primary-foreground/20 !bg-primary !text-primary-foreground"
align="start"
>
{groups.map(({ group, links }, index) => (
<DropdownMenuSub key={index}>
<DropdownMenuSubTrigger className="**:!text-primary-foreground focus:bg-primary-foreground/10 focus:!text-primary-foreground data-[state=open]:bg-primary-foreground/10 data-[state=open]:!text-primary-foreground">
{group}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="!border-primary-foreground/20 !bg-primary !text-primary-foreground">
{links.map(({ label, href }, index) => (
<DropdownMenuItem
asChild
key={`${label}-${index}`}
className="focus:bg-primary-foreground/10 focus:text-primary-foreground"
>
<a href={href}>{label}</a>
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
const MobileMenu = ({ menu, homeLink }: MobileMenuProps) => {
return (
<Sheet>
<SheetTrigger asChild>
<Button
size="icon"
variant="ghost"
className="text-primary-foreground hover:bg-primary-foreground/10 hover:text-primary-foreground focus:bg-primary-foreground/10 focus:text-primary-foreground data-[state=open]:bg-primary-foreground/10 data-[state=open]:text-primary-foreground"
aria-label="باز کردن منو"
>
<Menu />
</Button>
</SheetTrigger>
<SheetContent side="right" className="hide-scrollbar overflow-auto">
<div className="px-4">
<SheetHeader>
<Logo url={homeLink.href} className="max-h-fit shrink-0 basis-11">
<LogoImage
src={homeLink.logo.src}
alt={homeLink.logo.alt}
className="size-11 shrink-0 invert dark:invert-0"
/>
</Logo>
</SheetHeader>
{menu.map((group, index) => (
<div key={index} className="py-6 not-last:border-b">
{group.map(({ href, label, icon: Icon }, index) => (
<Button
asChild
variant="ghost"
className="w-full justify-start text-start"
key={`side-menu-item-${index}`}
>
<a href={href}>
<Icon />
{label}
</a>
</Button>
))}
</div>
))}
</div>
</SheetContent>
</Sheet>
);
};
const SecondaryMenu = ({ menu }: SecondaryMenuProps) => {
return (
<div className="relative overflow-visible bg-accent px-6 py-2">
<div className="hidden lg:contents">
<NavigationMenu
dir="rtl"
viewport={false}
className="relative z-50 justify-start overflow-visible text-start [&_a]:bg-transparent [&_a]:hover:bg-accent-foreground/10 [&_button]:bg-transparent [&_button]:hover:bg-accent-foreground/10"
>
<NavigationMenuList className="gap-3.5">
{menu.map(({ sections, ...item }, index) => (
<NavigationMenuItem value={`${index}`} key={index} className="static">
{sections ? (
<DesktopMenuDropdownItem sections={sections} {...item} />
) : (
<NavigationMenuLink
asChild
className={cn(
navigationMenuTriggerStyle(),
"bg-transparent hover:bg-accent-foreground/10",
)}
>
<a href={item.href}>{item.label}</a>
</NavigationMenuLink>
)}
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavigationMenu>
</div>
<div className="lg:hidden">
<SecondaryMenuMobile menu={menu} />
</div>
</div>
);
};
const SecondaryMenuMobile = ({ menu }: SecondaryMenuMobileProps) => {
if (!menu) return;
return (
<Sheet modal={false}>
<SheetTrigger asChild>
<Button variant="secondary">
<LayoutGrid />
دسته‌بندی‌ها
</Button>
</SheetTrigger>
<SheetContent
side="top"
className="!top-[calc(var(--primary-nav-height)+var(--secondary-nav-height))] z-40 !h-[calc(100dvh-var(--primary-nav-height)-var(--secondary-nav-height))] overflow-hidden [&>button]:hidden"
>
<div className="min-h-0 flex-1 touch-pan-y overflow-y-auto overscroll-contain pb-10">
<div className="p-6">
{menu.map(({ sections, label, href }, index) => (
<div key={index}>
<h2 className="border-b py-4 text-lg leading-normal font-bold">
{href ? <a href={href}>{label}</a> : <span>{label}</span>}
</h2>
{sections && (
<Accordion type="multiple">
{sections.map(({ id, label, items }) => (
<AccordionItem value={id} key={`menu-section-${id}`}>
<AccordionTrigger>{label}</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col">
{items.map(({ href, label }, index) => (
<Button
className="w-full justify-start"
asChild
variant="ghost"
size="sm"
key={`menu-item-${index}`}
>
<a href={href}>{label}</a>
</Button>
))}
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</div>
))}
</div>
</div>
</SheetContent>
</Sheet>
);
};
const DesktopMenuDropdownItem = ({
sections,
label,
href,
}: DesktopMenuDropdownItemProps) => {
const [activeSectionId, setActiveSectionId] = useState<string>();
const sharedClasses = "h-full space-y-2 overflow-auto pt-6 pb-2";
const MenuItemClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const selectedSectionId =
e.currentTarget.getAttribute("data-id") ?? undefined;
setActiveSectionId(selectedSectionId);
};
const MenuItemMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
const selectedSectionId =
e.currentTarget.getAttribute("data-id") ?? undefined;
setActiveSectionId(selectedSectionId);
};
const activeSectionData = useMemo(
() => sections?.find((section) => section.id === activeSectionId),
[sections, activeSectionId],
);
if (!sections) return;
return (
<Fragment>
<NavigationMenuTrigger>{label}</NavigationMenuTrigger>
<NavigationMenuContent
dir="rtl"
className="start-0 end-auto z-50 !overflow-visible p-0"
>
<div
style={
{
"--menu-margin-bottom": "5rem",
} as CSSProperties
}
className="relative flex min-w-max items-stretch overflow-visible max-h-[calc(100dvh-var(--secondary-nav-height)-var(--primary-nav-height)-var(--menu-margin-bottom))]"
>
<div className="relative z-0 shrink-0">
<div className={cn("w-60", sharedClasses)}>
<h2 className="px-6 text-sm font-bold">
<a href={href}>{label}</a>
</h2>
<ol>
{sections.map((section, index) => (
<li key={index}>
<Button
variant="ghost"
data-id={section.id}
data-state={
section.id === activeSectionId ? "active" : "inactive"
}
onClick={MenuItemClick}
onMouseEnter={MenuItemMouseEnter}
className={cn(
"relative w-full justify-start rounded-none px-6 text-start font-normal",
"data-[state=active]:bg-accent",
"transition-opacity after:absolute after:inset-y-0 after:start-0 after:h-full after:w-1 after:bg-primary data-[state=inactive]:after:opacity-0",
)}
>
{section.label}
</Button>
</li>
))}
</ol>
</div>
</div>
{activeSectionData && (
<div className="relative z-10 shrink-0">
<div
className={cn(
"w-50 border-s bg-popover shadow-sm",
sharedClasses,
)}
>
<h2 className="px-6 text-sm font-bold">
{activeSectionData.label}
</h2>
<ol>
{activeSectionData.items.map((item, index) => (
<li key={index}>
<NavigationMenuLink
className="px-6 hover:underline"
asChild
>
<a href={item.href}>{item.label}</a>
</NavigationMenuLink>
</li>
))}
</ol>
</div>
</div>
)}
</div>
</NavigationMenuContent>
</Fragment>
);
};
export { EcommerceNavbar2 };