rework/lightweight #12
@ -9,9 +9,11 @@
|
||||
"@universal-middleware/fastify": "^0.5.16",
|
||||
"clsx": "^2.1.1",
|
||||
"fastify": "^5.3.0",
|
||||
"solid-heroicons": "^3.2.4",
|
||||
"solid-highlight": "^0.1.26",
|
||||
"solid-js": "^1.9.5",
|
||||
"telefunc": "^0.2.3",
|
||||
"terracotta": "^1.0.6",
|
||||
"vike": "^0.4.228",
|
||||
"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=="],
|
||||
|
||||
"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-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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
|
||||
|
||||
@ -28,7 +28,7 @@ const iconStyles = {
|
||||
|
||||
export type IconColor = keyof typeof iconStyles;
|
||||
|
||||
type IconProps = JSX.IntrinsicElements["svg"] & {
|
||||
export type IconProps = JSX.IntrinsicElements["svg"] & {
|
||||
color?: IconColor;
|
||||
icon: keyof typeof icons;
|
||||
};
|
||||
|
||||
97
app/components/PrevNextLinks.tsx
Normal file
97
app/components/PrevNextLinks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
app/components/QuickLinks.tsx
Normal file
46
app/components/QuickLinks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
92
app/layouts/MobileNavigation.tsx
Normal file
92
app/layouts/MobileNavigation.tsx
Normal 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
259
app/layouts/Navigation.tsx
Normal 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
172
app/libs/navigation.ts
Normal 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;
|
||||
}
|
||||
@ -14,9 +14,11 @@
|
||||
"@universal-middleware/fastify": "^0.5.16",
|
||||
"clsx": "^2.1.1",
|
||||
"fastify": "^5.3.0",
|
||||
"solid-heroicons": "^3.2.4",
|
||||
"solid-highlight": "^0.1.26",
|
||||
"solid-js": "^1.9.5",
|
||||
"telefunc": "^0.2.3",
|
||||
"terracotta": "^1.0.6",
|
||||
"vike": "^0.4.228",
|
||||
"vike-solid": "^0.7.9"
|
||||
},
|
||||
|
||||
10
app/utils/cleanProps.ts
Normal file
10
app/utils/cleanProps.ts
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user