Files
reui-blocks/blocks/navbar10.tsx
T

825 lines
24 KiB
TypeScript

"use client";
import type { LucideIcon } from "lucide-react";
import {
Book,
ChevronRight,
Code,
Database,
Globe,
Layout,
MenuIcon,
Monitor,
Paintbrush,
Server,
Settings,
Shield,
Sparkles,
Terminal,
Users,
X,
} from "lucide-react";
import type { MouseEventHandler } from "react";
import { forwardRef, Fragment, useEffect, useRef, useState } from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
interface MenuLink {
label?: string;
description?: string;
url: string;
icon?: LucideIcon;
image?: string;
background?: string;
company?: {
logo: string;
name: string;
};
}
interface MenuGroup {
title: string;
links: MenuLink[];
}
interface MenuItem {
id?: number;
title: string;
url?: string;
links?: MenuLink[];
featuredLinks?: MenuLink[];
imageLink?: MenuLink;
groupLinks?: MenuGroup[];
}
interface DesktopMenuItemProps {
item: MenuItem;
index: number;
contained?: boolean;
}
interface MobileNavigationMenuProps {
open: boolean;
}
type NavLinkProps = {
link: MenuLink;
onMouseEnter?: MouseEventHandler<HTMLAnchorElement>;
onMouseLeave?: MouseEventHandler<HTMLAnchorElement>;
showDescription?: boolean;
};
const LOGO = {
url: "https://www.ShadcnBlocks",
src: "/block-media/shadcnblocks/block/block-1.svg",
alt: "لوگو",
title: "فروش‌یار",
};
const NAVIGATION: MenuItem[] = [
{
title: "محصولات",
id: 1,
links: [
{
label: "بینش‌ها",
icon: Book,
description: "گزارش محصول، بینش فروش و اعلان‌ها",
url: "#",
image:
"/block-media/shadcnblocks/block/photos/jeremy-bishop-iEjCQtcsVPY-unsplash.jpg",
},
{
label: "مهندسی",
icon: Code,
description: "آموزش API، راهنما و مستندات یکپارچه‌سازی",
url: "#",
image:
"/block-media/shadcnblocks/block/photos/carles-rabada-f7UprkNqi08-unsplash.jpg",
},
{
label: "فرهنگ",
icon: Users,
description: "ارزش‌ها و نحوه کار تیم درآمد",
url: "#",
image:
"/block-media/shadcnblocks/block/photos/john-murphey-ZWUWSEY6OGk-unsplash.jpg",
},
{
label: "مطبوعات",
icon: Globe,
description: "پوشش رسانه‌ای و مصاحبه‌ها",
url: "#",
image:
"/block-media/shadcnblocks/block/photos/kevin-charit-1fL2Q1JcbNc-unsplash.jpg",
},
{
label: "API",
icon: Monitor,
description: "دسترسی REST API امن برای داده فروش",
url: "#",
image:
"/block-media/shadcnblocks/block/photos/pat-whelen-gWfpmH0H2bM-unsplash.jpg",
},
{
label: "CLI",
icon: Terminal,
description: "CLI برای درون‌ریزی و اتوماسیون گزارش",
url: "#",
image:
"/block-media/shadcnblocks/block/photos/sam-wermut-FiUuNWxnb3k-unsplash.jpg",
},
{
label: "SDKs",
icon: Code,
description: "SDK برای اتصال سریع به CRM و BI",
url: "#",
image:
"/block-media/shadcnblocks/block/photos/vasilis-karkalas-qOaeVSKyhhE-unsplash.jpg",
},
],
},
{
title: "راه‌حل‌ها",
id: 2,
featuredLinks: [
{
label: "ویجت‌ها",
icon: Sparkles,
description: "ویجت‌های آماده داشبورد فروش",
background:
"/block-media/shadcnblocks/block/photos/john-murphey-ZWUWSEY6OGk-unsplash.jpg",
url: "#",
},
{
label: "قالب گزارش",
icon: Paintbrush,
description: "قالب‌های گزارش فصلی و پیش‌بینی",
background:
"/block-media/shadcnblocks/block/photos/kevin-charit-1fL2Q1JcbNc-unsplash.jpg",
url: "#",
},
],
links: [
{
description: "راه‌حل فروش B2B برای تیم‌های در حال رشد",
url: "#",
company: {
logo: "/block-media/shadcnblocks/block/logos/company/fictional-company-logo-1.svg",
name: "ARC",
},
},
{
description: "ابزار تحلیل برای تیم‌های SaaS",
url: "#",
company: {
logo: "/block-media/shadcnblocks/block/logos/company/fictional-company-logo-2.svg",
name: "descript",
},
},
{
description: "امنیت سازمانی برای داده‌های درآمد",
url: "#",
company: {
logo: "/block-media/shadcnblocks/block/logos/company/fictional-company-logo-3.svg",
name: "MERCURY",
},
},
{
description: "گزارش انطباق برای صنایع تحت نظارت",
url: "#",
company: {
logo: "/block-media/shadcnblocks/block/logos/company/fictional-company-logo-4.svg",
name: "ramp",
},
},
],
},
{
title: "پلتفرم",
id: 3,
imageLink: {
url: "#",
image:
"/block-media/shadcnblocks/block/photos/carles-rabada-f7UprkNqi08-unsplash.jpg",
label: "کاوش قابلیت‌های جدید",
},
groupLinks: [
{
title: "قابلیت‌های اصلی",
links: [
{
label: "داشبورد زنده",
icon: Server,
description: "شاخص‌های فروش در لحظه برای تیم‌های بزرگ",
url: "#",
},
{
label: "دسترسی نقش‌محور",
icon: Shield,
description: "ورود امن و دسترسی مبتنی بر نقش",
url: "#",
},
{
label: "انبار فرصت‌ها",
icon: Database,
description: "ذخیره پایدار تاریخچه معامله و فعالیت",
url: "#",
},
],
},
{
title: "گزارش‌ساز",
links: [
{
label: "بلوک گزارش",
icon: Layout,
description: "بلوک‌های قابل استفاده مجدد برای داشبورد یکپارچه",
url: "#",
},
{
label: "معیارها",
icon: Settings,
description: "معیارهای استاندارد برای مقایسه تیم‌ها",
url: "#",
},
{
label: "ویجت‌ها",
icon: Sparkles,
description: "نمودارها و نشانگرهای بصری",
url: "#",
},
],
},
],
},
{
title: "منابع",
url: "#",
},
{
title: "قیمت‌گذاری",
url: "#",
},
];
const BUTTON = {
label: "شروع رایگان",
isPrimary: true,
url: "#",
};
const MOBILE_BREAKPOINT = 1024;
interface Navbar10Props {
/** When true, keeps the navbar inside block preview instead of absolute viewport positioning. */
contained?: boolean;
className?: string;
}
const Navbar10 = ({ contained = false, className }: Navbar10Props) => {
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-20 w-full items-center justify-center overflow-visible bg-background text-start",
contained ? "relative" : "absolute top-0",
className,
)}
>
<NavigationMenu
dir="rtl"
className={cn(
"h-20 w-full max-w-full text-start",
"after:absolute after:inset-0 after:z-998 after:block after:size-full after:bg-background after:content-['']",
"[&>div:last-child]:inset-x-0 [&>div:last-child]:w-full [&>div:last-child]:justify-center",
"[&>div:last-child>div]:mt-0 [&>div:last-child>div]:w-full [&>div:last-child>div]:max-w-full",
"[&>div:last-child>div]:animate-none [&>div:last-child>div]:rounded-none [&>div:last-child>div]:border-0 [&>div:last-child>div]:border-b",
"[&>div:last-child>div]:shadow-[0px_-1px_0px_0px_rgba(0,0,0,0.05),0px_0px_0px_1px_rgba(17,26,37,0.05),0px_2px_5px_0px_rgba(16,25,36,0.1),0px_5px_20px_0px_rgba(16,25,36,0.1)]",
)}
>
<div className="relative z-999 container grid w-full grid-cols-2 items-center justify-between gap-8 xl:grid-cols-3">
<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>
<div className="hidden xl:flex">
<NavigationMenuList>
{NAVIGATION.map((item, index) => (
<DesktopMenuItem
key={`desktop-link-${index}`}
item={item}
index={index}
contained={contained}
/>
))}
</NavigationMenuList>
</div>
<div className="justify-self-end">
<div className="hidden xl:block">
<Button variant="ghost">
<a href={BUTTON.url} className="flex items-center gap-1">
{BUTTON.label}
<ChevronRight className="rtl:rotate-180" />
</a>
</Button>
</div>
<div className="xl:hidden">
<Button
className="size-11"
variant="ghost"
size="icon"
onClick={handleMobileMenu}
aria-label={open ? "بستن منو" : "باز کردن منو"}
>
{open ? (
<X className="size-5.5 stroke-foreground" />
) : (
<MenuIcon className="size-5.5 stroke-foreground" />
)}
</Button>
</div>
</div>
</div>
</NavigationMenu>
</section>
<MobileNavigationMenu open={open} />
</Fragment>
);
};
const DesktopMenuItem = ({
item,
index,
contained = false,
}: DesktopMenuItemProps) => {
if (item.links || item.featuredLinks || item.groupLinks) {
return (
<NavigationMenuItem key={`desktop-menu-item-${index}`} value={`${index}`}>
<NavigationMenuTrigger className="h-fit bg-transparent text-start font-normal text-foreground/60 [&_svg]:ms-1 [&_svg]:!ml-0">
{item.title}
</NavigationMenuTrigger>
<NavigationMenuContent
dir="rtl"
className="inset-x-0 left-auto right-auto hidden w-full !rounded-xl !border-0 !p-0 text-start xl:block"
>
<div
className={cn(
"mx-auto w-full animate-[fade-in-slide-down_0.35s_cubic-bezier(0.33,1,0.68,1)_forwards] px-8 pt-6 pb-12 text-start",
!contained && "max-w-[100vw]",
)}
>
<div className={cn("mx-auto w-full", contained ? "max-w-full" : "container")}>
{item.id === 1 && <DropdownMenu1 links={item.links} />}
{item.id === 2 && (
<DropdownMenu2
featuredLinks={item.featuredLinks}
links={item.links}
/>
)}
{item.id === 3 && (
<DropdownMenu3
groupLinks={item.groupLinks}
imageLink={item.imageLink}
/>
)}
</div>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
);
}
return (
<NavigationMenuItem key={`desktop-menu-item-${index}`} value={`${index}`}>
<NavigationMenuLink
href={item.url}
className={`${navigationMenuTriggerStyle()} h-fit bg-transparent text-start font-normal text-foreground/60`}
>
{item.title}
</NavigationMenuLink>
</NavigationMenuItem>
);
};
const DropdownMenu1 = ({ links }: { links?: MenuLink[] }) => {
const linksRef = useRef<HTMLAnchorElement[]>([]);
const imageRefs = useRef<HTMLDivElement[]>([]);
const updateImageClasses = (activeIndex: number) => {
imageRefs.current.forEach((img, i) => {
if (!img) return;
const isActive = i === activeIndex;
img.classList.toggle("opacity-100", isActive);
img.classList.toggle("translate-y-0", isActive);
img.classList.toggle("opacity-0", !isActive);
img.classList.toggle("translate-y-20", !isActive);
img.classList.toggle("z-10", isActive);
});
};
const handleMouseEnter =
(index: number) => (event: React.MouseEvent<HTMLAnchorElement>) => {
linksRef.current.forEach((link) => {
if (link && link !== event.currentTarget) {
link.classList.add("opacity-50");
}
});
updateImageClasses(index);
};
const handleMouseLeave = () => {
linksRef.current.forEach((link) => {
link?.classList.remove("opacity-50");
});
updateImageClasses(0);
};
if (!links) return null;
return (
<div className="grid grid-cols-2 gap-8">
<ul className="grid grid-cols-2 gap-8">
{links.map((link, index) => (
<NavLink
key={`default-nav-link-${index}`}
link={link}
onMouseEnter={handleMouseEnter(index)}
onMouseLeave={handleMouseLeave}
ref={(el) => {
if (el) linksRef.current[index] = el;
}}
/>
))}
</ul>
<div className="relative !h-[16rem] w-full overflow-hidden rounded-lg bg-muted">
{links.map((link, index) => (
<div
key={`default-nav-link-img-${index}`}
ref={(el) => {
if (el) imageRefs.current[index] = el;
}}
className={`will-change-opacity absolute top-14 start-14 aspect-video w-[43.75rem] overflow-hidden rounded-ss-md border-t border-s transition-all duration-600 ease-in-out will-change-transform ${index === 0
? "z-10 translate-y-0 opacity-100"
: "pointer-events-none z-0 translate-y-20 opacity-0"
}`}
>
<img
src={link.image}
alt={link.label}
className="size-full object-cover object-top"
/>
</div>
))}
</div>
</div>
);
};
const DropdownMenu2 = ({
links,
featuredLinks,
}: {
links?: MenuLink[];
featuredLinks?: MenuLink[];
}) => {
return (
<div>
<div className="flex gap-8 pb-8">
{featuredLinks &&
featuredLinks.map((link, index) => (
<FeaturedLink key={`desktop-featured-link-${index}`} link={link} />
))}
</div>
<Separator />
<div className="grid grid-cols-4 pt-8">
{links &&
links.map((link, index) => (
<NavLink key={`default-nav-link-${index}`} link={link} />
))}
</div>
</div>
);
};
const DropdownMenu3 = ({
groupLinks,
imageLink,
}: {
groupLinks?: MenuGroup[];
imageLink?: MenuLink;
}) => {
return (
<div className="grid grid-cols-2 gap-8">
<GroupLinks groupLinks={groupLinks} />
<FeaturedImageLink link={imageLink} />
</div>
);
};
const GroupLinks = ({ groupLinks }: { groupLinks?: MenuGroup[] }) => {
const linksRef = useRef<HTMLAnchorElement[]>([]);
const handleMouseEnter =
() => (event: React.MouseEvent<HTMLAnchorElement>) => {
linksRef.current.forEach((link) => {
if (link && link !== event.currentTarget) {
link.classList.add("opacity-50");
}
});
};
const handleMouseLeave = () => {
linksRef.current.forEach((link) => {
link?.classList.remove("opacity-50");
});
};
if (!groupLinks) return null;
let linkIndex = 0;
return (
<div className="grid grid-cols-2 gap-8">
{groupLinks.map((group, index1) => (
<div key={`group-link-${index1}`}>
<div className="mb-4 text-start text-xs text-muted-foreground">
{group.title}
</div>
<ul className="flex flex-col gap-8">
{group.links.map((link, index2) => {
const index = linkIndex++;
return (
<li key={`group-link-${index1}-${index2}`}>
<NavLink
link={link}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
ref={(el) => {
if (el) linksRef.current[index] = el;
}}
/>
</li>
);
})}
</ul>
</div>
))}
</div>
);
};
const FeaturedImageLink = ({ link }: { link?: MenuLink }) => {
if (!link) return null;
return (
<div className="hidden xl:block">
<a href={link.url} className="w-full max-w-[36.875rem]">
<AspectRatio
ratio={1.77245509}
className="overflow-hidden rounded-[0.25rem] bg-muted"
>
<div className="size-full">
<Badge className="absolute top-2 start-2">جدید</Badge>
<div className="flex w-full flex-col items-center justify-center gap-8 pt-10 text-center">
<div className="text-xl font-semibold">{link.label}</div>
<div className="w-[80%]">
<AspectRatio
ratio={1.5}
className="overflow-hidden rounded-[0.25rem] bg-muted"
>
<img
src={link.image}
alt={link.label}
className="size-full object-cover object-top"
/>
</AspectRatio>
</div>
</div>
</div>
</AspectRatio>
</a>
</div>
);
};
const FeaturedLink = ({ link }: { link: MenuLink }) => {
return (
<a
href={link.url}
className="group relative flex w-full overflow-hidden rounded-xl bg-muted px-8 py-7"
>
<div className="relative z-10 flex w-full items-center gap-6">
<div className="flex size-12 shrink-0 rounded-lg border bg-background shadow-lg">
{link.icon && (
<link.icon className="m-auto size-5 stroke-foreground" />
)}
</div>
<div className="flex flex-col gap-2">
<div className="text-lg font-semibold text-white">{link.label}</div>
<div className="font-medium text-white/80">{link.description}</div>
</div>
</div>
<img
src={link.background}
alt={link.label}
className="absolute inset-0 size-full object-cover object-top opacity-90 transition-opacity duration-300 ease-in-out group-hover:opacity-100"
/>
</a>
);
};
const NavLink = forwardRef<HTMLAnchorElement, NavLinkProps>(
({ link, onMouseEnter, onMouseLeave }, ref) => {
return (
<a
ref={ref}
href={link.url}
className="flex w-full gap-2 text-start transition-opacity duration-300"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{link.icon && (
<div className="flex size-6 shrink-0 rounded-md border shadow">
<link.icon className="m-auto size-3.5" />
</div>
)}
<div className="flex min-w-0 flex-col items-start gap-2 text-start">
{link.company && (
<div className="block text-base leading-normal xl:hidden">
{link.company.name}
</div>
)}
{link.company && (
<img
className="hidden h-6 xl:block"
src={link.company.logo}
alt={link.company.name}
/>
)}
{link.label && (
<div className="text-base leading-normal">{link.label}</div>
)}
<div className="text-sm leading-normal text-muted-foreground">
{link.description}
</div>
</div>
</a>
);
},
);
const MobileNavigationMenu = ({ open }: MobileNavigationMenuProps) => {
return (
<Sheet open={open}>
<SheetContent
aria-describedby={undefined}
side="top"
className="inset-0 z-998 h-dvh w-full bg-background pt-20 [&>button]:hidden"
>
<div className="flex-1 overflow-y-auto">
<div className="container py-8">
<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 min-h-full flex-col gap-6">
<Accordion
type="multiple"
dir="rtl"
className="w-full text-start"
>
{NAVIGATION.map((item, index) =>
renderMobileMenuItem(item, index),
)}
</Accordion>
<Button asChild>
<a href={BUTTON.url}>{BUTTON.label}</a>
</Button>
</div>
</div>
</div>
</SheetContent>
</Sheet>
);
};
const renderMobileMenuItem = (item: MenuItem, index: number) => {
if (item.links || item.featuredLinks || item.groupLinks) {
return (
<AccordionItem
key={item.title}
value={`nav-${index}`}
className="border-b-0"
>
<AccordionTrigger className="h-[2.5rem] items-center justify-start text-start text-base font-normal text-foreground hover:no-underline [&>svg]:ms-auto [&>svg]:me-0">
{item.title}
</AccordionTrigger>
<AccordionContent className="flex flex-col gap-6 p-2">
{item.featuredLinks && (
<div className="flex flex-col gap-2 p-2">
{item.featuredLinks.map((link, index) => (
<NavLink key={`default-nav-link-${index}`} link={link} />
))}
</div>
)}
{item.links && (
<div className="flex flex-col gap-2 p-2">
{item.links.map((link, index) => (
<NavLink key={`default-nav-link-${index}`} link={link} />
))}
</div>
)}
{item.groupLinks && (
<div className="flex flex-col gap-2 p-2">
{item.groupLinks.map((group, index1) => (
<div className="mb-8 last:mb-0" key={`group-link-${index1}`}>
<div className="mb-4 text-start text-xs text-muted-foreground">
{group.title}
</div>
<ul className="flex flex-col gap-2">
{group.links.map((link, index2) => (
<li key={`group-link-${index1}-${index2}`}>
<NavLink link={link} />
</li>
))}
</ul>
</div>
))}
</div>
)}
</AccordionContent>
</AccordionItem>
);
}
return (
<a
key={item.title}
href={item.url}
className="flex h-[2.5rem] items-center rounded-md text-start text-base leading-[3.75] font-normal text-foreground ring-ring/10 outline-ring/50 transition-all focus-visible:ring-4 focus-visible:outline-1 nth-last-1:border-0"
>
{item.title}
</a>
);
};
export { Navbar10 };