memento-dev/app/partials/Navigation.tsx
GauthierWebDev b29acdc202
All checks were successful
Update Memento Dev on VPS / deploy (push) Successful in 1m16s
seo/sitemap (#18)
Reviewed-on: #18
Co-authored-by: GauthierWebDev <gauthier@gauthierdaniels.fr>
Co-committed-by: GauthierWebDev <gauthier@gauthierdaniels.fr>
2025-04-22 11:26:37 +00:00

267 lines
7.0 KiB
TypeScript

import type { NavigationLink } from "@/libs/navigation";
import { chevronDown, chevronUp } from "solid-heroicons/solid";
import { createEffect, createSignal, For } from "solid-js";
import { usePageContext } from "vike-solid/usePageContext";
import { navigation } from "@/libs/navigation";
import { Link } from "@/components/Link";
import { Icon } from "solid-heroicons";
import clsx from "clsx";
type NavigationItemProps = {
section: (typeof navigation)[number];
onLinkClick?: (event: MouseEvent) => void;
};
function NavigationItem(props: NavigationItemProps) {
const pageContext = usePageContext();
const checkIsLinkActive = (link: NavigationLink) => {
return (
link.href === pageContext.urlPathname ||
link.subitems?.some((subitem) => subitem.href === pageContext.urlPathname)
);
};
const checkIsActive = () => {
return props.section.links.some((link) => checkIsLinkActive(link));
};
const [isOpened, setIsOpened] = createSignal(
checkIsActive() || pageContext.urlPathname === "/",
);
createEffect(() => {
// If the current URL path is the same as the link's href, set isOpened to true
if (!isOpened() && checkIsActive()) {
setIsOpened(true);
}
}, [pageContext.urlPathname]);
return (
<>
<h3
class={clsx(
"font-display font-medium cursor-pointer",
checkIsActive() ? "text-violet-600" : "text-slate-900",
)}
>
<button
onClick={() => setIsOpened((prev) => !prev)}
type="button"
aria-label={`${isOpened() ? "Masquer" : "Afficher"} les sous-sections de ${props.section.title}`}
class="contents cursor-pointer"
>
{isOpened() ? (
<Icon
path={chevronUp}
class="inline-block mr-2 h-5 w-5 text-slate-400 shrink-0"
/>
) : (
<Icon
path={chevronDown}
class="inline-block mr-2 h-5 w-5 text-slate-400 shrink-0"
/>
)}
<span class="sr-only">{isOpened() ? "Masquer" : "Afficher"}</span>
{props.section.title}
<span class="text-slate-400"> ({props.section.links.length})</span>
</button>
</h3>
{isOpened() && (
<ul class="!mt-0 ml-2 border-l-2 border-slate-100 lg:mt-4 lg:space-y-1 lg:border-slate-200 mb-2">
<For each={props.section.links}>
{(link) => (
<li class="relative">
<NavigationSubItem
link={link}
onLinkClick={props.onLinkClick}
isOpened={checkIsLinkActive(link)}
/>
</li>
)}
</For>
</ul>
)}
</>
);
}
type NavigationSubItemProps = {
link: NavigationLink;
onLinkClick?: (event: MouseEvent) => void;
isOpened?: boolean;
};
function NavigationSubItem(props: NavigationSubItemProps) {
const [isOpened, setIsOpened] = createSignal(props.isOpened);
const pageContext = usePageContext();
const checkIsLinkActive = (link: NavigationLink) => {
return (
link.href === pageContext.urlPathname ||
link.subitems?.some((subitem) => subitem.href === pageContext.urlPathname)
);
};
createEffect(() => {
setIsOpened(checkIsLinkActive(props.link) || props.isOpened);
}, [pageContext.urlPathname, props.link]);
return (
<>
<span class="pl-2 flex cursor-pointer">
{props.link.subitems.length > 0 && (
<button
onClick={() => setIsOpened((prev) => !prev)}
type="button"
aria-label={`${isOpened() ? "Masquer" : "Afficher"} les sous-sections de ${props.link.title}`}
>
<span class="sr-only">{isOpened() ? "Masquer" : "Afficher"}</span>
{isOpened() ? (
<Icon
path={chevronUp}
class="inline-block h-5 w-5 text-slate-400"
/>
) : (
<Icon
path={chevronDown}
class="inline-block h-5 w-5 text-slate-400"
/>
)}
</button>
)}
<Link
href={props.link.href}
onClick={props.onLinkClick ?? undefined}
class={clsx(
"block pl-2 w-full before:pointer-events-none before:absolute before:-left-1 before:h-1.5 before:w-1.5 before:rounded-full",
{ "before:top-1/2 before:-translate-y-1/2": !props.link.subitems },
{
"before:top-3 before:-translate-y-1/2 font-semibold":
props.link.subitems,
},
props.link.href !== pageContext.urlPathname && "before:hidden",
checkIsLinkActive(props.link)
? "text-violet-500 before:bg-violet-500"
: "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block",
)}
>
{props.link.title}
{props.link.subitems.length > 0 && (
<span class="text-slate-400"> ({props.link.subitems.length})</span>
)}
</Link>
</span>
{props.link.subitems.length > 0 && isOpened() && (
<ul class="ml-4 border-l-2 border-slate-100 space-y-1 lg:space-y-2 lg:border-slate-200 mb-4">
<For each={props.link.subitems}>
{(subitem) => (
<li class="relative">
<Link
href={subitem.href}
onClick={props.onLinkClick}
class={clsx(
"block w-full pl-3.5 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full",
subitem.href === pageContext.urlPathname
? "font-semibold text-violet-500 before:bg-violet-500"
: "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block",
)}
>
{subitem.title}
</Link>
</li>
)}
</For>
</ul>
)}
</>
);
}
export function Navigation(props: {
class?: string;
onLinkClick?: (event: MouseEvent) => void;
}) {
const firstSections = navigation.filter(
(section) => section.position === "start",
);
const lastSections = navigation.filter(
(section) => section.position === "end",
);
const filteredSections = navigation
.filter(
(section) =>
section.position === "auto" || section.position === undefined,
)
.reduce(
(acc, section) => {
if (!acc[section.type]) {
acc[section.type] = [];
}
acc[section.type].push(section);
return acc;
},
{} as Record<string, typeof navigation>,
);
return (
<nav class={clsx("text-base lg:text-sm", props.class)}>
<ul class="space-y-4">
<li>
<h2 class="font-display font-bold text-base text-slate-900">
{firstSections[0]?.type}
</h2>
<For each={firstSections}>
{(section) => (
<NavigationItem
section={section}
onLinkClick={props.onLinkClick}
/>
)}
</For>
</li>
<For each={Object.entries(filteredSections)}>
{([type, sections]) => (
<li>
<h2 class="font-display font-bold text-base text-slate-900">
{type}
</h2>
<For each={sections}>
{(section) => (
<NavigationItem
section={section}
onLinkClick={props.onLinkClick}
/>
)}
</For>
</li>
)}
</For>
<li>
<h2 class="font-display font-bold text-base text-slate-900">
{lastSections[0]?.type}
</h2>
<For each={lastSections}>
{(section) => (
<NavigationItem
section={section}
onLinkClick={props.onLinkClick}
/>
)}
</For>
</li>
</ul>
</nav>
);
}