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
+159 -39
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
};