Files
ar.badieiandCursor c2376b3b9e
Build & Push Docker Image / build (push) Has been cancelled
feat: redesign landing page with brand system and section blocks
Refresh the homepage around new hero/services/roadmap sections, shared design tokens, and updated brand assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 17:00:55 +03:30

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>
);
};