Files
reui-blocks/blocks/navbar8.tsx
T

484 lines
14 KiB
TypeScript

"use client";
import { Menu, X } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import {
Sheet,
SheetClose,
SheetContent,
SheetTitle,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
interface MenuItem {
title: string;
url?: string;
className?: string;
links?: {
label: string;
description?: string;
url: string;
image?: string;
}[];
}
interface MobileNavigationMenuProps {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
}
interface DesktopMenuItemProps {
item: MenuItem;
index: number;
}
const LOGO = {
url: "https://www.ShadcnBlocks",
src: "/block-media/shadcnblocks/block/block-1.svg",
alt: "لوگو",
title: "پشتیبانی‌یار",
};
const NAVIGATION: MenuItem[] = [
{
title: "محصولات",
links: [
{
label: "وبلاگ",
description: "بینش پشتیبانی و به‌روزرسانی محصول",
url: "#",
image: "/block-media/shadcnblocks/block/placeholder-1.svg",
},
{
label: "پلتفرم ما",
description: "میز خدمت، SLA و پایگاه دانش در یک جا",
url: "#",
image: "/block-media/shadcnblocks/block/placeholder-2.svg",
},
{
label: "فرصت‌های شغلی",
description: "موقعیت‌های باز در تیم محصول و موفقیت مشتری",
url: "#",
image: "/block-media/shadcnblocks/block/placeholder-3.svg",
},
{
label: "پشتیبانی مشتریان",
description: "تماس با ما یا جستجو در پایگاه دانش",
url: "#",
image: "/block-media/shadcnblocks/block/placeholder-4.svg",
},
{
label: "مستندات محصول",
description: "راهنماها، API و مستندات یکپارچه‌سازی",
url: "#",
image: "/block-media/shadcnblocks/block/placeholder-5.svg",
},
],
},
{
title: "شرکت",
links: [
{
label: "درباره ما",
url: "#",
description: "مأموریت ما کاهش زمان پاسخ بدون افت کیفیت است",
image: "/block-media/shadcnblocks/block/placeholder-1.svg",
},
{
label: "مرکز راهنما",
url: "#",
description: "پاسخ سریع برای راه‌اندازی و مدیریت تیکت",
image: "/block-media/shadcnblocks/block/placeholder-2.svg",
},
{
label: "آخرین اخبار",
url: "#",
description: "با اعلان‌های محصول و بهبود SLA به‌روز بمانید",
image: "/block-media/shadcnblocks/block/placeholder-3.svg",
},
],
},
{
title: "تماس",
url: "#",
},
];
const MOBILE_NAVIGATION: MenuItem[] = [
{
title: "محصولات",
className: "col-span-2",
links: [
{ label: "وبلاگ", url: "#" },
{ label: "پلتفرم ما", url: "#" },
{ label: "فرصت‌های شغلی", url: "#" },
{ label: "پشتیبانی مشتریان", url: "#" },
{ label: "مستندات محصول", url: "#" },
],
},
{
title: "شرکت",
className: "",
links: [
{ label: "درباره ما", url: "#" },
{ label: "مرکز راهنما", url: "#" },
{ label: "آخرین اخبار", url: "#" },
],
},
{
title: "جامعه",
className: "",
links: [
{ label: "انجمن", url: "#" },
{ label: "گروه جامعه", url: "#" },
{ label: "شرکای پیاده‌سازی", url: "#" },
{ label: "رویدادها", url: "#" },
],
},
];
const NAV_BUTTONS: {
label: string;
url: string;
variant:
| "ghost"
| "default"
| "link"
| "destructive"
| "outline"
| "secondary";
}[] = [
{
label: "ورود",
url: "#",
variant: "ghost",
},
{
label: "شروع رایگان",
url: "#",
variant: "default",
},
];
const SOCIAL_LINKS = [
{ label: "لینکدین", url: "#" },
{ label: "توییتر", url: "#" },
{ label: "فیسبوک", url: "#" },
];
const MOBILE_BREAKPOINT = 1024;
interface Navbar8Props {
/** When true, keeps the navbar inside block preview instead of fixed viewport positioning. */
contained?: boolean;
className?: string;
}
const Navbar8 = ({ contained = false, className }: Navbar8Props) => {
const [open, setOpen] = useState<boolean>(false);
const navRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleResize = () => {
if (window.innerWidth > MOBILE_BREAKPOINT) {
setOpen(false);
}
};
const handleScroll = () => {
if (contained || !navRef.current) return;
navRef.current.classList.toggle("bg-background", window.scrollY > 300);
navRef.current.classList.toggle(
"bg-transparent",
!(window.scrollY > 300),
);
};
handleResize();
handleScroll();
window.addEventListener("resize", handleResize);
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("scroll", handleScroll);
};
}, [contained]);
useEffect(() => {
document.body.style.overflow = open ? "hidden" : "auto";
}, [open]);
const handleMobileMenu = () => {
setOpen((prev) => !prev);
};
return (
<section
lang="fa"
className={cn(
"text-start",
contained && "relative min-h-[5.5rem] overflow-visible",
className,
)}
>
<div
className={cn(
"z-500 w-full transition-colors duration-500",
contained ? "relative bg-background" : "fixed top-0 bg-transparent",
)}
ref={navRef}
>
<div className="container border-b">
<div className="flex items-center justify-between gap-3.5 py-5">
<a
href={LOGO.url}
className="flex max-h-8 items-center gap-2 text-lg font-semibold tracking-tighter"
>
<img
src={LOGO.src}
alt={LOGO.alt}
className="inline-block size-8"
/>
<span className="hidden md:inline-block">{LOGO.title}</span>
</a>
<NavigationMenu
dir="rtl"
viewport={false}
className="relative z-50 hidden lg:flex"
>
<NavigationMenuList>
{NAVIGATION.map((item, index) => (
<DesktopMenuItem
key={`desktop-link-${index}`}
item={item}
index={index}
/>
))}
</NavigationMenuList>
</NavigationMenu>
<div className="flex items-center gap-3.5">
{NAV_BUTTONS.map((button, index) => (
<Button
key={`nav-button-${index}`}
variant={button.variant}
asChild
>
<a href={button.url}>{button.label}</a>
</Button>
))}
<div className="lg:hidden">
<Button
variant="ghost"
size="icon"
onClick={handleMobileMenu}
aria-label={open ? "بستن منو" : "باز کردن منو"}
>
<Menu className="size-5.5" />
</Button>
</div>
</div>
</div>
</div>
</div>
<MobileNavigationMenu open={open} setOpen={setOpen} />
</section>
);
};
const DesktopMenuItem = ({ item, index }: DesktopMenuItemProps) => {
const imagesRef = useRef<HTMLImageElement[]>([]);
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
const linkIndex = Number(event.currentTarget.getAttribute("data-index"));
if (item.links && imagesRef.current[linkIndex]) {
imagesRef.current.forEach((img, i) => {
img.classList.toggle("opacity-100", i === linkIndex);
img.classList.toggle("opacity-0", i !== linkIndex);
});
}
};
const handleMouseLeave = () => {
imagesRef.current.forEach((img) => {
img.classList.remove("opacity-100");
img.classList.add("opacity-0");
});
imagesRef.current[0]?.classList.add("opacity-100");
};
if (item.links) {
return (
<NavigationMenuItem
key={`desktop-menu-item-${index}`}
value={`${index}`}
className="relative"
>
<NavigationMenuTrigger className="bg-transparent text-start [&_svg]:ms-1 [&_svg]:!ml-0">
{item.title}
</NavigationMenuTrigger>
<NavigationMenuContent
dir="rtl"
className="top-full left-1/2 z-50 mt-1.5 !w-auto -translate-x-1/2 max-w-[calc(100vw-2rem)] !rounded-2xl !p-0 text-start"
>
<div className="grid min-h-[18.75rem] w-[45.25rem] max-w-[calc(100vw-2rem)] grid-cols-[1fr_22.5rem] gap-4 p-3">
<div className="text-start">
<div className="p-4 text-start leading-normal font-bold">
{item.title}
</div>
<ul>
{item.links.map((link, linkIndex) => (
<li key={`desktop-nav-sublink-${linkIndex}`}>
<a
href={link.url}
className="flex items-center gap-4 rounded-lg px-4 py-3 text-start hover:bg-muted"
data-index={linkIndex}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div>
<h3 className="leading-normal font-medium">
{link.label}
</h3>
<p className="leading-normal text-muted-foreground">
{link.description}
</p>
</div>
</a>
</li>
))}
</ul>
</div>
<div className="relative aspect-square w-full overflow-hidden rounded-xl">
{item.links.map((link, linkIndex) => (
<img
key={linkIndex}
ref={(el) => {
if (el) {
imagesRef.current[linkIndex] = el;
}
}}
src={link.image}
alt={link.label}
className={cn(
"absolute top-0 start-0 h-full w-full object-cover transition-opacity duration-300",
linkIndex === 0 ? "opacity-100" : "opacity-0",
)}
/>
))}
</div>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
);
}
return (
<NavigationMenuItem
key={`desktop-menu-item-${index}`}
value={`${index}`}
className={`${navigationMenuTriggerStyle()} bg-transparent text-start`}
>
<NavigationMenuLink href={item.url}>{item.title}</NavigationMenuLink>
</NavigationMenuItem>
);
};
const MobileNavigationMenu = ({ open, setOpen }: MobileNavigationMenuProps) => {
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetContent
aria-describedby={undefined}
side="top"
dir="rtl"
className="inset-0 z-600 h-dvh w-full bg-primary text-start text-primary-foreground [&>button]:hidden"
>
<div className="flex-1 overflow-y-auto">
<div className="container pb-12">
<div className="absolute -m-px h-px w-px overflow-hidden border-0 mask-clip-border p-0 text-nowrap whitespace-nowrap">
<SheetTitle className="text-primary">منوی موبایل</SheetTitle>
</div>
<div className="flex justify-start pt-5">
<SheetClose asChild>
<Button
size="icon"
className="size-9 rounded-full bg-muted/20 hover:bg-muted/20"
aria-label="بستن منو"
>
<X className="size-5.5" />
</Button>
</SheetClose>
</div>
<div className="flex h-full flex-col justify-between gap-30 pt-24">
<div className="grid w-full grid-cols-2 gap-x-4 gap-y-10 text-start">
{MOBILE_NAVIGATION.map((item, index) =>
renderMobileMenuItem(item, index),
)}
</div>
<div className="col-span-2 flex flex-col gap-4 text-start">
<div className="text-xs text-foreground/60 uppercase">
شبکه‌های اجتماعی
</div>
<div className="flex gap-4">
{SOCIAL_LINKS.map((link, index) => (
<a
key={`social-link-${index}`}
href={link.url}
className="text-primary-foreground"
>
{link.label}
</a>
))}
</div>
</div>
</div>
</div>
</div>
</SheetContent>
</Sheet>
);
};
const renderMobileMenuItem = (item: MenuItem, index: number) => {
return (
<div
className={cn(
"flex flex-col gap-4 text-start text-primary-foreground",
item.className,
)}
key={`mobile-menu-item-${index}`}
>
<div className="text-xs text-foreground/60 uppercase">{item.title}</div>
<ul className="flex flex-col gap-3">
{item.links?.map((link, i) => (
<li key={`mobile-nav-link-${i}`}>
<a
href={link.url}
className={cn(
"text-primary-foreground",
index === 0 ? "text-xl" : "text-base",
"leading-normal font-medium",
)}
>
{link.label}
</a>
</li>
))}
</ul>
</div>
);
};
export { Navbar8 };