feat: Add new components PrevNextLinks, QuickLinks, MobileNavigation, Navigation, and utility function cleanProps

This commit is contained in:
Gauthier Daniels 2025-04-19 15:06:08 +02:00
parent cec56f2896
commit 8837be52cd
9 changed files with 687 additions and 1 deletions

View File

@ -9,9 +9,11 @@
"@universal-middleware/fastify": "^0.5.16", "@universal-middleware/fastify": "^0.5.16",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fastify": "^5.3.0", "fastify": "^5.3.0",
"solid-heroicons": "^3.2.4",
"solid-highlight": "^0.1.26", "solid-highlight": "^0.1.26",
"solid-js": "^1.9.5", "solid-js": "^1.9.5",
"telefunc": "^0.2.3", "telefunc": "^0.2.3",
"terracotta": "^1.0.6",
"vike": "^0.4.228", "vike": "^0.4.228",
"vike-solid": "^0.7.9", "vike-solid": "^0.7.9",
}, },
@ -725,12 +727,16 @@
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="], "sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
"solid-heroicons": ["solid-heroicons@3.2.4", "", { "dependencies": { "solid-js": "^1.7.6" } }, "sha512-u6BMdFLvkJnvUGYzdFcWp1wvJ4hb9Y1zd3AbZ9D3bUmmiy9jBzNZX+RcqBCI2EKRvdQwAb1UB9bkESfqfhayDg=="],
"solid-highlight": ["solid-highlight@0.1.26", "", { "peerDependencies": { "prismjs": "^1.29.0", "solid-js": "^1.8.0" } }, "sha512-Iw1mi3vE+YCBBBU/+HHc5y8VNULaGXUX4OK2c9TbzegOGCitzW0uNIvb0s3S0KoVv7Uma/KadWHNFXkwZCwPgQ=="], "solid-highlight": ["solid-highlight@0.1.26", "", { "peerDependencies": { "prismjs": "^1.29.0", "solid-js": "^1.8.0" } }, "sha512-Iw1mi3vE+YCBBBU/+HHc5y8VNULaGXUX4OK2c9TbzegOGCitzW0uNIvb0s3S0KoVv7Uma/KadWHNFXkwZCwPgQ=="],
"solid-js": ["solid-js@1.9.5", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "^1.1.0", "seroval-plugins": "^1.1.0" } }, "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw=="], "solid-js": ["solid-js@1.9.5", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "^1.1.0", "seroval-plugins": "^1.1.0" } }, "sha512-ogI3DaFcyn6UhYhrgcyRAMbu/buBJitYQASZz5WzfQVPP10RD2AbCoRZ517psnezrasyCbWzIxZ6kVqet768xw=="],
"solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="],
"solid-use": ["solid-use@0.9.1", "", { "peerDependencies": { "solid-js": "^1.7" } }, "sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@ -765,6 +771,8 @@
"telefunc": ["telefunc@0.2.3", "", { "dependencies": { "@brillout/import": "^0.2.6", "@brillout/json-serializer": "^0.5.6", "@brillout/picocolors": "^1.0.26", "@brillout/vite-plugin-server-entry": "^0.7.5", "es-module-lexer": "^1.6.0", "magic-string": "^0.30.17", "ts-morph": "^19.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/parser": ">=7.0.0", "@babel/types": ">=7.0.0", "react": ">=18.0.0", "react-streaming": ">=0.3.3" }, "optionalPeers": ["@babel/core", "@babel/parser", "@babel/types", "react", "react-streaming"] }, "sha512-BZejs1iUrsodzti34+QvgaBUPMIf9H0eUJB0f1VzwVeU3mflvdNKZ5KHOP7iiQYyiNfFjcUbUktMFrjA2J8sHg=="], "telefunc": ["telefunc@0.2.3", "", { "dependencies": { "@brillout/import": "^0.2.6", "@brillout/json-serializer": "^0.5.6", "@brillout/picocolors": "^1.0.26", "@brillout/vite-plugin-server-entry": "^0.7.5", "es-module-lexer": "^1.6.0", "magic-string": "^0.30.17", "ts-morph": "^19.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/parser": ">=7.0.0", "@babel/types": ">=7.0.0", "react": ">=18.0.0", "react-streaming": ">=0.3.3" }, "optionalPeers": ["@babel/core", "@babel/parser", "@babel/types", "react", "react-streaming"] }, "sha512-BZejs1iUrsodzti34+QvgaBUPMIf9H0eUJB0f1VzwVeU3mflvdNKZ5KHOP7iiQYyiNfFjcUbUktMFrjA2J8sHg=="],
"terracotta": ["terracotta@1.0.6", "", { "dependencies": { "solid-use": "^0.9.0" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw=="],
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="], "tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],

View File

@ -28,7 +28,7 @@ const iconStyles = {
export type IconColor = keyof typeof iconStyles; export type IconColor = keyof typeof iconStyles;
type IconProps = JSX.IntrinsicElements["svg"] & { export type IconProps = JSX.IntrinsicElements["svg"] & {
color?: IconColor; color?: IconColor;
icon: keyof typeof icons; icon: keyof typeof icons;
}; };

View File

@ -0,0 +1,97 @@
import type { JSX } from "solid-js";
import { usePageContext } from "vike-solid/usePageContext";
import { cleanProps } from "@/utils/cleanProps";
import { navigation } from "@/libs/navigation";
import { Link } from "@/components/Link";
import clsx from "clsx";
function ArrowIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
<path d="m9.182 13.423-1.17-1.16 3.505-3.505H3V7.065h8.517l-3.506-3.5L9.181 2.4l5.512 5.511-5.511 5.512Z" />
</svg>
);
}
type PageLinkProps = Omit<JSX.IntrinsicElements["div"], "dir" | "title"> & {
title: string;
href: string;
dir?: "previous" | "next";
};
function PageLink(props: PageLinkProps) {
const pageCategory = navigation.find((section) => {
return section.links.some(
(link) =>
link.href === props.href ||
link.subitems.some((subitem) => subitem.href === props.href),
);
});
return (
<div {...cleanProps(props, "dir", "title")}>
<dt class="font-display text-sm font-medium text-slate-900 dark:text-white">
{props.dir === "next" ? "Suivant" : "Précédent"}
</dt>
<dd class="mt-1">
<Link
href={props.href}
class={clsx(
"flex items-center gap-x-2 text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300",
props.dir === "previous" && "flex-row-reverse",
)}
>
<p class="flex flex-col gap-0">
{pageCategory && (
<span class="text-violet-600 dark:text-violet-400 text-sm mb-1 leading-3">
{pageCategory.title}
</span>
)}
<span class="leading-4">{props.title}</span>
</p>
<ArrowIcon
class={clsx(
"h-6 w-6 flex-none fill-current",
props.dir === "previous" && "-scale-x-100",
)}
/>
</Link>
</dd>
</div>
);
}
export function PrevNextLinks() {
const { urlPathname } = usePageContext();
const allLinks = navigation
.flatMap((section) => section.links)
.flatMap((link) => {
return link.subitems ? [link, ...link.subitems] : link;
});
const getNeighboringLinks = () => {
const linkIndex = allLinks.findIndex((link) => link.href === urlPathname);
if (linkIndex === -1) return [null, null];
const previousPage = allLinks[linkIndex - 1] || null;
let nextPage = allLinks[linkIndex + 1] || null;
if (nextPage?.href === urlPathname) {
nextPage = allLinks[linkIndex + 2] || null;
}
return [previousPage, nextPage];
};
const [previousPage, nextPage] = getNeighboringLinks();
if (!nextPage && !previousPage) return null;
return (
<dl class="mt-12 flex gap-4 border-t border-slate-200 pt-6 dark:border-slate-800">
{previousPage && <PageLink dir="previous" {...previousPage} />}
{nextPage && <PageLink class="ml-auto text-right" {...nextPage} />}
</dl>
);
}

View File

@ -0,0 +1,46 @@
import type { JSXElement } from "solid-js";
import type { IconProps } from "./Icon";
import { Icon } from "./Icon";
import { Link } from "./Link";
type QuickLinksProps = {
children: JSXElement;
};
export function QuickLinks(props: QuickLinksProps) {
return (
<div class="not-prose my-12 grid grid-cols-1 gap-6 sm:grid-cols-2">
{props.children}
</div>
);
}
type QuickLinkProps = {
title: string;
description: string;
href: string;
icon: IconProps["icon"];
};
export function QuickLink(props: QuickLinkProps) {
return (
<div class="group relative rounded-xl border border-slate-200 dark:border-slate-800">
<div class="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--quick-links-hover-bg,var(--color-violet-50)),var(--quick-links-hover-bg,var(--color-violet-50)))_padding-box,linear-gradient(to_top,var(--color-indigo-400),var(--color-cyan-400),var(--color-violet-500))_border-box] group-hover:opacity-100 dark:[--quick-links-hover-bg:var(--color-slate-800)]" />
<div class="relative overflow-hidden rounded-xl p-6">
<Icon icon={props.icon} class="h-8 w-8" />
<h2 class="mt-4 font-display text-base text-slate-900 dark:text-white">
<Link href={props.href}>
<span class="absolute -inset-px rounded-xl" />
{props.title}
</Link>
</h2>
<p class="mt-1 text-sm text-slate-700 dark:text-slate-400">
{props.description}
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,92 @@
import type { JSX } from "solid-js";
import { usePageContext } from "vike-solid/usePageContext";
import { createEffect, createSignal } from "solid-js";
import { Dialog, DialogPanel } from "terracotta";
import { Navigation } from "./Navigation";
import { Link } from "@/components/Link";
import { Logo } from "@/components/Logo";
function MenuIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke-width="2"
stroke-linecap="round"
{...props}
>
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
);
}
function CloseIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
stroke-width="2"
stroke-linecap="round"
{...props}
>
<path d="M5 5l14 14M19 5l-14 14" />
</svg>
);
}
function CloseOnNavigation({ close }: { close: () => void }) {
const { urlPathname } = usePageContext();
createEffect(() => {
close();
}, [urlPathname, close]);
return null;
}
export function MobileNavigation() {
const [isOpen, setIsOpen] = createSignal(false);
const close = () => setIsOpen(false);
return (
<>
<button
type="button"
onClick={() => setIsOpen(true)}
class="relative"
aria-label="Ouvrir le menu de navigation"
>
<MenuIcon class="h-6 w-6 stroke-slate-500" />
</button>
<CloseOnNavigation close={close} />
<Dialog
isOpen={isOpen()}
onClose={close}
class="fixed inset-0 z-50 flex items-start overflow-y-auto bg-slate-900/50 pr-10 backdrop-blur-sm lg:hidden"
aria-label="Navigation"
>
<DialogPanel class="min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 sm:px-6 dark:bg-slate-900">
<div class="flex items-center">
<button
type="button"
onClick={close}
aria-label="Fermer le menu de navigation"
>
<CloseIcon class="h-6 w-6 stroke-slate-500" />
</button>
<Link href="/" class="ml-6" aria-label="Page d'accueil">
<Logo class="h-6 w-auto shrink-0" />
</Link>
</div>
<Navigation class="mt-5 px-1" onLinkClick={close} />
</DialogPanel>
</Dialog>
</>
);
}

259
app/layouts/Navigation.tsx Normal file
View File

@ -0,0 +1,259 @@
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 { urlPathname } = usePageContext();
const [isOpened, setIsOpened] = createSignal(
props.section.links.some(
(link) =>
link.href === urlPathname ||
link.subitems?.some((subitem) => subitem.href === urlPathname),
),
);
return (
<>
<h2
class={clsx(
"font-display font-medium cursor-pointer",
isOpened()
? "text-violet-600 dark:text-violet-200"
: "text-slate-900 dark:text-white ",
)}
>
<button
onClick={() => setIsOpened((prev) => !prev)}
type="button"
aria-label={`${isOpened() ? "Masquer" : "Afficher"} les sous-sections de ${props.section.title}`}
>
<span class="sr-only">{isOpened() ? "Masquer" : "Afficher"}</span>
{isOpened() ? (
<Icon
path={chevronUp}
class="inline-block mr-2 h-5 w-5 text-slate-400"
/>
) : (
<Icon
path={chevronDown}
class="inline-block mr-2 h-5 w-5 text-slate-400"
/>
)}
<span class="sr-only">{isOpened() ? "Masquer" : "Afficher"}</span>
{props.section.title}
<span class="text-slate-400 dark:text-slate-500">
{" "}
({props.section.links.length})
</span>
</button>
</h2>
{isOpened() && (
<ul class="!mt-0 ml-2 space-y-1 border-l-2 border-slate-100 lg:mt-4 lg:space-y-2 lg:border-slate-200 dark:border-slate-800 mb-4">
<For each={props.section.links}>
{(link) => (
<li class="relative">
<NavigationSubItem
link={link}
onLinkClick={props.onLinkClick}
isOpened={
link.href === urlPathname ||
link.subitems?.some(
(subitem) => subitem.href === urlPathname,
)
}
/>
</li>
)}
</For>
</ul>
)}
</>
);
}
type NavigationSubItemProps = {
link: (typeof navigation)[number]["links"][number];
onLinkClick?: (event: MouseEvent) => void;
isOpened?: boolean;
};
function NavigationSubItem(props: NavigationSubItemProps) {
const [isOpened, setIsOpened] = createSignal(props.isOpened);
const { urlPathname } = usePageContext();
createEffect(() => {
setIsOpened(
props.link.href === urlPathname ||
props.link.subitems?.some((subitem) => subitem.href === urlPathname),
);
}, [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 !== urlPathname && "before:hidden",
isOpened()
? "text-violet-500 before:bg-violet-500"
: "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300",
)}
>
{props.link.title}
{props.link.subitems.length > 0 && (
<span class="text-slate-400 dark:text-slate-500">
{" "}
({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 dark:border-slate-800 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 === 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 dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300",
)}
>
{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 dark:text-white">
{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 dark:text-white">
{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 dark:text-white">
{lastSections[0]?.type}
</h2>
<For each={lastSections}>
{(section) => (
<NavigationItem
section={section}
onLinkClick={props.onLinkClick}
/>
)}
</For>
</li>
</ul>
</nav>
);
}

172
app/libs/navigation.ts Normal file
View File

@ -0,0 +1,172 @@
const navigationsTypes = {
GLOBAL: "👋 Général",
CERTIFICATIONS: "🎓 Certifications",
DOCUMENTATIONS: "📚 Documentations",
OTHER: "🔗 Autres",
};
export type NavigationSection = {
title: string;
type: (typeof navigationsTypes)[keyof typeof navigationsTypes];
position: "start" | "end" | "auto";
links: NavigationLink[];
};
export type NavigationOG = Partial<{
image: string;
}>;
export type NavigationLink = {
title: string;
href: string;
og?: NavigationOG;
subitems: NavigationSubItem[];
};
export type NavigationSubItem = {
title: string;
href: string;
};
export const navigation: NavigationSection[] = [
{
title: "Préambule",
type: navigationsTypes.GLOBAL,
position: "start",
links: [
{ title: "Memento Dev", href: "/", subitems: [] },
{ title: "Certifications", href: "/certifications", subitems: [] },
{ title: "Documentations", href: "/docs", subitems: [] },
],
},
{
title: "Communauté",
type: navigationsTypes.GLOBAL,
position: "start",
links: [
{
title: "Influenceurs",
href: "/docs/communaute/influenceurs",
subitems: [],
},
{
title: "Partages et réutilisations",
href: "/docs/communaute/partages",
subitems: [],
},
],
},
{
title: "Légal",
type: navigationsTypes.OTHER,
position: "end",
links: [
{ title: "Mentions légales", href: "/mentions-legales", subitems: [] },
{
title: "Politique de confidentialité",
href: "/politique-de-confidentialite",
subitems: [],
},
],
},
{
title: "Développeur Web et Web Mobile",
type: navigationsTypes.CERTIFICATIONS,
position: "auto",
links: [
{ title: "Résumé du titre", href: "/certifications/dwwm", subitems: [] },
{
title: "Activité Type 1",
href: "/certifications/dwwm/at1",
subitems: [
{ title: "Résumé de l'AT", href: "/certifications/dwwm/at1" },
{ title: "CP 1", href: "/certifications/dwwm/at1/cp1" },
{ title: "CP 2", href: "/certifications/dwwm/at1/cp2" },
{ title: "CP 3", href: "/certifications/dwwm/at1/cp3" },
{ title: "CP 4", href: "/certifications/dwwm/at1/cp4" },
],
},
{
title: "Activité Type 2",
href: "/certifications/dwwm/at2",
subitems: [
{ title: "Résumé de l'AT", href: "/certifications/dwwm/at2" },
{ title: "CP 5", href: "/certifications/dwwm/at2/cp5" },
{ title: "CP 6", href: "/certifications/dwwm/at2/cp6" },
{ title: "CP 7", href: "/certifications/dwwm/at2/cp7" },
{ title: "CP 8", href: "/certifications/dwwm/at2/cp8" },
],
},
],
},
{
title: "Front-end",
type: navigationsTypes.DOCUMENTATIONS,
position: "auto",
links: [
{
title: "React",
href: "/docs/react",
subitems: [
{ title: "Introduction", href: "/docs/react" },
{ title: "Initialisation", href: "/docs/react/initialisation" },
{ title: "Syntaxe JSX", href: "/docs/react/jsx" },
{ title: "Premier composant", href: "/docs/react/premier-composant" },
{
title: "State et cycle de vie",
href: "/docs/react/state-et-cycle-de-vie",
},
{ title: "Hooks", href: "/docs/react/hooks" },
{ title: "Le hook useContext", href: "/docs/react/use-context" },
{ title: "Le hook useReducer", href: "/docs/react/use-reducer" },
],
},
],
},
{
title: "Base de données",
type: navigationsTypes.DOCUMENTATIONS,
position: "auto",
links: [
{
title: "Merise",
href: "/docs/merise",
og: { image: "/merise/og.webp" },
subitems: [
{ title: "Introduction", href: "/docs/merise" },
{
title: "Dictionnaire de données",
href: "/docs/merise/dictionnaire-de-donnees",
},
{ title: "Modèle Conceptuel de Données", href: "/docs/merise/mcd" },
{ title: "Modèle Logique de Données", href: "/docs/merise/mld" },
{ title: "Modèle Physique de Données", href: "/docs/merise/mpd" },
],
},
],
},
];
export function doesLinkSubitemExist(
link: NavigationLink,
subitemHref: string,
): boolean {
return link.subitems.some((subitem) => subitem.href === subitemHref);
}
export function findNavigationLink(
namespace: string,
href?: string,
): NavigationLink | undefined {
const currentUrl = `/${namespace}/${href}`
.replace(/\/+/g, "/")
.replace(/\/$/, "");
const foundLink = navigation
.flatMap((section) => section.links)
.find((link) => {
return link.href === currentUrl || doesLinkSubitemExist(link, currentUrl);
});
return foundLink;
}

View File

@ -14,9 +14,11 @@
"@universal-middleware/fastify": "^0.5.16", "@universal-middleware/fastify": "^0.5.16",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fastify": "^5.3.0", "fastify": "^5.3.0",
"solid-heroicons": "^3.2.4",
"solid-highlight": "^0.1.26", "solid-highlight": "^0.1.26",
"solid-js": "^1.9.5", "solid-js": "^1.9.5",
"telefunc": "^0.2.3", "telefunc": "^0.2.3",
"terracotta": "^1.0.6",
"vike": "^0.4.228", "vike": "^0.4.228",
"vike-solid": "^0.7.9" "vike-solid": "^0.7.9"
}, },

10
app/utils/cleanProps.ts Normal file
View File

@ -0,0 +1,10 @@
export function cleanProps(
props: Record<string, unknown>,
...propsToRemove: string[]
): Record<string, unknown> {
const newProps = { ...props };
for (const prop of propsToRemove) {
delete newProps[prop];
}
return newProps;
}