484 lines
14 KiB
TypeScript
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 };
|