maj majeur opengraph et page accueil

This commit is contained in:
sebvtl728 2025-10-26 03:24:39 +01:00
parent e7e5c5e3fd
commit f4cd293805
5 changed files with 659 additions and 91 deletions

View File

@ -8,15 +8,15 @@
<!-- ✅ Ajout Open Graph statique -->
<meta
property="og:title"
content="Titre par défaut - Centre de formations"
content="Centre de formations web IA"
/>
<meta
property="og:description"
content="Bienvenue sur notre Centre de formations aux métiers du numérique."
content="Bienvenue sur notre Centre de formations aux métiers du numérique en Vendée"
/>
<meta
property="og:image"
content="https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/01/Construction-logements-au-Mans-01.avif"
content="https://res.cloudinary.com/dh5qgexjo/image/upload/v1753856912/centre-formation-metier-numerique_iwxjzv.avif"
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://ton-site.com" />
@ -25,15 +25,15 @@
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Titre par défaut - Centre de formations"
content="Centre de formations web IA"
/>
<meta
name="twitter:description"
content="Bienvenue sur notre Centre de formations aux métiers du numérique."
content="Bienvenue sur notre Centre de formations aux métiers du numérique en Vendée."
/>
<meta
name="twitter:image"
content="https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/01/Construction-logements-au-Mans-01.avif"
content="https://res.cloudinary.com/dh5qgexjo/image/upload/v1753856912/centre-formation-metier-numerique_iwxjzv.avif"
/>
<!-- <link rel="preload" href="/src/assets/vendor-BFTbvl5C.js" as="script"> -->
<link rel="preload" href="/assets/index-CfManBR6.css" as="style" />

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useMemo } from "react";
import {
Box,
Typography,
@ -21,77 +21,42 @@ const ConstructSection = ({
missionTitle,
missionItems,
}) => {
// État pour gérer l'ouverture des accordéons
const [expanded, setExpanded] = useState(false);
const [missions, setMissions] = useState([]);
const [missions, setMissions] = useState(
Array.isArray(missionItems) && missionItems.length ? missionItems : []
);
const defaultMissions = useMemo(
() => [
{ title: "📌 Mission 1", details: "Détails de la mission 1" },
{ title: "📌 Mission 2", details: "Détails de la mission 2" },
{ title: "📌 Mission 3", details: "Détails de la mission 3" },
],
[]
);
// Chargement initial des missions depuis localStorage ou WordPress
useEffect(() => {
const loadMissions = async () => {
const hydrateMissions = async () => {
if (Array.isArray(missionItems) && missionItems.length) {
setMissions(missionItems);
return;
}
try {
// 1 Vérifier si localStorage contient des missions
const storedMissions = JSON.parse(localStorage.getItem("missions"));
if (storedMissions && storedMissions.length > 0) {
setMissions(storedMissions);
console.log("✅ Missions chargées depuis localStorage !");
const response = await api.get("wp/v2/pages/13?_fields=acf");
const fetched = response.data.acf?.mission;
if (Array.isArray(fetched) && fetched.length) {
setMissions(fetched);
} else {
// 2 Si localStorage est vide, récupérer les missions depuis WordPress
const response = await api.get("wp/v2/pages/13?_fields=acf");
if (
response.data.acf?.mission &&
Array.isArray(response.data.acf.mission)
) {
setMissions(response.data.acf.mission);
localStorage.setItem(
"missions",
JSON.stringify(response.data.acf.mission)
); // Sauvegarde en local
console.log("✅ Missions chargées depuis WordPress !");
} else {
// 3 Si aucune mission dans WordPress, utiliser des valeurs par défaut
const defaultMissions = [
{ title: "📌 Mission 1", details: "Détails de la mission 1" },
{ title: "📌 Mission 2", details: "Détails de la mission 2" },
{ title: "📌 Mission 3", details: "Détails de la mission 3" },
];
setMissions(defaultMissions);
localStorage.setItem("missions", JSON.stringify(defaultMissions));
console.log(
"⚠️ Aucune mission trouvée, valeurs par défaut utilisées !"
);
}
setMissions(defaultMissions);
}
} catch (error) {
console.error("❌ Erreur lors du chargement des missions :", error);
setMissions(defaultMissions);
}
};
loadMissions();
}, []);
// Mise à jour automatique de localStorage quand missions change
useEffect(() => {
if (missions.length > 0) {
localStorage.setItem("missions", JSON.stringify(missions));
}
}, [missions]);
// Gestion des changements depuis d'autres onglets/navigateurs
useEffect(() => {
const handleStorageChange = () => {
const updatedMissions =
JSON.parse(localStorage.getItem("missions")) || [];
setMissions(updatedMissions);
};
window.addEventListener("storage", handleStorageChange);
return () => {
window.removeEventListener("storage", handleStorageChange);
};
}, []);
hydrateMissions();
}, [missionItems, defaultMissions]);
// Gestion des accordéons
const handleAccordionChange = (panel) => (event, isExpanded) => {

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useRef, useEffect } from "react";
import { Box, Grid, Typography, Card, CardContent, Modal } from "@mui/material";
import pictoMaitriseOeuvre from "../assets/picto-maitrise-oeuvre.avif";
import pictoStructure from "../assets/picto-structure.avif";
@ -9,13 +9,14 @@ import PropTypes from "prop-types";
const modalStyle = {
padding: 4,
borderRadius: "20px",
background: "rgba(255, 255, 255, 0.9)",
background: "rgba(255, 255, 255, 0.95)",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
backdropFilter: "blur(10px)",
maxWidth: 600,
margin: "auto",
mt: 4,
position: "relative",
maxHeight: "80vh",
};
// Style commun pour l'icône affichée en haut à droite dans le modal
@ -54,19 +55,74 @@ const cardStyle = (image) => ({
backgroundPosition: "center left",
});
const ScrollableContent = ({ text }) => {
const contentRef = useRef(null);
const [showHint, setShowHint] = useState(false);
useEffect(() => {
const node = contentRef.current;
if (!node) return;
const updateHintVisibility = () => {
const needsScroll = node.scrollHeight > node.clientHeight;
const nearBottom =
node.scrollTop >= node.scrollHeight - node.clientHeight - 12;
setShowHint(needsScroll && !nearBottom);
};
updateHintVisibility();
node.addEventListener("scroll", updateHintVisibility);
return () => node.removeEventListener("scroll", updateHintVisibility);
}, [text]);
return (
<Box sx={{ position: "relative" }}>
<Typography
ref={contentRef}
variant="body1"
sx={{
color: "#333",
lineHeight: 1.6,
maxHeight: "60vh",
overflowY: "auto",
pr: 1,
}}
dangerouslySetInnerHTML={{ __html: text }}
/>
{showHint && (
<Box
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
pt: 4,
textAlign: "center",
background:
"linear-gradient(rgba(255,255,255,0), rgba(255,255,255,0.95))",
}}
>
<Typography
variant="caption"
sx={{ color: "#0e467f", fontWeight: 600, letterSpacing: 0.5 }}
>
Faites défiler pour tout lire
</Typography>
</Box>
)}
</Box>
);
};
// Composant Modal réutilisable
const ExpertiseModal = ({ modalId, image, title, text, openModal, handleClose }) => (
<Modal open={openModal === modalId} onClose={handleClose}>
<Box sx={modalStyle}>
<Box sx={{ ...modalStyle, maxHeight: "80vh" }}>
<Box sx={{ ...iconStyle, backgroundImage: `url(${image})` }} />
<Typography variant="h4" sx={{ mb: 2, fontWeight: "bold" }}>
{title}
</Typography>
<Typography
variant="body1"
sx={{ color: "#333", lineHeight: 1.6 }}
dangerouslySetInnerHTML={{ __html: text }}
/>
<ScrollableContent text={text} />
<button onClick={handleClose} style={buttonStyle}>
Retour
</button>
@ -233,4 +289,4 @@ Expertices.propTypes = {
};
export default Expertices;
export default Expertices;

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { updateHomePageACF, updateRankMathMeta, uploadImage } from "../../wordpress";
import { updateHomePageACF, updateRankMathMeta, uploadImage, getPageById } from "../../wordpress";
import { getToken } from "../../auth";
import api from "../../api";
import {
@ -12,9 +12,17 @@ import {
Box,
CircularProgress,
Stack,
Dialog,
DialogTitle,
DialogContent,
IconButton,
Slider,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import ImageUploaderCloudinary from "../ImageUploaderCloudinary";
import CloudinaryGallerySelector from "../CloudinaryGallerySelector";
const HOMEPAGE_ID = 13;
const HOMEPAGE_URL = "https://preprod.octopusdesign.fr/api-octopus/server/";
@ -51,9 +59,22 @@ const GestionPageAccueil = () => {
const [seoOgDescription, setSeoOgDescription] = useState("");
const [seoOgImage, setSeoOgImage] = useState("");
const [seoRobots, setSeoRobots] = useState("");
const [seoFocusKeyword, setSeoFocusKeyword] = useState("");
const [seoLoading, setSeoLoading] = useState(true);
const [seoError, setSeoError] = useState(null);
const [imageDialog, setImageDialog] = useState({
open: false,
title: "",
setter: null,
getter: null,
mode: "insert",
targetIndex: null,
width: 100,
alt: "",
});
const [ogDialog, setOgDialog] = useState({ open: false, source: "" });
const glassCardSx = {
background: "rgba(7, 13, 28, 0.82)",
border: "1px solid rgba(132, 169, 255, 0.18)",
@ -63,7 +84,6 @@ const GestionPageAccueil = () => {
backdropFilter: "blur(18px)",
color: "#f4f7ff",
};
const sectionTitleSx = {
fontWeight: "bold",
fontSize: { xs: "1.3rem", md: "1.6rem" },
@ -125,6 +145,284 @@ const GestionPageAccueil = () => {
},
};
const extractExpertiseTitle = (value) => {
if (!value) return "";
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
if (typeof parsed === "string") return parsed;
if (parsed?.titre_expertise) return parsed.titre_expertise;
} catch (err) {
return value;
}
return value;
}
if (typeof value === "object" && value.titre_expertise) {
return value.titre_expertise;
}
return "";
};
const openImageDialog = (
title,
getter,
setter,
mode = "insert",
targetIndex = null,
initialAlt = ""
) => {
setImageDialog({
open: true,
title,
setter,
getter,
mode,
targetIndex,
width: 100,
alt: initialAlt || "",
});
};
const closeImageDialog = () =>
setImageDialog({
open: false,
title: "",
setter: null,
getter: null,
mode: "insert",
targetIndex: null,
width: 100,
alt: "",
});
const buildImageMarkup = (url, width, alt = "") =>
`<p><img src="${url}" alt="${alt?.replace(/"/g, "&quot;") || ""}" data-custom-width="${width}" width="${width}%" style="width:${width}%;max-width:100%;height:auto;" /></p>`;
const handleImageSelected = (url) => {
if (!url || !imageDialog.setter) return;
const targetWidth = imageDialog.width || 100;
const insertMarkup = () => {
const imageMarkup = buildImageMarkup(url, targetWidth, imageDialog.alt);
imageDialog.setter((prev) => `${prev || ""}${imageMarkup}`);
};
const replaceImage = () => {
if (typeof window === "undefined" || !imageDialog.getter) {
insertMarkup();
return;
}
const currentHtml = imageDialog.getter() || "";
const parser = new window.DOMParser();
const doc = parser.parseFromString(currentHtml, "text/html");
const images = doc.body.querySelectorAll("img");
const target = images[imageDialog.targetIndex ?? -1];
if (target) {
target.setAttribute("src", url);
target.setAttribute("style", `width:${targetWidth}%;max-width:100%;height:auto;`);
target.setAttribute("data-custom-width", String(targetWidth));
target.setAttribute("width", `${targetWidth}%`);
target.setAttribute("alt", imageDialog.alt || target.getAttribute("alt") || "");
imageDialog.setter(doc.body.innerHTML);
} else {
insertMarkup();
}
};
if (imageDialog.mode === "replace") {
replaceImage();
} else {
insertMarkup();
}
closeImageDialog();
};
const getImagesFromHtml = (html) => {
if (typeof window === "undefined" || !html) return [];
const parser = new window.DOMParser();
const doc = parser.parseFromString(html, "text/html");
return Array.from(doc.body.querySelectorAll("img")).map((img) => {
const dataWidth = img.getAttribute("data-custom-width");
if (dataWidth) {
return {
src: img.getAttribute("src"),
width: Number(dataWidth) || 100,
alt: img.getAttribute("alt") || "",
};
}
const attrWidth = img.getAttribute("width");
if (attrWidth && attrWidth.includes("%")) {
return {
src: img.getAttribute("src"),
width: Number(attrWidth.replace("%", "")) || 100,
alt: img.getAttribute("alt") || "",
};
}
const styleAttr = img.getAttribute("style") || "";
const widthMatch = styleAttr.match(/width:\s*(\d+(?:\.\d+)?)%/i);
return {
src: img.getAttribute("src"),
width: widthMatch ? Number(widthMatch[1]) : 100,
alt: img.getAttribute("alt") || "",
};
});
};
const removeImageAt = (getter, setter, index) => {
if (typeof window === "undefined") return;
const currentHtml = getter() || "";
const parser = new window.DOMParser();
const doc = parser.parseFromString(currentHtml, "text/html");
const images = doc.body.querySelectorAll("img");
const target = images[index];
if (target) {
target.remove();
setter(doc.body.innerHTML);
}
};
const updateImageWidth = (getter, setter, index, width) => {
if (typeof window === "undefined") return;
const currentHtml = getter() || "";
const parser = new window.DOMParser();
const doc = parser.parseFromString(currentHtml, "text/html");
const images = doc.body.querySelectorAll("img");
const target = images[index];
if (target) {
target.setAttribute("style", `width:${width}%;max-width:100%;height:auto;`);
target.setAttribute("data-custom-width", String(width));
target.setAttribute("width", `${width}%`);
setter(doc.body.innerHTML);
}
};
const updateImageAlt = (getter, setter, index, alt) => {
if (typeof window === "undefined") return;
const currentHtml = getter() || "";
const parser = new window.DOMParser();
const doc = parser.parseFromString(currentHtml, "text/html");
const images = doc.body.querySelectorAll("img");
const target = images[index];
if (target) {
target.setAttribute("alt", alt);
setter(doc.body.innerHTML);
}
};
const renderImageManager = (label, getter, setter, key) => {
const images = getImagesFromHtml(getter());
if (!images.length) return null;
return (
<Box sx={{ mt: 2 }}>
<Typography sx={{ fontWeight: "bold", mb: 1 }}>
Images insérées {label}
</Typography>
<Stack spacing={2}>
{images.map((img, index) => (
<Paper
key={`${key}-${index}`}
variant="outlined"
sx={{
p: 2,
borderRadius: 3,
borderColor: "rgba(255,255,255,0.15)",
backgroundColor: "rgba(2,6,18,0.6)",
}}
>
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={2}
alignItems="center"
>
<Box
component="img"
src={img.src}
alt={`${label}-${index}`}
sx={{
width: 120,
height: 80,
objectFit: "cover",
borderRadius: 2,
border: "1px solid rgba(255,255,255,0.15)",
}}
/>
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={2}
alignItems="center"
flex={1}
>
<Box sx={{ minWidth: 220 }}>
<Typography
variant="caption"
sx={{ color: "rgba(255,255,255,0.7)", mb: 0.5, display: "block" }}
>
Largeur : {img.width}%
</Typography>
<Slider
value={img.width}
min={10}
max={100}
step={1}
onChange={(_, value) =>
updateImageWidth(
getter,
setter,
index,
Array.isArray(value) ? value[0] : value
)
}
sx={{
color: "#45d6ff",
}}
/>
</Box>
<Button
variant="contained"
sx={primaryButtonSx}
onClick={() =>
openImageDialog(
`Remplacer ${label}`,
getter,
setter,
"replace",
index,
img.alt
)
}
>
Remplacer
</Button>
<TextField
label="Texte alternatif"
fullWidth
variant="outlined"
value={img.alt}
onChange={(e) =>
updateImageAlt(getter, setter, index, e.target.value)
}
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<Button
color="error"
variant="outlined"
sx={ghostButtonSx}
onClick={() => removeImageAt(getter, setter, index)}
>
Supprimer
</Button>
</Stack>
</Stack>
</Paper>
))}
</Stack>
</Box>
);
};
const loadSeoMeta = useCallback(async () => {
try {
setSeoLoading(true);
@ -220,7 +518,7 @@ const GestionPageAccueil = () => {
// Expertises
setexpertiseTitle(data.acf?.titre_expertise || "");
setexpertiseTitle(extractExpertiseTitle(data.acf?.titre_expertise));
setexpertise1Title(data.acf?.gdc_expert?.titre_expertise_1 || "");
setexpertise1Text(data.acf?.gdc_expert?.text_expertise_1 || "");
setexpertise2Title(data.acf?.gdc_expert?.titre_expertise_2 || "");
@ -240,12 +538,33 @@ const GestionPageAccueil = () => {
loadSeoMeta();
}, [loadSeoMeta]);
useEffect(() => {
const fetchFocusKeyword = async () => {
try {
const pageData = await getPageById(HOMEPAGE_ID);
const focus =
pageData?._rank_math_seo_meta?.focus_keyword ||
pageData?._rank_math_seo_meta?.primary_keyword ||
"";
setSeoFocusKeyword(focus);
} catch (error) {
console.error("❌ Erreur chargement mot-clé principal :", error);
}
};
fetchFocusKeyword();
}, []);
// Enregistrement ACF + Rank Math
const handleSave = async () => {
try {
setError(null);
setSuccessMessage("");
const formattedExpertiseTitle =
typeof expertiseTitle === "string"
? { titre_expertise: expertiseTitle }
: expertiseTitle || { titre_expertise: "" };
const newData = {
hero_title: heroTitle,
hero_text: heroText,
@ -259,7 +578,7 @@ const GestionPageAccueil = () => {
construct_text: constructText,
},
gdc_expert: {
titre_expertise: expertiseTitle,
titre_expertise: formattedExpertiseTitle,
titre_expertise_1: expertise1Title,
text_expertise_1: expertise1Text,
titre_expertise_2: expertise2Title,
@ -282,6 +601,7 @@ const GestionPageAccueil = () => {
rank_math_twitter_description: seoOgDescription || seoDescription,
rank_math_twitter_image: seoOgImage,
rank_math_robots: seoRobots,
rank_math_focus_keyword: seoFocusKeyword,
});
localStorage.setItem("missions", JSON.stringify(missions));
@ -543,6 +863,16 @@ const GestionPageAccueil = () => {
</Box>
) : (
<Stack spacing={2}>
<TextField
label="Mot-clé principal"
fullWidth
variant="outlined"
value={seoFocusKeyword}
onChange={(e) => setSeoFocusKeyword(e.target.value)}
helperText="Mot-clé Rank Math principal (focus keyword)."
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="Meta Title"
fullWidth
@ -593,16 +923,48 @@ const GestionPageAccueil = () => {
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<TextField
label="Image OpenGraph (URL)"
fullWidth
variant="outlined"
value={seoOgImage}
onChange={(e) => setSeoOgImage(e.target.value)}
placeholder="https://..."
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<Stack spacing={1}>
<TextField
label="Image OpenGraph (URL)"
fullWidth
variant="outlined"
value={seoOgImage}
onChange={(e) => setSeoOgImage(e.target.value)}
placeholder="https://..."
InputLabelProps={{ sx: labelSx }}
sx={textFieldSx}
/>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<Button
variant="outlined"
sx={{ ...ghostButtonSx, flex: 1 }}
onClick={() => setOgDialog({ open: true, source: "upload" })}
>
Importer depuis l'ordinateur
</Button>
<Button
variant="outlined"
sx={{ ...ghostButtonSx, flex: 1 }}
onClick={() => setOgDialog({ open: true, source: "library" })}
>
Choisir dans Cloudinary
</Button>
</Stack>
{seoOgImage && (
<Box
component="img"
src={seoOgImage}
alt="Aperçu OG"
sx={{
width: "100%",
maxHeight: 180,
objectFit: "cover",
borderRadius: 2,
border: "1px solid rgba(255,255,255,0.15)",
}}
/>
)}
</Stack>
<TextField
label="Robots"
fullWidth
@ -727,6 +1089,28 @@ const GestionPageAccueil = () => {
onChange={setConstructText}
style={quillStyle}
/>
<Button
variant="outlined"
sx={{ ...ghostButtonSx, mt: 1 }}
onClick={() =>
openImageDialog(
"Texte Construct",
() => constructText,
setConstructText,
"insert",
null,
"Image texte construct"
)
}
>
Insérer une image Cloudinary
</Button>
{renderImageManager(
"Texte Construct",
() => constructText,
setConstructText,
"construct-text"
)}
</Box>
<Box>
@ -737,6 +1121,28 @@ const GestionPageAccueil = () => {
onChange={setConstructNote}
style={quillStyle}
/>
<Button
variant="outlined"
sx={{ ...ghostButtonSx, mt: 1 }}
onClick={() =>
openImageDialog(
"Notes Construct",
() => constructNote,
setConstructNote,
"insert",
null,
"Image notes construct"
)
}
>
Insérer une image Cloudinary
</Button>
{renderImageManager(
"Notes Construct",
() => constructNote,
setConstructNote,
"construct-note"
)}
</Box>
</Stack>
</Box>
@ -779,6 +1185,18 @@ const GestionPageAccueil = () => {
: index === 2
? setexpertise2Text
: setexpertise3Text;
const cloudKey =
index === 1
? "expertise1"
: index === 2
? "expertise2"
: "expertise3";
const getTextValue = () =>
index === 1
? expertise1Text
: index === 2
? expertise2Text
: expertise3Text;
return (
<Box
@ -808,6 +1226,28 @@ const GestionPageAccueil = () => {
onChange={setText}
style={quillStyle}
/>
<Button
variant="outlined"
sx={{ ...ghostButtonSx, mt: 1 }}
onClick={() =>
openImageDialog(
`Expertise ${index}`,
getTextValue,
setText,
"insert",
null,
`Image expertise ${index}`
)
}
>
Insérer une image Cloudinary
</Button>
{renderImageManager(
`Expertise ${index}`,
getTextValue,
setText,
`expertise-${index}`
)}
</Box>
);
})}
@ -831,6 +1271,113 @@ const GestionPageAccueil = () => {
</Stack>
)}
</Container>
<Dialog
open={imageDialog.open}
onClose={closeImageDialog}
fullWidth
maxWidth="md"
>
<DialogTitle
sx={{
pr: 6,
fontWeight: "bold",
color: "#0b1b3a",
}}
>
{imageDialog.title || "Ajouter une image"}
<IconButton
aria-label="Fermer"
onClick={closeImageDialog}
sx={{ position: "absolute", right: 16, top: 16 }}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers>
<Box sx={{ mb: 3 }}>
<TextField
label="Largeur (%)"
type="number"
value={imageDialog.width}
onChange={(e) =>
setImageDialog((prev) => ({
...prev,
width: Math.min(100, Math.max(10, Number(e.target.value) || 100)),
}))
}
InputLabelProps={{ sx: { color: "#0b1b3a" } }}
/>
</Box>
<Box sx={{ mb: 3 }}>
<TextField
label="Texte alternatif (description)"
fullWidth
value={imageDialog.alt}
onChange={(e) =>
setImageDialog((prev) => ({
...prev,
alt: e.target.value,
}))
}
helperText="Décrivez brièvement l'image pour l'accessibilité."
InputLabelProps={{ sx: { color: "#0b1b3a" } }}
/>
</Box>
<Typography variant="subtitle1" sx={{ fontWeight: "bold", mb: 1 }}>
Importer depuis votre ordinateur :
</Typography>
<ImageUploaderCloudinary onUploadSuccess={handleImageSelected} />
<Box sx={{ mt: 4 }}>
<Typography variant="subtitle1" sx={{ fontWeight: "bold", mb: 1 }}>
Ou sélectionner une image existante :
</Typography>
<CloudinaryGallerySelector onSelect={handleImageSelected} />
</Box>
</DialogContent>
</Dialog>
<Dialog
open={ogDialog.open}
onClose={() => setOgDialog({ open: false, source: "" })}
fullWidth
maxWidth="md"
>
<DialogTitle
sx={{
pr: 6,
fontWeight: "bold",
color: "#0b1b3a",
}}
>
Image OpenGraph
<IconButton
aria-label="Fermer"
onClick={() => setOgDialog({ open: false, source: "" })}
sx={{ position: "absolute", right: 16, top: 16 }}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers>
{ogDialog.source === "upload" ? (
<ImageUploaderCloudinary
onUploadSuccess={(url) => {
setSeoOgImage(url);
setOgDialog({ open: false, source: "" });
}}
/>
) : (
<CloudinaryGallerySelector
onSelect={(url) => {
setSeoOgImage(url);
setOgDialog({ open: false, source: "" });
}}
/>
)}
</DialogContent>
</Dialog>
</Box>
);
};

View File

@ -121,7 +121,7 @@ const Home = () => {
constructSector={acf?.gdc_construct?.construct_sector || "Secteurs"}
constructNoteText={acf?.gdc_construct?.construct_note || "Note société"}
missionTitle={acf?.mission_title || "Nos missions principales"}
missionItems={acf?.mission_items || []}
missionItems={acf?.mission || []}
/>
{/* Section Expertises */}