Build & Push Docker Image / build (push) Has been cancelled
Refresh the homepage around new hero/services/roadmap sections, shared design tokens, and updated brand assets. Co-authored-by: Cursor <cursoragent@cursor.com>
442 lines
14 KiB
TypeScript
442 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { Logo } from "@/components/logo";
|
|
import { Menu } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from "@/components/ui/accordion";
|
|
import {
|
|
MegaMenuMobileCards,
|
|
serviceMegaItems,
|
|
ServicesMegaMenu,
|
|
} from "@/components/nav-mega-menu";
|
|
import {
|
|
Sheet,
|
|
SheetClose,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from "@/components/ui/sheet";
|
|
import React from "react";
|
|
import { Container } from "@/components/container";
|
|
import { cn } from "@/lib/utils";
|
|
import { type } from "@/lib/typography";
|
|
import { ThemeToggle } from "@/components/theme-toggle";
|
|
|
|
type NavLeaf = {
|
|
name: string;
|
|
href: string;
|
|
};
|
|
|
|
type NavEntry =
|
|
| ({ kind: "link" } & NavLeaf)
|
|
| { kind: "mega"; name: string };
|
|
|
|
/**
|
|
* Top-level nav (RTL reading order, right → left):
|
|
* صفحه اصلی → خدمات → نمونه کارها → درباره ما → مجله → تماس با ما
|
|
*/
|
|
/** Experimental: no real routes yet — keep UI, avoid 404s. */
|
|
const navEntries: NavEntry[] = [
|
|
{ kind: "link", name: "صفحه اصلی", href: "/" },
|
|
{ kind: "mega", name: "خدمات" },
|
|
{ kind: "link", name: "نمونه کارها", href: "#" },
|
|
{ kind: "link", name: "درباره ما", href: "#" },
|
|
{ kind: "link", name: "مجله", href: "#" },
|
|
{ kind: "link", name: "تماس با ما", href: "#contact" },
|
|
];
|
|
|
|
const CTA = {
|
|
name: "درخواست مشاوره",
|
|
href: "#contact",
|
|
} as const;
|
|
|
|
const ENGLISH = {
|
|
name: "English",
|
|
href: "#",
|
|
} as const;
|
|
|
|
/** Scroll threshold before the floating glass shell engages. */
|
|
const SCROLL_THRESHOLD = 24;
|
|
|
|
/** Animate only the floating shell chrome — never content size or vertical layout. */
|
|
const HEADER_SHELL_EASE =
|
|
"transition-[padding,border-radius,background-color,border-color,box-shadow,backdrop-filter,-webkit-backdrop-filter,height] duration-300 ease-out";
|
|
|
|
/** Dim siblings on nav hover; hovered + active routes stay full strength. */
|
|
const navItemInteractionClass = cn(
|
|
"transition-[opacity,background-color,color] duration-300 ease-out",
|
|
"text-foreground opacity-100",
|
|
"group-hover/nav:opacity-40",
|
|
"hover:opacity-100",
|
|
"group-hover/nav:hover:opacity-100"
|
|
);
|
|
|
|
const navItemActiveClass = cn(
|
|
"bg-muted/60 text-foreground group-hover/nav:opacity-100",
|
|
"hover:bg-muted/70"
|
|
);
|
|
|
|
const navTriggerClass = cn(
|
|
"group relative inline-flex h-auto items-center gap-1 whitespace-nowrap",
|
|
type.nav,
|
|
navItemInteractionClass,
|
|
"rounded-md bg-transparent px-2.5 py-1.5 shadow-none",
|
|
"hover:bg-muted/50",
|
|
"focus-visible:bg-muted/50 focus-visible:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2",
|
|
"data-[state=open]:bg-muted/60 data-[state=open]:text-foreground",
|
|
"data-[state=open]:opacity-100 group-hover/nav:data-[state=open]:opacity-100"
|
|
);
|
|
|
|
function isLinkActive(pathname: string, href: string): boolean {
|
|
if (href === "/") return pathname === "/";
|
|
if (href.startsWith("/#")) return pathname === "/";
|
|
return pathname === href || pathname.startsWith(href + "/");
|
|
}
|
|
|
|
function isGroupActive(pathname: string): boolean {
|
|
return serviceMegaItems.some((item) => isLinkActive(pathname, item.href));
|
|
}
|
|
|
|
function NavLink({
|
|
href,
|
|
name,
|
|
active,
|
|
className,
|
|
}: {
|
|
href: string;
|
|
name: string;
|
|
active: boolean;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<Link
|
|
href={href}
|
|
aria-current={active ? "page" : undefined}
|
|
className={cn(
|
|
"relative inline-flex items-center whitespace-nowrap rounded-md px-2 py-1.5",
|
|
type.nav,
|
|
navItemInteractionClass,
|
|
"hover:bg-muted/50",
|
|
active && navItemActiveClass,
|
|
className
|
|
)}
|
|
>
|
|
{name}
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
function DesktopNav({ pathname }: { pathname: string }) {
|
|
return (
|
|
<ul className={cn("group/nav flex list-none items-center justify-start gap-x-1", type.nav)}>
|
|
{navEntries.map((entry) => {
|
|
if (entry.kind === "link") {
|
|
const active = isLinkActive(pathname, entry.href);
|
|
return (
|
|
<li key={entry.href}>
|
|
<NavLink
|
|
href={entry.href}
|
|
name={entry.name}
|
|
active={active}
|
|
className="px-2.5"
|
|
/>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<li key={entry.name}>
|
|
<ServicesMegaMenu
|
|
label={entry.name}
|
|
pathname={pathname}
|
|
active={isGroupActive(pathname)}
|
|
triggerClassName={cn(
|
|
navTriggerClass,
|
|
isGroupActive(pathname) && navItemActiveClass
|
|
)}
|
|
/>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
const mobileNavRowClass = cn(
|
|
"flex w-full items-center rounded-md px-3 py-1.5",
|
|
"text-muted-foreground/90 transition-[color,background-color,opacity]",
|
|
"duration-200 ease-out",
|
|
"hover:bg-muted/60 hover:text-foreground"
|
|
);
|
|
|
|
function MobileNavList({
|
|
pathname,
|
|
menuOpen,
|
|
onNavigate,
|
|
}: {
|
|
pathname: string;
|
|
menuOpen: boolean;
|
|
onNavigate: () => void;
|
|
}) {
|
|
let animIndex = 0;
|
|
|
|
return (
|
|
<ul className={cn("flex flex-col gap-0", type["nav-mobile"])}>
|
|
{navEntries.map((entry) => {
|
|
if (entry.kind === "link") {
|
|
const index = animIndex++;
|
|
const active = isLinkActive(pathname, entry.href);
|
|
return (
|
|
<li
|
|
key={entry.href}
|
|
className={cn(
|
|
"header-nav-item translate-y-1.5 opacity-0",
|
|
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
|
)}
|
|
style={{
|
|
animationDelay: menuOpen ? `${80 + index * 40}ms` : undefined,
|
|
}}
|
|
>
|
|
<SheetClose asChild>
|
|
<Link
|
|
href={entry.href}
|
|
className={cn(
|
|
mobileNavRowClass,
|
|
active && "bg-muted/50 text-foreground"
|
|
)}
|
|
>
|
|
{entry.name}
|
|
</Link>
|
|
</SheetClose>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
const index = animIndex++;
|
|
const groupActive = isGroupActive(pathname);
|
|
return (
|
|
<li
|
|
key={entry.name}
|
|
className={cn(
|
|
"header-nav-item translate-y-1.5 opacity-0",
|
|
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
|
)}
|
|
style={{
|
|
animationDelay: menuOpen ? `${80 + index * 40}ms` : undefined,
|
|
}}
|
|
>
|
|
<Accordion type="single" collapsible className="w-full">
|
|
<AccordionItem value={entry.name} className="border-none">
|
|
<AccordionTrigger
|
|
className={cn(
|
|
type["nav-mobile"],
|
|
"rounded-md px-3 py-1.5 hover:no-underline",
|
|
"text-muted-foreground/90 hover:bg-muted/60 hover:text-foreground",
|
|
groupActive && "text-foreground"
|
|
)}
|
|
>
|
|
<span className="flex items-center gap-1.5">
|
|
{entry.name}
|
|
{!groupActive ? null : (
|
|
<span className="size-1.5 rounded-full bg-primary" />
|
|
)}
|
|
</span>
|
|
</AccordionTrigger>
|
|
<AccordionContent className="pb-1 ps-1">
|
|
<MegaMenuMobileCards
|
|
items={serviceMegaItems}
|
|
pathname={pathname}
|
|
onNavigate={onNavigate}
|
|
/>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
</li>
|
|
);
|
|
})}
|
|
|
|
<li
|
|
className={cn(
|
|
"header-nav-item translate-y-1.5 opacity-0",
|
|
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
|
)}
|
|
style={{
|
|
animationDelay: menuOpen ? `${80 + animIndex * 40}ms` : undefined,
|
|
}}
|
|
>
|
|
<SheetClose asChild>
|
|
<Link
|
|
href={ENGLISH.href}
|
|
lang="en"
|
|
className={mobileNavRowClass}
|
|
>
|
|
{ENGLISH.name}
|
|
</Link>
|
|
</SheetClose>
|
|
</li>
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
export const HeroHeader = () => {
|
|
const pathname = usePathname();
|
|
const [menuOpen, setMenuOpen] = React.useState(false);
|
|
const [isScrolled, setIsScrolled] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
const handleScroll = () => {
|
|
setIsScrolled(window.scrollY > SCROLL_THRESHOLD);
|
|
};
|
|
handleScroll();
|
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
return () => window.removeEventListener("scroll", handleScroll);
|
|
}, []);
|
|
|
|
const topLevelCount = navEntries.length;
|
|
|
|
return (
|
|
<header
|
|
className={cn(
|
|
"pointer-events-none fixed inset-x-0 top-0 z-50 w-full",
|
|
"transition-[padding] duration-300 ease-out",
|
|
isScrolled
|
|
? "pt-[calc(env(safe-area-inset-top,0px)+0.5rem)]"
|
|
: "pt-[env(safe-area-inset-top,0px)]"
|
|
)}
|
|
data-scrolled={isScrolled ? "" : undefined}
|
|
>
|
|
{/* Same max-width + gutters as every page section — glass edges share the content grid. */}
|
|
<Container className="pointer-events-auto">
|
|
<div
|
|
className={cn(
|
|
"relative flex w-full items-center justify-between gap-4 md:gap-6 lg:gap-8",
|
|
isScrolled ? "h-14 md:h-16" : "h-16 md:h-20",
|
|
"border bg-transparent shadow-none backdrop-blur-none",
|
|
HEADER_SHELL_EASE,
|
|
isScrolled
|
|
? [
|
|
// Horizontal inset only — no vertical shift of logo / nav / CTA
|
|
"rounded-2xl border-border/50 bg-background/75 px-4 md:px-6",
|
|
"shadow-sm backdrop-blur-xl backdrop-saturate-150",
|
|
"supports-[backdrop-filter]:bg-background/65",
|
|
]
|
|
: // Flush with Container gutters — same vertical guides as page content
|
|
"rounded-none border-transparent px-0"
|
|
)}
|
|
>
|
|
{/* Logo + nav sit together so RTL reading starts: لوگو → صفحه اصلی → … */}
|
|
<div className="relative z-10 flex min-w-0 flex-1 items-center gap-5 lg:gap-8">
|
|
<Logo
|
|
variant="mark"
|
|
priority
|
|
className="shrink-0 transition-opacity duration-300 ease-out hover:opacity-80"
|
|
/>
|
|
|
|
<nav
|
|
aria-label="ناوبری اصلی"
|
|
className="hidden min-w-0 items-center xl:flex"
|
|
>
|
|
<DesktopNav pathname={pathname ?? ""} />
|
|
</nav>
|
|
</div>
|
|
|
|
<div className="relative z-10 flex shrink-0 items-center gap-2 sm:gap-3">
|
|
<Link
|
|
href={ENGLISH.href}
|
|
className={cn(
|
|
type.nav,
|
|
"hidden px-2.5 py-1.5 text-muted-foreground/80",
|
|
"transition-colors duration-300 ease-out hover:text-foreground xl:inline-flex"
|
|
)}
|
|
lang="en"
|
|
>
|
|
{ENGLISH.name}
|
|
</Link>
|
|
|
|
<ThemeToggle />
|
|
|
|
<Button
|
|
asChild
|
|
size="sm"
|
|
className="hidden hover:-translate-y-px xl:inline-flex"
|
|
>
|
|
<Link href={CTA.href}>{CTA.name}</Link>
|
|
</Button>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setMenuOpen(true)}
|
|
aria-expanded={menuOpen}
|
|
aria-controls="mobile-nav"
|
|
aria-label="باز کردن منو"
|
|
className="text-muted-foreground hover:text-foreground xl:hidden"
|
|
>
|
|
<Menu aria-hidden className="size-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
|
|
<Sheet open={menuOpen} onOpenChange={setMenuOpen}>
|
|
<SheetContent
|
|
id="mobile-nav"
|
|
side="right"
|
|
closePosition="start"
|
|
className={cn(
|
|
"pointer-events-auto w-[min(100%,20rem)] gap-0 border-border/50 bg-background/95 p-0",
|
|
"backdrop-blur-xl backdrop-saturate-150 shadow-xl",
|
|
"data-[state=open]:duration-300 data-[state=closed]:duration-300",
|
|
"ease-out"
|
|
)}
|
|
>
|
|
<SheetHeader className="border-b border-border/40 px-6 py-4">
|
|
<div className="flex items-center justify-between gap-3 pe-8">
|
|
<SheetTitle className="sr-only">منوی اصلی</SheetTitle>
|
|
<Logo variant="mark" />
|
|
</div>
|
|
</SheetHeader>
|
|
|
|
<nav
|
|
aria-label="ناوبری موبایل"
|
|
className="flex flex-1 flex-col px-3 py-4"
|
|
>
|
|
<MobileNavList
|
|
pathname={pathname ?? ""}
|
|
menuOpen={menuOpen}
|
|
onNavigate={() => setMenuOpen(false)}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
"header-nav-item mt-auto border-t border-border/40 px-3 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]",
|
|
"translate-y-1.5 opacity-0",
|
|
menuOpen && "animate-[header-nav-in_320ms_ease_forwards]"
|
|
)}
|
|
style={{
|
|
animationDelay: menuOpen
|
|
? `${80 + (topLevelCount + 1) * 40}ms`
|
|
: undefined,
|
|
}}
|
|
>
|
|
<SheetClose asChild>
|
|
<Button asChild className="w-full hover:-translate-y-px">
|
|
<Link href={CTA.href}>{CTA.name}</Link>
|
|
</Button>
|
|
</SheetClose>
|
|
</div>
|
|
</nav>
|
|
</SheetContent>
|
|
</Sheet>
|
|
</header>
|
|
);
|
|
};
|