557 lines
18 KiB
React
557 lines
18 KiB
React
import { useState, useEffect } from "react";
|
||
import { useParams, useNavigate } from "react-router-dom";
|
||
import { getPostById, updatePost, uploadImageFromUrl } from "../wordpress";
|
||
import { getToken } from "../auth";
|
||
import {
|
||
Box,
|
||
Typography,
|
||
TextField,
|
||
Button,
|
||
Grid,
|
||
Card,
|
||
CardContent,
|
||
Stack,
|
||
Chip,
|
||
Divider,
|
||
} from "@mui/material";
|
||
import { ArrowBack, Publish, Image as ImageIcon, History } from "@mui/icons-material";
|
||
import ImageUploaderCloudinary from "./ImageUploaderCloudinary";
|
||
import CloudinaryGallerySelector from "./CloudinaryGallerySelector";
|
||
import RichTextEditor from "./common/RichTextEditor";
|
||
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();
|
||
const navigate = useNavigate();
|
||
const [title, setTitle] = useState("");
|
||
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(() => {
|
||
if (!getToken()) navigate("/admin/login");
|
||
}, [navigate]);
|
||
|
||
// Article
|
||
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);
|
||
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();
|
||
}, [id, navigate]);
|
||
|
||
// 🧠 convertit URL en File
|
||
const convertUrlToFile = async (url) => {
|
||
const res = await fetch(url);
|
||
const blob = await res.blob();
|
||
return new File([blob], "image-cloudinary.jpg", { type: blob.type });
|
||
};
|
||
|
||
const toolbarOptions = [
|
||
[{ header: [1, 2, 3, false] }],
|
||
["bold", "italic", "underline", "strike"],
|
||
[{ list: "ordered" }, { list: "bullet" }],
|
||
["link", "image"],
|
||
["clean"],
|
||
];
|
||
|
||
const canSubmit =
|
||
title.trim().length > 0 && content.trim().length > 0 && Boolean(imageUrl);
|
||
|
||
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",
|
||
position: "relative",
|
||
px: { xs: 2, md: 6 },
|
||
pt: { xs: 6, md: 8 },
|
||
pb: { xs: 8, md: 10 },
|
||
}}
|
||
>
|
||
<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="contained"
|
||
startIcon={<ArrowBack />}
|
||
sx={composeButtonSx("ghost")}
|
||
>
|
||
Retour aux articles
|
||
</Button>
|
||
<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 l’article
|
||
</Typography>
|
||
<Typography
|
||
variant="body1"
|
||
sx={{
|
||
color: "rgba(255,255,255,0.85)",
|
||
maxWidth: 520,
|
||
}}
|
||
>
|
||
Ajustez votre contenu, remplacez le visuel mis en avant et publiez
|
||
les mises à jour directement sur le site Octopus.
|
||
</Typography>
|
||
</Stack>
|
||
</Stack>
|
||
|
||
<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>
|
||
|
||
{loading ? (
|
||
<Typography variant="body2" sx={{ color: "rgba(11, 26, 61, 0.65)" }}>
|
||
Chargement de l’article…
|
||
</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)",
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Box
|
||
sx={{
|
||
mb: 3,
|
||
borderRadius: 2,
|
||
overflow: "hidden",
|
||
border: "1px solid rgba(49,83,151,0.15)",
|
||
backgroundColor: "rgba(255,255,255,0.92)",
|
||
}}
|
||
>
|
||
<RichTextEditor
|
||
value={content}
|
||
onChange={setContent}
|
||
minHeight={280}
|
||
toolbarOptions={toolbarOptions}
|
||
/>
|
||
</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 />
|
||
• N’oubliez pas de relire la mise en forme dans l’aperç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,
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="subtitle1"
|
||
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||
>
|
||
Aperçu visuel
|
||
</Typography>
|
||
{imageUrl ? (
|
||
<Box
|
||
component="img"
|
||
src={imageUrl}
|
||
alt="Aperçu de l’image 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 l’article dans la liste des publications
|
||
et sur les réseaux sociaux.
|
||
</Typography>
|
||
</Box>
|
||
</Card>
|
||
</Stack>
|
||
</Grid>
|
||
</Grid>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
export default EditPost;
|