"use client"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; import { getApiUrl } from "../utils/getApiUrl"; import CarouselCompetences from "./CarouselCompetences"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import ModalGlossaire from "./ModalGlossaire"; interface ImageData { url: string; formats?: { large?: { url: string }; }; name?: string; } interface CompetenceData { name: string; content: string; picture?: ImageData[]; } interface GlossaireItem { mot_clef: string; slug: string; variantes: string[]; description: string; images?: ImageData[]; } interface ContentSectionProps { competenceData: CompetenceData | null; glossaireData: GlossaireItem[]; titleClass?: string; contentClass?: string; } /** * Fiche détail compétences — refonte "Digital Atelier" (étape 7.c). * * Trois changements structurants par rapport à la version pré-refonte : * * 1. **Style tokenisé** : même gabarit "feuillet de vellum" que le portfolio. * Les classes hardcodées `bg-white/70 text-blue-700 font-headline font-bold` * disparaissent. Le corps éditorial est rendu en `prose` Newsreader, les * titres Markdown en Manrope `text-primary`. * * 2. **Keywords glossaire & chatbot sans styles inline** : on retire les * `style="color: red/blue; cursor: pointer"` injectés dans le HTML. On * conserve les classes `.keyword` / `.chatbot-keyword` historiques et on * les stylise via `globals.css` avec la palette Stitch (voir `.glossary-keyword`). * Pour rester rétro-compatible avec les classes historiques, `keyword` est * renommée `glossary-keyword` dans la transformation. * * 3. **Event listeners scopés au wrapper** (ref `contentRef`) plutôt que * `document.body.addEventListener`. Avant : risque de fuite + interaction * avec d'autres parties du DOM. Après : la zone "contenu" capture ses clics * en bubbling, comportement identique mais sans effet de bord global. * * 4. **Chatbot via FAB global** (étape 7.e) : plus de `` local dans * cette fiche. Un clic sur "IA locale" dispatch `CustomEvent("grasbot:open")` * que le FAB monté dans `layout.tsx` écoute pour ouvrir le chatbot partagé. */ export default function ContentSectionCompetences({ competenceData, glossaireData, }: ContentSectionProps) { const [selectedMot, setSelectedMot] = useState(null); const contentRef = useRef(null); const apiUrl = getApiUrl(); // Délégation locale : capte les clics sur les keywords injectés dans le Markdown, // sans polluer document.body comme avant la refonte. useEffect(() => { const node = contentRef.current; if (!node) return; const handleClick = (event: Event) => { const target = event.target as HTMLElement; if (target.dataset?.chatbot === "true") { window.dispatchEvent(new CustomEvent("grasbot:open")); return; } if (target.classList?.contains("glossary-keyword")) { const mot = target.getAttribute("data-mot"); if (!mot) return; const glossaireMot = glossaireData.find((g) => g.mot_clef === mot); setSelectedMot(glossaireMot || null); } }; node.addEventListener("click", handleClick); return () => node.removeEventListener("click", handleClick); }, [glossaireData]); if (!competenceData) { return (

Cette compétence est introuvable.

Retour aux compétences
); } const { name, content, picture } = competenceData; const images = picture?.map((img) => ({ url: `${apiUrl}${img.formats?.large?.url || img.url}`, alt: img.name || `Visuel de la compétence ${name}`, })) || []; /** * Transforme le Markdown en injectant des spans `.glossary-keyword` / `.chatbot-keyword` * autour des mots-clés trouvés. Les styles sont définis dans `globals.css` * (palette Stitch, soulignement pointillé) plutôt qu'inline dans l'attribut style. */ function transformMarkdownWithKeywords(text: string) { if (!text) return ""; let modifiedText = text; modifiedText = modifiedText.replace( /\bIA locale\b/g, `IA locale` ); if (glossaireData.length) { glossaireData.forEach(({ mot_clef, variantes }) => { const regexVariants = variantes .map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) .join("|"); const regex = new RegExp(`\\b(${mot_clef}|${regexVariants})\\b`, "gi"); modifiedText = modifiedText.replace(regex, (match) => { return `${match}`; }); }); } return modifiedText; } const contentWithLinks = transformMarkdownWithKeywords(content); return (
Compétences
Compétence · Savoir-faire

{name}

{images.length > 0 && (
)}
{contentWithLinks}
{selectedMot && ( setSelectedMot(null)} /> )}
); }