chore: delete old project
This commit is contained in:
parent
aa2d2662ee
commit
ab6c8bad3f
@ -1,7 +0,0 @@
|
|||||||
# Environment variables declared in this file are automatically made available to Prisma.
|
|
||||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
|
||||||
|
|
||||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
|
||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
|
||||||
|
|
||||||
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
|
|
||||||
@ -1 +0,0 @@
|
|||||||
data/**/*.md
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 120
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
Generated with [vike.dev/new](https://vike.dev/new) ([version 410](https://www.npmjs.com/package/create-vike/v/0.0.410)) using this command:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
pnpm create vike@latest --react --tailwindcss --telefunc --fastify --eslint --prettier
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contents
|
|
||||||
|
|
||||||
* [React](#react)
|
|
||||||
|
|
||||||
* [`/pages/+config.ts`](#pagesconfigts)
|
|
||||||
* [Routing](#routing)
|
|
||||||
* [`/pages/_error/+Page.jsx`](#pages_errorpagejsx)
|
|
||||||
* [`/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`](#pagesonpagetransitionstartts-and-pagesonpagetransitionendts)
|
|
||||||
* [SSR](#ssr)
|
|
||||||
* [HTML Streaming](#html-streaming)
|
|
||||||
|
|
||||||
## React
|
|
||||||
|
|
||||||
This app is ready to start. It's powered by [Vike](https://vike.dev) and [React](https://react.dev/learn).
|
|
||||||
|
|
||||||
### `/pages/+config.ts`
|
|
||||||
|
|
||||||
Such `+` files are [the interface](https://vike.dev/config) between Vike and your code. It defines:
|
|
||||||
|
|
||||||
* A default [`<Layout>` component](https://vike.dev/Layout) (that wraps your [`<Page>` components](https://vike.dev/Page)).
|
|
||||||
* A default [`title`](https://vike.dev/title).
|
|
||||||
* Global [`<head>` tags](https://vike.dev/head-tags).
|
|
||||||
|
|
||||||
### Routing
|
|
||||||
|
|
||||||
[Vike's built-in router](https://vike.dev/routing) lets you choose between:
|
|
||||||
|
|
||||||
* [Filesystem Routing](https://vike.dev/filesystem-routing) (the URL of a page is determined based on where its `+Page.jsx` file is located on the filesystem)
|
|
||||||
* [Route Strings](https://vike.dev/route-string)
|
|
||||||
* [Route Functions](https://vike.dev/route-function)
|
|
||||||
|
|
||||||
### `/pages/_error/+Page.jsx`
|
|
||||||
|
|
||||||
The [error page](https://vike.dev/error-page) which is rendered when errors occur.
|
|
||||||
|
|
||||||
### `/pages/+onPageTransitionStart.ts` and `/pages/+onPageTransitionEnd.ts`
|
|
||||||
|
|
||||||
The [`onPageTransitionStart()` hook](https://vike.dev/onPageTransitionStart), together with [`onPageTransitionEnd()`](https://vike.dev/onPageTransitionEnd), enables you to implement page transition animations.
|
|
||||||
|
|
||||||
### SSR
|
|
||||||
|
|
||||||
SSR is enabled by default. You can [disable it](https://vike.dev/ssr) for all your pages or only for some pages.
|
|
||||||
|
|
||||||
### HTML Streaming
|
|
||||||
|
|
||||||
You can enable/disable [HTML streaming](https://vike.dev/stream) for all your pages, or only for some pages while still using it for others.
|
|
||||||
|
|
||||||
Binary file not shown.
@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 57 38" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(-1.76727,0,0,1.76727,49.1089,-3.53454)">
|
|
||||||
<path d="M16.161,18.989L20.49,23.32L21.9,21.91L2.1,2.1L0.69,3.51L2.714,5.535L-4.085,11.253L-4.085,13.054L3.185,19.167L4.629,17.337L-1.61,12.165L4.397,7.219L9.588,12.412L6,16L6.01,16.01L6,16.01L6,22L18,22L18,20.83L16.161,18.989ZM14.417,17.244L16,18.83L16,20L8,20L8,16.5L10.837,13.663L14.417,17.244ZM8,4L16,4L16,7.5L13.16,10.34L14.41,11.59L18,8.01L17.99,8L18,8L18,2L6,2L6,3.17L8,5.17L8,4ZM25.294,12.164L19.071,17.34L20.542,19.164L27.788,13.075L27.788,11.274L20.597,5.22L19.158,7.075L25.294,12.164Z" style="fill:url(#_Linear1);"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(12.792,-21.32,-21.32,-12.792,5.208,23.32)"><stop offset="0" style="stop-color:rgb(43,127,255);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(142,81,255);stop-opacity:1"/></linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,25 +0,0 @@
|
|||||||
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" };
|
|
||||||
}
|
|
||||||
@ -1,192 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
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" />;
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
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} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
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} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,121 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
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} />;
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
@ -1,278 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
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,13 +0,0 @@
|
|||||||
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: () => {},
|
|
||||||
});
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 1 - Installer et configurer son environnement de travail en fonction du projet web ou web mobile
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 1 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags: [DWWM, Environnement de développement]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 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é
|
|
||||||
|
|
||||||
Ce qui est attendu de ta part, c'est d'expliquer **comment** on peut installer et configurer les prérequis pour exécuter ton projet.
|
|
||||||
|
|
||||||
Tu as utilisé un framework PHP et React en front ?
|
|
||||||
Tu devras alors expliquer comment installer PHP, Composer, Node.js, npm _(ou autre gestionnaire de dépendances Node)_ et les autres dépendances nécessaires à ton projet comme la base de données !
|
|
||||||
|
|
||||||
Et pour te donner un ordre d'idée, voici ce que ça peut donner :
|
|
||||||
|
|
||||||
- Versionning _(Git, SVN, ...)_
|
|
||||||
- IDE ou éditeur de code _(Visual Studio Code, PhpStorm, ...)_
|
|
||||||
- Langages/runtimes _(PHP, Node.js, ...)_
|
|
||||||
- Gestionnaires de dépendances _(Composer, npm, ...)_
|
|
||||||
- Serveurs web _(Apache, Nginx, ...)_
|
|
||||||
- Base de données _(MySQL, PostgreSQL, ...)_
|
|
||||||
- DevOps _(Docker, Vagrant, ...)_
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
Tu l'as compris, c'est vaste !
|
|
||||||
Mais heureusement, tu dois uniquement expliquer comment installer et configurer les outils que tu as utilisés pour ton projet.
|
|
||||||
|
|
||||||
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." %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## ➕ Informations complémentaires
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🛠️ Ressources conseillées
|
|
||||||
|
|
||||||
_En cours de rédaction..._
|
|
||||||
|
|
||||||
## 🎯 Critères d'évaluation
|
|
||||||
|
|
||||||
- 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 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)
|
|
||||||
- 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
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 2 - Maquetter des interfaces utilisateur web ou web mobile
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 2 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags: [DWWM, Éco-conception, Accessibilité, SEO, Maquettage, UX, UI, Zoning, Wireframe, Prototypage, Design, Frontend]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Références
|
|
||||||
|
|
||||||
- REAC _(mise à jour du 02/07/2024)_, pages 17 et 18
|
|
||||||
- RE _(mise à jour du 02/07/2024)_, page 9
|
|
||||||
|
|
||||||
## 📋 En résumé
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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 :
|
|
||||||
|
|
||||||
1. **Analyse des besoins** : comprendre les besoins du client et de l'utilisateur final
|
|
||||||
2. **Création d'une arborescence** : définir la structure de l'application et les différentes pages
|
|
||||||
3. **Conception des zonings** : créer des schémas sans détails pour définir la disposition des éléments sur chaque page
|
|
||||||
4. **Réalisation des wireframes** : créer des maquettes plus détaillées avec des légendes et commentaires pour les interactions
|
|
||||||
5. **Récupération ou création de la charte graphique** : définir les couleurs, polices et styles à utiliser dans l'application
|
|
||||||
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
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
É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é**.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Il est probable que ton client t'impose une charte graphique à respecter et que cette dernière ne soit pas forcément accessible, ergonomique ou éco-conçue.
|
|
||||||
C'est à toi en tant que développeur d'expliquer à ton client les enjeux que ça représente pour son site web.
|
|
||||||
|
|
||||||
Il aura dans tous les cas le dernier mot, mais il est important de l'informer des conséquences de ses choix pour son site.
|
|
||||||
S'il ne veut pas améliorer prendre en compte ces informations, tu pourras toujours lui proposer de le faire évoluer par la suite, une fois le site en ligne _(mais attention, cela peut coûter cher pour rattraper les différents défauts)_.
|
|
||||||
|
|
||||||
### 🔍 Accessibilité
|
|
||||||
|
|
||||||
D'après toi, qu'est-ce que l'accessibilité en matière de développement web ?
|
|
||||||
|
|
||||||
Si tu as répondu que c'est le fait de rendre un site accessible à tous, tu as tout bon !
|
|
||||||
L'accessibilité est un point essentiel à prendre en compte lors de la réalisation de maquettes, car tout le monde doit pouvoir accéder à un site web, y compris les personnes en situation de handicap.
|
|
||||||
|
|
||||||
Pour améliorer l'accessibilité de ton site, il est recommandé de suivre les recommandations du W3C à propos du WCAG et du RGAA, mais je ne te cache pas c'est un gros morceau et qu'on se perdre en voulant tout respecter à la lettre. Alors voici un condensé des points les plus importants à respecter dans un premier temps :
|
|
||||||
|
|
||||||
- **Contraste** : Assure-toi que le texte est lisible sur n'importe quel fond. Il faut viser un score **AA** _(ratio 4.5:1)_ au minimum et idéalement **AAA** _(ratio 7:1)_ sur nos éléments textuels et pour l'iconographie.
|
|
||||||
- **Taille de police** : La taille de police doit être suffisamment grande pour être lisible par tout le monde. En général, on recommande une taille de 16px minimum.
|
|
||||||
- **Choix de la police** : Utilise des polices lisibles et évite les polices fantaisistes. Si tu cherches une police légère, gratuite et réputée pour être lisible, tu peux te tourner vers la police Luciole !
|
|
||||||
- **Intitulés des liens et boutons** : Les liens et boutons doivent être clairs et explicites. Évite les "Cliquez ici" et préfère des phrases plus descriptives. Évite également d'uniquer mettre un pictogramme pour un lien, car cela peut ne pas être compris par tout le monde.
|
|
||||||
|
|
||||||
Bien entendu, l'accessibilité ne se limite pas à ces points, mais c'est un bon début pour rendre ton site accessible à tous. J'y reviendrai plus en détail dans la compétence suivante, puisqu'on parlera de l'intégration de ces maquettes 😉
|
|
||||||
|
|
||||||
Et pour terminer ! _(oui, je sais, je parle beaucoup 😅)_
|
|
||||||
L'**éco-conception** !
|
|
||||||
|
|
||||||
### 🌍 L'éco-conception
|
|
||||||
|
|
||||||
L'éco-conception, c'est l'art de concevoir un produit ou un service en prenant en compte son impact environnemental tout au long de son cycle de vie.
|
|
||||||
Dans le cas d'un site web, cela signifie qu'on va réfléchir à la manière de réduire l'impact environnemental de notre site tout en respectant les besoins du client et de l'utilisateur final.
|
|
||||||
|
|
||||||
On évitera par exemple d'intégrer beaucoup d'images et autres médias.
|
|
||||||
De manière générale : moins il y a de requêtes, moins il y a de données à charger, et donc moins d'énergie consommée.
|
|
||||||
|
|
||||||
Même si dans l'ensemble on serait en droit de crier "greenwashing" _(et on aurait pas tort)_, il est important de prendre en compte l'impact environnemental de nos sites web.
|
|
||||||
Au delà de l'impact environnemental, un site éco-conçu est souvent plus rapide et plus léger, ce qui permet aux sites et applications d'être plus performants tant en termes de vitesse que de consommation de données.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- 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)_
|
|
||||||
- La charte graphique de l'entreprise est respectée
|
|
||||||
- Les exigences de sécurisation sont respectées
|
|
||||||
- Les maquettes tiennent compte de l'expérience utilisateur, y compris pour les personnes en situation de handicap et du type d'utilisation, y compris pour les équipements mobiles
|
|
||||||
- L'enchainement des maquettes est formalisé par un schéma
|
|
||||||
- La législation en vigueur est respectée, y compris celle relative à l'accessibilité
|
|
||||||
|
|
||||||
## 🤯 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 !
|
|
||||||
|
|
||||||
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 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.
|
|
||||||
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 😘.
|
|
||||||
|
|
||||||
Faire une galerie photo avec des images en pleine résolution, c'est bien, mais c'est lourd à charger. Et pourtant, on peut quand même rendre le site d'un photographe beau et éco-conçu, tout en préservant la qualité de son travail !
|
|
||||||
Ça peut sembler contre-intuitif, mais tout va se jouer dans le code où l'on pourra charger **uniquement au besoin** et à la **demande de l'utilisateur** les images dans notre page.
|
|
||||||
@ -1,211 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 3 - Réaliser des interfaces utilisateur statiques web ou web mobile
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 3 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags:
|
|
||||||
[
|
|
||||||
DWWM,
|
|
||||||
Intégration,
|
|
||||||
Responsive,
|
|
||||||
HTML,
|
|
||||||
CSS,
|
|
||||||
Accessibilité,
|
|
||||||
Éco-conception,
|
|
||||||
UX,
|
|
||||||
UI,
|
|
||||||
SEO,
|
|
||||||
Déploiement,
|
|
||||||
Reverse Proxy,
|
|
||||||
Frontend,
|
|
||||||
Serveur Web,
|
|
||||||
]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Références
|
|
||||||
|
|
||||||
- REAC _(mise à jour du 02/07/2024)_, pages 19 et 20
|
|
||||||
- RE _(mise à jour du 02/07/2024)_, page 10
|
|
||||||
|
|
||||||
## 📋 En résumé
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
{% /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 !)_
|
|
||||||
|
|
||||||
On va rentrer directement dans le vif du sujet, avec pour commencer... les technologies à utiliser !
|
|
||||||
Puisqu'on ne parle pas de dynamique mais de statique, tu vas devoir te tourner vers des technologies front-end, comme le **HTML** et le **CSS**. Le JavaScript sera également de la partie, mais qu'à partir de la CP 4 😉
|
|
||||||
|
|
||||||
Dans un premier temps, tu vas devoir parler du squelette de ton site, c'est-à-dire de la structure HTML. On retrouvera par ailleurs des notions essentielles comme :
|
|
||||||
|
|
||||||
- Les balises sémantiques _(pour une meilleure accessibilité et un meilleur référencement)_
|
|
||||||
- Les médias _(images, vidéos, sons, etc.)_
|
|
||||||
- Les liens _(pour naviguer d'une page à une autre)_
|
|
||||||
|
|
||||||
Ensuite, tu vas devoir t'occuper de la mise en forme de ton site, c'est-à-dire du CSS.
|
|
||||||
N'oublie pas : le site doit correspondre aux maquettes que tu as réalisées, et donc respecter la charte graphique.
|
|
||||||
|
|
||||||
Ensuite, tu vas devoir t'assurer que ton site est bien accessible à tous, rendre tes pages web responsives _(c'est-à-dire qu'elles s'adaptent à tous les types d'écrans)_ et enfin, publier ton site de manière sécurisée.
|
|
||||||
|
|
||||||
La partie éco-conception sera également à prendre en compte, en veillant à ne pas surcharger ton site en médias inutiles ou trop lourds.
|
|
||||||
|
|
||||||
## ➕ Informations complémentaires
|
|
||||||
|
|
||||||
### 🔍 Accessibilité
|
|
||||||
|
|
||||||
Dans la compétence précédente, je t'ai parlé de l'accessibilité et de l'importance de rendre un site accessible à tous. On a notamment vu qu'il est important de maîtriser le contraste, la taille de police, le choix de la police et les intitulés des liens et boutons.
|
|
||||||
|
|
||||||
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-*`.
|
|
||||||
|
|
||||||
{% quick-link
|
|
||||||
title="Attributs ARIA (Accessible Rich Internet Applications)"
|
|
||||||
href="https://developer.mozilla.org/fr/docs/Web/Accessibility/ARIA"
|
|
||||||
icon="presets"
|
|
||||||
description="> MDN Web Docs - Attributs ARIA"
|
|
||||||
/%}
|
|
||||||
|
|
||||||
### 🌐 Le référencement naturel
|
|
||||||
|
|
||||||
J'entends déjà l'un de mes collègues juré s'impatienter : "Et le référencement, on en parle ?"
|
|
||||||
La réponse : bien sûr que ~~non~~ **oui** !
|
|
||||||
|
|
||||||
Le référencement naturel, ou SEO _(pour Search Engine Optimization)_, est un ensemble de techniques visant à améliorer la visibilité d'un site web dans les moteurs de recherche.
|
|
||||||
On parlera dans un premier temps de l'optimisation du contenu, avec des balises `<title>`, `<meta>`, `<h1>`, `<h2>`, etc., mais aussi de l'optimisation technique, avec la vitesse de chargement, la compatibilité mobile, les balises sémantiques, etc.
|
|
||||||
|
|
||||||
On retrouvera également des notions de netlinking _(liens entrants et sortants)_, de maillage interne, de balises `<a>` et `<img>`, de sitemap, de `robots.txt`, etc.
|
|
||||||
|
|
||||||
Ça fait beaucoup, non ? 😅
|
|
||||||
Disons que c'est un vaste sujet et qui, même s'il ne fait pas toujours rêver, est indispensable pour que ton site soit visible sur les moteurs de recherche.
|
|
||||||
|
|
||||||
Difficile de dire au client "Désolé, votre site n'apparaît pas sur Google et je m'en fiche" !
|
|
||||||
|
|
||||||
### 🌳 L'éco-conception
|
|
||||||
|
|
||||||
On sait déjà ce que l'éco-conception.
|
|
||||||
Puisque l'une des principales préoccupations de l'éco-conception est de réduire son impact environnemental, on va éviter d'intégrer beaucoup d'images et autres médias.
|
|
||||||
Mais est-ce que ça veut pour autant dire que tu dois te contenter de sites tout moches et tout gris ? Non !
|
|
||||||
|
|
||||||
En HTML, au delà de l'import des médias, on ne va pas avoir beaucoup de contrôle sur la consommation énergétique.
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
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.
|
|
||||||
|
|
||||||
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 %}
|
|
||||||
|
|
||||||
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**.
|
|
||||||
|
|
||||||
{% callout type="question" title="Et le format SVG ? Ce n'est pas 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.
|
|
||||||
|
|
||||||
Il est particulièrement adapté pour les icônes, logos et autres éléments graphiques simples.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
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)_
|
|
||||||
|
|
||||||
On va évidemment utiliser une balise `<img>`, mais on va également utiliser des techniques de chargement différé, comme le lazy loading, qui permet de charger les images uniquement lorsqu'elles sont visibles à l'écran.
|
|
||||||
|
|
||||||
En tant que juré, j'ai souvent vu des projets qui exploitent un script JS pour faire du lazy loading, mais il existe un attribut HTML qui permet de faire ça très simplement : `loading="lazy"`.
|
|
||||||
De cette manière, nos images ne seront chargées que si elles sont visibles à l'écran, ce qui permet de réduire la consommation de bande passante et par conséquent l'impact environnemental.
|
|
||||||
|
|
||||||
En finalité, ça ressemble à ça :
|
|
||||||
|
|
||||||
{% snippet path="html/lazy-loading.html" language="html" /%}
|
|
||||||
|
|
||||||
Allez, arrêtons-nous là pour l'éco-conception !
|
|
||||||
|
|
||||||
### 📱 Le responsive design
|
|
||||||
|
|
||||||
Maintenant que notre site est éco-conçu, accessible et optimisé pour le référencement, il est temps de s'attaquer à la partie responsive design. Tu connais certainement déjà le principe, mais pour les autres, le responsive design c'est le fait de rendre un site web adaptatif et lisible sur tous les types d'écrans, que ce soit un ordinateur, une tablette ou un smartphone.
|
|
||||||
|
|
||||||
Pour cela, nous avons plusieurs possibilités :
|
|
||||||
|
|
||||||
- Les media queries _(pour adapter le style en fonction de la taille de l'écran et du type de support)_
|
|
||||||
- Les unités relatives _(pour adapter la taille des éléments en fonction de la taille de la police ou d'un élément parent)_
|
|
||||||
- `em` : unité relative à la taille de la police de l'élément parent
|
|
||||||
- `rem` : unité relative à la taille de la police de l'élément racine
|
|
||||||
- `%` : unité relative à la taille de l'élément parent
|
|
||||||
- `vw`/`vh` : unité relative à la largeur de la fenêtre _(ainsi que les variantes `lvh`/`lvw`, `svh`/`svw` et `dvh`/`dvw`)_
|
|
||||||
- Les grid et flexbox _(pour organiser les éléments de manière flexible et adaptative)_
|
|
||||||
|
|
||||||
Il est également possible d'utiliser des frameworks CSS, comme Tailwind CSS ou Bootstrap, qui proposent des composants et des classes prédéfinies pour faciliter la mise en place du responsive design.
|
|
||||||
|
|
||||||
### 🔒 Déploiement et sécurité
|
|
||||||
|
|
||||||
Dernière étape avant de pouvoir souffler un peu : le déploiement de ton site.
|
|
||||||
|
|
||||||
Pour déployer ton site, tu vas devoir choisir un hébergeur, un nom de domaine, configurer un serveur, transférer tes fichiers, etc.
|
|
||||||
Tu peux totalement utiliser des services "gratuits", comme Netlify ou Vercel, mais assure-toi que le service que tu choisis respecte les normes de sécurité, les normes de respect sur les données personnelles, mais surtout que tu sois en mesure de déployer ton site sans ces outils !
|
|
||||||
|
|
||||||
Enfin, n'oublie pas de sécuriser ton site, en utilisant un certificat SSL par exemple.
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
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 %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 🛠️ Ressources conseillées
|
|
||||||
|
|
||||||
_En cours de rédaction..._
|
|
||||||
|
|
||||||
## 🎯 Critères d'évaluation
|
|
||||||
|
|
||||||
- L'interface est conforme à la maquette et les besoins en éco-conception sont pris en compte
|
|
||||||
- L'interface tient compte de l'expérience utilisateur, y compris pour les personnes en situation de handicap
|
|
||||||
- L'interface respecte les recommandations de sécurité liées aux applications web ou web mobile
|
|
||||||
- L'interface s'adapte au type d'utilisation de l'application, et notamment à la taille, au type et à la disposition du support, y compris pour les équipements mobiles
|
|
||||||
- La règlementation en vigueur est respectée, y compris celle relative à l'accessibilité
|
|
||||||
- Le site est publié de manière sécurisée
|
|
||||||
- Le site est visible sur les moteurs de recherche et le référencement dépend du public
|
|
||||||
|
|
||||||
## 🤯 Aller plus loin
|
|
||||||
|
|
||||||
Tu utilises un site qui tourne sur un port spécifique, mais tu aimerais bien que ton site soit accessible sur les ports 80 _(HTTP)_ et 443 _(HTTPS)_ ?
|
|
||||||
Pour ça il y a une merveilleuse technologie qui s'appelle le reverse proxy !
|
|
||||||
|
|
||||||
Le reverse proxy, c'est un serveur qui va recevoir les requêtes HTTP et les rediriger vers le serveur qui héberge le site. Tu peux très bien faire la redirection sur le même serveur, c'est d'ailleurs ce qu'on va faire ici.
|
|
||||||
|
|
||||||
Prenons un exemple concret, le cas d'une application qui tourne sur le port 3000, mais que l'on souhaite rendre accessible sur le port 80.
|
|
||||||
|
|
||||||
Avec Nginx, on peut faire ça très simplement en créant un fichier de configuration dans `/etc/nginx/sites-available/`.
|
|
||||||
|
|
||||||
{% snippet path="nginx/reverse-proxy.conf" language="nginx" showLineNumbers=true /%}
|
|
||||||
|
|
||||||
... 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.
|
|
||||||
Si tu souhaites également rajouter le support du HTTPS, tu peux utiliser [Certbot](https://certbot.eff.org/) pour générer un certificat gratuit avec Let's Encrypt.
|
|
||||||
|
|
||||||
## 🧠 Documentations
|
|
||||||
|
|
||||||
- [Wikipédia - Accessibilité web](https://fr.wikipedia.org/wiki/Accessibilit%C3%A9_du_web)
|
|
||||||
- [Wikipédia - Référencement naturel](https://fr.wikipedia.org/wiki/R%C3%A9f%C3%A9rencement_naturel)
|
|
||||||
- [Wikipédia - Éco-conception numérique](https://fr.wikipedia.org/wiki/%C3%89coconception)
|
|
||||||
- [MDN - Accessible Rich Internet Applications _(ARIA)_](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA)
|
|
||||||
|
|
||||||
## 🛠️ Outils
|
|
||||||
|
|
||||||
- [Let's Encrypt](https://letsencrypt.org/)
|
|
||||||
- [Certbot](https://certbot.eff.org/)
|
|
||||||
- [WebAim Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
|
||||||
- [Contrast Checker](https://contrastchecker.com/)
|
|
||||||
- [Coolors - Contrast Checker](https://coolors.co/contrast-checker/112a46-acc8e5)
|
|
||||||
- [Tailwind CSS](https://tailwindcss.com/)
|
|
||||||
- [Bulma](https://bulma.io/)
|
|
||||||
- [Bootstrap](https://getbootstrap.com/)
|
|
||||||
@ -1,182 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 4 - Développer la partie dynamique des interfaces utilisateur web ou web mobile
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 4 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags: [DWWM]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Références
|
|
||||||
|
|
||||||
- REAC _(mise à jour du 02/07/2024)_, pages 21 et 22
|
|
||||||
- RE _(mise à jour du 02/07/2024)_, page 11
|
|
||||||
|
|
||||||
## 📋 En résumé
|
|
||||||
|
|
||||||
Ç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...
|
|
||||||
|
|
||||||
{% 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 :
|
|
||||||
|
|
||||||
- Les formulaires dynamiques _(ajout/suppression de champs, vérification des données, etc.)_
|
|
||||||
- Les animations _(chargement d'un témoin de chargement, apparition/disparition d'éléments, etc.)_
|
|
||||||
- Les interactions avec l'utilisateur _(drag and drop, ouverture de fenêtre modale, etc.)_
|
|
||||||
- Les appels à des services web _(API REST, etc.)_
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
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 type="question" title="Mon site est fait avec React/Angular/Vue.js, donc je valide automatiquement cette CP ?" %}
|
|
||||||
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)_.
|
|
||||||
|
|
||||||
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.
|
|
||||||
Cependant il va potentiellement y avoir un défaut majeur sur ton projet : le référencement naturel _(SEO)_.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## ➕ Informations complémentaires
|
|
||||||
|
|
||||||
### 🌐 Référencement d'un site généré côté client
|
|
||||||
|
|
||||||
Les sites web générés côté client _(**CSR**, ou "client-side rendering")_ ont un défaut majeur : ils ne sont pas très bien référencés par les moteurs de recherche.
|
|
||||||
|
|
||||||
En effet, les moteurs de recherche ont du mal à lire le contenu d'un site généré côté client, car ils n'exécutent pas le JavaScript.
|
|
||||||
C'est pour cela qu'il est recommandé de mettre en place un rendu côté serveur _(**SSR**, ou "server-side rendering")_ pour améliorer le référencement naturel de ton site.
|
|
||||||
|
|
||||||
Si tu veux en savoir plus sur le sujet, tu peux consulter [cet article de Google](https://web.dev/articles/rendering-on-the-web?hl=fr#rendering-terminology).
|
|
||||||
|
|
||||||
De mon côté, je recommande énormément de passer par le framework [Vike](https://vike.dev/) qui permet de générer des sites web ultra-rapides avec un rendu côté serveur et un rendu côté client, le tout en utilisant Vue.js, React ou _(presque)_ n'importe quel autre framework front-end JavaScript !
|
|
||||||
|
|
||||||
Tu as aussi la possibilité d'utiliser [Next.js](https://nextjs.org/) pour React, [Nuxt.js](https://nuxtjs.org/) pour Vue.js ou [SvelteKit](https://kit.svelte.dev/) pour Svelte qui permettent de faire du rendu côté serveur.
|
|
||||||
|
|
||||||
### 👴 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... 😅
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
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()` ?!" %}
|
|
||||||
|
|
||||||
Tout doux, tout doux, il existe une solution ! 😎
|
|
||||||
|
|
||||||
Si je te parle des requêtes XHR _(XMLHttpRequest)_ tu me dis... ?
|
|
||||||
|
|
||||||
> Mais c'est vieux ça, c'est pas du tout à la mode !
|
|
||||||
|
|
||||||
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... ?
|
|
||||||
|
|
||||||
> Fetch !
|
|
||||||
|
|
||||||
**Et sinon, pour faire simple :**
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% tabs defaultSelectedTab="xhr" %}
|
|
||||||
{% tab value="xhr" label="🥉 XHR" %}
|
|
||||||
{% snippet path="js/xhr/xhr.js" language="js" showLineNumbers=true /%}
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="jquery" label="🥈 jQuery" %}
|
|
||||||
{% snippet path="js/xhr/jquery-ajax.js" language="js" showLineNumbers=true /%}
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="fetch" label="🥇🏆 Fetch" %}
|
|
||||||
{% snippet path="js/xhr/fetch.js" language="js" showLineNumbers=true /%}
|
|
||||||
{% /tab %}
|
|
||||||
{% /tabs %}
|
|
||||||
|
|
||||||
Non seulement `fetch` est plus simple à utiliser et comprendre _(contrairement à XMLHttpRequest)_ mais elle est également plus légère que `jQuery.ajax()` puisqu'elle est native au navigateur ! Alors pourquoi s'en priver ? 😉
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🛠️ Ressources conseillées
|
|
||||||
|
|
||||||
_En cours de rédaction..._
|
|
||||||
|
|
||||||
## 🎯 Critères d'évaluation
|
|
||||||
|
|
||||||
- L'interface utilisateur est conforme au dossier de conception
|
|
||||||
- L'interface est dynamique et l'expérience utilisateur est améliorée, y compris pour les personnes en situation de handicap
|
|
||||||
- Les recommandations de sécurité liées aux applications web et web mobile sont respectées
|
|
||||||
- La règlementation en vigueur sont respectées, y compris celle relative à l'accessibilité
|
|
||||||
- Le code est documenté, y compris en anglais _(niveau B1 CECRL pour l'anglais)_
|
|
||||||
- Le jeu d'essai fonctionnel est complet et les tests unitaires sont réalisés pour les composants concernés
|
|
||||||
- Les tests de sécurité sont réalisés
|
|
||||||
|
|
||||||
## 🤯 Aller plus loin
|
|
||||||
|
|
||||||
Plus tôt, dans la CP 3, je t'ai parlé d'une astuce d'éco-conception pour différer le chargement des images avec l'attribut `loading="lazy"` sur les balises `<img>`. Mais on peut aller encore plus loin que simplement différer le chargement des images !
|
|
||||||
|
|
||||||
On peut également différer le chargement des scripts JavaScript avec l'attribut defer sur les balises `<script>`.
|
|
||||||
|
|
||||||
### 📜 Différer le chargement des scripts
|
|
||||||
|
|
||||||
Si ça te rassure, ça ne cassera pas ton site web et au contraire, ça peut même l'améliorer !
|
|
||||||
L'intérêt premier de cet attribut est de différer l'exécution du script jusqu'à ce que le document HTML soit entièrement chargé.
|
|
||||||
|
|
||||||
L'avantage de cette technique est qu'on va demander au navigateur de charger le script en parallèle du reste du contenu, sans bloquer le chargement de la page.
|
|
||||||
Comme ça : pas de page blanche pendant le chargement du script, et le script sera exécuté une fois que le navigateur aura fini de charger le reste de la page !
|
|
||||||
|
|
||||||
Maintenant, on sait qu'on peut charger de manière "asynchrone" nos images et nos scripts, mais ce n'est toujours pas terminé.. 😏
|
|
||||||
|
|
||||||
### 📺 Différer le chargement des iframes
|
|
||||||
|
|
||||||
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 !
|
|
||||||
|
|
||||||
{% callout type="note" title="Chargement d'un iframe Youtube uniquement au clic de l'utilisateur" %}
|
|
||||||
|
|
||||||
{% tabs defaultSelectedTab="html" %}
|
|
||||||
{% tab value="html" label="HTML - 1ère étape" %}
|
|
||||||
{% snippet path="html/defer-iframe.html" language="html" showLineNumbers=true /%}
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="js" label="JavaScript - 2ème étape" %}
|
|
||||||
{% snippet path="js/defer-iframe.ts" language="ts" showLineNumbers=true /%}
|
|
||||||
{% /tab %}
|
|
||||||
{% /tabs %}
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
{% callout type="question" title="Mais ça fait beaucoup de code juste pour charger des iframes, c'est vraiment nécessaire ?" %}
|
|
||||||
Pour être franc, il n'y a pas de solution idéale. Mais on peut améliorer les performances du site et gagner en sobriété numérique en ne chargeant pas des ressources lourdes inutilement.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- [MDN Web Docs - Attribut `loading` sur les balises `<img>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/img#loading)
|
|
||||||
- [MDN Web Docs - Attribut `defer` sur les balises `<script>`](https://developer.mozilla.org/fr/docs/Web/HTML/Element/script#defer)
|
|
||||||
- [MDN Web Docs - Attribut `data-*`](https://developer.mozilla.org/fr/docs/Web/HTML/Global_attributes/data-*)
|
|
||||||
- [MDN Web Docs - `fetch`](https://developer.mozilla.org/fr/docs/Web/API/Fetch_API)
|
|
||||||
- [Wikipédia - API](https://fr.wikipedia.org/wiki/Interface_de_programmation)
|
|
||||||
|
|
||||||
## 🛠️ Outils
|
|
||||||
|
|
||||||
- [Vike - Framework front-end pour un rendu côté serveur et côté client](https://vike.dev/)
|
|
||||||
- [Next.js - Framework pour React avec rendu côté serveur](https://nextjs.org/)
|
|
||||||
- [Nuxt.js - Framework pour Vue.js avec rendu côté serveur](https://nuxtjs.org/)
|
|
||||||
- [SvelteKit - Framework pour Svelte avec rendu côté serveur](https://kit.svelte.dev/)
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 5 - Mettre en place une base de données relationnelle
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 5 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags: [DWWM]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Références
|
|
||||||
|
|
||||||
- REAC _(mise à jour du 02/07/2024)_, pages 23 et 24
|
|
||||||
- RE _(mise à jour du 02/07/2024)_, page 11
|
|
||||||
|
|
||||||
## 📋 En résumé
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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...
|
|
||||||
Mais garde sous la main ta base de données non relationnelles
|
|
||||||
pour la prochaine compétence, ça te servira 😉
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🎨 Modélisation de la base de données
|
|
||||||
|
|
||||||
Commençons par le commencement : **comment créer une base de données relationnelle ?**
|
|
||||||
|
|
||||||
Il y a pléthore de possibilités, mais ici on va s'attarder sur une méthodologie française _(cocorico 🐓)_ qui est la méthode **Merise**.
|
|
||||||
On va se baser sur différents schémas issus de cette méthode pour créer notre base de données relationnelle, à savoir :
|
|
||||||
|
|
||||||
1. Le **dictionnaire des données** : qui va recenser toutes les données que l'on va stocker par la suite dans notre base de données
|
|
||||||
2. Le **MCD** _(Modèle Conceptuel des Données)_ : qui va représenter les données et leurs relations, sous la forme d'entités et d'associations dans un schéma graphique
|
|
||||||
3. Le **MLD** _(Modèle Logique des Données)_ : qui va représenter les données sous forme de tables et de relations, dans un schéma graphique
|
|
||||||
4. Le **MRD** _(Modèle Relationnel des Données)_ : qui va représenter les mêmes informations que le MLD, mais cette fois-ci en format texte
|
|
||||||
5. Le **MPD** _(Modèle Physique des Données)_ : qui va représenter les données sous forme de tables et de relations, en intégrant les types de données et les contraintes
|
|
||||||
|
|
||||||
Tu remarqueras que j'y ai indiqué un ordre dans la liste ci-dessus.
|
|
||||||
Si je peux te donner un indice : ce n'est pas pour rien que c'est une liste ordonnée 😉
|
|
||||||
|
|
||||||
Donc si tu réalises un dictionnaire de données après avoir fait ton MPD, c'est que tu n'as pas compris l'objectif du dictionnaire de données ! _(par exemple)_
|
|
||||||
|
|
||||||
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 !
|
|
||||||
|
|
||||||
{% quick-link
|
|
||||||
title="Introduction à 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."
|
|
||||||
icon="presets"
|
|
||||||
/%}
|
|
||||||
|
|
||||||
## 💾 Sauvegardes de la base de données
|
|
||||||
|
|
||||||
C'est bien beau de créer une base de données, mais si on ne la sauvegarde pas, on risque de tout perdre en cas de problème...
|
|
||||||
|
|
||||||
Certains hébergeurs permettent de faire des sauvegardes automatisées, mais dans le cas où tu dois toi-même sauvegarder ta base de données, il existe plusieurs solutions :
|
|
||||||
|
|
||||||
- **Les sauvegardes manuelles** : qui consistent à exporter le contenu de ta base de données dans un fichier _(généralement au format SQL)_
|
|
||||||
- **Les sauvegardes automatiques** : qui consistent à automatiser le processus de sauvegarde, généralement via un script ou un outil dédié
|
|
||||||
|
|
||||||
On va se concentrer _(que très rapidement, ne t'inquiète pas !)_ sur la partie automatisée, puisqu'elle permet également de comprendre comment faire une sauvegarde manuellement.
|
|
||||||
|
|
||||||
Pour mettre en place l'automatisation, on peut mettre en place une tâche planifiée : un processus qui va s'exécuter à intervalles réguliers pour sauvegarder notre base de données.
|
|
||||||
Sur Linux, on parlera d'un `cron job` _(ou `tâche cron` en français)_.
|
|
||||||
|
|
||||||
Sans rentrer dans les détails de configuration d'une tâche cron, on va devoir la créer en donnant plusieurs informations :
|
|
||||||
|
|
||||||
- **Le chemin vers le script de sauvegarde** : qui va contenir les commandes pour sauvegarder notre base de données
|
|
||||||
- **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
|
|
||||||
|
|
||||||
{% callout type="note" title="Exemple de script `bash` pour sauvegarder une base de données PostgreSQL" %}
|
|
||||||
{% 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.
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🛡️ Sécurité et confidentialité des données
|
|
||||||
|
|
||||||
On ne le répétera jamais assez, mais la sécurité et la confidentialité des données sont primordiales pour toute application.
|
|
||||||
|
|
||||||
Pour garantir la sécurité de ta base de données, il est recommandé de mettre en place plusieurs mesures :
|
|
||||||
|
|
||||||
- **Les sauvegardes régulières** : pour éviter de perdre des données en cas de problème
|
|
||||||
- **Les mises à jour régulières** : pour corriger les failles de sécurité et les bugs
|
|
||||||
- **Les accès restreints** : pour limiter l'accès à la base de données aux seules personnes autorisées
|
|
||||||
- **Les mots de passe forts** : pour éviter les attaques par force brute ou par dictionnaire
|
|
||||||
- **Les connexions sécurisées** : pour éviter les interceptions de données
|
|
||||||
|
|
||||||
Mais la sécurité ne s'arrête pas là, il est également important de garantir la confidentialité des données :
|
|
||||||
|
|
||||||
- **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" %}
|
|
||||||
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.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## ➕ Informations complémentaires
|
|
||||||
|
|
||||||
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...
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
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**.
|
|
||||||
|
|
||||||
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 😉
|
|
||||||
|
|
||||||
{% quick-link
|
|
||||||
title="Introduction à 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."
|
|
||||||
icon="presets"
|
|
||||||
/%}
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🛠️ Ressources conseillées
|
|
||||||
|
|
||||||
_En cours de rédaction..._
|
|
||||||
|
|
||||||
## 🎯 Critères d'évaluation
|
|
||||||
|
|
||||||
- Les données du schéma conceptuel et leurs relations sont identifiées et prises en compte
|
|
||||||
- Le schéma physique est conforme aux besoins exprimés dans le dossier de conception et respecte les règles des bases de données relationnelles
|
|
||||||
- Les règles de nommage sont respectées
|
|
||||||
- La sécurité, l'intégrité et la confidentialité des données est assurée
|
|
||||||
- La base de données de tests mise en place est conforme au schéma physique
|
|
||||||
- Les utilisateurs sont créés avec leurs droits respectifs conformément au dossier de conception
|
|
||||||
- La base de données créée est sauvegardée et elle peut être restaurée en cas d'incident
|
|
||||||
- La documentation technique des bases de données est comprise, en langue française ou anglaise _(niveau B1 du CECRL pour l'anglais)_
|
|
||||||
|
|
||||||
## 🤯 Aller plus loin
|
|
||||||
|
|
||||||
Pas trop mal à la tête ? On continue un tout petit peu ? 😅
|
|
||||||
Tu as vu qu'on précise entre parenthèses la longueur des données, mais pourquoi on fait ça ?
|
|
||||||
|
|
||||||
Tu n'es pas sans savoir que pour stocker des données et que pour les stocker, il nous faut de l'espace.
|
|
||||||
Et cet espace, on le définit en fonction de la longueur de nos données : on parle alors d'allocation.
|
|
||||||
|
|
||||||
En précisant une valeur entre les parenthèses, on vient dire à notre SGBD combien de place il doit réserver pour stocker nos données **au maximum**.
|
|
||||||
|
|
||||||
Dans le cas d'un `VARCHAR(30)`, on réserve 30 caractères pour stocker notre donnée, même si elle n'en fait que 5 _(allocation **dynamique**)_.
|
|
||||||
Dans le cas d'un `CHAR(30)`, on réserve également 30 caractères, mais cette fois-ci on "complète notre donnée avec des espaces" pour atteindre les 30 caractères _(allocation **statique**)_.
|
|
||||||
|
|
||||||
Si on ne précise pas de longueur, le SGBD va réserver une place par défaut qui varie d'un SGBD à l'autre.
|
|
||||||
Donc ce n'est pas parce que tu te dis : "255 caractères c'est très bien pour mon `VARCHAR`, pas besoin de le préciser puisque c'est la valeur par défaut !" que tu as raison... 😅
|
|
||||||
Si demain la norme change et que l'allocation par défaut pour les types `VARCHAR` passe à 100 caractères au lieu de 255 caractères, tu risques de te retrouver avec des données tronquées !
|
|
||||||
|
|
||||||
## 🧠 Documentations
|
|
||||||
|
|
||||||
- [Éditions ENI - Merise - Guide pratique (4e édition), par **Jean-Luc Baptiste**](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)
|
|
||||||
- [Medium - Non, les ID n'ont pas leur place dans un MCD, par **Jean Prulière**](https://jeanpruliere.medium.com/non-les-id-nont-pas-leur-place-dans-un-mcd-43b5cd5ce9b6)
|
|
||||||
- [SQL.sh - Cours et tutoriels SQL](https://sql.sh/)
|
|
||||||
- [Wikipédia - UML](<https://fr.wikipedia.org/wiki/UML_(informatique)>)
|
|
||||||
|
|
||||||
## 🛠️ Outils
|
|
||||||
|
|
||||||
- [Looping - Logiciel de modélisation de bases de données](https://www.looping-mcd.fr/)
|
|
||||||
- [Mocodo - Logiciel en ligne de modélisation de bases de données](https://www.mocodo.net/)
|
|
||||||
@ -1,165 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 6 - Développer des composants d'accès aux données SQL et NoSQL
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 6 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags: [DWWM]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Références
|
|
||||||
|
|
||||||
- REAC _(mise à jour du 02/07/2024)_, pages 25 et 26
|
|
||||||
- RE _(mise à jour du 02/07/2024)_, page 11
|
|
||||||
|
|
||||||
## 📋 En résumé
|
|
||||||
|
|
||||||
Gros morceau la création de bases de données, n'est-ce pas ? 😅
|
|
||||||
On va pouvoir souffler un coup en parlant maintenant de l'accès à ces bases de données. _(enfin, souffler... pas trop quand même)_
|
|
||||||
|
|
||||||
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 ?" %}
|
|
||||||
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 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.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
Alleeeez, on va voir ça de plus près ! 😎
|
|
||||||
|
|
||||||
## ⚙️ 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 ?" %}
|
|
||||||
**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.
|
|
||||||
|
|
||||||
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 %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Tu peux créer, lire, mettre à jour et supprimer des données sans avoir à écrire de requêtes SQL ou NoSQL.
|
|
||||||
|
|
||||||
Cependant ça vient aussi avec son lot de contraintes et de limitations...
|
|
||||||
|
|
||||||
Un ORM ou un ODM ne va pas forcément te permettre de faire tout ce que tu veux et dans certains cas, tu vas devoir écrire des requêtes SQL ou NoSQL à la main.
|
|
||||||
D'autre part, ces outils peuvent aussi avoir un impact sur les performances de ton application, surtout si tu fais des requêtes complexes ou si tu manipules de grandes quantités de données.
|
|
||||||
|
|
||||||
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" %}
|
|
||||||
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 :
|
|
||||||
|
|
||||||
> 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.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🔎 Intégrité des données
|
|
||||||
|
|
||||||
L'intégrité des données, c'est le fait de garantir que les données stockées dans la base de données sont correctes et cohérentes, de la création jusqu'à la suppression.
|
|
||||||
Si dans un champ de ta base de données tu attends un nombre entier, tu ne vas pas accepter une chaîne de caractères, n'est-ce pas ?
|
|
||||||
|
|
||||||
Et pour garantir cette intégrité, on va mettre en place des vérifications **avant** d'insérer ou de mettre à jour des données dans la base de données.
|
|
||||||
|
|
||||||
Rien de plus simple bien entendu !
|
|
||||||
|
|
||||||
Tu t'attends à avoir une adresse email dans un champ ?
|
|
||||||
Alors tu vas vérifier que l'adresse email est bien une adresse email, et non pas une chaîne de caractères lambda.
|
|
||||||
|
|
||||||
## 🔐 Confidentialité des données
|
|
||||||
|
|
||||||
La plupart du temps, nos bases de données vont accueillir des données confidentielles, comme :
|
|
||||||
|
|
||||||
- Des mots de passe
|
|
||||||
- Des informations personnelles _(nom, prénom, adresse, etc.)_
|
|
||||||
- Des données sensibles _(informations bancaires, médicales, etc.)_
|
|
||||||
|
|
||||||
Bien que notre bases de données se doit d'être sécurisée dans son accès et ses permissions, dans le cas d'une fuite il est important de sécuriser ces 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 ?" %}
|
|
||||||
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**.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
{% 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**.
|
|
||||||
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 !
|
|
||||||
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 :
|
|
||||||
|
|
||||||
> Message : "Bonjour"
|
|
||||||
> Décalage : 3
|
|
||||||
>
|
|
||||||
> Message chiffré : "Erqmruxu"
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
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 %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
Mais alors, comment on peut s'y prendre ?
|
|
||||||
|
|
||||||
🥁🥁🥁
|
|
||||||
|
|
||||||
Avec des bibliothèques, tout simplement ! 🙃
|
|
||||||
_(Ou si tu es un peu fou, tu peux essayer de le faire toi-même, mais attention à ce que ce soit **réellement sécurisé** sinon tu en deviens le seul et unique **responsable**)_
|
|
||||||
|
|
||||||
Tu as notamment des bibliothèques _(Node.js)_ qui sont très utilisées :
|
|
||||||
|
|
||||||
- Hachage : `bcrypt` _(ou encore `argon2`)_
|
|
||||||
- Chiffrement : `crypto` _(module natif de Node.js en plus, si ça c'est pas la classe 😎)_
|
|
||||||
|
|
||||||
Je te laisse te plonger dans les documentations associées, que tu retrouveras _(presque)_ tout en bas de cette fiche.
|
|
||||||
|
|
||||||
Et naturellement : **PERSONNE** ne doit avoir accès à ces données, à part les personnes autorisées/concernées bien entendu.
|
|
||||||
|
|
||||||
## 🎯 Critères d'évaluation
|
|
||||||
|
|
||||||
- Les traitements relatifs aux manipulations des données répondent aux fonctionnalités décrites dans le dossier de conception
|
|
||||||
- L'intégrité et la confidentialité des données sont maintenues
|
|
||||||
- Les cas d'exception sont pris en compte
|
|
||||||
- Toutes les entrées sont contrôlées et validées dans les composants serveurs sécurisés
|
|
||||||
- Les tests unitaires et de sécurité sont associés à chaque composant
|
|
||||||
- La démarche structurée de résolution de problème est adaptée en cas de dysfonctionnement
|
|
||||||
- Le système de veille permet de suivre les évolutions technologiques et les problématiques de sécurité liées aux bases de données SQL et NoSQL
|
|
||||||
|
|
||||||
## 🤯 Aller plus loin
|
|
||||||
|
|
||||||
T'es encore là ? Tu aimes ça les ~patates~ bases de données, hein ? 😏
|
|
||||||
Alors dans ce cas, je te recommande chaudement de te pencher sur PostgreSQL qui est, à mon sens, l'une des seules **vraies** bases de données relationnelles.
|
|
||||||
|
|
||||||
Je ne m'étalerai pas sur ce sujet, mais désolé MySQL/MariaDB de ne pas être au niveau... 😅
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
{% callout type="note" title="Gratuité des formations Dalibo" %}
|
|
||||||
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.
|
|
||||||
|
|
||||||
Tu peux retrouver ces supports sur la page [Formations](https://www.dalibo.com/formations) du site de Dalibo.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🧠 Documentations
|
|
||||||
|
|
||||||
- [SQL.sh - Cours et tutoriels SQL](https://sql.sh/)
|
|
||||||
- [Dalibo - Formations](https://www.dalibo.com/formations)
|
|
||||||
- [Wikipédia - Chiffrement de César](https://fr.wikipedia.org/wiki/Chiffrement_par_d%C3%A9calage)
|
|
||||||
- [bcrypt - Documentation](https://www.npmjs.com/package/bcrypt)
|
|
||||||
- [argon2 - Documentation](https://www.npmjs.com/package/argon2)
|
|
||||||
- [crypto - Documentation](https://nodejs.org/api/crypto.html)
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 7 - Développer des composants métier coté serveur
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 7 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags: [DWWM]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Références
|
|
||||||
|
|
||||||
- REAC _(mise à jour du 02/07/2024)_, pages 27 et 28
|
|
||||||
- RE _(mise à jour du 02/07/2024)_, page 12
|
|
||||||
|
|
||||||
## 📋 En résumé
|
|
||||||
|
|
||||||
Maintenant que l'on sait modéliser et dialoguer avec notre base de données, on va pouvoir s'attaquer à la logique métier de notre application.
|
|
||||||
Dans le cadre d'un projet web, ça représentera principalement nos contrôleurs, middlewares et services.
|
|
||||||
|
|
||||||
Si tu as déjà travaillé sur un projet web, tu as probablement déjà entendu parler du design pattern MVC.
|
|
||||||
Et si ce n'est pas le cas, pas de panique, on va voir ensemble ce que c'est !
|
|
||||||
|
|
||||||
## 💡 Le design pattern MVC
|
|
||||||
|
|
||||||
Le design pattern MVC est un modèle d'architecture logicielle qui sépare les données, la logique métier et l'interface utilisateur.
|
|
||||||
|
|
||||||
- **Modèle** : représente les données de l'application. Il contient les classes qui permettent de manipuler les données.
|
|
||||||
- **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.
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
Enfin, si, ils sont corrects... mais certains ne s'appliquent pas à tous les frameworks et architectures.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
Pour t'aider à mieux te représenter un schéma MVC avec les ordres de flux de données et de contrôle :
|
|
||||||
|
|
||||||
{% 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 ?" %}
|
|
||||||
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.)_.
|
|
||||||
|
|
||||||
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.
|
|
||||||
{% /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.
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
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 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 ?
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🧑⚖️ 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.
|
|
||||||
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é" %}
|
|
||||||
Pense à être cohérent en ce qui concerne la langue utilisée.
|
|
||||||
|
|
||||||
{% callout type="warning" title="Pas de franglais !" %}
|
|
||||||
É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 anglais, reste en anglais.
|
|
||||||
{% /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.
|
|
||||||
|
|
||||||
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 %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Au passage, tu as la possibilité de configurer ton éditeur de texte pour qu'il respecte ces conventions de nommage.
|
|
||||||
Sur VSCode, l'extension [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) te permettra de vérifier que ton code respecte bien les conventions de nommage que tu auras définies et [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) te permettra de formater ton code automatiquement selon ces mêmes conventions.
|
|
||||||
|
|
||||||
Ça me permet également de te rappeler que tu dois **documenter ton code** et ce, **dans la langue définie pour le projet**.
|
|
||||||
Le premier réflexe à avoir est de documenter l'installation et l'utilisation de ton projet dans le fichier `README.md` à la racine de ton projet.
|
|
||||||
|
|
||||||
Ensuite, n'ai pas peur d'abuser de la JSDoc _(ou PHPDoc si tu travailles en PHP)_ pour documenter tes fonctions et méthodes.
|
|
||||||
Par contre, n'abuse pas des commentaires potentiellement "inutiles" qui alourdissent la lecture de ton code, ça peut être contre-productif.
|
|
||||||
|
|
||||||
## 🔄 Le jeu d'essai et les tests unitaires
|
|
||||||
|
|
||||||
Histoire de faire simple : commençons par le jeu d'essai !
|
|
||||||
|
|
||||||
### 🎮 Le jeu d'essai
|
|
||||||
|
|
||||||
Le jeu d'essai est un ensemble de données qui permet de tester le bon fonctionnement de l'application.
|
|
||||||
Ce type de test se compose de trois parties :
|
|
||||||
|
|
||||||
- **Les données d'entrée** : ce sont les données que tu vas envoyer à ton application pour tester son comportement.
|
|
||||||
- **Les données de sortie attendues** : ce sont les données que tu attends en retour de ton application.
|
|
||||||
- **Les données de sortie obtenues** : ce sont les données que ton application te renvoie.
|
|
||||||
|
|
||||||
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 defaultSelectedTab="invalid" %}
|
|
||||||
{% tab value="invalid" label="Données invalides" %}
|
|
||||||
|
|
||||||
- **Les données d'entrée** :
|
|
||||||
- Adresse e-mail : `mauvaise-adresse@email`
|
|
||||||
- Mot de passe : `password`
|
|
||||||
- **Les données de sortie attendues** :
|
|
||||||
- Erreur : `Adresse e-mail invalide`
|
|
||||||
- Erreur : `Le mot de passe ne respecte pas les critères de sécurité requis`
|
|
||||||
- **Les données de sortie obtenues** :
|
|
||||||
- Erreur : `Adresse e-mail invalide`
|
|
||||||
- Erreur : `Le mot de passe ne respecte pas les critères de sécurité requis`
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="valid" label="Données valides" %}
|
|
||||||
|
|
||||||
- **Les données d'entrée** :
|
|
||||||
- Adresse e-mail : `bonne-adresse@email.fr`
|
|
||||||
- Mot de passe : `Password123&` _(bon, le mot de passe n'est absolument pas "fort", mais il respecte les critères imposés)_
|
|
||||||
- **Les données de sortie attendues** :
|
|
||||||
- Succès : `Utilisateur inscrit avec succès`
|
|
||||||
- **Les données de sortie obtenues** :
|
|
||||||
- Succès : `Utilisateur inscrit avec succès`
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="email-already-used" label="Adresse email déjà utilisée" %}
|
|
||||||
|
|
||||||
- **Les données d'entrée** :
|
|
||||||
- Adresse e-mail : `adresse-email@utilisee.fr`
|
|
||||||
- Mot de passe : `Password123&`
|
|
||||||
- **Les données de sortie attendues** :
|
|
||||||
- Erreur : `Adresse e-mail déjà utilisée`
|
|
||||||
- **Les données de sortie obtenues** :
|
|
||||||
- Erreur : `Adresse e-mail déjà utilisée`
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
{% /tabs %}
|
|
||||||
|
|
||||||
{% callout type="note" title="Faire ces tests facilement" %}
|
|
||||||
Si je te parle de client HTTP, tu me réponds... ?
|
|
||||||
[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, c'est un peu comme le jeu d'essai, mais en plus automatisé et surtout axé sur les fonctions et méthodes de ton application.
|
|
||||||
|
|
||||||
Le gros avantage que ça va te permettre, c'est de t'assurer que toutes les fonctionnalités développées fonctionnent comme prévu et ce, à chaque fois que tu modifies ton code.
|
|
||||||
Oui oui, tu as bien lu : **à chaque fois que tu modifies ton code**, pas forcément à chaque fois que tu modifies une fonction ou une méthode qui avait déjà des tests unitaires.
|
|
||||||
|
|
||||||
Alors pas forcément à la moindre modification, je veux plutôt dire que le but est de vérifier avant de livrer !
|
|
||||||
Tu peux d'ailleurs faire en sorte que **tous les tests unitaires** doivente passer avant de pouvoir pusher ton code sur la branche principale de ton dépôt Git. Au début c'est casse pied, mais tu verras que ça te permettra de gagner du temps sur le long terme.
|
|
||||||
|
|
||||||
L'objectif c'est de t'assurer que tu ne casses pas une fonctionnalité existante en ajoutant une nouvelle fonctionnalité ou en modifiant une fonctionnalité existante pour garantir que ton projet reste fonctionnel et ne casse pas sous les mains des utilisateurs.
|
|
||||||
|
|
||||||
## 🎯 Critères d'évaluation
|
|
||||||
|
|
||||||
- Les traitements répondent aux fonctionnalités décrites dans le dossier de conception
|
|
||||||
- Les composants métier sont sécurisés
|
|
||||||
- Les bonnes pratiques de la programmation orientée objet _(POO)_ sont respectées
|
|
||||||
- Les règles de nommage sont conformes aux normes de qualité de l'entreprise
|
|
||||||
- Le code source est documenté, y compris en anglais
|
|
||||||
- Un jeu d'essai fonctionnel et les tests unitaires ont été réalisés pour les composants concernés
|
|
||||||
- Les tests de sécurité sont réalisés
|
|
||||||
- La démarche structurée de résolution de problème est adaptée en cas de dysfonctionnement
|
|
||||||
|
|
||||||
## 🧠 Documentations
|
|
||||||
|
|
||||||
- [Wikipédia - Design pattern MVC](https://fr.wikipedia.org/wiki/Mod%C3%A8le-vue-contr%C3%B4leur) _(Attention, le schéma présenté n'est pas forcément le plus adapté à tous les frameworks et architectures)_
|
|
||||||
- [Wikipédia - Conventions de nommage](https://fr.wikipedia.org/wiki/Convention_de_nommage)
|
|
||||||
- [JSDoc - Documentation](https://jsdoc.app/)
|
|
||||||
- [PHPDoc - Documentation](https://www.phpdoc.org/)
|
|
||||||
|
|
||||||
## 🛠️ Outils
|
|
||||||
|
|
||||||
- [Bruno](https://www.usebruno.com/)
|
|
||||||
- [Postman](https://www.postman.com/)
|
|
||||||
- [Insomnia](https://insomnia.rest/)
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
---
|
|
||||||
title: CP 8 - Documenter le déploiement d'une application dynamique web ou web mobile
|
|
||||||
description: Synthèse et explications des attentes relatives à la compétence professionnelle 8 du titre professionnel Développeur Web et Web Mobile (DWWM-01280m04).
|
|
||||||
tags: [DWWM, Déploiement, Backend, Reverse Proxy, Serveur Web]
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Références
|
|
||||||
|
|
||||||
- REAC _(mise à jour du 02/07/2024)_, page 29
|
|
||||||
- RE _(mise à jour du 02/07/2024)_, page 12
|
|
||||||
|
|
||||||
## 📋 En résumé
|
|
||||||
|
|
||||||
Allez, on clos la dernière compétence professionnelle de ce millésime 2023 avec la documentation du déploiement !
|
|
||||||
Et heureusement, on n'attend pas de toi de maîtriser un serveur dans les détails, mais d'expliquer **comment** mettre en ligne ton projet.
|
|
||||||
|
|
||||||
Tu as le droit d'utiliser des plateformes de déploiement en ligne comme Vercel, Netlify, Heroku, etc.
|
|
||||||
Mais la compréhension, même basique, d'un serveur Linux est quelque chose d'extrêmement apprécié et enrichissant.
|
|
||||||
|
|
||||||
## 🤖 Les plateformes de déploiement en ligne
|
|
||||||
|
|
||||||
Selon la plateforme utilisée, la procédure de déploiement peut varier. Certaines plateformes peuvent déployer automatiquement ton projet à partir de ton dépôt Git, d'autres nécessitent de passer par la ligne de commande pour push une branche spécifique sur le serveur de la plateforme.
|
|
||||||
|
|
||||||
C'est à toi _(et ton équipe)_ de choisir la plateforme qui vous convient le mieux et de documenter la procédure de déploiement afin que tout le monde puisse s'y retrouver.
|
|
||||||
|
|
||||||
## 🤝 Les serveurs mutualisés
|
|
||||||
|
|
||||||
Beaucoup d'hébergeurs proposent des serveurs mutualisés, c'est-à-dire que plusieurs sites web partagent les ressources d'un même serveur _(même si tu n'es pas le responsable des autres sites hébergés sur le serveur)_.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
Moins de matériel physique = moins de consommation d'énergie = moins d'émissions de CO2.
|
|
||||||
{% /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)_.
|
|
||||||
Il est donc important de bien se renseigner sur les fonctionnalités proposées par l'hébergeur avant de choisir un serveur mutualisé.
|
|
||||||
|
|
||||||
## 🔨 Les serveurs dédiés et VPS
|
|
||||||
|
|
||||||
Maintenant, le meilleur du meilleur : les serveurs dédiés et les VPS !
|
|
||||||
Ça peut faire peur sur le papier car on devient l'unique gestionnaire et responsable du serveur, mais c'est certainement la meilleure façon de comprendre comment fonctionne un serveur web.
|
|
||||||
|
|
||||||
La configuration la plus classique que l'on retrouvera sur un serveur dédié ou un VPS est la suivante :
|
|
||||||
|
|
||||||
- Un système d'exploitation headless _(sans interface graphique, à l'ancienne ! 👴)_ comme Ubuntu Server, CentOS, Debian, etc.
|
|
||||||
- Un serveur web comme Apache, Nginx, ou Caddy
|
|
||||||
- Une ou plusieurs bases de données comme MySQL, PostgreSQL, MongoDB, etc.
|
|
||||||
- Un serveur de langage comme Node.js, PHP, Ruby, Python, etc.
|
|
||||||
- Un gestionnaire de processus comme PM2, Supervisor, etc.
|
|
||||||
- Un gestionnaire de paquets comme APT, YUM, etc.
|
|
||||||
- Un pare-feu comme UFW, iptables, etc.
|
|
||||||
|
|
||||||
{% 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)_.
|
|
||||||
Comme on dit souvent :
|
|
||||||
|
|
||||||
> Le problème se situe souvent entre la chaise et le clavier.
|
|
||||||
|
|
||||||
Si tu recherches un hébergeur qui se veut éco-responsable _(bien plus que la moyenne)_ : Infomaniak est un excellent choix.
|
|
||||||
_(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).
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 🚪 Les reverse proxies
|
|
||||||
|
|
||||||
Un serveur web classique écoute sur les ports 80 et 443 pour les requêtes HTTP et HTTPS.
|
|
||||||
Sauf que ton application va probablement tourner sur d'autres ports, que ce soit 3000, 5000 ou je ne sais quel autre numéro.
|
|
||||||
|
|
||||||
Notre objectif avec les reverse proxies, c'est de lier un domaine _(sur les ports 80 et 443)_ à un port interne spécifique de notre serveur.
|
|
||||||
C'est un peu comme une "pseudo-redirection", mais qui sera invisible pour l'utilisateur.
|
|
||||||
|
|
||||||
### 📦 Nginx
|
|
||||||
|
|
||||||
Nginx est un serveur web qui est souvent utilisé comme reverse proxy, notamment pour sa simplicité de configuration et sa syntaxe nettement moins verbeuse que celle d'Apache.
|
|
||||||
|
|
||||||
Prenons l'exemple d'un serveur Node.js qui tourne sur le port 5000.
|
|
||||||
|
|
||||||
{% callout type="note" title="Configuration Nginx pour un reverse proxy" %}
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# Ensemble de configurations pour un serveur Nginx
|
|
||||||
server {
|
|
||||||
listen 80; # Port 80 pour les requêtes HTTP
|
|
||||||
listen [::]:80; # Port 80 pour les requêtes HTTP en IPv6
|
|
||||||
server_name mon-domaine.com; # Ton domaine qui pointe vers ton serveur web qui fait tourner ton application Node.js
|
|
||||||
|
|
||||||
# Configuration pour le reverse proxy, qui va rediriger les requêtes vers le port 5000
|
|
||||||
location / {
|
|
||||||
proxy_pass http://0.0.0.0:5000; # Redirige les requêtes vers le port 5000 (interne au serveur)
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr; # Envoie l'adresse IP du client à l'application Node.js dans le header
|
|
||||||
proxy_set_header Host $http_host; # Envoie le nom de domaine à l'application Node.js dans le header
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Oui, c'est aussi simple que ça ! Alors effectivement, il y a d'autres configurations possibles, mais pour un usage basique : c'est tout ce dont tu as besoin.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
### 📦 Caddy
|
|
||||||
|
|
||||||
Caddy est un serveur web qui se veut simple à configurer et qui propose de nombreuses fonctionnalités "out-of-the-box", comme la gestion automatique des certificats SSL _(gratuits)_ avec Let's Encrypt.
|
|
||||||
|
|
||||||
Pour le coup je n'ai pas encore eu l'occasion de faire des tests avec Caddy, mais je sais que la configuration pour un reverse proxy est extrêmement simple, voire plus simple que celle de Nginx.
|
|
||||||
|
|
||||||
### 📦 Apache
|
|
||||||
|
|
||||||
On ne le présente plus, Apache est certainement **le** serveur web le plus utilisé au monde.
|
|
||||||
Au delà d'être d'être installé par défaut avec toutes les solutions type Wamp, Xampp, Mamp, etc., on utilise régulièrement Apache sur de vrais serveurs.
|
|
||||||
|
|
||||||
Sa configuration est un peu plus verbeuse que celle de Nginx, mais si ça t'intéresse de découvrir Apache : je t'invite à consulter la [documentation officielle](https://httpd.apache.org/docs/2.4/).
|
|
||||||
|
|
||||||
## ❓ Les autres solutions
|
|
||||||
|
|
||||||
Il existe d'autres solutions pour déployer une application web, mais je pense que tu as déjà pas mal de lecture pour aujourd'hui.
|
|
||||||
|
|
||||||
Par contre, il y a **une solution** que je te recommande de ne **jamais utiliser** : les serveurs FTP.
|
|
||||||
Alors oui, c'est pratique, c'est simple, mais on attend de toi une approche de développeur !
|
|
||||||
|
|
||||||
Tu as de nombreux outils spécialement conçus pour le déploiement, l'automatisation, la gestion de versions, etc.
|
|
||||||
C'est ton rôle d'utiliser ces outils pour déployer ton projet, et non pas de faire du simple drag-n-drop de fichiers via FTP et attendre 45 minutes pour transférer un dossier de 10 Mo _(oui, j'ai déjà vu ça et même bien pire que ça quand on se souvient qu'on a des node_modules ou les dépendances PHP dans le dossier vendor 🤡)_.
|
|
||||||
|
|
||||||
## 🎯 Critères d'évaluation
|
|
||||||
|
|
||||||
- La procédure de déploiement est rédigée ou mise à jour
|
|
||||||
- Les scripts de déploiement sont écrits et documentés
|
|
||||||
- Le système de veille permet de suivre les évolutions technologiques et les problématiques de sécurité liées au déploiement d'une application dynamique web ou web mobile, y compris dans le cadre d'une démarche DevOps
|
|
||||||
|
|
||||||
## 🧠 Documentations
|
|
||||||
|
|
||||||
- [Nginx](https://nginx.org/en/docs/)
|
|
||||||
- [Caddy](https://caddyserver.com/docs/)
|
|
||||||
- [Apache](https://httpd.apache.org/docs/2.4/)
|
|
||||||
- [Let's Encrypt](https://letsencrypt.org/docs/)
|
|
||||||
|
|
||||||
## 🛠️ Outils
|
|
||||||
|
|
||||||
- [Infomaniak](https://www.infomaniak.com/fr)
|
|
||||||
- [Vercel](https://vercel.com/)
|
|
||||||
- [Netlify](https://www.netlify.com/)
|
|
||||||
- [Heroku](https://www.heroku.com/)
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: Activité Type 2 - Développer la partie back-end d'une application web ou web mobile sécurisée
|
|
||||||
description: Synthèse et explications des attentes relatives à l'activité type 2 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 _(diagrammes, documentation etc.)_ et au développement de la logique métier côté serveur.
|
|
||||||
|
|
||||||
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 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
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
---
|
|
||||||
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)_
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
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 %}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
title: Influenceurs et créateurs de contenu
|
|
||||||
description: Liste d'influenceurs et créateurs de contenu dans le domaine du développement.
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
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 !
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## ❤️ Coups de coeur de Memento Dev
|
|
||||||
|
|
||||||
- [Alsa Créations](https://www.alsacreations.com/)
|
|
||||||
- [Benjamin Code](https://bento.me/benjamincode)
|
|
||||||
- [Code Garage](https://code-garage.com/)
|
|
||||||
- [Kipdev](https://kipdev.io/)
|
|
||||||
- [Korben](https://korben.info/)
|
|
||||||
|
|
||||||
## 🌟 Influenceurs et créateurs de contenu inspirants
|
|
||||||
|
|
||||||
- [laConsole](https://laconsole.dev/)
|
|
||||||
|
|
||||||
## 🫶 Apparaître sur cette page
|
|
||||||
|
|
||||||
Alors tout d'abord : merci pour ton implication dans la communauté tech !
|
|
||||||
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%20des%20influenceurs%20Memento%20Dev).
|
|
||||||
|
|
||||||
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" %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🥷 Disparaître de cette page
|
|
||||||
|
|
||||||
Pour retirer ton nom de cette page, même fonctionnement que pour apparaître sur cette page : tu peux m'envoyer un [email _(gauthier@gauthierdaniels.fr)_](mailto:gauthier@gauthierdaniels?subject=Demande%20de%20suppression%20de%20la%20page%20des%20influenceurs%20Memento%20Dev).
|
|
||||||
|
|
||||||
Toutefois, nous t'informons que l'historique des modifications est conservé sur le repository Gitea, et que nous ne pourrons pas supprimer les anciennes contributions.
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
title: Partages et réutilisations du contenu de Memento Dev
|
|
||||||
description: "Partagez et réutilisez le contenu de Memento Dev : Exportez, collaborez, intégrez !"
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
|
|
||||||
Tu souhaites partager ou réutiliser le contenu de Memento Dev ?
|
|
||||||
Tout d'abord, permet-moi de te remercier pour ton intérêt ! 🙏
|
|
||||||
|
|
||||||
L'entièreté du contenu de Memento Dev est disponible publiquement et est sous licence **CC BY-NC-SA 4.0**.
|
|
||||||
Cela signifie que tu peux le partager et le réutiliser, tant que tu respectes les conditions de la licence.
|
|
||||||
|
|
||||||
## Conditions de la licence
|
|
||||||
|
|
||||||
La licence **CC BY-NC-SA 4.0** impose les conditions suivantes :
|
|
||||||
|
|
||||||
- **Attribution** : Tu dois donner le crédit approprié, fournir un lien vers la licence et indiquer si des modifications ont été apportées.
|
|
||||||
Tu peux le faire de la manière suivante :
|
|
||||||
- En ajoutant un lien vers la page d'accueil de Memento Dev : [https://memento-dev.fr](https://memento-dev.fr)
|
|
||||||
- En ajoutant un lien vers la licence : [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
|
||||||
- **Pas d'utilisation commerciale** : Tu ne peux pas utiliser le matériel à des fins commerciales.
|
|
||||||
|
|
||||||
## Écoles, entreprises et utilisateurs réutilisant le contenu de Memento Dev
|
|
||||||
|
|
||||||
Un grand merci à ces entités qui utilisent le contenu de Memento Dev pour leurs formations ou leurs projets ! 🙏
|
|
||||||
|
|
||||||
- [Coda](https://coda.school)
|
|
||||||
- [O'clock](https://oclock.io)
|
|
||||||
- [Wild Code School](https://wildcodeschool.com)
|
|
||||||
|
|
||||||
{% 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 %}
|
|
||||||
@ -1,111 +0,0 @@
|
|||||||
---
|
|
||||||
title: Dictionnaire de Données Merise
|
|
||||||
description: Explorez le dictionnaire de données dans Merise, essentiel pour structurer et documenter les informations de votre système.
|
|
||||||
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
|
|
||||||
---
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
En gros : Le dictionnaire de données se veut **non technique** et **compréhensible par le client**.
|
|
||||||
|
|
||||||
Pour cet article et les suivants, on va se reposer sur une mise en situation fictive pour un contexte d'organisation d'un groupe de musique. _(Rassure-toi, pas besoin d'être musicien pour comprendre !)_
|
|
||||||
|
|
||||||
## Brief avec le client
|
|
||||||
|
|
||||||
Le client est un groupe qui doit faciliter l'organisation interne pour la gestion de ses différentes activités (répétitions, concerts, etc.).
|
|
||||||
Il souhaite donc créer une application pour gérer les informations suivantes :
|
|
||||||
|
|
||||||
- **Musicien** : Nom, prénom, instruments, adresse e-mail, mot de passe
|
|
||||||
- **Concert** : Date et heure, lieu, tarif
|
|
||||||
- **Répétition** : Date et heure, lieu
|
|
||||||
|
|
||||||
Le client a aussi précisé que le mot de passe doit faire plus de 12 caractères, et que l'adresse e-mail doit être unique.
|
|
||||||
|
|
||||||
En ce qui concerne les répétitions et les concerts, il nous a indiqué que tous les musiciens ne sont pas forcément présents à chaque répétition ou concert.
|
|
||||||
Il faut donc prévoir un moyen pour savoir qui est présent à chaque répétition et concert.
|
|
||||||
|
|
||||||
Pour le moment, on ne va pas se soucier de la technique, mais juste de lister les données que l'on doit stocker dans la base de données.
|
|
||||||
On va donc créer un tableau qui va nous permettre de lister toutes les données que l'on doit stocker dans notre base de données.
|
|
||||||
|
|
||||||
## Informations du dictionnaire de données
|
|
||||||
|
|
||||||
Avant de créer notre dictionnaire de données, regardons un peu ce qu'on peut y mettre :
|
|
||||||
|
|
||||||
- **Nom** : Nom de la donnée que l'on va stocker dans la base de données
|
|
||||||
- **Format** : Format de la donnée que l'on va stocker dans la base de données _(on y revient juste après !)_
|
|
||||||
- **Longueur** : Longueur de la donnée que l'on va stocker dans la base de données
|
|
||||||
- **Contraintes** : Contraintes sur la donnée que l'on va stocker dans la base de données
|
|
||||||
- **Document** : Document qui contient la donnée que l'on va stocker dans la base de données, un "groupe de données"
|
|
||||||
|
|
||||||
### Les différents formats
|
|
||||||
|
|
||||||
Rappelons-nous que le dictionnaire de données doit rester compréhensible par le client. Du moins, dans un premier temps.
|
|
||||||
Rien ne nous empêche de le rendre technique par la suite, cependant comme nous sommes en phase de conception : on doit rester simple.
|
|
||||||
|
|
||||||
Voici les différents formats que l'on peut utiliser dans le dictionnaire de données :
|
|
||||||
|
|
||||||
- **Alphabétique** _(Chaîne de caractères, uniquement A-Z)_
|
|
||||||
- **Alphanumérique** _(Chaîne de caractères, A-Z et 0-9)_
|
|
||||||
- **Numérique** _(Entier/flottant etc)_
|
|
||||||
- **Date**
|
|
||||||
- **Logique** _(Vrai/Faux)_
|
|
||||||
|
|
||||||
Et c'est tout ! On ne parle surtout pas de types de données techniques comme `VARCHAR`, `INT`, `FLOAT`, etc.
|
|
||||||
Le client n'a pas besoin de savoir ce que c'est, et on ne va pas lui en parler _(et s'il est vraiment curieux, il pourra consulter le futur **MPD**)_.
|
|
||||||
|
|
||||||
### Contraintes
|
|
||||||
|
|
||||||
Pour les contraintes, on reprendra les informations que l'on a récupérées dans le brief avec le client.
|
|
||||||
Si le client nous dit qu'une certaine donnée est obligatoire, on peut l'indiquer dans le dictionnaire de données. De même pour les valeurs par défaut, les valeurs uniques, etc.
|
|
||||||
|
|
||||||
Cependant, on n'ira pas plus loin sur ce terrain pour maintenir une compréhension simple par le client ! Que je ne te surprenne pas à lui dire :
|
|
||||||
|
|
||||||
> Alors là j'ai mis des ID en AUTO_INCREMENT, des clés primaires et étrangères, et j'ai mis des contraintes d'unicité sur les colonnes !
|
|
||||||
|
|
||||||
Tu risques de retrouver ton client en train de convulser sur le sol : **pas glop**.
|
|
||||||
|
|
||||||
## Notre dictionnaire de données
|
|
||||||
|
|
||||||
Voici donc le dictionnaire de données que l'on va créer pour notre application :
|
|
||||||
|
|
||||||
| 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 !
|
|
||||||
|
|
||||||
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" %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
Bien entendu, une date ne peut pas avoir de longueur, on a donc mis un tiret pour indiquer que ce n'est pas applicable.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Voilà, on a créé notre dictionnaire de données pour notre application de gestion de groupe de musique.
|
|
||||||
|
|
||||||
Pour le moment ça ne nous permet pas de créer notre base de données, mais au moins on a une bonne idée de ce que l'on doit stocker dans la base de données !
|
|
||||||
|
|
||||||
Pour la suite, on va se pencher sur le **MCD** _(Modèle Conceptuel de Données)_ qui va nous permettre de modéliser les données que l'on vient de récupérer et formaliser.
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
---
|
|
||||||
title: Modèle Conceptuel de Données (MCD) Merise
|
|
||||||
description: Comprenez le MCD dans Merise, une étape clé pour représenter les données de manière abstraite et cohérente.
|
|
||||||
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
|
|
||||||
---
|
|
||||||
|
|
||||||
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 ?
|
|
||||||
|
|
||||||
## Qu'est-ce que le MCD ?
|
|
||||||
|
|
||||||
Le **MCD** est un schéma qui va nous permettre de représenter les données que l'on a récupérées dans le dictionnaire de données.
|
|
||||||
|
|
||||||
Il va nous permettre de représenter les différentes données que l'on a, regroupée dans un rectangle nommé **entité**, ainsi que les relations entre elles.
|
|
||||||
On devra également indiquer les **cardinalités** de chaque relation entre les **entités**.
|
|
||||||
|
|
||||||
Tout comme le dictionnaire de données, ce schéma doit rester compréhensible par le client.
|
|
||||||
Il doit donc être le plus simple possible, et ne pas contenir de détails techniques.
|
|
||||||
|
|
||||||
Pour ce schéma _(ainsi que les suivants)_, on va utiliser le logiciel **Looping**.
|
|
||||||
|
|
||||||
## Définitions
|
|
||||||
|
|
||||||
Tu l'auras remarqué, ici on ne parle pas de "table" ou de "colonne".
|
|
||||||
On va exploiter d'autres termes comme **entité**, **attribut** ou **relation**.
|
|
||||||
|
|
||||||
Voici un petit lexique pour t'aider à comprendre :
|
|
||||||
|
|
||||||
| 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 !
|
|
||||||
|
|
||||||
## Exemple de MCD
|
|
||||||
|
|
||||||
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 :
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
On a ici un MCD qui représente trois **entités** :
|
|
||||||
|
|
||||||
- **Entité 1**
|
|
||||||
- **Entité 2**
|
|
||||||
- **Entité 3**
|
|
||||||
|
|
||||||
Chacune de ces entités a plusieurs **attributs** qui lui sont propres :
|
|
||||||
|
|
||||||
- **Entité 1** : code identité 1, attribut 2, attribut 3
|
|
||||||
- **Entité 2** : code identité 2, 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é ?" %}
|
|
||||||
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)_.
|
|
||||||
|
|
||||||
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**.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
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 :
|
|
||||||
|
|
||||||
- Une bulle ovale ou arrondie contenant un verbe à l'infinitif _(par exemple : "posséder")_
|
|
||||||
- Des **cardinalités** qui vont indiquer le nombre d'occurrences d'une entité par rapport à une autre.
|
|
||||||
|
|
||||||
Ici, on a :
|
|
||||||
|
|
||||||
- **Entité 1** 0,N - Contenir - 0,N **Entité 2**
|
|
||||||
- **Entité 1** 1,1 - Posséder - 0,N **Entité 3**
|
|
||||||
|
|
||||||
Mais qu'est-ce que ça veut dire tout ça ?
|
|
||||||
|
|
||||||
## Les cardinalités
|
|
||||||
|
|
||||||
Les cardinalités sont un élément essentiel du MCD.
|
|
||||||
Elles vont nous permettre de définir le nombre d'occurrences d'une entité par rapport à une autre.
|
|
||||||
|
|
||||||
Une cardinalité est définie par deux valeurs :
|
|
||||||
|
|
||||||
1. Le **minimum** d'occurrences
|
|
||||||
2. Le **maximum** d'occurrences
|
|
||||||
|
|
||||||
On va donc avoir des cardinalités de la forme : **X,Y**.
|
|
||||||
|
|
||||||
Toujours dans l'exemple précédent, on comprend donc que :
|
|
||||||
|
|
||||||
- **Entité 1** peut contenir entre 0 et N **Entité 2**
|
|
||||||
- **Entité 2** peut être contenue entre 0 et N **Entité 1**
|
|
||||||
- **Entité 1** doit posséder 1 et 1 seule **Entité 3**
|
|
||||||
- **Entité 3** peut être possédée entre 0 et N **Entité 1**
|
|
||||||
|
|
||||||
{% callout type="note" title="Les différentes valeurs" %}
|
|
||||||
La plupart du temps, nous allons retrouver les valeurs suivantes :
|
|
||||||
|
|
||||||
- **0**
|
|
||||||
- **1**
|
|
||||||
- **N**
|
|
||||||
|
|
||||||
**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**.
|
|
||||||
|
|
||||||
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**.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## Retour sur notre dictionnaire de données
|
|
||||||
|
|
||||||
Maintenant que l'on sait comment fonctionne un MCD, on va pouvoir retourner sur notre dictionnaire de données pour le formaliser en MCD.
|
|
||||||
|
|
||||||
Pour rappel, voici notre dictionnaire de données :
|
|
||||||
|
|
||||||
| 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
|
|
||||||
|
|
||||||
On va donc créer trois entités :
|
|
||||||
|
|
||||||
- **Musicien**
|
|
||||||
- **Concert**
|
|
||||||
- **Répétition**
|
|
||||||
|
|
||||||
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 est déjà pas trop mal, il nous reste plus qu'à ajouter les relations entre les entités et les cardinalités !
|
|
||||||
|
|
||||||
### Les relations
|
|
||||||
|
|
||||||
On va donc ajouter les relations entre les entités.
|
|
||||||
|
|
||||||
Sachant qu'un musicien peut participer à plusieurs concerts, et qu'un concert peut avoir plusieurs musiciens, on va créer une relation entre les deux entités.
|
|
||||||
On va donc créer une relation **"Participer"** entre les entités **Musicien** et **Concert**.
|
|
||||||
|
|
||||||
On en fera une relation **0,N** - **1,N**.
|
|
||||||
|
|
||||||
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**.
|
|
||||||
|
|
||||||
À la fin, on se retrouve avec un MCD qui ressemble à ça :
|
|
||||||

|
|
||||||
|
|
||||||
Et c'est tout ! Notre MCD est terminé... enfin presque !
|
|
||||||
|
|
||||||
### Aller plus loin
|
|
||||||
|
|
||||||
Si on souhaite aller plus loin, on peut ajouter de 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**)_.
|
|
||||||
|
|
||||||
Les entités qui héritent de l'entité générique sont appelées **entités spécialisées** _(ou **sous-types**)_.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
En regardant bien notre MCD, on se rend compte que les entités **Concert** et **Répétition** ont des attributs communs :
|
|
||||||
|
|
||||||
- Date et heure
|
|
||||||
- Lieu
|
|
||||||
|
|
||||||
La seule différence est que le concert a un tarif, contrairement à la répétition.
|
|
||||||
On va donc pouvoir créer une entité **générique** que l'on appelera **Événement**.
|
|
||||||
|
|
||||||
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**)_ :
|
|
||||||

|
|
||||||
|
|
||||||
{% 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** !
|
|
||||||
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.
|
|
||||||
|
|
||||||
On renforce ainsi l'intégrité de la base de données.
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
En conclusion, le MCD dans Merise est un outil indispensable pour structurer et visualiser les données de manière claire et cohérente.
|
|
||||||
|
|
||||||
Grâce à des entités, des attributs, des relations et des cardinalités, le MCD permet de représenter les informations de façon abstraite, tout en restant compréhensible pour le client.
|
|
||||||
|
|
||||||
Prochaine étape : le **MLD** _(Modèle Logique de Données)_ et le **MRD** _(Modèle Relationnel de Données)_ !
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Modèle Logique/Relationnel de Données (MLD/MRD) Merise
|
|
||||||
description: Plongez dans le MLD et MRD de Merise pour transformer votre modèle conceptuel en une structure relationnelle optimisée.
|
|
||||||
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
|
|
||||||
---
|
|
||||||
|
|
||||||
En cours de rédaction...
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Modèle Physique de Données (MPD) Merise
|
|
||||||
description: Apprenez à créer le MPD dans Merise, la dernière étape pour concrétiser votre base de données sur un SGBD.
|
|
||||||
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
|
|
||||||
---
|
|
||||||
|
|
||||||
En cours de rédaction...
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
---
|
|
||||||
title: Introduction à Merise
|
|
||||||
description: Parlons un peu de Merise, la fameuse méthodologie de modélisation pour la conception de bases de données.
|
|
||||||
tags: [Backend, Merise, BDD, MCD, MLD, MPD, SQL]
|
|
||||||
---
|
|
||||||
|
|
||||||
**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**.
|
|
||||||
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" %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% /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**.
|
|
||||||
|
|
||||||
## Les différentes schémas de Merise
|
|
||||||
|
|
||||||
Merise se compose de plusieurs schémas qui permettent de représenter les données et leurs relations.
|
|
||||||
|
|
||||||
1. **Dictionnaire de données**
|
|
||||||
2. **MCD** _(Modèle Conceptuel de Données)_
|
|
||||||
3. **MLD** _(Modèle Logique de Données)_
|
|
||||||
4. **MRD** _(Modèle Relationnel de Données)_
|
|
||||||
5. **MPD** _(Modèle Physique de Données)_
|
|
||||||
|
|
||||||
Voyons un peu plus en détail chacun de ces schémas, et comment ils s'articulent entre eux.
|
|
||||||
Des fiches détaillées seront individuellement rédigées pour chaque schéma, mais ici on va juste faire un petit tour d'horizon pour resituer le contexte.
|
|
||||||
|
|
||||||
### Dictionnaire de données
|
|
||||||
|
|
||||||
Le **dictionnaire de données** est un tableau qui va nous permettre de lister toutes les données que l'on doit stocker dans notre base de données.
|
|
||||||
Ces données proviendront de l'analyse des besoins du client, avec qui on aura discuté de ce qu'il souhaite faire avec son application.
|
|
||||||
|
|
||||||
Ce document doit être le plus simple possible et ne pas contenir de détails techniques.
|
|
||||||
Il doit être compréhensible par le client, qui pourra ainsi valider les données que l'on va stocker dans la base de données.
|
|
||||||
|
|
||||||
### MCD
|
|
||||||
|
|
||||||
Le **MCD** _(Modèle Conceptuel de Données)_ est un schéma qui va nous permettre de représenter les données que l'on a récupérées dans le dictionnaire de données.
|
|
||||||
Il va nous permettre de représenter les différentes données que l'on a, regroupée dans un rectangle nommé **entité**, ainsi que les relations entre elles.
|
|
||||||
|
|
||||||
Ce document, tout comme le dictionnaire de données, doit rester compréhensible par le client en évitant les détails techniques.
|
|
||||||
|
|
||||||
### MLD
|
|
||||||
|
|
||||||
Le **MLD** _(Modèle Logique de Données)_ est un schéma qui se base directement sur le MCD.
|
|
||||||
Il vient ajouter des détails techniques sur les entités et les relations, comme par exemple :
|
|
||||||
|
|
||||||
- Les tables et leurs colonnes _(sans les types de données)_
|
|
||||||
- Les clés primaires et étrangères
|
|
||||||
- Les tables de jointure _(tables pivot, de liaison, etc.)_
|
|
||||||
|
|
||||||
### MRD
|
|
||||||
|
|
||||||
Le **MRD** _(Modèle Relationnel de Données)_ est un schéma qui est une version textuelle du MLD.
|
|
||||||
Il s'agit d'une "fausse" étape, car le MRD n'est pas un schéma à proprement parler.
|
|
||||||
|
|
||||||
La plupart du temps, on parle de MLD textuel puisqu'il s'agit de la même chose.
|
|
||||||
|
|
||||||
### MPD
|
|
||||||
|
|
||||||
Pour terminer : le **MPD** _(Modèle Physique de Données)_ !
|
|
||||||
|
|
||||||
Le MPD est un schéma qui va nous permettre de représenter les données de manière physique, c'est-à-dire en tenant compte des spécificités du SGBD _(Système de Gestion de Base de Données)_ que l'on va utiliser.
|
|
||||||
Il va nous permettre de représenter les tables, les colonnes, les types de données, les index, les contraintes d'intégrité, etc.
|
|
||||||
|
|
||||||
Le MPD est donc un schéma qui est spécifique à un SGBD, et qui ne peut pas être utilisé tel quel sur un autre SGBD.
|
|
||||||
|
|
||||||
## Outils pour Merise
|
|
||||||
|
|
||||||
Il existe de nombreux outils pour réaliser des MCD, MLD et MPD.
|
|
||||||
|
|
||||||
Déjà, tu peux **bannir** tous les outils qui ne sont pas conçus pour Merise.
|
|
||||||
Merise étant français, la plupart des outils internationaux ne sont pas adaptés.
|
|
||||||
|
|
||||||
### Outils non recommandés
|
|
||||||
|
|
||||||
- **Draw.io** : Bien qu'il soit un bon outil, il n'est pas conçu pour Merise et ne respecte pas les normes de la méthodologie.
|
|
||||||
- **Lucidchart** : Même chose que Draw.io, il n'est pas conçu pour Merise.
|
|
||||||
- **DBDesigner** : Excellent outil, mais qui ne permet pas la réalisation d'un MCD ou d'un MLD correct. On pourra par contre l'utiliser pour le MPD !
|
|
||||||
|
|
||||||
### Outils recommandés
|
|
||||||
|
|
||||||
- **[Looping](https://looping-mcd.fr/)** : Il s'agit de l'outil par **excellence** pour la conception de bases de données tout en respectant la méthodologie Merise. Cerise sur le gâteau, il est **gratuit** !
|
|
||||||
- **[Mocodo](https://mocodo.net/)** : Un autre outil qui permet de réaliser des MCD et MLD. Il est extrêmement puissant et fonctionne sur le navigateur. Cependant, il manque de rigueur là où Looping excelle.
|
|
||||||
|
|
||||||
Pour toutes les rubriques suivantes, **Looping** sera utilisé comme outil.
|
|
||||||
|
|
||||||
## Ressources
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
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/)
|
|
||||||
- [Mocodo](https://mocodo.net/)
|
|
||||||
- [La vérité sur les id - Jean Prulière](https://jeanpruliere.medium.com/la-v%C3%A9rit%C3%A9-sur-les-id-507134adda12))
|
|
||||||
- [Merise - Wikipedia](<https://fr.wikipedia.org/wiki/Merise_(informatique)>)
|
|
||||||
|
|
||||||
Prochaine étape, on parle du **dictionnaire de données** !
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
---
|
|
||||||
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 ! 😄
|
|
||||||
@ -1,364 +0,0 @@
|
|||||||
---
|
|
||||||
title: Les hooks de React
|
|
||||||
description: Découvre les hooks de React, une fonctionnalité qui te permet de gérer le state et le cycle de vie de tes composants fonctionnels.
|
|
||||||
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
|
|
||||||
---
|
|
||||||
|
|
||||||
Ç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.
|
|
||||||
|
|
||||||
## 🎣 Qu'est-ce qu'un hook ?
|
|
||||||
|
|
||||||
Tu te souviens du charabia dans l'article précédent ?
|
|
||||||
|
|
||||||
> Un **hook** en React est une fonction qui permet d'exploiter les fonctionnalités de React dans un composant fonctionnel _(fonction)_.
|
|
||||||
|
|
||||||
Essayons de comprendre cette phrase un peu plus en détail en prenant l'origine des composants React.
|
|
||||||
|
|
||||||
Historiquement, on utilisait des **classes** pour déclarer des composants React plutôt que des **fonctions**.
|
|
||||||
C'était pas mal, mais ça devenait vite compliqué à gérer, notamment pour partager de la logique entre plusieurs composants _(comme le state par exemple)_.
|
|
||||||
|
|
||||||
Pour te donner un aperçu, voici à quoi ressemblait un composant de classe avec les trois étapes du cycle de vie :
|
|
||||||
|
|
||||||
{% 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 :
|
|
||||||
|
|
||||||
> C'est pas téros 😕
|
|
||||||
|
|
||||||
Mais si tu as bien fait attention, tu as pu remarquer trois méthodes _(on ne prend pas le constructor en compte)_ qui sont appelées à des moments précis du cycle de vie du composant :
|
|
||||||
|
|
||||||
- `componentDidMount` : appelée après le premier rendu du composant
|
|
||||||
- `componentDidUpdate` : appelée après chaque mise à jour du composant
|
|
||||||
- `componentWillUnmount` : appelée avant la suppression du composant
|
|
||||||
|
|
||||||
Seulement, comment on peut faire pour gérer ces étapes avec des composants fonctionnels ?
|
|
||||||
Et bien c'est là qu'interviennent les **hooks** !
|
|
||||||
|
|
||||||
Avant de te montrer **comment** reproduire ces étapes avec des **hooks**, voici les **principaux hooks** de base que tu vas **très souvent** utiliser :
|
|
||||||
|
|
||||||
- `useState` : pour déclarer un **state**
|
|
||||||
- `useEffect` : pour gérer le **cycle de vie** d'un composant _(on en parle juste après !)_
|
|
||||||
|
|
||||||
Il en existe d'autres bien entendu, mais ces deux-là sont les plus utilisés.
|
|
||||||
On reviendra sur les autres hooks "basiques" un peu plus tard 😉
|
|
||||||
|
|
||||||
## useEffect, ou la machinerie du cycle de vie
|
|
||||||
|
|
||||||
Ce hook.. il est **tellement** puissant !
|
|
||||||
Mais il est surtout très mal compris par les débutants _(et même parfois par les confirmés)_.
|
|
||||||
|
|
||||||
Il faut dire que Facebook n'a pas aidé en le nommant `useEffect`, surtout que tu vas voir : c'est un couteau suisse ce machin 😅
|
|
||||||
|
|
||||||
Pour faire court : `useEffect` permet de gérer le cycle de vie d'un composant fonctionnel _(comme `componentDidMount`, `componentDidUpdate` et `componentWillUnmount` pour les composants de classe)_.
|
|
||||||
|
|
||||||
Oui. Il fait tout. Tout seul.
|
|
||||||
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 😇
|
|
||||||
|
|
||||||
{% 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 ? 😂
|
|
||||||
Et bien dans ces exemples, on a trois manières d'écrire un useEffect :
|
|
||||||
|
|
||||||
1. Le hook est exécuté une seule fois, après le premier rendu 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é
|
|
||||||
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Le premier argument lui, est une fonction synchrone _(pas le droit de la rendre asynchrone !)_. On mettra dedans tout ce qu'on veut exécuter lors de l'appel du hook.
|
|
||||||
|
|
||||||
Le deuxième argument est un tableau de dépendances.
|
|
||||||
Selon ce tableau, le hook sera exécuté à des moments différents du cycle de vie du composant.
|
|
||||||
|
|
||||||
### ⚙️ ComponentDidMount
|
|
||||||
|
|
||||||
```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.
|
|
||||||
On peut donc dire que c'est l'équivalent de `componentDidMount` pour les composants de classe.
|
|
||||||
|
|
||||||
### 🔧 ComponentDidUpdate
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
React.useEffect(() => {
|
|
||||||
// ...
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
React.useEffect(() => {
|
|
||||||
// ...
|
|
||||||
}, [props.uneProp]);
|
|
||||||
```
|
|
||||||
|
|
||||||
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é.
|
|
||||||
|
|
||||||
### 🗑️ ComponentWillUnmount
|
|
||||||
|
|
||||||
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 😏
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
React.useEffect(() => {
|
|
||||||
// ...
|
|
||||||
return () => {
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
```
|
|
||||||
|
|
||||||
Tu as vu ce petit `return` ? Et bien, c'est notre équivalent de `componentWillUnmount` pour les composants de classe !
|
|
||||||
|
|
||||||
Dans cette fonction de retour, on mettra tout ce qu'on veut exécuter avant la suppression du composant.
|
|
||||||
|
|
||||||
### 📦 Les dépendances
|
|
||||||
|
|
||||||
Tu as pu voir que le deuxième argument de `useEffect` est un tableau de dépendances.
|
|
||||||
Ce tableau est très important, car il permet de gérer les mises à jour du composant.
|
|
||||||
|
|
||||||
L'idée ici, c'est **d'optimiser** le rendu du composant en évitant de déclencher des mises à jour inutiles.
|
|
||||||
Pour éviter que React se dise "Tiens, il y a eu un changement, je vais re-rendre le composant", on va lui dire "Non, non, il n'y a pas eu de changement sur la prop que je t'ai fourni, tu peux rester tranquille".
|
|
||||||
|
|
||||||
### 🤓 Exemple concret
|
|
||||||
|
|
||||||
Allez, mettons un peu ce qu'on voit de voir en pratique !
|
|
||||||
|
|
||||||
{% tabs defaultSelectedTab="jsx" %}
|
|
||||||
|
|
||||||
{% 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 !
|
|
||||||
|
|
||||||
Et... stoooop !
|
|
||||||
On revient sur le cycle de vie d'un composant maintenant qu'on a vu `useEffect` en action !
|
|
||||||
|
|
||||||
Je vais te donner un exemple de code supplémentaire et tu vas devoir deviner l'ordre d'apparition des messages dans la console.
|
|
||||||
|
|
||||||
```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 :
|
|
||||||
|
|
||||||
{% 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
|
|
||||||
- **C** - 1, 2, 3, 4
|
|
||||||
- **D** - La réponse D
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🧩 Les autres hooks
|
|
||||||
|
|
||||||
On a vu les deux hooks les plus utilisés, mais il en existe d'autres qui peuvent être très utiles dans certaines situations.
|
|
||||||
|
|
||||||
Par exemple, on a :
|
|
||||||
|
|
||||||
- `useContext` : pour accéder à un contexte
|
|
||||||
- `useReducer` : pour gérer un state complexe
|
|
||||||
- `useCallback` : pour éviter les re-rendus inutiles
|
|
||||||
- `useMemo` : pour éviter les calculs inutiles
|
|
||||||
- `useRef` : pour accéder à un élément du DOM
|
|
||||||
|
|
||||||
Ne t'inquiète pas, on va les voir plus tard car le hook `useEffect` est déjà bien assez complexe à comprendre pour le moment 😅
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Et voilà, tu as maintenant toutes les clés en main pour gérer le cycle de vie de tes composants fonctionnels avec les hooks de React !
|
|
||||||
|
|
||||||
Vraiment, même si les autres hooks restent importants _(voire obligatoires dans certains contextes)_, tu as déjà de quoi faire de bons composants avec seulement ces deux là 😁
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
---
|
|
||||||
title: Initialisation d'un projet React
|
|
||||||
description: Initialisons un nouveau projet React, avec ou sans TypeScript.
|
|
||||||
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI), Vite, Vike]
|
|
||||||
---
|
|
||||||
|
|
||||||
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.
|
|
||||||
Tu vas voir à quel point c'est simple !
|
|
||||||
|
|
||||||
## 👴 Ancienne méthode _(CRA)_
|
|
||||||
|
|
||||||
{% 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**.
|
|
||||||
Ne l'utilise donc pas pour créer de nouveaux projets !
|
|
||||||
|
|
||||||
{% /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.
|
|
||||||
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.
|
|
||||||
|
|
||||||
Cependant, cette méthode est désormais dépréciée. Au delà d'être dépréciée, cette méthode a même été **totalement retirée** de la documentation officielle de React.
|
|
||||||
|
|
||||||
À la place, la documentation nous propose plutôt d'utiliser :
|
|
||||||
|
|
||||||
- [Next.js _(App Router)_](https://react.dev/learn/creating-a-react-app#nextjs-app-router)
|
|
||||||
- [React Router _(v7)_](https://react.dev/learn/creating-a-react-app#react-router-v7)
|
|
||||||
- [Expo _(pour les applications natives)_](https://react.dev/learn/creating-a-react-app#expo)
|
|
||||||
- [Vite](https://react.dev/learn/build-a-react-app-from-scratch#vite)
|
|
||||||
- [Parcel](https://react.dev/learn/build-a-react-app-from-scratch#parcel)
|
|
||||||
- [Rsbuild](https://react.dev/learn/build-a-react-app-from-scratch#rsbuild)
|
|
||||||
|
|
||||||
Mais dans cet article, je vais plutôt te montrer comment initialiser un projet React avec Vite et par la suite : [Vike](https://vike.dev/).
|
|
||||||
|
|
||||||
Et si tu te demandes "pourquoi" _(à juste titre !)_, c'est tout simplement car il s'agit d'une solution plus légère, plus flexible et qui est en constante amélioration par la communauté !
|
|
||||||
|
|
||||||
## 🏃 Vite
|
|
||||||
|
|
||||||
Vite est un outil de développement web _(frontend)_ créé par [Evan You](https://evanyou.me/), le créateur de Vue.js.
|
|
||||||
Et si tu connais Vue.js, tu sais déjà qu'on parle d'un outil de qualité !
|
|
||||||
|
|
||||||
L'objectif principal derrière Vite, c'est d'améliorer considérablement la rapidité de développement de nos applications web, avec notamment un serveur de développement ultra-rapide, et une compilation de production optimisée.
|
|
||||||
|
|
||||||
On peut notamment comparer Vite à Webpack, Grunt ou encore Gulp, mais en beaucoup plus rapide et plus simple à utiliser.
|
|
||||||
Beaucoup moins "usine à gaz", beaucoup plus "plug-and-play".
|
|
||||||
|
|
||||||
Et si on voyait comment initialiser un projet React avec Vite ?
|
|
||||||
C'est parti ! 🚀
|
|
||||||
|
|
||||||
## 👷 Initialisation
|
|
||||||
|
|
||||||
Avant de commencer, assure-toi d'avoir Node.js installé sur ta machine.
|
|
||||||
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...
|
|
||||||
Ouvre ton terminal, et tape la commande suivante :
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
Je te laisse la liberté de répondre comme tu veux à ces questions, mais pour un projet React, tu vas devoir choisir le template `react` _(logique non ?)_.
|
|
||||||
|
|
||||||
Et voilà, c'est tout !
|
|
||||||
... oui j'ai menti, c'était vraiment pas difficile. 😅
|
|
||||||
|
|
||||||
## 🚀 Lancement
|
|
||||||
|
|
||||||
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 defaultSelectedTab="npm" %}
|
|
||||||
|
|
||||||
{% tab value="npm" label="NPM" %}
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mon-projet # Se rendre dans le dossier du projet
|
|
||||||
npm install # Installer les dépendances
|
|
||||||
npm run dev # Lancer le serveur de développement
|
|
||||||
```
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="yarn" label="Yarn" %}
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mon-projet # Se rendre dans le dossier du projet
|
|
||||||
yarn install # Installer les dépendances
|
|
||||||
yarn dev # Lancer le serveur de développement
|
|
||||||
```
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="pnpm" label="PNPM" %}
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd mon-projet # Se rendre dans le dossier du projet
|
|
||||||
pnpm install # Installer les dépendances
|
|
||||||
pnpm dev # Lancer le serveur de développement
|
|
||||||
```
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% /tabs %}
|
|
||||||
|
|
||||||
{% callout type="warning" title="Dépendances" %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
En général, il s'agit de http://localhost:4173 _(le port peut varier)_.
|
|
||||||
|
|
||||||
Et voilà, tu as maintenant un projet React initialisé avec Vite ! 🎉
|
|
||||||
|
|
||||||
Par contre je préfère te prévenir, avec cette solution on va davantage s'orienter sur des applications **CSR** _(Client-Side Rendering)_, mais on pourra également voir comment faire du **SSR** _(Server-Side Rendering)_ ou du **SSG** _(Static Site Generation)_ avec **Vike**.
|
|
||||||
|
|
||||||
## 💪 Vike
|
|
||||||
|
|
||||||
**Vike**, ou anciennement `vite-plugin-ssr`, est un **framework** basé sur Vite qui va nous permettre de créer des applications web avec presque n'importe quelle bibliothèque _(React, Vue, Svelte, etc.)_.
|
|
||||||
|
|
||||||
Dans notre cas, on s'en servira comme alternative au géant du SSR React : Next.js.
|
|
||||||
|
|
||||||
L'énorme avantage de Vike au delà de pouvoir utiliser n'importe quel framework/bibliothèque UI _(et même un framework maison ou du vanilla JS !)_, c'est que tout comme Vite, il est **extrêmement plus léger** et **flexible**.
|
|
||||||
|
|
||||||
Au niveau des fonctionnalités natives, on va retrouver un peu la même chose que Next.js, avec notamment :
|
|
||||||
|
|
||||||
- De quoi gérer le rendu à effectuer _(SSR, SSG, CSR)_
|
|
||||||
- Un système de layouts
|
|
||||||
- Un système de routage
|
|
||||||
- Un système pour séparer la logique front et la logique back
|
|
||||||
|
|
||||||
Et bien d'autres !
|
|
||||||
|
|
||||||
Mais on se réserve ça pour un autre article, car il est nécessaire de bien comprendre comment fonctionne React 😉.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Tu l'auras remarqué, initialiser un projet React avec Vite est vraiment très simple !
|
|
||||||
|
|
||||||
On parle ici d'une simple petite commande et de questions auxquelles répondre, et le tour est joué 🚀
|
|
||||||
@ -1,317 +0,0 @@
|
|||||||
---
|
|
||||||
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 !
|
|
||||||
@ -1,239 +0,0 @@
|
|||||||
---
|
|
||||||
title: Introduction à React
|
|
||||||
description: Parlons un peu de React, ce qu'il est, ce qu'il fait et pourquoi il est si populaire.
|
|
||||||
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
|
|
||||||
---
|
|
||||||
|
|
||||||
Parlons peu, parlons bien ! 😄
|
|
||||||
|
|
||||||
React est une **bibliothèque** _(non, pas un **framework** !)_ JavaScript open-source développée par Facebook.
|
|
||||||
Elle est utilisée pour construire des interfaces utilisateur _(UI)_ interactives et dynamiques.
|
|
||||||
|
|
||||||
{% callout type="note" title="Pourquoi React est si populaire ?" %}
|
|
||||||
|
|
||||||
- **Facilité d'utilisation** : React est facile à apprendre et à utiliser. Il est basé sur JavaScript, qui est l'un des langages de programmation les plus populaires.
|
|
||||||
- **Réutilisabilité des composants** : React permet de créer des composants réutilisables. Cela signifie que tu peux créer un composant une fois et l'utiliser partout où tu en as besoin.
|
|
||||||
- **Performances** : React utilise un DOM virtuel _(Virtual DOM)_ pour améliorer les performances de l'application.
|
|
||||||
- **Communauté active** : React a une communauté active de développeurs qui contribuent à son développement et partagent des ressources utiles.
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
Mais on peut aussi y noter des points faibles bien entendu, car tout n'est pas rose :
|
|
||||||
|
|
||||||
- **Courbe d'apprentissage** : Bien que React soit "facile" à apprendre, les concepts avancés demandent un peu de temps pour être maîtrisés.
|
|
||||||
- **Taille du bundle** : React est relativement lourd en termes de taille de bundle, ce qui peut affecter les performances de l'application en terme de chargement initial.
|
|
||||||
- **GAFAM** : Comme d'autres bibliothèques/frameworks, React n'échappe pas à la critique de la part de certains développeurs qui ne souhaitent pas utiliser des technologies développées par des géants du web.
|
|
||||||
|
|
||||||
## 🤔 Pourquoi une bibliothèque et pas un framework ?
|
|
||||||
|
|
||||||
Très grand débat que voilà ! Vraiment.. il y a des guerres qui se sont déclarées pour moins que ça 😅
|
|
||||||
|
|
||||||
Blague à part, pour pouvoir dire que React n'est pas un framework, il faut comprendre la différence entre les deux :
|
|
||||||
|
|
||||||
- **Framework** : Un framework est un ensemble de bibliothèques et de composants qui sont prédéfinis et structurés pour te permettre de construire une application.
|
|
||||||
En gros, le framework te dit comment faire les choses.
|
|
||||||
- **Bibliothèque** : Une bibliothèque est un ensemble de fonctions et de composants que tu peux utiliser pour construire une application.
|
|
||||||
En gros, c'est toi qui décides comment faire les choses.
|
|
||||||
|
|
||||||
Et si tu connais déjà React, je te vois venir avec tes grands sabots... !
|
|
||||||
|
|
||||||
{% callout type="note" title="React a ses propres règles, on ne peut pas faire n'importe quoi !" %}
|
|
||||||
|
|
||||||
C'est vrai ! React a ses propres règles et conventions, mais il te laisse quand même une grande liberté pour organiser ton code comme tu le souhaites.
|
|
||||||
|
|
||||||
Si on se concentre sur la **préoccupation principale** de React, c'est de gérer l'**interface utilisateur** _(UI)_ de ton application.
|
|
||||||
En aucun cas, React _(tel quel et "pour le moment")_ va te dire comment gérer ton état global, comment gérer tes requêtes HTTP, etc.
|
|
||||||
|
|
||||||
Mais tu peux totalement utiliser React **au sein** d'un framework !
|
|
||||||
Tu as notamment des frameworks comme [**Next.js**](https://nextjs.org/) ou [**Gatsby**](https://www.gatsbyjs.com/) qui utilisent React
|
|
||||||
avec des fonctionnalités supplémentaires pour gérer le routage, le rendu côté serveur, etc.
|
|
||||||
|
|
||||||
_(Le meilleur, selon moi, c'est [**Vike**](https://vike.dev/) qui te permet d'utiliser presque n'importe quelle bibliothèque avec une même architecture 😏)_
|
|
||||||
|
|
||||||
Mais concentrons-nous sur React en tant que bibliothèque, et non en tant que framework 😉
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 📝 JSX
|
|
||||||
|
|
||||||
Ce qui peut être déroutant au premier abord avec React, c'est le **JSX**.
|
|
||||||
On serait tenté de dire que c'est du HTML, mais en fait : **pas du tout** !
|
|
||||||
|
|
||||||
Le JSX est un sucre syntaxique _(syntactic sugar)_ qui permet d'écrire du code JavaScript en se basant sur le système de balisage HTML.
|
|
||||||
|
|
||||||
L'avantage de JSX c'est que le code devient beaucoup plus lisible et plus proche de ce que tu connais déjà avec HTML.
|
|
||||||
Mais il s'agit bien de JavaScript, et non de HTML !
|
|
||||||
|
|
||||||
## 🧩 Composants
|
|
||||||
|
|
||||||
React est basé sur le concept de **composants**. Un composant est une partie réutilisable de l'interface utilisateur _(UI)_ qui peut être affichée à l'écran.
|
|
||||||
|
|
||||||
Dans la majorité des cas, on va chercher à **mutualiser** les composants pour éviter de répéter du code inutilement.
|
|
||||||
L'exemple le plus flagrant sera par exemple tous les boutons de ton application, qui auront probablement tous la même apparence et le même comportement.
|
|
||||||
|
|
||||||
Il est aussi possible de les **imbriquer** les uns dans les autres !
|
|
||||||
En fait, on joue avec des Lego, mais en version code 👷
|
|
||||||
|
|
||||||
Mais si on veut vraiment montrer le potentiel de React, parlons maintenant... 🥁
|
|
||||||
Des **states**, **cycles de vie** et des **props** !
|
|
||||||
|
|
||||||
## 🛠️ States, Cycles de vie et Props
|
|
||||||
|
|
||||||
T'assomer aussi vite avec ces termes qui ne te parlent peut-être pas, c'est pas cool de ma part...
|
|
||||||
Pardon pour les gros mots, je me calme tout de suite ! 🙈
|
|
||||||
|
|
||||||
Si ça te rassure, je vais très rapidement évoquer ce qu'il se cache derrière ces termes barbares,
|
|
||||||
je réserve les détails pour des articles dédiés 😉
|
|
||||||
|
|
||||||
### 🗄️ States
|
|
||||||
|
|
||||||
... ou également appelés **états** en français.
|
|
||||||
|
|
||||||
Le but du state, c'est de stocker des données qui vont être **observées** par React.
|
|
||||||
À chaque fois que le state va être modifié, React va **réagir** et mettre à jour l'interface utilisateur _(UI)_ en conséquence afin d'afficher les nouvelles données.
|
|
||||||
|
|
||||||
### 🔄 Cycles de vie
|
|
||||||
|
|
||||||
Les **cycles de vie** _(lifecycle)_ sont des méthodes qui sont appelées à des moments précis dans le cycle de vie d'un composant React.
|
|
||||||
|
|
||||||
Si tu as lu la section qui parle brièvement des states, tu auras peut-être remarqué cette phrase :
|
|
||||||
|
|
||||||
> À chaque fois que le state va être modifié, React va **réagir** et mettre à jour l'interface utilisateur [...]
|
|
||||||
|
|
||||||
Et bien c'est là que les cycles de vie entrent en jeu !
|
|
||||||
Un composant sur React va avoir un cycle de vie, caractérisé par trois phases :
|
|
||||||
|
|
||||||
1. **Montage du composant** _(Mounting)_ : le composant est créé et inséré dans le DOM.
|
|
||||||
2. **Mise à jour du composant** _(Updating)_ : le composant est mis à jour en fonction des changements de state ou de props.
|
|
||||||
3. **Démontage du composant** _(Unmounting)_ : le composant est retiré du DOM.
|
|
||||||
|
|
||||||
Ces différentes phases vont nous permettre d'interagir avec le composant à des moments précis, et d'effectuer des actions en conséquence.
|
|
||||||
|
|
||||||
### 📦 Props
|
|
||||||
|
|
||||||
Et pour finir, les **props** _(properties ou tout simplement "propriétés" en français)_ !
|
|
||||||
|
|
||||||
Il s'agit ni plus ni moins que des **arguments** que tu vas passer à un composant, comme tu le ferais avec une fonction.
|
|
||||||
|
|
||||||
Cependant il faut noter une chose :
|
|
||||||
|
|
||||||
On transmet les props à un composant précis, qui sera donc un composant **enfant**.
|
|
||||||
Un composant enfant ne pourra pas transmettre des props à un composant parent, c'est unidirectionnel _(mais on verra comment on peut faire autrement 😉)_.
|
|
||||||
|
|
||||||
## 🖥️ Une petite démo ?
|
|
||||||
|
|
||||||
OK, mais vraiment petite !
|
|
||||||
|
|
||||||
Prenons l'exemple d'une application qui servira **uniquement** à afficher une liste de tâches _(une todolist donc !)_.
|
|
||||||
_(Bon... utiliser React uniquement pour ça c'est abusé, mais c'est pour l'exemple 😅)_
|
|
||||||
|
|
||||||
{% tabs defaultSelectedTab="demo-app" %}
|
|
||||||
|
|
||||||
{% tab value="demo-app" label="App.tsx" %}
|
|
||||||
|
|
||||||
```tsx showLineNumbers
|
|
||||||
import TodoList from "./TodoList";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>TodoList</h1>
|
|
||||||
|
|
||||||
<TodoList />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="demo-todolist" label="TodoList.tsx" %}
|
|
||||||
|
|
||||||
```tsx showLineNumbers
|
|
||||||
import TodoListItem from "./TodoListItem";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const TodoList = () => {
|
|
||||||
const [items, setItems] = React.useState<string[]>([]);
|
|
||||||
const [inputValue, setInputValue] = React.useState<string>("");
|
|
||||||
|
|
||||||
const handleInputValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setInputValue(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
// On empêche le comportement par défaut du formulaire
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// On ajoute un nouvel élément à la liste des tâches
|
|
||||||
setItems([...items, inputValue]);
|
|
||||||
|
|
||||||
// On réinitialise la valeur de l'input
|
|
||||||
setInputValue("");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<label htmlFor="todolist-input"></label>
|
|
||||||
|
|
||||||
<input id="todolist-input" type="text" value={inputValue} onChange={handleInputValueChange} />
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{items.map((item, index) => (
|
|
||||||
<li key={index}>
|
|
||||||
<TodoListItem item={item} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TodoList;
|
|
||||||
```
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="demo-todolistitem" label="TodoListItem.tsx" %}
|
|
||||||
|
|
||||||
```tsx showLineNumbers
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface TodoListItemProps {
|
|
||||||
item: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TodoListItem = (props: TodoListItemProps) => {
|
|
||||||
return <span>{props.item}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TodoListItem;
|
|
||||||
```
|
|
||||||
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% /tabs %}
|
|
||||||
|
|
||||||
On peut très bien imaginer des améliorations à cette application, comme par exemple :
|
|
||||||
|
|
||||||
- Supprimer une tâche
|
|
||||||
- Réinitialiser la liste des tâches
|
|
||||||
- Marquer une tâche comme terminée _(et inversement)_
|
|
||||||
- Ordonner les tâches pour afficher en priorité les tâches non terminées
|
|
||||||
- Enregistrer les tâches dans le navigateur pour les retrouver après un rafraîchissement de la page
|
|
||||||
|
|
||||||
Et si on se gardait ça pour la suite ? 😉
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Tu l'auras compris, React permet de résoudre un certain nombre de problématiques que l'on peut rencontrer lors du développement d'une application web.
|
|
||||||
|
|
||||||
Pas des problématiques majeures, mais ça nous permet tout de même en tant que développeur de gagner du temps et de l'efficacité !
|
|
||||||
|
|
||||||
Dans le cas où le fait que ce soit créé et maintenu par Facebook _(ou GAFAM de manière générale)_ est contre tes valeurs,
|
|
||||||
tu as des solutions très semblables qui existent, comme [**SolidJS**](https://www.solidjs.com/) par exemple.
|
|
||||||
|
|
||||||
Et si tu veux en savoir plus, je t'invite à lire les articles suivants qui vont te permettre de rentrer un peu plus dans le détail de React ! 🚀
|
|
||||||
@ -1,238 +0,0 @@
|
|||||||
---
|
|
||||||
title: Premier composant React
|
|
||||||
description: Voyons ensemble comment notre premier composant React !
|
|
||||||
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
|
|
||||||
---
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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 :
|
|
||||||
|
|
||||||
- Il ne fait que de l'affichage
|
|
||||||
- Il ne fait pas de calculs
|
|
||||||
- Il ne fait pas de requêtes HTTP
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## ⚙️ Syntaxes pour créer un composant
|
|
||||||
|
|
||||||
Il existe plusieurs façons de créer un composant React.
|
|
||||||
|
|
||||||
La plus simple est de créer une fonction qui retourne du JSX, mais il est aussi possible de créer une classe qui hérite `React.Component`.
|
|
||||||
|
|
||||||
Cependant, on ne va pas s'attarder sur la déclaration de composants dans des classes.
|
|
||||||
|
|
||||||
Pourquoi ? Déjà parce que la documentation officielle recommande désormais de définir des composants en tant que fonctions, mais surtout parce que c'est plus simple et plus lisible ! 🚀
|
|
||||||
|
|
||||||
## 👷 Création du composant
|
|
||||||
|
|
||||||
Allez c'est parti, occupons-nous de notre premier composant React !
|
|
||||||
|
|
||||||
```jsx showLineNumbers
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export function Button() {
|
|
||||||
return <button>Click me</button>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
... et c'est tout ! 🎉
|
|
||||||
On a créé notre premier composant React, c'est pas beau ça ?
|
|
||||||
|
|
||||||
Bon évidemment, il reste extrêmement sommaire et n'a pas d'intérêt à l'heure actuelle, mais c'est un bon début !
|
|
||||||
|
|
||||||
Avant d'aller plus loin, décortiquons un peu ce code :
|
|
||||||
|
|
||||||
- On importe `React` depuis la bibliothèque `React` _(ligne 1)_
|
|
||||||
- On crée une fonction `Button` _(lignes 3 à 5)_
|
|
||||||
- Cette fonction retourne un élément JSX `<button>Click me</button>`
|
|
||||||
|
|
||||||
## 🧩 Utilisation du composant
|
|
||||||
|
|
||||||
C'est bien beau d'avoir un composant, mais maintenant il faut l'utiliser !
|
|
||||||
|
|
||||||
```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)_.
|
|
||||||
|
|
||||||
Rien de bien sorcier, n'est-ce pas ? 😊
|
|
||||||
|
|
||||||
Mais maintenant, on va vouloir donner un peu plus de vie à notre bouton...
|
|
||||||
Parce que là pour le coup, il est inutile. 😅
|
|
||||||
|
|
||||||
## 📦 Props
|
|
||||||
|
|
||||||
Les **props** _(pour properties)_ sont des arguments que l'on peut passer à un composant React.
|
|
||||||
Il s'agit en quelque sorte des paramètres d'une fonction.
|
|
||||||
|
|
||||||
Imaginons ici que l'on veut rajouter :
|
|
||||||
|
|
||||||
- Un texte au bouton
|
|
||||||
- Une action à effectuer lorsqu'on clique dessus
|
|
||||||
|
|
||||||
Avant de t'expliquer comment faire, je vais te montrer ce que ça donne pour son utilisation :
|
|
||||||
|
|
||||||
```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 !
|
|
||||||
|
|
||||||
- Fonction `handleClick` qui affiche un message dans la console _(ligne 6)_
|
|
||||||
- On passe la fonction `handleClick` en tant que prop onClick à notre composant `Button` _(ligne 11)_
|
|
||||||
- On passe le texte `Clique-moi !` entre la balise ouvrante et fermante de notre composant `Button` _(ligne 12)_
|
|
||||||
|
|
||||||
Mais alors, comment on fait pour récupérer ces props dans notre composant Button ?
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
Mais tu peux également remarquer qu'il y a une propriété `children` qui n'est pas passée de la même manière que `onClick`.
|
|
||||||
|
|
||||||
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 !" %}
|
|
||||||
|
|
||||||
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 !
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## 🖇️ Les différentes props
|
|
||||||
|
|
||||||
Il existe plusieurs props que l'on peut passer à un composant React :
|
|
||||||
|
|
||||||
- `children` : Contient tout ce qui se trouve entre les balises ouvrante et fermante du composant
|
|
||||||
- Les autres props : Tout ce qui est passé en dehors des balises ouvrante et fermante du composant
|
|
||||||
|
|
||||||
Alors évidemment, cette explication est très réductrice, mais on va s'en contenter pour le moment.
|
|
||||||
|
|
||||||
Mais tu te doutes bien qu'il existe des props spécifiques à certains éléments HTML qui peuvent être passées à un composant React.
|
|
||||||
|
|
||||||
## 🔒 Mots clés protégés
|
|
||||||
|
|
||||||
En HTML, il existe des attributs qui sont utilisés dans certains éléments HTML. Cependant, comme ici on ne fait pas du HTML mais du JSX, on ne va pas pouvoir les utiliser tels quels.
|
|
||||||
|
|
||||||
Pour te donner un exemple concret, si tu veux ajouter une classe CSS à un élément, tu ne pourras pas utiliser l'attribut `class` mais `className`.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
{% 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 😉
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
On va donc utiliser des noms d'attributs qui sont similaires à ceux du HTML, mais qui sont adaptés pour le JSX.
|
|
||||||
|
|
||||||
- `class` devient `className` _(pour les classes CSS)_
|
|
||||||
- `for` devient `htmlFor` _(pour les labels)_
|
|
||||||
- `default` devient `defaultValue` _(pour les champs de formulaire)_
|
|
||||||
|
|
||||||
Et il en existe bien d'autres, mais on va s'arrêter là pour le moment ! 😊
|
|
||||||
|
|
||||||
## 👂 Écouteurs d'événements
|
|
||||||
|
|
||||||
Il existe également des attributs spécifiques pour les écouteurs d'événements.
|
|
||||||
Tu as notamment pu déjà faire la rencontre de `onClick`, mais il en existe bien d'autres !
|
|
||||||
|
|
||||||
Il s'agit de la syntaxe à adopter avec React pour ajouter des écouteurs d'événements à des composants JSX, et non pas d'utiliser `addEventListener` directement dans le JavaScript 😉
|
|
||||||
|
|
||||||
J'aimerai beaucoup pouvoir te donner une liste d'exemples d'écouteurs d'événements, mais il y en a tellement que je ne pourrais pas tous les citer ici.
|
|
||||||
La forme étant toujours la même, tu peux facilement savoir comment les écrire !
|
|
||||||
|
|
||||||
La forme est simple :
|
|
||||||
|
|
||||||
- Préfixe : `on`
|
|
||||||
- Événement : nom de l'événement en camelCase
|
|
||||||
|
|
||||||
Par exemple :
|
|
||||||
|
|
||||||
- `click` => `onClick`
|
|
||||||
- `change` => `onChange`
|
|
||||||
- `submit` => `onSubmit`
|
|
||||||
- `mouseenter` => `onMouseEnter`
|
|
||||||
- `mouseleave` => `onMouseLeave`
|
|
||||||
|
|
||||||
Et si tu cherches une liste complète des écouteurs d'événements, je t'invite à tout simplement consulter le [MDN Web Docs](https://developer.mozilla.org/fr/docs/Web/Events) qui est une mine d'or pour tout ce qui est lié au développement web !
|
|
||||||
|
|
||||||
Au début, on peut trouver ça un peu déroutant puisqu'on va naturellement les associer à du HTML, mais il faut se rappeler qu'on est dans du JSX _(oui, j'insiste beaucoup là-dessus !)_.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Et voilà, on a créé notre premier composant React !
|
|
||||||
On a même rapidement vu comment passer des props à un composant et comment les récupérer.
|
|
||||||
|
|
||||||
Et si pour le prochain article, on parle un peu des states pour rajouter un peu d'intéractivité à nos composants ?
|
|
||||||
Parce que c'est déjà cool ce qu'on a pu faire, mais on peut faire tellement plus avec React ! 🚀
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
---
|
|
||||||
title: Le state et le cycle de vie d'un composant React
|
|
||||||
description: Voyons ensemble comment gérer le state et le cycle de vie d'un composant React !
|
|
||||||
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
|
|
||||||
---
|
|
||||||
|
|
||||||
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 !
|
|
||||||
Commençons tranquillement avec le **cycle de vie** d'un composant, puisqu'il est indispensable pour comprendre le **state**.
|
|
||||||
|
|
||||||
## 🔄 Cycle de vie
|
|
||||||
|
|
||||||
Le **cycle de vie** d'un composant React est une série d'étapes que traverse un composant, de sa création _(montage)_ à sa destruction _(démontage)_.
|
|
||||||
|
|
||||||
Voici les trois différentes étapes du cycle de vie d'un composant React :
|
|
||||||
|
|
||||||
- **Montage** _(Mounting)_ : le composant est créé et inséré dans le DOM
|
|
||||||
- **Mise à jour** _(Updating)_ : le composant est mis à jour suite à un changement de props ou de state
|
|
||||||
- **Démontage** _(Unmounting)_ : le composant est retiré du DOM
|
|
||||||
|
|
||||||
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 ? 🦜🏴☠️" %}
|
|
||||||
|
|
||||||
Haha, non !
|
|
||||||
|
|
||||||
Un hook en React, est une fonction qui permet d'exploiter les fonctionnalités de React dans un composant fonctionnel _(fonction)_.
|
|
||||||
|
|
||||||
Bon... c'est un peu du charabia, mais on verra ça plus en détail dans le prochain article car il y a beaucoup à dire sur les hooks !
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
Mais pour le moment, restons en à une vue d'ensemble du cycle de vie !
|
|
||||||
|
|
||||||
## 🧠 State
|
|
||||||
|
|
||||||
Le state est un objet qui contient les données internes d'un composant.
|
|
||||||
Il est propre à chaque composant et peut être modifié par ce dernier (à ne pas confondre avec les props qui elles sont immuables).
|
|
||||||
|
|
||||||
Mais alors, pourquoi utiliser un state alors qu'on pourrait tout simplement déclarer une variable dans notre composant ?
|
|
||||||
|
|
||||||
Prenons cet exemple :
|
|
||||||
|
|
||||||
```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)
|
|
||||||
|
|
||||||
On serait tentés de croire que ce code fonctionne. Après tout, en vanilla JS _(JavaScript pur)_, on pourrait tout à fait faire ça !
|
|
||||||
Et maintenant tu t'en doutes _(sinon pourquoi j'en parlerai ?)_, ce code ne fonctionne pas.
|
|
||||||
|
|
||||||
Pourtant, si on regarde la console du navigateur on voit bien que la variable `count` est bien incrémentée !
|
|
||||||
|
|
||||||
La raison est très simple : React ne sait pas que la variable `count` a été modifiée.
|
|
||||||
Pour être plus précis, React ne sait pas qu'il doit mettre à jour l'interface utilisateur _(UI)_ suite à la modification de count.
|
|
||||||
|
|
||||||
C'est là qu'intervient le **state** !
|
|
||||||
Le **state** est **réactif** et permet à React de savoir quand il doit mettre à jour l'interface utilisateur _(UI)_.
|
|
||||||
|
|
||||||
## 📝 Déclaration du state
|
|
||||||
|
|
||||||
Pour déclarer un **state**, on utilise le **hook** `useState` de React.
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
const [count, setCount] = React.useState(0);
|
|
||||||
```
|
|
||||||
|
|
||||||
Et là tu vas peut-être te demander une chose...
|
|
||||||
|
|
||||||
{% 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
|
|
||||||
- `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 !
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Maintenant que notre state est déclaré, on peut l'utiliser dans notre composant.
|
|
||||||
|
|
||||||
```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)
|
|
||||||
|
|
||||||
Et voilà ! Pas besoin de plus pour gérer un state en React 😉
|
|
||||||
|
|
||||||
Mais qu'est-ce qu'il se passe sous le capot ?
|
|
||||||
C'est un peu plus complexe que ça, mais pour faire simple :
|
|
||||||
|
|
||||||
### ⚙️ 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)_.
|
|
||||||
|
|
||||||
### 🔧 Mise à jour du composant (Updating)
|
|
||||||
|
|
||||||
Ce state, à chaque modification, va déclencher un nouveau rendu du composant.
|
|
||||||
|
|
||||||
### 🗑️ Démontage du composant (Unmounting)
|
|
||||||
|
|
||||||
Et enfin, quand le composant est retiré du DOM, le state est détruit avec lui.
|
|
||||||
|
|
||||||
Ce fonctionnement est identique pour les props donnés à un composant d'ailleurs !
|
|
||||||
React est vraiment bien fait pour ça 😊
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
Plutôt simple, non ?
|
|
||||||
Alors maintenant que tu sais comment gérer le **state** et le **cycle de vie** d'un composant React, tu es prêt à te pencher sur la prochaine étape _(et pas des moindres)_ : les **hooks** !
|
|
||||||
|
|
||||||
Mais pour l'heure, je te laisse jouer avec les **states** et les **props** pour bien comprendre comment tout ça fonctionne.
|
|
||||||
@ -1,537 +0,0 @@
|
|||||||
---
|
|
||||||
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**.
|
|
||||||
@ -1,308 +0,0 @@
|
|||||||
---
|
|
||||||
title: L'utilisation des reducers avec React
|
|
||||||
description: Découvre les hooks de React, une fonctionnalité qui te permet de gérer le state et le cycle de vie de tes composants fonctionnels.
|
|
||||||
tags: [Frontend, React, JavaScript, TypeScript, Bibliothèque, Interface utilisateur (UI)]
|
|
||||||
---
|
|
||||||
|
|
||||||
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 ? 🤔
|
|
||||||
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
## Qu'est-ce que le hook useReducer ?
|
|
||||||
|
|
||||||
Le hook `useReducer` est une alternative à `useState` qui est plus adaptée pour gérer des **états complexes** ou des **états qui dépendent les uns des autres**.
|
|
||||||
|
|
||||||
Il est basé sur le concept de **reducers** que l'on peut retrouver dans la bibliothèque Redux.
|
|
||||||
|
|
||||||
Un reducer est une fonction qui prend en paramètre un état et une action, et qui retourne un nouvel état. Il permet un découpage plus fin de la logique de gestion de l'état.
|
|
||||||
|
|
||||||
Mais avant de rentrer dans les détails, donnons des exemples de quand utiliser `useReducer` ou `useState` !
|
|
||||||
|
|
||||||
### Quand utiliser useState ?
|
|
||||||
|
|
||||||
Si tu dois stocker un état simple, comme un booléen, un nombre ou une chaîne de caractères, alors `useState` est parfait pour cela.
|
|
||||||
|
|
||||||
Ne te casse donc pas la tête à remplacer tous tes `useState` par des `useReducer` si tu n'en as pas besoin. 😅
|
|
||||||
|
|
||||||
### Quand utiliser useReducer ?
|
|
||||||
|
|
||||||
Dès que l'on a des états complexes ou des états qui dépendent les uns des autres, il est recommandé d'utiliser `useReducer`.
|
|
||||||
|
|
||||||
Par exemple, si tu as un formulaire avec plusieurs champs, et que tu veux gérer l'état de chaque champ de manière indépendante, alors `useReducer` est une bonne solution.
|
|
||||||
|
|
||||||
Ça te permettra d'éviter de créer des tonnes de `useState` et/ou handlers pour chaque champ du formulaire.
|
|
||||||
|
|
||||||
Mais `useReducer` n'est pas seulement utile pour les formulaires, il peut être utilisé dans de nombreux cas, comme la gestion d'un panier d'achat, la gestion d'une ressource, etc.
|
|
||||||
|
|
||||||
## À quoi ressemble un reducer ?
|
|
||||||
|
|
||||||
Comme expliqué plus tôt, un reducer est une fonction qui prend en paramètre un état et une action, et qui retourne un nouvel état.
|
|
||||||
|
|
||||||
Parlons dans un premier temps de la signature d'un reducer :
|
|
||||||
|
|
||||||
{% 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`.
|
|
||||||
|
|
||||||
Le `state` est l'état actuel du composant, et l'`action` est un objet qui contient un type et d'autres propriétés.
|
|
||||||
|
|
||||||
En fonction du type de l'action, on retourne un nouvel état.
|
|
||||||
Par défaut _(c'est-à-dire si le type de l'action n'est pas reconnu)_, on retourne l'état actuel sans l'altérer.
|
|
||||||
|
|
||||||
Dans le reducer, il est strictement impossible d'altérer l'état actuel directement.
|
|
||||||
L'état est contraint au principe d'**immutabilité**.
|
|
||||||
|
|
||||||
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 ?" %}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Par exemple :
|
|
||||||
|
|
||||||
{% snippet path="react/reducer/reducer-why-spread-operator.jsx" language="jsx" showLineNumbers=true /%}
|
|
||||||
|
|
||||||
On perdrait ici la propriété `message` si on ne la déversait pas dans le nouvel état.
|
|
||||||
|
|
||||||
{% /callout %}
|
|
||||||
|
|
||||||
## Comment utiliser useReducer ?
|
|
||||||
|
|
||||||
Maintenant que tu as une idée de ce qu'est un reducer, voyons comment l'utiliser avec le hook `useReducer` au sein d'une application React ! 🚀
|
|
||||||
|
|
||||||
Naturellement, on va commencer par importer le hook `useReducer` :
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { useReducer } from "react";
|
|
||||||
```
|
|
||||||
|
|
||||||
Ensuite, on va définir notre état initial :
|
|
||||||
|
|
||||||
{% 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 :
|
|
||||||
|
|
||||||
{% tabs defaultSelectedTab="js" %}
|
|
||||||
|
|
||||||
{% tab value="js" label="JavaScript" %}
|
|
||||||
{% snippet path="react/reducer/reducer.js" language="js" showLineNumbers=true /%}
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% tab value="ts" label="TypeScript" %}
|
|
||||||
{% snippet path="react/reducer/reducer.ts" language="ts" showLineNumbers=true /%}
|
|
||||||
{% /tab %}
|
|
||||||
|
|
||||||
{% /tabs %}
|
|
||||||
|
|
||||||
{% callout type="question" title="C'est quoi `action.payload` ?" %}
|
|
||||||
|
|
||||||
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 :
|
|
||||||
|
|
||||||
{% 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.
|
|
||||||
|
|
||||||
Pour modifier l'état, on va donc appeler `dispatch` avec une action :
|
|
||||||
|
|
||||||
{% snippet path="react/reducer/reducer-dispatch-increment.js" language="js" /%}
|
|
||||||
|
|
||||||
Et voilà, tu sais maintenant comment utiliser `useReducer` dans une application React ! 🎉
|
|
||||||
|
|
||||||
## On nettoie tout ça !
|
|
||||||
|
|
||||||
Tout ce qui t'a été montré plus haut fonctionne, mais est-ce que pour autant ce code est qualitatif ? 🤔
|
|
||||||
|
|
||||||
**Non !**
|
|
||||||
|
|
||||||
Pourquoi ? Déjà, on a tout mis dans le même fichier, ce qui n'est pas très propre.
|
|
||||||
Mais surtout, notre code n'est pas à l'abri d'erreurs.
|
|
||||||
|
|
||||||
Que se passerait-il si on se trompait dans le type de l'action ?
|
|
||||||
Ou si on oubliait de passer un payload à l'action `SET` ?
|
|
||||||
|
|
||||||
C'est ce genre de comportements que l'on veut éviter pour nous assurer le bon fonctionnement de notre application.
|
|
||||||
|
|
||||||
Pour contrer ces problèmes, on va créer des actions et des types d'actions pour garantir la cohérence de notre code.
|
|
||||||
|
|
||||||
### Création des types d'actions
|
|
||||||
|
|
||||||
Nos types d'actions seront tous des chaînes de caractères. On va donc pouvoir les définir sous forme de constantes.
|
|
||||||
|
|
||||||
{% snippet path="react/reducer/reducer-actions-constants.js" language="js" /%}
|
|
||||||
|
|
||||||
{% 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 😉
|
|
||||||
|
|
||||||
{% 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
|
|
||||||
|
|
||||||
Si tu utilises JavaScript, je suis désolé de te dire que tu ne peux pas **fortement** typer les actions.
|
|
||||||
|
|
||||||
En revanche, si tu utilises TypeScript, tu peux définir les actions de la manière suivante :
|
|
||||||
|
|
||||||
{% snippet path="react/reducer/reducer-actions-union.ts" language="ts" /%}
|
|
||||||
|
|
||||||
Tu pourras alors utiliser `CounterAction` pour typer les actions de ton reducer :
|
|
||||||
|
|
||||||
{% snippet path="react/reducer/reducer-actions-union-use.ts" language="ts" /%}
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
{% 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 !
|
|
||||||
|
|
||||||
{% snippet path="react/reducer/reducer-dispatch-action-creator.js" language="js" /%}
|
|
||||||
|
|
||||||
## Les fichiers complets
|
|
||||||
|
|
||||||
On a vu beaucoup de chose et les fichiers sont un peu éparpillés.
|
|
||||||
Pour t'aider à mieux comprendre le fonctionnement du hook `useReducer` et comment l'implementer, voici les fichiers complets :
|
|
||||||
|
|
||||||
### Fichier counterReducer.js ou counterReducer.ts
|
|
||||||
|
|
||||||
{% 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
|
|
||||||
|
|
||||||
{% 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 !
|
|
||||||
|
|
||||||
{% 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** - `(state, action) => { /* ... */ }`
|
|
||||||
- **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 ?" %}
|
|
||||||
|
|
||||||
- **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
|
|
||||||
|
|
||||||
Alors, pas trop fatigué·e ? 😅
|
|
||||||
Au moins ça en valait la peine ! Tu te feras moins de nœuds au cerveau par la suite dans tes projets React !
|
|
||||||
|
|
||||||
Comme tu as pu le voir, `useReducer` est un outil puissant pour gérer des états complexes ou des états qui dépendent les uns des autres.
|
|
||||||
|
|
||||||
Même si ici notre exemple n'était qu'un simple compteur, tu peux appliquer tout ce que tu as pu voir dans des cas plus concrets.
|
|
||||||
|
|
||||||
Si tu veux aller encore plus loin, n'hésite pas à jeter un œil à la documentation officielle de React pour `useReducer`.
|
|
||||||
Tu peux également te renseigner sur Redux si tu veux aller encore plus loin dans la gestion de l'état de ton application, mais attention, c'est une autre paire de manches ! 😄
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
title: Synthèses et ressources pour développeurs
|
|
||||||
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: []
|
|
||||||
---
|
|
||||||
|
|
||||||
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 ! 🎉
|
|
||||||
|
|
||||||
## Pourquoi cette plateforme ?
|
|
||||||
|
|
||||||
À l'instar du [MDN Web Docs](https://developer.mozilla.org/fr/) et feu **Le Site du Zéro**, cette plateforme a pour objectif de te fournir des ressources de qualité pour t'aider à te perfectionner dans le développement.
|
|
||||||
|
|
||||||
## Contenu du Memento
|
|
||||||
|
|
||||||
Le contenu de cette plateforme est divisé en plusieurs sections :
|
|
||||||
|
|
||||||
- [**Certifications**](/certifications) : Des synthèses de référentiels des certifications de titres professionnels pour t'aider à te préparer aux examens.
|
|
||||||
- [**Documentations**](/docs) : Une documentation synthétique _(mais complète et détaillée)_ sur les différentes technologies du développement web.
|
|
||||||
|
|
||||||
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 ! 😊
|
|
||||||
|
|
||||||
Par contre je te préviens : certains concepts peuvent être un peu techniques et nécessiter un peu de temps pour être compris.
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# Ouvrir le fichier de tâches cron
|
|
||||||
crontab -e
|
|
||||||
|
|
||||||
# Ajouter la tâche de sauvegarde, toutes les nuits à minuit
|
|
||||||
0 * * * * /path/to/backup.sh
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<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"
|
|
||||||
/>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
document.querySelectorAll('button.iframe-loader').forEach((button: HTMLButtonElement) => {
|
|
||||||
// Pour chaque bouton qui doit charger un iframe, on écoute le clic dessus
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
// On récupère le container de l'iframe, qui dans notre exemple est la balise parente du bouton
|
|
||||||
const container: HTMLElement | null = button.closest('.iframe-container');
|
|
||||||
|
|
||||||
// Si le container n'existe pas, on arrête l'exécution de la fonction pour éviter un plantage
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const { src, width, height } = container.dataset as {
|
|
||||||
src: string,
|
|
||||||
width: string,
|
|
||||||
height: string
|
|
||||||
};
|
|
||||||
|
|
||||||
// On prépare notre iframe avec les données stockées dans le container
|
|
||||||
const iframe = document.createElement('iframe');
|
|
||||||
iframe.setAttribute('src', src);
|
|
||||||
iframe.setAttribute('width', width);
|
|
||||||
iframe.setAttribute('height', height);
|
|
||||||
|
|
||||||
// On supprime le contenu du container pour y ajouter notre iframe
|
|
||||||
container.innerHTML = '';
|
|
||||||
container.appendChild(iframe);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
fetch("https://api.exemple.com/data")
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => console.log(data));
|
|
||||||
7
app-old/data/snippets/js/xhr/jquery-ajax.js
vendored
7
app-old/data/snippets/js/xhr/jquery-ajax.js
vendored
@ -1,7 +0,0 @@
|
|||||||
$.ajax({
|
|
||||||
url: "https://api.exemple.com/data",
|
|
||||||
method: "GET",
|
|
||||||
success: function (data) {
|
|
||||||
console.log(data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.open("GET", "https://api.exemple.com/data", true);
|
|
||||||
xhr.onreadystatechange = function () {
|
|
||||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
|
||||||
console.log(JSON.parse(xhr.responseText));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.send();
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name monsite.fr;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for ; # On transmet l'adresse IP du client
|
|
||||||
proxy_set_header Host $host; # On transmet le nom de domaine
|
|
||||||
proxy_pass http://localhost:3000; # On redirige les requêtes vers le port 3000, où tourne notre application
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { initialState, actions, reducer } from "../reducers/counterReducer";
|
|
||||||
import { useReducer } from "react";
|
|
||||||
|
|
||||||
const Counter = () => {
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Count: {state.count}</p>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.increment())}>Increment</button>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.decrement())}>Decrement</button>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.reset())}>Reset</button>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.set(10))}>Set counter to 10</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Counter;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { initialState, actions, reducer } from "../reducers/counterReducer";
|
|
||||||
import { useReducer } from "react";
|
|
||||||
|
|
||||||
const Counter = () => {
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p>Count: {state.count}</p>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.increment())}>Increment</button>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.decrement())}>Decrement</button>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.reset())}>Reset</button>
|
|
||||||
|
|
||||||
<button onClick={() => dispatch(actions.set(10))}>Set counter to 10</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Counter;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
const CounterActionTypes = {
|
|
||||||
INCREMENT: "INCREMENT",
|
|
||||||
DECREMENT: "DECREMENT",
|
|
||||||
RESET: "RESET",
|
|
||||||
SET: "SET",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initialState = { count: 0 };
|
|
||||||
|
|
||||||
export const reducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case CounterActionTypes.INCREMENT:
|
|
||||||
return { ...state, count: state.count + 1 };
|
|
||||||
case CounterActionTypes.DECREMENT:
|
|
||||||
return { ...state, count: state.count - 1 };
|
|
||||||
case CounterActionTypes.RESET:
|
|
||||||
return { ...state, count: 0 };
|
|
||||||
case CounterActionTypes.SET:
|
|
||||||
return { ...state, count: action.payload };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
increment: () => ({ type: CounterActionTypes.INCREMENT }),
|
|
||||||
decrement: () => ({ type: CounterActionTypes.DECREMENT }),
|
|
||||||
reset: () => ({ type: CounterActionTypes.RESET }),
|
|
||||||
set: (value) => ({ type: CounterActionTypes.SET, payload: value }),
|
|
||||||
};
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
const enum CounterActionTypes {
|
|
||||||
INCREMENT = "INCREMENT",
|
|
||||||
DECREMENT = "DECREMENT",
|
|
||||||
RESET = "RESET",
|
|
||||||
SET = "SET",
|
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Action =
|
|
||||||
| { type: CounterActionTypes.INCREMENT }
|
|
||||||
| { type: CounterActionTypes.DECREMENT }
|
|
||||||
| { type: CounterActionTypes.RESET }
|
|
||||||
| { type: CounterActionTypes.SET; payload: number };
|
|
||||||
|
|
||||||
export const initialState: State = { count: 0 };
|
|
||||||
|
|
||||||
export const reducer = (state: State, action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case CounterActionTypes.INCREMENT:
|
|
||||||
return { ...state, count: state.count + 1 };
|
|
||||||
case CounterActionTypes.DECREMENT:
|
|
||||||
return { ...state, count: state.count - 1 };
|
|
||||||
case CounterActionTypes.RESET:
|
|
||||||
return { ...state, count: 0 };
|
|
||||||
case CounterActionTypes.SET:
|
|
||||||
return { ...state, count: action.payload };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
increment: (): Action => ({ type: CounterActionTypes.INCREMENT }),
|
|
||||||
decrement: (): Action => ({ type: CounterActionTypes.DECREMENT }),
|
|
||||||
reset: (): Action => ({ type: CounterActionTypes.RESET }),
|
|
||||||
set: (value: number): Action => ({ type: CounterActionTypes.SET, payload: value }),
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export const actions = {
|
|
||||||
increment: () => ({ type: CounterActionTypes.INCREMENT }),
|
|
||||||
decrement: () => ({ type: CounterActionTypes.DECREMENT }),
|
|
||||||
reset: () => ({ type: CounterActionTypes.RESET }),
|
|
||||||
set: (value) => ({ type: CounterActionTypes.SET, payload: value }),
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export const actions = {
|
|
||||||
increment: (): CounterAction => ({ type: CounterActionTypes.INCREMENT }),
|
|
||||||
decrement: (): CounterAction => ({ type: CounterActionTypes.DECREMENT }),
|
|
||||||
reset: (): CounterAction => ({ type: CounterActionTypes.RESET }),
|
|
||||||
set: (value: number): CounterAction => ({ type: CounterActionTypes.SET, payload: value }),
|
|
||||||
};
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export const INCREMENT = "INCREMENT";
|
|
||||||
export const DECREMENT = "DECREMENT";
|
|
||||||
export const RESET = "RESET";
|
|
||||||
export const SET = "SET";
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export const CounterActionTypes = {
|
|
||||||
INCREMENT: "INCREMENT",
|
|
||||||
DECREMENT: "DECREMENT",
|
|
||||||
RESET: "RESET",
|
|
||||||
SET: "SET",
|
|
||||||
};
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export const enum CounterActionTypes {
|
|
||||||
INCREMENT = "INCREMENT",
|
|
||||||
DECREMENT = "DECREMENT",
|
|
||||||
RESET = "RESET",
|
|
||||||
SET = "SET",
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
const reducer = (state: State, action: CounterAction) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case CounterActionTypes.INCREMENT:
|
|
||||||
return { ...state, count: state.count + 1 };
|
|
||||||
case CounterActionTypes.DECREMENT:
|
|
||||||
return { ...state, count: state.count - 1 };
|
|
||||||
case CounterActionTypes.RESET:
|
|
||||||
return { ...state, count: 0 };
|
|
||||||
case CounterActionTypes.SET:
|
|
||||||
return { ...state, count: action.payload };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export type CounterAction =
|
|
||||||
| { type: CounterActionTypes.INCREMENT }
|
|
||||||
| { type: CounterActionTypes.DECREMENT }
|
|
||||||
| { type: CounterActionTypes.RESET }
|
|
||||||
| { type: CounterActionTypes.SET; payload: number };
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
dispatch(actions.increment());
|
|
||||||
dispatch(actions.set(10));
|
|
||||||
@ -1 +0,0 @@
|
|||||||
dispatch({ type: "INCREMENT" });
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
const reducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "TYPE_1":
|
|
||||||
return { ...state /* Nouvel état */ };
|
|
||||||
case "TYPE_2":
|
|
||||||
return { ...state /* Nouvel état */ };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
const reducer = (state: State, action: Action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "TYPE_1":
|
|
||||||
return { ...state /* Nouvel état */ };
|
|
||||||
case "TYPE_2":
|
|
||||||
return { ...state /* Nouvel état */ };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
const [state, dispatch] = useReducer<State, Action>(reducer, initialState);
|
|
||||||
@ -1 +0,0 @@
|
|||||||
const initialState = { count: 0 };
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
type State = {
|
|
||||||
count: number;
|
|
||||||
};
|
|
||||||
const initialState: State = { count: 0 };
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
const initialState = { count: 0, message: "Hello" };
|
|
||||||
|
|
||||||
const reducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "INCREMENT":
|
|
||||||
return { count: state.count + 1 };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user