578 lines
16 KiB
TypeScript
578 lines
16 KiB
TypeScript
"use client";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import {
|
|
Book,
|
|
Cloud,
|
|
Code,
|
|
Database,
|
|
Gift,
|
|
Globe,
|
|
Heart,
|
|
Layout,
|
|
MenuIcon,
|
|
Monitor,
|
|
Paintbrush,
|
|
Server,
|
|
Settings,
|
|
Shield,
|
|
ShoppingCart,
|
|
Smartphone,
|
|
Sparkles,
|
|
Terminal,
|
|
Users,
|
|
X,
|
|
Zap,
|
|
} from "lucide-react";
|
|
import { Fragment, useEffect, useState } from "react";
|
|
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from "@/components/ui/accordion";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
NavigationMenu,
|
|
NavigationMenuContent,
|
|
NavigationMenuItem,
|
|
NavigationMenuLink,
|
|
NavigationMenuList,
|
|
NavigationMenuTrigger,
|
|
navigationMenuTriggerStyle,
|
|
} from "@/components/ui/navigation-menu";
|
|
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface MenuLink {
|
|
label: string;
|
|
description?: string;
|
|
url: string;
|
|
icon: LucideIcon;
|
|
}
|
|
|
|
interface MenuGroup {
|
|
title: string;
|
|
links: MenuLink[];
|
|
}
|
|
|
|
interface MenuItem {
|
|
title: string;
|
|
url?: string;
|
|
groups?: MenuGroup[];
|
|
}
|
|
|
|
interface DesktopMenuItemProps {
|
|
item: MenuItem;
|
|
index: number;
|
|
}
|
|
|
|
interface MobileNavigationMenuProps {
|
|
open: boolean;
|
|
}
|
|
|
|
const LOGO = {
|
|
url: "https://www.ShadcnBlocks",
|
|
src: "/block-media/shadcnblocks/block/block-1.svg",
|
|
alt: "لوگو",
|
|
title: "لیدهاب",
|
|
};
|
|
|
|
const NAVIGATION: MenuItem[] = [
|
|
{
|
|
title: "محصولات",
|
|
groups: [
|
|
{
|
|
title: "وبلاگ",
|
|
links: [
|
|
{
|
|
label: "بینشها",
|
|
icon: Book,
|
|
description: "اخبار محصول و بهروزرسانی CRM",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "مهندسی",
|
|
icon: Code,
|
|
description: "راهنمای فنی و API",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "فرهنگ",
|
|
icon: Users,
|
|
description: "ارزشها و نحوه کار تیم لیدهاب",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "مطبوعات",
|
|
icon: Globe,
|
|
description: "پوشش رسانهای",
|
|
url: "#",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: "ابزارهای توسعه",
|
|
links: [
|
|
{
|
|
label: "API",
|
|
icon: Monitor,
|
|
description: "دسترسی REST API",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "CLI",
|
|
icon: Terminal,
|
|
description: "ابزارهای CLI",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "SDKها",
|
|
icon: Code,
|
|
description: "یکپارچهسازی سریع با SDK",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "مستندات",
|
|
icon: Book,
|
|
description: "مستندات کامل محصول",
|
|
url: "#",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: "فروش",
|
|
links: [
|
|
{
|
|
label: "پلنها",
|
|
icon: ShoppingCart,
|
|
description: "انتخاب پلن مناسب تیم",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "اشتراکها",
|
|
icon: Database,
|
|
description: "گزینههای اشتراک تیم",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "اپ موبایل",
|
|
icon: Smartphone,
|
|
description: "مدیریت پایپلاین در حرکت",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "اعتبار معرفی",
|
|
icon: Gift,
|
|
description: "دعوت همکار با اعتبار اولیه",
|
|
url: "#",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: "راهحلها",
|
|
groups: [
|
|
{
|
|
title: "صنایع",
|
|
links: [
|
|
{
|
|
label: "خدمات B2B",
|
|
icon: ShoppingCart,
|
|
description: "راهحل برای تیمهای فروش میدانی",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "SaaS",
|
|
icon: Cloud,
|
|
description: "CRM برای تیمهای SaaS",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "فینتک",
|
|
icon: Shield,
|
|
description: "پایپلاین امن برای خدمات فینتک",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "سلامت دیجیتال",
|
|
icon: Heart,
|
|
description: "مدیریت لید برای خدمات سلامت دیجیتال",
|
|
url: "#",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: "ماژولها",
|
|
links: [
|
|
{
|
|
label: "ماژول",
|
|
icon: Layout,
|
|
description: "ماژولهای قابل استفاده مجدد CRM",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "فیلدها",
|
|
icon: Settings,
|
|
description: "مرجع فیلدهای سفارشی",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "ویجتها",
|
|
icon: Sparkles,
|
|
description: "ویجتهای داشبورد",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "قالبها",
|
|
icon: Paintbrush,
|
|
description: "قالبهای فضای کاری",
|
|
url: "#",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: "پلتفرم",
|
|
groups: [
|
|
{
|
|
title: "قابلیتهای اصلی",
|
|
links: [
|
|
{
|
|
label: "میز میزبان داده",
|
|
icon: Server,
|
|
description: "زیرساخت پایدار فرصتها",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "احراز هویت",
|
|
icon: Shield,
|
|
description: "ورود امن و نقشها",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "مخاطبین",
|
|
icon: Database,
|
|
description: "ذخیره مقیاسپذیر مخاطب و فعالیت",
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "اتوماسیون سریع",
|
|
icon: Zap,
|
|
description: "تریگرهای کمتأخیر برای پیگیری",
|
|
url: "#",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: "منابع",
|
|
url: "#",
|
|
},
|
|
{
|
|
title: "قیمتگذاری",
|
|
url: "#",
|
|
},
|
|
];
|
|
|
|
const DESKTOP_BUTTONS = [
|
|
{
|
|
label: "تماس",
|
|
isPrimary: false,
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "ورود",
|
|
isPrimary: false,
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "شروع رایگان",
|
|
isPrimary: true,
|
|
url: "#",
|
|
},
|
|
];
|
|
|
|
const MOBILE_BUTTONS = [
|
|
{
|
|
label: "شروع رایگان",
|
|
isPrimary: true,
|
|
url: "#",
|
|
},
|
|
{
|
|
label: "ورود",
|
|
isPrimary: false,
|
|
url: "#",
|
|
},
|
|
];
|
|
|
|
const MOBILE_BREAKPOINT = 1024;
|
|
|
|
interface Navbar11Props {
|
|
/** When true, positions inside block preview instead of the viewport. */
|
|
contained?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
const Navbar11 = ({ contained = false, className }: Navbar11Props) => {
|
|
const [open, setOpen] = useState<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
if (window.innerWidth > MOBILE_BREAKPOINT) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
|
|
handleResize();
|
|
|
|
window.addEventListener("resize", handleResize);
|
|
return () => {
|
|
window.removeEventListener("resize", handleResize);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
document.body.style.overflow = open ? "hidden" : "auto";
|
|
}, [open]);
|
|
|
|
const handleMobileMenu = () => {
|
|
const nextOpen = !open;
|
|
setOpen(nextOpen);
|
|
};
|
|
|
|
return (
|
|
<Fragment>
|
|
<section
|
|
lang="fa"
|
|
className={cn(
|
|
"pointer-events-auto z-999 flex h-16 w-full items-center justify-center bg-background text-start",
|
|
contained ? "relative" : "fixed top-0",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="container">
|
|
<div className="flex items-center justify-between gap-8">
|
|
<div className="flex items-center gap-8">
|
|
<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-6"
|
|
/>
|
|
<span className="hidden md:inline-block">{LOGO.title}</span>
|
|
</a>
|
|
<NavigationMenu
|
|
dir="rtl"
|
|
className="relative z-50 hidden xl:flex"
|
|
viewport={false}
|
|
>
|
|
<NavigationMenuList>
|
|
{NAVIGATION.map((item, index) => (
|
|
<DesktopMenuItem
|
|
key={`desktop-link-${index}`}
|
|
item={item}
|
|
index={index}
|
|
/>
|
|
))}
|
|
</NavigationMenuList>
|
|
</NavigationMenu>
|
|
</div>
|
|
<div className="hidden items-center gap-3 xl:flex">
|
|
{DESKTOP_BUTTONS.map((btn, index) => (
|
|
<Button
|
|
size="sm"
|
|
variant={!btn.isPrimary ? "outline" : "default"}
|
|
className={
|
|
btn.isPrimary
|
|
? "text-primary-foreground"
|
|
: "text-foreground"
|
|
}
|
|
asChild
|
|
key={`navbar-btn-${index}`}
|
|
>
|
|
<a href={btn.url}>{btn.label}</a>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<div className="xl:hidden">
|
|
<Button
|
|
className="size-11"
|
|
variant="ghost"
|
|
onClick={handleMobileMenu}
|
|
aria-label={open ? "بستن منو" : "باز کردن منو"}
|
|
>
|
|
{open ? (
|
|
<X className="size-5.5 stroke-foreground" />
|
|
) : (
|
|
<MenuIcon className="size-5.5 stroke-foreground" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<MobileNavigationMenu open={open} />
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
const DesktopMenuItem = ({ item, index }: DesktopMenuItemProps) => {
|
|
if (item.groups) {
|
|
return (
|
|
<NavigationMenuItem key={`desktop-menu-item-${index}`} value={`${index}`}>
|
|
<NavigationMenuTrigger className="h-fit bg-transparent px-2.5 text-start font-normal text-muted-foreground [&_svg]:ms-1 [&_svg]:!ml-0">
|
|
{item.title}
|
|
</NavigationMenuTrigger>
|
|
<NavigationMenuContent
|
|
dir="rtl"
|
|
className="start-0 end-auto left-auto z-50 !rounded-xl !border !p-0 text-start"
|
|
>
|
|
<ul
|
|
className="flex flex-row-reverse p-2 text-start"
|
|
style={{ width: item.groups.length * 248 }}
|
|
>
|
|
{item.groups.map((group, index1) => (
|
|
<li className="flex-1" key={`desktop-group-${index1}`}>
|
|
<ul>
|
|
<li className="px-3 py-2 text-start text-sm leading-normal text-muted-foreground">
|
|
{group.title}
|
|
</li>
|
|
{group.links.map((link, index2) => (
|
|
<li key={`desktop-links-${index1}-${index2}`}>
|
|
<NavigationMenuLink
|
|
asChild
|
|
className="group/link flex-row gap-2 px-3 py-2 text-start transition-colors duration-200"
|
|
>
|
|
<a href={link.url} className="text-start">
|
|
<div className="flex size-8 shrink-0 rounded-lg border duration-400 fade-in group-hover/link:bg-background">
|
|
<link.icon className="m-auto size-4 group-hover/link:stroke-black" />
|
|
</div>
|
|
<div className="flex min-w-0 flex-col gap-[2px] text-start">
|
|
<div className="text-sm font-medium">
|
|
{link.label}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground group-hover/link:text-foreground">
|
|
{link.description}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</NavigationMenuLink>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<NavigationMenuItem key={`desktop-menu-item-${index}`} value={`${index}`}>
|
|
<NavigationMenuLink
|
|
href={item.url}
|
|
className={`${navigationMenuTriggerStyle()} h-fit bg-transparent px-2.5 text-start font-normal text-muted-foreground`}
|
|
>
|
|
{item.title}
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
);
|
|
};
|
|
|
|
const MobileNavigationMenu = ({ open }: MobileNavigationMenuProps) => {
|
|
return (
|
|
<Sheet open={open}>
|
|
<SheetContent
|
|
aria-describedby={undefined}
|
|
side="top"
|
|
className="dark inset-0 z-998 h-dvh w-full bg-background pt-[3.9375rem] [&>button]:hidden"
|
|
>
|
|
<div className="h-full overflow-y-auto pt-10 pb-20">
|
|
<div className="container">
|
|
<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 flex-col gap-6">
|
|
<div className="flex flex-col gap-4">
|
|
{MOBILE_BUTTONS.map((btn, index) => (
|
|
<Button
|
|
variant={!btn.isPrimary ? "outline" : "default"}
|
|
className={
|
|
btn.isPrimary
|
|
? "text-primary-foreground"
|
|
: "text-foreground"
|
|
}
|
|
asChild
|
|
key={`navbar-btn-${index}`}
|
|
>
|
|
<a href={btn.url}>{btn.label}</a>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
<Accordion
|
|
type="multiple"
|
|
dir="rtl"
|
|
className="w-full text-start"
|
|
>
|
|
{NAVIGATION.map((item, index) =>
|
|
renderMobileMenuItem(item, index),
|
|
)}
|
|
</Accordion>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
};
|
|
|
|
const renderMobileMenuItem = (item: MenuItem, index: number) => {
|
|
if (item.groups) {
|
|
return (
|
|
<AccordionItem
|
|
key={item.title}
|
|
value={`nav-${index}`}
|
|
className="border-b-0"
|
|
>
|
|
<AccordionTrigger className="h-[3.75rem] items-center justify-start p-0 !px-4 text-start text-base leading-[3.75] font-normal text-muted-foreground hover:bg-muted hover:no-underline [&>svg]:ms-auto [&>svg]:me-0">
|
|
{item.title}
|
|
</AccordionTrigger>
|
|
<AccordionContent className="max-h-[60dvh] overflow-x-auto">
|
|
{item.groups.flatMap((group, groupIndex) =>
|
|
group.links.map((link, linkIndex) => (
|
|
<a
|
|
key={`mobile-link-${groupIndex}-${linkIndex}`}
|
|
href={link.url}
|
|
className="flex h-12 items-center gap-2 rounded-lg px-4 text-start text-muted-foreground transition-colors duration-300 hover:bg-muted hover:text-foreground"
|
|
>
|
|
<link.icon className="size-4 stroke-muted-foreground" />
|
|
{link.label}
|
|
</a>
|
|
)),
|
|
)}
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<a
|
|
key={item.title}
|
|
href={item.url}
|
|
className="flex h-[3.75rem] items-center rounded-md p-0 px-4 text-start text-base leading-[3.75] font-normal text-muted-foreground ring-ring/10 outline-ring/50 transition-all hover:bg-muted focus-visible:ring-4 focus-visible:outline-1 nth-last-1:border-0"
|
|
>
|
|
{item.title}
|
|
</a>
|
|
);
|
|
};
|
|
|
|
export { Navbar11 };
|