mise a jour structure visuel BO

This commit is contained in:
sebvtl728
2025-11-06 11:28:07 +01:00
parent 1890d830e1
commit a264f9b430
19 changed files with 4776 additions and 1031 deletions
+466 -88
View File
@@ -1,20 +1,110 @@
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { getPostById, updatePost, uploadImageFromUrl } from "../wordpress"; // utilise uploadImageFromUrl
import { getPostById, updatePost, uploadImageFromUrl } from "../wordpress";
import { getToken } from "../auth";
import {
Box,
Container,
Typography,
TextField,
Button,
Paper,
Grid,
Card,
CardContent,
Stack,
Chip,
Divider,
} from "@mui/material";
import { ArrowBack, Height, Publish } from "@mui/icons-material";
import { ArrowBack, Publish, Image as ImageIcon, History } from "@mui/icons-material";
import ImageUploaderCloudinary from "./ImageUploaderCloudinary";
import CloudinaryGallerySelector from "./CloudinaryGallerySelector";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import "../assets/styleCours.css";
const BUTTON_BASE_SX = Object.freeze({
borderRadius: 999,
textTransform: "none",
fontWeight: 600,
letterSpacing: 0.3,
display: "inline-flex",
alignItems: "center",
gap: 0.75,
transition:
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
"&:hover": {
transform: "translateY(-1px)",
},
"&:active": {
transform: "translateY(0)",
},
});
const BUTTON_SIZE_SX = Object.freeze({
small: {
px: 2.1,
py: 0.55,
fontSize: "0.78rem",
minHeight: 30,
},
medium: {
px: 2.8,
py: 0.8,
fontSize: "0.86rem",
minHeight: 36,
},
large: {
px: 3.4,
py: 1,
fontSize: "0.94rem",
minHeight: 42,
},
});
const BUTTON_VARIANT_SX = Object.freeze({
primary: {
background: "linear-gradient(135deg, #315397, #7bc0ff)",
color: "#ffffff",
border: "1px solid rgba(255,255,255,0.35)",
boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.55)",
"&:hover": {
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
boxShadow: "0 22px 32px -18px rgba(11, 26, 61, 0.6)",
},
"&.Mui-disabled": {
color: "rgba(255,255,255,0.72)",
background:
"linear-gradient(135deg, rgba(49,83,151,0.35), rgba(123,192,255,0.35))",
boxShadow: "none",
border: "1px solid rgba(49,83,151,0.18)",
},
},
ghost: {
background: "rgba(255,255,255,0.16)",
color: "rgba(255,255,255,0.92)",
border: "1px solid rgba(255,255,255,0.28)",
backdropFilter: "blur(8px)",
"&:hover": {
background: "rgba(255,255,255,0.24)",
boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
},
},
outline: {
background: "rgba(255,255,255,0.84)",
color: "#315397",
border: "1px solid rgba(49,83,151,0.26)",
"&:hover": {
background: "rgba(255,255,255,0.96)",
borderColor: "rgba(49,83,151,0.46)",
boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
},
},
});
const composeButtonSx = (variant, size = "medium") => ({
...BUTTON_BASE_SX,
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
});
function EditPost() {
const { id } = useParams();
@@ -23,6 +113,13 @@ function EditPost() {
const [content, setContent] = useState("");
const [imageUrl, setImageUrl] = useState(null); // preview
const [imageFile, setImageFile] = useState(null); // pour upload
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [postMeta, setPostMeta] = useState({
date: null,
modified: null,
status: "",
});
// Auth
useEffect(() => {
@@ -33,13 +130,21 @@ function EditPost() {
useEffect(() => {
const fetchPost = async () => {
try {
setLoading(true);
const post = await getPostById(id);
setTitle(post.title.rendered);
setContent(post.content.rendered);
setImageUrl(post?.jetpack_featured_media_url || null); // preview actuelle
} catch (err) {
console.error("Erreur chargement article :", err);
setImageUrl(post?.jetpack_featured_media_url || null);
setPostMeta({
date: post.date || null,
modified: post.modified || null,
status: post.status || "",
});
} catch (error) {
console.error("Erreur chargement article :", error);
navigate("/admin/gestion-articles");
} finally {
setLoading(false);
}
};
fetchPost();
@@ -62,119 +167,392 @@ function EditPost() {
],
};
const handleUpdate = async (e) => {
e.preventDefault();
try {
let imageId = null;
const canSubmit =
title.trim().length > 0 && content.trim().length > 0 && Boolean(imageUrl);
if (imageFile) {
// upload image vers WordPress
const handleUpdate = async (event) => {
event.preventDefault();
if (!canSubmit || submitting) {
return;
}
try {
setSubmitting(true);
let imageId = undefined;
if (imageFile || imageUrl) {
imageId = await uploadImageFromUrl(imageUrl);
}
await updatePost(id, title, content, imageId);
alert("✅ Article mis à jour !");
navigate("/admin/gestion-articles");
} catch (error) {
console.error("❌ Erreur update :", error);
alert("❌ Erreur mise à jour.");
} finally {
setSubmitting(false);
}
};
return (
<Box
className="glass-dashboard"
sx={{
minHeight: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "relative",
px: { xs: 2, md: 6 },
pt: { xs: 6, md: 8 },
pb: { xs: 8, md: 10 },
}}
>
<Container maxWidth="lg">
<Paper elevation={6} sx={{ p: 4, borderRadius: 3 }}>
<Box className="glass-orb orb-1" />
<Box className="glass-orb orb-2" />
<Box className="glass-orb orb-3" />
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}>
<Stack
direction={{ xs: "column", md: "row" }}
justifyContent="space-between"
alignItems={{ xs: "flex-start", md: "center" }}
spacing={2}
sx={{ mb: 3 }}
>
<Button
onClick={() => navigate("/admin/gestion-articles")}
variant="outlined"
variant="contained"
startIcon={<ArrowBack />}
sx={composeButtonSx("ghost")}
>
Retour
Retour aux articles
</Button>
<Typography variant="h4" align="center" sx={{ mb: 3 }}>
Modifier l'article
</Typography>
<form onSubmit={handleUpdate}>
<TextField
label="Titre"
fullWidth
value={title}
onChange={(e) => setTitle(e.target.value)}
sx={{ mb: 2 }}
/>
<Stack spacing={0.5}>
<Typography
variant="overline"
sx={{
letterSpacing: 2,
fontWeight: 700,
color: "rgba(255,255,255,0.8)",
}}
>
Publication Octopus
</Typography>
<Typography
variant="h3"
sx={{
color: "#ffffff",
fontWeight: 800,
letterSpacing: "-0.5px",
}}
>
Modifier larticle
</Typography>
<Typography
variant="body1"
sx={{ mt: 2, mb: 2, fontWeight: "bold" }}
sx={{
color: "rgba(255,255,255,0.85)",
maxWidth: 520,
}}
>
Contenu de l'article :
Ajustez votre contenu, remplacez le visuel mis en avant et publiez
les mises à jour directement sur le site Octopus.
</Typography>
</Stack>
</Stack>
<Box sx={{ height: 250, mb: 7 }}>
<ReactQuill
theme="snow"
value={content}
onChange={setContent}
modules={modules}
style={{ height: "100%" }}
/>
</Box>
<Typography fontWeight="bold">Image depuis ton ordi :</Typography>
<ImageUploaderCloudinary
onUploadSuccess={(url) => {
setImageUrl(url);
convertUrlToFile(url).then(setImageFile);
<Grid container spacing={3.5}>
<Grid item xs={12} lg={7}>
<Card
className="glass-subcard"
sx={{
borderRadius: 4,
p: { xs: 2.5, md: 3 },
boxShadow: "0 28px 36px -28px rgba(12, 29, 74, 0.5)",
}}
/>
>
<CardContent sx={{ p: 0 }}>
<Stack spacing={2}>
<Stack
direction="row"
spacing={1}
alignItems="center"
sx={{ flexWrap: "wrap" }}
>
<Chip
icon={<History fontSize="small" />}
label={
postMeta.modified
? `Modifié le ${new Intl.DateTimeFormat("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(postMeta.modified))}`
: "Modification en cours"
}
sx={{
bgcolor: "rgba(255,255,255,0.78)",
color: "#0b1a3d",
fontWeight: 600,
}}
/>
<Chip
label={
postMeta.status === "publish"
? "Publiée"
: postMeta.status === "draft"
? "Brouillon"
: postMeta.status || "Statut inconnu"
}
color={
postMeta.status === "publish"
? "success"
: postMeta.status === "draft"
? "warning"
: "default"
}
sx={{ fontWeight: 600 }}
/>
</Stack>
<Typography fontWeight="bold" sx={{ mt: 3 }}>
Ou choisir une image déjà envoyée :
</Typography>
<CloudinaryGallerySelector
onSelect={async (url) => {
setImageUrl(url);
const file = await convertUrlToFile(url);
setImageFile(file);
}}
/>
{loading ? (
<Typography variant="body2" sx={{ color: "rgba(11, 26, 61, 0.65)" }}>
Chargement de larticle
</Typography>
) : (
<Box component="form" onSubmit={handleUpdate} noValidate>
<TextField
label="Titre de l'article"
fullWidth
variant="outlined"
value={title}
onChange={(event) => setTitle(event.target.value)}
required
sx={{
mb: 3,
"& .MuiOutlinedInput-root": {
borderRadius: 2,
backgroundColor: "rgba(255,255,255,0.94)",
},
}}
/>
{imageUrl && (
<Box sx={{ mt: 2 }}>
<Typography variant="body2">Aperçu :</Typography>
<img
src={imageUrl}
style={{
width: "100%",
borderRadius: 6,
maxHeight: 200,
objectFit: "cover",
<Box
sx={{
mb: 3,
borderRadius: 2,
overflow: "hidden",
border: "1px solid rgba(49,83,151,0.15)",
backgroundColor: "rgba(255,255,255,0.92)",
}}
>
<ReactQuill
theme="snow"
value={content}
onChange={setContent}
modules={modules}
style={{ height: 280 }}
/>
</Box>
<Stack spacing={2.5}>
<Box>
<Typography
variant="subtitle1"
sx={{ fontWeight: 700, color: "#0b1a3d" }}
>
Remplacer le visuel
</Typography>
<Typography
variant="body2"
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
>
Importez une nouvelle image optimisée pour vos
contenus.
</Typography>
<Box sx={{ mt: 2 }}>
<ImageUploaderCloudinary
onUploadSuccess={(url) => {
setImageUrl(url);
convertUrlToFile(url).then(setImageFile);
}}
/>
</Box>
</Box>
<Divider
sx={{ borderColor: "rgba(11, 26, 61, 0.08)" }}
/>
<Box>
<Typography
variant="subtitle1"
sx={{ fontWeight: 700, color: "#0b1a3d" }}
>
Sélectionner depuis la médiathèque
</Typography>
<Typography
variant="body2"
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 2 }}
>
Choisissez un visuel existant dans Cloudinary.
</Typography>
<CloudinaryGallerySelector
onSelect={async (url) => {
setImageUrl(url);
const file = await convertUrlToFile(url);
setImageFile(file);
}}
/>
</Box>
<Stack
direction={{ xs: "column", sm: "row" }}
spacing={1.5}
sx={{ pt: 1 }}
>
<Button
type="submit"
variant="contained"
startIcon={<Publish />}
disabled={!canSubmit || submitting}
sx={{
...composeButtonSx("primary"),
minWidth: 220,
}}
>
{submitting ? "Enregistrement…" : "Mettre à jour"}
</Button>
<Button
variant="contained"
sx={composeButtonSx("outline")}
onClick={() => {
setTitle("");
setContent("");
setImageUrl(null);
setImageFile(null);
}}
>
Réinitialiser
</Button>
</Stack>
</Stack>
</Box>
)}
</Stack>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} lg={5}>
<Stack spacing={3}>
<Card
className="glass-subcard"
sx={{
borderRadius: 4,
p: { xs: 2.5, md: 3 },
boxShadow: "0 28px 36px -28px rgba(12, 29, 74, 0.45)",
}}
>
<Stack direction="row" spacing={2} alignItems="flex-start">
<Box
sx={{
width: 48,
height: 48,
borderRadius: 2,
bgcolor: "rgba(123,192,255,0.22)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#315397",
}}
>
<ImageIcon />
</Box>
<Box>
<Typography
variant="subtitle1"
sx={{ fontWeight: 700, color: "#0b1a3d", mb: 1 }}
>
Rappels éditoriaux
</Typography>
<Typography
variant="body2"
sx={{ color: "rgba(11, 26, 61, 0.68)" }}
>
Vérifiez la cohérence des titres et sous-titres.<br />
Ajoutez des liens internes vers vos formations Octopus.<br />
Noubliez pas de relire la mise en forme dans laperçu
WordPress.
</Typography>
</Box>
</Stack>
</Card>
<Card
className="glass-article-card"
sx={{
borderRadius: 4,
overflow: "hidden",
minHeight: 220,
}}
>
<Box
sx={{
backgroundColor: "rgba(255,255,255,0.9)",
p: { xs: 2.5, md: 3 },
display: "flex",
flexDirection: "column",
height: "100%",
gap: 1.5,
}}
alt="preview"
/>
</Box>
)}
<Button
type="submit"
variant="contained"
fullWidth
sx={{ mt: 3 }}
startIcon={<Publish />}
>
Mettre à jour
</Button>
</form>
</Paper>
</Container>
>
<Typography
variant="subtitle1"
sx={{ fontWeight: 700, color: "#0b1a3d" }}
>
Aperçu visuel
</Typography>
{imageUrl ? (
<Box
component="img"
src={imageUrl}
alt="Aperçu de limage sélectionnée"
sx={{
width: "100%",
borderRadius: 3,
height: 200,
objectFit: "cover",
boxShadow: "0 20px 28px -24px rgba(12, 29, 74, 0.48)",
}}
/>
) : (
<Box
sx={{
flexGrow: 1,
borderRadius: 3,
border: "1px dashed rgba(49,83,151,0.35)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "rgba(49,83,151,0.6)",
fontStyle: "italic",
}}
>
Aucun visuel sélectionné pour le moment
</Box>
)}
<Typography
variant="body2"
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
>
Ce visuel illustrera larticle dans la liste des publications
et sur les réseaux sociaux.
</Typography>
</Box>
</Card>
</Stack>
</Grid>
</Grid>
</Box>
</Box>
);
}