diff --git a/package.json b/package.json index 16d4750..eb3777c 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-dropzone": "^15.0.0", - "react-router-dom": "^7.13.2" + "react-fast-marquee": "^1.6.5", + "react-router-dom": "^7.13.2", + "swiper": "^12.1.4" }, "devDependencies": { "@eslint/js": "^9.30.1", diff --git a/src/App.tsx b/src/App.tsx index 5009234..a2d49c4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,7 @@ import NotFound from "./pages/404"; import useSettings from "./hooks/queries/useSettings"; import AboutPage from "./pages/public/AboutPage"; import Testimonies from "./pages/admin/TestimonyPage"; +import MethodPage from "./pages/public/MethodPage"; function MaintenanceGuard() { const location = useLocation(); @@ -90,7 +91,7 @@ export default function App() { "::-webkit-scrollbar-corner": { background: "transparent", }, - "ul:not(.MuiList-root, .MuiImageList-root)": { + "ul:not(.MuiList-root, .MuiImageList-root, .MuiTimeline-root)": { margin: "0 0 0 2rem", paddingLeft: "0", li: { @@ -155,6 +156,14 @@ export default function App() { } /> + + + + } + /> + {oppositeContent && ( + + {oppositeContent} + + )} + + + {dotIcon && ( + + )} + + {connector && ( + + )} + + + {content} + + + ); +} diff --git a/src/components/custom/MultipleMarquee.tsx b/src/components/custom/MultipleMarquee.tsx new file mode 100644 index 0000000..f4ead1b --- /dev/null +++ b/src/components/custom/MultipleMarquee.tsx @@ -0,0 +1,101 @@ +import { Children } from "react"; +import Marquee from "react-fast-marquee"; +import { ResponsiveStack } from "./ResponsiveLayout"; +import type { SxProps } from "@mui/system"; + +const MAX_ROWS = 10; + +export default function MultipleMarquee({ + rows = 2, + distribute = false, + direction = "horizontal", + gap = "24px", + sx, + children, + ...props +}: { + rows?: number; + distribute?: boolean; + direction?: "vertical" | "horizontal"; + gap?: string; + sx?: SxProps; + children: React.ReactNode[]; +} & Omit, "children" | "direction">) { + const items = Children.toArray(children).filter(Boolean); + + if (!rows || rows < 2 || rows > MAX_ROWS) { + throw new Error( + `MultipleMarquee: le nombre de lignes doit être compris entre 2 et ${MAX_ROWS}.`, + ); + } + + if (!distribute) { + // Mêmes items dans toutes les lignes + return ( + + {[...Array(rows)].map((_, rowIndex) => ( + + {items} + + ))} + + ); + } + + // Distribuer les items sur les lignes + const itemsPerRow = Math.ceil(items.length / rows); + + return ( + + {[...Array(rows)].map((_, rowIndex) => { + const start = rowIndex * itemsPerRow; + const end = start + itemsPerRow; + const rowItems = items.slice(start, end); + + return ( + + {rowItems} + + ); + })} + + ); +} diff --git a/src/components/custom/StickyMenuBar.tsx b/src/components/custom/StickyMenuBar.tsx new file mode 100644 index 0000000..91b81d9 --- /dev/null +++ b/src/components/custom/StickyMenuBar.tsx @@ -0,0 +1,218 @@ +import { AppBar, Link, Toolbar, useTheme } from "@mui/material"; +import CustomIconButton from "./CustomIconButton"; +import { useAuthContext } from "../../context/AuthContext"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { mdiChevronLeft, mdiChevronRight } from "@mdi/js"; + +export interface MenuSection { + id: string; + label: string; +} + +interface StickyMenuBarProps { + sections: MenuSection[]; +} + +export default function StickyMenuBar({ sections }: StickyMenuBarProps) { + const { isAuthenticated } = useAuthContext(); + const theme = useTheme(); + + const menuBarRef = useRef(null); + const [activeSection, setActiveSection] = useState(""); + const canAutoScrollRef = useRef(true); + const [showMenuBarArrows, setShowMenuBarArrows] = useState(false); + const [disableLeftMenuButton, setDisableLeftMenuButton] = useState(false); + const [disableRightMenuButton, setDisableRightMenuButton] = useState(false); + + const checkOverflow = useCallback(() => { + if (menuBarRef.current) { + setShowMenuBarArrows( + menuBarRef.current.scrollWidth > menuBarRef.current.clientWidth, + ); + setDisableLeftMenuButton(menuBarRef.current.scrollLeft === 0); + setDisableRightMenuButton( + menuBarRef.current.scrollLeft + menuBarRef.current.clientWidth >= + menuBarRef.current.scrollWidth, + ); + } + }, []); + + const setMenuBarRef = (node: HTMLDivElement | null) => { + if (menuBarRef.current) { + menuBarRef.current.removeEventListener("scroll", checkOverflow); + } + if (node) { + node.addEventListener("scroll", checkOverflow); + setTimeout(checkOverflow, 0); + } + menuBarRef.current = node; + }; + + // Scrollspy + auto-scroll de la toolbar + useEffect(() => { + const handleToolbarScroll = () => { + canAutoScrollRef.current = false; + }; + const toolbar = document.getElementById("toolbar-scrollable"); + toolbar?.addEventListener("scroll", handleToolbarScroll, { passive: true }); + + const handleScrollSpy = () => { + canAutoScrollRef.current = true; + let lastSectionId = ""; + for (const { id } of sections) { + const el = document.getElementById(id); + if (el) { + const rect = el.getBoundingClientRect(); + if ( + rect.top < window.innerHeight && + window.innerHeight - Math.max(rect.top, 0) >= 168 + ) { + lastSectionId = id; + } + } + } + setActiveSection(lastSectionId); + + if (!canAutoScrollRef.current) return; + const activeLink = document.querySelector( + `#toolbar-scrollable a[href="#${lastSectionId}"]`, + ); + const toolbar = document.getElementById("toolbar-scrollable"); + if (activeLink && toolbar) { + const linkRect = activeLink.getBoundingClientRect(); + const toolbarRect = toolbar.getBoundingClientRect(); + if ( + linkRect.left < toolbarRect.left || + linkRect.right > toolbarRect.right + ) { + activeLink.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "nearest", + }); + } + } + }; + + window.addEventListener("scroll", handleScrollSpy, { passive: true }); + handleScrollSpy(); + return () => { + window.removeEventListener("scroll", handleScrollSpy); + toolbar?.removeEventListener("scroll", handleToolbarScroll); + }; + }, [sections]); + + useEffect(() => { + window.addEventListener("resize", checkOverflow); + checkOverflow(); + return () => window.removeEventListener("resize", checkOverflow); + }, []); + + return ( + + {showMenuBarArrows && ( + + menuBarRef.current?.scrollBy({ left: -200, behavior: "smooth" }) + } + disabled={disableLeftMenuButton} + /> + )} + + {sections.map(({ id, label }) => ( + { + e.preventDefault(); + const el = document.getElementById(id); + if (el) { + const y = + el.getBoundingClientRect().top + + window.scrollY - + (isAuthenticated ? 180 : 132); + window.scrollTo({ top: y, behavior: "smooth" }); + } + }} + > + {label} + + ))} + + {showMenuBarArrows && ( + + menuBarRef.current?.scrollBy({ left: 200, behavior: "smooth" }) + } + disabled={disableRightMenuButton} + /> + )} + + ); +} diff --git a/src/components/entities/StackFormDialog.tsx b/src/components/entities/StackFormDialog.tsx index 6f0c9f1..1844909 100644 --- a/src/components/entities/StackFormDialog.tsx +++ b/src/components/entities/StackFormDialog.tsx @@ -15,7 +15,7 @@ import useCategories from "../../hooks/queries/useCategories"; import MediaPicker from "./media/MediaPicker"; import useMedias from "../../hooks/queries/useMedias"; import type { Media } from "../../types/entities/mediaTypes"; -import { extractId, getSelectValue } from "../../utils/normalizeRef"; +import { extractId, extractIds, getMultiSelectValue } from "../../utils/normalizeRef"; import { stripHtml } from "../../utils/stringUtils"; /** @@ -56,7 +56,7 @@ export default function StackFormDialog({ description: "", versions: [], skills: [], - category: "", + categories: [], }, }); @@ -112,25 +112,21 @@ export default function StackFormDialog({ sx={{ flex: "1 0 208px" }} /> + typeof c === "string" ? c : c.id, + ) } onChange={(e) => { - const categoryValue = getSelectValue(e); + const categoryValues = getMultiSelectValue(e); setEditingStack( editingStack - ? { ...editingStack, category: categoryValue } + ? { ...editingStack, categories: categoryValues } : null, ); - categoryValue !== - (typeof initialStack?.category === "string" - ? initialStack.category - : initialStack?.category?.id || "") && - setHasChanges(true); + setHasChanges(true); }} options={ categories @@ -142,6 +138,7 @@ export default function StackFormDialog({ : stripHtml(c.label), })) || [] } + emptyOption={false} sx={{ flex: "1 0 208px" }} fullWidth={false} /> @@ -207,16 +204,18 @@ export default function StackFormDialog({ key="confirm" color="success" onClick={() => { - if (typeof open === "string" && editingStack?.id) { - const input = editingStack; + if (!editingStack) return; + const input: any = { + ...editingStack, + icon: extractId(editingStack.icon), + categories: extractIds(editingStack.categories), + }; + delete input.__typename; + if (typeof open === "string") { delete input.id; - delete (input as any).__typename; - input.category = extractId(input.category); - handleEdit({ - variables: { id: open, input }, - }); + handleEdit({ variables: { id: open, input } }); } else { - handleAdd({ variables: { input: editingStack! } }); + handleAdd({ variables: { input } }); } }} disabled={ diff --git a/src/components/entities/project/public/ProjectMenuBar.tsx b/src/components/entities/project/public/ProjectMenuBar.tsx index 9b23eab..c14ab72 100644 --- a/src/components/entities/project/public/ProjectMenuBar.tsx +++ b/src/components/entities/project/public/ProjectMenuBar.tsx @@ -1,15 +1,8 @@ -import { AppBar, Link, Toolbar, useTheme } from "@mui/material"; -import CustomIconButton from "../../../custom/CustomIconButton"; import type { Project } from "../../../../types/entities/projectTypes"; -import { useAuthContext } from "../../../../context/AuthContext"; -import { useEffect, useRef, useState } from "react"; -import { mdiChevronLeft, mdiChevronRight } from "@mdi/js"; import { hasRichTextContent } from "../../../../utils/stringUtils"; +import StickyMenuBar from "../../../custom/StickyMenuBar"; export default function ProjectMenuBar({ project }: { project: Project }) { - const { isAuthenticated } = useAuthContext(); - const theme = useTheme(); - const sections: { id: string; label: string }[] = []; if ( @@ -84,229 +77,5 @@ export default function ProjectMenuBar({ project }: { project: Project }) { if (sections.length < 3) return null; - const menuBarRef = useRef(null); - const setMenuBarRef = (node: HTMLDivElement | null) => { - if (menuBarRef.current) { - menuBarRef.current.removeEventListener("scroll", checkOverflow); - } - if (node) { - node.addEventListener("scroll", checkOverflow); - // On vérifie l'overflow dès que le ref est attaché - setTimeout(checkOverflow, 0); - } - menuBarRef.current = node; - }; - - const [activeSection, setActiveSection] = useState(""); - - const [canAutoScroll, setCanAutoScroll] = useState(true); - - // Désactive l'auto-scroll si l'utilisateur scrolle la toolbar - useEffect(() => { - const toolbar = document.getElementById("toolbar-scrollable"); - if (!toolbar) return; - const handleToolbarScroll = () => setCanAutoScroll(false); - toolbar.addEventListener("scroll", handleToolbarScroll, { passive: true }); - return () => { - toolbar.removeEventListener("scroll", handleToolbarScroll); - }; - }, []); - - // Réactive l'auto-scroll si l'utilisateur scrolle la page (window) - useEffect(() => { - const handleWindowScroll = () => { - // On ne réactive que si ce n'est pas un scroll programmatique de la toolbar - setCanAutoScroll(true); - }; - window.addEventListener("scroll", handleWindowScroll, { passive: true }); - return () => { - window.removeEventListener("scroll", handleWindowScroll); - }; - }, []); - - useEffect(() => { - // Ce handler ne fait que le scrollspy et l'auto-scroll si canAutoScroll est actif - const handleScrollSpy = () => { - let lastSectionId = ""; - for (const { id } of sections) { - const el = document.getElementById(id); - if (el) { - const rect = el.getBoundingClientRect(); - if ( - rect.top < window.innerHeight && - window.innerHeight - Math.max(rect.top, 0) >= 168 - ) { - lastSectionId = id; - } - } - } - setActiveSection(lastSectionId); - // Ne scroller la toolbar que si le lien actif N'EST PAS visible ET que la toolbar n'est pas à une extrémité - if (!canAutoScroll) return; - const activeLink = document.querySelector( - `#toolbar-scrollable a[href="#${lastSectionId}"]`, - ); - const toolbar = document.getElementById("toolbar-scrollable"); - if (activeLink && toolbar) { - const linkRect = activeLink.getBoundingClientRect(); - const toolbarRect = toolbar.getBoundingClientRect(); - // On ne scroll que si le lien actif n'est pas visible ET qu'on n'est pas à une extrémité - if ( - linkRect.left < toolbarRect.left || - linkRect.right > toolbarRect.right - ) { - activeLink.scrollIntoView({ - behavior: "smooth", - block: "nearest", - inline: "nearest", - }); - } - } - }; - - window.addEventListener("scroll", handleScrollSpy, { - passive: true, - }); - handleScrollSpy(); - return () => window.removeEventListener("scroll", handleScrollSpy); - }, [sections, canAutoScroll]); - - // Callback ref pour garantir l'attachement de l'écouteur scroll - const [showMenuBarArrows, setShowMenuBarArrows] = useState(false); - const [disableLeftMenuButton, setDisableLeftMenuButton] = useState(false); - const [disableRightMenuButton, setDisableRightMenuButton] = useState(false); - - // checkOverflow doit être défini hors du useEffect pour être utilisé dans le callback ref - const checkOverflow = () => { - if (menuBarRef.current) { - setShowMenuBarArrows( - menuBarRef.current.scrollWidth > menuBarRef.current.clientWidth, - ); - setDisableLeftMenuButton(menuBarRef.current.scrollLeft === 0); - setDisableRightMenuButton( - menuBarRef.current.scrollLeft + menuBarRef.current.clientWidth >= - menuBarRef.current.scrollWidth, - ); - } - }; - - // Callback ref pour attacher/détacher l'écouteur scroll - - useEffect(() => { - window.addEventListener("resize", checkOverflow); - checkOverflow(); - return () => { - window.removeEventListener("resize", checkOverflow); - }; - }, []); - - return ( - - {/* Flèche gauche */} - {showMenuBarArrows && ( - { - menuBarRef.current?.scrollBy({ - left: -200, - behavior: "smooth", - }); - }} - disabled={disableLeftMenuButton} - /> - )} - - {sections.map(({ id, label }) => ( - { - e.preventDefault(); - const el = document.getElementById(id); - if (el) { - const y = el.getBoundingClientRect().top + window.scrollY - 96; - window.scrollTo({ top: y, behavior: "smooth" }); - } - }} - > - {label} - - ))} - - {/* Flèche droite */} - {showMenuBarArrows && ( - { - menuBarRef.current?.scrollBy({ left: 200, behavior: "smooth" }); - }} - disabled={disableRightMenuButton} - /> - )} - - ); + return ; } diff --git a/src/components/method/MethodContractSection.tsx b/src/components/method/MethodContractSection.tsx new file mode 100644 index 0000000..96110f7 --- /dev/null +++ b/src/components/method/MethodContractSection.tsx @@ -0,0 +1,90 @@ +import { Typography } from "@mui/material"; +import { ResponsiveBox, ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionCard from "./SectionCard"; +import SectionTitle from "./SectionTitle"; +import { useBreakpoints } from "../../hooks/mediaQueries"; + +export default function MethodContractSection() { + const { isLg } = useBreakpoints(); + + return ( + + + + + + La formation fait partie intégrante de la livraison. + + + Elle comprend la prise en main des interfaces d’administration, la + compréhension des workflows et, lorsque nécessaire, la remise d’une + documentation technique. + + + + + Une période de garantie est généralement appliquée après la + livraison d’un projet. Durant cette période, les corrections de bugs + liés au périmètre initial sont prises en charge sans facturation + supplémentaire. Les évolutions fonctionnelles, en revanche, sont + considérées comme des prestations additionnelles et font l’objet + d’une estimation distincte. + + + La maintenance d’un projet est divisée entre une approche préventive + et une approche curative. La maintenance préventive inclut les mises + à jour de dépendances, les corrections de sécurité, la surveillance + des performances et la gestion des sauvegardes. La maintenance + curative intervient en cas d’anomalie et suit un niveau de priorité + défini selon la criticité du problème. + + + + + Sur le plan juridique, une clause de confidentialité peut être + appliquée afin de protéger les informations sensibles du projet. + + + La propriété intellectuelle quant à elle, est transférée au client + une fois le paiement intégral effectué, à l’exception des composants + open source ou des librairies tierces utilisées dans le projet. + + + + + ); +} diff --git a/src/components/method/MethodDesignSection.tsx b/src/components/method/MethodDesignSection.tsx new file mode 100644 index 0000000..5279dff --- /dev/null +++ b/src/components/method/MethodDesignSection.tsx @@ -0,0 +1,120 @@ +import { Typography } from "@mui/material"; +import type { Category } from "../../types/entities/categoryTypes"; +import type { Stack } from "../../types/entities/stackTypes"; +import { ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionCard from "./SectionCard"; +import SectionTitle from "./SectionTitle"; +import StackGrid from "./StackGrid"; +import { useBreakpoints } from "../../hooks/mediaQueries"; + +export default function MethodDesignSection({ stacks }: { stacks: Stack[] }) { + const { isLg } = useBreakpoints(); + + return ( + + + + stack.categories?.some((c) => (c as Category).label === "Design"), + )} + /> + {/* Contenu */} + + {/* Design System */} + + + + Le design s’appuie sur une base cohérente qui structure l’ensemble + du produit et garantit l’unité visuelle de l’interface avant la + conception des pages. + + + Le système repose sur des breakpoints à la fois horizontaux et + verticaux. Ils influencent la composition des écrans et la densité + de contenu, notamment sur des interfaces complexes ou riches en + données. L’objectif est d’obtenir des interfaces réellement + adaptatives, et non simplement responsive. + + + La structure des layouts s’appuie sur un pas vertical et sur une + grille flexible. Cette variation permet de gérer des interfaces + mobile comme desktop, tout en conservant une logique de + composition cohérente. + + + L'identité visuelle repose au minimum sur une couleur primaire, + une couleur secondaire, une couleur d’accent ainsi qu’une palette + de couleurs neutres destinée aux éléments de fond, de surface et + de séparation. À cela s’ajoute un système de feedback standardisé + composé de quatre états fonctionnels : erreur, avertissement, + succès et information. + + + Les composants intègrent également un système d’états interactifs + afin de garantir une cohérence comportementale sur l’ensemble du + produit. + + + Une librairie Material Design sert de fondation et est adaptée à + chaque projet, ce qui permet de bénéficier d’un socle robuste tout + en conservant une flexibilité de personnalisation forte au niveau + de l’identité visuelle et des besoins fonctionnels. + + + + {/* Wireframe & Maquettes */} + + {/* Wireframes */} + + + En amont de la phase de maquettage, des wireframes peuvent être + utilisés afin de structurer la logique fonctionnelle des + interfaces. + + + Ils permettent de définir la hiérarchie de l’information, les + parcours utilisateurs et la structure globale des écrans sans + interférence liée à l’identité visuelle. Cette approche garantit + une validation rapide des choix fonctionnels avant d’engager le + travail graphique. + + + Les wireframes servent également de support d’échange entre la + conception et le développement. Ils permettent d’aligner la + compréhension du produit avant toute implémentation technique et + facilitent la transition vers le design system et les composants + réutilisables. + + + {/* Maquettes */} + + + Chaque interface est conçue selon une logique d’atomic design. + + + Les composants de base sont combinés pour former des structures + plus complexes, jusqu’aux écrans complets. + + + Les maquettes sont construites dès le départ pour être directement + exploitables par le développement. Elles intègrent les + comportements attendus, les différents états des composants et les + règles d’interaction, afin de limiter les interprétations et + d’assurer une intégration fluide entre design et code. + + + + + + ); +} diff --git a/src/components/method/MethodDevSection.tsx b/src/components/method/MethodDevSection.tsx new file mode 100644 index 0000000..86bf181 --- /dev/null +++ b/src/components/method/MethodDevSection.tsx @@ -0,0 +1,253 @@ +import { Typography } from "@mui/material"; +import { ResponsiveBox, ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionCard from "./SectionCard"; +import SectionTitle from "./SectionTitle"; +import StackGrid from "./StackGrid"; +import type { Category } from "../../types/entities/categoryTypes"; +import type { Stack } from "../../types/entities/stackTypes"; +import { useBreakpoints } from "../../hooks/mediaQueries"; + +function SecondaryContent({ stacks }: { stacks: Stack[] }) { + const { isLg } = useBreakpoints(); + + return ( + <> + {/* Performance */} + + + stack.categories?.some( + (c) => (c as Category).label === "Performance", + ), + )} + /> + + La performance repose à la fois sur une optimisation frontend + (réduction des requêtes inutiles, chargement différé des ressources, + optimisation du rendu), et sur une optimisation backend (mise en cache + via Redis, limitation des traitements lourds, structuration efficace + des requêtes). + + + L’objectif est de garantir une expérience fluide, même en cas de + montée en charge ou d’augmentation du volume de données. + + + {/* Sécurité */} + + + stack.categories?.some((c) => (c as Category).label === "Sécurité"), + )} + /> + + Un socle de sécurité est appliqué à l’ensemble des projets. + + + Il inclut notamment la sécurisation des accès API, la validation des + données côté serveur, la gestion des permissions utilisateurs et la + protection contre les injections et requêtes non autorisées. + + + Les informations sensibles sont isolées du code source et gérées via + des mécanismes sécurisés d’environnement. + + + {/* Tests */} + + + stack.categories?.some((c) => (c as Category).label === "Tests"), + )} + /> + + Les projets simples reposent principalement sur des tests manuels + structurés, tandis que les projets plus complexes peuvent intégrer des + tests automatisés pour sécuriser les règles métier et les interactions + critiques. + + + Les interfaces sont vérifiées sur la cohérence des états, la gestion + des erreurs et la compatibilité multi-supports afin d’assurer une + expérience utilisateur stable. + + + {/* Documentation */} + + + stack.categories?.some( + (c) => (c as Category).label === "Documentation", + ), + )} + /> + + Afin de de conserver un projet exploitable, une documentation + technique est maintenue tout au long du projet afin de garantir la + lisibilité, la transmission et la maintenabilité du système dans le + temps. + + + Elle inclut au minimum les informations essentielles au lancement de + l’environnement, à la structure du projet et aux principaux workflows. + + + + ); +} + +export default function MethodDevSection({ + stacks, + categories, +}: { + stacks: Stack[]; + categories: Category[]; +}) { + const { isLg } = useBreakpoints(); + + return ( + + + {/* Contenu principal */} + + {/* Frontend */} + + + stack.categories?.some( + (c) => + (c as Category).parent === + categories.find((cat) => cat.label === "Frontend")?.id, + ), + ) + .sort((a, b) => + ((a.categories?.[0] as Category)?.label ?? "").localeCompare( + (b.categories?.[0] as Category)?.label ?? "", + ), + )} + /> + + Dans le cas de sites statiques le frontend constitue la couche + principale de l’application. + + + Dans cette configuration, l’application fonctionne comme une + interface entièrement autonome, où les contenus sont soit intégrés + directement dans le code, soit consommés via des sources externes + simples (fichiers ou services). + + + Lorsque le projet nécessite des données dynamiques, le frontend + développé en React communique avec une API dédiée.{" "} + + + Dans certains cas, des mécanismes temps réel peuvent également être + intégrés via WebSocket, notamment pour des besoins de mise à jour + instantanée (chat, notifications...). Le frontend peut également + consommer des services externes comme des systèmes d’envoi d’emails + ou des API tierces. + + + {/* Backend */} + + + stack.categories?.some( + (c) => + (c as Category).parent === + categories.find((cat) => cat.label === "Backend")?.id, + ), + ) + .sort((a, b) => + ((a.categories?.[0] as Category)?.label ?? "").localeCompare( + (b.categories?.[0] as Category)?.label ?? "", + ), + )} + /> + + Le backend est mis en place uniquement lorsque le projet dépasse le + cadre d’une application frontend autonome. + + + Il est responsable de la logique métier, de la gestion des données + et des règles de fonctionnement du produit. + + + Le backend, développé en Node.js ou Symfony selon les besoins, + expose une API consommée par le frontend. Cette API isole la logique + métier, sécurise les échanges et garantit la cohérence des données. + + + {/* Wordpress */} + + + stack.categories?.some( + (c) => (c as Category).label === "Wordpress", + ), + )} + /> + + WordPress permet d'accélérer le développement de projets nécessitant + une gestion de contenu via une interface administrative conviviale. + + + Il n'est cependant envisagé que pour des projets dont la structure + de données reste simple. + + + Dans ce cas, le développement est structuré autour d’un thème + personnalisé, conçu spécifiquement pour le projet, et de + l'utilisation d'ACF qui permet de modéliser les contenus de manière + flexible. + + + Des blocs Gutenberg personnalisés peuvent également être développés + afin de permettre une édition plus modulaire des contenus. + + + + {/* Contenu secondaire */} + {isLg ? ( + + + + ) : ( + + + + )} + + ); +} diff --git a/src/components/method/MethodIAInfraSection.tsx b/src/components/method/MethodIAInfraSection.tsx new file mode 100644 index 0000000..5c7a6de --- /dev/null +++ b/src/components/method/MethodIAInfraSection.tsx @@ -0,0 +1,105 @@ +import Icon from "@mdi/react"; +import { ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionCard from "./SectionCard"; +import SectionTitle from "./SectionTitle"; +import { mdiRabbit } from "@mdi/js"; +import { useTheme } from "@mui/system"; +import StackGrid from "./StackGrid"; +import type { Stack } from "../../types/entities/stackTypes"; +import type { Category } from "../../types/entities/categoryTypes"; +import { Typography } from "@mui/material"; + +export default function MethodIAInfraSection({ stacks }: { stacks: Stack[] }) { + const theme = useTheme(); + + return ( + + {/* IA */} + + + IA + + } + titleColor={theme.palette.text.primary} + subtitle="Encadrer par l’expertise métier" + subtitleColor={theme.palette.primary.light} + nowrap={false} + /> + + + stack.categories?.some((c) => (c as Category).label === "IA"), + )} + /> + + Les outils d’assistance par IA font partie des workflows modernes de + développement. Je les utilise pour accélérer certaines tâches + techniques ou explorer des pistes d’implémentation. + + + Leur efficacité reste cependant directement liée à l’expertise de la + personne qui les utilise. La qualité d’un produit dépend avant tout de + la capacité à structurer une architecture cohérente, comprendre les + besoins métier implicites, appliquer les standards de développement et + concevoir une expérience utilisateur claire et maintenable. + + + Cet outil est donc utilisé comme une interface d’assistance sous + contrôle humain, au service de la qualité, de la fiabilité et de + l’efficacité de production. + + + {/* Infrastructure */} + + + + stack.categories?.some( + (c) => (c as Category).label === "Infrastructure", + ), + )} + /> + + La gestion des environnements suit une logique claire de séparation + entre local, staging et production. + + + L’environnement local est conteneurisé via Docker Compose afin de + garantir une reproduction fidèle des conditions d’exécution. + + + L’environnement de staging est considéré comme une réplique + fonctionnelle de la production. Il permet de valider les évolutions + avant mise en ligne et constitue une étape obligatoire dans le cycle + de livraison. + + + Les déploiements sont automatisés autant que possible via des + pipelines CI/CD. Ces pipelines intègrent des étapes de vérification + telles que le linting, l’exécution des tests, la construction de + l’application et le déploiement sur l’environnement cible. Un contrôle + manuel peut être conservé avant la mise en production lorsque le + niveau de risque le nécessite. + + + La gestion des secrets et des variables d’environnement est + strictement externalisée du code source afin de garantir la sécurité + du système. Les logs et le monitoring sont intégrés dans une logique + de suivi continu de la stabilité applicative. + + + + ); +} diff --git a/src/components/method/MethodProjectManagementSection.tsx b/src/components/method/MethodProjectManagementSection.tsx new file mode 100644 index 0000000..d4f11c2 --- /dev/null +++ b/src/components/method/MethodProjectManagementSection.tsx @@ -0,0 +1,107 @@ +import { Typography } from "@mui/material"; +import { ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionTitle from "./SectionTitle"; +import StackGrid from "./StackGrid"; +import SectionCard from "./SectionCard"; +import type { Stack } from "../../types/entities/stackTypes"; +import type { Category } from "../../types/entities/categoryTypes"; + +export default function MethodProjectManagementSection({ + stacks, +}: { + stacks: Stack[]; +}) { + return ( + + + + stack.categories?.some( + (c) => (c as Category).label === "Gestion de projet", + ), + )} + /> + {/* Contenu */} + + {/* Contenu principal */} + + + Le pilotage des projets suit une logique agile adaptée au contexte + du projet. + + + Il combine un flux Kanban et, lorsque cela est pertinent, des cycles + courts de type sprint. + + + Une organisation GitHub centralise l’ensemble des repositories liés + au projet. Cette organisation peut contenir plusieurs repositories + distincts, permettant d’isoler les responsabilités techniques tout + en conservant une vision globale du produit. + + + Les branches suivent une convention stricte afin de garantir la + stabilité du code. La branche principale correspond toujours à la + version en production, tandis que les développements s’effectuent + sur des branches dédiées aux fonctionnalités ou aux corrections. + Toute intégration dans la branche principale se fait via une pull + request, systématiquement revue avant fusion. + + + La gestion de projet elle-même est en principe réalisée directement + dans GitHub Projects. Cet espace sert de hub central pour la + planification, la rédaction des tâches, leur estimation et leur + priorisation. Chaque tâche est décrite de manière structurée, avec + un contexte clair, une intention fonctionnelle, des critères + d’acceptation et une checklist technique permettant de garantir + l’exécution sans ambiguïté. + + + Les échanges avec le client s’intègrent directement dans ce système + de gestion. La priorisation des tâches, leur validation ainsi que + leur découpage sont réalisés en collaboration avec le client, afin + d’assurer un alignement continu entre les attentes métier et + l’exécution technique. + + + Le workflow global suit une logique fluide mais maîtrisée : cadrage + des besoins, structuration dans GitHub Projects, estimation des + charges, priorisation collaborative, puis exécution progressive avec + suivi des livraisons. Cette approche permet de conserver une forte + agilité tout en gardant une visibilité claire sur l’avancement du + produit. + + + {/* Produit existant */} + + + Lorsqu’un projet repose sur une base déjà existante, une phase + d’audit peut être réalisée en amont. + + + Cet audit permet d’évaluer la structure du projet, la qualité du + code, les dépendances, les niveaux de sécurité, les performances + ainsi que les éventuels points de fragilité techniques ou + organisationnels. + + + L’objectif est de disposer d’une vision claire de l’état réel du + produit avant d’engager de nouveaux développements, afin d’éviter + l’accumulation de dette technique ou l’introduction de régressions. + + + Selon les résultats de l’audit, certaines recommandations peuvent + être formulées concernant la structure du projet, les priorités de + correction ou le niveau de maintenabilité à long terme. + + + + + ); +} diff --git a/src/components/method/MethodUXSection.tsx b/src/components/method/MethodUXSection.tsx new file mode 100644 index 0000000..9ea0473 --- /dev/null +++ b/src/components/method/MethodUXSection.tsx @@ -0,0 +1,73 @@ +import { Typography } from "@mui/material"; +import { ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionCard from "./SectionCard"; +import SectionTitle from "./SectionTitle"; +import { useBreakpoints } from "../../hooks/mediaQueries"; + +export default function MethodUXSection() { + const { isLg } = useBreakpoints(); + + return ( + + + + {/* Interfaces d'administration */} + + + Chaque projet dynamique intègre une interface d’administration + permettant la gestion des contenus.{" "} + + + Cette interface inclut généralement la gestion des pages ou entités, + une librairie de médias centralisée, ainsi qu’un système de rôles et + de permissions permettant de contrôler les accès des différents + utilisateurs. + + + Une attention particulière est portée à la simplicité d’usage. Les + parcours, les intitulés et les structures sont pensés pour rester + clairs et intuitifs au quotidien pour des utilisateurs non + techniques. L’objectif est de limiter les actions complexes, réduire + les risques d’erreur et permettre une prise en main rapide de + l’outil. + + + {/* Interface publique */} + + + L’expérience utilisateur côté visiteur est conçue comme un élément + central du produit. + + + L’objectif est de construire des interfaces claires, lisibles et + efficaces, dans lesquelles la navigation est pensée pour guider + naturellement les interactions. Chaque écran doit répondre à une + intention précise : informer, convertir, explorer ou interagir. + + + L’interaction est également un élément structurant de l’expérience. + Les états visuels, les transitions et les retours utilisateurs sont + utilisés pour rendre l’interface compréhensible et réactive. Le + temps de chargement et la stabilité de l’interface sont optimisés + afin de garantir une expérience fluide, y compris sur des connexions + ou des appareils moins performants. + + + + + ); +} diff --git a/src/components/method/MethodWorkSpaceSection.tsx b/src/components/method/MethodWorkSpaceSection.tsx new file mode 100644 index 0000000..b412dfc --- /dev/null +++ b/src/components/method/MethodWorkSpaceSection.tsx @@ -0,0 +1,181 @@ +import { useRef, useLayoutEffect, useCallback } from "react"; +import { Typography, useTheme } from "@mui/material"; +import type { Category } from "../../types/entities/categoryTypes"; +import type { Stack } from "../../types/entities/stackTypes"; +import { ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionTitle from "./SectionTitle"; +import StackGrid from "./StackGrid"; +import setupSrc from "../../assets/images/setup.png"; +import { useBreakpoints } from "../../hooks/mediaQueries"; +import { useAuthContext } from "../../context/AuthContext"; + +export default function MethodWorkspaceSection({ + stacks, + categories, +}: { + stacks: Stack[]; + categories: Category[]; +}) { + const theme = useTheme(); + const { isXl } = useBreakpoints(); + const { isAuthenticated } = useAuthContext(); + const imgRef = useRef(null); + const containerRef = useRef(null); + const offsetRef = useRef(0); + + const calculateOffset = useCallback(() => { + const img = imgRef.current; + const container = containerRef.current; + if (!img || !container || !img.naturalWidth || !img.naturalHeight) return; + + const containerWidth = container.clientWidth; + const containerHeight = container.clientHeight; + // largeur naturelle de l'image si elle remplissait la hauteur du conteneur + const naturalWidthAtHeight = + (img.naturalWidth / img.naturalHeight) * containerHeight; + const overflow = Math.max(0, naturalWidthAtHeight - containerWidth); + const offset = overflow / 2; + + offsetRef.current = offset; + img.style.width = `calc(100% + ${overflow}px)`; + img.style.marginLeft = `${-offset}px`; + }, []); + + useLayoutEffect(() => { + const img = imgRef.current; + const container = containerRef.current; + if (!img || !container) return; + + const handleScroll = () => { + const rect = img.getBoundingClientRect(); + const vh = window.innerHeight; + // progress: 0 when entering bottom of viewport, 1 when leaving top + const progress = (vh - rect.top) / (vh + rect.height); + const clamped = Math.max(0, Math.min(1, progress)); + const offset = offsetRef.current; + // shift from +offset (right) to -offset (left) + const translateX = offset - clamped * 2 * offset; + img.style.transform = `translateX(${translateX}px)`; + }; + + const init = () => { + calculateOffset(); + handleScroll(); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + window.addEventListener("resize", init); + + // Ajout d'un ResizeObserver sur le conteneur + const resizeObserver = new window.ResizeObserver(() => { + calculateOffset(); + handleScroll(); + }); + resizeObserver.observe(container); + + if (img.complete && img.naturalWidth) { + init(); + } + + return () => { + window.removeEventListener("scroll", handleScroll); + window.removeEventListener("resize", init); + resizeObserver.disconnect(); + }; + }, [calculateOffset]); + + return ( + + + + + + stack.categories?.some( + (c) => + (c as Category).parent === + categories.find( + (cat) => cat.label === "Environnement de travail", + )?.id || + (c as Category).label === "Environnement de travail", + ), + ) + .sort((a, b) => + ((a.categories?.[0] as Category)?.label ?? "").localeCompare( + (b.categories?.[0] as Category)?.label ?? "", + ), + )} + /> + + + Mon environnement de travail est pensé comme un espace de + production à part entière. + + + Je travaille principalement sous Debian. Mais j'utilise également + un environnement sous Windows 11 avec WSL et Ubuntu. L’ensemble + repose sur une stack de travail cohérente et homogène : terminal + Zsh, Homebrew, VS Code, Docker Compose pour les environnements + conteneurisés, TablePlus pour l’administration des bases de + données et Postman pour les tests et la validation des APIs. + + + L’objectif n’est pas tant de disposer d’un environnement + confortable que d’assurer une production fiable, reproductible et + maintenable, quel que soit le contexte d’exécution du projet. + + + Une place particulière est également laissée à la musique qui + m'accompagne au quotidien et fait partie intégrante de mon + équilibre de conception et de développement. + + + +
+ { + if (imgRef.current) { + calculateOffset(); + } + }} + /> +
+
+
+ ); +} diff --git a/src/components/method/MethodWorkflowsSection.tsx b/src/components/method/MethodWorkflowsSection.tsx new file mode 100644 index 0000000..14ae33b --- /dev/null +++ b/src/components/method/MethodWorkflowsSection.tsx @@ -0,0 +1,423 @@ +import { ResponsiveStack } from "../custom/ResponsiveLayout"; +import SectionTitle from "./SectionTitle"; +import { Swiper, SwiperSlide } from "swiper/react"; +import { FreeMode, Navigation, Pagination } from "swiper/modules"; +import "swiper/css"; +import "swiper/css/navigation"; +import "swiper/css/pagination"; +import "swiper/css/free-mode"; +import { Typography, useTheme } from "@mui/material"; +import { useState } from "react"; +import { useBreakpoints } from "../../hooks/mediaQueries"; +import Timeline from "@mui/lab/Timeline"; +import CustomTimelineItem from "../custom/CustomTimelineItem"; +import TimelineLink from "./TimelineLink"; +import { mdiCheck, mdiChevronLeft, mdiChevronRight, mdiReplay } from "@mdi/js"; +import CustomIconButton from "../custom/CustomIconButton"; + +export default function MethodWorkflowsSection() { + const theme = useTheme(); + const { isLg, isSm } = useBreakpoints(); + + const [swiperLocked, setSwiperLocked] = useState(false); + + return ( + + + `; + }, + }} + loop={!swiperLocked} + watchOverflow + onSwiper={(swiper) => setSwiperLocked(swiper.isLocked)} + onUpdate={(swiper) => setSwiperLocked(swiper.isLocked)} + onResize={(swiper) => setSwiperLocked(swiper.isLocked)} + style={ + { + width: `calc(100% + ${isLg ? 128 : 64}px)`, + minWidth: 0, + paddingBottom: "72px", + marginBottom: "-72px", + marginLeft: isLg ? -64 : -32, + marginRight: isLg ? -64 : -32, + paddingLeft: isLg ? 64 : isSm ? 32 : 24, + paddingRight: isLg ? 64 : isSm ? 32 : 24, + "--swiper-navigation-color": theme.palette.text.secondary, + "--swiper-pagination-color": theme.palette.text.secondary, + "--swiper-pagination-bullet-inactive-color": theme.palette.divider, + "--swiper-pagination-bullet-inactive-opacity": "1", + } as React.CSSProperties + } + > + + + + + Site statique + + + Production focalisée sur l’expérience utilisateur + + + + + Cadrage fonctionnel + + } + /> + + Wireframes{" "} + + & + +  Maquettes + + } + /> + + Développement frontend + + } + /> + + Tests{" "} + + & + +  Validation + + } + /> + + Livraison en production + + } + /> + + + Résultat : + {" "} + Site autonome + + } + dotIcon={mdiCheck} + connectorProps={{ + sx: { + background: `repeating-linear-gradient(to bottom, ${theme.palette.primary.dark} 0px, ${theme.palette.primary.dark} 2px, transparent 2px, transparent 4px)`, + }, + }} + /> + + Maintenance légère + + } + connectorProps={{ + sx: { + background: `repeating-linear-gradient(to bottom, ${theme.palette.primary.dark} 0px, ${theme.palette.primary.dark} 2px, transparent 2px, transparent 4px)`, + }, + }} + /> + + + + + + + + Produit dynamique + + + Architecture évolutive orientée produit + + + + + Analyse des besoins{" "} + + & + +  Cadrage fonctionnel + + } + /> + + Wireframes{" "} + + & + +  Maquettes + + } + /> + + Développement frontend{" "} + + & + +  backend + + } + /> + + Tests{" "} + + & + +  Validation + + } + /> + Déploiement continu + } + /> + Évolution par itérations} + dotIcon={mdiReplay} + /> + Formation + } + /> + + Livraison en production + + } + /> + + + Résulat :{" "} + + Produit évolutif + + } + dotIcon={mdiCheck} + connectorProps={{ + sx: { + background: `repeating-linear-gradient(to bottom, ${theme.palette.primary.dark} 0px, ${theme.palette.primary.dark} 2px, transparent 2px, transparent 4px)`, + }, + }} + /> + Maintenance + } + connectorProps={{ + sx: { + background: `repeating-linear-gradient(to bottom, ${theme.palette.primary.dark} 0px, ${theme.palette.primary.dark} 2px, transparent 2px, transparent 4px)`, + }, + }} + /> + + + + + + + + Produit existant + + + Adaptation à la réalité du projet + + + + + Audit technique{" "} + + & + +  fonctionnel + + } + /> + + Cartographie du système existant + + } + /> + + Priorisation des corrections + + } + /> + Évolution par itérations} + dotIcon={mdiReplay} + /> + + + Résulat :{" "} + + Système fiabilisé + + } + dotIcon={mdiCheck} + connectorProps={{ + sx: { + background: `repeating-linear-gradient(to bottom, ${theme.palette.primary.dark} 0px, ${theme.palette.primary.dark} 2px, transparent 2px, transparent 4px)`, + }, + }} + /> + Maintenance + } + connectorProps={{ + sx: { + background: `repeating-linear-gradient(to bottom, ${theme.palette.primary.dark} 0px, ${theme.palette.primary.dark} 2px, transparent 2px, transparent 4px)`, + }, + }} + /> + + + + {!swiperLocked && ( + <> + + + + )} + + + ); +} diff --git a/src/components/method/SectionCard.tsx b/src/components/method/SectionCard.tsx new file mode 100644 index 0000000..ca0b60e --- /dev/null +++ b/src/components/method/SectionCard.tsx @@ -0,0 +1,65 @@ +import { Card, CardContent, useTheme, type CardProps } from "@mui/material"; +import { useBreakpoints } from "../../hooks/mediaQueries"; +import StretchyTypography from "../custom/StretchyTypography"; +import { useRef } from "react"; + +export default function SectionCard({ + title, + children, + invisible = false, + ...props +}: { + children: React.ReactNode; + title?: string; + invisible?: boolean; +} & CardProps) { + const theme = useTheme(); + const { isMd, isSm } = useBreakpoints(); + const containerRef = useRef(null); + + return ( + + + {title && ( + + {title} + + )} + {children} + + + ); +} diff --git a/src/components/method/SectionTitle.tsx b/src/components/method/SectionTitle.tsx new file mode 100644 index 0000000..d5ca5bf --- /dev/null +++ b/src/components/method/SectionTitle.tsx @@ -0,0 +1,50 @@ +import { ResponsiveStack } from "../custom/ResponsiveLayout"; +import { Typography, useTheme } from "@mui/material"; + +export default function SectionTitle({ + title, + titleColor, + subtitle, + subtitleColor, + nowrap = true, +}: { + title: React.ReactNode; + subtitle: React.ReactNode; + titleColor?: string; + subtitleColor?: string; + nowrap?: boolean; +}) { + const theme = useTheme(); + + return ( + + + {title} + + + {subtitle} + + + ); +} diff --git a/src/components/method/StackGrid.tsx b/src/components/method/StackGrid.tsx new file mode 100644 index 0000000..930f558 --- /dev/null +++ b/src/components/method/StackGrid.tsx @@ -0,0 +1,29 @@ +import type { Media } from "../../types/entities/mediaTypes"; +import type { Stack } from "../../types/entities/stackTypes"; +import Picture from "../custom/Picture"; +import { ResponsiveBox } from "../custom/ResponsiveLayout"; + +export default function StackGrid({ stacks }: { stacks: Stack[] }) { + return ( + + {stacks.map( + (stack) => + stack.icon && ( + + ), + )} + + ); +} diff --git a/src/components/method/TimelineLink.tsx b/src/components/method/TimelineLink.tsx new file mode 100644 index 0000000..c61a624 --- /dev/null +++ b/src/components/method/TimelineLink.tsx @@ -0,0 +1,47 @@ +import { Link, useTheme } from "@mui/material"; +import { useAuthContext } from "../../context/AuthContext"; + +export default function TimelineLink({ + href, + children, +}: { + href: string; + children: React.ReactNode; +}) { + const theme = useTheme(); + const { isAuthenticated } = useAuthContext(); + return ( + { + e.preventDefault(); + const id = href.replace("#", ""); + const el = document.getElementById(id); + if (el) { + const y = + el.getBoundingClientRect().top + + window.scrollY - + (isAuthenticated ? 180 : 132); + window.scrollTo({ top: y, behavior: "smooth" }); + } + }} + sx={{ + color: theme.palette.text.primary, + textDecoration: "none", + display: "inline", + backgroundImage: `linear-gradient(${theme.palette.primary.main}, ${theme.palette.primary.main})`, + backgroundRepeat: "no-repeat", + backgroundPosition: "100% calc(100%)", + backgroundSize: "0% 1px", + transition: `color ${theme.transitions.duration.standard}ms ${theme.transitions.easing.easeInOut}, background-size ${theme.transitions.duration.standard}ms ${theme.transitions.easing.easeInOut}`, + "&:hover": { + color: theme.palette.primary.main, + backgroundSize: "100% 1px", + backgroundPosition: "0% calc(100%)", + }, + }} + > + {children} + + ); +} diff --git a/src/hooks/queries/useStacks.tsx b/src/hooks/queries/useStacks.tsx index 5e2d51b..705413b 100644 --- a/src/hooks/queries/useStacks.tsx +++ b/src/hooks/queries/useStacks.tsx @@ -3,7 +3,7 @@ import { STACKS_QUERY } from "../../services/stack/stackQueries"; import useCategories from "./useCategories"; import type { Stack } from "../../types/entities/stackTypes"; import useMedias from "./useMedias"; -import { normalizeRef } from "../../utils/normalizeRef"; +import { normalizeRef, normalizeRefs } from "../../utils/normalizeRef"; export default function useStacks() { const { categories } = useCategories(); @@ -15,7 +15,7 @@ export default function useStacks() { const stacks = (data?.stacks ?? []).map((stack) => ({ ...stack, icon: normalizeRef(stack.icon, medias), - category: normalizeRef(stack.category, categories), + categories: normalizeRefs(stack.categories ?? [], categories), })); return { stacks, loading, error, refetch }; } diff --git a/src/layout/public/PublicFooter.tsx b/src/layout/public/PublicFooter.tsx index 3e863c6..c8506a6 100644 --- a/src/layout/public/PublicFooter.tsx +++ b/src/layout/public/PublicFooter.tsx @@ -19,7 +19,11 @@ import { useContactForm } from "../../context/ContactFormContext"; * Pied de page pour les pages publiques, affichant un message de copyright. * Utilisé sur les pages d'accueil, de connexion, etc. */ -export default function PublicFooter() { +export default function PublicFooter({ + ref, +}: { + ref: React.Ref; +}) { const { maintenanceMode, loading } = useSettings(); const { isAuthenticated } = useAuthContext(); @@ -50,6 +54,7 @@ export default function PublicFooter() { return ( <> (null); + const [footerHeight, setFooterHeight] = useState(48); + + useLayoutEffect(() => { + const updateFooterHeight = () => { + if (footerRef.current) { + setFooterHeight(footerRef.current.offsetHeight); + } + }; + updateFooterHeight(); + window.addEventListener("resize", updateFooterHeight); + return () => window.removeEventListener("resize", updateFooterHeight); + }, []); + return ( {isAuthenticated ? : null} {children} - + ); diff --git a/src/pages/admin/StackPage.tsx b/src/pages/admin/StackPage.tsx index 9d546e1..4e0d693 100644 --- a/src/pages/admin/StackPage.tsx +++ b/src/pages/admin/StackPage.tsx @@ -45,12 +45,13 @@ export default function Stacks() { content: (item: Stack) => stripHtml(item.label), }, { - key: "category", - label: "Catégorie", + key: "categories", + label: "Catégories", content: (item: Stack) => - typeof item.category === "string" - ? stripHtml(item.category) - : stripHtml(item.category?.label || ""), + item.categories + ?.map((c) => (typeof c === "string" ? c : c.label)) + .map(stripHtml) + .join(", ") || "", }, { key: "versions", diff --git a/src/pages/public/MethodPage.tsx b/src/pages/public/MethodPage.tsx new file mode 100644 index 0000000..49faf17 --- /dev/null +++ b/src/pages/public/MethodPage.tsx @@ -0,0 +1,130 @@ +import { Typography } from "@mui/material"; +import Layout from "../../layout"; +import { ResponsiveStack } from "../../components/custom/ResponsiveLayout"; +import useStacks from "../../hooks/queries/useStacks"; +import Picture from "../../components/custom/Picture"; +import useCategories from "../../hooks/queries/useCategories"; +import StickyMenuBar from "../../components/custom/StickyMenuBar"; +import CallToAction from "../../components/CallToAction"; +import { useMemo, useState } from "react"; + +import MultipleMarquee from "../../components/custom/MultipleMarquee"; +import MethodWorkflowsSection from "../../components/method/MethodWorkflowsSection"; +import MethodProjectManagementSection from "../../components/method/MethodProjectManagementSection"; +import MethodDesignSection from "../../components/method/MethodDesignSection"; +import MethodUXSection from "../../components/method/MethodUXSection"; +import MethodDevSection from "../../components/method/MethodDevSection"; +import MethodIAInfraSection from "../../components/method/MethodIAInfraSection"; +import MethodWorkspaceSection from "../../components/method/MethodWorkSpaceSection"; +import MethodContractSection from "../../components/method/MethodContractSection"; +import { useResponsiveWidth } from "../../hooks/layout/useResponsiveWidth"; + +export default function MethodPage() { + const introWidth = useResponsiveWidth(6); + + const { stacks } = useStacks(); + const { categories } = useCategories(); + + const [stackShuffleSeed] = useState(() => Math.random().toString(36)); + + const shuffledStacks = useMemo(() => { + const hashWithSeed = (value: string) => { + const input = `${stackShuffleSeed}:${value}`; + let hash = 2166136261; + for (let i = 0; i < input.length; i++) { + hash ^= input.charCodeAt(i); + hash = Math.imul(hash, 16777619); + } + return hash >>> 0; + }; + + return [...stacks].sort( + (a, b) => hashWithSeed(String(a.id)) - hashWithSeed(String(b.id)), + ); + }, [stacks, stackShuffleSeed]); + + return ( + <> + + + {/* Header */} + + + Dans la pratique + + + Cette charte rassemble les intentions, les principes et les + exigences qui structurent ma manière d’aborder chaque projet - + envisagé comme un produit à part entière aux enjeux réels et pensé + pour être exploitable dans le temps. + + + + + + + + + + + + + {/* Footer */} + + + {shuffledStacks + .filter((stack) => !!stack.icon) + .map((stack) => ( + + ))} + + + + + + ); +} diff --git a/src/services/stack/stackQueries.ts b/src/services/stack/stackQueries.ts index 5fe56ef..17f950c 100644 --- a/src/services/stack/stackQueries.ts +++ b/src/services/stack/stackQueries.ts @@ -10,7 +10,7 @@ export const STACKS_QUERY = gql` description versions skills - category + categories } } `; diff --git a/src/types/entities/stackTypes.ts b/src/types/entities/stackTypes.ts index fcf5fd3..d1a2e72 100644 --- a/src/types/entities/stackTypes.ts +++ b/src/types/entities/stackTypes.ts @@ -14,7 +14,7 @@ export interface Stack { description?: string; versions: string[]; skills: string[]; - category?: string | Category; + categories?: (string | Category)[]; } export interface StackFormDialogProps extends EntityFormDialogProps {