Files
sass-mohfarawati/components/layout/site-header.tsx
T

289 lines
11 KiB
TypeScript

"use client";
import { AnimatePresence, motion } from "framer-motion";
import { LayoutDashboard, Menu, X } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
import { useState } from "react";
import { Container } from "@/components/layout/container";
import { LocaleToggle } from "@/components/layout/locale-toggle";
import { SiteLogo } from "@/components/layout/site-logo";
import { ThemeToggle } from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
import { getLocalizedPath, stripLocalePrefix } from "@/lib/locale";
import { cn } from "@/lib/utils";
const navItems = [
{ key: "home", path: "" },
{ key: "portfolio", path: "/portfolio" },
{ key: "products", path: "/products" },
{ key: "about", path: "/about" },
{ key: "contact", path: "/contact" },
];
type SiteHeaderProps = {
isAdmin?: boolean;
lightLogoUrl?: string | null;
darkLogoUrl?: string | null;
};
function isNavItemActive(currentPath: string, itemPath: string) {
if (itemPath === "/") {
return currentPath === "/";
}
return currentPath === itemPath || currentPath.startsWith(`${itemPath}/`);
}
function NavLinks({
onNavigate,
mobile = false,
}: {
onNavigate?: () => void;
mobile?: boolean;
}) {
const locale = useLocale();
const pathname = usePathname();
const t = useTranslations("navigation");
const currentPath = stripLocalePrefix(pathname);
return (
<>
{navItems.map((item) => (
(() => {
const itemPath = item.path || "/";
const isActive = isNavItemActive(currentPath, itemPath);
return (
<Link
key={item.key}
href={getLocalizedPath(locale, itemPath)}
className={cn(
"relative z-10 rounded-pill transition-colors",
mobile ? "px-4 py-3 text-sm font-medium" : "px-3.5 py-2 text-sm font-medium xl:px-4",
isActive
? "text-primary-foreground"
: "text-foreground/70 hover:text-foreground",
)}
onClick={onNavigate}
>
{isActive ? (
<motion.span
layoutId="site-header-active-tab"
className="absolute inset-0 -z-10 rounded-pill bg-primary shadow-xs"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
) : null}
{t(item.key)}
</Link>
);
})()
))}
</>
);
}
export function SiteHeader({
isAdmin = false,
lightLogoUrl,
darkLogoUrl,
}: SiteHeaderProps) {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const locale = useLocale();
const t = useTranslations("navigation");
const isArabic = locale === "ar";
const desktopControlRailClassName =
"inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1 shadow-panel backdrop-blur-chrome";
const desktopControlButtonClassName =
"h-9 w-9 rounded-pill border border-transparent p-0 text-foreground/80 hover:bg-accent hover:text-foreground";
const mobileControlButtonClassName =
"h-10 w-10 rounded-pill border border-border/70 bg-background text-foreground/80 shadow-xs hover:bg-accent hover:text-foreground";
return (
<header className="fixed inset-x-0 top-3 z-40 bg-transparent">
<Container className="max-w-[88rem]">
<div className="relative grid min-h-[4.5rem] grid-cols-[auto_1fr_auto] items-center gap-3 lg:min-h-[5rem]">
<div className="col-start-1 flex items-center justify-self-start">
<Link
href={getLocalizedPath(locale)}
className="inline-flex items-center rounded-pill px-1 py-1 sm:py-1.5 lg:py-2"
>
<SiteLogo
lightLogoUrl={lightLogoUrl}
darkLogoUrl={darkLogoUrl}
alt="mohfarawati"
priority
className="h-7 opacity-88 sm:h-8 lg:h-9"
/>
</Link>
</div>
<div className="pointer-events-none absolute left-1/2 top-1/2 hidden -translate-x-1/2 -translate-y-1/2 lg:flex">
<motion.nav
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.28, ease: "easeOut" }}
className={cn(
"pointer-events-auto inline-flex items-center gap-1 rounded-pill border border-border/70 bg-background/80 p-1.5 shadow-panel backdrop-blur-chrome",
isArabic ? "w-auto max-w-none" : "min-w-[27rem] max-w-[42rem]",
)}
>
<NavLinks />
</motion.nav>
</div>
<div className="hidden items-center justify-end lg:flex">
<motion.div
initial={{ opacity: 0, y: -8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.28, ease: "easeOut", delay: 0.03 }}
className={desktopControlRailClassName}
>
<LocaleToggle
locale={locale}
className={desktopControlButtonClassName}
/>
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
className={desktopControlButtonClassName}
/>
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className={desktopControlButtonClassName}>
<Link href="/root" aria-label={t("root")}>
<LayoutDashboard className="h-4 w-4" />
</Link>
</Button>
) : null}
</motion.div>
</div>
<Button
type="button"
onClick={() => setIsOpen((open) => !open)}
variant="ghost"
size="icon"
className="col-start-3 ml-auto h-11 w-11 justify-self-end rounded-pill border border-border/70 bg-background/80 text-foreground shadow-panel backdrop-blur-chrome hover:bg-accent lg:hidden"
aria-label={isOpen ? t("closeMenu") : t("openMenu")}
>
<motion.span
animate={{ rotate: isOpen ? 180 : 0, scale: isOpen ? 0.96 : 1 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="flex items-center justify-center"
>
{isOpen ? <X className="h-4 w-4" /> : <Menu className="h-4 w-4" />}
</motion.span>
</Button>
</div>
</Container>
<AnimatePresence initial={false}>
{isOpen ? (
<motion.div
initial={{ opacity: 0, y: -14 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.22, ease: "easeOut" }}
className="bg-transparent lg:hidden"
>
<Container className="pb-2 pt-3">
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="overflow-hidden rounded-surface border border-border/70 bg-card/95 shadow-panel backdrop-blur-chrome"
>
<motion.nav
initial="closed"
animate="open"
exit="closed"
variants={{
open: {
transition: {
staggerChildren: 0.04,
delayChildren: 0.02,
},
},
closed: {
transition: {
staggerChildren: 0.03,
staggerDirection: -1,
},
},
}}
className="flex flex-col gap-1 p-2"
>
{navItems.map((item) => {
const itemPath = item.path || "/";
const isActive = isNavItemActive(stripLocalePrefix(pathname), itemPath);
return (
<motion.div
key={item.key}
variants={{
open: { opacity: 1, y: 0 },
closed: { opacity: 0, y: -8 },
}}
transition={{ duration: 0.18, ease: "easeOut" }}
>
<Link
href={getLocalizedPath(locale, itemPath)}
className={cn(
"flex items-center justify-between rounded-nested px-4 py-3 text-sm font-medium transition-colors",
isActive
? "bg-accent text-foreground shadow-xs"
: "text-foreground/80 hover:bg-accent/70 hover:text-foreground",
)}
onClick={() => setIsOpen(false)}
>
<span>{t(item.key)}</span>
<span className="text-xs uppercase tracking-[0.18em] opacity-60">
0{navItems.findIndex((navItem) => navItem.key === item.key) + 1}
</span>
</Link>
</motion.div>
);
})}
</motion.nav>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ duration: 0.18, ease: "easeOut", delay: 0.04 }}
className="border-t border-border/70 bg-background/70 p-2"
>
<div className="flex items-center justify-between gap-2">
<LocaleToggle
locale={locale}
className={mobileControlButtonClassName}
/>
<div className="flex items-center gap-2">
<ThemeToggle
ariaLabel={t("themeToggle")}
variant="ghost"
className={mobileControlButtonClassName}
/>
{isAdmin ? (
<Button asChild variant="ghost" size="icon" className={mobileControlButtonClassName}>
<Link href="/root" aria-label={t("root")} onClick={() => setIsOpen(false)}>
<LayoutDashboard className="h-4 w-4" />
</Link>
</Button>
) : null}
</div>
</div>
</motion.div>
</motion.div>
</Container>
</motion.div>
) : null}
</AnimatePresence>
</header>
);
}