MAJ gestion des pages services
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user