chore: new architecture and technos

This commit is contained in:
Gauthier Daniels 2025-04-11 11:11:22 +02:00
commit 85937c2213
355 changed files with 39560 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.env
app/.pnpm-store
app/node_modules

7
app/.env Normal file
View File

@ -0,0 +1,7 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

3
app/.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"printWidth": 120
}

53
app/README.md Normal file
View File

@ -0,0 +1,53 @@
Generated with [vike.dev/new](https://vike.dev/new) ([version 410](https://www.npmjs.com/package/create-vike/v/0.0.410)) using this command:
```sh
pnpm create vike@latest --react --tailwindcss --telefunc --fastify --eslint --prettier
```
## Contents
* [React](#react)
* [`/pages/+config.ts`](#pagesconfigts)
* [Routing](#routing)
* [`/pages/_error/+Page.jsx`](#pages_errorpagejsx)
* [`/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`](#pagesonpagetransitionstartts-and-pagesonpagetransitionendts)
* [SSR](#ssr)
* [HTML Streaming](#html-streaming)
## React
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
### `/pages/+config.ts`
Such `+` files are [the interface](https://vike.dev/config) between Vike and your code. It defines:
* A default [`<Layout>` component](https://vike.dev/Layout) (that wraps your [`<Page>` components](https://vike.dev/Page)).
* A default [`title`](https://vike.dev/title).
* Global [`<head>` tags](https://vike.dev/head-tags).
### Routing
[Vike's built-in router](https://vike.dev/routing) lets you choose between:
* [Filesystem Routing](https://vike.dev/filesystem-routing) (the URL of a page is determined based on where its `+Page.jsx` file is located on the filesystem)
* [Route Strings](https://vike.dev/route-string)
* [Route Functions](https://vike.dev/route-function)
### `/pages/_error/+Page.jsx`
The [error page](https://vike.dev/error-page) which is rendered when errors occur.
### `/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`
The [`onPageTransitionStart()` hook](https://vike.dev/onPageTransitionStart), together with [`onPageTransitionEnd()`](https://vike.dev/onPageTransitionEnd), enables you to implement page transition animations.
### SSR
SSR is enabled by default. You can [disable it](https://vike.dev/ssr) for all your pages or only for some pages.
### HTML Streaming
You can enable/disable [HTML streaming](https://vike.dev/stream) for all your pages, or only for some pages while still using it for others.

BIN
app/assets/logo.afdesign Normal file

Binary file not shown.

Binary file not shown.

10
app/assets/logo.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 57 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(-1.76727,0,0,1.76727,49.1089,-3.53454)">
<path d="M16.161,18.989L20.49,23.32L21.9,21.91L2.1,2.1L0.69,3.51L2.714,5.535L-4.085,11.253L-4.085,13.054L3.185,19.167L4.629,17.337L-1.61,12.165L4.397,7.219L9.588,12.412L6,16L6.01,16.01L6,16.01L6,22L18,22L18,20.83L16.161,18.989ZM14.417,17.244L16,18.83L16,20L8,20L8,16.5L10.837,13.663L14.417,17.244ZM8,4L16,4L16,7.5L13.16,10.34L14.41,11.59L18,8.01L17.99,8L18,8L18,2L6,2L6,3.17L8,5.17L8,4ZM25.294,12.164L19.071,17.34L20.542,19.164L27.788,13.075L27.788,11.274L20.597,5.22L19.158,7.075L25.294,12.164Z" style="fill:url(#_Linear1);"/>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(12.792,-21.32,-21.32,-12.792,5.208,23.32)"><stop offset="0" style="stop-color:rgb(43,127,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(142,81,255);stop-opacity:1"/></linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
export function Image(props: { src: string; alt: string } & React.ComponentPropsWithoutRef<"img">) {
return <img {...props} src={props.src} alt={props.alt} loading="lazy" />;
}

View File

@ -0,0 +1,13 @@
import { usePageContext } from "vike-react/usePageContext";
import clsx from "clsx";
export function Link(props: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) {
const { urlPathname } = usePageContext();
const isActive = props.href === "/" ? urlPathname === props.href : urlPathname.startsWith(props.href);
return (
<a {...props} href={props.href} className={clsx(isActive && "is-active", props.className)}>
{props.children}
</a>
);
}

View File

@ -0,0 +1,95 @@
import type { Dispatch, SetStateAction } from "react";
import { createContext, useContext, useEffect, useState } from "react";
import { Button } from "@syntax/Button";
import clsx from "clsx";
type TabType = {
label: string;
value: string;
};
type TabsContextType = {
selectedTab: string;
selectTab: Dispatch<SetStateAction<string>>;
tabs: TabType[];
addTab: (tab: TabType) => void;
};
const TabsContext = createContext<TabsContextType>({
selectedTab: "",
selectTab: () => {},
tabs: [],
addTab: () => {},
});
export function Tabs({
defaultSelectedTab = "",
children,
}: {
defaultSelectedTab?: string;
children: React.ReactNode;
}) {
const [selectedTab, selectTab] = useState(defaultSelectedTab);
const [tabs, setTabs] = useState<TabType[]>([]);
const addTab = (tab: TabType) =>
setTabs((prevTabs) => {
// Append to the end of the array and make sure it's unique
if (prevTabs.some((t) => t.value === tab.value)) {
return prevTabs;
}
return [...prevTabs, tab];
});
return (
<TabsContext.Provider
value={{
selectedTab,
selectTab,
tabs,
addTab,
}}
>
<div className="relative">
<div className="max-w-full overflow-auto">
<ul className="-mb-4 !p-0 w-max flex items-stretch gap-2" aria-orientation="horizontal" role="tablist">
{tabs.map((tab) => (
<li key={tab.value} className="overflow-hidden" role="tab" aria-selected={selectedTab === tab.value}>
<TabItem tab={tab} isSelected={selectedTab === tab.value} select={() => selectTab(tab.value)} />
</li>
))}
</ul>
</div>
<div className={clsx("p-2")}>{children}</div>
</div>
</TabsContext.Provider>
);
}
export function TabItem({ tab, isSelected, select }: { tab: TabType; isSelected: boolean; select: () => void }) {
return (
<Button
variant={isSelected ? "primary" : "secondary"}
className={clsx("!rounded-md", isSelected && "cursor-default")}
onClick={select}
>
{tab.label}
</Button>
);
}
export function TabContent({ label, value, children }: { label: string; value: string; children: React.ReactNode }) {
const { addTab, selectedTab } = useContext(TabsContext);
useEffect(() => {
addTab({ label, value });
}, []);
return (
<div className={clsx("[&>*:first-of-type]:!mt-0", "[&>*:last-of-type]:!mb-0", selectedTab !== value && "hidden")}>
{children}
</div>
);
}

View File

@ -0,0 +1,23 @@
import { Link } from "@/components/common/Link";
import clsx from "clsx";
const variantStyles = {
primary:
"rounded-full bg-violet-300 py-2 px-4 text-sm font-semibold text-slate-900 hover:bg-violet-200 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-300/50 active:bg-violet-500",
secondary:
"rounded-full bg-slate-800 py-2 px-4 text-sm font-medium text-white hover:bg-slate-700 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50 active:text-slate-400",
};
type ButtonProps = {
variant?: keyof typeof variantStyles;
} & (React.ComponentPropsWithoutRef<typeof Link> | (React.ComponentPropsWithoutRef<"button"> & { href?: undefined }));
export function Button({ variant = "primary", className, ...props }: ButtonProps) {
className = clsx(variantStyles[variant], "cursor-pointer", className);
return typeof props.href === "undefined" ? (
<button className={className} {...props} />
) : (
<Link className={className} {...props} />
);
}

View File

@ -0,0 +1,44 @@
import { Icon } from "@syntax/Icon";
import clsx from "clsx";
const styles = {
note: {
container: "bg-violet-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
title: "text-violet-900 dark:text-violet-400",
body: "text-violet-800 [--tw-prose-background:var(--color-violet-50)] prose-a:text-violet-900 prose-code:text-violet-900 dark:text-slate-300 dark:prose-code:text-slate-300",
},
warning: {
container: "bg-amber-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
title: "text-amber-900 dark:text-amber-500",
body: "text-amber-800 [--tw-prose-underline:var(--color-amber-400)] [--tw-prose-background:var(--color-amber-50)] prose-a:text-amber-900 prose-code:text-amber-900 dark:text-slate-300 dark:[--tw-prose-underline:var(--color-violet-700)] dark:prose-code:text-slate-300",
},
};
const icons = {
note: (props: { className?: string }) => <Icon icon="lightbulb" {...props} />,
warning: (props: { className?: string }) => <Icon icon="warning" color="amber" {...props} />,
};
export function Callout({
title,
children,
type = "note",
}: {
title: string;
children: React.ReactNode;
type?: keyof typeof styles;
}) {
let IconComponent = icons[type];
return (
<div className={clsx("my-8 flex flex-col rounded-3xl p-6", styles[type].container)}>
<div className="flex items-center gap-6">
<IconComponent className="h-8 w-8 flex-none" />
<p className={clsx("!m-0 font-display text-xl text-balance", styles[type].title)}>{title}</p>
</div>
<div className="mt-4 flex-auto">
<div className={clsx("prose mt-2.5", styles[type].body)}>{children}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,19 @@
import { usePageContext } from "vike-react/usePageContext";
import { navigation } from "@/lib/navigation";
export function DocsHeader({ title }: { title?: string }) {
const { urlPathname } = usePageContext();
const section = navigation.find((section) => section.links.find((link) => link.href === urlPathname));
if (!title && !section) {
return null;
}
return (
<header className="mb-9 space-y-1">
{section && <p className="font-display text-sm font-medium text-violet-500">{section.title}</p>}
{title && <h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">{title}</h1>}
</header>
);
}

View File

@ -0,0 +1,33 @@
import { type Node } from "@markdoc/markdoc";
import { TableOfContents } from "@syntax/TableOfContents";
import { PrevNextLinks } from "@syntax/PrevNextLinks";
import { collectSections } from "@/lib/sections";
import { DocsHeader } from "@syntax/DocsHeader";
import { Prose } from "@syntax/Prose";
export function DocsLayout({
children,
frontmatter: { title },
nodes,
}: {
children: React.ReactNode;
frontmatter: { title?: string };
nodes: Array<Node>;
}) {
let tableOfContents = collectSections(nodes);
return (
<>
<div className="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
<article>
<DocsHeader title={title} />
<Prose>{children}</Prose>
</article>
<PrevNextLinks />
</div>
<TableOfContents tableOfContents={tableOfContents} />
</>
);
}

View File

@ -0,0 +1,25 @@
import { Highlight } from "prism-react-renderer";
import { Fragment } from "react";
export function Fence({ children, language }: { children: string; language: string }) {
return (
<Highlight code={children.trimEnd()} language={language} theme={{ plain: {}, styles: [] }}>
{({ className, style, tokens, getTokenProps }) => (
<pre className={className} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{"\n"}
</Fragment>
))}
</code>
</pre>
)}
</Highlight>
);
}

View File

@ -0,0 +1,132 @@
import { HeroBackground } from "@syntax/HeroBackground";
import blurIndigoImage from "@/images/blur-indigo.png";
import blurCyanImage from "@/images/blur-cyan.png";
import { Image } from "@/components/common/Image";
import { Highlight } from "prism-react-renderer";
import { Button } from "@syntax/Button";
import { Fragment } from "react";
import clsx from "clsx";
const codeLanguage = "javascript";
const code = `export default {
role: 'developer',
qualifications: [
'DWWM',
'CDA',
'CDUI',
]
}`;
const tabs = [
{ name: "memento-dev.config.js", isActive: true },
{ name: "package.json", isActive: false },
];
function TrafficLightsIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
<circle cx="5" cy="5" r="4.5" />
<circle cx="21" cy="5" r="4.5" />
<circle cx="37" cy="5" r="4.5" />
</svg>
);
}
export function Hero() {
return (
<div className="overflow-hidden bg-slate-900 dark:mt-[-4.75rem] dark:-mb-32 dark:pt-[4.75rem] dark:pb-32">
<div className="py-16 sm:px-2 lg:relative lg:px-0 lg:py-20">
<div className="mx-auto grid max-w-2xl w-full grid-cols-1 items-center gap-x-8 gap-y-16 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12">
<div className="relative z-10 md:text-center lg:text-left">
<Image
className="absolute right-full bottom-full -mr-72 -mb-56 opacity-50"
src={blurCyanImage}
alt=""
width={530}
height={530}
/>
<div className="relative">
<p className="inline bg-linear-to-r from-indigo-200 via-violet-400 to-indigo-200 bg-clip-text font-display text-5xl tracking-tight text-transparent">
Souviens-toi que tu développeras.
</p>
<p className="mt-3 text-2xl tracking-tight text-slate-400">
Découvrez des ressources essentielles pour améliorer vos compétences en développement.
</p>
<div className="mt-8 flex gap-4 md:justify-center lg:justify-start">
<Button href="/">Accédez aux ressources</Button>
<Button href="/" variant="secondary">
Voir sur Github
</Button>
</div>
</div>
</div>
<div className="relative lg:static xl:pl-10">
<div className="absolute inset-x-[-50vw] -top-32 -bottom-48 [mask-image:linear-gradient(transparent,white,white)] lg:-top-32 lg:right-0 lg:-bottom-32 lg:left-[calc(50%+14rem)] lg:[mask-image:none] dark:[mask-image:linear-gradient(transparent,white,transparent)] lg:dark:[mask-image:linear-gradient(white,white,transparent)]">
<HeroBackground className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 lg:left-0 lg:translate-x-0 lg:translate-y-[-60%]" />
</div>
<div className="relative">
<Image className="absolute -top-64 -right-64" src={blurCyanImage} alt="" width={530} height={530} />
<Image className="absolute -right-44 -bottom-40" src={blurIndigoImage} alt="" width={567} height={567} />
<div className="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10 blur-lg" />
<div className="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10" />
<div className="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur-sm">
<div className="absolute -top-px right-11 left-20 h-px bg-linear-to-r from-violet-300/0 via-violet-300/70 to-violet-300/0" />
<div className="absolute right-20 -bottom-px left-11 h-px bg-linear-to-r from-purple-400/0 via-purple-400 to-purple-400/0" />
<div className="pt-4 pl-4">
<TrafficLightsIcon className="h-2.5 w-auto stroke-slate-500/30" />
<div className="mt-4 flex space-x-2 text-xs">
{tabs.map((tab) => (
<div
key={tab.name}
className={clsx(
"flex h-6 rounded-full",
tab.isActive
? "bg-linear-to-r from-violet-400/30 via-violet-400 to-violet-400/30 p-px font-medium text-violet-300"
: "text-slate-500",
)}
>
<div className={clsx("flex items-center rounded-full px-2.5", tab.isActive && "bg-slate-800")}>
{tab.name}
</div>
</div>
))}
</div>
<div className="mt-6 flex items-start px-1 text-sm">
<div
aria-hidden="true"
className="border-r border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
>
{Array.from({
length: code.split("\n").length,
}).map((_, index) => (
<Fragment key={index}>
{(index + 1).toString().padStart(2, "0")}
<br />
</Fragment>
))}
</div>
<Highlight code={code} language={codeLanguage} theme={{ plain: {}, styles: [] }}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={clsx(className, "flex overflow-x-auto pb-6")} style={style}>
<code className="px-4">
{tokens.map((line, lineIndex) => (
<div key={lineIndex} {...getLineProps({ line })}>
{line.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
</div>
))}
</code>
</pre>
)}
</Highlight>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,121 @@
import { useId } from "react";
export function HeroBackground(props: React.ComponentPropsWithoutRef<"svg">) {
const id = useId();
return (
<svg aria-hidden="true" viewBox="0 0 668 1069" width={668} height={1069} fill="none" {...props}>
<defs>
<clipPath id={`${id}-clip-path`}>
<path fill="#fff" transform="rotate(-180 334 534.4)" d="M0 0h668v1068.8H0z" />
</clipPath>
</defs>
<g opacity=".4" clipPath={`url(#${id}-clip-path)`} strokeWidth={4}>
<path
opacity=".3"
d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474"
stroke="#334155"
/>
<path
d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588"
stroke="#334155"
/>
<path
d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175"
stroke="#334155"
/>
<circle cx="83.5" cy="384.1" r="10.438" transform="rotate(-180 83.5 384.1)" fill="#1E293B" stroke="#334155" />
<circle cx="83.5" cy="200.399" r="10.438" transform="rotate(-180 83.5 200.399)" stroke="#334155" />
<circle cx="83.5" cy="81.412" r="10.438" transform="rotate(-180 83.5 81.412)" stroke="#334155" />
<circle
cx="183.699"
cy="375.75"
r="10.438"
transform="rotate(-180 183.699 375.75)"
fill="#1E293B"
stroke="#334155"
/>
<circle
cx="183.699"
cy="563.625"
r="10.438"
transform="rotate(-180 183.699 563.625)"
fill="#1E293B"
stroke="#334155"
/>
<circle cx="384.1" cy="651.3" r="10.438" transform="rotate(-180 384.1 651.3)" fill="#1E293B" stroke="#334155" />
<circle
cx="484.301"
cy="574.062"
r="10.438"
transform="rotate(-180 484.301 574.062)"
fill="#0EA5E9"
fillOpacity=".42"
stroke="#0EA5E9"
/>
<circle
cx="384.1"
cy="749.412"
r="10.438"
transform="rotate(-180 384.1 749.412)"
fill="#1E293B"
stroke="#334155"
/>
<circle cx="384.1" cy="1027.05" r="10.438" transform="rotate(-180 384.1 1027.05)" stroke="#334155" />
<circle cx="283.9" cy="924.763" r="10.438" transform="rotate(-180 283.9 924.763)" stroke="#334155" />
<circle cx="183.699" cy="870.487" r="10.438" transform="rotate(-180 183.699 870.487)" stroke="#334155" />
<circle
cx="283.9"
cy="738.975"
r="10.438"
transform="rotate(-180 283.9 738.975)"
fill="#1E293B"
stroke="#334155"
/>
<circle
cx="83.5"
cy="695.138"
r="10.438"
transform="rotate(-180 83.5 695.138)"
fill="#1E293B"
stroke="#334155"
/>
<circle
cx="83.5"
cy="484.3"
r="10.438"
transform="rotate(-180 83.5 484.3)"
fill="#0EA5E9"
fillOpacity=".42"
stroke="#0EA5E9"
/>
<circle
cx="484.301"
cy="432.112"
r="10.438"
transform="rotate(-180 484.301 432.112)"
fill="#1E293B"
stroke="#334155"
/>
<circle
cx="584.5"
cy="432.112"
r="10.438"
transform="rotate(-180 584.5 432.112)"
fill="#1E293B"
stroke="#334155"
/>
<circle
cx="584.5"
cy="642.95"
r="10.438"
transform="rotate(-180 584.5 642.95)"
fill="#1E293B"
stroke="#334155"
/>
<circle cx="484.301" cy="851.699" r="10.438" transform="rotate(-180 484.301 851.699)" stroke="#334155" />
<circle cx="384.1" cy="256.763" r="10.438" transform="rotate(-180 384.1 256.763)" stroke="#334155" />
</g>
</svg>
);
}

View File

@ -0,0 +1,72 @@
import { InstallationIcon } from "@syntax/icons/InstallationIcon";
import { LightbulbIcon } from "@syntax/icons/LightbulbIcon";
import { PluginsIcon } from "@syntax/icons/PluginsIcon";
import { PresetsIcon } from "@syntax/icons/PresetsIcon";
import { ThemingIcon } from "@syntax/icons/ThemingIcon";
import { WarningIcon } from "@syntax/icons/WarningIcon";
import { useId } from "react";
import clsx from "clsx";
const icons = {
installation: InstallationIcon,
presets: PresetsIcon,
plugins: PluginsIcon,
theming: ThemingIcon,
lightbulb: LightbulbIcon,
warning: WarningIcon,
};
const iconStyles = {
blue: "[--icon-foreground:var(--color-slate-900)] [--icon-background:var(--color-white)]",
amber: "[--icon-foreground:var(--color-amber-900)] [--icon-background:var(--color-amber-100)]",
};
export function Icon({
icon,
color = "blue",
className,
...props
}: {
color?: keyof typeof iconStyles;
icon: keyof typeof icons;
} & Omit<React.ComponentPropsWithoutRef<"svg">, "color">) {
let id = useId();
let IconComponent = icons[icon];
return (
<svg aria-hidden="true" viewBox="0 0 32 32" fill="none" className={clsx(className, iconStyles[color])} {...props}>
<IconComponent id={id} color={color} />
</svg>
);
}
const gradients = {
blue: [{ stopColor: "#0EA5E9" }, { stopColor: "#22D3EE", offset: ".527" }, { stopColor: "#818CF8", offset: 1 }],
amber: [
{ stopColor: "#FDE68A", offset: ".08" },
{ stopColor: "#F59E0B", offset: ".837" },
],
};
export function Gradient({
color = "blue",
...props
}: {
color?: keyof typeof gradients;
} & Omit<React.ComponentPropsWithoutRef<"radialGradient">, "color">) {
return (
<radialGradient cx={0} cy={0} r={1} gradientUnits="userSpaceOnUse" {...props}>
{gradients[color].map((stop, stopIndex) => (
<stop key={stopIndex} {...stop} />
))}
</radialGradient>
);
}
export function LightMode({ className, ...props }: React.ComponentPropsWithoutRef<"g">) {
return <g className={clsx("dark:hidden", className)} {...props} />;
}
export function DarkMode({ className, ...props }: React.ComponentPropsWithoutRef<"g">) {
return <g className={clsx("hidden dark:inline", className)} {...props} />;
}

View File

@ -0,0 +1,54 @@
function LogomarkPaths() {
// return (
// <g fill="none" stroke="#38BDF8" strokeLinejoin="round" strokeWidth={3}>
// <path d="M10.308 5L18 17.5 10.308 30 2.615 17.5 10.308 5z" />
// <path d="M18 17.5L10.308 5h15.144l7.933 12.5M18 17.5h15.385L25.452 30H10.308L18 17.5z" />
// </g>
// );
return (
<>
<defs>
<linearGradient
id="l"
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(12.792,-21.32,-21.32,-12.792,5.208,23.32)"
>
<stop offset="0" style={{ stopColor: "rgb(43,127,255)" }} />
<stop offset="1" style={{ stopColor: "rgb(142,81,255)" }} />
</linearGradient>
</defs>
<g transform="matrix(-1.76727,0,0,1.76727,49.1089,-3.53454)">
<path
d="M16.161,18.989L20.49,23.32L21.9,21.91L2.1,2.1L0.69,3.51L2.714,5.535L-4.085,11.253L-4.085,13.054L3.185,19.167L4.629,17.337L-1.61,12.165L4.397,7.219L9.588,12.412L6,16L6.01,16.01L6,16.01L6,22L18,22L18,20.83L16.161,18.989ZM14.417,17.244L16,18.83L16,20L8,20L8,16.5L10.837,13.663L14.417,17.244ZM8,4L16,4L16,7.5L13.16,10.34L14.41,11.59L18,8.01L17.99,8L18,8L18,2L6,2L6,3.17L8,5.17L8,4ZM25.294,12.164L19.071,17.34L20.542,19.164L27.788,13.075L27.788,11.274L20.597,5.22L19.158,7.075L25.294,12.164Z"
style={{ fill: "url(#l)" }}
/>
</g>
</>
);
}
export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg viewBox="0 0 231 38" {...props}>
<LogomarkPaths />
<text
className="hidden lg:block"
fill="#1A202C"
fontFamily="Inter Variable, sans-serif"
fontSize={24}
fontWeight="bold"
letterSpacing="-.02em"
x={74}
y={26}
>
Memento Dev
</text>
</svg>
);
}

View File

@ -0,0 +1,82 @@
import { Suspense, useCallback, useEffect, useState } from "react";
import { usePageContext } from "vike-react/usePageContext";
import { Dialog, DialogPanel } from "@headlessui/react";
import { Navigation } from "@syntax/Navigation";
import { Link } from "@/components/common/Link";
import { Logo } from "@syntax/Logo";
function MenuIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" {...props}>
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
);
}
function CloseIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" {...props}>
<path d="M5 5l14 14M19 5l-14 14" />
</svg>
);
}
function CloseOnNavigation({ close }: { close: () => void }) {
const { urlPathname } = usePageContext();
useEffect(() => {
close();
}, [urlPathname, close]);
return null;
}
export function MobileNavigation() {
let [isOpen, setIsOpen] = useState(false);
let close = useCallback(() => setIsOpen(false), [setIsOpen]);
function onLinkClick(event: React.MouseEvent<HTMLAnchorElement>) {
let link = event.currentTarget;
if (
link.pathname + link.search + link.hash ===
window.location.pathname + window.location.search + window.location.hash
) {
close();
}
}
return (
<>
<button
type="button"
onClick={() => setIsOpen(true)}
className="relative"
aria-label="Ouvrir le menu de navigation"
>
<MenuIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Suspense fallback={null}>
<CloseOnNavigation close={close} />
</Suspense>
<Dialog
open={isOpen}
onClose={() => close()}
className="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 className="min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 sm:px-6 dark:bg-slate-900">
<div className="flex items-center">
<button type="button" onClick={() => close()} aria-label="Fermer le menu de navigation">
<CloseIcon className="h-6 w-6 stroke-slate-500" />
</button>
<Link href="/" className="ml-6" aria-label="Page d'accueil">
<Logo className="h-9 w-9" />
</Link>
</div>
<Navigation className="mt-5 px-1" onLinkClick={onLinkClick} />
</DialogPanel>
</Dialog>
</>
);
}

View File

@ -0,0 +1,47 @@
import { usePageContext } from "vike-react/usePageContext";
import { Link } from "@/components/common/Link";
import { navigation } from "@/lib/navigation";
import clsx from "clsx";
export function Navigation({
className,
onLinkClick,
}: {
className?: string;
onLinkClick?: React.MouseEventHandler<HTMLAnchorElement>;
}) {
const { urlPathname } = usePageContext();
return (
<nav className={clsx("text-base lg:text-sm", className)}>
<ul role="list" className="space-y-9">
{navigation.map((section) => (
<li key={section.title}>
<h2 className="font-display font-medium text-slate-900 dark:text-white">{section.title}</h2>
<ul
role="list"
className="mt-2 space-y-2 border-l-2 border-slate-100 lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800"
>
{section.links.map((link) => (
<li key={link.href} className="relative">
<Link
href={link.href}
onClick={onLinkClick}
className={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",
link.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",
)}
>
{link.title}
</Link>
</li>
))}
</ul>
</li>
))}
</ul>
</nav>
);
}

View File

@ -0,0 +1,64 @@
import { usePageContext } from "vike-react/usePageContext";
import { Link } from "@/components/common/Link";
import clsx from "clsx";
import { navigation } from "@/lib/navigation";
function ArrowIcon(props: React.ComponentPropsWithoutRef<"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>
);
}
function PageLink({
title,
href,
dir = "next",
...props
}: Omit<React.ComponentPropsWithoutRef<"div">, "dir" | "title"> & {
title: string;
href: string;
dir?: "previous" | "next";
}) {
return (
<div {...props}>
<dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
{dir === "next" ? "Suivant" : "Précédent"}
</dt>
<dd className="mt-1">
<Link
href={href}
className={clsx(
"flex items-center gap-x-1 text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300",
dir === "previous" && "flex-row-reverse",
)}
>
{title}
<ArrowIcon className={clsx("h-4 w-4 flex-none fill-current", dir === "previous" && "-scale-x-100")} />
</Link>
</dd>
</div>
);
}
export function PrevNextLinks() {
let { urlPathname } = usePageContext();
let allLinks = navigation.flatMap((section) => section.links);
let linkIndex = allLinks.findIndex((link) => link.href === urlPathname);
let previousPage = linkIndex > -1 ? allLinks[linkIndex - 1] : null;
let nextPage = linkIndex > -1 ? allLinks[linkIndex + 1] : null;
if (!nextPage && !previousPage) {
return null;
}
return (
<dl className="mt-12 flex border-t border-slate-200 pt-6 dark:border-slate-800">
{previousPage && <PageLink dir="previous" {...previousPage} />}
{nextPage && <PageLink className="ml-auto text-right" {...nextPage} />}
</dl>
);
}

View File

@ -0,0 +1,33 @@
import clsx from "clsx";
export function Prose<T extends React.ElementType = "div">({
as,
className,
...props
}: React.ComponentPropsWithoutRef<T> & {
as?: T;
}) {
let Component = as ?? "div";
return (
<Component
className={clsx(
className,
"prose max-w-none prose-slate dark:text-slate-400 dark:prose-invert",
// headings
"prose-headings:scroll-mt-28 prose-headings:font-display prose-headings:font-normal lg:prose-headings:scroll-mt-[8.5rem]",
// lead
"prose-lead:text-slate-500 dark:prose-lead:text-slate-400",
// links
"prose-a:font-semibold dark:prose-a:text-violet-400",
// link underline
"dark:[--tw-prose-background:var(--color-slate-900)] prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,var(--color-violet-300))] prose-a:hover:[--tw-prose-underline-size:6px] dark:prose-a:shadow-[inset_0_calc(-1*var(--tw-prose-underline-size,2px))_0_0_var(--tw-prose-underline,var(--color-violet-800))] dark:prose-a:hover:[--tw-prose-underline-size:6px]",
// pre
"prose-pre:rounded-xl prose-pre:bg-slate-900 prose-pre:shadow-lg dark:prose-pre:bg-slate-800/60 dark:prose-pre:ring-1 dark:prose-pre:shadow-none dark:prose-pre:ring-slate-300/10",
// hr
"dark:prose-hr:border-slate-800",
)}
{...props}
/>
);
}

View File

@ -0,0 +1,34 @@
import { Link } from "@/components/common/Link";
import { Icon } from "@syntax/Icon";
export function QuickLinks({ children }: { children: React.ReactNode }) {
return <div className="not-prose my-12 grid grid-cols-1 gap-6 sm:grid-cols-2">{children}</div>;
}
export function QuickLink({
title,
description,
href,
icon,
}: {
title: string;
description: string;
href: string;
icon: React.ComponentProps<typeof Icon>["icon"];
}) {
return (
<div className="group relative rounded-xl border border-slate-200 dark:border-slate-800">
<div className="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 className="relative overflow-hidden rounded-xl p-6">
<Icon icon={icon} className="h-8 w-8" />
<h2 className="mt-4 font-display text-base text-slate-900 dark:text-white">
<Link href={href}>
<span className="absolute -inset-px rounded-xl" />
{title}
</Link>
</h2>
<p className="mt-1 text-sm text-slate-700 dark:text-slate-400">{description}</p>
</div>
</div>
);
}

View File

@ -0,0 +1,8 @@
import type { SearchResult } from "@/lib/search";
import { buildSearchIndex, search } from "@/lib/search";
export const onSearch = async (query: string): Promise<SearchResult[]> => {
const searchIndex = buildSearchIndex("./app/docs");
return search(searchIndex, query);
};

View File

@ -0,0 +1,405 @@
import { forwardRef, Fragment, Suspense, useCallback, useEffect, useId, useRef, useState } from "react";
import Highlighter from "react-highlight-words";
import { usePageContext } from "vike-react/usePageContext";
import { navigate as routerNavigate } from "vike/client/router";
// import { usePathname, useRouter, useSearchParams } from "next/navigation";
import {
type AutocompleteApi,
type AutocompleteCollection,
type AutocompleteState,
createAutocomplete,
} from "@algolia/autocomplete-core";
import { Dialog, DialogPanel } from "@headlessui/react";
import clsx from "clsx";
import { navigation } from "@/lib/navigation";
import { onSearch } from "./Search.telefunc";
import type { SearchResult } from "@/lib/search";
type EmptyObject = Record<string, never>;
type Autocomplete = AutocompleteApi<SearchResult, React.SyntheticEvent, React.MouseEvent, React.KeyboardEvent>;
function SearchIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 20 20" {...props}>
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
</svg>
);
}
function useAutocomplete({ close }: { close: (autocomplete: Autocomplete) => void }) {
let id = useId();
// let router = useRouter();
let [autocompleteState, setAutocompleteState] = useState<AutocompleteState<SearchResult> | EmptyObject>({});
function navigate({ itemUrl }: { itemUrl?: string }) {
if (!itemUrl) {
return;
}
// router.push(itemUrl);
if (itemUrl === window.location.pathname + window.location.search + window.location.hash) {
close(autocomplete);
}
}
let [autocomplete] = useState<Autocomplete>(() =>
createAutocomplete<SearchResult, React.SyntheticEvent, React.MouseEvent, React.KeyboardEvent>({
id,
placeholder: "Find something...",
defaultActiveItemId: 0,
onStateChange({ state }) {
setAutocompleteState(state);
},
shouldPanelOpen({ state }) {
return state.query !== "";
},
navigator: {
navigate,
},
async getSources({ query }) {
return onSearch(query).then((searchResult) => {
return [
{
sourceId: "documentation",
getItems() {
console.log({ searchResult });
return [];
return searchResult;
},
getItemUrl({ item }) {
return item.url;
},
onSelect: navigate,
},
];
});
},
}),
);
return { autocomplete, autocompleteState };
}
function LoadingIcon(props: React.ComponentPropsWithoutRef<"svg">) {
let id = useId();
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="5.5" strokeLinejoin="round" />
<path stroke={`url(#${id})`} strokeLinecap="round" strokeLinejoin="round" d="M15.5 10a5.5 5.5 0 1 0-5.5 5.5" />
<defs>
<linearGradient id={id} x1="13" x2="9.5" y1="9" y2="15" gradientUnits="userSpaceOnUse">
<stop stopColor="currentColor" />
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
);
}
function HighlightQuery({ text, query }: { text: string; query: string }) {
return (
<Highlighter
highlightClassName="group-aria-selected:underline bg-transparent text-violet-600 dark:text-violet-400"
searchWords={[query]}
autoEscape={true}
textToHighlight={text}
/>
);
}
function SearchResult({
result,
autocomplete,
collection,
query,
}: {
result: Result;
autocomplete: Autocomplete;
collection: AutocompleteCollection<Result>;
query: string;
}) {
let id = useId();
let sectionTitle = navigation.find((section) =>
section.links.find((link) => link.href === result.url.split("#")[0]),
)?.title;
let hierarchy = [sectionTitle, result.pageTitle].filter((x): x is string => typeof x === "string");
return (
<li
className="group block cursor-default rounded-lg px-3 py-2 aria-selected:bg-slate-100 dark:aria-selected:bg-slate-700/30"
aria-labelledby={`${id}-hierarchy ${id}-title`}
{...autocomplete.getItemProps({
item: result,
source: collection.source,
})}
>
<div
id={`${id}-title`}
aria-hidden="true"
className="text-sm text-slate-700 group-aria-selected:text-violet-600 dark:text-slate-300 dark:group-aria-selected:text-violet-400"
>
<HighlightQuery text={result.title} query={query} />
</div>
{hierarchy.length > 0 && (
<div
id={`${id}-hierarchy`}
aria-hidden="true"
className="mt-0.5 truncate text-xs whitespace-nowrap text-slate-500 dark:text-slate-400"
>
{hierarchy.map((item, itemIndex, items) => (
<Fragment key={itemIndex}>
<HighlightQuery text={item} query={query} />
<span className={itemIndex === items.length - 1 ? "sr-only" : "mx-2 text-slate-300 dark:text-slate-700"}>
/
</span>
</Fragment>
))}
</div>
)}
</li>
);
}
function SearchResults({
autocomplete,
query,
collection,
}: {
autocomplete: Autocomplete;
query: string;
collection: AutocompleteCollection<Result>;
}) {
if (collection.items.length === 0) {
return (
<p className="px-4 py-8 text-center text-sm text-slate-700 dark:text-slate-400">
No results for &ldquo;
<span className="break-words text-slate-900 dark:text-white">{query}</span>
&rdquo;
</p>
);
}
return (
<ul {...autocomplete.getListProps()}>
{collection.items.map((result) => (
<SearchResult
key={result.url}
result={result}
autocomplete={autocomplete}
collection={collection}
query={query}
/>
))}
</ul>
);
}
const SearchInput = forwardRef<
React.ComponentRef<"input">,
{
autocomplete: Autocomplete;
autocompleteState: AutocompleteState<Result> | EmptyObject;
onClose: () => void;
}
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
let inputProps = autocomplete.getInputProps({ inputElement: null });
return (
<div className="group relative flex h-12">
<SearchIcon className="pointer-events-none absolute top-0 left-4 h-full w-5 fill-slate-400 dark:fill-slate-500" />
<input
ref={inputRef}
data-autofocus
className={clsx(
"flex-auto appearance-none bg-transparent pl-12 text-slate-900 outline-hidden placeholder:text-slate-400 focus:w-full focus:flex-none sm:text-sm dark:text-white [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden",
autocompleteState.status === "stalled" ? "pr-11" : "pr-4",
)}
{...inputProps}
onKeyDown={(event) => {
if (event.key === "Escape" && !autocompleteState.isOpen && autocompleteState.query === "") {
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
onClose();
} else {
inputProps.onKeyDown(event);
}
}}
/>
{autocompleteState.status === "stalled" && (
<div className="absolute inset-y-0 right-3 flex items-center">
<LoadingIcon className="h-6 w-6 animate-spin stroke-slate-200 text-slate-400 dark:stroke-slate-700 dark:text-slate-500" />
</div>
)}
</div>
);
});
function CloseOnNavigation({
close,
autocomplete,
}: {
close: (autocomplete: Autocomplete) => void;
autocomplete: Autocomplete;
}) {
const { urlParsed } = usePageContext();
const { pathname, search } = urlParsed;
useEffect(() => {
close(autocomplete);
}, [pathname, search, close, autocomplete]);
return null;
}
function SearchDialog({
open,
setOpen,
className,
}: {
open: boolean;
setOpen: (open: boolean) => void;
className?: string;
}) {
let formRef = useRef<React.ElementRef<"form">>(null);
let panelRef = useRef<React.ElementRef<"div">>(null);
let inputRef = useRef<React.ElementRef<typeof SearchInput>>(null);
let close = useCallback(
(autocomplete: Autocomplete) => {
setOpen(false);
autocomplete.setQuery("");
},
[setOpen],
);
let { autocomplete, autocompleteState } = useAutocomplete({
close() {
close(autocomplete);
},
});
useEffect(() => {
if (open) {
return;
}
function onKeyDown(event: KeyboardEvent) {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setOpen(true);
}
}
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [open, setOpen]);
return (
<>
<Suspense fallback={null}>
<CloseOnNavigation close={close} autocomplete={autocomplete} />
</Suspense>
<Dialog open={open} onClose={() => close(autocomplete)} className={clsx("fixed inset-0 z-50", className)}>
<div className="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" />
<div className="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh]">
<DialogPanel className="mx-auto transform-gpu overflow-hidden rounded-xl bg-white shadow-xl sm:max-w-xl dark:bg-slate-800 dark:ring-1 dark:ring-slate-700">
<div {...autocomplete.getRootProps({})}>
<form
ref={formRef}
{...autocomplete.getFormProps({
inputElement: inputRef.current,
})}
>
<SearchInput
ref={inputRef}
autocomplete={autocomplete}
autocompleteState={autocompleteState}
onClose={() => setOpen(false)}
/>
<div
ref={panelRef}
className="border-t border-slate-200 bg-white px-2 py-3 empty:hidden dark:border-slate-400/10 dark:bg-slate-800"
{...autocomplete.getPanelProps({})}
>
{autocompleteState.isOpen && (
<SearchResults
autocomplete={autocomplete}
query={autocompleteState.query}
collection={autocompleteState.collections[0]}
/>
)}
</div>
</form>
</div>
</DialogPanel>
</div>
</Dialog>
</>
);
}
function useSearchProps() {
let buttonRef = useRef<React.ElementRef<"button">>(null);
let [open, setOpen] = useState(false);
return {
buttonProps: {
ref: buttonRef,
onClick() {
setOpen(true);
},
},
dialogProps: {
open,
setOpen: useCallback((open: boolean) => {
let { width = 0, height = 0 } = buttonRef.current?.getBoundingClientRect() ?? {};
if (!open || (width !== 0 && height !== 0)) {
setOpen(open);
}
}, []),
},
};
}
export function Search() {
let [modifierKey, setModifierKey] = useState<string>();
let { buttonProps, dialogProps } = useSearchProps();
useEffect(() => {
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? "⌘" : "Ctrl ");
}, []);
return (
<>
<button
type="button"
className="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-80 md:flex-none md:rounded-lg md:py-2.5 md:pr-3.5 md:pl-4 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 lg:w-96 dark:md:bg-slate-800/75 dark:md:ring-white/5 dark:md:ring-inset dark:md:hover:bg-slate-700/40 dark:md:hover:ring-slate-500"
{...buttonProps}
>
<SearchIcon className="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 md:group-hover:fill-slate-400 dark:fill-slate-500" />
<span className="sr-only md:not-sr-only md:ml-2 md:text-slate-500 md:dark:text-slate-400">Search docs</span>
{modifierKey && (
<kbd className="ml-auto hidden font-medium text-slate-400 md:block dark:text-slate-500">
<kbd className="font-sans">{modifierKey}</kbd>
<kbd className="font-sans">K</kbd>
</kbd>
)}
</button>
<SearchDialog {...dialogProps} />
</>
);
}

View File

@ -0,0 +1,113 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { Link } from "@/components/common/Link";
import clsx from "clsx";
import { type Section, type Subsection } from "@/lib/sections";
export function TableOfContents({ tableOfContents }: { tableOfContents: Array<Section> }) {
let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id);
let getHeadings = useCallback((tableOfContents: Array<Section>) => {
return tableOfContents
.flatMap((node) => [node.id, ...node.children.map((child) => child.id)])
.map((id) => {
let el = document.getElementById(id);
if (!el) return null;
let style = window.getComputedStyle(el);
let scrollMt = parseFloat(style.scrollMarginTop);
let top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
return { id, top };
})
.filter((x): x is { id: string; top: number } => x !== null);
}, []);
useEffect(() => {
if (tableOfContents.length === 0) return;
const headings = getHeadings(tableOfContents);
function onScroll() {
const top = window.scrollY;
let current = headings[0]?.id;
for (const heading of headings) {
if (top >= heading.top - 10) {
current = heading.id;
} else {
break;
}
}
setCurrentSection(current);
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => {
window.removeEventListener("scroll", onScroll);
};
}, [getHeadings, tableOfContents]);
function isActive(section: Section | Subsection) {
if (section.id === currentSection) {
return true;
}
if (!section.children) {
return false;
}
return section.children.findIndex(isActive) > -1;
}
return (
<div className="hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
<nav aria-labelledby="on-this-page-title" className="w-56">
{tableOfContents.length > 0 && (
<>
<h2 id="on-this-page-title" className="font-display text-sm font-medium text-slate-900 dark:text-white">
Table des matières
</h2>
<ol role="list" className="mt-4 space-y-3 text-sm">
{tableOfContents.map((section) => (
<li key={section.id}>
<h3>
<Link
href={`#${section.id}`}
className={clsx(
isActive(section)
? "text-violet-500"
: "font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300",
)}
>
{section.title}
</Link>
</h3>
{section.children.length > 0 && (
<ol role="list" className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400">
{section.children.map((subSection) => (
<li key={subSection.id}>
<Link
href={`#${subSection.id}`}
className={
isActive(subSection)
? "text-violet-500"
: "hover:text-slate-600 dark:hover:text-slate-300"
}
>
{subSection.title}
</Link>
</li>
))}
</ol>
)}
</li>
))}
</ol>
</>
)}
</nav>
</div>
);
}

View File

@ -0,0 +1,107 @@
import { useEffect, useState } from "react";
// import { useTheme } from 'next-themes'
import { Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react";
import clsx from "clsx";
const themes = [
{ name: "Light", value: "light", icon: LightIcon },
{ name: "Dark", value: "dark", icon: DarkIcon },
{ name: "System", value: "system", icon: SystemIcon },
];
function LightIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
/>
</svg>
);
}
function DarkIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
/>
</svg>
);
}
function SystemIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
/>
</svg>
);
}
export function ThemeSelector(props: React.ComponentPropsWithoutRef<typeof Listbox<"div">>) {
const useTheme = () => {
return {
theme: "light",
setTheme: (theme: string) => {},
};
};
let { theme, setTheme } = useTheme();
let [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="h-6 w-6" />;
}
return (
<Listbox as="div" value={theme} onChange={setTheme} {...props}>
<Label className="sr-only">Theme</Label>
<ListboxButton
className="flex h-6 w-6 items-center justify-center rounded-lg ring-1 shadow-md shadow-black/5 ring-black/5 dark:bg-slate-700 dark:ring-white/5 dark:ring-inset"
aria-label="Theme"
>
<LightIcon className={clsx("h-4 w-4 dark:hidden", theme === "system" ? "fill-slate-400" : "fill-violet-400")} />
<DarkIcon
className={clsx("hidden h-4 w-4 dark:block", theme === "system" ? "fill-slate-400" : "fill-violet-400")}
/>
</ListboxButton>
<ListboxOptions className="absolute top-full left-1/2 mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium ring-1 shadow-md shadow-black/5 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
{themes.map((theme) => (
<ListboxOption
key={theme.value}
value={theme.value}
className={({ focus, selected }) =>
clsx("flex cursor-pointer items-center rounded-[0.625rem] p-1 select-none", {
"text-violet-500": selected,
"text-slate-900 dark:text-white": focus && !selected,
"text-slate-700 dark:text-slate-400": !focus && !selected,
"bg-slate-100 dark:bg-slate-900/40": focus,
})
}
>
{({ selected }) => (
<>
<div className="rounded-md bg-white p-1 ring-1 shadow-sm ring-slate-900/5 dark:bg-slate-700 dark:ring-white/5 dark:ring-inset">
<theme.icon
className={clsx("h-4 w-4", selected ? "fill-violet-400 dark:fill-violet-400" : "fill-slate-400")}
/>
</div>
<div className="ml-3">{theme.name}</div>
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
);
}

View File

@ -0,0 +1,39 @@
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
export function InstallationIcon({
id,
color,
}: {
id: string;
color?: React.ComponentProps<typeof Gradient>["color"];
}) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 12 3)" />
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 21 -21 0 16 7)" />
</defs>
<LightMode>
<circle cx={12} cy={12} r={12} fill={`url(#${id}-gradient)`} />
<path
d="m8 8 9 21 2-10 10-2L8 8Z"
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</LightMode>
<DarkMode>
<path
d="m4 4 10.286 24 2.285-11.429L28 14.286 4 4Z"
fill={`url(#${id}-gradient-dark)`}
stroke={`url(#${id}-gradient-dark)`}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</DarkMode>
</>
);
}

View File

@ -0,0 +1,38 @@
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
export function LightbulbIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 11)" />
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 24.5001 -19.2498 0 16 5.5)" />
</defs>
<LightMode>
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20 24.995c0-1.855 1.094-3.501 2.427-4.792C24.61 18.087 26 15.07 26 12.231 26 7.133 21.523 3 16 3S6 7.133 6 12.23c0 2.84 1.389 5.857 3.573 7.973C10.906 21.494 12 23.14 12 24.995V27a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-2.005Z"
className="fill-[var(--icon-background)]"
fillOpacity={0.5}
/>
<path
d="M25 12.23c0 2.536-1.254 5.303-3.269 7.255l1.391 1.436c2.354-2.28 3.878-5.547 3.878-8.69h-2ZM16 4c5.047 0 9 3.759 9 8.23h2C27 6.508 21.998 2 16 2v2Zm-9 8.23C7 7.76 10.953 4 16 4V2C10.002 2 5 6.507 5 12.23h2Zm3.269 7.255C8.254 17.533 7 14.766 7 12.23H5c0 3.143 1.523 6.41 3.877 8.69l1.392-1.436ZM13 27v-2.005h-2V27h2Zm1 1a1 1 0 0 1-1-1h-2a3 3 0 0 0 3 3v-2Zm4 0h-4v2h4v-2Zm1-1a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2Zm0-2.005V27h2v-2.005h-2ZM8.877 20.921C10.132 22.136 11 23.538 11 24.995h2c0-2.253-1.32-4.143-2.731-5.51L8.877 20.92Zm12.854-1.436C20.32 20.852 19 22.742 19 24.995h2c0-1.457.869-2.859 2.122-4.074l-1.391-1.436Z"
className="fill-[var(--icon-foreground)]"
/>
<path
d="M20 26a1 1 0 1 0 0-2v2Zm-8-2a1 1 0 1 0 0 2v-2Zm2 0h-2v2h2v-2Zm1 1V13.5h-2V25h2Zm-5-11.5v1h2v-1h-2Zm3.5 4.5h5v-2h-5v2Zm8.5-3.5v-1h-2v1h2ZM20 24h-2v2h2v-2Zm-2 0h-4v2h4v-2Zm-1-10.5V25h2V13.5h-2Zm2.5-2.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2ZM18.5 18a3.5 3.5 0 0 0 3.5-3.5h-2a1.5 1.5 0 0 1-1.5 1.5v2ZM10 14.5a3.5 3.5 0 0 0 3.5 3.5v-2a1.5 1.5 0 0 1-1.5-1.5h-2Zm2.5-3.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2Z"
className="fill-[var(--icon-foreground)]"
/>
</LightMode>
<DarkMode>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 2C10.002 2 5 6.507 5 12.23c0 3.144 1.523 6.411 3.877 8.691.75.727 1.363 1.52 1.734 2.353.185.415.574.726 1.028.726H12a1 1 0 0 0 1-1v-4.5a.5.5 0 0 0-.5-.5A3.5 3.5 0 0 1 9 14.5V14a3 3 0 1 1 6 0v9a1 1 0 1 0 2 0v-9a3 3 0 1 1 6 0v.5a3.5 3.5 0 0 1-3.5 3.5.5.5 0 0 0-.5.5V23a1 1 0 0 0 1 1h.36c.455 0 .844-.311 1.03-.726.37-.833.982-1.626 1.732-2.353 2.354-2.28 3.878-5.547 3.878-8.69C27 6.507 21.998 2 16 2Zm5 25a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1 3 3 0 0 0 3 3h4a3 3 0 0 0 3-3Zm-8-13v1.5a.5.5 0 0 1-.5.5 1.5 1.5 0 0 1-1.5-1.5V14a1 1 0 1 1 2 0Zm6.5 2a.5.5 0 0 1-.5-.5V14a1 1 0 1 1 2 0v.5a1.5 1.5 0 0 1-1.5 1.5Z"
fill={`url(#${id}-gradient-dark)`}
/>
</DarkMode>
</>
);
}

View File

@ -0,0 +1,47 @@
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
export function PluginsIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 11)" />
<Gradient id={`${id}-gradient-dark-1`} color={color} gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)" />
<Gradient id={`${id}-gradient-dark-2`} color={color} gradientTransform="matrix(0 14 -14 0 16 10)" />
</defs>
<LightMode>
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<g
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 9v14l12 6V15L3 9Z" />
<path d="M27 9v14l-12 6V15l12-6Z" />
</g>
<path d="M11 4h8v2l6 3-10 6L5 9l6-3V4Z" fillOpacity={0.5} className="fill-[var(--icon-background)]" />
<g
className="stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M20 5.5 27 9l-12 6L3 9l7-3.5" />
<path d="M20 5c0 1.105-2.239 2-5 2s-5-.895-5-2m10 0c0-1.105-2.239-2-5-2s-5 .895-5 2m10 0v3c0 1.105-2.239 2-5 2s-5-.895-5-2V5" />
</g>
</LightMode>
<DarkMode strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
<path
d="M17.676 3.38a3.887 3.887 0 0 0-3.352 0l-9 4.288C3.907 8.342 3 9.806 3 11.416v9.168c0 1.61.907 3.073 2.324 3.748l9 4.288a3.887 3.887 0 0 0 3.352 0l9-4.288C28.093 23.657 29 22.194 29 20.584v-9.168c0-1.61-.907-3.074-2.324-3.748l-9-4.288Z"
stroke={`url(#${id}-gradient-dark-1)`}
/>
<path
d="M16.406 8.087a.989.989 0 0 0-.812 0l-7 3.598A1.012 1.012 0 0 0 8 12.61v6.78c0 .4.233.762.594.925l7 3.598a.989.989 0 0 0 .812 0l7-3.598c.361-.163.594-.525.594-.925v-6.78c0-.4-.233-.762-.594-.925l-7-3.598Z"
fill={`url(#${id}-gradient-dark-2)`}
stroke={`url(#${id}-gradient-dark-2)`}
/>
</DarkMode>
</>
);
}

View File

@ -0,0 +1,35 @@
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
export function PresetsIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 20 3)" />
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)" />
</defs>
<LightMode>
<circle cx={20} cy={12} r={12} fill={`url(#${id}-gradient)`} />
<g
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
fillOpacity={0.5}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 5v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
<path d="M18 17v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V17a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
<path d="M18 5v4a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
<path d="M3 25v2a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
</g>
</LightMode>
<DarkMode fill={`url(#${id}-gradient-dark)`}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3 17V4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Zm16 10v-9a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2Zm0-23v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1ZM3 28v-3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z"
/>
<path d="M2 4v13h2V4H2Zm2-2a2 2 0 0 0-2 2h2V2Zm8 0H4v2h8V2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 13V4h-2v13h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-8 0h8v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Zm16 1v9h2v-9h-2Zm3-3a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1v-2Zm6 0h-6v2h6v-2Zm3 3a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2Zm0 9v-9h-2v9h2Zm-3 3a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2Zm-6 0h6v-2h-6v2Zm-3-3a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1h-2Zm2-18V4h-2v5h2Zm0 0h-2a2 2 0 0 0 2 2V9Zm8 0h-8v2h8V9Zm0 0v2a2 2 0 0 0 2-2h-2Zm0-5v5h2V4h-2Zm0 0h2a2 2 0 0 0-2-2v2Zm-8 0h8V2h-8v2Zm0 0V2a2 2 0 0 0-2 2h2ZM2 25v3h2v-3H2Zm2-2a2 2 0 0 0-2 2h2v-2Zm9 0H4v2h9v-2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 3v-3h-2v3h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-9 0h9v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Z" />
</DarkMode>
</>
);
}

View File

@ -0,0 +1,51 @@
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
export function ThemingIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="matrix(0 21 -21 0 12 11)" />
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)" />
</defs>
<LightMode>
<circle cx={12} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<path
d="M27 12.13 19.87 5 13 11.87v14.26l14-14Z"
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
fillOpacity={0.5}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 3h10v22a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V3Z"
className="fill-[var(--icon-background)]"
fillOpacity={0.5}
/>
<path
d="M3 9v16a4 4 0 0 0 4 4h2a4 4 0 0 0 4-4V9M3 9V3h10v6M3 9h10M3 15h10M3 21h10"
className="stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M29 29V19h-8.5L13 26c0 1.5-2.5 3-5 3h21Z"
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</LightMode>
<DarkMode>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3 2a1 1 0 0 0-1 1v21a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H3Zm16.752 3.293a1 1 0 0 0-1.593.244l-1.045 2A1 1 0 0 0 17 8v13a1 1 0 0 0 1.71.705l7.999-8.045a1 1 0 0 0-.002-1.412l-6.955-6.955ZM26 18a1 1 0 0 0-.707.293l-10 10A1 1 0 0 0 16 30h13a1 1 0 0 0 1-1V19a1 1 0 0 0-1-1h-3ZM5 18a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H5Zm-1-5a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Zm1-7a1 1 0 0 0 0 2h6a1 1 0 1 0 0-2H5Z"
fill={`url(#${id}-gradient-dark)`}
/>
</DarkMode>
</>
);
}

View File

@ -0,0 +1,47 @@
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
export function WarningIcon({ id, color }: { id: string; color?: React.ComponentProps<typeof Gradient>["color"] }) {
return (
<>
<defs>
<Gradient id={`${id}-gradient`} color={color} gradientTransform="rotate(65.924 1.519 20.92) scale(25.7391)" />
<Gradient id={`${id}-gradient-dark`} color={color} gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)" />
</defs>
<LightMode>
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
<path
d="M3 16c0 7.18 5.82 13 13 13s13-5.82 13-13S23.18 3 16 3 3 8.82 3 16Z"
fillOpacity={0.5}
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="m15.408 16.509-1.04-5.543a1.66 1.66 0 1 1 3.263 0l-1.039 5.543a.602.602 0 0 1-1.184 0Z"
className="fill-[var(--icon-foreground)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
fillOpacity={0.5}
stroke="currentColor"
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</LightMode>
<DarkMode>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 16C2 8.268 8.268 2 16 2s14 6.268 14 14-6.268 14-14 14S2 23.732 2 16Zm11.386-4.85a2.66 2.66 0 1 1 5.228 0l-1.039 5.543a1.602 1.602 0 0 1-3.15 0l-1.04-5.543ZM16 20a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"
fill={`url(#${id}-gradient-dark)`}
/>
</DarkMode>
</>
);
}

View File

@ -0,0 +1,17 @@
---
title: Installation
description: Quidem magni aut exercitationem maxime rerum eos.
tags: [installation, getting-started]
---
Quasi sapiente voluptates aut minima non doloribus similique quisquam. In quo expedita ipsum nostrum corrupti incidunt. Et aut eligendi ea perferendis.
---
## Quis vel iste dicta
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur.
### Et pariatur ab quas
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur quaerat exercitationem. Consequatur et cum atque mollitia qui quia necessitatibus.

View File

@ -0,0 +1,74 @@
---
title: Introdution to string theory
nextjs:
metadata:
title: Introdution to string theory
description: Quidem magni aut exercitationem maxime rerum eos.
---
Quasi sapiente voluptates aut minima non doloribus similique quisquam. In quo expedita ipsum nostrum corrupti incidunt. Et aut eligendi ea perferendis.
---
## Quis vel iste dicta
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur.
### Et pariatur ab quas
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur quaerat exercitationem. Consequatur et cum atque mollitia qui quia necessitatibus.
```js
/** @type {import('@tailwindlabs/lorem').ipsum} */
export default {
lorem: 'ipsum',
dolor: ['sit', 'amet', 'consectetur'],
adipiscing: {
elit: true,
},
}
```
Possimus saepe veritatis sint nobis et quam eos. Architecto consequatur odit perferendis fuga eveniet possimus rerum cumque. Ea deleniti voluptatum deserunt voluptatibus ut non iste. Provident nam asperiores vel laboriosam omnis ducimus enim nesciunt quaerat. Minus tempora cupiditate est quod.
### Natus aspernatur iste
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur quaerat exercitationem. Consequatur et cum atque mollitia qui quia necessitatibus.
Voluptas beatae omnis omnis voluptas. Cum architecto ab sit ad eaque quas quia distinctio. Molestiae aperiam qui quis deleniti soluta quia qui. Dolores nostrum blanditiis libero optio id. Mollitia ad et asperiores quas saepe alias.
---
## Quos porro ut molestiae
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur.
### Voluptatem quas possimus
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur quaerat exercitationem. Consequatur et cum atque mollitia qui quia necessitatibus.
Possimus saepe veritatis sint nobis et quam eos. Architecto consequatur odit perferendis fuga eveniet possimus rerum cumque. Ea deleniti voluptatum deserunt voluptatibus ut non iste. Provident nam asperiores vel laboriosam omnis ducimus enim nesciunt quaerat. Minus tempora cupiditate est quod.
### Id vitae minima
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur quaerat exercitationem. Consequatur et cum atque mollitia qui quia necessitatibus.
Voluptas beatae omnis omnis voluptas. Cum architecto ab sit ad eaque quas quia distinctio. Molestiae aperiam qui quis deleniti soluta quia qui. Dolores nostrum blanditiis libero optio id. Mollitia ad et asperiores quas saepe alias.
---
## Vitae laborum maiores
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur.
### Corporis exercitationem
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur quaerat exercitationem. Consequatur et cum atque mollitia qui quia necessitatibus.
Possimus saepe veritatis sint nobis et quam eos. Architecto consequatur odit perferendis fuga eveniet possimus rerum cumque. Ea deleniti voluptatum deserunt voluptatibus ut non iste. Provident nam asperiores vel laboriosam omnis ducimus enim nesciunt quaerat. Minus tempora cupiditate est quod.
### Reprehenderit magni
Sit commodi iste iure molestias qui amet voluptatem sed quaerat. Nostrum aut pariatur. Sint ipsa praesentium dolor error cumque velit tenetur quaerat exercitationem. Consequatur et cum atque mollitia qui quia necessitatibus.
Voluptas beatae omnis omnis voluptas. Cum architecto ab sit ad eaque quas quia distinctio. Molestiae aperiam qui quis deleniti soluta quia qui. Dolores nostrum blanditiis libero optio id. Mollitia ad et asperiores quas saepe alias.

239
app/data/docs/react/page.md Normal file
View File

@ -0,0 +1,239 @@
---
title: "Introduction à React"
description: "Parlons un peu de React, ce qu'il est, ce qu'il fait et pourquoi il est si populaire."
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
---
Parlons peu, parlons bien ! 😄
React est une **bibliothèque** _(non, pas un **framework** !)_ JavaScript open-source développée par Facebook.
Elle est utilisée pour construire des interfaces utilisateur _(UI)_ interactives et dynamiques.
{% callout type="note" title="Pourquoi React est si populaire ?" %}
- **Facilité d'utilisation** : React est facile à apprendre et à utiliser. Il est basé sur JavaScript, qui est l'un des langages de programmation les plus populaires.
- **Réutilisabilité des composants** : React permet de créer des composants réutilisables. Cela signifie que tu peux créer un composant une fois et l'utiliser partout où tu en as besoin.
- **Performances** : React utilise un DOM virtuel _(Virtual DOM)_ pour améliorer les performances de l'application.
- **Communauté active** : React a une communauté active de développeurs qui contribuent à son développement et partagent des ressources utiles.
{% /callout %}
Mais on peut aussi y noter des points faibles bien entendu, car tout n'est pas rose :
- **Courbe d'apprentissage** : Bien que React soit "facile" à apprendre, les concepts avancés demandent un peu de temps pour être maîtrisés.
- **Taille du bundle** : React est relativement lourd en termes de taille de bundle, ce qui peut affecter les performances de l'application en terme de chargement initial.
- **GAFAM** : Comme d'autres bibliothèques/frameworks, React n'échappe pas à la critique de la part de certains développeurs qui ne souhaitent pas utiliser des technologies développées par des géants du web.
## 🤔 Pourquoi une bibliothèque et pas un framework ?
Très grand débat que voilà ! Vraiment.. il y a des guerres qui se sont déclarées pour moins que ça 😅
Blague à part, pour pouvoir dire que React n'est pas un framework, il faut comprendre la différence entre les deux :
- **Framework** : Un framework est un ensemble de bibliothèques et de composants qui sont prédéfinis et structurés pour te permettre de construire une application.
En gros, le framework te dit comment faire les choses.
- **Bibliothèque** : Une bibliothèque est un ensemble de fonctions et de composants que tu peux utiliser pour construire une application.
En gros, c'est toi qui décides comment faire les choses.
Et si tu connais déjà React, je te vois venir avec tes grands sabots... !
{% callout type="note" title="React a ses propres règles, on ne peut pas faire n'importe quoi !" %}
C'est vrai ! React a ses propres règles et conventions, mais il te laisse quand même une grande liberté pour organiser ton code comme tu le souhaites.
Si on se concentre sur la **préoccupation principale** de React, c'est de gérer l'**interface utilisateur** _(UI)_ de ton application.
En aucun cas, React _(tel quel et "pour le moment")_ va te dire comment gérer ton état global, comment gérer tes requêtes HTTP, etc.
Mais tu peux totalement utiliser React **au sein** d'un framework !
Tu as notamment des frameworks comme [**Next.js**](https://nextjs.org/) ou [**Gatsby**](https://www.gatsbyjs.com/) qui utilisent React
avec des fonctionnalités supplémentaires pour gérer le routage, le rendu côté serveur, etc.
_(Le meilleur, selon moi, c'est [**Vike**](https://vike.dev/) qui te permet d'utiliser presque n'importe quelle bibliothèque avec une même architecture 😏)_
Mais concentrons-nous sur React en tant que bibliothèque, et non en tant que framework 😉
{% /callout %}
## 📝 JSX
Ce qui peut être déroutant au premier abord avec React, c'est le **JSX**.
On serait tenté de dire que c'est du HTML, mais en fait : **pas du tout** !
Le JSX est un sucre syntaxique _(syntactic sugar)_ qui permet d'écrire du code JavaScript en se basant sur le système de balisage HTML.
L'avantage de JSX c'est que le code devient beaucoup plus lisible et plus proche de ce que tu connais déjà avec HTML.
Mais il s'agit bien de JavaScript, et non de HTML !
## 🧩 Composants
React est basé sur le concept de **composants**. Un composant est une partie réutilisable de l'interface utilisateur _(UI)_ qui peut être affichée à l'écran.
Dans la majorité des cas, on va chercher à **mutualiser** les composants pour éviter de répéter du code inutilement.
L'exemple le plus flagrant sera par exemple tous les boutons de ton application, qui auront probablement tous la même apparence et le même comportement.
Il est aussi possible de les **imbriquer** les uns dans les autres !
En fait, on joue avec des Lego, mais en version code 👷
Mais si on veut vraiment montrer le potentiel de React, parlons maintenant... 🥁
Des **states**, **cycles de vie** et des **props** !
## 🛠️ States, Cycles de vie et Props
T'assomer aussi vite avec ces termes qui ne te parlent peut-être pas, c'est pas cool de ma part...
Pardon pour les gros mots, je me calme tout de suite ! 🙈
Si ça te rassure, je vais très rapidement évoquer ce qu'il se cache derrière ces termes barbares,
je réserve les détails pour des articles dédiés 😉
### 🗄️ States
... ou également appelés **états** en français.
Le but du state, c'est de stocker des données qui vont être **observées** par React.
À chaque fois que le state va être modifié, React va **réagir** et mettre à jour l'interface utilisateur _(UI)_ en conséquence afin d'afficher les nouvelles données.
### 🔄 Cycles de vie
Les **cycles de vie** _(lifecycle)_ sont des méthodes qui sont appelées à des moments précis dans le cycle de vie d'un composant React.
Si tu as lu la section qui parle brièvement des states, tu auras peut-être remarqué cette phrase :
> À chaque fois que le state va être modifié, React va **réagir** et mettre à jour l'interface utilisateur [...]
Et bien c'est là que les cycles de vie entrent en jeu !
Un composant sur React va avoir un cycle de vie, caractérisé par trois phases :
1. **Montage du composant** _(Mounting)_ : le composant est créé et inséré dans le DOM.
2. **Mise à jour du composant** _(Updating)_ : le composant est mis à jour en fonction des changements de state ou de props.
3. **Démontage du composant** _(Unmounting)_ : le composant est retiré du DOM.
Ces différentes phases vont nous permettre d'interagir avec le composant à des moments précis, et d'effectuer des actions en conséquence.
### 📦 Props
Et pour finir, les **props** _(properties ou tout simplement "propriétés" en français)_ !
Il s'agit ni plus ni moins que des **arguments** que tu vas passer à un composant, comme tu le ferais avec une fonction.
Cependant il faut noter une chose :
On transmet les props à un composant précis, qui sera donc un composant **enfant**.
Un composant enfant ne pourra pas transmettre des props à un composant parent, c'est unidirectionnel _(mais on verra comment on peut faire autrement 😉)_.
## 🖥️ Une petite démo ?
OK, mais vraiment petite !
Prenons l'exemple d'une application qui servira **uniquement** à afficher une liste de tâches _(une todolist donc !)_.
_(Bon... utiliser React uniquement pour ça c'est abusé, mais c'est pour l'exemple 😅)_
{% tabs defaultSelectedTab="demo-app" %}
{% tab value="demo-app" label="App.tsx" %}
```tsx showLineNumbers
import TodoList from "./TodoList";
import React from "react";
const App = () => {
return (
<div>
<h1>TodoList</h1>
<TodoList />
</div>
);
};
```
{% /tab %}
{% tab value="demo-todolist" label="TodoList.tsx" %}
```tsx showLineNumbers
import TodoListItem from "./TodoListItem";
import React from "react";
const TodoList = () => {
const [items, setItems] = React.useState<string[]>([]);
const [inputValue, setInputValue] = React.useState<string>("");
const handleInputValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
// On empêche le comportement par défaut du formulaire
event.preventDefault();
// On ajoute un nouvel élément à la liste des tâches
setItems([...items, inputValue]);
// On réinitialise la valeur de l'input
setInputValue("");
};
return (
<div>
<form onSubmit={handleSubmit}>
<label htmlFor="todolist-input"></label>
<input id="todolist-input" type="text" value={inputValue} onChange={handleInputValueChange} />
</form>
<ul>
{items.map((item, index) => (
<li key={index}>
<TodoListItem item={item} />
</li>
))}
</ul>
</div>
);
};
export default TodoList;
```
{% /tab %}
{% tab value="demo-todolistitem" label="TodoListItem.tsx" %}
```tsx showLineNumbers
import React from "react";
interface TodoListItemProps {
item: string;
}
const TodoListItem = (props: TodoListItemProps) => {
return <span>{props.item}</span>;
};
export default TodoListItem;
```
{% /tab %}
{% /tabs %}
On peut très bien imaginer des améliorations à cette application, comme par exemple :
- Supprimer une tâche
- Réinitialiser la liste des tâches
- Marquer une tâche comme terminée _(et inversement)_
- Ordonner les tâches pour afficher en priorité les tâches non terminées
- Enregistrer les tâches dans le navigateur pour les retrouver après un rafraîchissement de la page
Et si on se gardait ça pour la suite ? 😉
## Conclusion
Tu l'auras compris, React permet de résoudre un certain nombre de problématiques que l'on peut rencontrer lors du développement d'une application web.
Pas des problématiques majeures, mais ça nous permet tout de même en tant que développeur de gagner du temps et de l'efficacité !
Dans le cas où le fait que ce soit créé et maintenu par Facebook _(ou GAFAM de manière générale)_ est contre tes valeurs,
tu as des solutions très semblables qui existent, comme [**SolidJS**](https://www.solidjs.com/) par exemple.
Et si tu veux en savoir plus, je t'invite à lire les articles suivants qui vont te permettre de rentrer un peu plus dans le détail de React ! 🚀

17
app/database/todoItems.ts Normal file
View File

@ -0,0 +1,17 @@
interface TodoItem {
text: string;
}
const todosDefault = [{ text: "Buy milk" }, { text: "Buy strawberries" }];
const database =
// We create an in-memory database.
// - We use globalThis so that the database isn't reset upon HMR.
// - The database is reset when restarting the server, use a proper database (SQLite/PostgreSQL/...) if you want persistent data.
// biome-ignore lint:
((globalThis as unknown as { __database: { todos: TodoItem[] } }).__database ??= { todos: todosDefault });
const { todos } = database;
export { todos };
export type { TodoItem };

65
app/eslint.config.js Normal file
View File

@ -0,0 +1,65 @@
// @ts-nocheck
import eslint from "@eslint/js";
import prettier from "eslint-plugin-prettier/recommended";
import react from "eslint-plugin-react/configs/recommended.js";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
{
ignores: [
"dist/*",
// Temporary compiled files
"**/*.ts.build-*.mjs",
// JS files at the root of the project
"*.js",
"*.cjs",
"*.mjs",
],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
warnOnUnsupportedTypeScriptVersion: false,
sourceType: "module",
ecmaVersion: "latest",
},
},
},
{
rules: {
"@typescript-eslint/no-unused-vars": [
1,
{
argsIgnorePattern: "^_",
},
],
"@typescript-eslint/no-namespace": 0,
"react/react-in-jsx-scope": false,
},
},
{
files: ["**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"],
...react,
languageOptions: {
...react.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
settings: {
react: {
version: "detect",
},
},
},
prettier,
);

68
app/fastify-entry.ts Normal file
View File

@ -0,0 +1,68 @@
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { vikeHandler } from "./server/vike-handler";
import { telefuncHandler } from "./server/telefunc-handler";
import Fastify from "fastify";
import { createHandler } from "@universal-middleware/fastify";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const root = __dirname;
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
const hmrPort = process.env.HMR_PORT ? parseInt(process.env.HMR_PORT, 10) : 24678;
async function startServer() {
const app = Fastify();
// Avoid pre-parsing body, otherwise it will cause issue with universal handlers
// This will probably change in the future though, you can follow https://github.com/magne4000/universal-middleware for updates
app.removeAllContentTypeParsers();
app.addContentTypeParser("*", function (_request, _payload, done) {
done(null, "");
});
await app.register(await import("@fastify/middie"));
if (process.env.NODE_ENV === "production") {
await app.register(await import("@fastify/static"), {
root: `${root}/dist/client`,
wildcard: false,
});
} else {
// Instantiate Vite's development server and integrate its middleware to our server.
// ⚠️ We should instantiate it *only* in development. (It isn't needed in production
// and would unnecessarily bloat our server in production.)
const vite = await import("vite");
const viteDevMiddleware = (
await vite.createServer({
root,
server: { middlewareMode: true, hmr: { port: hmrPort } },
})
).middlewares;
app.use(viteDevMiddleware);
}
app.post<{ Body: string }>("/_telefunc", createHandler(telefuncHandler)());
/**
* Vike route
*
* @link {@see https://vike.dev}
**/
app.all("/*", createHandler(vikeHandler)());
return app;
}
const app = await startServer();
app.listen(
{
port: port,
host: "0.0.0.0",
},
() => {
console.log(`Server listening on http://localhost:${port}`);
},
);

BIN
app/images/blur-cyan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
app/images/blur-indigo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

View File

@ -0,0 +1,95 @@
import { MobileNavigation } from "@syntax/MobileNavigation";
import { usePageContext } from "vike-react/usePageContext";
import { ThemeSelector } from "@syntax/ThemeSelector";
import { Link } from "@/components/common/Link";
import { Navigation } from "@syntax/Navigation";
import { useEffect, useState } from "react";
import { Search } from "@syntax/Search";
import { Hero } from "@syntax/Hero";
import { Logo } from "@syntax/Logo";
import clsx from "clsx";
import "./style.css";
import "./tailwind.css";
import "./prism.css";
import "unfonts.css";
function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
</svg>
);
}
function Header() {
let [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
function onScroll() {
setIsScrolled(window.scrollY > 0);
}
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
return (
<header
className={clsx(
"sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 sm:px-6 lg:px-8 dark:shadow-none",
isScrolled
? "dark:bg-slate-900/95 dark:backdrop-blur-sm dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75"
: "dark:bg-transparent",
)}
>
<div className="mr-6 flex lg:hidden">
<MobileNavigation />
</div>
<div className="relative flex grow basis-0 items-center">
<Link href="/" aria-label="Home page">
<Logo className="h-9 w-auto lg:block" />
</Link>
</div>
<div className="-my-5 mr-6 sm:mr-8 md:mr-0">
<Search />
</div>
<div className="relative flex basis-0 justify-end gap-6 sm:gap-8 md:grow">
<ThemeSelector className="relative z-10" />
<Link href="https://github.com" className="group" aria-label="GitHub">
<GitHubIcon className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300" />
</Link>
</div>
</header>
);
}
export default function DefaultLayout({ children }: { children: React.ReactNode }) {
const { urlPathname } = usePageContext();
const isHomePage = urlPathname === "/";
return (
<div className="flex w-full flex-col">
<Header />
{isHomePage && <Hero />}
<div className="relative mx-auto w-full flex max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
<div className="hidden lg:relative lg:block lg:flex-none">
<div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" />
<div className="absolute top-16 right-0 bottom-0 hidden h-12 w-px bg-linear-to-t from-slate-800 dark:block" />
<div className="absolute top-28 right-0 bottom-0 hidden w-px bg-slate-800 dark:block" />
<div className="sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64 overflow-x-hidden overflow-y-auto py-16 pr-8 pl-0.5 xl:w-72 xl:pr-16">
<Navigation />
</div>
</div>
{children}
</div>
</div>
);
}

47
app/layouts/prism.css Normal file
View File

@ -0,0 +1,47 @@
pre[class*="language-"] {
color: var(--color-slate-50);
}
.token.tag,
.token.class-name,
.token.selector,
.token.selector .class,
.token.selector.class,
.token.function {
color: var(--color-pink-400);
}
.token.attr-name,
.token.keyword,
.token.rule,
.token.pseudo-class,
.token.important {
color: var(--color-slate-300);
}
.token.module {
color: var(--color-pink-400);
}
.token.attr-value,
.token.class,
.token.string,
.token.property {
color: var(--color-sky-300);
}
.token.punctuation,
.token.attr-equals {
color: var(--color-slate-500);
}
.token.unit,
.language-css .token.function {
color: var(--color-teal-200);
}
.token.comment,
.token.operator,
.token.combinator {
color: var(--color-slate-400);
}

35
app/layouts/style.css Normal file
View File

@ -0,0 +1,35 @@
/* Links */
a {
text-decoration: none;
}
#sidebar a {
padding: 2px 10px;
margin-left: -10px;
}
#sidebar a.is-active {
background-color: #eee;
}
/* Reset */
body {
margin: 0;
font-family: sans-serif;
}
* {
box-sizing: border-box;
}
/* Page Transition Animation */
#page-content {
opacity: 1;
transition: opacity 0.3s ease-in-out;
}
body.page-is-transitioning #page-content {
opacity: 0;
}
#root {
height: 100%;
width: 100%;
display: flex;
}

48
app/layouts/tailwind.css Normal file
View File

@ -0,0 +1,48 @@
@import "tailwindcss";
@import "./prism.css";
@plugin '@tailwindcss/typography';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--text-*: initial;
--text-xs: 0.75rem;
--text-xs--line-height: 1rem;
--text-sm: 0.875rem;
--text-sm--line-height: 1.5rem;
--text-base: 1rem;
--text-base--line-height: 2rem;
--text-lg: 1.125rem;
--text-lg--line-height: 1.75rem;
--text-xl: 1.25rem;
--text-xl--line-height: 2rem;
--text-2xl: 1.5rem;
--text-2xl--line-height: 2.5rem;
--text-3xl: 2rem;
--text-3xl--line-height: 2.5rem;
--text-4xl: 2.5rem;
--text-4xl--line-height: 3rem;
--text-5xl: 3rem;
--text-5xl--line-height: 3.5rem;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--text-7xl: 4.5rem;
--text-7xl--line-height: 1;
--text-8xl: 6rem;
--text-8xl--line-height: 1;
--text-9xl: 8rem;
--text-9xl--line-height: 1;
--font-sans: "Inter Variable";
--font-display: "Lexend Variable";
--font-display--font-feature-settings: "ss01";
--container-8xl: 88rem;
}
@layer base {
[inert] ::-webkit-scrollbar {
display: none;
}
}

13
app/lib/navigation.ts Normal file
View File

@ -0,0 +1,13 @@
export const navigation = [
{
title: "Introduction",
links: [
{ title: "Getting started", href: "/" },
{ title: "Installation", href: "/docs/installation" },
],
},
{
title: "React",
links: [{ title: "Introduction", href: "/docs/react" }],
},
];

128
app/lib/search.ts Normal file
View File

@ -0,0 +1,128 @@
import { slugifyWithCounter } from "@sindresorhus/slugify";
import Markdoc from "@markdoc/markdoc";
import FlexSearch from "flexsearch";
import glob from "fast-glob";
import * as path from "path";
import * as fs from "fs";
const slugify = slugifyWithCounter();
interface Node {
type: string;
attributes?: {
content?: string;
level?: number;
id?: string;
};
children?: Node[];
}
interface Section {
content: string;
hash?: string;
subsections: string[];
}
export interface SearchResult {
url: string;
title: string;
pageTitle?: string;
content?: string;
}
function toString(node: Node): string {
let str = node.type === "text" && typeof node.attributes?.content === "string" ? node.attributes.content : "";
if ("children" in node) {
for (let child of node.children) {
str += toString(child);
}
}
return str;
}
function extractSections(node: Node, sections: Section[], isRoot: boolean = true): void {
if (isRoot) {
slugify.reset();
}
if (node.type === "heading" || node.type === "paragraph") {
let content = toString(node).trim();
if (node.type === "heading" && node.attributes?.level <= 2) {
let hash = node.attributes?.id ?? slugify(content);
sections.push({ content, hash, subsections: [] });
} else {
sections[sections.length - 1].subsections.push(content);
}
} else if ("children" in node) {
for (let child of node.children) {
extractSections(child, sections, false);
}
}
}
export function buildSearchIndex(pagesDir: string): FlexSearch.Document<SearchResult> {
const cache = new Map<string, [string, Section[]]>();
const sectionIndex = new FlexSearch.Document<SearchResult>({
tokenize: "full",
document: {
id: "url",
index: ["title", "content"],
},
context: {
resolution: 9,
depth: 2,
bidirectional: true,
},
});
const files = glob.sync("**/page.md", { cwd: pagesDir });
const data = files.map((file) => {
const url = file === "page.md" ? "/" : `/${file.replace(/\/page\.md$/, "")}`;
const md = fs.readFileSync(path.join(pagesDir, file), "utf8");
let sections: Section[];
if (cache.get(file)?.[0] === md) {
sections = cache.get(file)![1];
} else {
const ast = Markdoc.parse(md);
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
sections = [{ content: title ?? "", subsections: [] }];
extractSections(ast, sections);
cache.set(file, [md, sections]);
}
return { url, sections };
});
for (const { url, sections } of data) {
for (const { content, hash, subsections } of sections) {
sectionIndex.add({
url: url + (hash ? `#${hash}` : ""),
title: content,
content: [content, ...subsections].join("\n"),
pageTitle: hash ? sections[0].content : undefined,
});
}
}
return sectionIndex;
}
export function search(
sectionIndex: FlexSearch.Document<SearchResult>,
query: string,
options: Record<string, any> = {},
): SearchResult[] {
const result = sectionIndex.search(query, {
...options,
enrich: true,
});
if (result.length === 0) {
return [];
}
return result[0].result.map((item: any) => ({
url: item.id,
title: item.doc.title,
pageTitle: item.doc.pageTitle,
}));
}

92
app/lib/sections.ts Normal file
View File

@ -0,0 +1,92 @@
import { type Node } from "@markdoc/markdoc";
import { slugifyWithCounter } from "@sindresorhus/slugify";
interface HeadingNode extends Node {
type: "heading";
attributes: {
level: 1 | 2 | 3 | 4 | 5 | 6;
id?: string;
[key: string]: unknown;
};
}
type H2Node = HeadingNode & {
attributes: {
level: 2;
};
};
type H3Node = HeadingNode & {
attributes: {
level: 3;
};
};
function isHeadingNode(node: Node): node is HeadingNode {
return (
node.type === "heading" &&
[1, 2, 3, 4, 5, 6].includes(node.attributes.level) &&
(typeof node.attributes.id === "string" || typeof node.attributes.id === "undefined")
);
}
function isH2Node(node: Node): node is H2Node {
return isHeadingNode(node) && node.attributes.level === 2;
}
function isH3Node(node: Node): node is H3Node {
return isHeadingNode(node) && node.attributes.level === 3;
}
function getNodeText(node: Node) {
let text = "";
for (let child of node.children ?? []) {
if (child.type === "text") {
text += child.attributes.content;
}
text += getNodeText(child);
}
return text;
}
export type Subsection = H3Node["attributes"] & {
id: string;
title: string;
children?: undefined;
};
export type Section = H2Node["attributes"] & {
id: string;
title: string;
children: Array<Subsection>;
};
export function collectSections(nodes: Array<Node>, slugify = slugifyWithCounter()) {
const sections: Array<Section> = [];
for (const node of nodes) {
if (isH2Node(node) || isH3Node(node)) {
const title = getNodeText(node);
if (title) {
const id = slugify(title);
if (isH3Node(node)) {
if (!sections[sections.length - 1]) {
throw new Error("Cannot add `h3` to table of contents without a preceding `h2`");
}
sections[sections.length - 1].children.push({
...node.attributes,
id,
title,
});
} else {
sections.push({ ...node.attributes, id, title, children: [] });
}
}
}
sections.push(...collectSections(node.children ?? [], slugify));
}
return sections;
}

59
app/markdoc/nodes.ts Normal file
View File

@ -0,0 +1,59 @@
import { Config, nodes as defaultNodes, Node, Tag } from "@markdoc/markdoc";
import { slugifyWithCounter } from "@sindresorhus/slugify";
import yaml from "js-yaml";
import { DocsLayout } from "@syntax/DocsLayout";
import { Fence } from "@syntax/Fence";
let documentSlugifyMap = new Map();
const nodes = {
document: {
...defaultNodes.document,
render: DocsLayout,
transform(node: Node, config: Config) {
documentSlugifyMap.set(config, slugifyWithCounter());
return new Tag(
this.render,
{
frontmatter: yaml.load(node.attributes.frontmatter),
nodes: node.children,
},
node.transformChildren(config),
);
},
},
heading: {
...defaultNodes.heading,
transform(node: Node, config: Config) {
const slugify = documentSlugifyMap.get(config);
const attributes = node.transformAttributes(config);
const children = node.transformChildren(config);
const text = children.filter((child) => typeof child === "string").join(" ");
const id = attributes.id ?? slugify(text);
return new Tag(`h${node.attributes.level}`, { ...attributes, id }, children);
},
},
th: {
...defaultNodes.th,
attributes: {
...defaultNodes.th.attributes,
scope: {
type: String,
default: "col",
},
},
},
fence: {
render: Fence,
attributes: {
language: {
type: String,
},
},
},
};
export default nodes;

60
app/markdoc/tags.tsx Normal file
View File

@ -0,0 +1,60 @@
import { QuickLink, QuickLinks } from "@syntax/QuickLinks";
import { TabContent, Tabs } from "@/components/md/Tabs";
import { Callout } from "@syntax/Callout";
const tags = {
callout: {
attributes: {
title: { type: String },
type: {
type: String,
default: "note",
matches: ["note", "warning"],
errorLevel: "critical",
},
},
render: Callout,
},
figure: {
selfClosing: true,
attributes: {
src: { type: String },
alt: { type: String },
caption: { type: String },
},
render: ({ src, alt = "", caption }: { src: string; alt: string; caption: string }) => (
<figure>
<img src={src} alt={alt} loading="lazy" />
<figcaption>{caption}</figcaption>
</figure>
),
},
"quick-links": {
render: QuickLinks,
},
"quick-link": {
selfClosing: true,
render: QuickLink,
attributes: {
title: { type: String },
description: { type: String },
icon: { type: String },
href: { type: String },
},
},
tabs: {
render: Tabs,
attributes: {
defaultSelectedTab: { type: String },
},
},
tab: {
render: TabContent,
attributes: {
label: { type: String },
value: { type: String },
},
},
};
export default tags;

59
app/package.json Normal file
View File

@ -0,0 +1,59 @@
{
"scripts": {
"dev": "tsx ./fastify-entry.ts",
"build": "vike build",
"preview": "cross-env NODE_ENV=production tsx ./fastify-entry.ts",
"lint": "eslint ."
},
"dependencies": {
"@algolia/autocomplete-core": "^1.18.1",
"@fastify/middie": "^9.0.3",
"@fastify/static": "^8.1.1",
"@fontsource-variable/inter": "^5.2.5",
"@fontsource-variable/lexend": "^5.2.5",
"@headlessui/react": "^2.2.0",
"@markdoc/markdoc": "^0.5.1",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.16",
"@universal-middleware/core": "^0.4.4",
"@universal-middleware/fastify": "^0.5.9",
"@vitejs/plugin-react": "^4.3.4",
"clsx": "^2.1.1",
"fast-glob": "^3.3.3",
"fastify": "^5.2.1",
"flexsearch": "^0.7.43",
"js-yaml": "^4.1.0",
"prism-react-renderer": "^2.4.1",
"prisma": "^6.5.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight-words": "^0.21.0",
"simple-functional-loader": "^1.2.1",
"telefunc": "^0.1.87",
"unplugin-fonts": "^1.3.1",
"vike": "^0.4.224",
"vike-react": "^0.5.13"
},
"devDependencies": {
"@eslint/js": "^9.22.0",
"@tailwindcss/vite": "^4.0.12",
"@types/js-yaml": "^4.0.9",
"@types/node": "^18.19.76",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/react-highlight-words": "^0.20.0",
"cross-env": "^7.0.3",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"tailwindcss": "^4.0.12",
"tsx": "^4.19.3",
"typescript": "^5.8.2",
"typescript-eslint": "^8.26.0",
"vite": "^6.2.1"
},
"type": "module"
}

12
app/pages/+Head.tsx Normal file
View File

@ -0,0 +1,12 @@
// https://vike.dev/Head
import React from "react";
import logoUrl from "../assets/logo.svg";
export default function HeadDefault() {
return (
<>
<link rel="icon" href={logoUrl} />
</>
);
}

24
app/pages/+config.ts Normal file
View File

@ -0,0 +1,24 @@
import vikeReact from "vike-react/config";
import type { Config } from "vike/types";
import Layout from "../layouts/LayoutDefault.js";
// Default config (can be overridden by pages)
// https://vike.dev/config
export default {
// https://vike.dev/Layout
Layout,
// https://vike.dev/head-tags
title: "Memento Dev",
description: "Demo showcasing Vike",
htmlAttributes: {
class: "h-full antialiased",
},
bodyAttributes: {
class: "flex min-h-full bg-white dark:bg-slate-900",
},
extends: vikeReact,
} satisfies Config;

View File

@ -0,0 +1,6 @@
import type { OnPageTransitionEndAsync } from "vike/types";
export const onPageTransitionEnd: OnPageTransitionEndAsync = async () => {
console.log("Page transition end");
document.querySelector("body")?.classList.remove("page-is-transitioning");
};

View File

@ -0,0 +1,6 @@
import type { OnPageTransitionStartAsync } from "vike/types";
export const onPageTransitionStart: OnPageTransitionStartAsync = async () => {
console.log("Page transition start");
document.querySelector("body")?.classList.add("page-is-transitioning");
};

View File

@ -0,0 +1,32 @@
import { usePageContext } from "vike-react/usePageContext";
import { Link } from "@/components/common/Link";
export default function Page() {
const { is404 } = usePageContext();
if (is404) {
return (
<>
<div className="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
<div className="flex h-full flex-col items-center justify-center text-center">
<p className="font-display text-sm font-medium text-slate-900 dark:text-white">404</p>
<h1 className="mt-3 font-display text-3xl tracking-tight text-slate-900 dark:text-white">
Page introuvable
</h1>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
Désolé, nous ne pouvons pas trouver la page que vous recherchez.
</p>
<Link href="/" className="mt-8 text-sm font-medium text-slate-900 dark:text-white">
Retour à l'accueil
</Link>
</div>
</div>
</>
);
}
return (
<>
<h1>500 Internal Server Error</h1>
<p>Something went wrong.</p>
</>
);
}

16
app/pages/docs/+Page.tsx Normal file
View File

@ -0,0 +1,16 @@
import type { Data } from "./+data";
import { useData } from "vike-react/useData";
import Markdoc from "@markdoc/markdoc";
import nodes from "@/markdoc/nodes";
import tags from "@/markdoc/tags";
import React from "react";
export default function Page() {
const { doc } = useData<Data>();
const parsedDoc = Markdoc.parse(doc.content);
const transformedDoc = Markdoc.transform(parsedDoc, { nodes, tags, variables: {} });
return Markdoc.renderers.react(transformedDoc, React);
}

29
app/pages/docs/+data.ts Normal file
View File

@ -0,0 +1,29 @@
import type { PageContext } from "vike/types";
import { docsService } from "@/services/DocsService";
import { useConfig } from "vike-react/useConfig";
import Markdoc from "@markdoc/markdoc";
import { render } from "vike/abort";
export type Data = Awaited<ReturnType<typeof data>>;
export async function data(pageContext: PageContext) {
const config = useConfig();
const { key } = pageContext.routeParams;
const doc = await docsService.getDoc("docs", key);
if (!doc) {
throw render(404);
}
config({
title: doc.title,
description: doc.description,
});
docsService.transform(doc);
return { doc };
}

12
app/pages/docs/+route.ts Normal file
View File

@ -0,0 +1,12 @@
import type { PageContext } from "vike/types";
const routeRegex = /^\/docs\/(.*)$/;
export function route(pageContext: PageContext) {
const match = pageContext.urlPathname.match(routeRegex);
if (!match) return false;
const [, key] = match;
return { routeParams: { key } };
}

View File

@ -0,0 +1,7 @@
export default function Page() {
return (
<main className="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
<h1 className={"font-bold text-3xl pb-4"}>My Vike app</h1>
</main>
);
}

View File

@ -0,0 +1,17 @@
import React, { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button
type="button"
className={
"inline-block border border-black rounded bg-gray-200 px-2 py-1 text-xs font-medium uppercase leading-normal"
}
onClick={() => setCount((count) => count + 1)}
>
Counter {count}
</button>
);
}

View File

@ -0,0 +1,17 @@
import React from "react";
import { useData } from "vike-react/useData";
import type { Data } from "./+data.js";
export default function Page() {
const movie = useData<Data>();
return (
<>
<h1>{movie.title}</h1>
Release Date: {movie.release_date}
<br />
Director: {movie.director}
<br />
Producer: {movie.producer}
</>
);
}

View File

@ -0,0 +1,32 @@
// https://vike.dev/data
import type { PageContextServer } from "vike/types";
import type { MovieDetails } from "../types.js";
import { useConfig } from "vike-react/useConfig";
export type Data = Awaited<ReturnType<typeof data>>;
export const data = async (pageContext: PageContextServer) => {
// https://vike.dev/useConfig
const config = useConfig();
const response = await fetch(`https://brillout.github.io/star-wars/api/films/${pageContext.routeParams.id}.json`);
let movie = (await response.json()) as MovieDetails;
config({
// Set <title>
title: movie.title,
});
// We remove data we don't need because the data is passed to
// the client; we should minimize what is sent over the network.
movie = minimize(movie);
return movie;
};
function minimize(movie: MovieDetails): MovieDetails {
const { id, title, release_date, director, producer } = movie;
const minimizedMovie = { id, title, release_date, director, producer };
return minimizedMovie;
}

View File

@ -0,0 +1,22 @@
import React from "react";
import { useData } from "vike-react/useData";
import type { Data } from "./+data.js";
export default function Page() {
const movies = useData<Data>();
return (
<>
<h1>Star Wars Movies</h1>
<ol>
{movies.map(({ id, title, release_date }) => (
<li key={id}>
<a href={`/star-wars/${id}`}>{title}</a> ({release_date})
</li>
))}
</ol>
<p>
Source: <a href="https://brillout.github.io/star-wars">brillout.github.io/star-wars</a>.
</p>
</>
);
}

View File

@ -0,0 +1,32 @@
// https://vike.dev/data
import type { Movie, MovieDetails } from "../types.js";
import { useConfig } from "vike-react/useConfig";
export type Data = Awaited<ReturnType<typeof data>>;
export const data = async () => {
// https://vike.dev/useConfig
const config = useConfig();
const response = await fetch("https://brillout.github.io/star-wars/api/films.json");
const moviesData = (await response.json()) as MovieDetails[];
config({
// Set <title>
title: `${moviesData.length} Star Wars Movies`,
});
// We remove data we don't need because the data is passed to the client; we should
// minimize what is sent over the network.
const movies = minimize(moviesData);
return movies;
};
function minimize(movies: MovieDetails[]): Movie[] {
return movies.map((movie) => {
const { title, release_date, id } = movie;
return { title, release_date, id };
});
}

View File

@ -0,0 +1,10 @@
export type Movie = {
id: string;
title: string;
release_date: string;
};
export type MovieDetails = Movie & {
director: string;
producer: string;
};

14
app/pages/todo/+Page.tsx Normal file
View File

@ -0,0 +1,14 @@
import type { Data } from "./+data";
import React from "react";
import { useData } from "vike-react/useData";
import { TodoList } from "./TodoList.js";
export default function Page() {
const data = useData<Data>();
return (
<>
<h1>To-do List</h1>
<TodoList initialTodoItems={data.todo} />
</>
);
}

View File

@ -0,0 +1,3 @@
export const config = {
prerender: false,
};

11
app/pages/todo/+data.ts Normal file
View File

@ -0,0 +1,11 @@
// https://vike.dev/data
import { todos } from "../../database/todoItems";
import type { PageContextServer } from "vike/types";
export type Data = {
todo: { text: string }[];
};
export default async function data(_pageContext: PageContextServer): Promise<Data> {
return { todo: todos };
}

View File

@ -0,0 +1,7 @@
// We use Telefunc (https://telefunc.com) for data mutations. Being able to use Telefunc for fetching initial data is work-in-progress (https://vike.dev/data-fetching#tools).
import { todos } from "../../database/todoItems";
export async function onNewTodo({ text }: { text: string }) {
todos.push({ text });
}

View File

@ -0,0 +1,52 @@
import { onNewTodo } from "./TodoList.telefunc";
import React, { useState } from "react";
export function TodoList({ initialTodoItems }: { initialTodoItems: { text: string }[] }) {
const [todoItems, setTodoItems] = useState(initialTodoItems);
const [newTodo, setNewTodo] = useState("");
return (
<>
<ul>
{todoItems.map((todoItem, index) => (
// biome-ignore lint:
<li key={index}>{todoItem.text}</li>
))}
</ul>
<div>
<form
onSubmit={async (ev) => {
ev.preventDefault();
// Optimistic UI update
setTodoItems((prev) => [...prev, { text: newTodo }]);
try {
await onNewTodo({ text: newTodo });
setNewTodo("");
} catch (e) {
console.error(e);
// rollback
setTodoItems((prev) => prev.slice(0, -1));
}
}}
>
<input
type="text"
onChange={(ev) => setNewTodo(ev.target.value)}
value={newTodo}
className={
"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 w-full sm:w-auto p-2 mr-1 mb-1"
}
/>
<button
type="submit"
className={
"text-white bg-purple-700 hover:bg-purple-800 focus:ring-2 focus:outline-hidden focus:ring-purple-300 font-medium rounded-lg text-sm w-full sm:w-auto p-2"
}
>
Add to-do
</button>
</form>
</div>
</>
);
}

5572
app/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

14
app/prisma/schema.prisma Normal file
View File

@ -0,0 +1,14 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

View File

@ -0,0 +1,22 @@
import { telefunc } from "telefunc";
// TODO: stop using universal-middleware and directly integrate server middlewares instead. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.)
import type { Get, UniversalHandler } from "@universal-middleware/core";
export const telefuncHandler: Get<[], UniversalHandler> = () => async (request, context, runtime) => {
const httpResponse = await telefunc({
url: request.url.toString(),
method: request.method,
body: await request.text(),
context: {
...context,
...runtime,
},
});
const { body, statusCode, contentType } = httpResponse;
return new Response(body, {
status: statusCode,
headers: {
"content-type": contentType,
},
});
};

View File

@ -0,0 +1,18 @@
/// <reference lib="webworker" />
import { renderPage } from "vike/server";
// TODO: stop using universal-middleware and directly integrate server middlewares instead. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.)
import type { Get, UniversalHandler } from "@universal-middleware/core";
export const vikeHandler: Get<[], UniversalHandler> = () => async (request, context, runtime) => {
const pageContextInit = { ...context, ...runtime, urlOriginal: request.url, headersOriginal: request.headers };
const pageContext = await renderPage(pageContextInit);
const response = pageContext.httpResponse;
const { readable, writable } = new TransformStream();
response.pipe(writable);
return new Response(readable, {
status: response.statusCode,
headers: response.headers,
});
};

132
app/services/DocsService.ts Normal file
View File

@ -0,0 +1,132 @@
import type { Node } from "@markdoc/markdoc";
import { slugifyWithCounter } from "@sindresorhus/slugify";
import { buildFlexSearch } from "./FlexSearchService";
import Markdoc from "@markdoc/markdoc";
import nodes from "@/markdoc/nodes";
import tags from "@/markdoc/tags";
import glob from "fast-glob";
import path from "path";
import fs from "fs";
export type FlexSearchData = { key: string; sections: DocSection[] }[];
type DocsCache = Map<string, DocData>;
type DocSection = [string, string | null, string[]];
type DocData = { title: string; description: string; content: string; sections: DocSection[] };
type DocExtension = "mdx" | "md";
class DocsService {
private static readonly DOCS_PATH = path.resolve("../../app/data");
private static readonly DOCS_EXTS: DocExtension[] = ["mdx", "md"]; // Order matters
private static instance: DocsService;
public search: ReturnType<typeof buildFlexSearch> = buildFlexSearch([]);
private slugify = slugifyWithCounter();
private cache: DocsCache = new Map();
public static getInstance(): DocsService {
if (!DocsService.instance) {
DocsService.instance = new DocsService();
}
return DocsService.instance;
}
private getFromCache(key: string): DocData | undefined {
return this.cache.get(key);
}
private setToCache(key: string, value: DocData): void {
this.cache.set(key, value);
}
private nodeToString(node: Node): string {
let string = "";
if (node.type === "text" && typeof node.attributes?.content === "string") {
string = node.attributes.content;
}
if (node.children) {
for (const child of node.children) {
string += this.nodeToString(child);
}
}
return string;
}
private extractSections(node: Node, sections: DocSection[], isRoot = true) {
if (isRoot) {
this.slugify.reset();
}
if (["heading", "paragraph"].includes(node.type)) {
const content = this.nodeToString(node).trim();
if (node.type === "heading" && node.attributes.level <= 2) {
const hash = node.attributes?.id ?? this.slugify(content);
sections.push([content, hash, []]);
} else {
sections.at(-1)?.[2].push(content);
}
} else if (node.children) {
for (const child of node.children) {
this.extractSections(child, sections, false);
}
}
}
public async fetchDocs() {
const docs = glob.sync(DocsService.DOCS_PATH + `/**/*.{${DocsService.DOCS_EXTS.join(",")}}`);
const data = docs.map((doc) => {
const content = fs.readFileSync(doc, "utf-8");
const extension = path.extname(doc).slice(1) as DocExtension;
const key = doc
.replace(DocsService.DOCS_PATH, "")
.replace(`page.${extension}`, "")
.replace(`.${extension}`, "")
.replace(/\/$/g, "");
const ast = Markdoc.parse(content);
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1];
const sections: DocSection[] = [[title, null, []]];
this.extractSections(ast, sections);
this.setToCache(key, { title, description, content, sections });
return { key, sections };
});
this.search = buildFlexSearch(data);
}
public transform(doc: DocData) {
const ast = Markdoc.parse(doc.content);
const transformed = Markdoc.transform(ast, { nodes, tags, variables: {} });
return {
...doc,
tags: transformed,
};
}
public async getDoc(namespace: "docs" | "certifications", key: string) {
try {
await this.fetchDocs();
const doc = this.getFromCache(`/${namespace}/${key}`);
if (!doc) {
throw new Error("Doc not found");
}
return doc;
} catch (error) {
return null;
}
}
}
export const docsService = DocsService.getInstance();

View File

@ -0,0 +1,56 @@
import type { FlexSearchData } from "./DocsService";
import FlexSearch from "flexsearch";
interface SearchResult {
id: string;
doc: {
title: string;
pageTitle?: string;
};
}
export function buildFlexSearch(data: FlexSearchData) {
const sectionIndex = new FlexSearch.Document({
tokenize: "full",
document: {
id: "url",
index: "content",
store: ["title", "pageTitle"],
},
context: {
resolution: 9,
depth: 2,
bidirectional: true,
},
});
for (const { key, sections } of data) {
for (const [title, hash, content] of sections) {
sectionIndex.add({
url: key + (hash ? `#${hash}` : ""),
title,
content: [title, ...content].join("\n"),
pageTitle: hash ? sections[0][0] : undefined,
});
}
}
return function search(query: string) {
const result = sectionIndex.search<true>(query, 5, {
enrich: true,
});
if (result.length === 0) return [];
return result[0].result.map((rawItem) => {
const item = rawItem as unknown as SearchResult;
return {
url: item.id,
title: item.doc.title,
pageTitle: item.doc.pageTitle,
};
});
};
}

24
app/tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"module": "ESNext",
"noEmit": true,
"moduleResolution": "Bundler",
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["vite/client", "vike-react"],
"jsx": "preserve",
"jsxImportSource": "react",
"baseUrl": "./",
"paths": {
"@/*": ["./*"],
"@syntax/*": ["./components/syntax/*"]
}
},
"exclude": ["dist"]
}

1
app/types.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module "simple-functional-loader";

29
app/vite.config.ts Normal file
View File

@ -0,0 +1,29 @@
import tailwindcss from "@tailwindcss/vite";
import Unfonts from "unplugin-fonts/vite";
import { telefunc } from "telefunc/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import vike from "vike/plugin";
export default defineConfig({
plugins: [
Unfonts({
fontsource: {
families: ["Inter Variable", "Lexend Variable"],
},
}),
vike({}),
react({}),
tailwindcss(),
telefunc(),
],
build: {
target: "es2022",
},
resolve: {
alias: {
"@syntax": __dirname + "/components/syntax",
"@": __dirname,
},
},
});

14
compose.yml Normal file
View File

@ -0,0 +1,14 @@
services:
memento-dev:
container_name: memento-dev
build:
context: .
dockerfile: pnpm.Dockerfile
env_file:
- .env
ports:
- "${PORT}:${PORT}"
- "${HMR_PORT}:${HMR_PORT}"
volumes:
- ./app:/app
restart: unless-stopped

View File

@ -0,0 +1,5 @@
This folder stores temp files that Docusaurus' client bundler accesses.
DO NOT hand-modify files in this folder because they will be overwritten in the
next build. You can clear all build artifacts (including this folder) with the
`docusaurus clear` command.

View File

@ -0,0 +1,6 @@
export default [
require("/Users/jack/Projets/Perso/memento-dev/node_modules/.pnpm/infima@0.2.0-alpha.45/node_modules/infima/dist/css/default/default.css"),
require("/Users/jack/Projets/Perso/memento-dev/node_modules/.pnpm/@docusaurus+theme-classic@3.6.1_@types+react@18.3.3_react-dom@18.3.1_react@18.3.1__react@18.3.1_typescript@5.2.2/node_modules/@docusaurus/theme-classic/lib/prism-include-languages"),
require("/Users/jack/Projets/Perso/memento-dev/node_modules/.pnpm/@docusaurus+theme-classic@3.6.1_@types+react@18.3.3_react-dom@18.3.1_react@18.3.1__react@18.3.1_typescript@5.2.2/node_modules/@docusaurus/theme-classic/lib/nprogress"),
require("/Users/jack/Projets/Perso/memento-dev/src/css/custom.css"),
];

View File

@ -0,0 +1,82 @@
{
"theme.AnnouncementBar.closeButtonAriaLabel": "Fermer",
"theme.BackToTopButton.buttonAriaLabel": "Retour au début de la page",
"theme.CodeBlock.copied": "Copié",
"theme.CodeBlock.copy": "Copier",
"theme.CodeBlock.copyButtonAriaLabel": "Copier le code",
"theme.CodeBlock.wordWrapToggle": "Activer/désactiver le retour à la ligne",
"theme.DocSidebarItem.collapseCategoryAriaLabel": "Réduire la catégorie '{label}' de la barre latérale",
"theme.DocSidebarItem.expandCategoryAriaLabel": "Développer la catégorie '{label}' de la barre latérale",
"theme.ErrorPageContent.title": "Cette page a planté.",
"theme.ErrorPageContent.tryAgain": "Réessayer",
"theme.NavBar.navAriaLabel": "Main",
"theme.NotFound.p1": "Nous n'avons pas trouvé ce que vous recherchez.",
"theme.NotFound.p2": "Veuillez contacter le propriétaire du site qui vous a lié à l'URL d'origine et leur faire savoir que leur lien est cassé.",
"theme.NotFound.title": "Page introuvable",
"theme.TOCCollapsible.toggleButtonLabel": "Sur cette page",
"theme.admonition.caution": "attention",
"theme.admonition.danger": "danger",
"theme.admonition.info": "info",
"theme.admonition.note": "remarque",
"theme.admonition.tip": "astuce",
"theme.admonition.warning": "attention",
"theme.blog.archive.description": "Archive",
"theme.blog.archive.title": "Archive",
"theme.blog.author.noPosts": "This author has not written any posts yet.",
"theme.blog.author.pageTitle": "{authorName} - {nPosts}",
"theme.blog.authorsList.pageTitle": "Authors",
"theme.blog.authorsList.viewAll": "View All Authors",
"theme.blog.paginator.navAriaLabel": "Pagination de la liste des articles du blog",
"theme.blog.paginator.newerEntries": "Nouvelles entrées",
"theme.blog.paginator.olderEntries": "Anciennes entrées",
"theme.blog.post.paginator.navAriaLabel": "Pagination des articles du blog",
"theme.blog.post.paginator.newerPost": "Article plus récent",
"theme.blog.post.paginator.olderPost": "Article plus ancien",
"theme.blog.post.plurals": "Un article|{count} articles",
"theme.blog.post.readMore": "Lire plus",
"theme.blog.post.readMoreLabel": "En savoir plus sur {title}",
"theme.blog.post.readingTime.plurals": "Une minute de lecture|{readingTime} minutes de lecture",
"theme.blog.sidebar.navAriaLabel": "Navigation article de blog récent",
"theme.blog.tagTitle": "{nPosts} tagués avec « {tagName} »",
"theme.colorToggle.ariaLabel": "Basculer entre le mode sombre et clair (actuellement {mode})",
"theme.colorToggle.ariaLabel.mode.dark": "mode sombre",
"theme.colorToggle.ariaLabel.mode.light": "mode clair",
"theme.common.editThisPage": "Éditer cette page",
"theme.common.headingLinkTitle": "Lien direct vers {heading}",
"theme.common.skipToMainContent": "Aller au contenu principal",
"theme.contentVisibility.draftBanner.message": "This page is a draft. It will only be visible in dev and be excluded from the production build.",
"theme.contentVisibility.draftBanner.title": "Draft page",
"theme.contentVisibility.unlistedBanner.message": "Cette page n'est pas répertoriée. Les moteurs de recherche ne l'indexeront pas, et seuls les utilisateurs ayant un lien direct peuvent y accéder.",
"theme.contentVisibility.unlistedBanner.title": "Page non répertoriée",
"theme.docs.DocCard.categoryDescription.plurals": "1 élément|{count} éléments",
"theme.docs.breadcrumbs.home": "Page d'accueil",
"theme.docs.breadcrumbs.navAriaLabel": "Fil d'Ariane",
"theme.docs.paginator.navAriaLabel": "Pages de documentation",
"theme.docs.paginator.next": "Suivant",
"theme.docs.paginator.previous": "Précédent",
"theme.docs.sidebar.closeSidebarButtonAriaLabel": "Fermer la barre de navigation",
"theme.docs.sidebar.collapseButtonAriaLabel": "Réduire le menu latéral",
"theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral",
"theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral",
"theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral",
"theme.docs.sidebar.navAriaLabel": "Docs sidebar",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Ouvrir/fermer la barre de navigation",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} avec \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "Un document tagué|{count} documents tagués",
"theme.docs.versionBadge.label": "Version: {versionLabel}",
"theme.docs.versions.latestVersionLinkLabel": "dernière version",
"theme.docs.versions.latestVersionSuggestionLabel": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.",
"theme.docs.versions.unreleasedVersionLabel": "Ceci est la documentation de la prochaine version {versionLabel} de {siteTitle}.",
"theme.lastUpdated.atDate": " le {date}",
"theme.lastUpdated.byUser": " par {user}",
"theme.lastUpdated.lastUpdatedAtBy": "Dernière mise à jour{atDate}{byUser}",
"theme.navbar.mobileLanguageDropdown.label": "Langues",
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Retour au menu principal",
"theme.navbar.mobileVersionsDropdown.label": "Versions",
"theme.tags.tagsListLabel": "Tags :",
"theme.tags.tagsPageLink": "Voir tous les tags",
"theme.tags.tagsPageTitle": "Tags",
"theme.unlistedContent.title": "Page non répertoriée",
"theme.unlistedContent.message": "Cette page n'est pas répertoriée. Les moteurs de recherche ne l'indexeront pas, et seuls les utilisateurs ayant un lien direct peuvent y accéder."
}

View File

@ -0,0 +1,4 @@
{
"name": "docusaurus-lunr-search",
"id": "default"
}

View File

@ -0,0 +1,4 @@
{
"name": "docusaurus-plugin-content-docs",
"id": "default"
}

View File

@ -0,0 +1 @@
{"categoryGeneratedIndex":{"title":"💽 Bases de données","slug":"/category/-bases-de-données","permalink":"/category/-bases-de-données","sidebar":"tutorialSidebar","navigation":{"previous":{"title":"Retours d'expériences","permalink":"/titres-professionnels/retours"},"next":{"title":"Modélisation avec Merise","permalink":"/ressources/bases-de-donnees/modelisation/merise"}}}}

View File

@ -0,0 +1 @@
{"categoryGeneratedIndex":{"title":"🌈 Frontend","slug":"/category/-frontend","permalink":"/category/-frontend","sidebar":"tutorialSidebar","navigation":{"previous":{"title":"Création du MCD","permalink":"/ressources/bases-de-donnees/modelisation/mcd"},"next":{"title":"Introduction à React","permalink":"/ressources/frontend/react/intro"}}}}

View File

@ -0,0 +1 @@
{"categoryGeneratedIndex":{"title":"🎓 Titres professionnels","description":"Tu retrouveras ici l'ensemble des titres professionnels couverts par la plateforme Memento Dev.","slug":"/category/-titres-professionnels","permalink":"/category/-titres-professionnels","sidebar":"tutorialSidebar","navigation":{"previous":{"title":"Titres professionnels","permalink":"/glossaires/titres-professionnel"},"next":{"title":"Résumé","permalink":"/titres-professionnels/CDA/intro"}}}}

View File

@ -0,0 +1 @@
{"categoryGeneratedIndex":{"title":"🗃️ Archives","description":"Archives des différents référentiels arrivés à expiration.","slug":"/category/-archives","permalink":"/category/-archives","sidebar":"tutorialSidebar","navigation":{"previous":{"title":"CP 8","permalink":"/titres-professionnels/DWWM/AT2/CP8"},"next":{"title":"Résumé","permalink":"/titres-professionnels/archives/DWWM/2018-2023/intro"}}}}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"tag":{"label":"Accessibilité","permalink":"/tags/accessibilite","allTagsPath":"/tags","count":3,"items":[{"id":"titres-professionnels/DWWM/AT1/CP2","title":"CP 2","description":"Synthèse de la CP 2 \"Maquetter des interfaces utilisateur web ou web mobile\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT1/CP2"},{"id":"titres-professionnels/DWWM/AT1/CP3","title":"CP 3","description":"Synthèse de la CP 3 \"Réaliser des interfaces utilisateur statiques web ou web mobile\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT1/CP3"},{"id":"titres-professionnels/DWWM/AT1/CP4","title":"CP 4","description":"Synthèse de la CP 4 \"Développer la partie dynamique des interfaces utilisateur web ou web mobile\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT1/CP4"}],"unlisted":false}}

View File

@ -0,0 +1 @@
{"tag":{"label":"Architecture","permalink":"/tags/architecture","allTagsPath":"/tags","count":1,"items":[{"id":"titres-professionnels/DWWM/AT2/CP7","title":"CP 7","description":"Synthèse de la CP 7 \"Développer des composants métier coté serveur\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT2/CP7"}],"unlisted":false}}

View File

@ -0,0 +1 @@
{"tags":[{"label":"Conception","permalink":"/tags/conception","count":4},{"label":"Modélisation","permalink":"/tags/modelisation","count":4},{"label":"Base de données","permalink":"/tags/base-de-donnees","count":5},{"label":"Merise","permalink":"/tags/merise","count":4},{"label":"Dictionnaire de Données","permalink":"/tags/dictionnaire-de-donnees","count":4},{"label":"Modèle Conceptuel de Données (MCD)","permalink":"/tags/modele-conceptuel-de-donnees-mcd","count":4},{"label":"Modèle Logique de Données (MLD)","permalink":"/tags/modele-logique-de-donnees-mld","count":4},{"label":"Modèle Relationnel de Données (MRD)","permalink":"/tags/modele-relationnel-de-donnees-mrd","count":4},{"label":"Modèle Physique de Données (MPD)","permalink":"/tags/modele-physique-de-donnees-mpd","count":4},{"label":"SQL","permalink":"/tags/sql","count":5},{"label":"DWWM","permalink":"/tags/dwwm","count":12},{"label":"CDA","permalink":"/tags/cda","count":14},{"label":"Frontend","permalink":"/tags/frontend","count":8},{"label":"React","permalink":"/tags/react","count":8},{"label":"JavaScript/TypeScript","permalink":"/tags/java-script-type-script","count":9},{"label":"Bibliothèque","permalink":"/tags/bibliotheque","count":8},{"label":"Interface utilisateur (UI)","permalink":"/tags/interface-utilisateur-ui","count":8},{"label":"Vite","permalink":"/tags/vite","count":1},{"label":"Vike","permalink":"/tags/vike","count":1},{"label":"SSR","permalink":"/tags/ssr","count":1},{"label":"SSG","permalink":"/tags/ssg","count":1},{"label":"CSR","permalink":"/tags/csr","count":1},{"label":"Environnement de développement","permalink":"/tags/environnement-de-developpement","count":2},{"label":"Eco-conception","permalink":"/tags/eco-conception","count":3},{"label":"Accessibilité","permalink":"/tags/accessibilite","count":3},{"label":"SEO/Référencement naturel","permalink":"/tags/seo-referencement-naturel","count":3},{"label":"Maquettage","permalink":"/tags/maquettage","count":1},{"label":"Wireframe","permalink":"/tags/wireframe","count":1},{"label":"Zoning","permalink":"/tags/zoning","count":1},{"label":"Front-end","permalink":"/tags/front-end","count":3},{"label":"Intégration","permalink":"/tags/integration","count":1},{"label":"Responsive","permalink":"/tags/responsive","count":1},{"label":"HTML","permalink":"/tags/html","count":1},{"label":"CSS","permalink":"/tags/css","count":1},{"label":"Déploiement","permalink":"/tags/deploiement","count":2},{"label":"Reverse Proxy","permalink":"/tags/reverse-proxy","count":2},{"label":"Serveur web","permalink":"/tags/serveur-web","count":2},{"label":"Back-end","permalink":"/tags/back-end","count":4},{"label":"ORM/ODM","permalink":"/tags/orm-odm","count":1},{"label":"Sécurité","permalink":"/tags/securite","count":2},{"label":"Chiffrement","permalink":"/tags/chiffrement","count":1},{"label":"Hachage","permalink":"/tags/hachage","count":1},{"label":"Architecture","permalink":"/tags/architecture","count":1},{"label":"Design Pattern","permalink":"/tags/design-pattern","count":1},{"label":"Tests","permalink":"/tags/tests","count":1}]}

View File

@ -0,0 +1 @@
{"tag":{"label":"Back-end","permalink":"/tags/back-end","allTagsPath":"/tags","count":4,"items":[{"id":"titres-professionnels/DWWM/AT2/CP5","title":"CP 5","description":"Synthèse de la CP 5 \"Mettre en place une base de données relationnelle\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT2/CP5"},{"id":"titres-professionnels/DWWM/AT2/CP6","title":"CP 6","description":"Synthèse de la CP 6 \"Développer des composants d'accès aux données SQL et NoSQL\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT2/CP6"},{"id":"titres-professionnels/DWWM/AT2/CP7","title":"CP 7","description":"Synthèse de la CP 7 \"Développer des composants métier coté serveur\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT2/CP7"},{"id":"titres-professionnels/DWWM/AT2/CP8","title":"CP 8","description":"Synthèse de la CP 8 \"Documenter le déploiement d'une application dynamique web ou web mobile\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT2/CP8"}],"unlisted":false}}

View File

@ -0,0 +1 @@
{"tag":{"label":"Base de données","permalink":"/tags/base-de-donnees","allTagsPath":"/tags","count":5,"items":[{"id":"titres-professionnels/DWWM/AT2/CP5","title":"CP 5","description":"Synthèse de la CP 5 \"Mettre en place une base de données relationnelle\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT2/CP5"},{"id":"titres-professionnels/DWWM/AT2/CP6","title":"CP 6","description":"Synthèse de la CP 6 \"Développer des composants d'accès aux données SQL et NoSQL\" du titre professionnel Développeur Web et Web Mobile (DWWM TP-01280m04).","permalink":"/titres-professionnels/DWWM/AT2/CP6"},{"id":"ressources/bases-de-donnees/modelisation/dictionnaire-de-donnees","title":"Création du MCD","description":"Tout en respectant la méthode Merise, on va voir comment créer un Modèle Conceptuel de Données (MCD) pour une base de données.","permalink":"/ressources/bases-de-donnees/modelisation/dictionnaire-de-donnees"},{"id":"ressources/bases-de-donnees/modelisation/mcd","title":"Création du MCD","description":"Tout en respectant la méthode Merise, on va voir comment créer un Modèle Conceptuel de Données (MCD) pour une base de données.","permalink":"/ressources/bases-de-donnees/modelisation/mcd"},{"id":"ressources/bases-de-donnees/modelisation/merise","title":"Modélisation avec Merise","description":"Voyons ensemble comment on peut modéliser une base de données avec Merise !","permalink":"/ressources/bases-de-donnees/modelisation/merise"}],"unlisted":false}}

View File

@ -0,0 +1 @@
{"tag":{"label":"Bibliothèque","permalink":"/tags/bibliotheque","allTagsPath":"/tags","count":8,"items":[{"id":"ressources/frontend/react/initialiser-un-projet-react","title":"Initialisation","description":"Initialisons un nouveau projet React, avec ou sans TypeScript.","permalink":"/ressources/frontend/react/initialiser-un-projet-react"},{"id":"ressources/frontend/react/intro","title":"Introduction à React","description":"Parlons un peu de React, ce qu'il est, ce qu'il fait et pourquoi il est si populaire.","permalink":"/ressources/frontend/react/intro"},{"id":"ressources/frontend/react/contextes","title":"Le hook useContext de React","description":"Découvrez comment utiliser le hook useContext de React pour gérer les contextes dans vos applications.","permalink":"/ressources/frontend/react/contextes"},{"id":"ressources/frontend/react/reducers","title":"Le hook useReducer de React","description":"Découvrez comment utiliser le hook useReducer de React pour gérer l'état de vos composants de manière plus efficace.","permalink":"/ressources/frontend/react/reducers"},{"id":"ressources/frontend/react/hooks","title":"Les hooks de React","description":"Découvre les hooks de React, une fonctionnalité qui te permet de gérer le state et le cycle de vie de tes composants fonctionnels.","permalink":"/ressources/frontend/react/hooks"},{"id":"ressources/frontend/react/premier-composant","title":"Premier composant","description":"Voyons ensemble comment notre premier composant React !","permalink":"/ressources/frontend/react/premier-composant"},{"id":"ressources/frontend/react/state-et-cycle-de-vie","title":"State et cycle de vie","description":"Voyons ensemble comment gérer le state et le cycle de vie d'un composant React !","permalink":"/ressources/frontend/react/state-et-cycle-de-vie"},{"id":"ressources/frontend/react/syntaxe-jsx","title":"Syntaxe JSX","description":"Découvrons la syntaxe JSX, un langage de balisage utilisé par React pour décrire l'interface utilisateur.","permalink":"/ressources/frontend/react/syntaxe-jsx"}],"unlisted":false}}

View File

@ -0,0 +1 @@
{"tag":{"label":"CDA","permalink":"/tags/cda","allTagsPath":"/tags","count":14,"items":[{"id":"titres-professionnels/CDA/AT1/CP1","title":"CP 1","description":"Synthèse de la CP 1 \"Installer et configurer son environnement de travail en fonction du projet\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT1/CP1"},{"id":"titres-professionnels/CDA/AT3/CP10","title":"CP 10","description":"Synthèse de la CP 10 \"Préparer et documenter le déploiement dune application\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT3/CP10"},{"id":"titres-professionnels/CDA/AT3/CP11","title":"CP 11","description":"Synthèse de la CP 11 \"Contribuer à la mise en production dans une démarche DevOps\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT3/CP11"},{"id":"titres-professionnels/CDA/AT1/CP2","title":"CP 2","description":"Synthèse de la CP 2 \"Développer des interfaces utilisateur\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT1/CP2"},{"id":"titres-professionnels/CDA/AT1/CP3","title":"CP 3","description":"Synthèse de la CP 3 \"Développer des composants métier\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT1/CP3"},{"id":"titres-professionnels/CDA/AT1/CP4","title":"CP 4","description":"Synthèse de la CP 4 \"Contribuer à la gestion dun projet informatique\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT1/CP4"},{"id":"titres-professionnels/CDA/AT2/CP5","title":"CP 5","description":"Synthèse de la CP 5 \"Analyser les besoins et maquetter une application\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT2/CP5"},{"id":"titres-professionnels/CDA/AT2/CP6","title":"CP 6","description":"Synthèse de la CP 6 \"Définir larchitecture logicielle dune application\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT2/CP6"},{"id":"titres-professionnels/CDA/AT2/CP7","title":"CP 7","description":"Synthèse de la CP 7 \"Concevoir et mettre en place une base de données relationnelle\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT2/CP7"},{"id":"titres-professionnels/CDA/AT2/CP8","title":"CP 8","description":"Synthèse de la CP 8 \"Développer des composants daccès aux données SQL et NoSQL\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT2/CP8"},{"id":"titres-professionnels/CDA/AT3/CP9","title":"CP 9","description":"Synthèse de la CP 9 \"Préparer et exécuter les plans de tests dune application\" du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04).","permalink":"/titres-professionnels/CDA/AT3/CP9"},{"id":"ressources/bases-de-donnees/modelisation/dictionnaire-de-donnees","title":"Création du MCD","description":"Tout en respectant la méthode Merise, on va voir comment créer un Modèle Conceptuel de Données (MCD) pour une base de données.","permalink":"/ressources/bases-de-donnees/modelisation/dictionnaire-de-donnees"},{"id":"ressources/bases-de-donnees/modelisation/mcd","title":"Création du MCD","description":"Tout en respectant la méthode Merise, on va voir comment créer un Modèle Conceptuel de Données (MCD) pour une base de données.","permalink":"/ressources/bases-de-donnees/modelisation/mcd"},{"id":"titres-professionnels/CDA/intro","title":"Résumé","description":"Résumé du titre professionnel Concepteur Développeur d'Applications (CDA TP-01281m04, actif depuis décembre 2023).","permalink":"/titres-professionnels/CDA/intro"}],"unlisted":false}}

Some files were not shown because too many files have changed in this diff Show More