Compare commits

..

3 Commits

12 changed files with 725 additions and 440 deletions

View File

@ -42,6 +42,6 @@ export function Button(props: ButtonProps) {
href={props.href} href={props.href}
/> />
) : ( ) : (
<button class={className} {...(props as JSX.IntrinsicElements["button"])} /> <button {...(props as JSX.IntrinsicElements["button"])} class={className} />
); );
} }

View File

@ -1,319 +1,24 @@
import type { ComponentProps, ParentComponent } from "solid-js"; import type { ComponentProps, ParentComponent } from "solid-js";
import { createEffect, createMemo, mergeProps, on, splitProps } from "solid-js"; import {
createEffect,
createMemo,
For,
mergeProps,
on,
splitProps,
} from "solid-js";
import { clipboard } from "solid-heroicons/solid";
import { Icon } from "solid-heroicons";
import * as Prismjs from "prismjs"; import * as Prismjs from "prismjs";
import toast from "solid-toast";
import clsx from "clsx"; import clsx from "clsx";
/**
* @see https://prismjs.com/#supported-languages
*/
export const Language = {
JAVASCRIPT: "javascript",
HTML: "html",
CSS: "css",
MARKUP: "markup",
XML: "xml",
SVG: "svg",
ABAP: "abap",
ABNF: "abnf",
ACTION_SCRIPT: "actionscript",
ADA: "ada",
AGDA: "agda",
AL: "al",
ANTLR4: "antlr4",
G4: "g4",
APACHE_CONFIGURATION: "apacheconf",
APEX: "apex",
APL: "apl",
APPLESCRIPT: "applescript",
AQL: "aql",
ARDUINO: "arduino",
ARFF: "arff",
ARM_ASSEMBLY: "armasm",
ARTURO: "arturo",
ASCIIDOC: "asciidoc",
ASP_NET: "aspnet",
ASSEMBLY_6502: "asm6502",
ASSEMBLY_ATMEL_AVR: "asmatmel",
AUTO_HOTKEY: "autohotkey",
AUTO_IT: "autoit",
AVI_SYNTH: "avisynth",
AVRO_IDL: "avro-idl",
AWK: "awk",
BASH: "bash",
BASIC: "basic",
BATCH: "batch",
BBCODE: "bbcode",
BBJ: "bbj",
BICEP: "bicep",
BIRB: "birb",
BISON: "bison",
BNF: "bnf",
BQN: "bqn",
BRAINFUCK: "brainfuck",
BRIGHT_SCRIPT: "brightscript",
BRO: "bro",
BSL: "bsl",
C: "c",
CSHARP: "csharp",
CPP: "cpp",
CF_SCRIPT: "cfscript",
CHAI_SCRIPT: "chaiscript",
CIL: "cil",
CILK_C: "cilkc",
CILK_CPP: "cilkcpp",
CLOJURE: "clojure",
CMAKE: "cmake",
COBOL: "cobol",
COFFEE_SCRIPT: "coffeescript",
CONCURNAS: "concurnas",
CONTENT_SECURITY_POLICY: "csp",
COOKLANG: "cooklang",
Coq: "coq",
CRYSTAL: "crystal",
CSS_EXTRAS: "css-extras",
CSV: "csv",
CUE: "cue",
CYPHER: "cypher",
D: "d",
DART: "dart",
DATA_WEAVE: "dataweave",
DAX: "dax",
DHALL: "dhall",
DIFF: "diff",
DJANGO: "django",
JINJA2: "jinja2",
DNS_ZONE_FILE: "dns-zone-file",
DOCKER: "docker",
DOT: "dot",
EBNF: "ebnf",
EDITOR_CONFIG: "editorconfig",
EIFFEL: "eiffel",
EJS: "ejs",
ELIXIR: "elixir",
ELM: "elm",
EMBEDDED_LUA_TEMPLATING: "etlua",
ERB: "erb",
ERLANG: "erlang",
EXCEL_FORMULA: "excel-formula",
FSHARP: "fsharp",
FACTOR: "factor",
FALSE: "false",
FIRESTORE_SECURITY_RULES: "firestore-security-rules",
FLOW: "flow",
FORTRAN: "fortran",
FREEMARKER_TEMPLATE_LANGUAGE: "ftl",
GAMEMAKER_LANGUAGE: "gml",
GAP: "gap",
GCODE: "gcode",
GD_SCRIPT: "gdscript",
GEDCOM: "gedcom",
GETTEXT: "gettext",
GHERKIN: "gherkin",
GIT: "git",
GLSL: "glsl",
GN: "gn",
GNU_LINKER_SCRIPT: "linker-script",
GO: "go",
GO_MODULE: "go-module",
GRADLE: "gradle",
GRAPHQL: "graphql",
GROOVY: "groovy",
HAML: "haml",
HANDLEBARS: "handlebars",
HASKELL: "haskell",
HAXE: "haxe",
HCL: "hcl",
HLSL: "hlsl",
HOON: "hoon",
HTTP: "http",
HTTP_PUBLIC_KEY_PINS: "hpkp",
HTTP_STRICT_TRANSPORT_SECURITY: "hsts",
ICHIGOJAM: "ichigojam",
ICON: "icon",
ICU_MESSAGE_FORMAT: "icu-message-format",
IDRIS: "idris",
IGNORE: "ignore",
INFORM_7: "inform7",
INI: "ini",
IO: "io",
J: "j",
JAVA: "java",
JAVADOC: "javadoc",
JAVADOC_LIKE: "javadoclike",
JAVA_STACK_TRACE: "javastacktrace",
JEXL: "jexl",
JOLIE: "jolie",
JQ: "jq",
JSDOC: "jsdoc",
JS_EXTRAS: "js-extras",
JSON: "json",
JSON5: "json5",
JSONP: "jsonp",
JS_STACK_TRACE: "jsstacktrace",
JS_TEMPLATES: "js-templates",
JULIA: "julia",
KEEPALIVED_CONFIGURE: "keepalived",
KEYMAN: "keyman",
KOTLIN: "kotlin",
KUMIR: "kumir",
KUSTO: "kusto",
LATEX: "latex",
LATTE: "latte",
LESS: "less",
LILYPOND: "lilypond",
LIQUID: "liquid",
LISP: "lisp",
LIVESCRIPT: "livescript",
LLVM: "llvm",
LOG: "log",
LOLCODE: "lolcode",
LUA: "lua",
MAGMA: "magma",
MAKEFILE: "makefile",
MARKDOWN: "markdown",
MARKUP_TEMPLATING: "markup-templating",
MATA: "mata",
MATLAB: "matlab",
MAXSCRIPT: "maxscript",
MEL: "mel",
MERMAID: "mermaid",
METAFONT: "metafont",
MIZAR: "mizar",
MONGODB: "mongodb",
MONKEY: "monkey",
MOONSCRIPT: "moonscript",
N1QL: "n1ql",
N4JS: "n4js",
NAND_TO_TETRIS_HDL: "nand2tetris",
NANINOVEL_SCRIPT: "naniscript",
NASM: "nasm",
NEON: "neon",
NEVOD: "nevod",
NGINX: "nginx",
NIM: "nim",
NIX: "nix",
NSIS: "nsis",
OBJECTIVE_C: "objectivec",
OCAML: "ocaml",
ODIN: "odin",
OPENCL: "opencl",
OPENQASM: "openqasm",
OZ: "oz",
PARI_GP: "parigp",
PARSER: "parser",
PASCAL: "pascal",
PASCALIGO: "pascaligo",
PATROL_SCRIPTING_LANGUAGE: "psl",
PC_AXIS: "pcaxis",
PEOPLECODE: "peoplecode",
PERL: "perl",
PHP: "php",
PHP_DOC: "phpdoc",
PHP_EXTRAS: "php-extras",
PLANT_UML: "plant-uml",
PL_SQL: "plsql",
POWERQUERY: "powerquery",
POWERSHELL: "powershell",
PROCESSING: "processing",
PROLOG: "prolog",
PROMQL: "promql",
PROPERTIES: "properties",
PROTOBUF: "protobuf",
PUG: "pug",
PUPPET: "puppet",
PURE: "pure",
PUREBASIC: "purebasic",
PURESCRIPT: "purescript",
PYTHON: "python",
QSHARP: "qsharp",
Q: "q",
QML: "qml",
QORE: "qore",
R: "r",
RACKET: "racket",
RAZOR: "razor",
REACT_JSX: "jsx",
REACT_TSX: "tsx",
REASON: "reason",
REGEX: "regex",
REGO: "rego",
RENPY: "renpy",
RESCRIPT: "rescript",
REST: "rest",
RIP: "rip",
ROBOCONF: "roboconf",
ROBOT_FRAMEWORK: "robotframework",
RUBY: "ruby",
RUST: "rust",
SAS: "sas",
SASS: "sass",
SCSS: "scss",
SCALA: "scala",
SCHEME: "scheme",
SHELL_SESSION: "shell-session",
SMALI: "smali",
SMALLTALK: "smalltalk",
SMARTY: "smarty",
SML: "sml",
SOLIDITY: "solidity",
SOLUTION_FILE: "solution-file",
SOY: "soy",
SPARQL: "sparql",
SPLUNK_SPL: "splunk-spl",
SQF: "sqf",
SQL: "sql",
SQUIRREL: "squirrel",
STAN: "stan",
STATA_ADO: "stata",
STRUCTURED_TEXT: "iecst",
STYLUS: "stylus",
SUPERCOLLIDER: "supercollider",
SWIFT: "swift",
SYSTEMD: "systemd",
T4_TEMPLATING: "t4-templating",
T4_CSHARP: "t4-cs",
T4_VB: "t4-vb",
TAP: "tap",
TCL: "tcl",
TEMPLATE_TOOLKIT_2: "tt2",
TEXTILE: "textile",
TOML: "toml",
TREMOR: "tremor",
TURTLE: "turtle",
TWIG: "twig",
TYPESCRIPT: "typescript",
TYPOSCRIPT: "typoscript",
UNREALSCRIPT: "unrealscript",
UO_RAZOR_SCRIPT: "uorazor",
URI: "uri",
V: "v",
VALA: "vala",
VB_NET: "vbnet",
VELOCITY: "velocity",
VERILOG: "verilog",
VHDL: "vhdl",
VIM: "vim",
VISUAL_BASIC: "visual-basic",
WARP_SCRIPT: "warpscript",
WEB_ASSEMBLY: "wasm",
WEB_IDL: "web-idl",
WGSL: "wgsl",
WIKI: "wiki",
WOLFRAM: "wolfram",
WREN: "wren",
XEORA: "xeora",
XML_DOC: "xml-doc",
XOJO: "xojo",
XQUERY: "xquery",
YAML: "yaml",
YANG: "yang",
ZIG: "zig",
} as const;
export type Language = (typeof Language)[keyof typeof Language];
type Props = { type Props = {
language: string; language: string;
class?: string;
dark?: boolean;
withLineNumbers?: boolean;
} & ComponentProps<"code">; } & ComponentProps<"code">;
export const Highlight: ParentComponent<Props> = (_props) => { export const Highlight: ParentComponent<Props> = (_props) => {
@ -326,16 +31,16 @@ export const Highlight: ParentComponent<Props> = (_props) => {
]); ]);
const languageClass = createMemo(() => `language-${props.language}`); const languageClass = createMemo(() => `language-${props.language}`);
const highlightedCode = createMemo<string | undefined>(() => { const highlightedCode = createMemo<string | undefined>(() => {
const childrenString = props.children?.toString(); const childrenString = props.children?.toString();
if (!childrenString) { if (!childrenString) return;
return;
}
const grammar = Prismjs.languages[props.language]; const grammar = Prismjs.languages[props.language];
if (!grammar) { if (!grammar) return;
return;
}
const result = Prismjs.highlight(childrenString, grammar, props.language); const result = Prismjs.highlight(childrenString, grammar, props.language);
return result; return result;
}); });
@ -345,15 +50,76 @@ export const Highlight: ParentComponent<Props> = (_props) => {
}), }),
); );
const handleCopyToClipboard = () => {
if (props.innerHTML) {
navigator.clipboard.writeText(props.innerHTML);
} else if (props.children) {
navigator.clipboard.writeText(props.children.toString());
}
toast.success("Copié dans le presse-papier", {
duration: 2000,
position: "top-right",
});
};
return ( return (
<pre class={clsx("prism-code flex overflow-x-auto pb-6", languageClass())}> <div
<code class={clsx(
class={clsx("px-4", props.class)} "group relative flex items-start px-4 py-2 w-full",
innerHTML={highlightedCode()} props.class,
{...rest} )}
>
<button
class="absolute cursor-pointer top-0 right-2 text-slate-500 bg-slate-200/10 rounded-md hover:bg-linear-to-r hover:from-violet-400/30 hover:via-violet-400 hover:to-violet-400/30 p-px hover:text-violet-300"
type="button"
onClick={handleCopyToClipboard}
> >
{props.children} <span
</code> class={clsx(
</pre> props.dark ? "hover:bg-slate-800" : "hover:bg-white",
"p-2 block rounded-md",
)}
>
<span class="sr-only">Copier l'extrait de code</span>
<Icon path={clipboard} class="w-5 h-5" />
</span>
</button>
{props.withLineNumbers && props.children?.toString() && (
<div
aria-hidden="true"
class="border-r leading-6 border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
>
<For
each={Array.from({
length: props.children.toString().split("\n").length,
})}
>
{(_, index) => (
<>
{(index() + 1).toString().padStart(2, "0")}
<br />
</>
)}
</For>
</div>
)}
<pre
class={clsx(
"not-prose w-full prism-code flex overflow-x-auto",
languageClass(),
)}
>
<code
class={clsx("px-4", "leading-6")}
innerHTML={highlightedCode()}
{...rest}
>
{props.children}
</code>
</pre>
</div>
); );
}; };

View File

@ -25,8 +25,6 @@ export function Prose(props: ProseProps) {
"prose-a:font-semibold", "prose-a:font-semibold",
// link underline // link underline
"prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,var(--color-violet-300))] prose-a:hover:[--tw-prose-underline-size:6px]", "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]",
// pre
"prose-pre:rounded-xl prose-pre:bg-slate-900 prose-pre:shadow-lg",
)} )}
{...props} {...props}
/> />

106
app/components/Snippet.tsx Normal file
View File

@ -0,0 +1,106 @@
import type { JSX } from "solid-js";
import { For, createSignal } from "solid-js";
import { Highlight } from "./Highlight";
import clsx from "clsx";
function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg aria-hidden="true" viewBox="0 0 42 10" fill="none" {...props}>
<circle cx="5" cy="5" r="4.5" class="fill-red-400" />
<circle cx="21" cy="5" r="4.5" class="fill-amber-300" />
<circle cx="37" cy="5" r="4.5" class="fill-green-500" />
</svg>
);
}
type SnippetTab = {
name: string;
codeLanguage: string;
code: string;
};
type SnippetProps = {
children?: JSX.Element;
class?: string;
snippets: SnippetTab[];
dark?: boolean;
};
export function Snippet(props: SnippetProps) {
const [selectedTab, setSelectedTab] = createSignal<SnippetTab>(
props.snippets[0],
);
const isActive = (tab: SnippetTab) => selectedTab()?.name === tab.name;
const selectTab = (name: string) => {
const tab = props.snippets.find((tab) => tab.name === name);
if (tab) setSelectedTab(tab);
};
return (
<div
class={clsx(
"relative rounded-2xl ring-1 ring-white/10 backdrop-blur-sm",
props.dark ? "bg-[#0A101F]/80" : "bg-slate-50",
props.class,
)}
>
<div class="absolute -top-px right-11 left-20 h-px bg-linear-to-r from-violet-300/0 via-violet-300/70 to-violet-300/0" />
<div class="absolute right-20 -bottom-px left-11 h-px bg-linear-to-r from-purple-400/0 via-purple-400 to-purple-400/0" />
<div class="pt-4 pl-4">
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
<div class="mt-4 flex space-x-2 text-xs">
<For each={props.snippets}>
{(tab) => (
<div
class={clsx(
"flex h-6 rounded-full",
{ "cursor-pointer": tab.codeLanguage && !isActive(tab) },
isActive(tab)
? clsx(
"bg-linear-to-r from-violet-400/30 via-violet-400 to-violet-400/30 p-px font-medium",
props.dark ? "text-violet-300" : "text-violet-600",
)
: props.dark
? "text-slate-400"
: "text-slate-500",
)}
>
<button
type="button"
class={clsx(
"flex items-center rounded-full px-2.5",
isActive(tab) && props.dark
? "bg-slate-800"
: "bg-violet-100",
)}
disabled={!tab.codeLanguage}
onClick={() => selectTab(tab.name)}
>
{tab.name}
</button>
</div>
)}
</For>
</div>
{selectedTab() && (
<div class="mt-6">
<Highlight
class={clsx(
"!pt-0 !px-1 max-h-96 overflow-auto",
props.dark && "dark text-white",
)}
language={selectedTab().codeLanguage}
withLineNumbers
>
{selectedTab().code}
</Highlight>
</div>
)}
</div>
</div>
);
}

131
app/components/Tabs.tsx Normal file
View File

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

View File

@ -1,46 +1,80 @@
pre[class*=language-] { .token.comment {
color: var(--color-slate-50) font-style: italic;
} }
.token.tag,
.token.class-name, .dark .token.module,
.token.selector, .dark .token.attr-name,
.token.selector .class, .dark .token.keyword,
.token.selector.class, .dark .token.rule,
.dark .token.pseudo-class,
.dark .token.important {
color: #CB5FDE;
}
.dark .token.comment,
.dark .token.operator,
.dark .token.combinator {
color: var(--color-slate-400);
}
.dark .token.punctuation,
.dark .token.attr-equals {
color: var(--color-slate-500);
}
.dark .token.attr-value,
.dark .token.class,
.dark .token.string {
color: #89CA6C;
}
.dark .token.property,
.dark .token.tag {
color: #E3596F;
}
.dark .token.function-variable {
color: #61AFEF;
}
/** Light mode */
.token.attr-value,
.token.class,
.token.string {
color: #50A14F;
}
.token.function-variable,
.token.generic-function,
.token.function { .token.function {
color: var(--color-pink-400) color: #4078F2;
} }
.token.module,
.token.attr-name, .token.attr-name,
.token.keyword,
.token.rule, .token.rule,
.token.pseudo-class, .token.pseudo-class,
.token.important { .token.important {
color: var(--color-slate-300) color: #A626A4;
} }
.token.keyword,
.token.module { .token.property,
color: var(--color-pink-400) .token.tag {
color: #E55649;
} }
.token.attr-value,
.token.class, .token.generic,
.token.string, .token.class-name {
.token.property { color: #C18401;
color: var(--color-sky-300)
}
.token.punctuation,
.token.attr-equals {
color: var(--color-slate-500)
}
.token.unit,
.language-css .token.function {
color: var(--color-teal-200)
} }
.token.comment, .token.comment,
.token.operator,
.token.combinator { .token.combinator {
color: var(--color-slate-400) color: #A0A1A7;
}
.prism-code {
margin: 0
}
.prism-code + .prism-code {
margin-bottom: 1rem
} }
.token.operator,
.token.builtin {
color: #0184BC;
}

View File

@ -1,20 +1,37 @@
import { Show } from "solid-js";
import { usePageContext } from "vike-solid/usePageContext"; import { usePageContext } from "vike-solid/usePageContext";
import { Link } from "@/components/Link";
export default function Page() { export default function Page() {
const { is404 } = usePageContext(); const pageContext = usePageContext();
return (
<Show if (pageContext.is404) {
when={is404} return (
fallback={ <>
<> <div class="max-w-2xl min-w-0 flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
<h1>500 Internal Server Error</h1> <div class="flex h-full flex-col items-center justify-center text-center">
<p>Something went wrong.</p> <p class="font-display text-sm font-medium text-slate-900">404</p>
</> <h1 class="mt-3 font-display text-3xl tracking-tight text-slate-900">
} Page introuvable
> </h1>
<h1>404 Page Not Found</h1> <p class="mt-2 text-sm text-slate-500">
<p>This page could not be found.</p> Désolé, nous ne pouvons pas trouver la page que vous recherchez.
</Show> </p>
); <Link href="/" class="mt-8 text-sm font-medium text-slate-900">
Retour à l&apos;accueil
</Link>
</div>
</div>
</>
);
}
return (
<>
<h1>Erreur {pageContext.abortStatusCode || 500}</h1>
<p>
{(pageContext.abortReason as string) ??
"Une erreur s'est produite lors du chargement de la page."}
</p>
</>
);
} }

View File

@ -0,0 +1,35 @@
---
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: []
---
import Callout from "@/components/Callout";
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>

View File

@ -0,0 +1,150 @@
---
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)]
---
import Callout from "@/components/Callout";
import tabs from "./tabs";
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.reactTodolist />
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 ! 🚀

View File

@ -0,0 +1,86 @@
import { Snippet } from "@/components/Snippet";
const reactTodoListSnippets = [
{
name: "App.tsx",
codeLanguage: "tsx",
code: `import TodoList from "./TodoList";
import React from "react";
const App = () => {
return (
<div>
<h1>TodoList</h1>
<TodoList />
</div>
);
};`,
},
{
name: "TodoList.tsx",
codeLanguage: "tsx",
code: `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;`,
},
{
name: "TodoListItem.tsx",
codeLanguage: "tsx",
code: `import React from "react";
interface TodoListItemProps {
item: string;
}
const TodoListItem = (props: TodoListItemProps) => {
return <span>{props.item}</span>;
};
export default TodoListItem;`,
},
];
export default {
reactTodolist: () => {
return <Snippet snippets={reactTodoListSnippets} />;
},
};

View File

@ -2,26 +2,35 @@ import type { JSX } from "solid-js";
import blurIndigoImage from "@/images/blur-indigo.webp"; import blurIndigoImage from "@/images/blur-indigo.webp";
import blurCyanImage from "@/images/blur-cyan.webp"; import blurCyanImage from "@/images/blur-cyan.webp";
import { Highlight } from "@/components/Highlight";
import { HeroBackground } from "./HeroBackground"; import { HeroBackground } from "./HeroBackground";
import { Snippet } from "@/components/Snippet";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Image } from "@/components/Image"; import { Image } from "@/components/Image";
import { For } from "solid-js";
import clsx from "clsx";
const codeLanguage = "javascript"; const snippets = [
const code = `export default { {
role: 'developer', name: "memento-dev.config.js",
qualifications: [ codeLanguage: "javascript",
'DWWM', code: `export default {
'CDA', role: "developer",
'CDUI', qualifications: [
] "DWWM",
}`; "CDA",
"CDUI",
const tabs = [ ]
{ name: "memento-dev.config.js", isActive: true }, }`,
{ name: "package.json", isActive: false }, },
{
name: "package.json",
codeLanguage: "json",
code: `{
"name": "memento-dev",
"version": "2.0.0",
"description": "Memento Dev est une plateforme open-source, soutenue et maintenue par une communauté de contributeurs passionnés.",
"main": "index.ts",
"license": "MIT"
}`,
},
]; ];
function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) { function TrafficLightsIcon(props: JSX.IntrinsicElements["svg"]) {
@ -87,55 +96,8 @@ export function HeroSection() {
/> />
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10 blur-lg" /> <div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10 blur-lg" />
<div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10" /> <div class="absolute inset-0 rounded-2xl bg-linear-to-tr from-violet-300 via-violet-300/70 to-purple-300 opacity-10" />
<div class="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur-sm">
<div class="absolute -top-px right-11 left-20 h-px bg-linear-to-r from-violet-300/0 via-violet-300/70 to-violet-300/0" />
<div class="absolute right-20 -bottom-px left-11 h-px bg-linear-to-r from-purple-400/0 via-purple-400 to-purple-400/0" />
<div class="pt-4 pl-4">
<TrafficLightsIcon class="h-2.5 w-auto stroke-slate-500/30" />
<div class="mt-4 flex space-x-2 text-xs">
<For each={tabs}>
{(tab) => (
<div
class={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
class={clsx(
"flex items-center rounded-full px-2.5",
tab.isActive && "bg-slate-800",
)}
>
{tab.name}
</div>
</div>
)}
</For>
</div>
<div class="mt-6 flex items-start px-1 text-sm">
<div
aria-hidden="true"
class="border-r border-slate-300/5 pr-4 font-mono text-slate-600 select-none"
>
<For
each={Array.from({ length: code.split("\n").length })}
>
{(_, index) => (
<>
{(index() + 1).toString().padStart(2, "0")}
<br />
</>
)}
</For>
</div>
<Highlight language={codeLanguage}>{code}</Highlight> <Snippet class="min-h-64" dark snippets={snippets} />
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -58,7 +58,7 @@ function NavigationItem(props: NavigationItemProps) {
</button> </button>
</h3> </h3>
{isOpened() && ( {isOpened() && (
<ul class="!mt-0 ml-2 space-y-1 border-l-2 border-slate-100 lg:mt-4 lg:space-y-2 lg:border-slate-200 mb-2"> <ul class="!mt-0 ml-2 border-l-2 border-slate-100 lg:mt-4 lg:space-y-1 lg:border-slate-200 mb-2">
<For each={props.section.links}> <For each={props.section.links}>
{(link) => ( {(link) => (
<li class="relative"> <li class="relative">