215 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|