Files
Octopus-React-Wp/frontend/src/components/EditPost.jsx
T

557 lines
18 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 larticle
</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 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)",
},
}}
/>
<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 />
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,
}}
>
<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>
);
}
export default EditPost;