Compare commits

..

No commits in common. "64b36c816fc3da55b1219d15752e5cda43baad8d" and "77fb4348bad5b1c8e55bfdbdf55b32b4bb4552d7" have entirely different histories.

251 changed files with 25441 additions and 9045 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
**/node_modules/
**/.pnpm-store/
**/dist/
.git

View File

@ -1,2 +1,3 @@
PORT=5500 PORT=5500
HMR_PORT=5501 HMR_PORT=5501
NODE_ENV=development

View File

@ -41,7 +41,7 @@ jobs:
VPS_PATH: ${{ secrets.VPS_PATH }} VPS_PATH: ${{ secrets.VPS_PATH }}
VPS_PORT: ${{ secrets.VPS_PORT }} VPS_PORT: ${{ secrets.VPS_PORT }}
run: | run: |
ssh -i ~/.ssh/id_ed25519 -p $VPS_PORT $VPS_USER@$VPS_HOST "cd $VPS_PATH && COMPOSE_BAKE=true docker compose -f compose.prod.yml build --no-cache" ssh -i ~/.ssh/id_ed25519 -p $VPS_PORT $VPS_USER@$VPS_HOST "cd $VPS_PATH && docker compose -f compose-prod.yml build --no-cache"
echo "📦 The application have been builded on the VPS." echo "📦 The application have been builded on the VPS."
- name: Start the application - name: Start the application
@ -51,5 +51,5 @@ jobs:
VPS_PATH: ${{ secrets.VPS_PATH }} VPS_PATH: ${{ secrets.VPS_PATH }}
VPS_PORT: ${{ secrets.VPS_PORT }} VPS_PORT: ${{ secrets.VPS_PORT }}
run: | run: |
ssh -i ~/.ssh/id_ed25519 -p $VPS_PORT $VPS_USER@$VPS_HOST "cd $VPS_PATH && docker compose -f compose.prod.yml up -d" ssh -i ~/.ssh/id_ed25519 -p $VPS_PORT $VPS_USER@$VPS_HOST "cd $VPS_PATH && docker compose -f compose-prod.yml up -d"
echo "🚀 The application has been started on the VPS." echo "🚀 The application has been started on the VPS."

8
.gitignore vendored
View File

@ -1,12 +1,6 @@
/.env /.env
**/*~lock*
app-old/.pnpm-store
app-old/node_modules
app-old/dist
app-old/public/sitemap.xml
app/.pnpm-store app/.pnpm-store
app/node_modules app/node_modules
app/dist app/dist
app/public/sitemap.xml app/public/sitemap.xml
**/*~lock*

View File

@ -1,4 +0,0 @@
{
"biome.searchInPath": false,
"biome.lspBin": "app/node_modules/@biomejs/biome/bin/biome",
}

View File

@ -1,15 +0,0 @@
FROM oven/bun:alpine
WORKDIR /app
COPY ./app/package.json ./app/bun.lock ./
RUN bun install --frozen-lockfile
COPY ./app /app
RUN bun run build
EXPOSE 3000
CMD [ "bun", "run", "preview" ]

View File

@ -1,49 +0,0 @@
Generated with [vike.dev/new](https://vike.dev/new) ([version 429](https://www.npmjs.com/package/create-vike/v/0.0.429)) using this command:
```sh
bun create vike@latest --solid --tailwindcss --authjs --telefunc --fastify --google-analytics --eslint --prettier --biome
```
## Contents
* [`/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)
This app is ready to start. It's powered by [Vike](https://vike.dev) and [SolidJS](https://www.solidjs.com/guides/getting-started).
### `/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.

9
app/.env Executable file → Normal file
View File

@ -1,4 +1,7 @@
# Google Analytics # 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
# See the documentation https://support.google.com/analytics/answer/9304153?hl=en#zippy=%2Cweb # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
PUBLIC_ENV__GOOGLE_ANALYTICS="G-XXXXXXXXXX" # 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"

149
app/.gitignore vendored
View File

@ -1,149 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# firebase-admin service-account
firebase
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Cloudflare
.wrangler/
# Vercel
.vercel/
# Sentry Vite Plugin
.env.sentry-build-plugin
# aws-cdk
.cdk.staging
cdk.out
## Panda
styled-system
styled-system-studio

1
app/.prettierignore Normal file
View File

@ -0,0 +1 @@
data/**/*.md

0
app/.prettierrc Executable file → Normal file
View File

10
app/README.md Executable file → Normal file
View File

@ -1,11 +1,13 @@
Generated with [vike.dev/new](https://vike.dev/new) ([version 429](https://www.npmjs.com/package/create-vike/v/0.0.429)) using this command: 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 ```sh
bun create vike@latest --solid --tailwindcss --telefunc --fastify --google-analytics --eslint --prettier --biome pnpm create vike@latest --react --tailwindcss --telefunc --fastify --eslint --prettier
``` ```
## Contents ## Contents
* [React](#react)
* [`/pages/+config.ts`](#pagesconfigts) * [`/pages/+config.ts`](#pagesconfigts)
* [Routing](#routing) * [Routing](#routing)
* [`/pages/_error/+Page.jsx`](#pages_errorpagejsx) * [`/pages/_error/+Page.jsx`](#pages_errorpagejsx)
@ -13,7 +15,9 @@ bun create vike@latest --solid --tailwindcss --telefunc --fastify --google-analy
* [SSR](#ssr) * [SSR](#ssr)
* [HTML Streaming](#html-streaming) * [HTML Streaming](#html-streaming)
This app is ready to start. It's powered by [Vike](https://vike.dev) and [SolidJS](https://www.solidjs.com/guides/getting-started). ## React
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
### `/pages/+config.ts` ### `/pages/+config.ts`

View File

@ -1,11 +0,0 @@
import type { PageContext } from "vike/types";
export function buildPublicUrl(pageContext: PageContext, resource: string) {
const { baseUrl } = pageContext;
const url = new URL(
resource,
process.env.NODE_ENV === "production" ? "https://memento-dev.fr" : baseUrl,
).toString();
return url;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +0,0 @@
import type { JSX } from "solid-js";
import { Link } from "./Link";
import clsx from "clsx";
const variantStyles = {
primary:
"bg-violet-300 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:
"bg-slate-800 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",
ghost:
"bg-transparent font-medium text-slate-900 hover:bg-slate-100 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-300/50 active:bg-slate-200",
};
const sizeStyles = {
sm: "rounded-md py-1 px-2 text-xs",
md: "rounded-full py-2 px-4 text-sm",
lg: "rounded-full py-3 px-6 text-base",
};
type ButtonProps = {
variant?: keyof typeof variantStyles;
size?: keyof typeof sizeStyles;
className?: string;
} & (
| JSX.IntrinsicElements["button"]
| (JSX.IntrinsicElements["a"] & { href: string })
);
export function Button(props: ButtonProps) {
const className = clsx(
variantStyles[props.variant ?? "primary"],
sizeStyles[props.size ?? "md"],
"cursor-pointer",
props.className,
);
return "href" in props && props.href ? (
<Link
{...(props as JSX.IntrinsicElements["a"])}
class={className}
href={props.href}
/>
) : (
<button {...(props as JSX.IntrinsicElements["button"])} class={className} />
);
}

View File

@ -1,68 +0,0 @@
import type { JSX } from "solid-js";
import { Icon } from "./Icon";
import clsx from "clsx";
const styles = {
note: {
container: "bg-violet-50",
title: "text-violet-900",
body: "text-slate-800 [--tw-prose-background:var(--color-slate-50)] prose-a:text-slate-900 prose-code:text-slate-900",
},
warning: {
container: "bg-amber-50",
title: "text-amber-900",
body: "text-slate-800 [--tw-prose-underline:var(--color-slate-400)] [--tw-prose-background:var(--color-slate-50)] prose-a:text-slate-900 prose-code:text-slate-900",
},
question: {
container: "bg-amber-50",
title: "text-amber-900",
body: "text-slate-800 [--tw-prose-underline:var(--color-slate-400)] [--tw-prose-background:var(--color-slate-50)] prose-a:text-slate-900 prose-code:text-slate-900",
},
};
const icons = {
note: (props: { class?: string }) => <Icon icon="lightbulb" {...props} />,
warning: (props: { class?: string }) => (
<Icon icon="warning" color="amber" {...props} />
),
question: (props: { class?: string }) => (
<Icon icon="question" color="blue" {...props} />
),
};
export default function Callout(props: {
title: string;
children: JSX.Element;
type?: keyof typeof styles;
collapsible?: boolean;
}) {
const IconComponent = icons[props.type || "note"];
return (
<div
class={clsx(
"my-8 flex flex-col rounded-3xl p-6",
styles[props.type || "note"].container,
{ "cursor-pointer": props.collapsible },
)}
>
<div class="flex items-center gap-6">
<IconComponent class="h-8 w-8 flex-none" />
<p
class={clsx(
"!m-0 font-display text-xl text-balance",
styles[props.type || "note"].title,
)}
>
{props.title}
</p>
</div>
<div class="mt-4 flex-auto">
<div class={clsx("prose mt-2.5", styles[props.type || "note"].body)}>
{props.children}
</div>
</div>
</div>
);
}

View File

@ -1,49 +0,0 @@
import clsx from "clsx";
type ToggleProps = {
id: string;
label: string;
onChange?: (checked: boolean) => void;
checked: boolean;
};
export function Toggle(props: ToggleProps) {
return (
<div class="flex items-center justify-center">
<input
type="checkbox"
id={props.id}
class="sr-only"
onChange={(e) => props.onChange?.(e.target.checked)}
checked={props.checked}
aria-checked={props.checked}
role="switch"
aria-label={props.label}
/>
<label
for={props.id}
class="flex cursor-pointer items-center justify-between rounded-full"
>
<span class="relative flex h-6 w-10 items-center">
<span
class={clsx(
"h-4 w-4 rounded-full bg-white shadow-md transition-transform duration-200 ease-in-out z-10",
props.checked
? "translate-x-[calc(100%+.25em)]"
: "translate-x-1",
)}
/>
<span
class={clsx(
"absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 rounded-full transition duration-200 ease-in-out z-0",
props.checked ? "bg-violet-500" : "bg-slate-300",
)}
/>
</span>
<span class="ml-2 text-sm text-slate-700">{props.label}</span>
</label>
</div>
);
}

View File

@ -1,117 +0,0 @@
import type { ComponentProps, ParentComponent } from "solid-js";
import {
createEffect,
createMemo,
For,
mergeProps,
on,
splitProps,
} from "solid-js";
import { clipboard } from "solid-heroicons/solid";
import { Icon } from "solid-heroicons";
import * as Prismjs from "prismjs";
import toast from "solid-toast";
import clsx from "clsx";
type Props = {
language: string;
class?: string;
dark?: boolean;
withLineNumbers?: boolean;
} & ComponentProps<"code">;
export const Highlight: ParentComponent<Props> = (_props) => {
const props = mergeProps({ language: "javascript" }, _props);
const [, rest] = splitProps(props, [
"language",
"children",
"class",
"innerHTML",
]);
const languageClass = createMemo(() => `language-${props.language}`);
const highlightedCode = createMemo<string | undefined>(() => {
const childrenString = props.children?.toString();
if (!childrenString) return;
const grammar = Prismjs.languages[props.language];
if (!grammar) return;
const result = Prismjs.highlight(childrenString, grammar, props.language);
return result;
});
createEffect(
on([languageClass, highlightedCode], () => {
Prismjs.highlightAll();
}),
);
const handleCopyToClipboard = () => {
if (props.innerHTML) {
navigator.clipboard.writeText(props.innerHTML);
} else if (props.children) {
navigator.clipboard.writeText(props.children.toString());
}
toast.success("Copié dans le presse-papier", {
duration: 2000,
position: "top-right",
});
};
return (
<div class={clsx("group flex items-start px-4 py-2 w-full", props.class)}>
<button
class="absolute cursor-pointer z-10 top-2 right-2 text-slate-500 bg-slate-200/10 rounded-md hover:bg-linear-to-r hover:from-violet-400/30 hover:via-violet-400 hover:to-violet-400/30 p-px hover:text-violet-300"
type="button"
onClick={handleCopyToClipboard}
>
<span
class={clsx(
props.dark ? "hover:bg-slate-800" : "hover:bg-white",
"p-2 block rounded-md",
)}
>
<span class="sr-only">Copier l'extrait de code</span>
<Icon path={clipboard} class="w-5 h-5" />
</span>
</button>
{props.withLineNumbers && props.children?.toString() && (
<div
aria-hidden="true"
class="border-r leading-6 border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
>
<For
each={Array.from({
length: props.children.toString().split("\n").length,
})}
>
{(_, index) => (
<>
{(index() + 1).toString().padStart(2, "0")}
<br />
</>
)}
</For>
</div>
)}
<pre
class={clsx("not-prose h-full w-full prism-code flex", languageClass())}
>
<code
class={clsx("leading-6", props.withLineNumbers ? "px-4" : "pr-4")}
innerHTML={highlightedCode()}
{...rest}
>
{props.children}
</code>
</pre>
</div>
);
};

View File

@ -1,84 +0,0 @@
import type { JSX } from "solid-js";
import { InstallationIcon } from "@/icons/InstallationIcon";
import { LightbulbIcon } from "@/icons/LightbulbIcon";
import { QuestionIcon } from "@/icons/QuestionIcon";
import { PluginsIcon } from "@/icons/PluginsIcon";
import { PresetsIcon } from "@/icons/PresetsIcon";
import { ThemingIcon } from "@/icons/ThemingIcon";
import { WarningIcon } from "@/icons/WarningIcon";
import { useId } from "@/hooks/useId";
import { For } from "solid-js";
import clsx from "clsx";
const icons = {
installation: InstallationIcon,
presets: PresetsIcon,
plugins: PluginsIcon,
theming: ThemingIcon,
lightbulb: LightbulbIcon,
warning: WarningIcon,
question: QuestionIcon,
};
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 type IconColor = keyof typeof iconStyles;
export type IconProps = JSX.IntrinsicElements["svg"] & {
color?: IconColor;
icon: keyof typeof icons;
};
export function Icon(props: IconProps) {
const id = useId();
const IconComponent = icons[props.icon];
return (
<svg
{...props}
aria-hidden="true"
viewBox="0 0 32 32"
fill="none"
class={clsx(props.class, iconStyles[props.color || "blue"])}
>
<IconComponent id={id} color={props.color || "blue"} />
</svg>
);
}
const gradients = {
blue: [
{ "stop-color": "#0EA5E9" },
{ "stop-color": "#22D3EE", offset: ".527" },
{ "stop-color": "#818CF8", offset: 1 },
],
amber: [
{ "stop-color": "#FDE68A", offset: ".08" },
{ "stop-color": "#F59E0B", offset: ".837" },
],
};
type GradientProps = JSX.IntrinsicElements["radialGradient"] & {
color?: keyof typeof gradients;
};
export function Gradient(props: GradientProps) {
return (
<radialGradient
cx={0}
cy={0}
r={1}
gradientUnits="userSpaceOnUse"
{...props}
>
<For each={gradients[props.color || "blue"]}>
{(stop) => <stop {...stop} />}
</For>
</radialGradient>
);
}

View File

@ -1,23 +0,0 @@
import clsx from "clsx";
type IframeProps = {
src: string;
title: string;
width?: string;
height?: string;
class?: string;
};
export default function Iframe(props: IframeProps) {
return (
<div class={clsx("max-w-full pointer-events-none w-full")}>
<iframe
src={props.src}
width={props.width}
height={props.height}
title={props.title}
class={props.class}
/>
</div>
);
}

View File

@ -1,17 +0,0 @@
import type { JSX } from "solid-js";
type ImageProps = JSX.IntrinsicElements["img"] & { src: string; alt: string };
export default function Image(props: ImageProps) {
const isDecorationImage = props.alt === "";
return (
<img
{...props}
src={props.src}
aria-hidden={isDecorationImage ? "true" : undefined}
alt={isDecorationImage ? undefined : props.alt}
loading="lazy"
/>
);
}

View File

@ -1,37 +0,0 @@
import type { JSX } from "solid-js";
import { usePageContext } from "vike-solid/usePageContext";
type LinkProps = JSX.IntrinsicElements["a"] & { href: string };
export function Link(props: LinkProps) {
const { urlPathname } = usePageContext();
const isActive =
props.href === "/"
? urlPathname === props.href
: urlPathname.startsWith(props.href);
const isSameDomain = !(
props.href.startsWith("http") || props.href.startsWith("mailto")
);
const downloadExtensions = [".pdf", ".zip"];
const isDownload = downloadExtensions.some((extension) =>
props.href.endsWith(extension),
);
return (
<a
{...props}
{...(isActive && { "aria-current": "page" })}
{...(isDownload && { download: true })}
{...(!isSameDomain || isDownload
? { target: "_blank", rel: "noopener noreferrer" }
: { target: "_self" })}
>
{props.children}
</a>
);
}

View File

@ -1,42 +0,0 @@
import type { JSX } from "solid-js";
import { useId } from "@/hooks/useId";
function LogomarkPaths() {
const id = useId();
return (
<>
<defs>
<linearGradient
id={id}
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" stop-color="rgb(43,127,255)" />
<stop offset="1" stop-color="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(#${id})` }}
/>
</g>
</>
);
}
export function Logo(props: JSX.IntrinsicElements["svg"]) {
return (
<svg viewBox="0 0 58 38" {...props}>
<title>Memento Dev</title>
<LogomarkPaths />
</svg>
);
}

View File

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

View File

@ -1,32 +0,0 @@
import type { JSX } from "solid-js";
import { Dynamic } from "solid-js/web";
import clsx from "clsx";
type ProseProps = JSX.IntrinsicElements["div"] & {
class?: string;
as?: keyof JSX.IntrinsicElements;
};
export function Prose(props: ProseProps) {
const Component = props.as ?? "div";
return (
<Dynamic
component={Component}
class={clsx(
props.class,
"prose max-w-none prose-slate",
// 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",
// links
"prose-a:font-semibold",
// link underline
"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]",
)}
{...props}
/>
);
}

View File

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

View File

@ -1,351 +0,0 @@
import type { SearchResult } from "@/services/FlexSearchService";
import type { JSX, Accessor, Setter } from "solid-js";
import {
createContext,
useContext,
For,
createEffect,
createSignal,
} from "solid-js";
import { Highlighter } from "solid-highlight-words";
import { useDebounce } from "@/hooks/useDebounce";
import { Dialog, DialogPanel } from "terracotta";
import { navigation } from "@/libs/navigation";
import { navigate } from "vike/client/router";
import { useId } from "@/hooks/useId";
import clsx from "clsx";
const SearchContext = createContext<{
query: Accessor<string>;
close: () => void;
results: Accessor<SearchResult[]>;
isLoading: Accessor<boolean>;
isOpened: Accessor<boolean>;
setQuery: Setter<string>;
setIsOpened: Setter<boolean>;
setIsLoading: Setter<boolean>;
setResults: Setter<SearchResult[]>;
}>({
query: () => "",
close: () => {},
results: () => [],
isLoading: () => false,
isOpened: () => false,
setQuery: () => {},
setIsOpened: () => {},
setIsLoading: () => {},
setResults: () => {},
});
function SearchIcon(props: JSX.IntrinsicElements["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 LoadingIcon(props: JSX.IntrinsicElements["svg"]) {
const id = useId();
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="5.5" stroke-linejoin="round" />
<path
stroke={`url(#${id})`}
stroke-linecap="round"
stroke-linejoin="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 stop-color="currentColor" />
<stop offset="1" stop-color="currentColor" stop-opacity="0" />
</linearGradient>
</defs>
</svg>
);
}
function SearchInput() {
const { close, setQuery, query, isLoading } = useContext(SearchContext);
return (
<div class="group relative flex h-12">
<SearchIcon class="pointer-events-none absolute top-0 left-4 h-full w-5 fill-slate-400" />
<input
data-autofocus
class={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 [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden",
isLoading() ? "pr-11" : "pr-4",
)}
onKeyDown={(event) => {
if (event.key === "Escape") {
// 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();
}
close();
}
}}
value={query()}
onInput={(event) => {
const { value } = event.currentTarget;
setQuery(value);
}}
/>
{isLoading() && (
<div class="absolute inset-y-0 right-3 flex items-center">
<LoadingIcon class="h-6 w-6 animate-spin stroke-slate-200 text-slate-400" />
</div>
)}
</div>
);
}
function HighlightQuery(props: { text: string; query: string }) {
return (
<Highlighter
highlightClass="group-aria-selected:underline bg-transparent text-violet-600"
searchWords={[props.query]}
autoEscape={true}
textToHighlight={props.text}
/>
);
}
function SearchResultItem(props: { result: SearchResult; query: string }) {
const { close } = useContext(SearchContext);
const id = useId();
const getHierarchy = (): string[] => {
const sectionTitle = navigation.find((section) => {
return section.links.find(
(link) => link.href === props.result.url.split("#")[0],
);
})?.title;
return [sectionTitle, props.result.pageTitle].filter(
(x): x is string => typeof x === "string",
);
};
return (
<li
class="group block cursor-default rounded-lg px-3 py-2 aria-selected:bg-slate-100 hover:bg-slate-100"
aria-labelledby={`${id}-hierarchy ${id}-title`}
tab-index={0}
onKeyDown={(event) => {
if (event.key === "Enter") {
navigate(props.result.url);
close();
}
}}
onClick={() => {
navigate(props.result.url);
close();
}}
>
<div
id={`${id}-title`}
aria-hidden="true"
class="text-sm text-slate-700 group-aria-selected:text-violet-600"
>
<HighlightQuery text={props.result.title} query={props.query} />
</div>
{getHierarchy().length > 0 && (
<div
id={`${id}-hierarchy`}
aria-hidden="true"
class="mt-0.5 truncate text-xs whitespace-nowrap text-slate-500 dark:text-slate-400"
>
<For each={getHierarchy()}>
{(item, itemIndex) => (
<>
<HighlightQuery text={item} query={props.query} />
<span
class={
itemIndex() === getHierarchy().length - 1
? "sr-only"
: "mx-2 text-slate-300 dark:text-slate-700"
}
>
/
</span>
</>
)}
</For>
</div>
)}
</li>
);
}
function SearchResults() {
const { results, query } = useContext(SearchContext);
if (results().length === 0) {
return (
<p class="px-4 py-8 text-center text-sm text-slate-700">
Aucun résultat pour &ldquo;
<span class="break-words text-slate-900">{query()}</span>
&rdquo;
</p>
);
}
return (
<ul>
<For each={results()}>
{(result) => (
<li>
<SearchResultItem result={result} query={query()} />
</li>
)}
</For>
</ul>
);
}
function SearchDialog(props: { class?: string }) {
const { close, isOpened, setIsOpened, results } = useContext(SearchContext);
createEffect(() => {
if (isOpened()) return;
function onKeyDown(event: KeyboardEvent) {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setIsOpened(true);
}
}
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isOpened, setIsOpened]);
const handleClickOutside = (event: MouseEvent) => {
const { target, currentTarget } = event;
if (target instanceof Node && currentTarget instanceof Node) {
if (target === currentTarget) close();
}
};
return (
<>
<Dialog
isOpen={isOpened()}
onClose={close}
class={clsx("fixed inset-0 z-50", props.class)}
>
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" />
<div
class="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]"
onClick={handleClickOutside}
onKeyDown={(event) => {
if (event.key === "Escape") close();
}}
>
<DialogPanel class="mx-auto transform-gpu overflow-hidden rounded-xl bg-white shadow-xl sm:max-w-xl">
<form onSubmit={(event) => event.preventDefault()}>
<SearchInput />
<div class="border-t border-slate-200 bg-white px-2 py-3 empty:hidden">
{results().length > 0 && <SearchResults />}
</div>
</form>
</DialogPanel>
</div>
</Dialog>
</>
);
}
export function Search() {
const [results, setResults] = createSignal<SearchResult[]>([]);
const [modifierKey, setModifierKey] = createSignal<string>();
const [isLoading, setIsLoading] = createSignal(false);
const [isOpened, setIsOpened] = createSignal(false);
const [query, setQuery] = createSignal("");
const debouncedQuery = useDebounce(query, 300);
const onSearch = async (query: string) => {
const response = await fetch(`/search?query=${query}`);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
};
createEffect(() => {
const platform = navigator.userAgentData?.platform || navigator.platform;
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(platform) ? "⌘" : "Ctrl ");
}, []);
createEffect(() => {
const query = debouncedQuery();
if (query.length === 0) {
setIsLoading(false);
setResults([]);
return;
}
setIsLoading(true);
onSearch(query)
.then(setResults)
.finally(() => setIsLoading(false));
});
return (
<SearchContext.Provider
value={{
query,
close: () => setIsOpened(false),
results,
isLoading,
isOpened,
setQuery,
setIsOpened,
setIsLoading,
setResults,
}}
>
<button
type="button"
class="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"
onClick={() => setIsOpened(true)}
>
<SearchIcon class="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 md:group-hover:fill-slate-400" />
<span class="sr-only md:not-sr-only md:ml-2 md:text-slate-500">
Rechercher...
</span>
{modifierKey && (
<kbd class="ml-auto hidden font-medium text-slate-400 md:block">
<kbd class="font-sans">{modifierKey()}</kbd>
<kbd class="font-sans">K</kbd>
</kbd>
)}
</button>
<SearchDialog />
</SearchContext.Provider>
);
}

View File

@ -1,114 +0,0 @@
import type { JSX } from "solid-js";
import { createSignal, onCleanup } from "solid-js";
type SmoothScrollProps = JSX.IntrinsicElements["div"] & {
children: JSX.Element;
};
export function SmoothScroll(props: SmoothScrollProps) {
const [isScrolling, setIsScrolling] = createSignal(false);
let animationFrameId: number | null = null;
const easeOutQuad = (t: number, b: number, c: number, d: number) => {
const time = t / d;
return -c * time * (time - 2) + b;
};
const smoothScroll = (deltaY: number) => {
const scrollSpeed = 3;
const currentScroll = window.scrollY;
const targetScroll = deltaY * scrollSpeed;
const duration = 300;
const startTime = performance.now();
const animateScroll = (currentTime: number) => {
const elapsedTime = currentTime - startTime;
const ease = easeOutQuad(
elapsedTime,
currentScroll,
targetScroll,
duration,
);
window.scrollTo(0, ease);
if (elapsedTime < duration) {
animationFrameId = requestAnimationFrame(animateScroll);
} else {
setIsScrolling(false);
}
};
animationFrameId = requestAnimationFrame(animateScroll);
};
const isMobile = () => {
const regex =
/Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
return regex.test(navigator.userAgent);
};
const isElementScrollable = (element: HTMLElement) => {
if (!element) return false;
return (
element.scrollHeight > element.clientHeight && element.tagName !== "HTML"
);
};
const findScrollableParent = (element: HTMLElement) => {
let currentElement: HTMLElement | null = element;
while (currentElement) {
if (isElementScrollable(currentElement)) {
return currentElement;
}
currentElement = currentElement.parentElement;
}
return null;
};
const handleWheel = (event: WheelEvent) => {
if (isMobile()) return;
const hoveredElement = document.elementFromPoint(
event.clientX,
event.clientY,
) as HTMLElement;
if (findScrollableParent(hoveredElement)) {
if (animationFrameId !== null) cancelAnimationFrame(animationFrameId);
return;
}
event.preventDefault();
event.stopPropagation();
if (isScrolling()) {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
}
}
requestAnimationFrame(() => {
smoothScroll(event.deltaY);
setIsScrolling(false);
});
setIsScrolling(true);
};
onCleanup(() => {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId);
}
});
return (
<div {...props} onWheel={handleWheel} style={{ "touch-action": "auto" }}>
{props.children}
</div>
);
}

View File

@ -1,156 +0,0 @@
import type { JSX } from "solid-js";
import { For, createSignal } from "solid-js";
import { Highlight } from "./Highlight";
import clsx from "clsx";
function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
<circle cx="5" cy="5" r="4.5" class="fill-red-400" />
<circle cx="21" cy="5" r="4.5" class="fill-amber-300" />
<circle cx="37" cy="5" r="4.5" class="fill-green-500" />
</svg>
);
}
type SnippetTab = {
name: string;
codeLanguage: string;
code: string;
withLineNumbers?: boolean;
children?: never;
};
type CommonTab = {
name: string;
children: JSX.Element;
codeLanguage?: never;
code?: never;
withLineNumbers?: never;
};
type SnippetProps = {
children?: JSX.Element;
class?: string;
snippets: (SnippetTab | CommonTab)[];
dark?: boolean;
};
export function Snippet(props: SnippetProps) {
let tabs: HTMLDivElement | undefined;
let nav: HTMLDivElement | undefined;
const [selectedTab, setSelectedTab] = createSignal<SnippetTab | CommonTab>(
props.snippets[0],
);
const isActive = (tab: SnippetTab | CommonTab) => {
return selectedTab()?.name === tab.name;
};
const selectTab = (name: string) => {
const tab = props.snippets.find((tab) => tab.name === name);
if (tab) setSelectedTab(tab);
if (!tabs || !nav) return;
const navWidth = nav.offsetWidth || 0;
const tabsWidth = tabs.scrollWidth;
if (tabsWidth > navWidth) {
const tabElement: HTMLDivElement | null = tabs.querySelector(
`div[data-tab="${name}"]`,
);
if (!tabElement) return;
const tabOffsetLeft = tabElement.offsetLeft;
const tabWidth = tabElement.offsetWidth;
const scrollLeft = Math.max(
0,
tabOffsetLeft - navWidth / 2 + tabWidth / 2,
);
nav.scrollTo({ left: scrollLeft, behavior: "smooth" });
}
};
const canBeSelected = (tab: SnippetTab | CommonTab) => {
return (tab.code || tab.children) !== undefined;
};
return (
<div
class={clsx(
"relative rounded-2xl ring-1 ring-white/10 backdrop-blur-sm",
props.dark ? "bg-[#0A101F]/80" : "bg-slate-50",
props.class,
)}
>
<div class="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 class="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 class="pt-4 pl-4">
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
<nav ref={nav} class="overflow-x-auto">
<div ref={tabs} class="mt-4 flex space-x-2 text-xs w-max mb-2">
<For each={props.snippets}>
{(tab) => (
<div
data-tab={tab.name}
class={clsx(
"flex h-6 rounded-full",
{ "cursor-pointer": canBeSelected(tab) && !isActive(tab) },
isActive(tab)
? clsx(
"bg-linear-to-r from-violet-400/30 via-violet-400 to-violet-400/30 p-px font-medium",
props.dark ? "text-violet-300" : "text-violet-600",
)
: props.dark
? "text-slate-400"
: "text-slate-500",
)}
>
<button
type="button"
class={clsx(
"flex items-center rounded-full px-2.5",
isActive(tab) && {
"bg-slate-800": props.dark,
"bg-violet-100": !props.dark,
},
)}
disabled={!canBeSelected(tab)}
onClick={() => selectTab(tab.name)}
>
{tab.name}
</button>
</div>
)}
</For>
</div>
</nav>
{selectedTab() && (
<div class="mt-6">
{selectedTab().code && (
<Highlight
class={clsx(
"!pt-0 !px-1 max-h-96 overflow-auto mb-2",
props.dark && "dark text-white",
)}
language={(selectedTab() as SnippetTab).codeLanguage}
withLineNumbers={(selectedTab() as SnippetTab).withLineNumbers}
>
{(selectedTab() as SnippetTab).code}
</Highlight>
)}
{!selectedTab().code && (
<div class="pb-1">{(selectedTab() as CommonTab).children}</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@ -1,131 +0,0 @@
import type { JSX, Accessor, Setter } from "solid-js";
import {
createContext,
useContext,
createSignal,
onMount,
For,
} from "solid-js";
import { Button } from "@/components/Button";
import clsx from "clsx";
type TabType = {
label: string;
value: string;
};
type TabsContextType = {
selectedTab: Accessor<string>;
setSelectedTab: Setter<string>;
tabs: Accessor<TabType[]>;
addTab: (tab: TabType) => void;
};
const TabsContext = createContext<TabsContextType>({
selectedTab: () => "",
setSelectedTab: () => {},
tabs: () => [],
addTab: () => {},
});
export default function Tabs(props: {
defaultSelectedTab?: string;
children: JSX.Element;
}) {
const [selectedTab, setSelectedTab] = createSignal(
props.defaultSelectedTab || "",
);
const [tabs, setTabs] = createSignal<TabType[]>([]);
const addTab = (tab: TabType) => {
console.log("Adding tab", tab);
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,
setSelectedTab,
tabs,
addTab,
}}
>
<div class="relative">
<div class="max-w-full overflow-x-auto overflow-y-hidden">
<ul
class="!p-0 w-max flex items-stretch gap-1 !m-0"
aria-orientation="horizontal"
role="tablist"
>
<For each={tabs()}>
{(tab) => (
<li class="overflow-hidden">
<TabItem
tab={tab}
isSelected={selectedTab() === tab.value}
select={() => setSelectedTab(tab.value)}
/>
</li>
)}
</For>
</ul>
</div>
<div class="-mt-1 p-2">{props.children}</div>
</div>
</TabsContext.Provider>
);
}
function TabItem(props: {
tab: TabType;
isSelected: boolean;
select: () => void;
}) {
return (
<Button
variant={props.isSelected ? "primary" : "secondary"}
class={clsx("!rounded-md", props.isSelected && "cursor-default")}
onClick={props.select}
>
{props.tab.label}
</Button>
);
}
Tabs.Item = (props: {
label: string;
value: string;
children: JSX.Element;
}) => {
const tabsContext = useContext(TabsContext);
if (!tabsContext) {
throw new Error("Tabs.Item must be used within Tabs");
}
onMount(() => {
console.log("Mounting tab", props.label);
tabsContext.addTab({ label: props.label, value: props.value });
});
return (
<div
class={clsx(
"first:!mt-0",
"last:!mb-0",
tabsContext.selectedTab() !== props.value && "hidden",
)}
>
{props.children}
</div>
);
};

View File

@ -0,0 +1,25 @@
import type { PageContext } from "vike/types";
import { getTelefuncContext } from "@/lib/getTelefuncContext";
import { CookieParser } from "@/services/CookieParser";
export type ConsentCookies = keyof PageContext["cookies"]["consent"];
export async function onUpdateConsentCookie(cookieName: ConsentCookies, cookieValue: boolean) {
const context = getTelefuncContext();
const { reply } = context;
CookieParser.set(reply, cookieName, cookieValue.toString(), 365);
return { ok: true, message: "Updated consent cookie", cookieName, cookieValue };
}
export async function onSetAllConsentCookie(cookieValue: boolean) {
const context = getTelefuncContext();
const { reply } = context;
CookieParser.set(reply, "analytics", cookieValue.toString(), 365);
CookieParser.set(reply, "customization", cookieValue.toString(), 365);
return { ok: true, message: "Updated consents cookies" };
}

View File

@ -0,0 +1,192 @@
import { onUpdateConsentCookie, onSetAllConsentCookie, type ConsentCookies } from "./Cookies.telefunc";
import React, { useState, useContext, createContext, useMemo } from "react";
import { usePageContext } from "vike-react/usePageContext";
import { reload } from "vike/client/router";
import { Button } from "@syntax/Button";
import { toast } from "react-toastify";
import { Toggle } from "./Toggle";
import { Link } from "./Link";
export const CookiesContext = createContext<{
cookies: {
analytics: boolean;
customization: boolean;
};
setCookie: (cookieName: ConsentCookies, cookieValue: boolean) => void;
setAllCookies: (cookieValue: boolean) => void;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
isSelectionOpen: boolean;
setIsSelectionOpen: (isSelectionOpen: boolean) => void;
}>({
cookies: {
analytics: false,
customization: false,
},
setCookie: (_cookieName: ConsentCookies, _cookieValue: boolean) => {},
setAllCookies: () => {},
isOpen: false,
setIsOpen: () => {},
isSelectionOpen: false,
setIsSelectionOpen: () => {},
});
type CookiesContainerProps = {
children?: React.ReactNode;
};
export function CookiesContainer(props: CookiesContainerProps) {
const { cookies } = usePageContext();
const [consentCookies, setConsentCookies] = useState(cookies.consent);
const [isSelectionOpen, setIsSelectionOpen] = useState(false);
const [isOpen, setIsOpen] = useState(() => {
return !Object.keys(cookies.consent).every((value) => value);
});
const toastPromiseMessages = useMemo(
() => ({
pending: "Mise à jour des cookies...",
success: "Cookies mis à jour !",
error: "Erreur lors de la mise à jour des cookies",
}),
[],
);
const handleUpdateCookie = (cookieName: ConsentCookies, cookieValue: boolean) => {
setConsentCookies((prev) => ({
...prev,
[cookieName]: cookieValue,
}));
toast.promise(onUpdateConsentCookie(cookieName, cookieValue), toastPromiseMessages).then(() => {
setIsOpen(false);
reload();
});
};
const handleSetAll = (value: boolean) => {
setConsentCookies({ analytics: true, customization: true });
toast.promise(onSetAllConsentCookie(value), toastPromiseMessages).then(() => {
setIsOpen(false);
setIsSelectionOpen(false);
reload();
});
};
return (
<CookiesContext.Provider
value={{
cookies: consentCookies,
setCookie: handleUpdateCookie,
setAllCookies: handleSetAll,
isOpen,
setIsOpen,
isSelectionOpen,
setIsSelectionOpen,
}}
>
{props.children}
{isSelectionOpen && <CookieChoices />}
{isOpen && <CookieModal />}
</CookiesContext.Provider>
);
}
function CookieChoices() {
const cookiesContext = useContext(CookiesContext);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm">
<div className="relative flex flex-col gap-2 bg-slate-50 dark:bg-slate-800 rounded-md shadow-xl w-full max-w-sm p-4">
<Button
variant="ghost"
size="sm"
className="absolute top-0 right-0"
onClick={() => cookiesContext.setIsSelectionOpen(false)}
>
Fermer
</Button>
<p className="font-display dark:text-slate-300 font-bold text-lg">Personnalisation des cookies 🍪</p>
<div className="flex flex-col gap-2 w-full items-start">
<Toggle
id="cookies-analytics"
label="Cookies d&lsquo;analyse (Umami et Google Analytics)"
checked={cookiesContext.cookies.analytics}
onChange={(checked) => cookiesContext.setCookie("analytics", checked)}
/>
<Toggle
id="cookies-customization"
label="Cookie de personnalisation (thème)"
checked={cookiesContext.cookies.customization}
onChange={(checked) => cookiesContext.setCookie("customization", checked)}
/>
</div>
</div>
</div>
);
}
function CookieModal() {
const cookiesContext = useContext(CookiesContext);
return (
<div className="flex flex-col fixed bottom-4 left-4 bg-slate-50 dark:bg-slate-800 z-50 rounded-md shadow-xl w-full max-w-sm overflow-hidden">
<Button
variant="ghost"
size="sm"
className="absolute top-0 right-0"
onClick={() => cookiesContext.setIsOpen(false)}
>
Fermer
</Button>
<div className="flex flex-col gap-2 p-4">
<p className="font-display dark:text-slate-300">
<span className="text-sm">Coucou c&apos;est nous...</span>
<br />
<span className="font-bold text-lg">les cookies ! 🍪</span>
</p>
<p className="text-slate-700 dark:text-slate-300">
On ne t&lsquo;embête pas longtemps, on te laisse même le choix <em>(si ça c&lsquo;est pas la classe 😎)</em>.
</p>
<p className="text-slate-700 dark:text-slate-300">
Si tu veux en savoir plus, tu peux consulter la page{" "}
<Link href="/politique-de-confidentialite" className="font-bold">
Politique de confidentialité
</Link>
.
</p>
</div>
<div className="grid items-center grid-cols-3 justify-between bg-slate-100 dark:bg-slate-700">
<button
className="cursor-pointer px-2 py-1 text-slate-600 dark:text-slate-300"
onClick={() => cookiesContext.setAllCookies(false)}
>
Non merci
</button>
<button
className="cursor-pointer px-2 py-1 text-slate-600 dark:text-slate-300"
onClick={() => cookiesContext.setIsSelectionOpen(true)}
>
Je choisis
</button>
<button
className="cursor-pointer px-2 py-1 font-bold text-white dark:text-black bg-violet-600 dark:bg-violet-300"
onClick={() => cookiesContext.setAllCookies(true)}
>
Oui, j&lsquo;ai faim !
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,20 @@
import React from "react";
import clsx from "clsx";
type IframeProps = {
src: string;
width?: string;
height?: string;
className?: string;
};
export function Iframe(props: IframeProps) {
return (
<iframe
src={props.src}
className={clsx("max-w-full pointer-events-none", props.className)}
width={props.width}
height={props.height}
/>
);
}

View File

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

View File

@ -0,0 +1,26 @@
import { usePageContext } from "vike-react/usePageContext";
import { prefetch } from "vike/client/router";
import React from "react";
import clsx from "clsx";
export function Link(props: React.AnchorHTMLAttributes<HTMLAnchorElement> & { href: string; className?: string }) {
const { urlPathname } = usePageContext();
const isActive = props.href === "/" ? urlPathname === props.href : urlPathname.startsWith(props.href);
const isSameDomain = !(props.href.startsWith("http") || props.href.startsWith("mailto"));
const isDownload = props.href.endsWith(".pdf") || props.href.endsWith(".zip");
const handleMouseEnter = () => prefetch(props.href);
return (
<a
{...props}
href={props.href}
className={clsx(isActive && "is-active", props.className)}
{...(isDownload ? { download: true } : {})}
{...(!isSameDomain || isDownload ? { target: "_blank", rel: "noopener noreferrer" } : {})}
{...(isSameDomain ? { onMouseEnter: handleMouseEnter } : {})}
>
{props.children}
</a>
);
}

View File

@ -0,0 +1,45 @@
import React from "react";
import clsx from "clsx";
type ToggleProps = {
id: string;
label: string;
onChange?: (checked: boolean) => void;
checked: boolean;
};
export function Toggle(props: ToggleProps) {
return (
<div className="flex items-center justify-center">
<input
type="checkbox"
id={props.id}
className="sr-only"
onChange={(e) => props.onChange?.(e.target.checked)}
checked={props.checked}
aria-checked={props.checked}
role="switch"
aria-label={props.label}
/>
<label htmlFor={props.id} className="flex cursor-pointer items-center justify-between rounded-full">
<span className="relative flex h-6 w-10 items-center">
<span
className={clsx(
"h-4 w-4 rounded-full bg-white shadow-md transition-transform duration-200 ease-in-out z-10",
props.checked ? "translate-x-[calc(100%+.25em)]" : "translate-x-1",
)}
/>
<span
className={clsx(
"absolute top-1/2 left-1/2 h-full w-full -translate-x-1/2 -translate-y-1/2 rounded-full transition duration-200 ease-in-out z-0",
props.checked ? "bg-violet-500" : "bg-slate-300",
)}
/>
</span>
<span className="ml-2 text-sm text-slate-700 dark:text-slate-300">{props.label}</span>
</label>
</div>
);
}

View File

@ -0,0 +1,95 @@
import type { Dispatch, SetStateAction } from "react";
import React, { 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-x-auto overflow-y-hidden">
<ul className="!p-0 w-max flex items-stretch gap-1 !m-0" 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="-mt-1 p-2">{children}</div>
</div>
</TabsContext.Provider>
);
}
export function TabItem({ tab, isSelected, select }: { tab: TabType; isSelected: boolean; select: () => void }) {
return (
<Button
variant={isSelected ? "primary" : "secondary"}
className={clsx("!rounded-md", isSelected && "cursor-default")}
onClick={select}
>
{tab.label}
</Button>
);
}
export function TabContent({ label, value, children }: { label: string; value: string; children: React.ReactNode }) {
const { addTab, selectedTab } = useContext(TabsContext);
useEffect(() => {
addTab({ label, value });
}, []);
return (
<div className={clsx("[&>*:first-of-type]:!mt-0", "[&>*:last-of-type]:!mb-0", selectedTab !== value && "hidden")}>
{children}
</div>
);
}

View File

@ -0,0 +1,33 @@
import { Link } from "@/components/common/Link";
import React from "react";
import clsx from "clsx";
const variantStyles = {
primary:
"bg-violet-300 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:
"bg-slate-800 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",
ghost:
"bg-transparent font-medium text-slate-900 dark:text-slate-400 hover:bg-slate-100 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-300/50 active:bg-slate-200",
};
const sizeStyles = {
sm: "rounded-md py-1 px-2 text-xs",
md: "rounded-full py-2 px-4 text-sm",
lg: "rounded-full py-3 px-6 text-base",
};
type ButtonProps = {
variant?: keyof typeof variantStyles;
size?: keyof typeof sizeStyles;
} & (React.ComponentPropsWithoutRef<typeof Link> | (React.ComponentPropsWithoutRef<"button"> & { href?: undefined }));
export function Button({ variant = "primary", size = "md", className, ...props }: ButtonProps) {
className = clsx(variantStyles[variant], sizeStyles[size], "cursor-pointer", className);
return typeof props.href === "undefined" ? (
<button className={className} {...props} />
) : (
<Link className={className} {...props} />
);
}

View File

@ -0,0 +1,82 @@
import { ClipboardDocumentIcon } from "@heroicons/react/24/outline";
import { prismThemes } from "@/data/themes/prism";
import React, { Fragment, useMemo } from "react";
import { Highlight } from "prism-react-renderer";
import { useTheme } from "@/hooks/useTheme";
import { toast } from "react-toastify";
import { Button } from "./Button";
import Prism from "prismjs";
import clsx from "clsx";
export default function CSRSnippet({
children,
language,
label,
showLineNumbers = false,
}: {
children: string;
language: string;
label?: string;
showLineNumbers?: boolean;
}) {
const { theme } = useTheme();
const prismTheme = useMemo(() => {
return prismThemes[theme];
}, [theme]);
const copyToClipboard = () => {
navigator.clipboard.writeText(children.trimEnd());
toast.success("Code copié dans le presse-papier");
};
return (
<>
<Highlight code={children.trimEnd()} language={language} theme={prismTheme} prism={Prism}>
{({ className, style, tokens, getTokenProps }) => (
<div className="relative w-full">
{label && (
<div className="absolute px-4 py-1 left-0 text-sm text-gray-700 dark:text-gray-200 italic w-full bg-gray-200 dark:bg-gray-700 rounded-t-xl">
{label}
</div>
)}
<pre className={clsx(className, { "pt-11": label })} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{showLineNumbers && (
<span
className="text-gray-400 dark:text-gray-500 text-right font-mono w-8 inline-block pr-4"
style={{ userSelect: "none" }}
>
{lineIndex + 1}
</span>
)}
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{"\n"}
</Fragment>
))}
</code>
</pre>
</div>
)}
</Highlight>
<Button
className={clsx(
"absolute right-2 w-8 h-8 aspect-square opacity-0 group-hover:opacity-50 hover:opacity-100 transition-opacity",
label ? "top-10" : "top-2",
)}
size="sm"
variant="secondary"
onClick={copyToClipboard}
>
<ClipboardDocumentIcon className="w-full" />
</Button>
</>
);
}

View File

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

View File

@ -0,0 +1,33 @@
import { usePageContext } from "vike-react/usePageContext";
import { ClockIcon } from "@heroicons/react/24/outline";
import { navigation } from "@/lib/navigation";
import React from "react";
type DocsHeaderProps = {
title?: string;
estimatedReadingTime?: string;
};
export function DocsHeader(props: DocsHeaderProps) {
const { urlPathname } = usePageContext();
const section = navigation.find((section) => section.links.find((link) => link.href === urlPathname));
if (!props.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>}
{props.title && (
<h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">{props.title}</h1>
)}
{props.estimatedReadingTime && (
<p className="text-sm text-slate-500 dark:text-slate-400 inline-flex items-center gap-1">
<ClockIcon className="w-4" /> {props.estimatedReadingTime}
</p>
)}
</header>
);
}

View File

@ -0,0 +1,36 @@
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";
import React from "react";
export function DocsLayout({
children,
frontmatter: { title },
estimatedReadingTime,
nodes,
}: {
children: React.ReactNode;
frontmatter: { title?: string };
estimatedReadingTime?: string;
nodes: Array<Node>;
}) {
const 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 grow">
<article>
<DocsHeader title={title} estimatedReadingTime={estimatedReadingTime} />
<Prose>{children}</Prose>
</article>
<PrevNextLinks />
</div>
<TableOfContents tableOfContents={tableOfContents} />
</>
);
}

View File

@ -0,0 +1,20 @@
import { clientOnly } from "vike-react/clientOnly";
import { SSRSnippet } from "./SSRSnippet";
import React from "react";
const CSRSnippet = clientOnly(() => import("./CSRSnippet"));
export function Fence({ children, language }: { children: string; language: string }) {
const props = {
language,
label: undefined,
showLineNumbers: false,
children,
};
return (
<div className="relative group">
<CSRSnippet {...props} fallback={<SSRSnippet {...props} />} />
</div>
);
}

View File

@ -0,0 +1,132 @@
import { HeroBackground } from "@syntax/HeroBackground";
import blurIndigoImage from "@/images/blur-indigo.webp";
import blurCyanImage from "@/images/blur-cyan.webp";
import { Image } from "@/components/common/Image";
import { Highlight } from "prism-react-renderer";
import React, { Fragment } from "react";
import { Button } from "@syntax/Button";
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 tes compétences en développement.
</p>
<div className="mt-8 flex gap-4 md:justify-center lg:justify-start">
<Button href="/docs">Accédez aux ressources</Button>
<Button href="https://github.com/GauthierWebDev/memento-dev" variant="secondary">
Voir sur Github
</Button>
</div>
</div>
</div>
<div className="relative lg:static xl:pl-10">
<div className="absolute inset-x-[-50vw] -top-32 -bottom-48 [mask-image:linear-gradient(transparent,white,white)] lg:-top-32 lg:right-0 lg:-bottom-32 lg:left-[calc(50%+14rem)] lg:[mask-image:none] dark:[mask-image:linear-gradient(transparent,white,transparent)] lg:dark:[mask-image:linear-gradient(white,white,transparent)]">
<HeroBackground className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 lg:left-0 lg:translate-x-0 lg:translate-y-[-60%]" />
</div>
<div className="relative">
<Image className="absolute -top-64 -right-64" src={blurCyanImage} alt="" width={530} height={530} />
<Image className="absolute -right-44 -bottom-40" src={blurIndigoImage} alt="" width={567} height={567} />
<div className="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10 blur-lg" />
<div className="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10" />
<div className="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur-sm">
<div className="absolute -top-px right-11 left-20 h-px bg-linear-to-r from-violet-300/0 via-violet-300/70 to-violet-300/0" />
<div className="absolute right-20 -bottom-px left-11 h-px bg-linear-to-r from-purple-400/0 via-purple-400 to-purple-400/0" />
<div className="pt-4 pl-4">
<TrafficLightsIcon className="h-2.5 w-auto stroke-slate-500/30" />
<div className="mt-4 flex space-x-2 text-xs">
{tabs.map((tab) => (
<div
key={tab.name}
className={clsx(
"flex h-6 rounded-full",
tab.isActive
? "bg-linear-to-r from-violet-400/30 via-violet-400 to-violet-400/30 p-px font-medium text-violet-300"
: "text-slate-500",
)}
>
<div className={clsx("flex items-center rounded-full px-2.5", tab.isActive && "bg-slate-800")}>
{tab.name}
</div>
</div>
))}
</div>
<div className="mt-6 flex items-start px-1 text-sm">
<div
aria-hidden="true"
className="border-r border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
>
{Array.from({
length: code.split("\n").length,
}).map((_, index) => (
<Fragment key={index}>
{(index + 1).toString().padStart(2, "0")}
<br />
</Fragment>
))}
</div>
<Highlight code={code} language={codeLanguage} theme={{ plain: {}, styles: [] }}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={clsx(className, "flex overflow-x-auto pb-6")} style={style}>
<code className="px-4">
{tokens.map((line, lineIndex) => (
<div key={lineIndex} {...getLineProps({ line })}>
{line.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
</div>
))}
</code>
</pre>
)}
</Highlight>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

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

View File

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

View File

@ -0,0 +1,37 @@
import React from "react";
function LogomarkPaths() {
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 58 38" {...props}>
<LogomarkPaths />
</svg>
);
}

View File

@ -0,0 +1,85 @@
import React, { 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() {
const [isOpen, setIsOpen] = useState(false);
const close = useCallback(() => setIsOpen(false), [setIsOpen]);
function onLinkClick(event: React.MouseEvent<HTMLAnchorElement>) {
const 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-6 w-auto shrink-0" />
</Link>
</div>
<Navigation className="mt-5 px-1" onLinkClick={onLinkClick} />
</DialogPanel>
</Dialog>
</>
);
}

View File

@ -0,0 +1,205 @@
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid";
import { usePageContext } from "vike-react/usePageContext";
import React, { useEffect, useState } from "react";
import { Link } from "@/components/common/Link";
import { navigation } from "@/lib/navigation";
import clsx from "clsx";
type NavigationItemProps = {
section: (typeof navigation)[number];
onLinkClick?: React.MouseEventHandler<HTMLAnchorElement>;
};
function NavigationItem(props: NavigationItemProps) {
const { urlPathname } = usePageContext();
const [isOpened, setIsOpened] = useState(() => {
return props.section.links.some(
(link) => link.href === urlPathname || link.subitems?.some((subitem) => subitem.href === urlPathname),
);
});
return (
<>
<h2
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setIsOpened((prev) => !prev);
e.preventDefault();
}
}}
className={clsx(
"font-display font-medium cursor-pointer",
isOpened ? "text-violet-600 dark:text-violet-200" : "text-slate-900 dark:text-white ",
)}
onClick={() => setIsOpened((prev) => !prev)}
>
{isOpened ? (
<ChevronUpIcon className="inline-block mr-2 h-5 w-5 text-slate-400" />
) : (
<ChevronDownIcon className="inline-block mr-2 h-5 w-5 text-slate-400" />
)}
<span className="sr-only">{isOpened ? "Masquer" : "Afficher"}</span>
{props.section.title}
<span className="text-slate-400 dark:text-slate-500"> ({props.section.links.length})</span>
</h2>
{isOpened && (
<ul
role="list"
className="!mt-0 ml-2 space-y-1 border-l-2 border-slate-100 lg:mt-4 lg:space-y-2 lg:border-slate-200 dark:border-slate-800 mb-4"
>
{props.section.links.map((link) => (
<li key={link.href} className="relative">
<NavigationSubItem
link={link}
onLinkClick={props.onLinkClick}
isOpened={link.href === urlPathname || link.subitems?.some((subitem) => subitem.href === urlPathname)}
/>
</li>
))}
</ul>
)}
</>
);
}
type NavigationSubItemProps = {
link: (typeof navigation)[number]["links"][number];
onLinkClick?: React.MouseEventHandler<HTMLAnchorElement>;
isOpened?: boolean;
};
function NavigationSubItem(props: NavigationSubItemProps) {
const [isOpened, setIsOpened] = useState(props.isOpened);
const { urlPathname } = usePageContext();
useEffect(() => {
setIsOpened(
props.link.href === urlPathname || props.link.subitems?.some((subitem) => subitem.href === urlPathname),
);
}, [urlPathname, props.link]);
return (
<>
<span className="pl-2 flex cursor-pointer">
{props.link.subitems.length > 0 && (
<span
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
setIsOpened((prev) => !prev);
e.preventDefault();
}
}}
onClick={() => setIsOpened((prev) => !prev)}
>
{isOpened ? (
<ChevronUpIcon className="inline-block h-5 w-5 text-slate-400" />
) : (
<ChevronDownIcon className="inline-block h-5 w-5 text-slate-400" />
)}
<span className="sr-only">{isOpened ? "Masquer" : "Afficher"}</span>
</span>
)}
<Link
href={props.link.href}
onClick={props.onLinkClick}
className={clsx(
"block pl-2 w-full before:pointer-events-none before:absolute before:-left-1 before:h-1.5 before:w-1.5 before:rounded-full",
{ "before:top-1/2 before:-translate-y-1/2": !props.link.subitems },
{ "before:top-3 before:-translate-y-1/2 font-semibold": props.link.subitems },
props.link.href !== urlPathname && "before:hidden",
isOpened
? "text-violet-500 before:bg-violet-500"
: "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300",
)}
>
{props.link.title}
{props.link.subitems.length > 0 && (
<span className="text-slate-400 dark:text-slate-500"> ({props.link.subitems.length})</span>
)}
</Link>
</span>
{props.link.subitems.length > 0 && isOpened && (
<ul
role="list"
className="ml-4 border-l-2 border-slate-100 space-y-1 lg:space-y-2 lg:border-slate-200 dark:border-slate-800 mb-4"
>
{props.link.subitems.map((subitem) => (
<li key={subitem.href} className="relative">
<Link
href={subitem.href}
onClick={props.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",
subitem.href === urlPathname
? "font-semibold text-violet-500 before:bg-violet-500"
: "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300",
)}
>
{subitem.title}
</Link>
</li>
))}
</ul>
)}
</>
);
}
export function Navigation({
className,
onLinkClick,
}: {
className?: string;
onLinkClick?: React.MouseEventHandler<HTMLAnchorElement>;
}) {
const firstSections = navigation.filter((section) => section.position === "start");
const lastSections = navigation.filter((section) => section.position === "end");
const filteredSections = navigation
.filter((section) => section.position === "auto" || section.position === undefined)
.reduce(
(acc, section) => {
if (!acc[section.type]) {
acc[section.type] = [];
}
acc[section.type].push(section);
return acc;
},
{} as Record<string, typeof navigation>,
);
return (
<nav className={clsx("text-base lg:text-sm", className)}>
<ul role="list" className="space-y-4">
<li>
<h2 className="font-display font-bold text-base text-slate-900 dark:text-white">{firstSections[0]?.type}</h2>
{firstSections.map((section) => (
<NavigationItem key={section.title} section={section} onLinkClick={onLinkClick} />
))}
</li>
{Object.entries(filteredSections).map(([type, sections]) => (
<li key={type}>
<h2 className="font-display font-bold text-base text-slate-900 dark:text-white">{type}</h2>
{sections.map((section) => (
<NavigationItem key={section.title} section={section} onLinkClick={onLinkClick} />
))}
</li>
))}
<li>
<h2 className="font-display font-bold text-base text-slate-900 dark:text-white">{lastSections[0]?.type}</h2>
{lastSections.map((section) => (
<NavigationItem key={section.title} section={section} onLinkClick={onLinkClick} />
))}
</li>
</ul>
</nav>
);
}

View File

@ -0,0 +1,89 @@
import { usePageContext } from "vike-react/usePageContext";
import { Link } from "@/components/common/Link";
import { navigation } from "@/lib/navigation";
import React from "react";
import clsx from "clsx";
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";
}) {
const pageCategory = navigation.find((section) => {
return section.links.some((link) => link.href === href || link.subitems.some((subitem) => subitem.href === href));
});
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-2 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",
)}
>
<p className="flex flex-col gap-0">
{pageCategory && (
<span className="text-violet-600 dark:text-violet-400 text-sm mb-1 leading-3">{pageCategory.title}</span>
)}
<span className="leading-4">{title}</span>
</p>
<ArrowIcon className={clsx("h-6 w-6 flex-none fill-current", dir === "previous" && "-scale-x-100")} />
</Link>
</dd>
</div>
);
}
export function PrevNextLinks() {
const { urlPathname } = usePageContext();
const allLinks = navigation
.flatMap((section) => section.links)
.flatMap((link) => {
return link.subitems ? [link, ...link.subitems] : link;
});
const getNeighboringLinks = () => {
const linkIndex = allLinks.findIndex((link) => link.href === urlPathname);
if (linkIndex === -1) return [null, null];
const previousPage = allLinks[linkIndex - 1] || null;
let nextPage = allLinks[linkIndex + 1] || null;
// In case the next page is the same as the current page (in subitems),
// we need to skip it to get the correct next page.
if (nextPage?.href === urlPathname) {
nextPage = allLinks[linkIndex + 2] || null;
}
return [previousPage, nextPage];
};
const [previousPage, nextPage] = getNeighboringLinks();
if (!nextPage && !previousPage) return null;
return (
<dl className="mt-12 flex gap-4 border-t border-slate-200 pt-6 dark:border-slate-800">
{previousPage && <PageLink dir="previous" {...previousPage} />}
{nextPage && <PageLink className="ml-auto text-right" {...nextPage} />}
</dl>
);
}

View File

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

View File

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

View File

@ -0,0 +1,58 @@
import { Highlight, Prism } from "prism-react-renderer";
import { prismThemes } from "@/data/themes/prism";
import React, { Fragment, useMemo } from "react";
import { useTheme } from "@/hooks/useTheme";
import clsx from "clsx";
export function SSRSnippet({
children,
language,
label,
showLineNumbers = false,
}: {
children: string;
language: string;
label?: string;
showLineNumbers?: boolean;
}) {
const { theme } = useTheme();
const prismTheme = useMemo(() => {
return prismThemes[theme];
}, [theme]);
return (
<Highlight code={children.trimEnd()} language={language} theme={prismTheme} prism={Prism}>
{({ className, style, tokens, getTokenProps }) => (
<div className="relative w-full">
{label && (
<div className="absolute px-4 py-1 left-0 text-sm text-gray-700 dark:text-gray-200 italic w-full bg-gray-200 dark:bg-gray-700 rounded-t-xl">
{label}
</div>
)}
<pre className={clsx(className, { "pt-11": !!label })} style={style}>
<code>
{tokens.map((line, lineIndex) => (
<Fragment key={lineIndex}>
{showLineNumbers && (
<span
className="text-gray-400 dark:text-gray-500 text-right font-mono w-8 inline-block pr-4"
style={{ userSelect: "none" }}
>
{lineIndex + 1}
</span>
)}
{line
.filter((token) => !token.empty)
.map((token, tokenIndex) => (
<span key={tokenIndex} {...getTokenProps({ token })} />
))}
{"\n"}
</Fragment>
))}
</code>
</pre>
</div>
)}
</Highlight>
);
}

View File

@ -0,0 +1,13 @@
import { buildFlexSearch, type SearchResult } from "@/services/FlexSearchService";
import { docsService } from "@/services/DocsService";
export const onSearch = async (query: string, maxResults?: number): Promise<SearchResult[]> => {
const search = buildFlexSearch(await docsService.fetchDocs());
const results = search(query);
if (maxResults) {
return results.slice(0, maxResults);
}
return results;
};

View File

@ -0,0 +1,278 @@
import React, { useId, useState, useEffect, createContext, useContext, Fragment } from "react";
import { SearchResult } from "@/services/FlexSearchService";
import { Dialog, DialogPanel } from "@headlessui/react";
import { useDebounce } from "@/hooks/useDebounce";
import Highlighter from "react-highlight-words";
import { navigation } from "@/lib/navigation";
import { navigate } from "vike/client/router";
import { onSearch } from "./Search.telefunc";
import clsx from "clsx";
const SearchContext = createContext<{
query: string;
close: () => void;
results: SearchResult[];
isLoading: boolean;
isOpened: boolean;
setQuery: (query: string) => void;
setIsOpened: (isOpened: boolean) => void;
setIsLoading: (isLoading: boolean) => void;
setResults: (results: SearchResult[]) => void;
}>({
query: "",
close: () => {},
results: [],
isLoading: false,
isOpened: false,
setQuery: () => {},
setIsOpened: () => {},
setIsLoading: () => {},
setResults: () => {},
});
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 LoadingIcon(props: React.ComponentPropsWithoutRef<"svg">) {
const 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 SearchInput() {
const { close, setQuery, query, isLoading } = useContext(SearchContext);
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
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",
isLoading ? "pr-11" : "pr-4",
)}
onKeyDown={(event) => {
if (event.key === "Escape") {
// 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();
}
close();
}
}}
value={query}
onChange={(event) => setQuery(event.currentTarget.value)}
/>
{isLoading && (
<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 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 SearchResultItem({ result, query }: { result: SearchResult; query: string }) {
const { close } = useContext(SearchContext);
const id = useId();
const sectionTitle = navigation.find((section) =>
section.links.find((link) => link.href === result.url.split("#")[0]),
)?.title;
const 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 hover:bg-slate-100 dark:hover:bg-slate-700/30"
aria-labelledby={`${id}-hierarchy ${id}-title`}
role="option"
tabIndex={0}
onClick={() => {
navigate(result.url);
close();
}}
>
<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() {
const { results, query } = useContext(SearchContext);
if (results.length === 0) {
return (
<p className="px-4 py-8 text-center text-sm text-slate-700 dark:text-slate-400">
Aucun résultat pour &ldquo;
<span className="break-words text-slate-900 dark:text-white">{query}</span>
&rdquo;
</p>
);
}
return (
<ul>
{results.map((result) => (
<SearchResultItem key={result.url} result={result} query={query} />
))}
</ul>
);
}
function SearchDialog({ className }: { className?: string }) {
const { close, isOpened, setIsOpened, results } = useContext(SearchContext);
useEffect(() => {
if (isOpened) return;
function onKeyDown(event: KeyboardEvent) {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setIsOpened(true);
}
}
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isOpened, setIsOpened]);
return (
<>
<Dialog open={isOpened} onClose={close} 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">
<form onSubmit={(event) => event.preventDefault()}>
<SearchInput />
<div className="border-t border-slate-200 bg-white px-2 py-3 empty:hidden dark:border-slate-400/10 dark:bg-slate-800">
{results.length > 0 && <SearchResults />}
</div>
</form>
</DialogPanel>
</div>
</Dialog>
</>
);
}
export function Search() {
const [results, setResults] = useState<SearchResult[]>([]);
const [debouncedQuery, setDebouncedQuery] = useDebounce();
const [modifierKey, setModifierKey] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const [isOpened, setIsOpened] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
const platform = navigator.userAgentData?.platform || navigator.platform;
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(platform) ? "⌘" : "Ctrl ");
}, []);
useEffect(() => {
setDebouncedQuery(query);
}, [query]);
useEffect(() => {
if (debouncedQuery.length === 0) {
setIsLoading(false);
setResults([]);
return;
}
setIsLoading(true);
onSearch(debouncedQuery, 5)
.then(setResults)
.finally(() => {
setIsLoading(false);
});
}, [debouncedQuery]);
return (
<SearchContext.Provider
value={{
query,
close: () => setIsOpened(false),
results,
isLoading,
isOpened,
setQuery,
setIsOpened,
setIsLoading,
setResults,
}}
>
<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"
onClick={() => setIsOpened(true)}
>
<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">Rechercher...</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 />
</SearchContext.Provider>
);
}

View File

@ -0,0 +1,48 @@
import type { Data } from "@/pages/docs/+data";
import { clientOnly } from "vike-react/clientOnly";
import { useData } from "vike-react/useData";
import { SSRSnippet } from "./SSRSnippet";
import React from "react";
const CSRSnippet = clientOnly(() => import("./CSRSnippet"));
export function Snippet({
path,
language,
label,
showLineNumbers,
}: {
path: string;
language: string;
label?: string;
showLineNumbers: boolean;
}) {
const { snippets } = useData<Data>();
const snippet = snippets.find((snippet) => snippet.path === path);
if (!snippet || !snippet.content) {
return (
<div className="bg-red-600/10 p-4 rounded-md flex items-center justify-center">
<p className="text-red-500 text-center">
<b className="uppercase">Snippet introuvable</b>
<br />
<code>{path}</code>
</p>
</div>
);
}
const props = {
language,
label,
showLineNumbers,
children: snippet.content,
};
return (
<div className="relative group">
<CSRSnippet {...props} fallback={<SSRSnippet {...props} />} />
</div>
);
}

View File

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

View File

@ -0,0 +1,86 @@
import { Label, Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react";
import React, { useEffect, useState } from "react";
import { useTheme } from "@/hooks/useTheme";
import clsx from "clsx";
const themes = [
{ name: "Clair", value: "light", icon: LightIcon },
{ name: "Sombre", value: "dark", icon: DarkIcon },
];
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>
);
}
export function ThemeSelector(props: React.ComponentPropsWithoutRef<typeof Listbox<"div">>) {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
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 cursor-pointer"
aria-label="Theme"
>
<LightIcon className={clsx("h-4 w-4 dark:hidden", "fill-violet-400")} />
<DarkIcon className={clsx("hidden h-4 w-4 dark:block", "fill-violet-400")} />
</ListboxButton>
<ListboxOptions className="absolute top-full left-1/2 mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium ring-1 shadow-md shadow-black/5 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
{themes.map((theme) => (
<ListboxOption
key={theme.value}
value={theme.value}
className={({ focus, selected }) =>
clsx("flex cursor-pointer items-center rounded-[0.625rem] p-1 select-none", {
"text-violet-500": selected,
"text-slate-900 dark:text-white": focus && !selected,
"text-slate-700 dark:text-slate-400": !focus && !selected,
"bg-slate-100 dark:bg-slate-900/40": focus,
})
}
>
{({ selected }) => (
<>
<div className="rounded-md bg-white p-1 ring-1 shadow-sm ring-slate-900/5 dark:bg-slate-700 dark:ring-white/5 dark:ring-inset">
<theme.icon
className={clsx("h-4 w-4", selected ? "fill-violet-400 dark:fill-violet-400" : "fill-slate-400")}
/>
</div>
<div className="ml-3">{theme.name}</div>
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</Listbox>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,48 @@
import { DarkMode, Gradient, LightMode } from "@syntax/Icon";
import React from "react";
export function QuestionIcon({ 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="m 16.39 14.617 l 1.179 -3.999 C 17.38 9.304 16.133 9.127 15.469 10.645 C 15.306 11.269 14.71 11.12 14.71 10.537 a 1.66 1.66 5 1 1 3.808 0.217 l -1.5182 5.4314 a 0.602 0.602 5 0 1 -1.1795 -0.1032 Z"
className="fill-[var(--icon-foreground)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
fillOpacity={0.5}
stroke="currentColor"
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</LightMode>
<DarkMode>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M2 16C2 8.268 8.268 2 16 2s14 6.268 14 14-6.268 14-14 14S2 23.732 2 16Zm11.386-4.85a2.66 2.66 0 1 1 5.228 0l-1.039 5.543a1.602 1.602 0 0 1-3.15 0l-1.04-5.543ZM16 20a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"
fill={`url(#${id}-gradient-dark)`}
/>
</DarkMode>
</>
);
}

View File

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

View File

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

View File

@ -1,42 +0,0 @@
function getEnvironmentVariable<T = undefined>(
key: string,
defaultValue: T,
formatter?: (data: string) => T,
): T {
const value = process.env[key];
if (value === undefined) return defaultValue;
if (formatter) return formatter(value);
return value as T;
}
function getEnvironmentVariableOrThrow<T = undefined>(
key: string,
formatter?: (data: string) => T,
): T {
const value = process.env[key];
if (value === undefined)
throw new Error(`Missing environment variable: ${key}`);
if (formatter) return formatter(value);
return value as T;
}
const PORT = getEnvironmentVariableOrThrow<number>("PORT", (data) =>
Number.parseInt(data, 10),
);
const HMR_PORT = getEnvironmentVariableOrThrow<number>("HMR_PORT", (data) =>
Number.parseInt(data, 10),
);
const BASE_URL = getEnvironmentVariable<string>(
"BASE_URL",
`http://localhost:${PORT}`,
);
const NODE_ENV = getEnvironmentVariable<string>("NODE_ENV", "development");
export const config = {
PORT,
HMR_PORT,
BASE_URL,
NODE_ENV,
};

View File

@ -0,0 +1,13 @@
import { createContext } from "react";
export type Theme = "light" | "dark";
export type ThemeContextType = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
export const ThemeContext = createContext<ThemeContextType>({
theme: "light",
setTheme: () => {},
});

View File

@ -4,8 +4,6 @@ description: Synthèse et explications des attentes relatives à la compétence
tags: [DWWM, Environnement de développement] tags: [DWWM, Environnement de développement]
--- ---
import Callout from "@/components/Callout";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 15 et 16 - REAC _(mise à jour du 02/07/2024)_, pages 15 et 16
@ -34,24 +32,32 @@ Mais heureusement, tu dois uniquement expliquer comment installer et configurer
Si tu fais un projet Laravel et React, pas besoin d'expliquer comment installer et configurer Ruby et Java, par exemple 😉 Si tu fais un projet Laravel et React, pas besoin d'expliquer comment installer et configurer Ruby et Java, par exemple 😉
<Callout type="note" title="Utilisation de XAMPP, WAMP, MAMP, LAMP, Laragon etc."> {% callout type="note" title="Utilisation de XAMPP, WAMP, MAMP, LAMP, Laragon etc." %}
Si tu utilises un logiciel comme XAMPP, WAMP, MAMP, LAMP, Laragal etc., tu as évidemment le droit de le mentionner dans ta présentation et dossier de projet. Si tu utilises un logiciel comme XAMPP, WAMP, MAMP, LAMP, Laragal etc., tu as évidemment le droit de le mentionner dans ta présentation et dossier de projet.
Toutefois, il est préférable que tu saches expliquer comment installer et configurer les éléments nécessaires de manières individuelles. Toutefois, il est préférable que tu saches expliquer comment installer et configurer les éléments nécessaires de manières individuelles.
</Callout>
{% /callout %}
## Informations complémentaires ## Informations complémentaires
<Callout type="warning" title="Versions des outils et dépendances"> {% callout type="warning" title="Versions des outils et dépendances" %}
Même si le choix des outils que tu utilises est libre, il est important de préciser les versions que tu as utilisées pour ton projet. Même si le choix des outils que tu utilises est libre, il est important de préciser les versions que tu as utilisées pour ton projet.
Étant donné que chaque version corrige probablement diverses failles de sécurité et/ou ajoute des fonctionnalités, c'est le bon moment pour montrer que tu prends la veille technologique au sérieux. Étant donné que chaque version corrige probablement diverses failles de sécurité et/ou ajoute des fonctionnalités, c'est le bon moment pour montrer que tu prends la veille technologique au sérieux.
</Callout>
{% /callout %}
## 🛠️ Ressources conseillées
_En cours de rédaction..._
## 🎯 Critères d'évaluation ## 🎯 Critères d'évaluation
- Les outils de développement nécessaires sont installés et configurés - Les outils de développement nécessaires sont installés et configurés
- Les outils de gestion de versions et de collaboration sont installés - Les outils de gestion de versions et de collaboration sont installés
- Les conteneurs implémentent les services requis pour l'environnement de développement - Les conteneurs implémentes les services requis pour l'environnement de développement
- La documentation technique de l'environnement de travail est comprise, en langue française ou anglaise _(niveau B1 CECRL pour l'anglais)_ - La documentation technique de l'environnement de travail est comprise, en langue française ou anglaise (niveau B1 CECRL pour l'anglais)
- Le système de veille permet de suivre les évolutions technologies et les problématiques de sécurité en lien avec l'installation et la configuration d'un environnement de travail - Le système de veille permet de suivre les évolutions technologies et les problématiques de sécurité en lien avec l'installation et la configuration d'un environnement de travail

View File

@ -4,9 +4,6 @@ description: Synthèse et explications des attentes relatives à la compétence
tags: [DWWM, Éco-conception, Accessibilité, SEO, Maquettage, UX, UI, Zoning, Wireframe, Prototypage, Design, Frontend] tags: [DWWM, Éco-conception, Accessibilité, SEO, Maquettage, UX, UI, Zoning, Wireframe, Prototypage, Design, Frontend]
--- ---
import Callout from "@/components/Callout";
import Image from "@/components/Image";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 17 et 18 - REAC _(mise à jour du 02/07/2024)_, pages 17 et 18
@ -17,7 +14,7 @@ import Image from "@/components/Image";
Pour cette compétence, tu vas devoir réaliser des maquettes d'interfaces utilisateur. Pour cette compétence, tu vas devoir réaliser des maquettes d'interfaces utilisateur.
Mais par maquettage, on ne parle pas de maquette en papier ou en carton, mais bien de maquettes numériques. Mais par maquettage, on ne parle pas de maquette en papier ou en carton, mais bien de maquettes numériques.
<Image src="https://i.giphy.com/28n0C19zo9OOvHnYww.webp" width="480" height="269" class="mx-auto rounded-md" /> {% iframe src="https://giphy.com/embed/28n0C19zo9OOvHnYww" width="480" height="269" className="mx-auto" /%}
... Blague à part, on entend par cette compétence professionnelle la réalisation de maquettes au travers des différentes étapes de maquettage : ... Blague à part, on entend par cette compétence professionnelle la réalisation de maquettes au travers des différentes étapes de maquettage :
@ -29,7 +26,8 @@ Mais par maquettage, on ne parle pas de maquette en papier ou en carton, mais bi
6. **Création de maquettes graphiques** : réaliser des maquettes graphiques en respectant la charte graphique et les wireframes 6. **Création de maquettes graphiques** : réaliser des maquettes graphiques en respectant la charte graphique et les wireframes
7. **Mise en place du prototypage** : créer un prototype interactif pour tester l'application avant le développement 7. **Mise en place du prototypage** : créer un prototype interactif pour tester l'application avant le développement
<Callout type="question" title="Mais je ne suis pas webdesigner ! 😱"> {% callout type="question" title="Mais je ne suis pas webdesigner ! 😱" %}
Pas de panique ! Pas de panique !
Tu n'as pas besoin d'être un webdesigner pour réaliser des maquettes. Tu n'as pas besoin d'être un webdesigner pour réaliser des maquettes.
@ -38,7 +36,8 @@ Mais par maquettage, on ne parle pas de maquette en papier ou en carton, mais bi
Ce qu'on attend de toi, c'est de respecter les consignes du cahier des charges tout en répondant au besoin du client et en faisant attention à l'**expérience utilisateur** et l'**accessibilité**. Ce qu'on attend de toi, c'est de respecter les consignes du cahier des charges tout en répondant au besoin du client et en faisant attention à l'**expérience utilisateur** et l'**accessibilité**.
Si par ailleurs tu cherches un outil gratuit pour réaliser des maquettes, tu peux te tourner vers [Figma](https://www.figma.com/fr-fr/) qui permet non seulement de créer des **maquettes** et **prototypes**, mais aussi de **collaborer** avec d'autres personnes sur un même projet. Si par ailleurs tu cherches un outil gratuit pour réaliser des maquettes, tu peux te tourner vers [Figma](https://www.figma.com/fr-fr/) qui permet non seulement de créer des **maquettes** et **prototypes**, mais aussi de **collaborer** avec d'autres personnes sur un même projet.
</Callout>
{% /callout %}
## Informations complémentaires ## Informations complémentaires
@ -80,6 +79,10 @@ Au delà de l'impact environnemental, un site éco-conçu est souvent plus rapid
Tu l'auras compris, que tu sois éco-convaincu ou non, c'est un point qui est en faveur de l'expérience utilisateur ! 🚀 Tu l'auras compris, que tu sois éco-convaincu ou non, c'est un point qui est en faveur de l'expérience utilisateur ! 🚀
## 🛠️ Ressources conseillées
_En cours de rédaction..._
## 🎯 Critères d'évaluation ## 🎯 Critères d'évaluation
- Les maquettes sont réalisées conformément au dossier de conception, en langue française ou anglaise _(niveau B1 du CECRL pour l'anglais)_ - Les maquettes sont réalisées conformément au dossier de conception, en langue française ou anglaise _(niveau B1 du CECRL pour l'anglais)_
@ -89,12 +92,12 @@ Tu l'auras compris, que tu sois éco-convaincu ou non, c'est un point qui est en
- L'enchainement des maquettes est formalisé par un schéma - L'enchainement des maquettes est formalisé par un schéma
- La législation en vigueur est respectée, y compris celle relative à l'accessibilité - La législation en vigueur est respectée, y compris celle relative à l'accessibilité
## 🤯 Aller plus loin (hors référentiel) ## 🤯 Aller plus loin _(hors référentiel)_
Tu trouves que je n'ai pas assez parlé dans cette compétence ? Alors je vais te donner quelques pistes pour aller plus loin ! Tu trouves que je n'ai pas assez parlé dans cette compétence ? Alors je vais te donner quelques pistes pour aller plus loin !
Je vais parler un peu plus en détail de l'éco-conception, tant il s'agit d'un sujet important, relativement récent et souvent négligé _(ou incompris)_. Je vais parler un peu plus en détail de l'éco-conception, tant il s'agit d'un sujet important, relativement récent et souvent négligé _(ou incompris)_.
Le premier réflexe que l'on pourrait avoir en entendant ce terme, c'est crier au greenwashing et penser qu'on attend de nous de revenir dans les années 90 avec des sites tout moches et tout plats. Le premier réflexe que l'on pourrait avoir en entendu ce terme, c'est crier au greenwashing et penser qu'on attend de nous de revenir dans les années 90 avec des sites tout moches et tout plats.
Alors... Greenwashing ? Tout dépend de qui en parle et avec quels arguments, mais la cause elle-même est noble et mérite d'être prise en compte. Alors... Greenwashing ? Tout dépend de qui en parle et avec quels arguments, mais la cause elle-même est noble et mérite d'être prise en compte.
Pour le côté "site tout moche", c'est effectivement une possibilité, mais uniquement si c'est un développeur PHP ou Java qui s'en occupe 😘. Pour le côté "site tout moche", c'est effectivement une possibilité, mais uniquement si c'est un développeur PHP ou Java qui s'en occupe 😘.

View File

@ -20,10 +20,6 @@ tags:
] ]
--- ---
import QuickLinks from "@/components/QuickLinks";
import Callout from "@/components/Callout";
import tabs from "./tabs";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 19 et 20 - REAC _(mise à jour du 02/07/2024)_, pages 19 et 20
@ -34,9 +30,9 @@ import tabs from "./tabs";
Pfiou, les maquettes sont terminées et tu as survécu à mes pavés d'explications ! 💪 Pfiou, les maquettes sont terminées et tu as survécu à mes pavés d'explications ! 💪
Eh bien... c'est reparti pour un tour, car maintenant tu vas devoir réaliser les interfaces statiques web ou web mobile à partir de ces maquettes. Eh bien... c'est reparti pour un tour, car maintenant tu vas devoir réaliser les interfaces statiques web ou web mobile à partir de ces maquettes.
<Callout type="question" title="Mais qu'est-ce qu'une interface statique ?"> {% callout type="question" title="Mais qu'est-ce qu'une interface statique ?" %}
Une interface statique, c'est une interface qui ne bouge pas, qui n'a pas d'interactions avec l'utilisateur autre que les différents liens qui peuvent être présents. Une interface statique, c'est une interface qui ne bouge pas, qui n'a pas d'interactions avec l'utilisateur autre que les différents liens qui peuvent être présents.
</Callout> {% /callout %}
Pour réaliser ces interfaces, tu vas devoir respecter les maquettes que tu as réalisées précédemment, tout en prenant en compte les besoins en éco-conception et en accessibilité. _(je radote, mais c'est important !)_ Pour réaliser ces interfaces, tu vas devoir respecter les maquettes que tu as réalisées précédemment, tout en prenant en compte les besoins en éco-conception et en accessibilité. _(je radote, mais c'est important !)_
@ -64,12 +60,12 @@ Dans la compétence précédente, je t'ai parlé de l'accessibilité et de l'imp
Mais au delà de ces aspects, on va également pouvoir donner plus de précisions au navigateur sur le contenu de notre site, en utilisant des balises sémantiques ainsi que des attributs spécifiques : `alt` pour les images, `title` pour les liens, mais surtout `aria-*`. Mais au delà de ces aspects, on va également pouvoir donner plus de précisions au navigateur sur le contenu de notre site, en utilisant des balises sémantiques ainsi que des attributs spécifiques : `alt` pour les images, `title` pour les liens, mais surtout `aria-*`.
<QuickLinks.QuickLink {% quick-link
title="Attributs ARIA (Accessible Rich Internet Applications)" title="Attributs ARIA (Accessible Rich Internet Applications)"
href="https://developer.mozilla.org/fr/docs/Web/Accessibility/ARIA" href="https://developer.mozilla.org/fr/docs/Web/Accessibility/ARIA"
icon="presets" icon="presets"
description="> MDN Web Docs - Attributs ARIA" description="> MDN Web Docs - Attributs ARIA"
/> /%}
### 🌐 Le référencement naturel ### 🌐 Le référencement naturel
@ -97,24 +93,24 @@ Mais pas aucun contrôle !
L'action la plus primordiale sur les images, c'est dans un premier temps d'utiliser des ressources compressées, mais aussi de les dimensionner correctement. L'action la plus primordiale sur les images, c'est dans un premier temps d'utiliser des ressources compressées, mais aussi de les dimensionner correctement.
<Callout type="note" title="En CSS, ça prend 2 secondes de redimensionner une image"> {% callout type="note" title="En CSS, ça prend 2 secondes de redimensionner une image" %}
Effectivement, il est possible de redimensionner les images en leur appliquant une largeur et une hauteur. Effectivement, il est possible de redimensionner les images en leur appliquant une largeur et une hauteur.
Mais l'image reste chargée en entier, même si elle n'est pas affichée dans sa totalité. Mais l'image reste chargée en entier, même si elle n'est pas affichée dans sa totalité.
Ça voudrait donc dire imposer au navigateur de télécharger une image en haute résolution pour l'afficher en miniature : pas terrible. Ça voudrait donc dire imposer au navigateur de télécharger une image en haute résolution pour l'afficher en miniature : pas terrible.
Pour éviter ça, on va dans un premier temps réduire et compresser l'image et on peut également utiliser l'attribut `srcset` qui permet de charger une image en fonction de la taille de l'écran. Pour éviter ça, on va dans un premier temps réduire et compresser l'image et on peut également utiliser l'attribut `srcset` qui permet de charger une image en fonction de la taille de l'écran.
</Callout> {% /callout %}
Maintenant que nos images sont prêtes, il faut que je vous dise que ce n'est pas tout ! Maintenant que nos images sont prêtes, il faut que je vous dise que ce n'est pas tout !
Il faut également penser au format de nos images. La plupart du temps, on croise des images en JPEG, PNG ou GIF, mais il existe un format plus récent et plus performant : le **WebP**. Il faut également penser au format de nos images. La plupart du temps, on croise des images en JPEG, PNG ou GIF, mais il existe un format plus récent et plus performant : le **WebP**.
<Callout type="question" title="Et le format SVG ? Ce n'est pas bien ?"> {% callout type="question" title="Et le format SVG ? Ce n'est pas bien ?" %}
Si, si, le format SVG est très bien ! Si, si, le format SVG est très bien !
C'est vrai que je ne l'évoque pas ici, mais le SVG est un format d'image vectorielle qui a l'avantage d'être léger et de s'adapter à toutes les tailles d'écran sans créer de flou ou pixélisation. C'est vrai que je ne l'évoque pas ici, mais le SVG est un format d'image vectorielle qui a l'avantage d'être léger et de s'adapter à toutes les tailles d'écran sans créer de flou ou pixélisation.
Il est particulièrement adapté pour les icônes, logos et autres éléments graphiques simples. Il est particulièrement adapté pour les icônes, logos et autres éléments graphiques simples.
</Callout> {% /callout %}
Allez cette fois-ci, on ne touche plus aux images et leur format, par contre "comment peut-on les charger ?". Allez cette fois-ci, on ne touche plus aux images et leur format, par contre "comment peut-on les charger ?".
Celui qui répond "avec une simple balise `<img>` et son attribut `src`" a tout faux ! _(enfin non, pas tout faux, mais pas tout à fait juste)_ Celui qui répond "avec une simple balise `<img>` et son attribut `src`" a tout faux ! _(enfin non, pas tout faux, mais pas tout à fait juste)_
@ -126,7 +122,7 @@ De cette manière, nos images ne seront chargées que si elles sont visibles à
En finalité, ça ressemble à ça : En finalité, ça ressemble à ça :
<tabs.htmlLazyLoading /> {% snippet path="html/lazy-loading.html" language="html" /%}
Allez, arrêtons-nous là pour l'éco-conception ! Allez, arrêtons-nous là pour l'éco-conception !
@ -155,16 +151,20 @@ Tu peux totalement utiliser des services "gratuits", comme Netlify ou Vercel, ma
Enfin, n'oublie pas de sécuriser ton site, en utilisant un certificat SSL par exemple. Enfin, n'oublie pas de sécuriser ton site, en utilisant un certificat SSL par exemple.
<Callout type="question" title="Un certificat quoi ? 🤔"> {% callout type="question" title="Un certificat quoi ? 🤔" %}
Un certificat SSL est un fichier de données qui sécurise les échanges de données entre un serveur et un navigateur en cryptant les données transmises. Il garantit que les données sont sécurisées et ne peuvent pas être interceptées. Un certificat SSL est un fichier de données qui sécurise les échanges de données entre un serveur et un navigateur en cryptant les données transmises. Il garantit que les données sont sécurisées et ne peuvent pas être interceptées.
Tu peux en générer un gratuitement avec [Let's Encrypt](https://letsencrypt.org/), mais il faudra le renouveler tous les 3 mois. Tu peux en générer un gratuitement avec [Let's Encrypt](https://letsencrypt.org/), mais il faudra le renouveler tous les 3 mois.
</Callout> {% /callout %}
Si tu as la main sur la configuration du serveur, tu pourras également mettre en place des règles de sécurité, comme le CSP _(Content Security Policy)_, qui permet de limiter les risques de failles XSS _(Cross-Site Scripting)_. Si tu as la main sur la configuration du serveur, tu pourras également mettre en place des règles de sécurité, comme le CSP _(Content Security Policy)_, qui permet de limiter les risques de failles XSS _(Cross-Site Scripting)_.
Tu peux également activer la compression Gzip pour réduire la taille des fichiers envoyés au navigateur afin d'accélérer le chargement du site et de répondre davantage aux critères d'éco-conception. Tu peux également activer la compression Gzip pour réduire la taille des fichiers envoyés au navigateur afin d'accélérer le chargement du site et de répondre davantage aux critères d'éco-conception.
## 🛠️ Ressources conseillées
_En cours de rédaction..._
## 🎯 Critères d'évaluation ## 🎯 Critères d'évaluation
- L'interface est conforme à la maquette et les besoins en éco-conception sont pris en compte - L'interface est conforme à la maquette et les besoins en éco-conception sont pris en compte
@ -186,7 +186,7 @@ Prenons un exemple concret, le cas d'une application qui tourne sur le port 3000
Avec Nginx, on peut faire ça très simplement en créant un fichier de configuration dans `/etc/nginx/sites-available/`. Avec Nginx, on peut faire ça très simplement en créant un fichier de configuration dans `/etc/nginx/sites-available/`.
<tabs.nginxReverseProxy /> {% snippet path="nginx/reverse-proxy.conf" language="nginx" showLineNumbers=true /%}
... Tadaaa ! C'est tout ! ... Tadaaa ! C'est tout !
Bien entendu, il va falloir activer ce site avec un lien symbolique dans `/etc/nginx/sites-enabled/` et redémarrer Nginx pour que les changements soient pris en compte. Bien entendu, il va falloir activer ce site avec un lien symbolique dans `/etc/nginx/sites-enabled/` et redémarrer Nginx pour que les changements soient pris en compte.

View File

@ -4,10 +4,6 @@ description: Synthèse et explications des attentes relatives à la compétence
tags: [DWWM] tags: [DWWM]
--- ---
import Callout from "@/components/Callout";
import Image from "@/components/Image";
import tabs from "./tabs";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 21 et 22 - REAC _(mise à jour du 02/07/2024)_, pages 21 et 22
@ -17,7 +13,7 @@ import tabs from "./tabs";
Ça y est, on commence à parler développement pour de vrai maintenant ! On quitte doucement l'intégration pour maintenant rajouter de l'interactivité à nos interfaces utilisateur, ce qui veut dire "utilisation d'un langage script côté client", soit... Ça y est, on commence à parler développement pour de vrai maintenant ! On quitte doucement l'intégration pour maintenant rajouter de l'interactivité à nos interfaces utilisateur, ce qui veut dire "utilisation d'un langage script côté client", soit...
<Image src="https://i.giphy.com/SvFocn0wNMx0iv2rYz.webp" width="480" height="480" class="mx-auto rounded-md" /> {% iframe src="https://giphy.com/embed/SvFocn0wNMx0iv2rYz" width="480" height="480" className="mx-auto" /%}
C'est le meilleur moment pour parler de nombreuses fonctionnalités implémentées sur ton application avec JavaScript, comme : C'est le meilleur moment pour parler de nombreuses fonctionnalités implémentées sur ton application avec JavaScript, comme :
@ -26,13 +22,13 @@ C'est le meilleur moment pour parler de nombreuses fonctionnalités implémenté
- Les interactions avec l'utilisateur _(drag and drop, ouverture de fenêtre modale, etc.)_ - Les interactions avec l'utilisateur _(drag and drop, ouverture de fenêtre modale, etc.)_
- Les appels à des services web _(API REST, etc.)_ - Les appels à des services web _(API REST, etc.)_
<Callout type="note" title="Consommation d'API"> {% callout type="note" title="Consommation d'API" %}
Bien que j'ai mentionné le fait que faire des appels à des services web corresponde entièrement à cette CP, il est important de noter que la consommation d'API est une compétence à part entière, qui sera abordée dans la CP 7 qui correspond à la mise en place de services web et composants métier. Bien que j'ai mentionné le fait que faire des appels à des services web corresponde entièrement à cette CP, il est important de noter que la consommation d'API est une compétence à part entière, qui sera abordée dans la CP 7 qui correspond à la mise en place de services web et composants métier.
Ne te focalise donc pas sur ce que fait l'API en arrière plan, concentre toi sur comment configurer tes requêtes et comment traiter les réponses obtenues ! Ne te focalise donc pas sur ce que fait l'API en arrière plan, concentre toi sur comment configurer tes requêtes et comment traiter les réponses obtenues !
</Callout> {% /callout %}
<Callout type="question" title="Mon site est fait avec React/Angular/Vue.js, donc je valide automatiquement cette CP ?"> {% callout type="question" title="Mon site est fait avec React/Angular/Vue.js, donc je valide automatiquement cette CP ?" %}
Pas si vite ! 😏 Pas si vite ! 😏
Effectivement, ton site répond _(en théorie)_ en tous points pour la compétence actuelle, mais il est important de montrer que tu sais comment fonctionne le JavaScript "vanilla" _(c'est-à-dire sans framework ou bibliothèque)_. Effectivement, ton site répond _(en théorie)_ en tous points pour la compétence actuelle, mais il est important de montrer que tu sais comment fonctionne le JavaScript "vanilla" _(c'est-à-dire sans framework ou bibliothèque)_.
@ -40,7 +36,7 @@ C'est le meilleur moment pour parler de nombreuses fonctionnalités implémenté
Mais on ne va pas se le cacher, si tu as réussi à réaliser un projet avec un framework, c'est déjà un très bon point pour toi qui permet de démontrer que tu as de bonnes connaissances en JavaScript. Mais on ne va pas se le cacher, si tu as réussi à réaliser un projet avec un framework, c'est déjà un très bon point pour toi qui permet de démontrer que tu as de bonnes connaissances en JavaScript.
Cependant il va potentiellement y avoir un défaut majeur sur ton projet : le référencement naturel _(SEO)_. Cependant il va potentiellement y avoir un défaut majeur sur ton projet : le référencement naturel _(SEO)_.
</Callout> {% /callout %}
## Informations complémentaires ## Informations complémentaires
@ -62,7 +58,7 @@ Tu as aussi la possibilité d'utiliser [Next.js](https://nextjs.org/) pour React
Je me permets également de lâcher une bombe sur une certaine techno JS : **jQuery**. Je me permets également de lâcher une bombe sur une certaine techno JS : **jQuery**.
Bon sang, celui-là il me fait penser à un vieux pote qui a pris un coup de vieux... 😅 Bon sang, celui-là il me fait penser à un vieux pote qui a pris un coup de vieux... 😅
<Callout type="question" title="jQuery, c'est quoi ?"> {% callout type="question" title="jQuery, c'est quoi ?" %}
jQuery est une bibliothèque JavaScript qui a été très populaire dans les années 2000 et 2010. jQuery est une bibliothèque JavaScript qui a été très populaire dans les années 2000 et 2010.
Elle a été créée pour simplifier l'écriture de scripts JavaScript et pour faciliter la manipulation du DOM. Elle a été créée pour simplifier l'écriture de scripts JavaScript et pour faciliter la manipulation du DOM.
@ -70,14 +66,15 @@ Bon sang, celui-là il me fait penser à un vieux pote qui a pris un coup de vie
Mais depuis l'arrivée des frameworks front-end comme React, Angular ou Vue.js, jQuery a perdu de sa superbe et est de moins en moins utilisée. Mais depuis l'arrivée des frameworks front-end comme React, Angular ou Vue.js, jQuery a perdu de sa superbe et est de moins en moins utilisée.
Cependant, il est toujours bon de connaître jQuery, car il est possible que tu tombes sur un projet qui l'utilise encore, comme sur des templates Wordpress qui commencent à dater par exemple. Cependant, il est toujours bon de connaître jQuery, car il est possible que tu tombes sur un projet qui l'utilise encore, comme sur des templates Wordpress qui commencent à dater par exemple.
</Callout> {% /callout %}
Mais alors, pourquoi je te parle de jQuery ? Mais alors, pourquoi je te parle de jQuery ?
Eh bien.. pour faire simple, aujourd'hui jQuery est relativement obsolète et surtout très lourd pour ce que ça rajoute à un projet. Eh bien.. pour faire simple, aujourd'hui jQuery est relativement obsolète et surtout très lourd pour ce que ça rajoute à un projet.
Dans la mesure du possible, il est recommandé de ne pas utiliser jQuery pour un nouveau projet, et de préférer JavaScript "vanilla" ou un framework ou bibliothèque front-end comme React, Angular ou Vue.js _(attention, d'un point de vue éco-conception l'utilisation d'un framework n'est pas forcément la meilleure solution)_. Dans la mesure du possible, il est recommandé de ne pas utiliser jQuery pour un nouveau projet, et de préférer JavaScript "vanilla" ou un framework ou bibliothèque front-end comme React, Angular ou Vue.js _(attention, d'un point de vue éco-conception l'utilisation d'un framework n'est pas forcément la meilleure solution)_.
<Callout type="question" title="Mais comment je vais faire pour mes consommations d'API, vu que j'utilisais `jQuery.ajax()` ?!"> {% callout type="question" title="Mais comment je vais faire pour mes consommations d'API, vu que j'utilisais `jQuery.ajax()` ?!" %}
Tout doux, tout doux, il existe une solution ! 😎 Tout doux, tout doux, il existe une solution ! 😎
Si je te parle des requêtes XHR _(XMLHttpRequest)_ tu me dis... ? Si je te parle des requêtes XHR _(XMLHttpRequest)_ tu me dis... ?
@ -93,10 +90,27 @@ Dans la mesure du possible, il est recommandé de ne pas utiliser jQuery pour un
Fetch est une API plus moderne et plus simple à utiliser que les requêtes XHR, et elle est supportée par tous les navigateurs modernes. Fetch est une API plus moderne et plus simple à utiliser que les requêtes XHR, et elle est supportée par tous les navigateurs modernes.
Elle permet de faire des requêtes HTTP de manière asynchrone et de gérer les réponses de manière plus simple. Elle permet de faire des requêtes HTTP de manière asynchrone et de gérer les réponses de manière plus simple.
<tabs.xhrRequest /> {% tabs defaultSelectedTab="xhr" %}
{% tab value="xhr" label="🥉 XHR" %}
{% snippet path="js/xhr/xhr.js" language="js" showLineNumbers=true /%}
{% /tab %}
{% tab value="jquery" label="🥈 jQuery" %}
{% snippet path="js/xhr/jquery-ajax.js" language="js" showLineNumbers=true /%}
{% /tab %}
{% tab value="fetch" label="🥇🏆 Fetch" %}
{% snippet path="js/xhr/fetch.js" language="js" showLineNumbers=true /%}
{% /tab %}
{% /tabs %}
Non seulement `fetch` est plus simple à utiliser et comprendre _(contrairement à XMLHttpRequest)_ mais elle est également plus légère que `jQuery.ajax()` puisqu'elle est native au navigateur ! Alors pourquoi s'en priver ? 😉 Non seulement `fetch` est plus simple à utiliser et comprendre _(contrairement à XMLHttpRequest)_ mais elle est également plus légère que `jQuery.ajax()` puisqu'elle est native au navigateur ! Alors pourquoi s'en priver ? 😉
</Callout>
{% /callout %}
## 🛠️ Ressources conseillées
_En cours de rédaction..._
## 🎯 Critères d'évaluation ## 🎯 Critères d'évaluation
@ -129,18 +143,28 @@ Maintenant, on sait qu'on peut charger de manière "asynchrone" nos images et no
Prenons l'exemple d'un site qui incorpore plusieurs dizaines de vidéos Youtube sur une seule page. On aura donc des `<iframe>` qui vont charger des vidéos Youtube, et ça, c'est pas très éco-responsable... 😕 Prenons l'exemple d'un site qui incorpore plusieurs dizaines de vidéos Youtube sur une seule page. On aura donc des `<iframe>` qui vont charger des vidéos Youtube, et ça, c'est pas très éco-responsable... 😕
Mais on peut améliorer notre page en mettant en place une légère interaction JavaScript pour charger l'iframe uniquement si l'utilisateur clique sur un bouton ! Mais on peut améliorer notre page en mettant en place une légère interaction JavaScript pour charger l'iframe uniquement si l'utilisateur clique sur un bouton !
<Callout type="note" title="Chargement d'un iframe Youtube uniquement au clic de l'utilisateur"> {% callout type="note" title="Chargement d'un iframe Youtube uniquement au clic de l'utilisateur" %}
<tabs.deferIframe />
</Callout>
<Callout type="question" title="Mais ça fait beaucoup de code juste pour charger des iframes, c'est vraiment nécessaire ?"> {% tabs defaultSelectedTab="html" %}
{% tab value="html" label="HTML - 1ère étape" %}
{% snippet path="html/defer-iframe.html" language="html" showLineNumbers=true /%}
{% /tab %}
{% tab value="js" label="JavaScript - 2ème étape" %}
{% snippet path="js/defer-iframe.ts" language="ts" showLineNumbers=true /%}
{% /tab %}
{% /tabs %}
{% /callout %}
{% callout type="question" title="Mais ça fait beaucoup de code juste pour charger des iframes, c'est vraiment nécessaire ?" %}
Pour être franc, il n'y a pas de solution idéale. Mais on peut améliorer les performances du site et gagner en sobriété numérique en ne chargeant pas des ressources lourdes inutilement. Pour être franc, il n'y a pas de solution idéale. Mais on peut améliorer les performances du site et gagner en sobriété numérique en ne chargeant pas des ressources lourdes inutilement.
Est-ce que tu savais que le simple fait de charger un iframe d'une vidéo Youtube demande au navigateur de faire une dizaine de requêtes HTTP pour charger la vidéo, les scripts et les styles de Youtube ? Imagine si on mixe plusieurs sources pour nos iframes, comme Dailymotion, Vimeo, etc. 😱 Est-ce que tu savais que le simple fait de charger un iframe d'une vidéo Youtube demande au navigateur de faire une dizaine de requêtes HTTP pour charger la vidéo, les scripts et les styles de Youtube ? Imagine si on mixe plusieurs sources pour nos iframes, comme Dailymotion, Vimeo, etc. 😱
Et le pire dans tout ça, c'est que le navigateur va charger ces ressources même si l'utilisateur ne comptait pas regarder la vidéo ! Et le pire dans tout ça, c'est que le navigateur va charger ces ressources même si l'utilisateur ne comptait pas regarder la vidéo !
Alors autant faire en sorte que notre site réponde au besoin de l'utilisateur, sans pour autant supprimer les fonctionnalités _(comme nos iframes)_ qui peuvent être utiles. Alors autant faire en sorte que notre site réponde au besoin de l'utilisateur, sans pour autant supprimer les fonctionnalités _(comme nos iframes)_ qui peuvent être utiles.
</Callout> {% /callout %}
## 🧠 Documentations ## 🧠 Documentations

View File

@ -0,0 +1,23 @@
---
title: Activité Type 1 - Développer la partie front-end d'une application web ou web mobile sécurisée
description: Synthèse et explications des attentes relatives à l'activité type 1 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
tags: [DWWM]
---
## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 15 et 16
- RE _(mise à jour du 02/07/2024)_, page 9
## 📋 En résumé
Cette activité type concerne tout ce qui est relatif à la conception _(maquettes, arborescence etc.)_ et à la création de l'interface.
Voyons un peu plus en détail ce qui est attendu pour chacune de ces compétences professionnelles ! 🚀
Elle est divisée en 4 **compétences professionnelles** _(CP)_ :
- **CP 1** : Installer et configurer son environnement de travail en fonction du projet web ou web mobile
- **CP 2** : Maquetter des interfaces utilisateur web ou web mobile
- **CP 3** : Réaliser des interfaces utilisateur statiques web ou web mobile
- **CP 4** : Développer la partie dynamique des interfaces utilisateur web ou web mobile

View File

@ -4,10 +4,6 @@ description: Synthèse et explications des attentes relatives à la compétence
tags: [DWWM] tags: [DWWM]
--- ---
import QuickLinks from "@/components/QuickLinks";
import Callout from "@/components/Callout";
import tabs from "./tabs";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 23 et 24 - REAC _(mise à jour du 02/07/2024)_, pages 23 et 24
@ -18,11 +14,11 @@ import tabs from "./tabs";
Le front-end : c'est **fini** ! Le front-end : c'est **fini** !
Mais avant de nous attaquer au back-end d'un point de vue code, on va voir ce qui est attendu dans cette CP qui parle de la mise en place d'une base de données relationnelle. Mais avant de nous attaquer au back-end d'un point de vue code, on va voir ce qui est attendu dans cette CP qui parle de la mise en place d'une base de données relationnelle.
<Callout type="question" title="Mais attend ! J'ai juste une base de données non relationnelle à mettre en place, c'est bon ?"> {% callout type="question" title="Mais attend ! J'ai juste une base de données non relationnelle à mettre en place, c'est bon ?" %}
J'aurai aimé te dire que oui, mais ça va être un poil trop léger pour cette compétence... J'aurai aimé te dire que oui, mais ça va être un poil trop léger pour cette compétence...
Mais garde sous la main ta base de données non relationnelles Mais garde sous la main ta base de données non relationnelles
pour la prochaine compétence, ça te servira 😉 pour la prochaine compétence, ça te servira 😉
</Callout> {% /callout %}
## 🎨 Modélisation de la base de données ## 🎨 Modélisation de la base de données
@ -45,12 +41,12 @@ Donc si tu réalises un dictionnaire de données après avoir fait ton MPD, c'es
Si tu souhaites en savoir plus sur la méthode Merise, je t'invite à lire les articles dédiés sur le Memento. Si tu souhaites en savoir plus sur la méthode Merise, je t'invite à lire les articles dédiés sur le Memento.
Voici un lien vers l'introduction de la méthode Merise ! Voici un lien vers l'introduction de la méthode Merise !
<QuickLinks.QuickLink {% quick-link
title="Introduction à Merise" title="Introduction à Merise"
href="/docs/merise/" href="/docs/merise/"
description="Parlons un peu de Merise, la fameuse méthodologie de modélisation pour la conception de bases de données." description="Parlons un peu de Merise, la fameuse méthodologie de modélisation pour la conception de bases de données."
icon="presets" icon="presets"
/> /%}
## 💾 Sauvegardes de la base de données ## 💾 Sauvegardes de la base de données
@ -72,18 +68,18 @@ Sans rentrer dans les détails de configuration d'une tâche cron, on va devoir
- **La fréquence d'exécution** : qui va déterminer à quelle fréquence notre tâche va s'exécuter _(toutes les heures, tous les jours, toutes les semaines, etc.)_ - **La fréquence d'exécution** : qui va déterminer à quelle fréquence notre tâche va s'exécuter _(toutes les heures, tous les jours, toutes les semaines, etc.)_
- **Le compte utilisateur** : qui va exécuter la tâche, généralement le compte de l'utilisateur qui a les droits d'accès à la base de données - **Le compte utilisateur** : qui va exécuter la tâche, généralement le compte de l'utilisateur qui a les droits d'accès à la base de données
<Callout type="note" title="Exemple de script `bash` pour sauvegarder une base de données PostgreSQL"> {% callout type="note" title="Exemple de script `bash` pour sauvegarder une base de données PostgreSQL" %}
<tabs.bashPgCronFile /> {% snippet path="bash/pg_cron_file.sh" language="bash" showLineNumbers=true /%}
Ce script va permettre de sauvegarder une base de données PostgreSQL en exportant son contenu dans un fichier SQL. Ce script va permettre de sauvegarder une base de données PostgreSQL en exportant son contenu dans un fichier SQL.
Il est important de remplacer les variables `DB_USER`, `DB_NAME` et `BACKUP_DIR` par les informations de ta base de données. Il est important de remplacer les variables `DB_USER`, `DB_NAME` et `BACKUP_DIR` par les informations de ta base de données.
Une fois ce script créé, il suffira de le rendre exécutable et de le planifier dans une tâche cron pour automatiser la sauvegarde de ta base de données. Une fois ce script créé, il suffira de le rendre exécutable et de le planifier dans une tâche cron pour automatiser la sauvegarde de ta base de données.
<tabs.bashPgCronRegister /> {% snippet path="bash/pg_cron_register.sh" language="bash" /%}
Et voilà ! Ta base de données sera sauvegardée toutes les nuits à minuit, sans que tu aies besoin d'intervenir manuellement. Et voilà ! Ta base de données sera sauvegardée toutes les nuits à minuit, sans que tu aies besoin d'intervenir manuellement.
</Callout> {% /callout %}
## 🛡️ Sécurité et confidentialité des données ## 🛡️ Sécurité et confidentialité des données
@ -101,18 +97,18 @@ Mais la sécurité ne s'arrête pas là, il est également important de garantir
- **Le chiffrement des données** : pour éviter que des tiers puissent lire les données stockées, en cas de fuite - **Le chiffrement des données** : pour éviter que des tiers puissent lire les données stockées, en cas de fuite
<Callout type="warning" title="Identifiants de connexion"> {% callout type="warning" title="Identifiants de connexion" %}
Même en développement sur ta machine locale, prend l'habitude de ne jamais utiliser les identifiants par défaut de ta base de données _(comme `root` sans mot de passe par exemple)_. Même en développement sur ta machine locale, prend l'habitude de ne jamais utiliser les identifiants par défaut de ta base de données _(comme `root` sans mot de passe par exemple)_.
L'objectif est de te mettre dans les conditions réelles d'un environnement de production, où la sécurité est primordiale. Ça t'évitera de prendre de mauvaises habitudes qui pourraient te coûter cher par la suite. L'objectif est de te mettre dans les conditions réelles d'un environnement de production, où la sécurité est primordiale. Ça t'évitera de prendre de mauvaises habitudes qui pourraient te coûter cher par la suite.
</Callout> {% /callout %}
## Informations complémentaires ## Informations complémentaires
Si tu utilises une autre méthode de modélisation que Merise, tu as évidemment le droit de le faire ! Si tu utilises une autre méthode de modélisation que Merise, tu as évidemment le droit de le faire !
Fais juste attention à une chose... Fais juste attention à une chose...
<Callout type="warning" title="Attention au respect des documents !"> {% callout type="warning" title="Attention au respect des documents !" %}
Si tu utilises une autre méthode de modélisation, fais attention à bien respecter les noms des documents. Si tu utilises une autre méthode de modélisation, fais attention à bien respecter les noms des documents.
Par exemple, si tu fais un MCD, il faut que tu l'appelles comme ça et pas autrement. Par exemple, si tu fais un MCD, il faut que tu l'appelles comme ça et pas autrement.
@ -122,13 +118,17 @@ Fais juste attention à une chose...
N'oublie pas : tu as toutes les ressources nécessaires pour réaliser un MCD, un MLD ou un MPD sur le Memento 😉 N'oublie pas : tu as toutes les ressources nécessaires pour réaliser un MCD, un MLD ou un MPD sur le Memento 😉
<QuickLinks.QuickLink {% quick-link
title="Introduction à Merise" title="Introduction à Merise"
href="/docs/merise/" href="/docs/merise/"
description="Parlons un peu de Merise, la fameuse méthodologie de modélisation pour la conception de bases de données." description="Parlons un peu de Merise, la fameuse méthodologie de modélisation pour la conception de bases de données."
icon="presets" icon="presets"
/> /%}
</Callout> {% /callout %}
## 🛠️ Ressources conseillées
_En cours de rédaction..._
## 🎯 Critères d'évaluation ## 🎯 Critères d'évaluation

View File

@ -4,8 +4,6 @@ description: Synthèse et explications des attentes relatives à la compétence
tags: [DWWM] tags: [DWWM]
--- ---
import Callout from "@/components/Callout";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 25 et 26 - REAC _(mise à jour du 02/07/2024)_, pages 25 et 26
@ -18,25 +16,25 @@ On va pouvoir souffler un coup en parlant maintenant de l'accès à ces bases de
Et tu sais quoi, comme tout ce qu'on a vu jusqu'à maintenant, on va alléger un peu les choses en parlant de merveilleux outils comme les **ORM** et les **ODM** ! Et tu sais quoi, comme tout ce qu'on a vu jusqu'à maintenant, on va alléger un peu les choses en parlant de merveilleux outils comme les **ORM** et les **ODM** !
<Callout type="question" title="C'est quoi un ORM et ODM ? Quelles sont les différences ?"> {% callout type="question" title="C'est quoi un ORM et ODM ? Quelles sont les différences ?" %}
Les ORM _(Object-Relational Mapping)_ et les ODM _(Object-Document Mapper)_ sont des outils qui permettent de faire le lien entre les bases de données et les langages de programmation. Les ORM _(Object-Relational Mapping)_ et les ODM _(Object-Document Mapper)_ sont des outils qui permettent de faire le lien entre les bases de données et les langages de programmation.
- Les ORM sont utilisés pour les bases de données relationnelles, comme MySQL, PostgreSQL ou SQLite. Ils permettent de manipuler les données de la base de données sous forme d'objets, ce qui facilite leur utilisation dans le code. - Les ORM sont utilisés pour les bases de données relationnelles, comme MySQL, PostgreSQL ou SQLite. Ils permettent de manipuler les données de la base de données sous forme d'objets, ce qui facilite leur utilisation dans le code.
- Les ODM sont utilisés pour les bases de données NoSQL, comme MongoDB. Ils fonctionnent de la même manière que les ORM, mais pour les bases de données NoSQL. - Les ODM sont utilisés pour les bases de données NoSQL, comme MongoDB. Ils fonctionnent de la même manière que les ORM, mais pour les bases de données NoSQL.
En gros, les ORM et les ODM permettent de simplifier la manipulation des données dans le code, en évitant d'avoir à écrire des requêtes à la main. En gros, les ORM et les ODM permettent de simplifier la manipulation des données dans le code, en évitant d'avoir à écrire des requêtes à la main.
</Callout> {% /callout %}
Alleeeez, on va voir ça de plus près ! 😎 Alleeeez, on va voir ça de plus près ! 😎
## ⚙️ Utilisation d'un ORM ou d'un ODM ## ⚙️ Utilisation d'un ORM ou d'un ODM
<Callout type="question" title="Je fais mes requêtes SQL à la main, il faut que j'apprenne à utiliser un ORM/ODM ?"> {% callout type="question" title="Je fais mes requêtes SQL à la main, il faut que j'apprenne à utiliser un ORM/ODM ?" %}
**Non** ! _(enfin, pas pour passer la certification en tout cas)_ **Non** ! _(enfin, pas pour passer la certification en tout cas)_
D'un certain côté, c'est nettement plus intéressant de savoir réaliser les requêtes par toi-même, sans utiliser d'outils qui génèrent du SQL à ta place. D'un certain côté, c'est nettement plus intéressant de savoir réaliser les requêtes par toi-même, sans utiliser d'outils qui génèrent du SQL à ta place.
En entreprise, tu vas certainement utiliser ces fameux outils, mais dès que l'on va chercher à avoir les requêtes les plus optimisées possibles, il va falloir mettre les mains dans le cambouis ! En entreprise, tu vas certainement utiliser ces fameux outils, mais dès que l'on va chercher à avoir les requêtes les plus optimisées possibles, il va falloir mettre les mains dans le cambouis !
</Callout> {% /callout %}
Mais alors, pourquoi faire des requêtes à la main quand on peut utiliser un ORM ou un ODM ? Mais alors, pourquoi faire des requêtes à la main quand on peut utiliser un ORM ou un ODM ?
Eh bien, c'est simple : les ORM et les ODM te permettent de manipuler les données de la base de données sous forme d'objets, ce qui est beaucoup plus pratique et lisible dans le code. Eh bien, c'est simple : les ORM et les ODM te permettent de manipuler les données de la base de données sous forme d'objets, ce qui est beaucoup plus pratique et lisible dans le code.
@ -50,7 +48,7 @@ D'autre part, ces outils peuvent aussi avoir un impact sur les performances de t
Imagines un peu si tu réalises une application qui doit gérer des tonnes de données en temps réel, comme une application de spéculation boursière 😅 Imagines un peu si tu réalises une application qui doit gérer des tonnes de données en temps réel, comme une application de spéculation boursière 😅
<Callout type="warning" title="Les ORM et ODM, c'est cool, mais pas magique"> {% callout type="warning" title="Les ORM et ODM, c'est cool, mais pas magique" %}
Si tu comptes présenter un projet avec un ORM ou un ODM, il va falloir que tu sois capable de justifier tes choix techniques et de montrer que tu sais ce que tu fais... et ce que fait l'outil que tu utilises ! Si tu comptes présenter un projet avec un ORM ou un ODM, il va falloir que tu sois capable de justifier tes choix techniques et de montrer que tu sais ce que tu fais... et ce que fait l'outil que tu utilises !
Tu dois être capable de répondre à des questions comme celle-ci : Tu dois être capable de répondre à des questions comme celle-ci :
@ -58,7 +56,7 @@ Imagines un peu si tu réalises une application qui doit gérer des tonnes de do
> Quelle est la requête SQL générée par l'ORM/ODM pour cette opération ? > Quelle est la requête SQL générée par l'ORM/ODM pour cette opération ?
Ton jury ne cherchera pas à te piéger, mais il attend de toi que tu sois capable de comprendre ce que tu fais et pourquoi tu le fais. Ton jury ne cherchera pas à te piéger, mais il attend de toi que tu sois capable de comprendre ce que tu fais et pourquoi tu le fais.
</Callout> {% /callout %}
## 🔎 Intégrité des données ## 🔎 Intégrité des données
@ -84,13 +82,13 @@ Bien que notre bases de données se doit d'être sécurisée dans son accès et
Pour les mots de passe, on va les hacher avant de les stocker dans la base de données. Pour les mots de passe, on va les hacher avant de les stocker dans la base de données.
<Callout type="question" title="C'est quoi le hachage ?"> {% callout type="question" title="C'est quoi le hachage ?" %}
Le hachage est une manière de sécuriser un contenu textuel en le transformant en une chaîne de caractères "aléatoire", appelée **hash**. Le hachage est une manière de sécuriser un contenu textuel en le transformant en une chaîne de caractères "aléatoire", appelée **hash**.
Il est important de noter que le hachage est **unidirectionnel**, c'est-à-dire qu'il est impossible de retrouver la valeur d'origine à partir de son hash contrairement au **chiffrement**. Il est important de noter que le hachage est **unidirectionnel**, c'est-à-dire qu'il est impossible de retrouver la valeur d'origine à partir de son hash contrairement au **chiffrement**.
</Callout> {% /callout %}
<Callout type="question" title="Et le chiffrement, ça sert à quoi ?"> {% callout type="question" title="Et le chiffrement, ça sert à quoi ?" %}
Comme le hachage, le chiffrement permet de sécuriser des données. Cependant : le chiffrement est **bidirectionnel**. Comme le hachage, le chiffrement permet de sécuriser des données. Cependant : le chiffrement est **bidirectionnel**.
C'est à dire que l'on peut retrouver les données d'origine à partir des données chiffrées. C'est à dire que l'on peut retrouver les données d'origine à partir des données chiffrées.
@ -104,15 +102,15 @@ Pour les mots de passe, on va les hacher avant de les stocker dans la base de do
> >
> Message chiffré : "Erqmruxu" > Message chiffré : "Erqmruxu"
<Callout type="warning" title="Attention !"> {% callout type="warning" title="Attention !" %}
Le chiffrement n'est pas une solution de sécurité absolue, il est possible de retrouver les données d'origine à partir des données chiffrées. Le chiffrement n'est pas une solution de sécurité absolue, il est possible de retrouver les données d'origine à partir des données chiffrées.
D'ailleurs le chiffre de César est un chiffrement très simple à casser, on ne va donc pas l'utiliser pour protéger les données sensibles ! D'ailleurs le chiffre de César est un chiffrement très simple à casser, on ne va donc pas l'utiliser pour protéger les données sensibles !
</Callout> {% /callout %}
On va privilégier un algorithme de chiffrement qui se base sur une **clé secrète**, qui sera la clé pour chiffrer et déchiffrer les données. On va privilégier un algorithme de chiffrement qui se base sur une **clé secrète**, qui sera la clé pour chiffrer et déchiffrer les données.
C'est d'ailleurs plus ou moins ce qui est fait avec la célèbre [machine Enigma](<https://fr.wikipedia.org/wiki/Enigma_(machine)>) utilisée par les allemands pendant la Seconde Guerre Mondiale pour chiffrer leurs messages et éviter qu'ils soient interceptés et compris par les alliés. C'est d'ailleurs plus ou moins ce qui est fait avec la célèbre [machine Enigma](<https://fr.wikipedia.org/wiki/Enigma_(machine)>) utilisée par les allemands pendant la Seconde Guerre Mondiale pour chiffrer leurs messages et éviter qu'ils soient interceptés et compris par les alliés.
</Callout> {% /callout %}
Mais alors, comment on peut s'y prendre ? Mais alors, comment on peut s'y prendre ?
@ -150,12 +148,12 @@ Je ne m'étalerai pas sur ce sujet, mais désolé MySQL/MariaDB de ne pas être
Les ressources que je m'apprête à te recommander sont un peu plus avancées, mais ce sont d'excellentes portes d'entrées vers des métiers comme DBA par exemple. Les ressources que je m'apprête à te recommander sont un peu plus avancées, mais ce sont d'excellentes portes d'entrées vers des métiers comme DBA par exemple.
Tu retrouveras des notions très bien expliquées et pertinentes pour t'améliorer sur le sujet dans les ressources de [Dalibo](https://www.dalibo.com/formations). Tu retrouveras des notions très bien expliquées et pertinentes pour t'améliorer sur le sujet dans les ressources de [Dalibo](https://www.dalibo.com/formations).
<Callout type="note" title="Gratuité des formations Dalibo"> {% callout type="note" title="Gratuité des formations Dalibo" %}
Dalibo propose des formations, mais qui ne sont pas gratuites pour autant. Dalibo propose des formations, mais qui ne sont pas gratuites pour autant.
Seuls les supports de cours sont disponibles gratuitement, aux formats EPUB et PDF. Seuls les supports de cours sont disponibles gratuitement, aux formats EPUB et PDF.
Tu peux retrouver ces supports sur la page [Formations](https://www.dalibo.com/formations) du site de Dalibo. Tu peux retrouver ces supports sur la page [Formations](https://www.dalibo.com/formations) du site de Dalibo.
</Callout> {% /callout %}
## 🧠 Documentations ## 🧠 Documentations

View File

@ -4,10 +4,6 @@ description: Synthèse et explications des attentes relatives à la compétence
tags: [DWWM] tags: [DWWM]
--- ---
import Callout from "@/components/Callout";
import Image from "@/components/Image";
import tabs from "./tabs";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 27 et 28 - REAC _(mise à jour du 02/07/2024)_, pages 27 et 28
@ -29,31 +25,31 @@ Le design pattern MVC est un modèle d'architecture logicielle qui sépare les d
- **Vue** : représente l'interface utilisateur. C'est ce que l'utilisateur voit et avec quoi il interagit. - **Vue** : représente l'interface utilisateur. C'est ce que l'utilisateur voit et avec quoi il interagit.
- **Contrôleur** : fait le lien entre le modèle et la vue. Il contient la logique métier de l'application. - **Contrôleur** : fait le lien entre le modèle et la vue. Il contient la logique métier de l'application.
<Callout type="warning" title="Les schémas disponibles en ligne"> {% callout type="warning" title="Les schémas disponibles en ligne" %}
Il existe de nombreux schémas qui expliquent le design pattern MVC mais ils ne sont pas tous corrects. Il existe de nombreux schémas qui expliquent le design pattern MVC mais ils ne sont pas tous corrects.
Enfin, si, ils sont corrects... mais certains ne s'appliquent pas à tous les frameworks et architectures. Enfin, si, ils sont corrects... mais certains ne s'appliquent pas à tous les frameworks et architectures.
</Callout> {% /callout %}
Pour t'aider à mieux te représenter un schéma MVC avec les ordres de flux de données et de contrôle : Pour t'aider à mieux te représenter un schéma MVC avec les ordres de flux de données et de contrôle :
<Image alt="Schéma MVC pour une application web basique" src="/patterns/mvc.webp" class="max-h-96 mx-auto" /> {% img alt="Schéma MVC pour une application web basique" src="/patterns/mvc.webp" className="max-h-96 mx-auto" /%}
<Callout type="question" title="Pourquoi la Vue ne retourne pas directement au client ?"> {% callout type="question" title="Pourquoi la Vue ne retourne pas directement au client ?" %}
La vue ne retourne pas directement au client car elle doit passer par le contrôleur. La vue ne retourne pas directement au client car elle doit passer par le contrôleur.
On ne s'en rend pas forcément compte, mais la vue est généralement générée par le contrôleur via un moteur de template _(EJS, Twig, etc.)_. On ne s'en rend pas forcément compte, mais la vue est généralement générée par le contrôleur via un moteur de template _(EJS, Twig, etc.)_.
Une fois le HTML généré, le contrôleur s'occupe de l'envoyer dans la réponse HTTP au client. Une fois le HTML généré, le contrôleur s'occupe de l'envoyer dans la réponse HTTP au client.
C'est ce qui permet de garder une séparation entre la logique métier et l'interface utilisateur. C'est ce qui permet de garder une séparation entre la logique métier et l'interface utilisateur.
</Callout> {% /callout %}
Le concept est simple : chaque partie de l'application a un **rôle bien défini** et ne doit pas empiéter sur le rôle des autres. Le concept est simple : chaque partie de l'application a un **rôle bien défini** et ne doit pas empiéter sur le rôle des autres.
<Callout type="question" title="Et si j'ai des middlewares ?"> {% callout type="question" title="Et si j'ai des middlewares ?" %}
Dans la majorité des cas, les middlewares s'exécutent avant le contrôleur même si on peut en avoir à différents moments de la circulation de la donnée. Dans la majorité des cas, les middlewares s'exécutent avant le contrôleur même si on peut en avoir à différents moments de la circulation de la donnée.
Si tu as déjà utilisé Express, tu as probablement déjà utilisé un middleware pour vérifier si l'utilisateur est connecté avant de lui afficher une page qui est réservée aux utilisateurs connectés. Si tu as déjà utilisé Express, tu as probablement déjà utilisé un middleware pour vérifier si l'utilisateur est connecté avant de lui afficher une page qui est réservée aux utilisateurs connectés.
</Callout> {% /callout %}
<Callout type="note" title="Le cas de React (ou Vue, Angular, Solid, etc.)"> {% callout type="note" title="Le cas de React (ou Vue, Angular, Solid, etc.)" %}
D'après toi, est-ce que React doit être considéré comme la vue dans le design pattern MVC ? D'après toi, est-ce que React doit être considéré comme la vue dans le design pattern MVC ?
La réponse est **non** ! La réponse est **non** !
@ -63,26 +59,26 @@ Le concept est simple : chaque partie de l'application a un **rôle bien défini
On va donc faire simple : on parlera plutôt d'une architecture "client-serveur" avec React côté client et notre API côté serveur. On va donc faire simple : on parlera plutôt d'une architecture "client-serveur" avec React côté client et notre API côté serveur.
Mais ça n'empêche pas que ton API puisse être une API REST _(ou GraphQL)_ qui respecte le design pattern MVC ! Mais ça n'empêche pas que ton API puisse être une API REST _(ou GraphQL)_ qui respecte le design pattern MVC !
Tout dépendra de si tu demandes dans ton serveur back-end de retourner une vue _(HTML)_ au navigateur. Tout dépendra de si tu demandes dans ton serveur back-end de retourner une vue _(HTML)_ au navigateur.
</Callout> {% /callout %}
## 🧑‍⚖️ Règles et conventions de nommage ## 🧑‍⚖️ Règles et conventions de nommage
Peu importe le contexte dans lequel tu réalises le projet que tu vas soutenir face à ton jury, tu dois respecter les règles et conventions de nommage de l'entreprise. Peu importe le contexte dans lequel tu réalises le projet que tu vas soutenir face à ton jury, tu dois respecter les règles et conventions de nommage de l'entreprise.
Si tu fais un projet personnel, tu peux définir les tiennes, du moment que tu es en mesure de les expliquer à ton jury et que tu les respectes du début à la fin. Si tu fais un projet personnel, tu peux définir les tiennes, du moment que tu es en mesure de les expliquer à ton jury et que tu les respectes du début à la fin.
<Callout type="note" title="La cohérence, c'est la clé"> {% callout type="note" title="La cohérence, c'est la clé" %}
Pense à être cohérent en ce qui concerne la langue utilisée. Pense à être cohérent en ce qui concerne la langue utilisée.
<Callout type="warning" title="Pas de franglais !"> {% callout type="warning" title="Pas de franglais !" %}
Évite de mélanger plusieurs langues dans tes nommages. Évite de mélanger plusieurs langues dans tes nommages.
Si tu choisis de travailler en français, reste en français. Si tu choisis de travailler en français, reste en français.
Si tu choisis de travailler en anglais, reste en anglais. Si tu choisis de travailler en anglais, reste en anglais.
</Callout> {% /callout %}
D'ailleurs, je te recommande chaudement de travailler en anglais ne serait-ce que pour te familiariser avec la langue de Shakespeare qui est, on le rappelle, la langue la plus répandue dans le monde de l'informatique. D'ailleurs, je te recommande chaudement de travailler en anglais ne serait-ce que pour te familiariser avec la langue de Shakespeare qui est, on le rappelle, la langue la plus répandue dans le monde de l'informatique.
Tu as évidemment le droit d'utiliser des traducteurs en ligne pour t'aider à trouver le bon mot _(ou la bonne expression)_, on ne te demande pas d'être bilingue ! Tu as évidemment le droit d'utiliser des traducteurs en ligne pour t'aider à trouver le bon mot _(ou la bonne expression)_, on ne te demande pas d'être bilingue !
</Callout> {% /callout %}
Au delà de la langue utilisée, on va également parler de la syntaxe des noms de fichiers, dossiers, classes, méthodes, variables, etc. Au delà de la langue utilisée, on va également parler de la syntaxe des noms de fichiers, dossiers, classes, méthodes, variables, etc.
Pour t'aider à te lancer, tu peux t'inspirer des conventions de nommage les plus répandues que tu trouveras facilement en ligne. Pour t'aider à te lancer, tu peux t'inspirer des conventions de nommage les plus répandues que tu trouveras facilement en ligne.
@ -111,16 +107,54 @@ Ce type de test se compose de trois parties :
Si on prend l'exemple d'un formulaire d'inscription où nous vérifions que l'utilisateur utilise une adresse e-mail valide et unique, ainsi qu'un mot de passe fort _(12 caractères minimum, au moins une majuscule, une minuscule, un chiffre et un caractère spécial)_, voici ce que pourrait donner notre jeu d'essai : Si on prend l'exemple d'un formulaire d'inscription où nous vérifions que l'utilisateur utilise une adresse e-mail valide et unique, ainsi qu'un mot de passe fort _(12 caractères minimum, au moins une majuscule, une minuscule, un chiffre et un caractère spécial)_, voici ce que pourrait donner notre jeu d'essai :
<tabs.testSuite /> {% tabs defaultSelectedTab="invalid" %}
{% tab value="invalid" label="Données invalides" %}
<Callout type="note" title="Faire ces tests facilement"> - **Les données d'entrée** :
- Adresse e-mail : `mauvaise-adresse@email`
- Mot de passe : `password`
- **Les données de sortie attendues** :
- Erreur : `Adresse e-mail invalide`
- Erreur : `Le mot de passe ne respecte pas les critères de sécurité requis`
- **Les données de sortie obtenues** :
- Erreur : `Adresse e-mail invalide`
- Erreur : `Le mot de passe ne respecte pas les critères de sécurité requis`
{% /tab %}
{% tab value="valid" label="Données valides" %}
- **Les données d'entrée** :
- Adresse e-mail : `bonne-adresse@email.fr`
- Mot de passe : `Password123&` _(bon, le mot de passe n'est absolument pas "fort", mais il respecte les critères imposés)_
- **Les données de sortie attendues** :
- Succès : `Utilisateur inscrit avec succès`
- **Les données de sortie obtenues** :
- Succès : `Utilisateur inscrit avec succès`
{% /tab %}
{% tab value="email-already-used" label="Adresse email déjà utilisée" %}
- **Les données d'entrée** :
- Adresse e-mail : `adresse-email@utilisee.fr`
- Mot de passe : `Password123&`
- **Les données de sortie attendues** :
- Erreur : `Adresse e-mail déjà utilisée`
- **Les données de sortie obtenues** :
- Erreur : `Adresse e-mail déjà utilisée`
{% /tab %}
{% /tabs %}
{% callout type="note" title="Faire ces tests facilement" %}
Si je te parle de client HTTP, tu me réponds... ? Si je te parle de client HTTP, tu me réponds... ?
[Bruno](https://www.usebruno.com/) ? [Postman](https://www.postman.com/) ? [Insomnia](https://insomnia.rest/) ? [Bruno](https://www.usebruno.com/) ? [Postman](https://www.postman.com/) ? [Insomnia](https://insomnia.rest/) ?
Bingo ! 🎉 Bingo ! 🎉
Utiliser un client HTTP comme Bruno, Postman ou Insomnia te permettra de tester facilement les routes de ton API, et de vérifier que les données que tu envoies sont bien traitées par ton serveur. Utiliser un client HTTP comme Bruno, Postman ou Insomnia te permettra de tester facilement les routes de ton API, et de vérifier que les données que tu envoies sont bien traitées par ton serveur.
</Callout> {% /callout %}
### 🧪 Les tests unitaires ### 🧪 Les tests unitaires

View File

@ -4,9 +4,6 @@ description: Synthèse et explications des attentes relatives à la compétence
tags: [DWWM, Déploiement, Backend, Reverse Proxy, Serveur Web] tags: [DWWM, Déploiement, Backend, Reverse Proxy, Serveur Web]
--- ---
import Callout from "@/components/Callout";
import tabs from "./tabs";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, page 29 - REAC _(mise à jour du 02/07/2024)_, page 29
@ -33,11 +30,11 @@ Beaucoup d'hébergeurs proposent des serveurs mutualisés, c'est-à-dire que plu
Il s'agit d'une solution moins coûteuse que les serveurs dédiés ou les VPS, mais qui peut être moins performante en fonction de la qualité de l'hébergeur. Il s'agit d'une solution moins coûteuse que les serveurs dédiés ou les VPS, mais qui peut être moins performante en fonction de la qualité de l'hébergeur.
Cependant, pour un site web de petite ou moyenne envergure, un serveur mutualisé peut suffire et surtout : il est souvent plus simple à gérer. Cependant, pour un site web de petite ou moyenne envergure, un serveur mutualisé peut suffire et surtout : il est souvent plus simple à gérer.
<Callout type="note" title="Parenthèse éco-conception"> {% callout type="note" title="Parenthèse éco-conception" %}
On peut également considérer que l'utilisation de serveurs mutualisés est plus écologique, car elle permet de mutualiser les ressources et de limiter le nombre de serveurs physiques nécessaires pour héberger des sites web. On peut également considérer que l'utilisation de serveurs mutualisés est plus écologique, car elle permet de mutualiser les ressources et de limiter le nombre de serveurs physiques nécessaires pour héberger des sites web.
Moins de matériel physique = moins de consommation d'énergie = moins d'émissions de CO2. Moins de matériel physique = moins de consommation d'énergie = moins d'émissions de CO2.
</Callout> {% /callout %}
Mais attention, les serveurs mutualisés ne permettent pas de configurer entièrement le serveur _(par exemple, tu ne pourras pas installer un serveur Node.js sur un serveur mutualisé qui n'est pas prévu pour)_. Mais attention, les serveurs mutualisés ne permettent pas de configurer entièrement le serveur _(par exemple, tu ne pourras pas installer un serveur Node.js sur un serveur mutualisé qui n'est pas prévu pour)_.
Il est donc important de bien se renseigner sur les fonctionnalités proposées par l'hébergeur avant de choisir un serveur mutualisé. Il est donc important de bien se renseigner sur les fonctionnalités proposées par l'hébergeur avant de choisir un serveur mutualisé.
@ -57,7 +54,8 @@ La configuration la plus classique que l'on retrouvera sur un serveur dédié ou
- Un gestionnaire de paquets comme APT, YUM, etc. - Un gestionnaire de paquets comme APT, YUM, etc.
- Un pare-feu comme UFW, iptables, etc. - Un pare-feu comme UFW, iptables, etc.
<Callout type="note" title="Dernière parenthèse éco-conception"> {% callout type="note" title="Dernière parenthèse éco-conception" %}
Les serveurs dédiés et les VPS sont souvent plus énergivores que les serveurs mutualisés, car ils sont allumés en permanence _(sauf configuration spécifique)_ et consomment plus d'énergie pour fonctionner. Les serveurs dédiés et les VPS sont souvent plus énergivores que les serveurs mutualisés, car ils sont allumés en permanence _(sauf configuration spécifique)_ et consomment plus d'énergie pour fonctionner.
Sur le papier, ça sonne moins bien, mais dans le concret : un serveur dédié ou un VPS bien configuré peut être plus écologique qu'un serveur mutualisé mal configuré _(qui consomme plus d'énergie pour moins de performance)_. Sur le papier, ça sonne moins bien, mais dans le concret : un serveur dédié ou un VPS bien configuré peut être plus écologique qu'un serveur mutualisé mal configuré _(qui consomme plus d'énergie pour moins de performance)_.
@ -70,7 +68,8 @@ La configuration la plus classique que l'on retrouvera sur un serveur dédié ou
D'ailleurs, sur toute la partie RGPD : Infomaniak a une politique de confidentialité et de sécurité très sérieuse que tu peux retrouver [juste ici](https://www.infomaniak.com/fr/cgv/reglement-general-protection-donnees). D'ailleurs, sur toute la partie RGPD : Infomaniak a une politique de confidentialité et de sécurité très sérieuse que tu peux retrouver [juste ici](https://www.infomaniak.com/fr/cgv/reglement-general-protection-donnees).
Et promis : elle est lisible et compréhensible, pas comme certaines politiques de confidentialité qui sont plus longues que l'intégrale de la saga Harry Potter. Et promis : elle est lisible et compréhensible, pas comme certaines politiques de confidentialité qui sont plus longues que l'intégrale de la saga Harry Potter.
</Callout>
{% /callout %}
Avant d'arrêter de parler de serveurs à configurer soi-même, je me permets d'ouvrir une toute petite rubrique sur la mise en ligne d'applications tournant sur des ports autres que le 80 (ou 443 pour le HTTPS), comme on peut le faire avec un serveur Node.js. Avant d'arrêter de parler de serveurs à configurer soi-même, je me permets d'ouvrir une toute petite rubrique sur la mise en ligne d'applications tournant sur des ports autres que le 80 (ou 443 pour le HTTPS), comme on peut le faire avec un serveur Node.js.
@ -88,11 +87,26 @@ Nginx est un serveur web qui est souvent utilisé comme reverse proxy, notamment
Prenons l'exemple d'un serveur Node.js qui tourne sur le port 5000. Prenons l'exemple d'un serveur Node.js qui tourne sur le port 5000.
<Callout type="note" title="Configuration Nginx pour un reverse proxy"> {% callout type="note" title="Configuration Nginx pour un reverse proxy" %}
<tabs.nginxReverseProxy />
```nginx
# Ensemble de configurations pour un serveur Nginx
server {
listen 80; # Port 80 pour les requêtes HTTP
listen [::]:80; # Port 80 pour les requêtes HTTP en IPv6
server_name mon-domaine.com; # Ton domaine qui pointe vers ton serveur web qui fait tourner ton application Node.js
# Configuration pour le reverse proxy, qui va rediriger les requêtes vers le port 5000
location / {
proxy_pass http://0.0.0.0:5000; # Redirige les requêtes vers le port 5000 (interne au serveur)
proxy_set_header X-Forwarded-For $remote_addr; # Envoie l'adresse IP du client à l'application Node.js dans le header
proxy_set_header Host $http_host; # Envoie le nom de domaine à l'application Node.js dans le header
}
}
```
Oui, c'est aussi simple que ça ! Alors effectivement, il y a d'autres configurations possibles, mais pour un usage basique : c'est tout ce dont tu as besoin. Oui, c'est aussi simple que ça ! Alors effectivement, il y a d'autres configurations possibles, mais pour un usage basique : c'est tout ce dont tu as besoin.
</Callout> {% /callout %}
### 📦 Caddy ### 📦 Caddy

View File

@ -4,8 +4,6 @@ description: Synthèse et explications des attentes relatives à l'activité typ
tags: [DWWM] tags: [DWWM]
--- ---
import QuickLinks from "@/components/QuickLinks";
## 📚 Références ## 📚 Références
- REAC _(mise à jour du 02/07/2024)_, pages 15 et 16 - REAC _(mise à jour du 02/07/2024)_, pages 15 et 16
@ -19,29 +17,7 @@ Voyons un peu plus en détail ce qui est attendu pour chacune de ces compétence
Elle est divisée en 4 **compétences professionnelles** _(CP)_ : Elle est divisée en 4 **compétences professionnelles** _(CP)_ :
<QuickLinks> - **CP 5** : Mettre en place une base de données relationnelle
<QuickLinks.QuickLink - **CP 6** : Développer des composants d'accès aux données SQL et NoSQL
title="CP 5" - **CP 7** : Développer des composants métier coté serveur
href="at2/cp5" - **CP 8** : Documenter le déploiement d'une application dynamique web ou web mobile
description="Mettre en place une base de données relationnelle"
icon="presets"
/>
<QuickLinks.QuickLink
title="CP 6"
href="at2/cp6"
description="Développer des composants d'accès aux données SQL et NoSQL"
icon="presets"
/>
<QuickLinks.QuickLink
title="CP 7"
href="at2/cp7"
description="Développer des composants métier coté serveur"
icon="presets"
/>
<QuickLinks.QuickLink
title="CP 8"
href="at2/cp8"
description="Documenter le déploiement d'une application dynamique web ou web mobile"
icon="presets"
/>
</QuickLinks>

View File

@ -0,0 +1,59 @@
---
title: Résumé du titre professionnel DWWM
description: Découvre le résumé du titre professionnel DWWM (TP-01280m04), qui te permettra de te préparer au mieux à l'examen !
tags: [DWWM]
---
## Informations administratives
- Nom complet du titre : **Développeur Web et Web Mobile**
- Sigle : **DWWM**
- Code RNCP : **37674**
- Code titre : **01280m04**
### Documentations officielles
- [REAC - Référentiel Emploi Activités Compétences _(02/07/2024)_](/downloads/dwwm/REAC_DWWM_V04_02072024.pdf)
- [RE - Référentiel d'Évaluation _(02/07/2024)_](/downloads/dwwm/REV2_DWWM_V04_02072024.pdf)
> Provenance des documentations : [Site DGEFP Grand public](https://www.banque.di.afpa.fr/EspaceEmployeursCandidatsActeurs/titre-professionnel/01280m04)
## Activités types et compétences professionnelles
## 📚 Activité type 1 - Développer la partie front-end d'une application web ou web mobile sécurisée
- CP 1 - Installer et configurer son environnement de travail en fonction du projet web ou web mobile
- CP 2 - Maquetter des interfaces utilisateur web ou web mobile
- CP 3 - Réaliser des interfaces utilisateur statiques web ou web mobile
- CP 4 - Développer la partie dynamique des interfaces utilisateur web ou web mobile
## 📚 Activité type 2 - Développer la partie back-end d'une application web ou web mobile sécurisée
- CP 5 - Mettre en place une base de données relationnelle
- CP 6 - Développer des composants d'accès aux données SQL et NoSQL
- CP 7 - Développer des composants métier coté serveur
- CP 8 - Documenter le déploiement d'une application dynamique web ou web mobile
## Compétences transverses
- Communiquer en français et en anglais
- Mettre en oeuvre une démarche de résolution de problème
- Apprendre en continu
## Déroulé de l'examen
{% callout type="note" title="Déroulé relatif au passage de l'épreuve dans sa globalité" %}
En cas de repassage d'un CCP, se référer au Référentiel d'Évaluation pour connaître les modalités de l'épreuve :
- Pages 17 et 18 pour l'AT 1
- Pages 19 et 20 pour l'AT 2
{% /callout %}
**Durée totale de l'examen** : 2h _(dont 1h30 de soutenance face au jury)_
- Questionnaire professionnel _(30 minutes, sans présence du jury)_
- Présentation d'un projet réalisé en amont de la session _(35 minutes, face au jury)_
- Entretien technique _(40 minutes, face au jury)_
- Entretien final _(15 minutes, face au jury)_

View File

@ -0,0 +1,36 @@
---
title: Certifications Memento Dev
description: Tu te prépares à passer un examen de certification, comme DWWM, CDA ou encore CDUI ? Découvre donc de bons conseils pour t'aider à te préparer au mieux !
tags: []
---
Tu te prépares à passer un examen de certification, comme DWWM, CDA ou encore CDUI ?
Découvre donc de bons conseils pour t'aider à te préparer au mieux !
## Certifications couvertes sur le Memento
{% quick-links %}
{% quick-link
title="DWWM"
description="Titre professionnel Développeur Web et Web Mobile"
href="/certifications/dwwm"
icon="presets"
/%}
{% /quick-links %}
## Certifications en cours de rédaction
- **CDA** : Concepteur Développeur d'Applications
- **CDUI** : Concepteur Designer UI
## Besoin d'un accompagnement ?
{% callout type="note" title="Accompagnement" %}
En qualité de jury habilité sur les titres professionnels **DWWM**, **CDA** et **CDUI**, je peux t'accompagner dans ta préparation à l'examen.
Qu'il s'agisse d'une aide à la **compréhension des référentiels**, d'une **préparation à l'oral** ou d'un **accompagnement sur un projet**, je suis là pour t'aider à réussir !
Tu peux me contacter par [email _(gauthier@gauthierdaniels.fr)_](mailto:gauthier@gauthierdaniels?subject=Demande%20d'accompagnement%20pour%20le%20titre%20professionnel%20X) pour bénéficier d'un accompagnement personnalisé et de conseils adaptés à tes besoins.
{% /callout %}

View File

@ -4,15 +4,15 @@ description: Liste d'influenceurs et créateurs de contenu dans le domaine du d
tags: [] tags: []
--- ---
import Callout from "@/components/Callout"; {% callout type="warning" title="Vérification des contenus" %}
<Callout type="warning" title="Vérification des contenus">
Les contenus créés par ces personnalités n'ont pas été vérifiés. Il est donc important de rester critique face à ces informations et d'être en mesure de les remettre en question si nécessaire. Les contenus créés par ces personnalités n'ont pas été vérifiés. Il est donc important de rester critique face à ces informations et d'être en mesure de les remettre en question si nécessaire.
Toutefois, ces personnalités sont reconnues dans le domaine du développement et peuvent être une source d'inspiration pour les développeurs. Toutefois, ces personnalités sont reconnues dans le domaine du développement et peuvent être une source d'inspiration pour les développeurs.
De manière générale, cela peut être une superbe opportunité de t'aider à faire de la veille technique et à rester à jour sur les dernières tendances, technologies et mises à jour dans le développement ! De manière générale, cela peut être une superbe opportunité de t'aider à faire de la veille technique et à rester à jour sur les dernières tendances, technologies et mises à jour dans le développement !
</Callout>
{% /callout %}
## ❤️ Coups de coeur de Memento Dev ## ❤️ Coups de coeur de Memento Dev
@ -33,10 +33,12 @@ Pour figurer sur cette page, tu peux tout simplement m'en faire la demande par [
Si ça te rassure, il n'y a aucun prérequis spécifique pour apparaître sur cette page, du moment que tu es actif dans la communauté du développement et que tu crées du contenu en lien avec la tech _(pas obligatoire que du développement)_. Si ça te rassure, il n'y a aucun prérequis spécifique pour apparaître sur cette page, du moment que tu es actif dans la communauté du développement et que tu crées du contenu en lien avec la tech _(pas obligatoire que du développement)_.
<Callout type="note" title="Lien vers tes différents réseaux"> {% callout type="note" title="Lien vers tes différents réseaux" %}
En ce qui concerne les liens : un seul est autorisé, et doit pointer vers une page où l'on peut te suivre ou consulter tes contenus. En ce qui concerne les liens : un seul est autorisé, et doit pointer vers une page où l'on peut te suivre ou consulter tes contenus.
Il peut s'agir d'un HUB _(Linktree, Bento, etc.)_ où l'on peut retrouver tous tes liens, ou d'un lien vers un réseau social _(X/Twitter, Youtube, etc.)_ où tu es actif. Il peut s'agir d'un HUB _(Linktree, Bento, etc.)_ où l'on peut retrouver tous tes liens, ou d'un lien vers un réseau social _(X/Twitter, Youtube, etc.)_ où tu es actif.
</Callout>
{% /callout %}
## 🥷 Disparaître de cette page ## 🥷 Disparaître de cette page

View File

@ -4,8 +4,6 @@ description: "Partagez et réutilisez le contenu de Memento Dev : Exportez, coll
tags: [] tags: []
--- ---
import Callout from "@/components/Callout";
Tu souhaites partager ou réutiliser le contenu de Memento Dev ? Tu souhaites partager ou réutiliser le contenu de Memento Dev ?
Tout d'abord, permet-moi de te remercier pour ton intérêt ! 🙏 Tout d'abord, permet-moi de te remercier pour ton intérêt ! 🙏
@ -30,6 +28,8 @@ Un grand merci à ces entités qui utilisent le contenu de Memento Dev pour leur
- [O'clock](https://oclock.io) - [O'clock](https://oclock.io)
- [Wild Code School](https://wildcodeschool.com) - [Wild Code School](https://wildcodeschool.com)
<Callout type="note" title="Tu utilises mon contenu et tu souhaites apparaître ici ?"> {% callout type="note" title="Tu utilises mon contenu et tu souhaites apparaître ici ?" %}
Pour figurer sur cette page, tu peux tout simplement m'en faire la demande par [email _(gauthier@gauthierdaniels.fr)_](mailto:gauthier@gauthierdaniels?subject=Demande%20d'ajout%20sur%20la%20page%20partages%20et%20r%C3%A9utilisations%20Memento%20Dev). Pour figurer sur cette page, tu peux tout simplement m'en faire la demande par [email _(gauthier@gauthierdaniels.fr)_](mailto:gauthier@gauthierdaniels?subject=Demande%20d'ajout%20sur%20la%20page%20partages%20et%20r%C3%A9utilisations%20Memento%20Dev).
</Callout>
{% /callout %}

View File

@ -4,9 +4,6 @@ description: Explorez le dictionnaire de données dans Merise, essentiel pour st
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL] tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
--- ---
import DictionnaryTable from "./DictionnaryTable";
import Callout from "@/components/Callout";
Le **dictionnaire de données** est un document qui contient toutes les informations sur les données qui vont être stockées dans la future base de données. Le **dictionnaire de données** est un document qui contient toutes les informations sur les données qui vont être stockées dans la future base de données.
Ici, on ne va pas parler de tables, de colonnes ou de relations, mais uniquement de **données**. Ces informations nous sont données par le client, et il est important que le dictionnaire reste compréhensible par le client. Ici, on ne va pas parler de tables, de colonnes ou de relations, mais uniquement de **données**. Ces informations nous sont données par le client, et il est important que le dictionnaire reste compréhensible par le client.
@ -73,13 +70,25 @@ Tu risques de retrouver ton client en train de convulser sur le sol : **pas glop
Voici donc le dictionnaire de données que l'on va créer pour notre application : Voici donc le dictionnaire de données que l'on va créer pour notre application :
<DictionnaryTable /> | Nom de la donnée | Format | Longueur | Contraintes | Document |
| --------------------------- | -------------- | -------- | ------------------- | ---------- |
| Nom | Alphabétique | 30 | Obligatoire | Musicien |
| Prénom | Alphabétique | 30 | Obligatoire | Musicien |
| Instruments | Alphabétique | 30 | Obligatoire | Musicien |
| Adresse e-mail | Alphanumérique | 50 | Obligatoire, unique | Musicien |
| Mot de passe | Alphanumérique | > 12 | Obligatoire | Musicien |
| Date et heure de concert | Date | - | Obligatoire | Concert |
| Lieu de concert | Alphabétique | 50 | Obligatoire | Concert |
| Tarif | Numérique | - | - | Concert |
| Date et heure de répétition | Date | - | Obligatoire | Répétition |
| Lieu de répétition | Alphabétique | 50 | Obligatoire | Répétition |
Voilà, on a notre dictionnaire de données ! Voilà, on a notre dictionnaire de données !
Faisons quand même un petit point sur les données que l'on a récupérées et la façon dont on les a représentées. Faisons quand même un petit point sur les données que l'on a récupérées et la façon dont on les a représentées.
<Callout type="note" title="Retour rapide sur le dictionnaire de données"> {% callout type="note" title="Retour rapide sur le dictionnaire de données" %}
Dans certains cas, on a précisé des longueurs de données. On l'a fait uniquement pour des données textuelles _(Alphabétiques et Alphanumériques)_. Dans certains cas, on a précisé des longueurs de données. On l'a fait uniquement pour des données textuelles _(Alphabétiques et Alphanumériques)_.
Au niveau des contraintes, on a majoritairement _(sauf pour le tarif d'un concert)_ mis des contraintes d'obligation sur les données. Au niveau des contraintes, on a majoritairement _(sauf pour le tarif d'un concert)_ mis des contraintes d'obligation sur les données.
@ -90,7 +99,8 @@ Faisons quand même un petit point sur les données que l'on a récupérées et
Pour le mot de passe, on a mis une contrainte de longueur supérieure à 12 caractères. Pour le mot de passe, on a mis une contrainte de longueur supérieure à 12 caractères.
Évidemment on ne viendra pas stocker le mot de passe en clair dans la base de données, on va utiliser la donnée réelle _(non transformée)_ pour éviter de perdre le client entre la longueur réelle du mot de passe et la longueur de son hash. Évidemment on ne viendra pas stocker le mot de passe en clair dans la base de données, on va utiliser la donnée réelle _(non transformée)_ pour éviter de perdre le client entre la longueur réelle du mot de passe et la longueur de son hash.
</Callout>
{% /callout %}
## Conclusion ## Conclusion

View File

@ -4,10 +4,6 @@ description: Comprenez le MCD dans Merise, une étape clé pour représenter les
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL] tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
--- ---
import DictionnaryTable from "../dictionnaire-de-donnees/DictionnaryTable";
import Callout from "@/components/Callout";
import TermsTable from "./TermsTable";
On va enfin pouvoir commencer à réaliser notre premier schéma : le **MCD** _(Modèle Conceptuel de Données)_ ! On va enfin pouvoir commencer à réaliser notre premier schéma : le **MCD** _(Modèle Conceptuel de Données)_ !
Mais déjà... qu'est-ce que c'est que ce MCD ? Mais déjà... qu'est-ce que c'est que ce MCD ?
@ -31,7 +27,13 @@ On va exploiter d'autres termes comme **entité**, **attribut** ou **relation**.
Voici un petit lexique pour t'aider à comprendre : Voici un petit lexique pour t'aider à comprendre :
<TermsTable /> | Terme | Définition |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| **Entité** | Représentation d'un regroupement de données _(rectangle)_ |
| **Attribut** | Donnée précise d'une entité |
| **Relation** | Lien entre deux entités _(bulle ovale/arrondie)_, accompagné d'un verbe à l'infinitif |
| **Cardinalité** | Nombre d'occurrences _(minimum et maximum)_ d'une entité par rapport à une autre |
| **Discriminant** _(ou **déterminant**/**identifiant**)_ | Attribut qui permet d'identifier une entité de manière unique _(ex: matricule)_ |
C'est tout un lexique à apprendre, mais pas de panique tu vas vite t'y habituer ! C'est tout un lexique à apprendre, mais pas de panique tu vas vite t'y habituer !
@ -40,7 +42,7 @@ C'est tout un lexique à apprendre, mais pas de panique tu vas vite t'y habituer
Forcément, les définitions sans donner un exemple ça n'aide pas beaucoup à comprendre... Forcément, les définitions sans donner un exemple ça n'aide pas beaucoup à comprendre...
Voici un petit exemple tout simple de MCD pour illustrer tout ça : Voici un petit exemple tout simple de MCD pour illustrer tout ça :
![Exemple de MCD](/images/merise/mcd-basic.webp) ![Exemple de MCD](/merise/mcd-basic.webp)
On a ici un MCD qui représente trois **entités** : On a ici un MCD qui représente trois **entités** :
@ -54,13 +56,13 @@ Chacune de ces entités a plusieurs **attributs** qui lui sont propres :
- **Entité 2** : code identité 2, attribut 2, attribut 3 - **Entité 2** : code identité 2, attribut 2, attribut 3
- **Entité 3** : code identité 3, attribut 2, attribut 3 - **Entité 3** : code identité 3, attribut 2, attribut 3
<Callout type="question" title="Pourquoi le premier attribut est en gras et souligné ?"> {% callout type="question" title="Pourquoi le premier attribut est en gras et souligné ?" %}
Dans le MCD, un attribut en gras est un attribut **unique**. Dans le MCD, un attribut en gras est un attribut **unique**.
S'il est souligné en plus d'être en gras, c'est qu'il s'agit d'un **discriminant** _(ou déterminant/identifiant)_. S'il est souligné en plus d'être en gras, c'est qu'il s'agit d'un **discriminant** _(ou déterminant/identifiant)_.
Il permet d'identifier de manière unique une entité. Il permet d'identifier de manière unique une entité.
Comme le MCD n'est **pas technique**, on n'utilisera pas le terme de **clé primaire** ou **ID**. Comme le MCD n'est **pas technique**, on n'utilisera pas le terme de **clé primaire** ou **ID**.
</Callout> {% /callout %}
Et pour terminer, on remarque aussi que certaines de nos entités sont reliées entre elles par des **relations**. Et pour terminer, on remarque aussi que certaines de nos entités sont reliées entre elles par des **relations**.
Les relations se caractèrisent par : Les relations se caractèrisent par :
@ -94,7 +96,7 @@ Toujours dans l'exemple précédent, on comprend donc que :
- **Entité 1** doit posséder 1 et 1 seule **Entité 3** - **Entité 1** doit posséder 1 et 1 seule **Entité 3**
- **Entité 3** peut être possédée entre 0 et N **Entité 1** - **Entité 3** peut être possédée entre 0 et N **Entité 1**
<Callout type="note" title="Les différentes valeurs"> {% callout type="note" title="Les différentes valeurs" %}
La plupart du temps, nous allons retrouver les valeurs suivantes : La plupart du temps, nous allons retrouver les valeurs suivantes :
- **0** - **0**
@ -107,7 +109,7 @@ Toujours dans l'exemple précédent, on comprend donc que :
Par exemple : **1,5** signifie "1 à 5" et **0,3** signifie "0 à 3". Par exemple : **1,5** signifie "1 à 5" et **0,3** signifie "0 à 3".
Si la valeur n'est pas connue à l'avance ou qu'aucune limite n'est nécessaire, on utilisera alors **N**. Si la valeur n'est pas connue à l'avance ou qu'aucune limite n'est nécessaire, on utilisera alors **N**.
</Callout> {% /callout %}
## Retour sur notre dictionnaire de données ## Retour sur notre dictionnaire de données
@ -115,7 +117,18 @@ Maintenant que l'on sait comment fonctionne un MCD, on va pouvoir retourner sur
Pour rappel, voici notre dictionnaire de données : Pour rappel, voici notre dictionnaire de données :
<DictionnaryTable /> | Nom de la donnée | Format | Longueur | Contraintes | Document |
| --------------------------- | -------------- | -------- | ------------------- | ---------- |
| Nom | Alphabétique | 30 | Obligatoire | Musicien |
| Prénom | Alphabétique | 30 | Obligatoire | Musicien |
| Instruments | Alphabétique | 30 | Obligatoire | Musicien |
| Adresse e-mail | Alphanumérique | 50 | Obligatoire, unique | Musicien |
| Mot de passe | Alphanumérique | > 12 | Obligatoire | Musicien |
| Date et heure de concert | Date | - | Obligatoire | Concert |
| Lieu de concert | Alphabétique | 50 | Obligatoire | Concert |
| Tarif | Numérique | - | - | Concert |
| Date et heure de répétition | Date | - | Obligatoire | Répétition |
| Lieu de répétition | Alphabétique | 50 | Obligatoire | Répétition |
### Les entités ### Les entités
@ -128,7 +141,7 @@ On va donc créer trois entités :
Ces entités vont contenir les attributs que l'on a récupérés dans le dictionnaire de données. Ces entités vont contenir les attributs que l'on a récupérés dans le dictionnaire de données.
On se retrouve pour le moment avec un MCD qui ressemble à ça : On se retrouve pour le moment avec un MCD qui ressemble à ça :
![MCD avec uniquement les entités](/images/merise/mcd-1.webp) ![MCD avec uniquement les entités](/merise/mcd-1.webp)
On est déjà pas trop mal, il nous reste plus qu'à ajouter les relations entre les entités et les cardinalités ! On est déjà pas trop mal, il nous reste plus qu'à ajouter les relations entre les entités et les cardinalités !
@ -145,7 +158,7 @@ Pour la répétition, on va faire la même chose !
On va créer une relation **"Répéter"** entre les entités **Musicien** et **Répétition**. On va créer une relation **"Répéter"** entre les entités **Musicien** et **Répétition**.
À la fin, on se retrouve avec un MCD qui ressemble à ça : À la fin, on se retrouve avec un MCD qui ressemble à ça :
![MCD avec les relations](/images/merise/mcd-2.webp) ![MCD avec les relations](/merise/mcd-2.webp)
Et c'est tout ! Notre MCD est terminé... enfin presque ! Et c'est tout ! Notre MCD est terminé... enfin presque !
@ -153,11 +166,11 @@ Et c'est tout ! Notre MCD est terminé... enfin presque !
Si on souhaite aller plus loin, on peut ajouter de l'héritage. Si on souhaite aller plus loin, on peut ajouter de l'héritage.
<Callout type="note" title="Rapide point sur l'héritage"> {% callout type="note" title="Rapide point sur l'héritage" %}
L'héritage _(ou aussi appelé **spécialisation** ou **généralisation**)_ est un concept qui va nous permettre de factoriser les propriétés identiques dans une entité commune. Cette entitée est appelée **entité générique** _(ou **sur-type**)_. L'héritage _(ou aussi appelé **spécialisation** ou **généralisation**)_ est un concept qui va nous permettre de factoriser les propriétés identiques dans une entité commune. Cette entitée est appelée **entité générique** _(ou **sur-type**)_.
Les entités qui héritent de l'entité générique sont appelées **entités spécialisées** _(ou **sous-types**)_. Les entités qui héritent de l'entité générique sont appelées **entités spécialisées** _(ou **sous-types**)_.
</Callout> {% /callout %}
En regardant bien notre MCD, on se rend compte que les entités **Concert** et **Répétition** ont des attributs communs : En regardant bien notre MCD, on se rend compte que les entités **Concert** et **Répétition** ont des attributs communs :
@ -169,19 +182,19 @@ On va donc pouvoir créer une entité **générique** que l'on appelera **Évén
Cette entité générique va contenir les attributs communs aux deux entités, et on va faire hériter les entités **Concert** et **Répétition** de cette entité. Cette entité générique va contenir les attributs communs aux deux entités, et on va faire hériter les entités **Concert** et **Répétition** de cette entité.
On se retrouve donc avec ces trois entités _(**Événement**, **Concert** et **Répétition**)_ : On se retrouve donc avec ces trois entités _(**Événement**, **Concert** et **Répétition**)_ :
![MCD avec héritage](/images/merise/mcd-3.webp) ![MCD avec héritage](/merise/mcd-3.webp)
<Callout type="question" title="Pourquoi ne pas stocker le type d'événement ?"> {% callout type="question" title="Pourquoi ne pas stocker le type d'événement ?" %}
Effectivement, on aurait pu stocker le type d'événement dans l'entité **Événement** ! Effectivement, on aurait pu stocker le type d'événement dans l'entité **Événement** !
Il s'agit d'une autre approche qui est tout à fait valable. Il s'agit d'une autre approche qui est tout à fait valable.
Cependant, il est plus simple de créer une entité générique qui va nous permettre de factoriser les attributs communs et éviter de devoir rendre plusieurs attributs nullables en fonction du type d'événement. Cependant, il est plus simple de créer une entité générique qui va nous permettre de factoriser les attributs communs et éviter de devoir rendre plusieurs attributs nullables en fonction du type d'événement.
On renforce ainsi l'intégrité de la base de données. On renforce ainsi l'intégrité de la base de données.
</Callout> {% /callout %}
Le MCD final ressemble donc à ça : Le MCD final ressemble donc à ça :
![MCD final](/images/merise/mcd-4.webp) ![MCD final](/merise/mcd-4.webp)
Si tu souhaites télécharger le MCD que l'on vient de créer, tu peux le faire ici : [MCD Merise pour Looping](/downloads/merise/band-manager.loo). Si tu souhaites télécharger le MCD que l'on vient de créer, tu peux le faire ici : [MCD Merise pour Looping](/downloads/merise/band-manager.loo).

View File

@ -4,19 +4,19 @@ description: Parlons un peu de Merise, la fameuse méthodologie de modélisation
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL] tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
--- ---
import Callout from "@/components/Callout";
**Merise**, c'est quoi ? **Merise**, c'est quoi ?
Il s'agit d'une **méthodologie** de **modélisation** française _(🐔)_, conçue pour la conception de **bases de données**. Il s'agit d'une **méthodologie** de **modélisation** française _(🐔)_, conçue pour la conception de **bases de données**.
Bien qu'elle ait été créée dans les années 70/80, elle est toujours d'actualité et largement utilisée dans le domaine de l'informatique. Bien qu'elle ait été créée dans les années 70/80, elle est toujours d'actualité et largement utilisée dans le domaine de l'informatique.
<Callout type="note" title="C'est dans les vieux pots qu'on fait la meilleure soupe"> {% callout type="note" title="C'est dans les vieux pots qu'on fait la meilleure soupe" %}
Ce n'est pas parce qu'une méthodologie est ancienne qu'elle est obsolète. Ce n'est pas parce qu'une méthodologie est ancienne qu'elle est obsolète.
Au contraire, elle a fait ses preuves et est toujours pertinente aujourd'hui bien que surtout utilisée en France. Au contraire, elle a fait ses preuves et est toujours pertinente aujourd'hui bien que surtout utilisée en France.
Rien ne t'oblige à l'utiliser, mais il est bon de la connaître, surtout si tu préfères éviter de foncer dans le mur lors de la conception de ta base de données. Rien ne t'oblige à l'utiliser, mais il est bon de la connaître, surtout si tu préfères éviter de foncer dans le mur lors de la conception de ta base de données.
</Callout>
{% /callout %}
On parlera ici que de la partie **modélisation** de Merise, même si Merise comprend aussi des aspects d'**analyse** et de **gestion de projet**. On parlera ici que de la partie **modélisation** de Merise, même si Merise comprend aussi des aspects d'**analyse** et de **gestion de projet**.
@ -95,14 +95,16 @@ Pour toutes les rubriques suivantes, **Looping** sera utilisé comme outil.
## Ressources ## Ressources
<Callout type="warning" title="Ressources disponibles sur internet"> {% callout type="warning" title="Ressources disponibles sur internet" %}
**Attention !** **Attention !**
Beaucoup de ressources sur internet parlent de Merise, mais elles ne sont pas forcément justes. Beaucoup de ressources sur internet parlent de Merise, mais elles ne sont pas forcément justes.
Peu importe l'origine de la ressource, il est important de vérifier les informations et de ne pas se fier aveuglément à ce qui est écrit. Peu importe l'origine de la ressource, il est important de vérifier les informations et de ne pas se fier aveuglément à ce qui est écrit.
Je recommande énormément le livre [Guide pratique _(4e édition)_](https://www.editions-eni.fr/livre/merise-guide-pratique-4e-edition-modelisation-des-donnees-et-des-traitements-manipulations-avec-le-langage-sql-conception-d-une-application-mobile-android-ou-ios-9782409046667) de **Jean-Luc Baptiste**, aux **Éditions ENI**. Je recommande énormément le livre [Guide pratique (4e édition)](https://www.editions-eni.fr/livre/merise-guide-pratique-4e-edition-modelisation-des-donnees-et-des-traitements-manipulations-avec-le-langage-sql-conception-d-une-application-mobile-android-ou-ios-9782409046667) de **Jean-Luc Baptiste**, aux **Éditions ENI**.
</Callout>
{% /callout %}
- [Looping](https://looping-mcd.fr/) - [Looping](https://looping-mcd.fr/)
- [Mocodo](https://mocodo.net/) - [Mocodo](https://mocodo.net/)

45
app/data/docs/page.md Normal file
View File

@ -0,0 +1,45 @@
---
title: Documentations du Memento
description: Plonge toi dans une documentation synthétique et concise, conçue pour les développeurs ou passionnés de l'information en quête de savoir !
tags: []
---
## Documentations rédigées
{% quick-links %}
{% quick-link
title="React"
description="Introduction et synthèse de la bibliothèque React"
href="/docs/react"
icon="presets"
/%}
{% /quick-links %}
## Documentations en cours de rédaction
{% quick-links %}
{% quick-link
title="Merise"
description="Introduction et synthèse de la méthode Merise"
href="/docs/merise"
icon="presets"
/%}
{% /quick-links %}
## Documentations à venir
- HTML
- CSS
- JavaScript
- PHP
- SQL
- Node.js
- Express.js
- UML
- Maquettage
Et bien d'autres encore ! 😄

View File

@ -4,9 +4,6 @@ description: Découvre les hooks de React, une fonctionnalité qui te permet de
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)] tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
--- ---
import Callout from "@/components/Callout";
import tabs from "./tabs";
Ça y est, on rentre dans le vif du sujet avec les **hooks** de React ! Ça y est, on rentre dans le vif du sujet avec les **hooks** de React !
On en a déjà parlé un peu dans l'article précédent _(notamment avec le hook `useState` pour déclarer un state)_, mais on va maintenant les aborder en détail. On en a déjà parlé un peu dans l'article précédent _(notamment avec le hook `useState` pour déclarer un state)_, mais on va maintenant les aborder en détail.
@ -24,7 +21,75 @@ C'était pas mal, mais ça devenait vite compliqué à gérer, notamment pour pa
Pour te donner un aperçu, voici à quoi ressemblait un composant de classe avec les trois étapes du cycle de vie : Pour te donner un aperçu, voici à quoi ressemblait un composant de classe avec les trois étapes du cycle de vie :
<tabs.reactClassComponent /> {% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
import React from "react";
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
console.log("Component mounted");
}
componentDidUpdate() {
console.log("Component updated");
}
componentWillUnmount() {
console.log("Component unmounted");
}
render() {
return <div>{this.state.count}</div>;
}
}
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import React from 'react';
type MyComponentState = {
count: number;
};
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state: MyComponentState = { count: 0 };
}
componentDidMount() {
console.log('Component mounted');
}
componentDidUpdate() {
console.log('Component updated');
}
componentWillUnmount() {
console.log('Component unmounted');
}
render() {
return <div>{this.state.count}</div>;
}
}
```
{% /tab %}
{% /tabs %}
Comme dirait l'un de mes chers confrères jury : Comme dirait l'un de mes chers confrères jury :
@ -61,7 +126,39 @@ Tu comprends pourquoi je dis "couteau suisse" ? 😏
Alors sur le papier c'est top, mais maintenant je te laisse t'amuser à comprendre comment ça fonctionne 😇 Alors sur le papier c'est top, mais maintenant je te laisse t'amuser à comprendre comment ça fonctionne 😇
<tabs.reactUseEffectSyntaxes /> {% tabs defaultSelectedTab="1" %}
{% tab value="1" label="Écriture #1" %}
```jsx
React.useEffect(() => {
// ...
}, []);
```
{% /tab %}
{% tab value="2" label="Écriture #2" %}
```jsx
React.useEffect(() => {
// ...
}, [props.uneProp]);
```
{% /tab %}
{% tab value="3" label="Écriture #3" %}
```jsx
React.useEffect(() => {
// ...
});
```
{% /tab %}
{% /tabs %}
Pas cool, hein ? 😂 Pas cool, hein ? 😂
Et bien dans ces exemples, on a trois manières d'écrire un useEffect : Et bien dans ces exemples, on a trois manières d'écrire un useEffect :
@ -70,9 +167,11 @@ Et bien dans ces exemples, on a trois manières d'écrire un useEffect :
2. Le hook est exécuté à chaque mise à jour du composant 2. Le hook est exécuté à chaque mise à jour du composant
3. Le hook est exécuté à chaque mise à jour du composant, mais seulement si la propriété `uneProp` de `props` a changé 3. Le hook est exécuté à chaque mise à jour du composant, mais seulement si la propriété `uneProp` de `props` a changé
<Callout type="note" title="`useEffect` et les mises à jour du composant"> {% callout type="note" title="`useEffect` et les mises à jour du composant" %}
Alors quand je dis "le hook est exécuté à chaque mise à jour du composant", il faut également prendre en compte qu'il est également exécuté après le premier rendu du composant. Alors quand je dis "le hook est exécuté à chaque mise à jour du composant", il faut également prendre en compte qu'il est également exécuté après le premier rendu du composant.
</Callout>
{% /callout %}
Mais alors, comment on fait pour gérer ces étapes avec des composants fonctionnels ? Mais alors, comment on fait pour gérer ces étapes avec des composants fonctionnels ?
Si tu n'as pas vu la différence entre les trois écritures, tu remarqueras que c'est le deuxième argument de useEffect qui fait la différence. Si tu n'as pas vu la différence entre les trois écritures, tu remarqueras que c'est le deuxième argument de useEffect qui fait la différence.
@ -84,19 +183,31 @@ Selon ce tableau, le hook sera exécuté à des moments différents du cycle de
### ⚙️ ComponentDidMount ### ⚙️ ComponentDidMount
<tabs.reactUseEffectMount /> ```jsx
React.useEffect(() => {
// ...
}, []);
```
Le tableau de dépendances est vide, on sous-entend que le hook ne dépend d'aucune variable et sera exécuté une seule fois. Le tableau de dépendances est vide, on sous-entend que le hook ne dépend d'aucune variable et sera exécuté une seule fois.
On peut donc dire que c'est l'équivalent de `componentDidMount` pour les composants de classe. On peut donc dire que c'est l'équivalent de `componentDidMount` pour les composants de classe.
### 🔧 ComponentDidUpdate ### 🔧 ComponentDidUpdate
<tabs.reactUseEffectUpdate /> ```jsx
React.useEffect(() => {
// ...
});
```
Ici, le tableau de dépendances est absent _(et tout va bien, il est optionnel !)_. Ici, le tableau de dépendances est absent _(et tout va bien, il est optionnel !)_.
Le hook sera exécuté à chaque mise à jour du composant, ainsi que lors du premier rendu. Le hook sera exécuté à chaque mise à jour du composant, ainsi que lors du premier rendu.
<tabs.reactUseEffectUpdateDependency /> ```jsx
React.useEffect(() => {
// ...
}, [props.uneProp]);
```
Dans ce cas, le tableau de dépendances contient la propriété `uneProp` de `props`. Dans ce cas, le tableau de dépendances contient la propriété `uneProp` de `props`.
Le hook sera exécuté à chaque mise à jour du composant _(ainsi qu'au montage)_, mais seulement si la propriété `uneProp` a changé. Le hook sera exécuté à chaque mise à jour du composant _(ainsi qu'au montage)_, mais seulement si la propriété `uneProp` a changé.
@ -106,7 +217,14 @@ Le hook sera exécuté à chaque mise à jour du composant _(ainsi qu'au montage
Et là, tu te dis : "Mais comment je fais pour gérer le démontage du composant ?". Et là, tu te dis : "Mais comment je fais pour gérer le démontage du composant ?".
Hehehe, c'est là que ça devient intéressant 😏 Hehehe, c'est là que ça devient intéressant 😏
<tabs.reactUseEffectUnmount /> ```jsx
React.useEffect(() => {
// ...
return () => {
// ...
};
}, []);
```
Tu as vu ce petit `return` ? Et bien, c'est notre équivalent de `componentWillUnmount` pour les composants de classe ! Tu as vu ce petit `return` ? Et bien, c'est notre équivalent de `componentWillUnmount` pour les composants de classe !
@ -124,9 +242,65 @@ Pour éviter que React se dise "Tiens, il y a eu un changement, je vais re-rendr
Allez, mettons un peu ce qu'on voit de voir en pratique ! Allez, mettons un peu ce qu'on voit de voir en pratique !
Voici un exemple de code qui utilise `useEffect` pour gérer le cycle de vie d'un composant : {% tabs defaultSelectedTab="jsx" %}
<tabs.reactUseEffectExample /> {% tab value="jsx" label="JSX" %}
```jsx
import React from "react";
export const Counter = () => {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log("Component mounted");
return () => {
console.log("Component unmounted");
};
}, []);
React.useEffect(() => {
console.log("Component updated");
});
const increment = () => setCount(count + 1);
return <button onClick={increment}>{count}</button>;
};
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import React from "react";
export const Counter = () => {
const [count, setCount] = React.useState<number>(0);
React.useEffect(() => {
console.log("Component mounted");
return () => {
console.log("Component unmounted");
};
}, []);
React.useEffect(() => {
console.log("Component updated");
});
const increment = () => setCount(count + 1);
return <button onClick={increment}>{count}</button>;
};
```
{% /tab %}
{% /tabs %}
### 🔢 On revient sur le cycle de vie ! ### 🔢 On revient sur le cycle de vie !
@ -135,16 +309,39 @@ On revient sur le cycle de vie d'un composant maintenant qu'on a vu `useEffect`
Je vais te donner un exemple de code supplémentaire et tu vas devoir deviner l'ordre d'apparition des messages dans la console. Je vais te donner un exemple de code supplémentaire et tu vas devoir deviner l'ordre d'apparition des messages dans la console.
<tabs.reactUseEffectChallenge /> ```jsx
import React from "react";
export const MyComponent = () => {
React.useEffect(() => {
console.log("1");
});
console.log("2");
React.useEffect(() => {
console.log("3");
}, []);
const logInRender = () => {
console.log("4");
return null;
};
return <div>{logInRender()}</div>;
};
```
Voici les possibilités : Voici les possibilités :
<Callout type="question" title="Quel est l'ordre d'apparition des messages dans la console ?"> {% callout type="question" title="Quel est l'ordre d'apparition des messages dans la console ?" %}
- **A** - 4, 2, 1, 3 - **A** - 4, 2, 1, 3
- **B** - 2, 4, 1, 3 - **B** - 2, 4, 1, 3
- **C** - 1, 2, 3, 4 - **C** - 1, 2, 3, 4
- **D** - La réponse D - **D** - La réponse D
</Callout>
{% /callout %}
## 🧩 Les autres hooks ## 🧩 Les autres hooks

View File

@ -4,9 +4,6 @@ description: Initialisons un nouveau projet React, avec ou sans TypeScript.
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI), Vite, Vike] tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI), Vite, Vike]
--- ---
import Callout from "@/components/Callout";
import tabs from "./tabs";
Allez, on se lance dans la création d'un projet React ! 🚀 Allez, on se lance dans la création d'un projet React ! 🚀
L'article sera très court, car il n'y a pas grand chose à dire sur la création d'un projet React. L'article sera très court, car il n'y a pas grand chose à dire sur la création d'un projet React.
@ -14,11 +11,12 @@ Tu vas voir à quel point c'est simple !
## 👴 Ancienne méthode _(CRA)_ ## 👴 Ancienne méthode _(CRA)_
{% callout type="warning" title="Dépréciation de `create-react-app`" %}
<Callout type="warning" title="Dépréciation de `create-react-app`">
Sur cette courte section on va parler d'une méthode qui est **dépréciée**. Sur cette courte section on va parler d'une méthode qui est **dépréciée**.
Ne l'utilise donc pas pour créer de nouveaux projets ! Ne l'utilise donc pas pour créer de nouveaux projets !
</Callout>
{% /callout %}
Il y a encore quelques mois/années, on passait régulièrement par le **CRA**, ou `create-react-app`, pour initialiser un projet React. Il y a encore quelques mois/années, on passait régulièrement par le **CRA**, ou `create-react-app`, pour initialiser un projet React.
L'avantage que proposait cette méthode était de nous fournir un projet prêt à l'emploi, avec une structure de fichiers déjà en place, et des dépendances déjà installées. L'avantage que proposait cette méthode était de nous fournir un projet prêt à l'emploi, avec une structure de fichiers déjà en place, et des dépendances déjà installées.
@ -59,7 +57,33 @@ Si ce n'est pas le cas, tu peux le télécharger ici.
Prépare toi maintenant pour la partie la plus difficile de cet article... Prépare toi maintenant pour la partie la plus difficile de cet article...
Ouvre ton terminal, et tape la commande suivante : Ouvre ton terminal, et tape la commande suivante :
<tabs.reactInit /> {% tabs defaultSelectedTab="npm" %}
{% tab value="npm" label="NPM" %}
```bash
npm init vite
```
{% /tab %}
{% tab value="yarn" label="Yarn" %}
```bash
yarn create vite
```
{% /tab %}
{% tab value="pnpm" label="PNPM" %}
```bash
pnpm create vite
```
{% /tab %}
{% /tabs %}
Tu vas ensuite devoir répondre à quelques questions, notamment le nom de ton projet, le template que tu souhaites utiliser _(React, Vue, Vanilla, etc.)_, et si tu souhaites utiliser TypeScript. Tu vas ensuite devoir répondre à quelques questions, notamment le nom de ton projet, le template que tu souhaites utiliser _(React, Vue, Vanilla, etc.)_, et si tu souhaites utiliser TypeScript.
@ -72,11 +96,45 @@ Et voilà, c'est tout !
Une fois que tu as répondu à toutes les questions, tu vas devoir te rendre dans le dossier de ton projet, et lancer le serveur de développement : Une fois que tu as répondu à toutes les questions, tu vas devoir te rendre dans le dossier de ton projet, et lancer le serveur de développement :
<tabs.reactInstall /> {% tabs defaultSelectedTab="npm" %}
{% tab value="npm" label="NPM" %}
```bash
cd mon-projet # Se rendre dans le dossier du projet
npm install # Installer les dépendances
npm run dev # Lancer le serveur de développement
```
{% /tab %}
{% tab value="yarn" label="Yarn" %}
```bash
cd mon-projet # Se rendre dans le dossier du projet
yarn install # Installer les dépendances
yarn dev # Lancer le serveur de développement
```
{% /tab %}
{% tab value="pnpm" label="PNPM" %}
```bash
cd mon-projet # Se rendre dans le dossier du projet
pnpm install # Installer les dépendances
pnpm dev # Lancer le serveur de développement
```
{% /tab %}
{% /tabs %}
{% callout type="warning" title="Dépendances" %}
<Callout type="warning" title="Installation des dépendances">
Pense à installer les dépendances de ton projet avant de lancer le moindre script _(comme `dev`, `build` etc)_, sinon ça ne va pas très bien se passer ! Pense à installer les dépendances de ton projet avant de lancer le moindre script _(comme `dev`, `build` etc)_, sinon ça ne va pas très bien se passer !
</Callout>
{% /callout %}
Tu auras ensuite un message dans ton terminal qui t'indiquera l'adresse à laquelle tu pourras accéder à ton application. Tu auras ensuite un message dans ton terminal qui t'indiquera l'adresse à laquelle tu pourras accéder à ton application.
En général, il s'agit de http://localhost:4173 _(le port peut varier)_. En général, il s'agit de http://localhost:4173 _(le port peut varier)_.

View File

@ -0,0 +1,317 @@
---
title: La syntaxe JSX de React
description: Découvrons la syntaxe JSX, un langage de balisage utilisé par React pour décrire l'interface utilisateur.
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
---
Avant de commencer à parler des composants React, découvrons tranquillement la syntaxe **JSX**.
Le **JSX** est un sucre syntaxique _(une syntaxe plus lisible et plus simple que le JavaScript pur)_ qui permet de décrire l'interface utilisateur _(UI)_ de notre application.
Le sigle en lui-même signifie **JavaScript XML**, dans le sens où l'on va retrouver une syntaxe proche du **XML** _(eXtensible Markup Language)_ qui est un langage de balisage _(comme le **HTML**)_.
## 🔍 Différences entre HTML et JSX
Et oui, le **JSX** ressemble beaucoup au **HTML** et c'est normal !
C'est l'objectif premier de **React** : rendre la création d'interfaces utilisateur _(UI)_ plus simple et plus intuitive.
Cependant il ne faut pas oublier que le **JSX** n'est pas du **HTML**, mais du **JavaScript**.
Pour faire plus simple, voici un élément **HTML** et son équivalent avec React _(avec et sans JSX)_ :
{% tabs defaultSelectedTab="html" %}
{% tab value="html" label="HTML" %}
```html
<button class="button">Clique moi !</button>
```
{% /tab %}
{% tab value="react-no-jsx" label="React sans JSX" %}
```js
React.createElement("button", { className: "button" }, "Clique moi !");
```
{% /tab %}
{% tab value="jsx" label="React avec JSX" %}
```jsx
<button className="button">Clique moi !</button>
```
{% /tab %}
{% /tabs %}
Comme tu peux le constater, la différence entre le **JSX** et le **HTML** est minime.
Il y a toutefois des différences, comme certains mots réservés _(comme `class` qui devient `className`)_ ou encore la manière de déclarer des événements _(comme `onclick` qui devient `onClick`)_.
Par contre si on regarde la différence entre le **JSX** et le **JavaScript pur** _(en utilisant React quand même)_, on voit bien que le **JSX** est beaucoup plus lisible et plus simple à écrire.
Là où c'est encore plus flagrant, c'est quand on commence à imbriquer des éléments _(comme des composants React par exemple)_ !
{% tabs defaultSelectedTab="react-no-jsx" %}
{% tab value="react-no-jsx" label="React sans JSX" %}
```js
React.createElement(
React.Fragment,
null,
React.createElement("h2", null, "Formulaire de contact"),
React.createElement(
"form",
{ onSubmit: handleSubmit },
React.createElement(
"fieldset",
null,
React.createElement("label", { htmlFor: "lastname" }, "Nom"),
React.createElement("input", { type: "text", name: "lastname", id: "lastname", required: true }),
),
React.createElement(
"fieldset",
null,
React.createElement("label", { htmlFor: "email" }, "Email"),
React.createElement("input", { type: "email", name: "email", id: "email", required: true }),
),
React.createElement(
"fieldset",
null,
React.createElement("label", { htmlFor: "message" }, "Message"),
React.createElement("textarea", { name: "message", id: "message", required: true }),
),
React.createElement(
"fieldset",
null,
React.createElement(
"label",
{ htmlFor: "gdpr" },
React.createElement("input", { type: "checkbox", name: "gdpr", id: "gdpr", required: true }),
"J'accepte que mes données soient utilisées pour me recontacter",
),
),
React.createElement("button", { type: "submit" }, "Envoyer"),
),
);
```
{% /tab %}
{% tab value="jsx" label="React avec JSX" %}
```jsx
<React.Fragment>
<h2>Formulaire de contact</h2>
<form onSubmit={handleSubmit}>
<fieldset>
<label htmlFor="lastname">Nom</label>
<input type="text" name="lastname" id="lastname" required>
</fieldset>
<fieldset>
<label for="email">Email</label>
<input type="email" name="email" id="email" required>
</fieldset>
<fieldset>
<label for="message">Message</label>
<textarea name="message" id="message" required></textarea>
</fieldset>
<fieldset>
<label for="gdpr">
<input type="checkbox" name="gdpr" id="gdpr" required>
J'accepte que mes données soient utilisées pour me recontacter
</label>
</fieldset>
<button type="submit">Envoyer</button>
</form>
</React.Fragment>
```
{% /tab %}
{% /tabs %}
Et bien même si le code final est **identique**, le **JSX** apporte une lisibilité et une simplicité d'écriture qui est très appréciable. Pas mal non ? 😄
Et donc oui ! En faisant du **JSX**, on fait en réalité du **JavaScript** et **pas du HTML** !
{% callout type="note" title="Importation de React et ses exports" %}
Au sein de ses pages, tu verras **toujours** que j'importe le contenu de React en intégralité _(comme `import React from 'react';`)_.
Dans la réalité, on va destructurer les exports de React pour n'importer que ce dont on a besoin.
Cependant, pour te donner l'information d'où provient chaque élément, je préfère importer React en intégralité et que tu puisses visualiser les éléments de React utilisés avec leur provenance.
{% /callout %}
## 🧩 Intégration de JavaScript dans le JSX
Mais l'un des autres avantages du **JSX** est la possibilité d'ajouter du JavaScript directement dans le code !
Pour pouvoir ajouter du JavaScript dans le **JSX**, il suffit d'entourer le code JavaScript avec des accolades `{}`.
C'est un peu comme si on "ouvrait un portail" pour insérer du JavaScript dans notre code **JSX**.
### 📦 Variables et fonctions
Par exemple, si tu veux afficher une variable dans ton JSX, tu peux le faire directement :
```jsx
const name = "Jean Dupont";
return <h1>Bonjour {name} !</h1>;
```
Et si tu veux appeler une fonction, c'est tout aussi simple :
```jsx
const sayHello = () => "Bonjour !";
return <p>{sayHello()}</p>;
```
### 📝 Expressions
Tu peux également ajouter des expressions _(comme des conditions ternaires par exemple)_ :
```jsx
const age = 18;
return <p>{age >= 18 ? "Majeur" : "Mineur"}</p>;
```
Mais tu peux aussi faire un **affichage conditionnel** de manière très simple :
```jsx
const isLogged = false;
return (
<div>
{isLogged && <p>Bienvenue sur notre site !</p>}
{!isLogged && <p>Connectez-vous pour accéder à notre site</p>}
</div>
);
```
### 🔄️ Boucles
Maintenant imagine que tu souhaites créer une interface qui liste des éléments provenant d'un tableau.
```jsx
const fruits = ["pomme", "banane", "fraise"];
```
Dans un premier temps, on va revoir très rapidement comment on peut parser un tableau en JavaScript :
- `for` :
```js
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
```
- `forEach` :
```js
fruits.forEach((fruit) => {
console.log(fruit);
});
```
- `map` :
```js
fruits.map((fruit) => {
console.log(fruit);
});
```
En soit, toutes ces méthodes sont très bien et font ce qu'on leur demande sans souci.
Cependant, React ne va pas forcément aimer ça sauf pour `map`.
La raison est simple :
React a besoin qu'on lui **retourne un élément** _(ou un tableau d'éléments)_ pour pouvoir les afficher.
Alors avec des `console.log` on ne va pas aller loin, mais si au lieu de retourner un `console.log` on retournait un élément **JSX** ? 🤔
```jsx
const fruits = ["pomme", "banane", "fraise"];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit}>{fruit}</li>
))}
</ul>
);
```
[Voir l'exemple sur PlayCode](https://playcode.io/1940876)
Et là : **BAM** ! 💥
Tu viens de créer une liste de fruits en utilisant un tableau de fruits.
Mais par contre...
{% callout type="question" title="C'est quoi ce `key` qui vient d'apparaître ?" %}
La `key` est une propriété spéciale que React utilise pour identifier chaque élément de manière unique.
Cela permet à React de savoir quel élément a été ajouté, modifié ou supprimé.
Il est **obligatoire** d'avoir une `key` **unique** pour chaque élément d'une liste.
Si tu listes des éléments qui ont un identifiant unique _(comme l'`id` qu'on aura dans nos données stockées dans une base de données par exemple)_, tu peux utiliser cet identifiant comme `key`.
{% /callout %}
## 📦 Les props
Les **props** _(ou propriétés)_ sont des arguments que l'on peut passer à un composant React.
Je ne vais pas trop rentrer dans les détails ici, car on va les voir dans l'article d'après !
Mais pour te donner un aperçu, voici comment on peut passer des **props** à un composant :
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
const Button = (props) => {
return <button onClick={props.onClick}>{props.children}</button>;
};
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
type ButtonProps = {
onClick: () => void;
children: React.ReactNode;
};
const Button = (props: ButtonProps) => {
return <button onClick={props.onClick}>{props.children}</button>;
};
```
{% /tab %}
{% /tabs %}
Ici, on a un composant `Button` qui prend deux **props** : `onClick` et `children`.
`onClick` est une fonction qui sera appelée lorsqu'on cliquera sur le bouton, et `children` est tout ce qui se trouve entre les balises ouvrante et fermante du composant.
## Conclusion
Alors, plutôt cool le **JSX** non ? 😎
Même si cette syntaxe rebute certains développeurs _(souvent ils se la jouent puristes, mais chuuuuut 🤫)_, elle est toutefois très appréciée pour sa simplicité et sa lisibilité.
Question de goût après tout !

View File

@ -4,20 +4,19 @@ description: Parlons un peu de React, ce qu'il est, ce qu'il fait et pourquoi il
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)] tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
--- ---
import Callout from "@/components/Callout";
import tabs from "./tabs";
Parlons peu, parlons bien ! 😄 Parlons peu, parlons bien ! 😄
React est une **bibliothèque** _(non, pas un **framework** !)_ JavaScript open-source développée par Facebook. 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. Elle est utilisée pour construire des interfaces utilisateur _(UI)_ interactives et dynamiques.
<Callout type="note" title="Pourquoi React est si populaire ?"> {% 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. - **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. - **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. - **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. - **Communauté active** : React a une communauté active de développeurs qui contribuent à son développement et partagent des ressources utiles.
</Callout>
{% /callout %}
Mais on peut aussi y noter des points faibles bien entendu, car tout n'est pas rose : Mais on peut aussi y noter des points faibles bien entendu, car tout n'est pas rose :
@ -38,7 +37,8 @@ Blague à part, pour pouvoir dire que React n'est pas un framework, il faut comp
Et si tu connais déjà React, je te vois venir avec tes grands sabots... ! 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 !"> {% 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. 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. Si on se concentre sur la **préoccupation principale** de React, c'est de gérer l'**interface utilisateur** _(UI)_ de ton application.
@ -51,7 +51,8 @@ Et si tu connais déjà React, je te vois venir avec tes grands sabots... !
_(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 😏)_ _(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 😉 Mais concentrons-nous sur React en tant que bibliothèque, et non en tant que framework 😉
</Callout>
{% /callout %}
## 📝 JSX ## 📝 JSX
@ -126,7 +127,95 @@ OK, mais vraiment petite !
Prenons l'exemple d'une application qui servira **uniquement** à afficher une liste de tâches _(une todolist donc !)_. 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 😅)_ _(Bon... utiliser React uniquement pour ça c'est abusé, mais c'est pour l'exemple 😅)_
<tabs.reactTodolist /> {% 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 : On peut très bien imaginer des améliorations à cette application, comme par exemple :

View File

@ -4,13 +4,11 @@ description: Voyons ensemble comment notre premier composant React !
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)] tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
--- ---
import Callout from "@/components/Callout";
import tabs from "./tabs";
Rentrons maintenant dans le vif du sujet en créant notre premier composant React ! Rentrons maintenant dans le vif du sujet en créant notre premier composant React !
Dans cet article, on va faire un composant très simple : un **dumb component** qui sera tout simplement un bouton. Dans cet article, on va faire un composant très simple : un **dumb component** qui sera tout simplement un bouton.
<Callout type="question" title="Un... Dumb component ?"> {% callout type="question" title="Un... Dumb component ?" %}
Un **dumb component** est un composant React qui ne contient pas de logique. Un **dumb component** est un composant React qui ne contient pas de logique.
Enfin si, il peut en contenir _(mais rien de foufou)_, son rôle est de simplement afficher des données. Enfin si, il peut en contenir _(mais rien de foufou)_, son rôle est de simplement afficher des données.
@ -19,7 +17,8 @@ Dans cet article, on va faire un composant très simple : un **dumb component**
- Il ne fait que de l'affichage - Il ne fait que de l'affichage
- Il ne fait pas de calculs - Il ne fait pas de calculs
- Il ne fait pas de requêtes HTTP - Il ne fait pas de requêtes HTTP
</Callout>
{% /callout %}
## ⚙️ Syntaxes pour créer un composant ## ⚙️ Syntaxes pour créer un composant
@ -35,7 +34,13 @@ Pourquoi ? Déjà parce que la documentation officielle recommande désormais de
Allez c'est parti, occupons-nous de notre premier composant React ! Allez c'est parti, occupons-nous de notre premier composant React !
<tabs.reactButtonComponent /> ```jsx showLineNumbers
import React from "react";
export function Button() {
return <button>Click me</button>;
}
```
... et c'est tout ! 🎉 ... et c'est tout ! 🎉
On a créé notre premier composant React, c'est pas beau ça ? On a créé notre premier composant React, c'est pas beau ça ?
@ -52,7 +57,20 @@ Avant d'aller plus loin, décortiquons un peu ce code :
C'est bien beau d'avoir un composant, mais maintenant il faut l'utiliser ! C'est bien beau d'avoir un composant, mais maintenant il faut l'utiliser !
<tabs.reactUseButtonComponent /> ```jsx
import React from "react";
import { Button } from "./Button";
export function App() {
return (
<div>
<h1>Mon premier composant React</h1>
<Button />
</div>
);
}
```
On importe notre composant `Button` _(ligne 3)_ et on l'utilise dans notre composant `App` _(ligne 9)_. On importe notre composant `Button` _(ligne 3)_ et on l'utilise dans notre composant `App` _(ligne 9)_.
@ -73,7 +91,23 @@ Imaginons ici que l'on veut rajouter :
Avant de t'expliquer comment faire, je vais te montrer ce que ça donne pour son utilisation : Avant de t'expliquer comment faire, je vais te montrer ce que ça donne pour son utilisation :
<tabs.reactUseButtonComponentProps /> ```jsx
import { Button } from "./Button";
import React from "react";
export function App() {
function handleClick() {
console.log("Je suis cliqué !");
}
return (
<div>
<h1>Mon premier composant React</h1>
<Button onClick={handleClick}>Clique-moi !</Button>
</div>
);
}
```
Voyons un peu ce qui change ! Voyons un peu ce qui change !
@ -83,7 +117,38 @@ Voyons un peu ce qui change !
Mais alors, comment on fait pour récupérer ces props dans notre composant Button ? Mais alors, comment on fait pour récupérer ces props dans notre composant Button ?
<tabs.reactButtonComponentProps /> {% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
import React from "react";
export function Button(props) {
return <button onClick={props.onClick}>{props.children}</button>;
}
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import React from "react";
type ButtonProps = {
onClick: () => void;
children: React.ReactNode;
};
export function Button(props: ButtonProps) {
return <button onClick={props.onClick}>{props.children}</button>;
}
```
{% /tab %}
{% /tabs %}
Comme tu peux voir, on récupère les props passées à notre composant `Button` en tant que paramètre de la fonction. Comme tu peux voir, on récupère les props passées à notre composant `Button` en tant que paramètre de la fonction.
@ -91,12 +156,14 @@ Mais tu peux également remarquer qu'il y a une propriété `children` qui n'est
C'est totalement normal ! `children` est une prop spéciale qui contient tout ce qui se trouve entre les balises ouvrante et fermante du composant. C'est totalement normal ! `children` est une prop spéciale qui contient tout ce qui se trouve entre les balises ouvrante et fermante du composant.
<Callout type="note" title="Le JavaScript inline, c'est pas bien !"> {% callout type="note" title="Le JavaScript inline, c'est pas bien !" %}
Tu as totalement raison ! On recommande effectivement de ne pas faire du JS inline dans notre HTML et de privilégier un fichier distinct pour notre JavaScript. Tu as totalement raison ! On recommande effectivement de ne pas faire du JS inline dans notre HTML et de privilégier un fichier distinct pour notre JavaScript.
Et donc tu sais déjà qu'on va préférer l'utilisation des `addEventListener` 😏 Et donc tu sais déjà qu'on va préférer l'utilisation des `addEventListener` 😏
... cependant ici, **on ne fait pas du HTML** mais du JSX, et c'est une autre histoire ! ... cependant ici, **on ne fait pas du HTML** mais du JSX, et c'est une autre histoire !
</Callout>
{% /callout %}
## 🖇️ Les différentes props ## 🖇️ Les différentes props
@ -119,11 +186,13 @@ Mais alors, pourquoi ?
Comme le JSX reste avant tout du JavaScript, on ne peut pas utiliser des mots-clés réservés comme `class`, `for`, `default`, etc. Comme le JSX reste avant tout du JavaScript, on ne peut pas utiliser des mots-clés réservés comme `class`, `for`, `default`, etc.
<Callout type="note" collapsible title="Plus d'informations sur les mots-clés protégés"> {% callout type="note" collapsible=true title="Plus d'informations sur les mots-clés protégés" %}
React comprendra ces mots clés au sein des composants, cependant il va générer un avertissement dans la console du navigateur pour te prévenir que tu utilises un mot-clé protégé qui est "ambigu". React comprendra ces mots clés au sein des composants, cependant il va générer un avertissement dans la console du navigateur pour te prévenir que tu utilises un mot-clé protégé qui est "ambigu".
Cependant, certaines bibliothèques qui utilisent le JSX, comme SolidJS par exemple, utilisent ces mots-clés protégés au sein des composants. Ca ne veut pas dire que c'est "bien" ou "pas bien", mais qu'il faut être conscient de ce que l'on fait et de comment est interprété notre code par la bibliothèque 😉 Cependant, certaines bibliothèques qui utilisent le JSX, comme SolidJS par exemple, utilisent ces mots-clés protégés au sein des composants. Ca ne veut pas dire que c'est "bien" ou "pas bien", mais qu'il faut être conscient de ce que l'on fait et de comment est interprété notre code par la bibliothèque 😉
</Callout>
{% /callout %}
On va donc utiliser des noms d'attributs qui sont similaires à ceux du HTML, mais qui sont adaptés pour le JSX. On va donc utiliser des noms d'attributs qui sont similaires à ceux du HTML, mais qui sont adaptés pour le JSX.

View File

@ -4,9 +4,6 @@ description: Voyons ensemble comment gérer le state et le cycle de vie d'un com
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)] tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
--- ---
import Callout from "@/components/Callout";
import tabs from "./tabs";
Dans le précédent article, nous avons vu comment créer notre premier composant React avec notamment le concept de **props**. Dans le précédent article, nous avons vu comment créer notre premier composant React avec notamment le concept de **props**.
Voyons maintenant comment gérer le **state** et le **cycle de vie** d'un composant React ! Voyons maintenant comment gérer le **state** et le **cycle de vie** d'un composant React !
@ -24,13 +21,15 @@ Voici les trois différentes étapes du cycle de vie d'un composant React :
On verra un peu plus en détail ces étapes dans l'article suivant qui traitera un certain hook de React : `useEffect`. On verra un peu plus en détail ces étapes dans l'article suivant qui traitera un certain hook de React : `useEffect`.
<Callout type="question" title="Hook, comme le capitaine ? 🦜🏴‍☠️"> {% callout type="question" title="Hook, comme le capitaine ? 🦜🏴‍☠️" %}
Haha, non ! Haha, non !
Un hook en React, est une fonction qui permet d'exploiter les fonctionnalités de React dans un composant fonctionnel _(fonction)_. Un hook en React, est une fonction qui permet d'exploiter les fonctionnalités de React dans un composant fonctionnel _(fonction)_.
Bon... c'est un peu du charabia, mais on verra ça plus en détail dans le prochain article car il y a beaucoup à dire sur les hooks ! Bon... c'est un peu du charabia, mais on verra ça plus en détail dans le prochain article car il y a beaucoup à dire sur les hooks !
</Callout>
{% /callout %}
Mais pour le moment, restons en à une vue d'ensemble du cycle de vie ! Mais pour le moment, restons en à une vue d'ensemble du cycle de vie !
@ -43,7 +42,20 @@ Mais alors, pourquoi utiliser un state alors qu'on pourrait tout simplement déc
Prenons cet exemple : Prenons cet exemple :
<tabs.reactLocalVariable /> ```jsx showLineNumbers
import React from "react";
export function Counter() {
let count = 0;
function increment() {
count += 1;
console.log("Increment", count);
}
return <button onClick={increment}>{count}</button>;
}
```
[Voir l'exemple sur PlayCode](https://playcode.io/1940876) [Voir l'exemple sur PlayCode](https://playcode.io/1940876)
@ -62,11 +74,14 @@ Le **state** est **réactif** et permet à React de savoir quand il doit mettre
Pour déclarer un **state**, on utilise le **hook** `useState` de React. Pour déclarer un **state**, on utilise le **hook** `useState` de React.
<tabs.reactStateDeclaration /> ```jsx
const [count, setCount] = React.useState(0);
```
Et là tu vas peut-être te demander une chose... Et là tu vas peut-être te demander une chose...
<Callout type="question" title="Ouh là... Pourquoi on a deux assignements ?"> {% callout type="question" title="Ouh là... Pourquoi on a deux assignements ?" %}
Bien vu ! Effectivement on va avoir deux assignements pour déclarer un state : Bien vu ! Effectivement on va avoir deux assignements pour déclarer un state :
- `count` : la valeur du state - `count` : la valeur du state
@ -75,13 +90,26 @@ Et là tu vas peut-être te demander une chose...
Si tu as déjà fait de la POO, le principe de **getter** et **setter** te sera familier puisque c'est un peu le même principe ! Si tu as déjà fait de la POO, le principe de **getter** et **setter** te sera familier puisque c'est un peu le même principe !
Le hook `useState` prend en paramètre la **valeur initiale du state** _(ici 0)_ et retourne un tableau avec la valeur du state et la fonction pour le modifier. Le hook `useState` prend en paramètre la **valeur initiale du state** _(ici 0)_ et retourne un tableau avec la valeur du state et la fonction pour le modifier.
</Callout>
{% /callout %}
## 🔄 Utilisation du state ## 🔄 Utilisation du state
Maintenant que notre state est déclaré, on peut l'utiliser dans notre composant. Maintenant que notre state est déclaré, on peut l'utiliser dans notre composant.
<tabs.reactStateUsage /> ```jsx showLineNumbers
import React from "react";
export function Counter() {
const [count, setCount] = React.useState(0);
function increment() {
setCount(count + 1);
}
return <button onClick={increment}>{count}</button>;
}
```
[Voir l'exemple sur PlayCode](https://playcode.io/1940705) [Voir l'exemple sur PlayCode](https://playcode.io/1940705)
@ -90,7 +118,7 @@ Et voilà ! Pas besoin de plus pour gérer un state en React 😉
Mais qu'est-ce qu'il se passe sous le capot ? Mais qu'est-ce qu'il se passe sous le capot ?
C'est un peu plus complexe que ça, mais pour faire simple : C'est un peu plus complexe que ça, mais pour faire simple :
### ⚙️ Montage du composant (Mounting) ### ⚙️ Montage du composant _(Mounting)_
On vient prévenir React que notre composant va avoir un **state** et on lui donne une valeur initiale _(ici 0)_. On vient prévenir React que notre composant va avoir un **state** et on lui donne une valeur initiale _(ici 0)_.

View File

@ -0,0 +1,537 @@
---
title: Le hook useContext de React
description: Découvrez comment utiliser le hook useContext de React pour gérer les contextes dans vos applications.
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
---
Les contextes sont un moyen de diffuser des données au travers des composants, sans avoir à les passer explicitement à chaque composant.
Pour faire simple, imaginons une arborescence de plusieurs composants imbriqués les uns dans les autres :
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
import { useState } from "react";
const App = () => {
const [theme, setTheme] = useState("light");
return <A theme={theme} setTheme={theme} />;
};
const A = ({ theme, setTheme }) => {
return <B theme={theme} setTheme={setTheme} />;
};
const B = ({ theme, setTheme }) => {
return <C theme={theme} setTheme={setTheme} />;
};
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
type Theme = "light" | "dark";
const App = () => {
const [theme, setTheme] = useState<Theme>("light");
return <A theme={theme} setTheme={theme} />;
};
const A = ({ theme, setTheme }: { theme: Theme; setTheme: Dispatch<SetStateAction<Theme>> }) => {
return <B theme={theme} setTheme={setTheme} />;
};
const B = ({ theme, setTheme }: { theme: Theme; setTheme: Dispatch<SetStateAction<Theme>> }) => {
return <C theme={theme} setTheme={setTheme} />;
};
```
{% /tab %}
{% /tabs %}
Fastidieux, n'est-ce pas ? On transmet à chaque fois les mêmes données, et ce, à chaque niveau de l'arborescence.
C'est là que les contextes entrent en jeu !
On va pouvoir alors déclarer notre contexte _(qui contiendra les données à diffuser)_ et le fournir à un niveau supérieur de l'arborescence.
## Déclaration d'un contexte
Avant de penser à notre contexte, on va réfléchir à ce que l'on veut diffuser et les valeurs par défaut.
Si on reprend notre exemple avec le thème clair et sombre, on sait que l'on va vouloir diffuser la valeur du thème et une fonction pour le changer.
On va donc préparer le terrain en créant un fichier `ThemeContext.jsx` _(ou `ThemeContext.tsx` si tu utilises TypeScript)_ :
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
import { createContext } from "react";
// On crée notre contexte, avec une valeur par défaut : un thème clair
const ThemeContext = createContext({
theme: "light",
setTheme: () => {},
});
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import type { Dispatch, SetStateAction } from "react";
import { createContext } from "react";
// On crée un type pour les valeurs de thème
export type Theme = "light" | "dark";
// On crée un type pour notre contexte
type ThemeContextType = {
theme: Theme;
setTheme: Dispatch<SetStateAction<Theme>>;
};
// On crée notre contexte, avec une valeur par défaut : un thème clair
const ThemeContext = createContext<ThemeContextType>({
theme: "light",
setTheme: () => {},
});
```
{% /tab %}
{% /tabs %}
## Fournir un contexte
Maintenant on peut le dire : notre contexte est prêt à être utilisé !
Il ne reste plus qu'à le fournir à notre arborescence de composants en lui créant un `Provider`.
{% callout type="question" title="Un provider ?" %}
Un `Provider` est un composant qui va permettre de **diffuser** les données du contexte à ses enfants.
Il est important de noter que le `Provider` doit **englober** les composants qui vont utiliser le contexte.
{% /callout %}
Un contexte React est un objet qui contient deux propriétés : `Provider` et `Consumer`.
Le `Provider` est un composant qui va permettre de diffuser les données du contexte à ses enfants.
Le `Consumer` est un composant qui va permettre de récupérer les données du contexte.
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
import { useState } from "react";
const App = () => {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<A />
</ThemeContext.Provider>
);
};
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import type { Theme } from "./ThemeContext";
import { useState } from "react";
const App = () => {
const [theme, setTheme] = useState<Theme>("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<A />
</ThemeContext.Provider>
);
};
```
{% /tab %}
{% /tabs %}
Mais on peut aller encore plus loin, en créant un Provider dédié à notre contexte !
Cela permettra de simplifier l'arborescence de composants et de rendre le code plus lisible :
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
import { createContext, useState } from "react";
const ThemeContext = createContext({
theme: "light",
setTheme: () => {},
});
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light");
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
};
export { ThemeContext, ThemeProvider };
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import type { ReactNode } from "react";
import { createContext, useState } from "react";
export type Theme = "light" | "dark";
type ThemeContextType = {
theme: Theme;
setTheme: Dispatch<SetStateAction<Theme>>;
};
const ThemeContext = createContext<ThemeContextType>({
theme: "light",
setTheme: () => {},
});
type ThemeProviderProps = {
children: ReactNode;
};
const ThemeProvider = ({ children }: ThemeProviderProps) => {
const [theme, setTheme] = useState<Theme>("light");
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
};
export { ThemeContext, ThemeProvider };
```
{% /tab %}
{% /tabs %}
Et pour terminer, on va maintenant pouvoir directement imbriquer notre `ThemeProvider` dans notre `App` :
```jsx
import { ThemeProvider } from "./ThemeContext";
const App = () => {
return (
<ThemeProvider>
<A />
</ThemeProvider>
);
};
```
## Utilisation d'un contexte
C'est bien beau de créer un contexte, mais comment l'utiliser ?
Tu te souviens peut-être du `Consumer` que l'on a évoqué plus tôt, non ?
Et bien, il est temps de le mettre en pratique ! 😁
Pour commencer, nous allons avoir besoin du hook `useContext` de React.
Ce hook va nous permettre de récupérer les données du contexte, et ce, directement dans nos composants.
```jsx
import { ThemeContext } from "./ThemeContext";
import { useContext } from "react";
const C = () => {
const { theme, setTheme } = useContext(ThemeContext);
return <>{/** JSX */}</>;
};
```
Pas mal, non ? 😉
Fini l'arborescence de composants à rallonge, on peut maintenant récupérer les données du contexte directement dans nos composants !
## Les défauts des contextes
Seulement... Un grand pouvoir implique de grandes responsabilités. 🕷️
Bien que les contextes soient très pratiques, il faut prendre en compte quelques points :
- On ne peut pas utiliser les contextes pour tout et n'importe quoi. Ils sont plutôt adaptés pour diffuser des données qui sont utilisées par plusieurs composants.
- Les contextes peuvent rendre le code plus difficile à comprendre.
- L'utilisation de nombreux contextes va faire apparaître ce qu'on appelle le **context hell**.
### Le context hell
Dans cet article, nous avons vu comment créer un contexte et l'utiliser.
Et par chance, nous n'avons pas encore rencontré le **context hell**.
Mais maintenant, que se passe-t-il si on a besoin de plusieurs contextes _(plusieurs dizaines par exemple !)_ dans notre application ?
On va se retrouver avec une arborescence de composants qui va devenir de plus en plus difficile à comprendre et à maintenir.
Et c'est ça, le **context hell**.
```jsx
root.render(
<StrictMode>
<UserProvider>
<ThemeProvider>
<LanguageProvider>
<PostProvider>
<SettingsProvider>
<SocketProvider>
<FriendProvider>
<NotificationProvider>
<ChatProvider>
<MusicProvider>
<VideoProvider>
<GameProvider>
<WeatherProvider>
<NewsProvider>
<CalendarProvider>
<TaskProvider>
<NoteProvider>
<App />
</NoteProvider>
</TaskProvider>
</CalendarProvider>
</NewsProvider>
</WeatherProvider>
</GameProvider>
</VideoProvider>
</MusicProvider>
</ChatProvider>
</NotificationProvider>
</FriendProvider>
</SocketProvider>
</SettingsProvider>
</PostProvider>
</LanguageProvider>
</ThemeProvider>
</UserProvider>
</StrictMode>,
);
```
Maintenant, demande à un développeur d'inverser le provider `UserProvider` avec le provider `NoteProvider`.
C'est jouable sans difficulté, mais si tu entends des cris de désespoir, c'est normal. 😅
Pour éviter de tomber dans le **context hell**, il est important de bien réfléchir à l'utilisation des contextes dans notre application avec ces quelques questions :
- Est-ce que l'utilisation d'un contexte est vraiment nécessaire pour ce cas d'usage ?
- Est-ce que le contexte est utilisé par plusieurs composants ?
- Est-ce que le contexte est utilisé par des composants éloignés dans l'arborescence ?
Mais alors, si tu as besoin d'autant de contextes dans ton application, comment faire ?
Et bien, il existe des solutions pour éviter le **context hell** :
- Utiliser des bibliothèques tierces comme Redux _(solution lourde, mais très puissante)_
- Créer un nouveau composant qui va regrouper tous les contextes _(solution plus légère, mais plus difficile à maintenir)_
N'étant pas un grand fan de Redux, je vais plutôt te présenter la deuxième solution.
Mais si tu veux en savoir plus sur Redux, n'hésite pas à consulter la documentation officielle !
### Résoudre le context hell avec un composant dédié
Parlons de ce fameux composant qui va regrouper tous les contextes !
On ne parle pas ici d'un simple composant Providers qui va imbriquer tous les Provider de nos contextes, mais d'une solution plus élégante.
Après tout, nous sommes des feignants développeurs, non ? 😏
Réfléchissons à ce que l'on veut faire :
- On veut pouvoir regrouper tous les contextes dans un seul composant.
- On veut pouvoir ajouter ou supprimer des contextes facilement.
- On veut pouvoir facilement les ordonner entre eux.
- On veut éviter le **context hell**.
Et si on créait un composant Providers qui va nous permettre de faire tout ça ?
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
const Providers = ({ providers, children }) => {
return (
<>
{/** Ouverture des providers */}
{children}
{/** Fermeture des providers */}
</>
);
};
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import type { ReactNode } from "react";
type ProvidersProps = {
providers: ReactNode[];
children: ReactNode;
};
const Providers = ({ providers, children }: ProvidersProps) => {
return (
<>
{/** Ouverture des providers */}
{children}
{/** Fermeture des providers */}
</>
);
};
```
{% /tab %}
{% /tabs %}
Ici on ne va pas remettre une cascade de Provider comme on a pu le voir plus tôt.
On va chercher à créer une fonction qui va nous permettre de les imbriquer les uns dans les autres.
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
const nest = (children, component) => {
return React.cloneElement(component, {}, children);
};
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
const nest = (children: ReactNode, component: ReactNode) => {
return React.cloneElement(component, {}, children);
};
```
{% /tab %}
{% /tabs %}
{% callout type="note" title="React.cloneElement" %}
`React.cloneElement` est une fonction qui va permettre de cloner un élément React en lui passant de nouvelles propriétés.
Cela va nous permettre de créer une nouvelle arborescence de composants sans modifier l'arborescence actuelle.
Le premier argument est l'élément à cloner _(le composant)_, et le deuxième argument est un objet contenant les nouvelles propriétés.
Le troisième argument est le contenu de l'élément cloné _(les enfants)_.
{% /callout %}
Et maintenant, on va pouvoir utiliser notre fonction `nest` pour imbriquer nos Provider en utilisant la méthode `reduceRight` :
{% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
```jsx
const nest = (children, component) => {
return React.cloneElement(component, {}, children);
};
const Providers = ({ providers, children }) => {
return providers.reduceRight(nest, children);
};
```
{% /tab %}
{% tab value="tsx" label="TSX" %}
```tsx
import type { ReactNode } from "react";
type ProvidersProps = {
providers: ReactNode[];
children: ReactNode;
};
const nest = (children: ReactNode, component: ReactNode) => {
return React.cloneElement(component, {}, children);
};
const Providers = ({ providers, children }: ProvidersProps) => {
return providers.reduceRight(nest, children);
};
```
{% /tab %}
{% /tabs %}
{% callout type="note" title="reduceRight" %}
reduceRight est une méthode qui va permettre de réduire un tableau _(ou un objet)_ en appliquant une fonction de rappel de droite à gauche.
Cela va nous permettre de réduire un tableau de `Provider` en les imbriquant les uns dans les autres sans se soucier de l'ordre _(qui est défini par le tableau)_.
Dans l'idée, on commence par le **dernier** élément du tableau, et on l'imbrique avec l'élément **précédent** du tableau et ainsi de suite jusqu'au **premier** élément du tableau.
Chaque itération va créer un nouvel élément imbriqué dans le précédent, en appelant la fonction `nest` qui est passée en argument.
{% /callout %}
Et voilà ! Il ne nous reste plus qu'à utiliser notre composant `Providers` pour regrouper tous nos `Provider` :
```jsx
root.render(
<StrictMode>
<Providers
providers={[
<UserProvider />,
<ThemeProvider />,
<LanguageProvider />,
<PostProvider />,
<SettingsProvider />,
<SocketProvider />,
<FriendProvider />,
// ...
]}
>
<App />
</Providers>
</StrictMode>,
);
```
Évidemment le fichier contiendra toujours beaucoup de lignes, mais au moins, on a évité le **context hell** !
Il sera nettement plus facile de modifier l'ordre des Provider ou d'en ajouter de nouveaux.
## Conclusion
Ça casse un peu la tête, mais les contextes sont un outil très puissant pour diffuser des données dans nos applications React.
C'est aussi une excellente solution pour éviter d'utiliser des bibliothèques tierces comme Redux _(qui est très bien, mais qui peut être un peu lourd pour des petites applications)_.
On prendra d'ailleurs le temps de parler de Redux et de Zustand dans un prochain article 😉
Et si tu as besoin de plusieurs contextes dans ton application, n'oublie pas de réfléchir à l'utilisation de notre composant Providers pour éviter le **context hell**.

View File

@ -4,16 +4,15 @@ description: Découvre les hooks de React, une fonctionnalité qui te permet de
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)] tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
--- ---
import Callout from "@/components/Callout";
import tabs from "./tabs";
Si tu as lu les précédentes pages concernant les hooks de React _(useState, useEffect et useContext)_, tu as déjà une bonne vision de la manière dont tu peux concevoir une application React. Si tu as lu les précédentes pages concernant les hooks de React _(useState, useEffect et useContext)_, tu as déjà une bonne vision de la manière dont tu peux concevoir une application React.
Mais si je te dis que tu peux aller encore plus loin avec useReducer pour la gestion des états, est-ce que tu serais intéressé·e ? 🤔 Mais si je te dis que tu peux aller encore plus loin avec useReducer pour la gestion des états, est-ce que tu serais intéressé·e ? 🤔
<Callout type="question" title="Pourquoi ? useState ne suffit pas ?"> {% callout type="question" title="Pourquoi ? useState ne suffit pas ?" %}
Le hook `useState` est génial et essentiel pour gérer l'état local d'un composant, mais il n'est pas adapté pour des états dits "complexes" ou pour des états qui dépendent les uns des autres. Le hook `useState` est génial et essentiel pour gérer l'état local d'un composant, mais il n'est pas adapté pour des états dits "complexes" ou pour des états qui dépendent les uns des autres.
</Callout>
{% /callout %}
## Qu'est-ce que le hook useReducer ? ## Qu'est-ce que le hook useReducer ?
@ -47,7 +46,17 @@ Comme expliqué plus tôt, un reducer est une fonction qui prend en paramètre u
Parlons dans un premier temps de la signature d'un reducer : Parlons dans un premier temps de la signature d'un reducer :
<tabs.reactReducerExample /> {% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
{% snippet path="react/reducer/reducer-example.jsx" language="jsx" showLineNumbers=true /%}
{% /tab %}
{% tab value="tsx" label="TSX" %}
{% snippet path="react/reducer/reducer-example.tsx" language="tsx" showLineNumbers=true /%}
{% /tab %}
{% /tabs %}
Comme tu peux le voir, on récupère bien deux paramètres : `state` et `action`. Comme tu peux le voir, on récupère bien deux paramètres : `state` et `action`.
@ -61,17 +70,19 @@ L'état est contraint au principe d'**immutabilité**.
On fera donc des `return` de l'état actuel avec les modifications nécessaires. On fera donc des `return` de l'état actuel avec les modifications nécessaires.
<Callout type="note" title="Pourquoi déverser le contenu de l'état actuel ?"> {% callout type="note" title="Pourquoi déverser le contenu de l'état actuel ?" %}
Si on ne déverse pas le contenu de l'état actuel, on perdrait les propriétés qui ne sont pas modifiées par l'action. Si on ne déverse pas le contenu de l'état actuel, on perdrait les propriétés qui ne sont pas modifiées par l'action.
En déversant le contenu de l'état actuel, on s'assure de ne pas perdre ces propriétés. En déversant le contenu de l'état actuel, on s'assure de ne pas perdre ces propriétés.
Par exemple : Par exemple :
<tabs.reactReducerWhySpreadOperator /> {% snippet path="react/reducer/reducer-why-spread-operator.jsx" language="jsx" showLineNumbers=true /%}
On perdrait ici la propriété `message` si on ne la déversait pas dans le nouvel état. On perdrait ici la propriété `message` si on ne la déversait pas dans le nouvel état.
</Callout>
{% /callout %}
## Comment utiliser useReducer ? ## Comment utiliser useReducer ?
@ -79,34 +90,68 @@ Maintenant que tu as une idée de ce qu'est un reducer, voyons comment l'utilise
Naturellement, on va commencer par importer le hook `useReducer` : Naturellement, on va commencer par importer le hook `useReducer` :
<tabs.reactUseReducerImport /> ```js
import { useReducer } from "react";
```
Ensuite, on va définir notre état initial : Ensuite, on va définir notre état initial :
<tabs.reactReducerInitialState /> {% tabs defaultSelectedTab="js" %}
{% tab value="js" label="JavaScript" %}
{% snippet path="react/reducer/reducer-initial-state.js" language="js" /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
{% snippet path="react/reducer/reducer-initial-state.ts" language="ts" /%}
{% /tab %}
{% /tabs %}
On peut maintenant définir notre reducer : On peut maintenant définir notre reducer :
<tabs.reactCounterReducer /> {% tabs defaultSelectedTab="js" %}
{% tab value="js" label="JavaScript" %}
{% snippet path="react/reducer/reducer.js" language="js" showLineNumbers=true /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
{% snippet path="react/reducer/reducer.ts" language="ts" showLineNumbers=true /%}
{% /tab %}
{% /tabs %}
{% callout type="question" title="C'est quoi `action.payload` ?" %}
<Callout type="question" title="C'est quoi `action.payload` ?">
La propriété `payload` de l'action est optionnelle. Il s'agit d'une convention pour passer des données à l'action. La propriété `payload` de l'action est optionnelle. Il s'agit d'une convention pour passer des données à l'action.
Le `!` après `action.payload` signifie que l'on est sûr que `payload` est défini. Le `!` après `action.payload` signifie que l'on est sûr que `payload` est défini.
Cela permet d'éviter une erreur de type avec TypeScript. Cela permet d'éviter une erreur de type avec TypeScript.
Dans le cas du type `SET`, le payload sera défini obligatoirement avec un nombre qui sera la nouvelle valeur de la propriété `count` de l'état. Dans le cas du type `SET`, le payload sera défini obligatoirement avec un nombre qui sera la nouvelle valeur de la propriété `count` de l'état.
</Callout>
{% /callout %}
Enfin, on peut utiliser le hook useReducer dans notre composant : Enfin, on peut utiliser le hook useReducer dans notre composant :
<tabs.reactUseReducerUsage /> {% tabs defaultSelectedTab="js" %}
{% tab value="js" label="JavaScript" %}
{% snippet path="react/reducer/reducer-hook.js" language="js" /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
{% snippet path="react/reducer/reducer-hook.ts" language="ts" /%}
{% /tab %}
{% /tabs %}
`state` contient l'état actuel, et `dispatch` est une fonction qui permet d'envoyer une action au reducer. `state` contient l'état actuel, et `dispatch` est une fonction qui permet d'envoyer une action au reducer.
Pour modifier l'état, on va donc appeler `dispatch` avec une action : Pour modifier l'état, on va donc appeler `dispatch` avec une action :
<tabs.reactDispatchIncrement /> {% snippet path="react/reducer/reducer-dispatch-increment.js" language="js" /%}
Et voilà, tu sais maintenant comment utiliser `useReducer` dans une application React ! 🎉 Et voilà, tu sais maintenant comment utiliser `useReducer` dans une application React ! 🎉
@ -130,15 +175,27 @@ Pour contrer ces problèmes, on va créer des actions et des types d'actions pou
Nos types d'actions seront tous des chaînes de caractères. On va donc pouvoir les définir sous forme de constantes. Nos types d'actions seront tous des chaînes de caractères. On va donc pouvoir les définir sous forme de constantes.
<tabs.reactActionsConstants /> {% snippet path="react/reducer/reducer-actions-constants.js" language="js" /%}
{% callout type="note" title="Regrouper les exports" %}
<Callout type="note" title="Regrouper les exports">
Et là, tu te dis : "Pourquoi ne pas regrouper les exports dans un seul objet ?" Et là, tu te dis : "Pourquoi ne pas regrouper les exports dans un seul objet ?"
Bien vu ! Et pour TypeScript, on peut aller encore plus loin en créant un `enum` pour les types d'actions 😉 Bien vu ! Et pour TypeScript, on peut aller encore plus loin en créant un `enum` pour les types d'actions 😉
<tabs.reactActionsEnum /> {% tabs defaultSelectedTab="js" %}
</Callout>
{% tab value="js" label="JavaScript" %}
{% snippet path="react/reducer/reducer-actions-enum.js" language="js" /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
{% snippet path="react/reducer/reducer-actions-enum.ts" language="ts" /%}
{% /tab %}
{% /tabs %}
{% /callout %}
### Typage des actions ### Typage des actions
@ -146,21 +203,31 @@ Si tu utilises JavaScript, je suis désolé de te dire que tu ne peux pas **fort
En revanche, si tu utilises TypeScript, tu peux définir les actions de la manière suivante : En revanche, si tu utilises TypeScript, tu peux définir les actions de la manière suivante :
<tabs.reactActionsUnion /> {% snippet path="react/reducer/reducer-actions-union.ts" language="ts" /%}
Tu pourras alors utiliser `CounterAction` pour typer les actions de ton reducer : Tu pourras alors utiliser `CounterAction` pour typer les actions de ton reducer :
<tabs.reactActionsUnionUsage /> {% snippet path="react/reducer/reducer-actions-union-use.ts" language="ts" /%}
### Action creators ### Action creators
Pour éviter de se tromper dans le type de l'action, on peut se créer des fonctions qui vont nous permettre de créer des actions. Pour éviter de se tromper dans le type de l'action, on peut se créer des fonctions qui vont nous permettre de créer des actions.
<tabs.reactActionCreator /> {% tabs defaultSelectedTab="js" %}
{% tab value="js" label="JavaScript" %}
{% snippet path="react/reducer/reducer-action-creator.js" language="js" /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
{% snippet path="react/reducer/reducer-action-creator.ts" language="ts" /%}
{% /tab %}
{% /tabs %}
Maintenant le dispatch de nos actions sera beaucoup plus simple et éviter davantage les erreurs lors du développement ! Maintenant le dispatch de nos actions sera beaucoup plus simple et éviter davantage les erreurs lors du développement !
<tabs.reactDispatchActionCreator /> {% snippet path="react/reducer/reducer-dispatch-action-creator.js" language="js" /%}
## Les fichiers complets ## Les fichiers complets
@ -169,36 +236,64 @@ Pour t'aider à mieux comprendre le fonctionnement du hook `useReducer` et comme
### Fichier counterReducer.js ou counterReducer.ts ### Fichier counterReducer.js ou counterReducer.ts
<tabs.reactFileCounterReducer /> {% tabs defaultSelectedTab="js" %}
{% tab value="js" label="JavaScript" %}
{% snippet path="react/reducer/file-counterReducer.js" language="js" showLineNumbers=true label="src/reducers/counterReducer.js" /%}
{% /tab %}
{% tab value="ts" label="TypeScript" %}
{% snippet path="react/reducer/file-counterReducer.ts" language="ts" showLineNumbers=true label="src/reducers/counterReducer.ts" /%}
{% /tab %}
{% /tabs %}
### Fichier Counter.jsx ou Counter.tsx ### Fichier Counter.jsx ou Counter.tsx
<tabs.reactFileCounterComponent /> {% tabs defaultSelectedTab="jsx" %}
{% tab value="jsx" label="JSX" %}
{% snippet path="react/reducer/file-counter.jsx" language="jsx" showLineNumbers=true label="src/components/Counter.jsx" /%}
{% /tab %}
{% tab value="tsx" label="TSX" %}
{% snippet path="react/reducer/file-counter.tsx" language="tsx" showLineNumbers=true label="src/components/Counter.tsx" /%}
{% /tab %}
{% /tabs %}
## C'est l'heure des questions ! ## C'est l'heure des questions !
<Callout type="question" title="Quand utiliser `useReducer` ?"> {% callout type="question" title="Quand utiliser `useReducer` ?" %}
- **A** - Pour des états simples - **A** - Pour des états simples
- **B** - Pour des états complexes ou des états qui dépendent les uns des autres - **B** - Pour des états complexes ou des états qui dépendent les uns des autres
</Callout>
<Callout type="question" title="Quelle est la signature d'un reducer ?"> {% /callout %}
{% callout type="question" title="Quelle est la signature d'un reducer ?" %}
- **A** - `(state, action) => { /* ... */ }` - **A** - `(state, action) => { /* ... */ }`
- **B** - `(action, state) => { /* ... */ }` - **B** - `(action, state) => { /* ... */ }`
- **C** - `(state) => { /* ... */ }` - **C** - `(state) => { /* ... */ }`
- **D** - `(action) => { /* ... */ }` - **D** - `(action) => { /* ... */ }`
</Callout>
<Callout type="question" title="Pourquoi déverser le contenu de l'état actuel dans le nouvel état ?"> {% /callout %}
{% callout type="question" title="Pourquoi déverser le contenu de l'état actuel dans le nouvel état ?" %}
- **A** - Pour rendre le code plus lisible - **A** - Pour rendre le code plus lisible
- **B** - Pour ne pas perdre les propriétés qui ne sont pas modifiées par l'action - **B** - Pour ne pas perdre les propriétés qui ne sont pas modifiées par l'action
</Callout>
<Callout type="question" title="Pourquoi utiliser des constantes pour les types d'actions ?"> {% /callout %}
{% callout type="question" title="Pourquoi utiliser des constantes pour les types d'actions ?" %}
- **A** - Pour rendre le code plus lisible - **A** - Pour rendre le code plus lisible
- **B** - Pour alourdir inutillement le code - **B** - Pour alourdir inutillement le code
- **C** - Pour éviter de se tromper dans le type de l'action - **C** - Pour éviter de se tromper dans le type de l'action
</Callout>
{% /callout %}
## Conclusion ## Conclusion

19
app/pages/index/+Page.mdx → app/data/page.md Executable file → Normal file
View File

@ -4,8 +4,6 @@ description: Plonge toi dans une documentation synthétique et concise, conçue
tags: [] tags: []
--- ---
import QuickLinks from "@/components/QuickLinks";
Toi qui vient d'arriver sur cette plateforme, sache que si tu cherches un coup de main pour mieux comprendre certaines notions dans le développement, tu es tombé au bon endroit ! Toi qui vient d'arriver sur cette plateforme, sache que si tu cherches un coup de main pour mieux comprendre certaines notions dans le développement, tu es tombé au bon endroit !
En prime, tu trouveras également des synthèses de certains référentiels de titres professionnels ! 🎉 En prime, tu trouveras également des synthèses de certains référentiels de titres professionnels ! 🎉
@ -18,21 +16,8 @@ En prime, tu trouveras également des synthèses de certains référentiels de t
Le contenu de cette plateforme est divisé en plusieurs sections : Le contenu de cette plateforme est divisé en plusieurs sections :
<QuickLinks> - [**Certifications**](/certifications) : Des synthèses de référentiels des certifications de titres professionnels pour t'aider à te préparer aux examens.
<QuickLinks.QuickLink - [**Documentations**](/docs) : Une documentation synthétique _(mais complète et détaillée)_ sur les différentes technologies du développement web.
href="/certifications"
title="Certifications"
description="Des synthèses de référentiels des certifications de titres professionnels pour t'aider à te préparer aux examens."
icon="presets"
/>
<QuickLinks.QuickLink
href="/docs"
title="Documentations"
description="Des documentations synthétiques (mais complètes et détaillées) sur les différentes technologies du développement web."
icon="presets"
/>
</QuickLinks>
Les différents contenus ne sont pas uniquement destinées aux développeurs, mais également aux passionnés de l'information en quête de savoir ! Les différents contenus ne sont pas uniquement destinées aux développeurs, mais également aux passionnés de l'information en quête de savoir !
Il est donc possible que tu trouves des articles qui ne te concernent pas directement, mais qui pourraient t'intéresser tout de même ! 😊 Il est donc possible que tu trouves des articles qui ne te concernent pas directement, mais qui pourraient t'intéresser tout de même ! 😊

View File

@ -0,0 +1,13 @@
#!/bin/bash
# Variables
DB_USER="user"
DB_NAME="database"
BACKUP_DIR="/path/to/backup"
DATE=$(date +"%Y%m%d%H%M%S")
# Création du répertoire de sauvegarde
mkdir -p $BACKUP_DIR
# Sauvegarde de la base de données
pg_dump -U $DB_USER $DB_NAME > $BACKUP_DIR/$DB_NAME-$DATE.sql

View File

@ -0,0 +1,5 @@
# Ouvrir le fichier de tâches cron
crontab -e
# Ajouter la tâche de sauvegarde, toutes les nuits à minuit
0 * * * * /path/to/backup.sh

View File

@ -0,0 +1,9 @@
<div
className="iframe-container"
data-src="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
data-width="1280"
data-height="720"
>
<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" loading="lazy" />
<button type="button" className="iframe-loader">Charger la vidéo</button>
</div>

View File

@ -0,0 +1,7 @@
<img
src="clairiere.jpg"
srcset="clairiere-480w.webp 480w, clairiere-800w.webp 800w"
sizes="(max-width: 480px) 100vw, (max-width: 800px) 50vw, 800px"
alt="Une clairière verdoyante"
loading="lazy"
/>

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