Compare commits

..

3 Commits

Author SHA1 Message Date
2a0283785a style: Improve indentation for consistency
All checks were successful
Update Memento Dev on VPS / deploy (push) Successful in 33s
2025-04-23 13:28:33 +02:00
7d00c50b2f style: Update PageLinkProps dir property to be required 2025-04-23 13:25:47 +02:00
20cb60be16 style: Improve code formatting for better readability 2025-04-23 13:25:31 +02:00
4 changed files with 338 additions and 393 deletions

View File

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

View File

@ -1,13 +1,7 @@
import type { SearchResult } from "@/services/FlexSearchService";
import type { JSX, Accessor, Setter } from "solid-js";
import {
createContext,
useContext,
For,
createEffect,
createSignal,
} from "solid-js";
import { createContext, useContext, For, createEffect, createSignal } from "solid-js";
import { Highlighter } from "solid-highlight-words";
import { useDebounce } from "@/hooks/useDebounce";
@ -18,334 +12,305 @@ import { useId } from "@/hooks/useId";
import clsx from "clsx";
const SearchContext = createContext<{
query: Accessor<string>;
close: () => void;
results: Accessor<SearchResult[]>;
isLoading: Accessor<boolean>;
isOpened: Accessor<boolean>;
setQuery: Setter<string>;
setIsOpened: Setter<boolean>;
setIsLoading: Setter<boolean>;
setResults: Setter<SearchResult[]>;
query: Accessor<string>;
close: () => void;
results: Accessor<SearchResult[]>;
isLoading: Accessor<boolean>;
isOpened: Accessor<boolean>;
setQuery: Setter<string>;
setIsOpened: Setter<boolean>;
setIsLoading: Setter<boolean>;
setResults: Setter<SearchResult[]>;
}>({
query: () => "",
close: () => {},
results: () => [],
isLoading: () => false,
isOpened: () => false,
setQuery: () => {},
setIsOpened: () => {},
setIsLoading: () => {},
setResults: () => {},
query: () => "",
close: () => {},
results: () => [],
isLoading: () => false,
isOpened: () => false,
setQuery: () => {},
setIsOpened: () => {},
setIsLoading: () => {},
setResults: () => {},
});
function SearchIcon(props: JSX.IntrinsicElements["svg"]) {
return (
<svg aria-hidden="true" viewBox="0 0 20 20" {...props}>
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
</svg>
);
return (
<svg aria-hidden="true" viewBox="0 0 20 20" {...props}>
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
</svg>
);
}
function LoadingIcon(props: JSX.IntrinsicElements["svg"]) {
const id = useId();
const id = useId();
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="5.5" stroke-linejoin="round" />
<path
stroke={`url(#${id})`}
stroke-linecap="round"
stroke-linejoin="round"
d="M15.5 10a5.5 5.5 0 1 0-5.5 5.5"
/>
<defs>
<linearGradient
id={id}
x1="13"
x2="9.5"
y1="9"
y2="15"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="currentColor" />
<stop offset="1" stop-color="currentColor" stop-opacity="0" />
</linearGradient>
</defs>
</svg>
);
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="5.5" stroke-linejoin="round" />
<path stroke={`url(#${id})`} stroke-linecap="round" stroke-linejoin="round" d="M15.5 10a5.5 5.5 0 1 0-5.5 5.5" />
<defs>
<linearGradient id={id} x1="13" x2="9.5" y1="9" y2="15" gradientUnits="userSpaceOnUse">
<stop stop-color="currentColor" />
<stop offset="1" stop-color="currentColor" stop-opacity="0" />
</linearGradient>
</defs>
</svg>
);
}
function SearchInput() {
const { close, setQuery, query, isLoading } = useContext(SearchContext);
const { close, setQuery, query, isLoading } = useContext(SearchContext);
return (
<div class="group relative flex h-12">
<SearchIcon class="pointer-events-none absolute top-0 left-4 h-full w-5 fill-slate-400" />
<input
data-autofocus
class={clsx(
"flex-auto appearance-none bg-transparent pl-12 text-slate-900 outline-hidden placeholder:text-slate-400 focus:w-full focus:flex-none sm:text-sm [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden",
isLoading() ? "pr-11" : "pr-4",
)}
onKeyDown={(event) => {
if (event.key === "Escape") {
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
return (
<div class="group relative flex h-12">
<SearchIcon class="pointer-events-none absolute top-0 left-4 h-full w-5 fill-slate-400" />
<input
data-autofocus
class={clsx(
"flex-auto appearance-none bg-transparent pl-12 text-slate-900 outline-hidden placeholder:text-slate-400 focus:w-full focus:flex-none sm:text-sm [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden",
isLoading() ? "pr-11" : "pr-4",
)}
onKeyDown={(event) => {
if (event.key === "Escape") {
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
close();
}
}}
value={query()}
onInput={(event) => {
const { value } = event.currentTarget;
setQuery(value);
}}
/>
{isLoading() && (
<div class="absolute inset-y-0 right-3 flex items-center">
<LoadingIcon class="h-6 w-6 animate-spin stroke-slate-200 text-slate-400" />
</div>
)}
</div>
);
close();
}
}}
value={query()}
onInput={(event) => {
const { value } = event.currentTarget;
setQuery(value);
}}
/>
{isLoading() && (
<div class="absolute inset-y-0 right-3 flex items-center">
<LoadingIcon class="h-6 w-6 animate-spin stroke-slate-200 text-slate-400" />
</div>
)}
</div>
);
}
function HighlightQuery(props: { text: string; query: string }) {
return (
<Highlighter
highlightClass="group-aria-selected:underline bg-transparent text-violet-600"
searchWords={[props.query]}
autoEscape={true}
textToHighlight={props.text}
/>
);
return (
<Highlighter
highlightClass="group-aria-selected:underline bg-transparent text-violet-600"
searchWords={[props.query]}
autoEscape={true}
textToHighlight={props.text}
/>
);
}
function SearchResultItem(props: { result: SearchResult; query: string }) {
const { close } = useContext(SearchContext);
const id = useId();
const { close } = useContext(SearchContext);
const id = useId();
const getHierarchy = (): string[] => {
const sectionTitle = navigation.find((section) => {
return section.links.find(
(link) => link.href === props.result.url.split("#")[0],
);
})?.title;
const getHierarchy = (): string[] => {
const sectionTitle = navigation.find((section) => {
return section.links.find((link) => link.href === props.result.url.split("#")[0]);
})?.title;
return [sectionTitle, props.result.pageTitle].filter(
(x): x is string => typeof x === "string",
);
};
return [sectionTitle, props.result.pageTitle].filter((x): x is string => typeof x === "string");
};
return (
<li
class="group block cursor-default rounded-lg px-3 py-2 aria-selected:bg-slate-100 hover:bg-slate-100"
aria-labelledby={`${id}-hierarchy ${id}-title`}
tab-index={0}
onKeyDown={(event) => {
if (event.key === "Enter") {
navigate(props.result.url);
close();
}
}}
onClick={() => {
navigate(props.result.url);
close();
}}
>
<div
id={`${id}-title`}
aria-hidden="true"
class="text-sm text-slate-700 group-aria-selected:text-violet-600"
>
<HighlightQuery text={props.result.title} query={props.query} />
</div>
{getHierarchy().length > 0 && (
<div
id={`${id}-hierarchy`}
aria-hidden="true"
class="mt-0.5 truncate text-xs whitespace-nowrap text-slate-500 dark:text-slate-400"
>
<For each={getHierarchy()}>
{(item, itemIndex) => (
<>
<HighlightQuery text={item} query={props.query} />
<span
class={
itemIndex() === getHierarchy().length - 1
? "sr-only"
: "mx-2 text-slate-300 dark:text-slate-700"
}
>
/
</span>
</>
)}
</For>
</div>
)}
</li>
);
const handleNavigate = (url: string) => {
navigate(`/${url}`.replace(/\/+/g, "/"));
close();
};
return (
<li
class="group block cursor-default rounded-lg px-3 py-2 aria-selected:bg-slate-100 hover:bg-slate-100"
aria-labelledby={`${id}-hierarchy ${id}-title`}
tab-index={0}
onKeyDown={(event) => {
if (event.key === "Enter") handleNavigate(props.result.url);
}}
onClick={() => handleNavigate(props.result.url)}
>
<div id={`${id}-title`} aria-hidden="true" class="text-sm text-slate-700 group-aria-selected:text-violet-600">
<HighlightQuery text={props.result.title} query={props.query} />
</div>
{getHierarchy().length > 0 && (
<div
id={`${id}-hierarchy`}
aria-hidden="true"
class="mt-0.5 truncate text-xs whitespace-nowrap text-slate-500 dark:text-slate-400"
>
<For each={getHierarchy()}>
{(item, itemIndex) => (
<>
<HighlightQuery text={item} query={props.query} />
<span
class={
itemIndex() === getHierarchy().length - 1 ? "sr-only" : "mx-2 text-slate-300 dark:text-slate-700"
}
>
/
</span>
</>
)}
</For>
</div>
)}
</li>
);
}
function SearchResults() {
const { results, query } = useContext(SearchContext);
const { results, query } = useContext(SearchContext);
if (results().length === 0) {
return (
<p class="px-4 py-8 text-center text-sm text-slate-700">
Aucun résultat pour &ldquo;
<span class="break-words text-slate-900">{query()}</span>
&rdquo;
</p>
);
}
if (results().length === 0) {
return (
<p class="px-4 py-8 text-center text-sm text-slate-700">
Aucun résultat pour &ldquo;
<span class="break-words text-slate-900">{query()}</span>
&rdquo;
</p>
);
}
return (
<ul>
<For each={results()}>
{(result) => (
<li>
<SearchResultItem result={result} query={query()} />
</li>
)}
</For>
</ul>
);
return (
<ul>
<For each={results()}>
{(result) => (
<li>
<SearchResultItem result={result} query={query()} />
</li>
)}
</For>
</ul>
);
}
function SearchDialog(props: { class?: string }) {
const { close, isOpened, setIsOpened, results } = useContext(SearchContext);
const { close, isOpened, setIsOpened, results } = useContext(SearchContext);
createEffect(() => {
if (isOpened()) return;
createEffect(() => {
if (isOpened()) return;
function onKeyDown(event: KeyboardEvent) {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setIsOpened(true);
}
}
function onKeyDown(event: KeyboardEvent) {
if (event.key === "k" && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setIsOpened(true);
}
}
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isOpened, setIsOpened]);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isOpened, setIsOpened]);
const handleClickOutside = (event: MouseEvent) => {
const { target, currentTarget } = event;
const handleClickOutside = (event: MouseEvent) => {
const { target, currentTarget } = event;
if (target instanceof Node && currentTarget instanceof Node) {
if (target === currentTarget) close();
}
};
if (target instanceof Node && currentTarget instanceof Node) {
if (target === currentTarget) close();
}
};
return (
<>
<Dialog
isOpen={isOpened()}
onClose={close}
class={clsx("fixed inset-0 z-50", props.class)}
>
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" />
return (
<>
<Dialog isOpen={isOpened()} onClose={close} class={clsx("fixed inset-0 z-50", props.class)}>
<div class="fixed inset-0 bg-slate-900/50 backdrop-blur-sm" />
<div
class="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh]"
onClick={handleClickOutside}
onKeyDown={(event) => {
if (event.key === "Escape") close();
}}
>
<DialogPanel class="mx-auto transform-gpu overflow-hidden rounded-xl bg-white shadow-xl sm:max-w-xl">
<form onSubmit={(event) => event.preventDefault()}>
<SearchInput />
<div class="border-t border-slate-200 bg-white px-2 py-3 empty:hidden">
{results().length > 0 && <SearchResults />}
</div>
</form>
</DialogPanel>
</div>
</Dialog>
</>
);
<div
class="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh]"
onClick={handleClickOutside}
onKeyDown={(event) => {
if (event.key === "Escape") close();
}}
>
<DialogPanel class="mx-auto transform-gpu overflow-hidden rounded-xl bg-white shadow-xl sm:max-w-xl">
<form onSubmit={(event) => event.preventDefault()}>
<SearchInput />
<div class="border-t border-slate-200 bg-white px-2 py-3 empty:hidden">
{results().length > 0 && <SearchResults />}
</div>
</form>
</DialogPanel>
</div>
</Dialog>
</>
);
}
export function Search() {
const [results, setResults] = createSignal<SearchResult[]>([]);
const [modifierKey, setModifierKey] = createSignal<string>();
const [isLoading, setIsLoading] = createSignal(false);
const [isOpened, setIsOpened] = createSignal(false);
const [query, setQuery] = createSignal("");
const [results, setResults] = createSignal<SearchResult[]>([]);
const [modifierKey, setModifierKey] = createSignal<string>();
const [isLoading, setIsLoading] = createSignal(false);
const [isOpened, setIsOpened] = createSignal(false);
const [query, setQuery] = createSignal("");
const debouncedQuery = useDebounce(query, 300);
const debouncedQuery = useDebounce(query, 300);
const onSearch = async (query: string) => {
const response = await fetch(`/search?query=${query}`);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
};
const onSearch = async (query: string) => {
const response = await fetch(`/search?query=${query}`);
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
};
createEffect(() => {
const platform = navigator.userAgentData?.platform || navigator.platform;
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(platform) ? "⌘" : "Ctrl ");
}, []);
createEffect(() => {
const platform = navigator.userAgentData?.platform || navigator.platform;
setModifierKey(/(Mac|iPhone|iPod|iPad)/i.test(platform) ? "⌘" : "Ctrl ");
}, []);
createEffect(() => {
const query = debouncedQuery();
createEffect(() => {
const query = debouncedQuery();
if (query.length === 0) {
setIsLoading(false);
setResults([]);
return;
}
if (query.length === 0) {
setIsLoading(false);
setResults([]);
return;
}
setIsLoading(true);
setIsLoading(true);
onSearch(query)
.then(setResults)
.finally(() => setIsLoading(false));
});
onSearch(query)
.then(setResults)
.finally(() => setIsLoading(false));
});
return (
<SearchContext.Provider
value={{
query,
close: () => setIsOpened(false),
results,
isLoading,
isOpened,
setQuery,
setIsOpened,
setIsLoading,
setResults,
}}
>
<button
type="button"
class="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-80 md:flex-none md:rounded-lg md:py-2.5 md:pr-3.5 md:pl-4 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 lg:w-96"
onClick={() => setIsOpened(true)}
>
<SearchIcon class="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 md:group-hover:fill-slate-400" />
<span class="sr-only md:not-sr-only md:ml-2 md:text-slate-500">
Rechercher...
</span>
{modifierKey && (
<kbd class="ml-auto hidden font-medium text-slate-400 md:block">
<kbd class="font-sans">{modifierKey()}</kbd>
<kbd class="font-sans">K</kbd>
</kbd>
)}
</button>
<SearchDialog />
</SearchContext.Provider>
);
return (
<SearchContext.Provider
value={{
query,
close: () => setIsOpened(false),
results,
isLoading,
isOpened,
setQuery,
setIsOpened,
setIsLoading,
setResults,
}}
>
<button
type="button"
class="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-80 md:flex-none md:rounded-lg md:py-2.5 md:pr-3.5 md:pl-4 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 lg:w-96"
onClick={() => setIsOpened(true)}
>
<SearchIcon class="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 md:group-hover:fill-slate-400" />
<span class="sr-only md:not-sr-only md:ml-2 md:text-slate-500">Rechercher...</span>
{modifierKey && (
<kbd class="ml-auto hidden font-medium text-slate-400 md:block">
<kbd class="font-sans">{modifierKey()}</kbd>
<kbd class="font-sans">K</kbd>
</kbd>
)}
</button>
<SearchDialog />
</SearchContext.Provider>
);
}

View File

@ -30,13 +30,13 @@ Le design pattern MVC est un modèle d'architecture logicielle qui sépare les d
- **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.
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 :
<Image alt="Schéma MVC pour une application web basique" src="/patterns/mvc.webp" class="max-h-96 mx-auto" />
<Image alt="Schéma MVC pour une application web basique" src="/images/patterns/mvc.webp" class="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.
@ -49,8 +49,10 @@ Pour t'aider à mieux te représenter un schéma MVC avec les ordres de flux de
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.
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.)">
@ -74,9 +76,8 @@ Si tu fais un projet personnel, tu peux définir les tiennes, du moment que tu e
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.
É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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB