Files
admin/apps/web/web-runtime/chrome/components/HeaderNav.tsx
T

215 lines
7.4 KiB
TypeScript

'use client';
import { useTranslations } from 'next-intl';
import React from 'react';
import { PageLink } from '#web-runtime/components/links';
import type { LanguageAlternative, NavigationLink, SiteMenu } from '#web-runtime/types';
import { Button } from '#shared/ui/Button';
import LanguageAlternatives from './LanguageAlternatives';
function Chevron({ open }: { open: boolean }) {
return (
<svg
viewBox="0 0 12 12"
className={`size-3 shrink-0 transition-transform duration-150 motion-reduce:transition-none ${open ? 'rotate-180' : ''}`}
aria-hidden="true"
>
<path
d="M2 4l4 4 4-4"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function MenuLink({ link, className }: { link: NavigationLink; className: string }) {
return <PageLink link={{ ...link, variant: 'ghost' }} className={className} />;
}
export default function HeaderNav({
menus,
ctaItems,
languageAlternatives
}: {
menus: SiteMenu[];
ctaItems: NavigationLink[];
languageAlternatives: LanguageAlternative[];
}) {
const t = useTranslations('Components.Navigation');
const [openMenu, setOpenMenu] = React.useState<number | null>(null);
const [mobileOpen, setMobileOpen] = React.useState(false);
const rootRef = React.useRef<HTMLDivElement>(null);
const visibleMenus = menus.filter((menu) => menu.items.length > 0);
const hasMenuContent = visibleMenus.length > 0 || ctaItems.length > 0;
const anythingOpen = openMenu !== null || mobileOpen;
const closeAll = React.useCallback(() => {
setOpenMenu(null);
setMobileOpen(false);
}, []);
React.useEffect(() => {
if (!anythingOpen) {
return;
}
const handlePointerDown = (event: PointerEvent) => {
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
closeAll();
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
closeAll();
}
};
document.addEventListener('pointerdown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('pointerdown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [anythingOpen, closeAll]);
return (
<div ref={rootRef} className="flex min-w-0 flex-1 items-center gap-8 max-lg:gap-4">
{visibleMenus.length > 0 ? (
<nav
className="flex min-w-0 flex-1 items-center gap-1 max-lg:hidden"
aria-label={t('main')}
>
{visibleMenus.map((menu, menuIndex) =>
menu.title ? (
<div className="relative" key={menuIndex}>
<Button
variant="ghost"
className="gap-1.5 px-2.5 py-2 text-sm font-semibold text-[#5d675f] hover:translate-y-0 hover:text-[#172019]"
aria-expanded={openMenu === menuIndex}
aria-haspopup="true"
onClick={() => {
setOpenMenu(openMenu === menuIndex ? null : menuIndex);
}}
>
{menu.title}
<Chevron open={openMenu === menuIndex} />
</Button>
{openMenu === menuIndex ? (
<div className="absolute top-full left-0 z-30 mt-2 flex min-w-52 flex-col gap-0.5 rounded-2xl border border-[#d6d7cf] bg-[#fffdf8] p-2 shadow-[0_1rem_3rem_rgb(23_32_25/12%)]">
{menu.items.map((item, itemIndex) => (
<MenuLink
key={`${item.label}-${itemIndex}`}
link={item}
className="w-full rounded-lg! border-0! px-3! py-2! text-sm font-semibold text-[#172019] hover:bg-[#f4f1e9] hover:text-[#172019]"
/>
))}
</div>
) : null}
</div>
) : (
<div
className="flex items-center gap-1"
aria-label={undefined}
key={`menu-${menuIndex}`}
>
{menu.items.map((item, itemIndex) => (
<PageLink
key={`${item.label}-${itemIndex}`}
link={item}
className="px-2.5 py-2 text-sm font-semibold whitespace-nowrap text-[#5d675f] hover:text-[#172019]"
/>
))}
</div>
)
)}
</nav>
) : null}
<div className="ml-auto flex shrink-0 items-center gap-4">
<LanguageAlternatives alternatives={languageAlternatives} />
{ctaItems.map((item, index) => (
<PageLink
key={`${item.label}-${index}`}
link={item}
className="whitespace-nowrap max-lg:hidden"
/>
))}
{hasMenuContent ? (
<Button
variant="outline"
className="hidden size-11 border-[#172019]/15 p-0 hover:translate-y-0 hover:border-[#172019]/15 hover:bg-white/60 max-lg:flex"
aria-expanded={mobileOpen}
aria-label={mobileOpen ? t('close') : t('open')}
onClick={() => {
setMobileOpen((open) => !open);
setOpenMenu(null);
}}
>
<svg viewBox="0 0 20 20" className="size-5" aria-hidden="true">
{mobileOpen ? (
<path
d="M5 5l10 10M15 5L5 15"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
) : (
<path
d="M3 6h14M3 10h14M3 14h14"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
)}
</svg>
</Button>
) : null}
</div>
{mobileOpen ? (
<nav
className="absolute inset-x-0 top-full z-30 border-b border-[#172019]/10 bg-[#f4f1e9] shadow-[0_1.5rem_3rem_rgb(23_32_25/12%)] lg:hidden"
aria-label={t('main')}
>
<div className="mx-auto flex w-[min(calc(100%-2rem),76rem)] flex-col gap-5 py-6 max-[45rem]:w-[min(calc(100%-1.25rem),76rem)]">
{visibleMenus.map((menu, menuIndex) => (
<div key={`${menu.title}-${menuIndex}`}>
{menu.title ? (
<p className="mb-1 font-mono text-[0.7rem] font-bold tracking-[0.08em] text-[#5d675f] uppercase">
{menu.title}
</p>
) : null}
<div className="flex flex-col">
{menu.items.map((item, itemIndex) => (
<MenuLink
key={`${item.label}-${itemIndex}`}
link={item}
className="w-full py-2! text-[0.95rem] font-semibold text-[#172019]"
/>
))}
</div>
</div>
))}
{ctaItems.length > 0 ? (
<div className="flex flex-wrap gap-3 border-t border-[#d6d7cf] pt-5">
{ctaItems.map((item, index) => (
<PageLink key={`${item.label}-${index}`} link={item} />
))}
</div>
) : null}
</div>
</nav>
) : null}
</div>
);
}