Add initial project files
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user