MAJ gestion des pages services

This commit is contained in:
sebvtl728 2025-10-26 15:38:17 +01:00
parent 82406fb046
commit f242bcc5d6
11 changed files with 1866 additions and 195 deletions

View File

@ -172,11 +172,11 @@ const Header = () => {
<MenuItem
component={Link}
to="/services/structure-beton-charpente-metallique-bois"
to="/services/formations-web"
onClick={handleMenuClose}
>
<WorkIcon sx={{ marginRight: 1 }} />
Vidéos entreprise
Formations web
</MenuItem>
<MenuItem
@ -375,11 +375,11 @@ const Header = () => {
<ListItem
button
component={Link}
to="/services/structure-beton-charpente-metallique-bois"
selected={location.pathname.includes("/services/structure-beton-charpente-metallique-bois")}
to="/services/formations-web"
selected={location.pathname.includes("/services/formations-web")}
>
<WorkIcon sx={{ marginRight: 0.5 }} />
<ListItemText primary="Vidéos entreprise..." />
<ListItemText primary="Formations web..." />
</ListItem>
<ListItem

File diff suppressed because it is too large Load Diff

View File

@ -27,18 +27,46 @@ const ServiceCinq = () => {
return <p>Chargement...</p>;
}
const getStoredValue = (key) =>
typeof window !== "undefined" ? window.localStorage.getItem(key) : null;
const storedHeroBackground = getStoredValue(`hero_bg_${pageId}`);
const storedHeroImage = getStoredValue(`hero_img_${pageId}`);
const heroBackground =
acfData?.hero_background ||
acfData?.background_hero ||
acfData?.hero_bg ||
storedHeroBackground ||
"linear-gradient(135deg, #050d1f 0%, #02050d 85%)";
const heroImage =
acfData?.hero_background_image ||
acfData?.hero_background_image_url ||
acfData?.hero_image_url ||
acfData?.hero_image ||
acfData?.image_hero ||
acfData?.img_hero_url ||
acfData?.img_hero ||
storedHeroImage ||
"https://picsum.photos/id/1026/1920/1080.webp";
const serviceDetails = {
// Hero
title: acfData?.titre_principal_tce || "Titre héros !", // Modifiable individuellement
subtitle: <span dangerouslySetInnerHTML={{ __html: acfData?.description_principal_tce || "Description, héros" }} />,
subtitle:
acfData?.description_principal_tce ||
"<p>Description, héros</p>",
image: "https://picsum.photos/id/1026/1920/1080.webp",
image: heroImage,
heroBackground,
ctaText: "En savoir plus",
ctaLink: "/contact",
// section 1
interestTitle: acfData?.second_titre_tce || "Titre interet", // Modifiable individuellement
description: <span dangerouslySetInnerHTML={{ __html: acfData?.second_description_tce || "Description interet" }} />,
description:
acfData?.second_description_tce || "<p>Description interet</p>",
// section 2
desirTitle: "Titre", // Modifiable individuellement

View File

@ -25,18 +25,48 @@ const ServiceDeux = () => {
if (loading) {
return <p>Chargement...</p>;
}
const serviceDetails = {
// Hero
title: acfData?.titre_principal_beton || "Structure béton charpente métallique & bois",
subtitle: <span dangerouslySetInnerHTML={{ __html: acfData?.description_principal_beton || "Solidité, Résistance, Durabilité"}} />,
const getStoredValue = (key) =>
typeof window !== "undefined" ? window.localStorage.getItem(key) : null;
image: "https://picsum.photos/id/240/1920/1080.webp",
const storedHeroBackground = getStoredValue(`hero_bg_${pageId}`);
const storedHeroImage = getStoredValue(`hero_img_${pageId}`);
const heroBackground =
acfData?.hero_background ||
acfData?.background_hero ||
acfData?.hero_bg ||
storedHeroBackground ||
"linear-gradient(135deg, #041226 0%, #01040a 90%)";
const heroImage =
acfData?.hero_background_image ||
acfData?.hero_background_image_url ||
acfData?.hero_image_url ||
acfData?.hero_image ||
acfData?.image_hero ||
acfData?.img_hero_url ||
acfData?.img_hero ||
storedHeroImage ||
"https://picsum.photos/id/240/1920/1080.webp";
const serviceDetails = {
// Hero
title: acfData?.titre_principal_beton || "formations-web",
subtitle:
acfData?.description_principal_beton ||
"<p>Solidité, Résistance, Durabilité</p>",
image: heroImage,
heroBackground,
ctaText: "En savoir plus !",
ctaLink: "/contact",
// section 1
interestTitle: acfData?.second_titre_beton ||"Béton ou Charpente :\n Quel Matériau Choisir pour une Construction Durable ?", // Modifiable individuellement
description: <span dangerouslySetInnerHTML={{ __html: acfData?.second_description_beton || "Le béton est le matériau de construction le plus répandu dans le monde. \nIl est présent dans tous les secteurs de la construction.\n\n Longtemps associé à limage négative des grands ensembles, gris et vieillissants, le béton a réalisé des progrès spectaculaires ces dernières années, tant au niveau de ses performances techniques que des aspects esthétiques. \n\nIl commence même à entrer dans les foyers comme produit de décoration !\nUne charpente est un assemblage de pièces de bois et/ou de métal, servant à soutenir ou couvrir des constructions et faisant partie de la toiture."}} />,
description:
acfData?.second_description_beton ||
"<p>Le béton est le matériau de construction le plus répandu dans le monde. Il est présent dans tous les secteurs de la construction.</p><p>Longtemps associé à limage négative des grands ensembles, gris et vieillissants, le béton a réalisé des progrès spectaculaires ces dernières années, tant au niveau de ses performances techniques que des aspects esthétiques.</p><p>Il commence même à entrer dans les foyers comme produit de décoration ! Une charpente est un assemblage de pièces de bois et/ou de métal, servant à soutenir ou couvrir des constructions et faisant partie de la toiture.</p>",
// section 2
desirTitle: acfData?.troisieme_titre_beton || "Titre",// Modifiable individuellement
@ -80,4 +110,4 @@ const ServiceDeux = () => {
return <ServicePageTemplate {...serviceDetails} />;
};
export default ServiceDeux;
export default ServiceDeux;

View File

@ -3,15 +3,6 @@ import { Helmet } from "react-helmet-async"; // ✅ SEO
import ServicePageTemplate from "../ServicePageTemplate";
import { getPageById } from "../../wordpress";
/**
* Fonction pour supprimer les balises HTML des champs WYSIWYG
*/
const stripHtml = (html) => {
const doc = new DOMParser().parseFromString(html || "", "text/html");
return doc.body.textContent || "";
};
const ServiceQuatre = () => {
const [acfData, setAcfData] = useState(null);
const [seoData, setSeoData] = useState(null); // Stocke les données SEO de RankMath
@ -58,36 +49,61 @@ const ServiceQuatre = () => {
return <p>Chargement...</p>;
}
const getStoredValue = (key) =>
typeof window !== "undefined" ? window.localStorage.getItem(key) : null;
const storedHeroBackground = getStoredValue(`hero_bg_${pageId}`);
const storedHeroImage = getStoredValue(`hero_img_${pageId}`);
const heroBackground =
acfData?.hero_background ||
acfData?.background_hero ||
acfData?.hero_bg ||
storedHeroBackground ||
"linear-gradient(135deg, #07132a 0%, #01040c 90%)";
const heroImage =
acfData?.hero_background_image ||
acfData?.hero_background_image_url ||
acfData?.hero_image_url ||
acfData?.hero_image ||
acfData?.image_hero ||
acfData?.img_hero_url ||
acfData?.img_hero ||
storedHeroImage ||
"https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/02/service-securite-incendie.webp";
const serviceDetails = {
title: acfData?.titre_principal || "Titre héros !",
subtitle: stripHtml(acfData?.description_principal) || "Description, héros",
subtitle: acfData?.description_principal || "<p>Description, héros</p>",
image: acfData?.hero_image || "https://preprod.octopusdesign.fr/api-octopus/server/wp-content/uploads/2025/02/service-securite-incendie.webp",
image: heroImage,
heroBackground,
ctaText: "En savoir plus",
ctaLink: "/contact",
interestTitle: acfData?.second_titre || "Titre interet",
description: stripHtml(acfData?.second_description) || "Description interet",
description: acfData?.second_description || "<p>Description interet</p>",
desirTitle: "Titre",
features: [
{
title: acfData?.titre_carte_one || "Titre !",
description: stripHtml(acfData?.description_carte_one).substring(0, 100) + "...",
modalText: stripHtml(acfData?.description_carte_one) || "Description Modal.",
description: acfData?.description_carte_one || "",
modalText: acfData?.description_carte_one || "Description Modal.",
modalImage: "https://picsum.photos/id/300/800/400.webp",
},
{
title: acfData?.titre_carte_two || "Titre !",
description: stripHtml(acfData?.description_carte_two).substring(0, 100) + "...",
modalText: stripHtml(acfData?.description_carte_two) || "Description Modal.",
description: acfData?.description_carte_two || "",
modalText: acfData?.description_carte_two || "Description Modal.",
modalImage: "https://picsum.photos/id/301/800/400.webp",
},
{
title: acfData?.titre_carte_tree || "Titre !",
description: stripHtml(acfData?.description_carte_tree).substring(0, 100) + "...",
modalText: stripHtml(acfData?.description_carte_tree) || "Description Modal.",
description: acfData?.description_carte_tree || "",
modalText: acfData?.description_carte_tree || "Description Modal.",
modalImage: "https://picsum.photos/id/302/800/400.webp",
},
],
@ -122,4 +138,4 @@ const ServiceQuatre = () => {
};
export default ServiceQuatre;
export default ServiceQuatre;

View File

@ -8,6 +8,7 @@ const ServiceTrois = () => {
subtitle: "IN3 est un bureau détudes technique spécialisé dans lingénierie électrique du bâtiment.",
image: "https://picsum.photos/id/1019/1920/1080.webp",
heroBackground: "linear-gradient(135deg,#0c101f 0%,#03050b 85%)",
ctaText: "En savoir plus",
ctaLink: "/contact",
@ -69,4 +70,4 @@ const ServiceTrois = () => {
return <ServicePageTemplate {...serviceDetails} />;
};
export default ServiceTrois;
export default ServiceTrois;

View File

@ -10,6 +10,7 @@ const ServiceUn = () => {
"Notre bureau détudes in3 au Mans conçoit des bâtiments à construire ou à rénover selon le programme fourni par le maître de louvrage, de diriger lexécution des marchés de travaux, de proposer le règlement des travaux et leur réception. Nos missions sont les suivantes :", // Modifiable individuellement
image: "https://picsum.photos/id/1026/1920/1080.webp",
heroBackground: "linear-gradient(135deg,#0b1428 0%,#16223f 40%,#04070f 100%)",
ctaText: "En savoir plus",
ctaLink: "/contact",

View File

@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo, isValidElement, useEffect } from "react";
import {
Box,
Typography,
@ -18,6 +18,13 @@ import "swiper/css/navigation";
import "swiper/css/pagination";
import PropTypes from "prop-types";
import { Helmet } from "react-helmet-async";
import {
extractFirstImageSrc,
removeImageTags,
stripHtml,
} from "../utils/htmlHelpers";
const DEFAULT_CARD_IMAGE = "https://picsum.photos/id/43/800/400.webp";
const ServicePageTemplate = ({
title,
@ -27,6 +34,7 @@ const ServicePageTemplate = ({
desirTitle,
features,
image,
heroBackground,
ctaText,
ctaLink,
carouselItems,
@ -35,13 +43,88 @@ const ServicePageTemplate = ({
const [openModal, setOpenModal] = useState(false);
const [modalContent, setModalContent] = useState({});
const [loadingImage, setLoadingImage] = useState(true);
const [heroLoaded, setHeroLoaded] = useState(false);
const renderRichText = (value) => {
if (value === null || value === undefined) return null;
if (isValidElement(value)) return value;
if (typeof value === "string") {
return <span dangerouslySetInnerHTML={{ __html: value }} />;
}
return null;
};
const descriptionIsElement = isValidElement(description);
const rawDescriptionHtml =
!descriptionIsElement && typeof description === "string" ? description : "";
const descriptionMedia = useMemo(() => {
if (!rawDescriptionHtml) {
return { html: rawDescriptionHtml, media: null };
}
if (typeof window === "undefined" || !window.DOMParser) {
return { html: rawDescriptionHtml, media: null };
}
const parser = new window.DOMParser();
const doc = parser.parseFromString(rawDescriptionHtml, "text/html");
const firstImage = doc.body.querySelector("img");
if (firstImage) {
const media = {
src: firstImage.getAttribute("src"),
alt: firstImage.getAttribute("alt") || "",
};
firstImage.remove();
return { html: doc.body.innerHTML, media };
}
return { html: rawDescriptionHtml, media: null };
}, [rawDescriptionHtml]);
useEffect(() => {
if (!image) {
setHeroLoaded(true);
return;
}
setHeroLoaded(false);
const img = new Image();
img.src = image;
img.onload = () => setHeroLoaded(true);
img.onerror = () => setHeroLoaded(true);
return () => {
img.onload = null;
img.onerror = null;
};
}, [image]);
const defaultGradient = "linear-gradient(135deg, #0a1930 0%, #020710 85%)";
const activeGradient = heroBackground || defaultGradient;
const heroBackgroundStyle =
heroLoaded && image
? `url(${image}), ${activeGradient}`
: activeGradient;
const buildModalPayload = (feature) => {
const rawHtml = feature.modalText || feature.description || "";
const sanitizedHtml = removeImageTags(rawHtml);
const extractedImage = extractFirstImageSrc(rawHtml);
const dynamicImage =
extractedImage || feature.modalImage || DEFAULT_CARD_IMAGE;
return {
title: feature.title,
text: sanitizedHtml || "Texte non disponible.",
image: dynamicImage,
};
};
const getPreviewText = (feature) => {
const rawPreview = feature.description || feature.modalText || "";
const textOnly = stripHtml(removeImageTags(rawPreview));
if (!textOnly) return "Contenu indisponible.";
return textOnly.length > 140 ? `${textOnly.substring(0, 140)}` : textOnly;
};
const handleCardClick = (feature) => {
setModalContent({
title: feature.title,
text: feature.modalText || "Texte non disponible.",
image: feature.modalImage || "https://picsum.photos/id/43/800/400.webp",
});
setModalContent(buildModalPayload(feature));
setLoadingImage(true);
setOpenModal(true);
};
@ -77,9 +160,9 @@ const ServicePageTemplate = ({
<Box
sx={{
mt: 5,
backgroundImage: `url(${image})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundImage: heroBackgroundStyle,
backgroundSize: heroLoaded && image ? "cover, cover" : "cover",
backgroundPosition: heroLoaded && image ? "center, center" : "center",
minHeight: "70vh",
display: "flex",
alignItems: "center",
@ -87,6 +170,8 @@ const ServicePageTemplate = ({
textAlign: "center",
color: "black",
padding: "20px",
backgroundColor: heroLoaded ? undefined : "#020712",
transition: "background-image 0.8s ease, opacity 0.4s ease",
}}
>
<Box
@ -112,8 +197,8 @@ const ServicePageTemplate = ({
>
{title}
</Typography>
<Typography variant="body1" sx={{ mb: 4 }}>
{subtitle}
<Typography variant="body1" component="div" sx={{ mb: 4 }}>
{renderRichText(subtitle) ?? subtitle ?? ""}
</Typography>
<Button
href={ctaLink}
@ -133,29 +218,69 @@ const ServicePageTemplate = ({
</Box>
{/* Section numero 1 */}
<Box sx={{ padding: "40px 20px", textAlign: "center" }}>
<Box sx={{ fontWeight: "5", textAlign: "center" }}>
<Typography
variant="h2"
sx={{
fontWeight: "bold",
mb: 2,
mb: 7,
fontSize: { xs: "2rem", md: "2rem", lg: "2rem" },
whiteSpace: "pre-line",
}}
>
{interestTitle}
</Typography>
<Typography
variant="body1"
sx={{
maxWidth: "1000px",
margin: "0 auto",
mb: 4,
whiteSpace: "pre-line",
}}
>
{description}
</Typography>
{descriptionIsElement ? (
<Typography variant="body1" component="div">
{renderRichText(description)}
</Typography>
) : (
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", md: "row" },
gap: 4,
alignItems: "center",
justifyContent: "center",
textAlign: { xs: "center", md: "left" },
}}
>
{descriptionMedia.media?.src && (
<Box
component="img"
src={descriptionMedia.media.src}
alt={descriptionMedia.media.alt || "Illustration"}
sx={{
width: { xs: "100%", md: "42%" },
borderRadius: 4,
objectFit: "cover",
boxShadow: "0 25px 45px rgba(0,0,0,0.25)",
mb: { xs: 2, md: 5 },
mx: { xs: "auto", md: 0 },
marginLeft: { xs: 2, md: 6 } ,
}}
/>
)}
<Typography
variant="body1"
component="div"
sx={{
flex: 1,
textAlign: { xs: "center", md: "left" },
"& p": {
margin: "0 auto",
textAlign: "left",
lineHeight: 1.65,
maxWidth: 760,
},
"& p + p": {
marginTop: 1.2,
},
}}
dangerouslySetInnerHTML={{ __html: descriptionMedia.html || "" }}
/>
</Box>
)}
</Box>
{/* Section numero 2 */}
@ -198,20 +323,13 @@ const ServicePageTemplate = ({
>
{feature.title}
</Typography>
<Typography
variant="body2"
sx={{ color: "#555" }}
dangerouslySetInnerHTML={{
__html:
feature.description.length > 100
? `${feature.description.substring(0, 100)}...`
: feature.description,
}}
/>
</CardContent>
</Card>
</Grid>
))}
<Typography variant="body2" sx={{ color: "#555" }}>
{getPreviewText(feature)}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
@ -352,7 +470,7 @@ ServicePageTemplate.propTypes = {
modalImage: PropTypes.string,
})
).isRequired,
image: PropTypes.string.isRequired,
image: PropTypes.string,
ctaText: PropTypes.string.isRequired,
ctaLink: PropTypes.string.isRequired,
carouselItems: PropTypes.arrayOf(
@ -362,12 +480,14 @@ ServicePageTemplate.propTypes = {
image: PropTypes.string.isRequired,
})
),
heroBackground: PropTypes.string,
seo: PropTypes.object, // Ajout du SEO en option
};
// Valeurs par défaut
ServicePageTemplate.defaultProps = {
carouselItems: [],
heroBackground: "linear-gradient(135deg, #0a1930 0%, #020710 85%)",
seo: {}, // SEO par défaut vide
};

View File

@ -63,7 +63,7 @@ function YourApp() {
element={<ServiceUn />}
/>
<Route
path="/services/structure-beton-charpente-metallique-bois"
path="/services/formations-web"
element={<ServiceDeux />}
/>
<Route path="/services/electricite" element={<ServiceTrois />} />

View File

@ -0,0 +1,48 @@
const hasDomParser =
(typeof window !== "undefined" && typeof window.DOMParser !== "undefined") ||
typeof DOMParser !== "undefined";
const createDocument = (html) => {
if (!hasDomParser || !html) return null;
const ParserConstructor =
typeof DOMParser !== "undefined"
? DOMParser
: typeof window !== "undefined" && window.DOMParser
? window.DOMParser
: null;
if (!ParserConstructor) {
return null;
}
const parser = new ParserConstructor();
return parser.parseFromString(html, "text/html");
};
export const extractFirstImageSrc = (html) => {
if (!html) return "";
const doc = createDocument(html);
if (doc) {
const img = doc.querySelector("img");
if (img) {
return img.getAttribute("src") || "";
}
}
const match = html.match(/<img[^>]+src=["']([^"']+)["']/i);
return match?.[1] || "";
};
export const removeImageTags = (html) => {
if (!html) return "";
return html.replace(/<img\b[^>]*>/gi, "");
};
export const stripHtml = (html) => {
if (!html) return "";
const doc = createDocument(html);
if (doc) {
return doc.body.textContent || "";
}
return html.replace(/<[^>]*>/g, "");
};

View File

@ -267,7 +267,7 @@ export async function getPageById(pageId) {
try {
const response = await axios.get(
`https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/${pageId}?_fields=id,title,acf,_rank_math_seo_meta`
`https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/${pageId}?_fields=id,title,link,acf,_rank_math_seo_meta`
);
return response.data;
} catch (error) {