mise a jour structure visuel BO
This commit is contained in:
@@ -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 l’article
|
||||
</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 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)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{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 />
|
||||
• 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,
|
||||
}}
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user