Compare commits
No commits in common. "64b36c816fc3da55b1219d15752e5cda43baad8d" and "77fb4348bad5b1c8e55bfdbdf55b32b4bb4552d7" have entirely different histories.
64b36c816f
...
77fb4348ba
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
**/node_modules/
|
||||||
|
**/.pnpm-store/
|
||||||
|
**/dist/
|
||||||
|
.git
|
||||||
@ -1,2 +1,3 @@
|
|||||||
PORT=5500
|
PORT=5500
|
||||||
HMR_PORT=5501
|
HMR_PORT=5501
|
||||||
|
NODE_ENV=development
|
||||||
@ -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."
|
||||||
|
|||||||
10
.gitignore
vendored
10
.gitignore
vendored
@ -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*
|
||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"biome.searchInPath": false,
|
|
||||||
"biome.lspBin": "app/node_modules/@biomejs/biome/bin/biome",
|
|
||||||
}
|
|
||||||
15
Dockerfile
15
Dockerfile
@ -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" ]
|
|
||||||
49
README.md
49
README.md
@ -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
9
app/.env
Executable file → Normal 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
149
app/.gitignore
vendored
@ -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
1
app/.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
data/**/*.md
|
||||||
0
app/.prettierrc
Executable file → Normal file
0
app/.prettierrc
Executable file → Normal file
22
app/README.md
Executable file → Normal file
22
app/README.md
Executable file → Normal file
@ -1,19 +1,23 @@
|
|||||||
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
|
||||||
|
|
||||||
* [`/pages/+config.ts`](#pagesconfigts)
|
* [React](#react)
|
||||||
* [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`](#pagesconfigts)
|
||||||
|
* [Routing](#routing)
|
||||||
|
* [`/pages/_error/+Page.jsx`](#pages_errorpagejsx)
|
||||||
|
* [`/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`](#pagesonpagetransitionstartts-and-pagesonpagetransitionendts)
|
||||||
|
* [SSR](#ssr)
|
||||||
|
* [HTML Streaming](#html-streaming)
|
||||||
|
|
||||||
|
## React
|
||||||
|
|
||||||
|
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
|
||||||
|
|
||||||
### `/pages/+config.ts`
|
### `/pages/+config.ts`
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
1334
app/bun.lock
1334
app/bun.lock
File diff suppressed because it is too large
Load Diff
@ -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} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
@ -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 “
|
|
||||||
<span class="break-words text-slate-900">{query()}</span>
|
|
||||||
”
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
25
app/components/common/Cookies.telefunc.ts
Normal file
25
app/components/common/Cookies.telefunc.ts
Normal 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" };
|
||||||
|
}
|
||||||
192
app/components/common/Cookies.tsx
Normal file
192
app/components/common/Cookies.tsx
Normal 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‘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'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‘embête pas longtemps, on te laisse même le choix <em>(si ça c‘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‘ai faim !
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
app/components/common/Iframe.tsx
Normal file
20
app/components/common/Iframe.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/components/common/Image.tsx
Normal file
5
app/components/common/Image.tsx
Normal 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" />;
|
||||||
|
}
|
||||||
26
app/components/common/Link.tsx
Normal file
26
app/components/common/Link.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
app/components/common/Toggle.tsx
Normal file
45
app/components/common/Toggle.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
app/components/md/Tabs.tsx
Normal file
95
app/components/md/Tabs.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
import 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
app/components/syntax/Button.tsx
Normal file
33
app/components/syntax/Button.tsx
Normal 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} />
|
||||||
|
);
|
||||||
|
}
|
||||||
82
app/components/syntax/CSRSnippet.tsx
Normal file
82
app/components/syntax/CSRSnippet.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
app/components/syntax/Callout.tsx
Normal file
55
app/components/syntax/Callout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
app/components/syntax/DocsHeader.tsx
Normal file
33
app/components/syntax/DocsHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/components/syntax/DocsLayout.tsx
Normal file
36
app/components/syntax/DocsLayout.tsx
Normal 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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
app/components/syntax/Fence.tsx
Normal file
20
app/components/syntax/Fence.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
app/components/syntax/Hero.tsx
Normal file
132
app/components/syntax/Hero.tsx
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { HeroBackground } from "@syntax/HeroBackground";
|
||||||
|
import blurIndigoImage from "@/images/blur-indigo.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
app/components/syntax/HeroBackground.tsx
Normal file
121
app/components/syntax/HeroBackground.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
app/components/syntax/Icon.tsx
Normal file
74
app/components/syntax/Icon.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
37
app/components/syntax/Logo.tsx
Normal file
37
app/components/syntax/Logo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
85
app/components/syntax/MobileNavigation.tsx
Normal file
85
app/components/syntax/MobileNavigation.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
205
app/components/syntax/Navigation.tsx
Normal file
205
app/components/syntax/Navigation.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
app/components/syntax/PrevNextLinks.tsx
Normal file
89
app/components/syntax/PrevNextLinks.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
app/components/syntax/Prose.tsx
Normal file
34
app/components/syntax/Prose.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
app/components/syntax/QuickLinks.tsx
Normal file
35
app/components/syntax/QuickLinks.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
app/components/syntax/SSRSnippet.tsx
Normal file
58
app/components/syntax/SSRSnippet.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
app/components/syntax/Search.telefunc.ts
Normal file
13
app/components/syntax/Search.telefunc.ts
Normal 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;
|
||||||
|
};
|
||||||
278
app/components/syntax/Search.tsx
Normal file
278
app/components/syntax/Search.tsx
Normal 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 “
|
||||||
|
<span className="break-words text-slate-900 dark:text-white">{query}</span>
|
||||||
|
”
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/components/syntax/Snippet.tsx
Normal file
48
app/components/syntax/Snippet.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
app/components/syntax/TableOfContents.tsx
Normal file
105
app/components/syntax/TableOfContents.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
app/components/syntax/ThemeSelector.tsx
Normal file
86
app/components/syntax/ThemeSelector.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
app/components/syntax/icons/InstallationIcon.tsx
Normal file
40
app/components/syntax/icons/InstallationIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
app/components/syntax/icons/LightbulbIcon.tsx
Normal file
39
app/components/syntax/icons/LightbulbIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/components/syntax/icons/PluginsIcon.tsx
Normal file
48
app/components/syntax/icons/PluginsIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
app/components/syntax/icons/PresetsIcon.tsx
Normal file
36
app/components/syntax/icons/PresetsIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/components/syntax/icons/QuestionIcon.tsx
Normal file
48
app/components/syntax/icons/QuestionIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
app/components/syntax/icons/ThemingIcon.tsx
Normal file
52
app/components/syntax/icons/ThemingIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/components/syntax/icons/WarningIcon.tsx
Normal file
48
app/components/syntax/icons/WarningIcon.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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,
|
|
||||||
};
|
|
||||||
13
app/contexts/ThemeContext.ts
Normal file
13
app/contexts/ThemeContext.ts
Normal 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: () => {},
|
||||||
|
});
|
||||||
@ -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.
|
|
||||||
|
|
||||||
Toutefois, il est préférable que tu saches expliquer comment installer et configurer les éléments nécessaires de manières individuelles.
|
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.
|
||||||
</Callout>
|
|
||||||
|
Toutefois, il est préférable que tu saches expliquer comment installer et configurer les éléments nécessaires de manières individuelles.
|
||||||
|
|
||||||
|
{% /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.
|
|
||||||
|
|
||||||
É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.
|
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.
|
||||||
</Callout>
|
|
||||||
|
É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 %}
|
||||||
|
|
||||||
|
## 🛠️ 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
|
||||||
@ -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,16 +26,18 @@ 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 !
|
|
||||||
|
|
||||||
Tu n'as pas besoin d'être un webdesigner pour réaliser des maquettes.
|
Pas de panique !
|
||||||
Évidemment, si tu as des compétences en design, c'est un plus car en général les développeurs ne sont pas tous réputés pour être de bons designers... 😅
|
|
||||||
|
|
||||||
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é**.
|
Tu n'as pas besoin d'être un webdesigner pour réaliser des maquettes.
|
||||||
|
Évidemment, si tu as des compétences en design, c'est un plus car en général les développeurs ne sont pas tous réputés pour être de bons designers... 😅
|
||||||
|
|
||||||
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.
|
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é**.
|
||||||
</Callout>
|
|
||||||
|
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 %}
|
||||||
|
|
||||||
## ➕ 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 😘.
|
||||||
@ -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.
|
||||||
@ -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,21 +22,21 @@ 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)_.
|
||||||
|
|
||||||
Si tu as utilisé un framework, tu peux tout à fait montrer des extraits de code en JavaScript pur pour montrer que tu sais comment ça fonctionne "sous le capot" !
|
Si tu as utilisé un framework, tu peux tout à fait montrer des extraits de code en JavaScript pur pour montrer que tu sais comment ça fonctionne "sous le capot" !
|
||||||
|
|
||||||
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,41 +58,59 @@ 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.
|
||||||
|
|
||||||
jQuery a été très utilisée pour les animations, les requêtes AJAX, la manipulation du DOM, etc.
|
jQuery a été très utilisée pour les animations, les requêtes AJAX, la manipulation du DOM, etc.
|
||||||
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 ! 😎
|
|
||||||
|
|
||||||
Si je te parle des requêtes XHR _(XMLHttpRequest)_ tu me dis... ?
|
Tout doux, tout doux, il existe une solution ! 😎
|
||||||
|
|
||||||
> Mais c'est vieux ça, c'est pas du tout à la mode !
|
Si je te parle des requêtes XHR _(XMLHttpRequest)_ tu me dis... ?
|
||||||
|
|
||||||
Et tu as raison, mais si maintenant je te dis qu'il y a une autre solution, native, plus moderne et plus performante, tu me dis... ?
|
> Mais c'est vieux ça, c'est pas du tout à la mode !
|
||||||
|
|
||||||
> Fetch !
|
Et tu as raison, mais si maintenant je te dis qu'il y a une autre solution, native, plus moderne et plus performante, tu me dis... ?
|
||||||
|
|
||||||
**Et sinon, pour faire simple :**
|
> Fetch !
|
||||||
|
|
||||||
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.
|
**Et sinon, pour faire 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 />
|
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.
|
||||||
|
|
||||||
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 ? 😉
|
{% tabs defaultSelectedTab="xhr" %}
|
||||||
</Callout>
|
{% 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 ? 😉
|
||||||
|
|
||||||
|
{% /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" %}
|
||||||
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.
|
{% tab value="html" label="HTML - 1ère étape" %}
|
||||||
|
{% snippet path="html/defer-iframe.html" language="html" showLineNumbers=true /%}
|
||||||
|
{% /tab %}
|
||||||
|
|
||||||
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. 😱
|
{% tab value="js" label="JavaScript - 2ème étape" %}
|
||||||
|
{% snippet path="js/defer-iframe.ts" language="ts" showLineNumbers=true /%}
|
||||||
|
{% /tab %}
|
||||||
|
{% /tabs %}
|
||||||
|
|
||||||
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 !
|
{% /callout %}
|
||||||
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 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.
|
||||||
|
|
||||||
|
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 !
|
||||||
|
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 %}
|
||||||
|
|
||||||
## 🧠 Documentations
|
## 🧠 Documentations
|
||||||
|
|
||||||
23
app/data/certifications/dwwm/at1/page.md
Normal file
23
app/data/certifications/dwwm/at1/page.md
Normal 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
|
||||||
@ -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,34 +97,38 @@ 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.
|
||||||
Mais si tu fais un MCD il faut qu'il respecte la méthode Merise, **sinon ce n'est pas un MCD**.
|
Mais si tu fais un MCD il faut qu'il respecte la méthode Merise, **sinon ce n'est pas un MCD**.
|
||||||
|
|
||||||
Ton jury peut être très pointilleux là-dessus, donc fais attention à bien respecter les noms des documents, leur contenu et leur structure.
|
Ton jury peut être très pointilleux là-dessus, donc fais attention à bien respecter les noms des documents, leur contenu et leur structure.
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -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,15 +48,15 @@ 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 :
|
||||||
|
|
||||||
> 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,35 +82,35 @@ 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.
|
||||||
|
|
||||||
Si tu as déjà eu l'occasion d'envoyer des "messages codés", c'est que tu as déjà utilisé le chiffrement sans pour autant le savoir !
|
Si tu as déjà eu l'occasion d'envoyer des "messages codés", c'est que tu as déjà utilisé le chiffrement sans pour autant le savoir !
|
||||||
L'un des chiffrements les plus connus est le **chiffre de César**, qui consiste à décaler les lettres de l'alphabet d'un certain nombre de positions.
|
L'un des chiffrements les plus connus est le **chiffre de César**, qui consiste à décaler les lettres de l'alphabet d'un certain nombre de positions.
|
||||||
|
|
||||||
Par exemple :
|
Par exemple :
|
||||||
|
|
||||||
> Message : "Bonjour"
|
> Message : "Bonjour"
|
||||||
> Décalage : 3
|
> Décalage : 3
|
||||||
>
|
>
|
||||||
> 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
|
||||||
|
|
||||||
@ -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,60 +25,60 @@ 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** !
|
||||||
|
|
||||||
React est une bibliothèque _(pas une "librarie" et encore moins un framework ⚠️)_ JavaScript qui permet de créer des interfaces utilisateur, mais elle n'est pas liée de manière directe à un serveur.
|
React est une bibliothèque _(pas une "librarie" et encore moins un framework ⚠️)_ JavaScript qui permet de créer des interfaces utilisateur, mais elle n'est pas liée de manière directe à un serveur.
|
||||||
Certes, on va consommer une API pour récupérer des données, mais React n'est que le réceptacle de ces données côté client _(navigateur)_.
|
Certes, on va consommer une API pour récupérer des données, mais React n'est que le réceptacle de ces données côté client _(navigateur)_.
|
||||||
|
|
||||||
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** :
|
||||||
Si je te parle de client HTTP, tu me réponds... ?
|
- Adresse e-mail : `mauvaise-adresse@email`
|
||||||
[Bruno](https://www.usebruno.com/) ? [Postman](https://www.postman.com/) ? [Insomnia](https://insomnia.rest/) ?
|
- 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`
|
||||||
|
|
||||||
Bingo ! 🎉
|
{% /tab %}
|
||||||
|
|
||||||
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.
|
{% tab value="valid" label="Données valides" %}
|
||||||
</Callout>
|
|
||||||
|
- **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... ?
|
||||||
|
[Bruno](https://www.usebruno.com/) ? [Postman](https://www.postman.com/) ? [Insomnia](https://insomnia.rest/) ?
|
||||||
|
|
||||||
|
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.
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
### 🧪 Les tests unitaires
|
### 🧪 Les tests unitaires
|
||||||
|
|
||||||
@ -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,20 +54,22 @@ 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.
|
|
||||||
|
|
||||||
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)_.
|
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.
|
||||||
Comme on dit souvent :
|
|
||||||
|
|
||||||
> Le problème se situe souvent entre la chaise et le clavier.
|
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)_.
|
||||||
|
Comme on dit souvent :
|
||||||
|
|
||||||
Si tu recherches un hébergeur qui se veut éco-responsable _(bien plus que la moyenne)_ : Infomaniak est un excellent choix.
|
> Le problème se situe souvent entre la chaise et le clavier.
|
||||||
_(Non, je ne détiens aucune part chez eux, mais j'apprécie leur démarche et leur qualité de service donc un peu de pub gratuite ne fait pas de mal !)_
|
|
||||||
|
|
||||||
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).
|
Si tu recherches un hébergeur qui se veut éco-responsable _(bien plus que la moyenne)_ : Infomaniak est un excellent choix.
|
||||||
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.
|
_(Non, je ne détiens aucune part chez eux, mais j'apprécie leur démarche et leur qualité de service donc un peu de pub gratuite ne fait pas de mal !)_
|
||||||
</Callout>
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
{% /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 />
|
|
||||||
|
|
||||||
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.
|
```nginx
|
||||||
</Callout>
|
# 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.
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
### 📦 Caddy
|
### 📦 Caddy
|
||||||
|
|
||||||
@ -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>
|
|
||||||
59
app/data/certifications/dwwm/page.md
Normal file
59
app/data/certifications/dwwm/page.md
Normal 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)_
|
||||||
36
app/data/certifications/page.md
Normal file
36
app/data/certifications/page.md
Normal 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 %}
|
||||||
@ -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.
|
|
||||||
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.
|
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.
|
||||||
</Callout>
|
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 %}
|
||||||
|
|
||||||
## 🥷 Disparaître de cette page
|
## 🥷 Disparaître de cette page
|
||||||
|
|
||||||
@ -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).
|
|
||||||
</Callout>
|
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 %}
|
||||||
@ -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,24 +70,37 @@ 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)_.
|
|
||||||
|
|
||||||
Au niveau des contraintes, on a majoritairement _(sauf pour le tarif d'un concert)_ mis des contraintes d'obligation sur les 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)_.
|
||||||
On a aussi mis une contrainte d'unicité sur l'adresse e-mail, car il ne peut pas y avoir deux membres avec la même adresse e-mail.
|
|
||||||
|
|
||||||
Dans certains cas, on a mis des contraintes de longueur sur les données. On a fait ça pour éviter de stocker des données trop longues dans la base de 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.
|
||||||
Bien entendu, une date ne peut pas avoir de longueur, on a donc mis un tiret pour indiquer que ce n'est pas applicable.
|
On a aussi mis une contrainte d'unicité sur l'adresse e-mail, car il ne peut pas y avoir deux membres avec la même adresse e-mail.
|
||||||
|
|
||||||
Pour le mot de passe, on a mis une contrainte de longueur supérieure à 12 caractères.
|
Dans certains cas, on a mis des contraintes de longueur sur les données. On a fait ça pour éviter de stocker des données trop longues dans la base de données.
|
||||||
É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.
|
Bien entendu, une date ne peut pas avoir de longueur, on a donc mis un tiret pour indiquer que ce n'est pas applicable.
|
||||||
</Callout>
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
@ -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 :
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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,20 +96,20 @@ 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**
|
||||||
- **1**
|
- **1**
|
||||||
- **N**
|
- **N**
|
||||||
|
|
||||||
**N** signifie "N'importe quel nombre" _(0, 1, 2, 3, ...)_.
|
**N** signifie "N'importe quel nombre" _(0, 1, 2, 3, ...)_.
|
||||||
Mais dès que l'on connait le nombre exact, on peut le mettre à la place de **N**.
|
Mais dès que l'on connait le nombre exact, on peut le mettre à la place de **N**.
|
||||||
|
|
||||||
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 :
|
||||||

|

|
||||||
|
|
||||||
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 :
|
||||||

|

|
||||||
|
|
||||||
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**)_ :
|
||||||

|

|
||||||
|
|
||||||
<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 :
|
||||||

|

|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
@ -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.
|
|
||||||
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.
|
Ce n'est pas parce qu'une méthodologie est ancienne qu'elle est obsolète.
|
||||||
</Callout>
|
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.
|
||||||
|
|
||||||
|
{% /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 !**
|
|
||||||
|
|
||||||
Beaucoup de ressources sur internet parlent de Merise, mais elles ne sont pas forcément justes.
|
**Attention !**
|
||||||
|
|
||||||
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.
|
Beaucoup de ressources sur internet parlent de Merise, mais elles ne sont pas forcément justes.
|
||||||
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>
|
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**.
|
||||||
|
|
||||||
|
{% /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
45
app/data/docs/page.md
Normal 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 ! 😄
|
||||||
@ -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.
|
|
||||||
</Callout>
|
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 %}
|
||||||
|
|
||||||
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
|
|
||||||
- **B** - 2, 4, 1, 3
|
- **A** - 4, 2, 1, 3
|
||||||
- **C** - 1, 2, 3, 4
|
- **B** - 2, 4, 1, 3
|
||||||
- **D** - La réponse D
|
- **C** - 1, 2, 3, 4
|
||||||
</Callout>
|
- **D** - La réponse D
|
||||||
|
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
## 🧩 Les autres hooks
|
## 🧩 Les autres hooks
|
||||||
|
|
||||||
@ -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" %}
|
||||||
|
|
||||||
<Callout type="warning" title="Installation des dépendances">
|
{% tab value="npm" label="NPM" %}
|
||||||
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>
|
```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" %}
|
||||||
|
|
||||||
|
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 %}
|
||||||
|
|
||||||
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)_.
|
||||||
317
app/data/docs/react/jsx/page.md
Normal file
317
app/data/docs/react/jsx/page.md
Normal 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 !
|
||||||
@ -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.
|
|
||||||
- **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.
|
- **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.
|
||||||
- **Performances** : React utilise un DOM virtuel _(Virtual DOM)_ pour améliorer les performances de l'application.
|
- **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.
|
||||||
- **Communauté active** : React a une communauté active de développeurs qui contribuent à son développement et partagent des ressources utiles.
|
- **Performances** : React utilise un DOM virtuel _(Virtual DOM)_ pour améliorer les performances de l'application.
|
||||||
</Callout>
|
- **Communauté active** : React a une communauté active de développeurs qui contribuent à son développement et partagent des ressources utiles.
|
||||||
|
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
Mais on peut aussi y noter des points faibles bien entendu, car tout n'est pas rose :
|
Mais on peut aussi y noter des points faibles bien entendu, car tout n'est pas rose :
|
||||||
|
|
||||||
@ -38,20 +37,22 @@ 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.
|
|
||||||
|
|
||||||
Si on se concentre sur la **préoccupation principale** de React, c'est de gérer l'**interface utilisateur** _(UI)_ de ton application.
|
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.
|
||||||
En aucun cas, React _(tel quel et "pour le moment")_ va te dire comment gérer ton état global, comment gérer tes requêtes HTTP, etc.
|
|
||||||
|
|
||||||
Mais tu peux totalement utiliser React **au sein** d'un framework !
|
Si on se concentre sur la **préoccupation principale** de React, c'est de gérer l'**interface utilisateur** _(UI)_ de ton application.
|
||||||
Tu as notamment des frameworks comme [**Next.js**](https://nextjs.org/) ou [**Gatsby**](https://www.gatsbyjs.com/) qui utilisent React
|
En aucun cas, React _(tel quel et "pour le moment")_ va te dire comment gérer ton état global, comment gérer tes requêtes HTTP, etc.
|
||||||
avec des fonctionnalités supplémentaires pour gérer le routage, le rendu côté serveur, etc.
|
|
||||||
|
|
||||||
_(Le meilleur, selon moi, c'est [**Vike**](https://vike.dev/) qui te permet d'utiliser presque n'importe quelle bibliothèque avec une même architecture 😏)_
|
Mais tu peux totalement utiliser React **au sein** d'un framework !
|
||||||
|
Tu as notamment des frameworks comme [**Next.js**](https://nextjs.org/) ou [**Gatsby**](https://www.gatsbyjs.com/) qui utilisent React
|
||||||
|
avec des fonctionnalités supplémentaires pour gérer le routage, le rendu côté serveur, etc.
|
||||||
|
|
||||||
Mais concentrons-nous sur React en tant que bibliothèque, et non en tant que framework 😉
|
_(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 😏)_
|
||||||
</Callout>
|
|
||||||
|
Mais concentrons-nous sur React en tant que bibliothèque, et non en tant que framework 😉
|
||||||
|
|
||||||
|
{% /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 :
|
||||||
|
|
||||||
@ -4,22 +4,21 @@ 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.
|
|
||||||
Enfin si, il peut en contenir _(mais rien de foufou)_, son rôle est de simplement afficher des données.
|
|
||||||
|
|
||||||
En gros :
|
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.
|
||||||
|
|
||||||
- Il ne fait que de l'affichage
|
En gros :
|
||||||
- Il ne fait pas de calculs
|
|
||||||
- Il ne fait pas de requêtes HTTP
|
- Il ne fait que de l'affichage
|
||||||
</Callout>
|
- Il ne fait pas de calculs
|
||||||
|
- Il ne fait pas de requêtes HTTP
|
||||||
|
|
||||||
|
{% /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.
|
|
||||||
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 !
|
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.
|
||||||
</Callout>
|
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 !
|
||||||
|
|
||||||
|
{% /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".
|
|
||||||
|
|
||||||
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 😉
|
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".
|
||||||
</Callout>
|
|
||||||
|
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 %}
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@ -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 !
|
|
||||||
|
|
||||||
Un hook en React, est une fonction qui permet d'exploiter les fonctionnalités de React dans un composant fonctionnel _(fonction)_.
|
Haha, non !
|
||||||
|
|
||||||
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 !
|
Un hook en React, est une fonction qui permet d'exploiter les fonctionnalités de React dans un composant fonctionnel _(fonction)_.
|
||||||
</Callout>
|
|
||||||
|
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 %}
|
||||||
|
|
||||||
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,26 +74,42 @@ 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 :
|
|
||||||
|
|
||||||
- `count` : la valeur du state
|
Bien vu ! Effectivement on va avoir deux assignements pour déclarer un state :
|
||||||
- `setCount` : la fonction qui permet de modifier la valeur du state
|
|
||||||
|
|
||||||
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 !
|
- `count` : la valeur du state
|
||||||
|
- `setCount` : la fonction qui permet de modifier la valeur du state
|
||||||
|
|
||||||
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.
|
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 !
|
||||||
</Callout>
|
|
||||||
|
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 %}
|
||||||
|
|
||||||
## 🔄 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)_.
|
||||||
|
|
||||||
537
app/data/docs/react/use-context/page.md
Normal file
537
app/data/docs/react/use-context/page.md
Normal 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**.
|
||||||
@ -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.
|
|
||||||
</Callout>
|
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 %}
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
En déversant le contenu de l'état actuel, on s'assure de ne pas perdre ces propriétés.
|
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.
|
||||||
|
|
||||||
Par exemple :
|
En déversant le contenu de l'état actuel, on s'assure de ne pas perdre ces propriétés.
|
||||||
|
|
||||||
<tabs.reactReducerWhySpreadOperator />
|
Par exemple :
|
||||||
|
|
||||||
On perdrait ici la propriété `message` si on ne la déversait pas dans le nouvel état.
|
{% snippet path="react/reducer/reducer-why-spread-operator.jsx" language="jsx" showLineNumbers=true /%}
|
||||||
</Callout>
|
|
||||||
|
On perdrait ici la propriété `message` si on ne la déversait pas dans le nouvel état.
|
||||||
|
|
||||||
|
{% /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" %}
|
||||||
|
|
||||||
<Callout type="question" title="C'est quoi `action.payload` ?">
|
{% tab value="js" label="JavaScript" %}
|
||||||
La propriété `payload` de l'action est optionnelle. Il s'agit d'une convention pour passer des données à l'action.
|
{% snippet path="react/reducer/reducer.js" language="js" showLineNumbers=true /%}
|
||||||
|
{% /tab %}
|
||||||
|
|
||||||
Le `!` après `action.payload` signifie que l'on est sûr que `payload` est défini.
|
{% tab value="ts" label="TypeScript" %}
|
||||||
Cela permet d'éviter une erreur de type avec TypeScript.
|
{% snippet path="react/reducer/reducer.ts" language="ts" showLineNumbers=true /%}
|
||||||
|
{% /tab %}
|
||||||
|
|
||||||
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.
|
{% /tabs %}
|
||||||
</Callout>
|
|
||||||
|
{% 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 %}
|
||||||
|
|
||||||
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 ?"
|
|
||||||
|
|
||||||
Bien vu ! Et pour TypeScript, on peut aller encore plus loin en créant un `enum` pour les types d'actions 😉
|
Et là, tu te dis : "Pourquoi ne pas regrouper les exports dans un seul objet ?"
|
||||||
|
|
||||||
<tabs.reactActionsEnum />
|
Bien vu ! Et pour TypeScript, on peut aller encore plus loin en créant un `enum` pour les types d'actions 😉
|
||||||
</Callout>
|
|
||||||
|
{% tabs defaultSelectedTab="js" %}
|
||||||
|
|
||||||
|
{% 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
|
|
||||||
- **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 ?">
|
- **A** - Pour des états simples
|
||||||
- **A** - `(state, action) => { /* ... */ }`
|
- **B** - Pour des états complexes ou des états qui dépendent les uns des autres
|
||||||
- **B** - `(action, state) => { /* ... */ }`
|
|
||||||
- **C** - `(state) => { /* ... */ }`
|
|
||||||
- **D** - `(action) => { /* ... */ }`
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
<Callout type="question" title="Pourquoi déverser le contenu de l'état actuel dans le nouvel état ?">
|
{% /callout %}
|
||||||
- **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
|
|
||||||
</Callout>
|
|
||||||
|
|
||||||
<Callout type="question" title="Pourquoi utiliser des constantes pour les types d'actions ?">
|
{% callout type="question" title="Quelle est la signature d'un reducer ?" %}
|
||||||
- **A** - Pour rendre le code plus lisible
|
|
||||||
- **B** - Pour alourdir inutillement le code
|
- **A** - `(state, action) => { /* ... */ }`
|
||||||
- **C** - Pour éviter de se tromper dans le type de l'action
|
- **B** - `(action, state) => { /* ... */ }`
|
||||||
</Callout>
|
- **C** - `(state) => { /* ... */ }`
|
||||||
|
- **D** - `(action) => { /* ... */ }`
|
||||||
|
|
||||||
|
{% /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
|
||||||
|
- **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 ?" %}
|
||||||
|
|
||||||
|
- **A** - Pour rendre le code plus lisible
|
||||||
|
- **B** - Pour alourdir inutillement le code
|
||||||
|
- **C** - Pour éviter de se tromper dans le type de l'action
|
||||||
|
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
## Conclusion
|
## Conclusion
|
||||||
|
|
||||||
19
app/pages/index/+Page.mdx → app/data/page.md
Executable file → Normal file
19
app/pages/index/+Page.mdx → app/data/page.md
Executable file → Normal 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 ! 😊
|
||||||
13
app/data/snippets/bash/pg_cron_file.sh
Normal file
13
app/data/snippets/bash/pg_cron_file.sh
Normal 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
|
||||||
5
app/data/snippets/bash/pg_cron_register.sh
Normal file
5
app/data/snippets/bash/pg_cron_register.sh
Normal 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
|
||||||
9
app/data/snippets/html/defer-iframe.html
Normal file
9
app/data/snippets/html/defer-iframe.html
Normal 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>
|
||||||
7
app/data/snippets/html/lazy-loading.html
Normal file
7
app/data/snippets/html/lazy-loading.html
Normal 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
Loading…
Reference in New Issue
Block a user