maj majeur opengraph et page accueil
This commit is contained in:
parent
e7e5c5e3fd
commit
f4cd293805
@ -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" />
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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, """) || ""}" 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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user