chore: new architecture and technos
This commit is contained in:
commit
85937c2213
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/.env
|
||||||
|
app/.pnpm-store
|
||||||
|
app/node_modules
|
||||||
7
app/.env
Normal file
7
app/.env
Normal 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
3
app/.prettierrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
53
app/README.md
Normal file
53
app/README.md
Normal 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
BIN
app/assets/logo.afdesign
Normal file
Binary file not shown.
BIN
app/assets/logo.afdesign~lock~
Normal file
BIN
app/assets/logo.afdesign~lock~
Normal file
Binary file not shown.
10
app/assets/logo.svg
Normal file
10
app/assets/logo.svg
Normal 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 |
3
app/components/common/Image.tsx
Normal file
3
app/components/common/Image.tsx
Normal 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" />;
|
||||||
|
}
|
||||||
13
app/components/common/Link.tsx
Normal file
13
app/components/common/Link.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
app/components/md/Tabs.tsx
Normal file
95
app/components/md/Tabs.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
app/components/syntax/Button.tsx
Normal file
23
app/components/syntax/Button.tsx
Normal 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} />
|
||||||
|
);
|
||||||
|
}
|
||||||
44
app/components/syntax/Callout.tsx
Normal file
44
app/components/syntax/Callout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
app/components/syntax/DocsHeader.tsx
Normal file
19
app/components/syntax/DocsHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
app/components/syntax/DocsLayout.tsx
Normal file
33
app/components/syntax/DocsLayout.tsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
app/components/syntax/Fence.tsx
Normal file
25
app/components/syntax/Fence.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
app/components/syntax/Hero.tsx
Normal file
132
app/components/syntax/Hero.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
app/components/syntax/HeroBackground.tsx
Normal file
121
app/components/syntax/HeroBackground.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
app/components/syntax/Icon.tsx
Normal file
72
app/components/syntax/Icon.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
54
app/components/syntax/Logo.tsx
Normal file
54
app/components/syntax/Logo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
app/components/syntax/MobileNavigation.tsx
Normal file
82
app/components/syntax/MobileNavigation.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
app/components/syntax/Navigation.tsx
Normal file
47
app/components/syntax/Navigation.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
app/components/syntax/PrevNextLinks.tsx
Normal file
64
app/components/syntax/PrevNextLinks.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
app/components/syntax/Prose.tsx
Normal file
33
app/components/syntax/Prose.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
app/components/syntax/QuickLinks.tsx
Normal file
34
app/components/syntax/QuickLinks.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
app/components/syntax/Search.telefunc.ts
Normal file
8
app/components/syntax/Search.telefunc.ts
Normal 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);
|
||||||
|
};
|
||||||
405
app/components/syntax/Search.tsx
Normal file
405
app/components/syntax/Search.tsx
Normal 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 “
|
||||||
|
<span className="break-words text-slate-900 dark:text-white">{query}</span>
|
||||||
|
”
|
||||||
|
</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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
app/components/syntax/TableOfContents.tsx
Normal file
113
app/components/syntax/TableOfContents.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
app/components/syntax/ThemeSelector.tsx
Normal file
107
app/components/syntax/ThemeSelector.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
app/components/syntax/icons/InstallationIcon.tsx
Normal file
39
app/components/syntax/icons/InstallationIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
app/components/syntax/icons/LightbulbIcon.tsx
Normal file
38
app/components/syntax/icons/LightbulbIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
app/components/syntax/icons/PluginsIcon.tsx
Normal file
47
app/components/syntax/icons/PluginsIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
app/components/syntax/icons/PresetsIcon.tsx
Normal file
35
app/components/syntax/icons/PresetsIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
app/components/syntax/icons/ThemingIcon.tsx
Normal file
51
app/components/syntax/icons/ThemingIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
app/components/syntax/icons/WarningIcon.tsx
Normal file
47
app/components/syntax/icons/WarningIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
app/data/docs/installation/page.md
Normal file
17
app/data/docs/installation/page.md
Normal 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.
|
||||||
74
app/data/docs/introduction-to-string-theory/page.md
Normal file
74
app/data/docs/introduction-to-string-theory/page.md
Normal 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
239
app/data/docs/react/page.md
Normal 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
17
app/database/todoItems.ts
Normal 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
65
app/eslint.config.js
Normal 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
68
app/fastify-entry.ts
Normal 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
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
BIN
app/images/blur-indigo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 219 KiB |
95
app/layouts/LayoutDefault.tsx
Normal file
95
app/layouts/LayoutDefault.tsx
Normal 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
47
app/layouts/prism.css
Normal 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
35
app/layouts/style.css
Normal 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
48
app/layouts/tailwind.css
Normal 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
13
app/lib/navigation.ts
Normal 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
128
app/lib/search.ts
Normal 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
92
app/lib/sections.ts
Normal 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
59
app/markdoc/nodes.ts
Normal 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
60
app/markdoc/tags.tsx
Normal 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
59
app/package.json
Normal 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
12
app/pages/+Head.tsx
Normal 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
24
app/pages/+config.ts
Normal 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;
|
||||||
6
app/pages/+onPageTransitionEnd.ts
Normal file
6
app/pages/+onPageTransitionEnd.ts
Normal 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");
|
||||||
|
};
|
||||||
6
app/pages/+onPageTransitionStart.ts
Normal file
6
app/pages/+onPageTransitionStart.ts
Normal 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");
|
||||||
|
};
|
||||||
32
app/pages/_error/+Page.tsx
Normal file
32
app/pages/_error/+Page.tsx
Normal 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
16
app/pages/docs/+Page.tsx
Normal 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
29
app/pages/docs/+data.ts
Normal 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
12
app/pages/docs/+route.ts
Normal 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 } };
|
||||||
|
}
|
||||||
7
app/pages/index/+Page.tsx
Normal file
7
app/pages/index/+Page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
app/pages/index/Counter.tsx
Normal file
17
app/pages/index/Counter.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
app/pages/star-wars/@id/+Page.tsx
Normal file
17
app/pages/star-wars/@id/+Page.tsx
Normal 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}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
app/pages/star-wars/@id/+data.ts
Normal file
32
app/pages/star-wars/@id/+data.ts
Normal 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;
|
||||||
|
}
|
||||||
22
app/pages/star-wars/index/+Page.tsx
Normal file
22
app/pages/star-wars/index/+Page.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
app/pages/star-wars/index/+data.ts
Normal file
32
app/pages/star-wars/index/+data.ts
Normal 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 };
|
||||||
|
});
|
||||||
|
}
|
||||||
10
app/pages/star-wars/types.ts
Normal file
10
app/pages/star-wars/types.ts
Normal 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
14
app/pages/todo/+Page.tsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
3
app/pages/todo/+config.ts
Normal file
3
app/pages/todo/+config.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const config = {
|
||||||
|
prerender: false,
|
||||||
|
};
|
||||||
11
app/pages/todo/+data.ts
Normal file
11
app/pages/todo/+data.ts
Normal 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 };
|
||||||
|
}
|
||||||
7
app/pages/todo/TodoList.telefunc.ts
Normal file
7
app/pages/todo/TodoList.telefunc.ts
Normal 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 });
|
||||||
|
}
|
||||||
52
app/pages/todo/TodoList.tsx
Normal file
52
app/pages/todo/TodoList.tsx
Normal 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
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
14
app/prisma/schema.prisma
Normal 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")
|
||||||
|
}
|
||||||
22
app/server/telefunc-handler.ts
Normal file
22
app/server/telefunc-handler.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
18
app/server/vike-handler.ts
Normal file
18
app/server/vike-handler.ts
Normal 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
132
app/services/DocsService.ts
Normal 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();
|
||||||
56
app/services/FlexSearchService.ts
Normal file
56
app/services/FlexSearchService.ts
Normal 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
24
app/tsconfig.json
Normal 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
1
app/types.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module "simple-functional-loader";
|
||||||
29
app/vite.config.ts
Normal file
29
app/vite.config.ts
Normal 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
14
compose.yml
Normal 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
|
||||||
5
old/.docusaurus/DONT-EDIT-THIS-FOLDER
Normal file
5
old/.docusaurus/DONT-EDIT-THIS-FOLDER
Normal 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.
|
||||||
6
old/.docusaurus/client-modules.js
Normal file
6
old/.docusaurus/client-modules.js
Normal 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"),
|
||||||
|
];
|
||||||
82
old/.docusaurus/codeTranslations.json
Normal file
82
old/.docusaurus/codeTranslations.json
Normal 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."
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "docusaurus-lunr-search",
|
||||||
|
"id": "default"
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "docusaurus-plugin-content-docs",
|
||||||
|
"id": "default"
|
||||||
|
}
|
||||||
@ -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"}}}}
|
||||||
@ -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"}}}}
|
||||||
@ -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"}}}}
|
||||||
@ -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
@ -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}}
|
||||||
@ -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}}
|
||||||
@ -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}]}
|
||||||
@ -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}}
|
||||||
@ -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}}
|
||||||
@ -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}}
|
||||||
@ -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 d’une 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 d’un 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 l’architecture logicielle d’une 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 d’accè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 d’une 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
Loading…
Reference in New Issue
Block a user