567 lines
17 KiB
React
567 lines
17 KiB
React
// ✅ Importations nécessaires
|
|
import { useState, useEffect, useMemo } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Button,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
CardMedia,
|
|
TextField,
|
|
Stack,
|
|
Chip,
|
|
CircularProgress,
|
|
Tooltip,
|
|
} from "@mui/material";
|
|
import { Add, ArrowBack, Edit, Delete } from "@mui/icons-material";
|
|
import api from "../../api";
|
|
import { getToken } from "../../auth";
|
|
import { deletePost } from "../../wordpress";
|
|
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)",
|
|
},
|
|
},
|
|
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)",
|
|
},
|
|
},
|
|
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)",
|
|
},
|
|
},
|
|
danger: {
|
|
background: "linear-gradient(135deg, #f05b6b, #ff8a80)",
|
|
color: "#3a0a0f",
|
|
border: "1px solid rgba(240,91,107,0.35)",
|
|
boxShadow: "0 18px 26px -18px rgba(105, 21, 33, 0.45)",
|
|
"&:hover": {
|
|
background: "linear-gradient(135deg, #e34659, #ff7575)",
|
|
boxShadow: "0 22px 30px -18px rgba(105, 21, 33, 0.52)",
|
|
},
|
|
},
|
|
});
|
|
|
|
const composeButtonSx = (variant, size = "medium") => ({
|
|
...BUTTON_BASE_SX,
|
|
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
|
|
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
|
|
});
|
|
|
|
const GestionArticles = () => {
|
|
const [posts, setPosts] = useState([]);
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
const navigate = useNavigate();
|
|
|
|
// ✅ Vérifier si l'utilisateur est connecté
|
|
useEffect(() => {
|
|
if (!getToken()) {
|
|
navigate("/admin/login");
|
|
}
|
|
}, [navigate]);
|
|
|
|
// ✅ Récupérer les articles et leurs images associées
|
|
useEffect(() => {
|
|
const fetchPosts = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await api.get(
|
|
"wp/v2/posts?_fields=id,title,excerpt,featured_media,date,status"
|
|
);
|
|
const postsData = response.data;
|
|
|
|
const postsWithImages = await Promise.all(
|
|
postsData.map(async (post) => {
|
|
if (post.featured_media) {
|
|
try {
|
|
const mediaResponse = await api.get(`wp/v2/media/${post.featured_media}`);
|
|
return { ...post, image: mediaResponse.data.source_url, imageId: post.featured_media };
|
|
} catch (error) {
|
|
console.error(`❌ Erreur chargement image article ${post.id}:`, error);
|
|
return { ...post, image: null, imageId: null };
|
|
}
|
|
}
|
|
return { ...post, image: null, imageId: null };
|
|
})
|
|
);
|
|
|
|
setPosts(postsWithImages);
|
|
} catch (error) {
|
|
console.error("❌ Erreur chargement des articles :", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchPosts();
|
|
}, []);
|
|
|
|
// ✅ Supprimer un article et son image associée
|
|
const stripHtml = (value = "") =>
|
|
value.replace(/(<([^>]+)>)/gi, "").replace(/ /gi, " ").trim();
|
|
|
|
const previewExcerpt = (value = "", limit = 160) => {
|
|
const clean = stripHtml(value);
|
|
return clean.length > limit ? `${clean.slice(0, limit)}…` : clean;
|
|
};
|
|
|
|
const formatDate = (value) => {
|
|
if (!value) {
|
|
return "Date inconnue";
|
|
}
|
|
try {
|
|
return new Intl.DateTimeFormat("fr-FR", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
}).format(new Date(value));
|
|
} catch {
|
|
return "Date inconnue";
|
|
}
|
|
};
|
|
|
|
const handleDeletePost = async (postId, imageId) => {
|
|
const confirmed = window.confirm(
|
|
"Supprimer cet article et son visuel associé ?"
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
try {
|
|
await deletePost(postId, imageId);
|
|
setPosts((prev) => prev.filter((post) => post.id !== postId));
|
|
alert("✅ Article supprimé avec succès !");
|
|
} catch (error) {
|
|
console.error("❌ Erreur suppression article :", error);
|
|
alert("⚠ Erreur : impossible de supprimer l'article.");
|
|
}
|
|
};
|
|
|
|
const handleCreate = () => navigate("/admin/create-post");
|
|
const handleBack = () => navigate("/admin/dashboard");
|
|
const handleEdit = (postId) => navigate(`/admin/edit-post/${postId}`);
|
|
|
|
const filteredPosts = useMemo(() => {
|
|
const term = searchTerm.trim().toLowerCase();
|
|
if (!term) {
|
|
return posts;
|
|
}
|
|
return posts.filter((post) => {
|
|
const title = post.title?.rendered?.toLowerCase?.() || "";
|
|
const excerpt = stripHtml(post.excerpt?.rendered || "").toLowerCase();
|
|
return title.includes(term) || excerpt.includes(term);
|
|
});
|
|
}, [posts, searchTerm]);
|
|
|
|
const stats = useMemo(() => {
|
|
const total = posts.length;
|
|
const withImage = posts.reduce(
|
|
(acc, post) => (post.image ? acc + 1 : acc),
|
|
0
|
|
);
|
|
const latest = posts.reduce((current, candidate) => {
|
|
if (!candidate?.date) {
|
|
return current;
|
|
}
|
|
if (!current) {
|
|
return candidate;
|
|
}
|
|
return new Date(candidate.date) > new Date(current.date)
|
|
? candidate
|
|
: current;
|
|
}, null);
|
|
return {
|
|
total,
|
|
withImage,
|
|
withoutImage: total - withImage,
|
|
latestLabel: latest?.title?.rendered || "Aucun article",
|
|
latestDate: latest?.date ? formatDate(latest.date) : "—",
|
|
};
|
|
}, [posts]);
|
|
|
|
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: 1240, mx: "auto" }}>
|
|
<Stack
|
|
direction={{ xs: "column", md: "row" }}
|
|
justifyContent="space-between"
|
|
alignItems={{ xs: "flex-start", md: "center" }}
|
|
spacing={2}
|
|
sx={{ mb: 3 }}
|
|
>
|
|
<Button
|
|
startIcon={<ArrowBack />}
|
|
variant="contained"
|
|
onClick={handleBack}
|
|
sx={{ ...composeButtonSx("ghost") }}
|
|
>
|
|
Retour
|
|
</Button>
|
|
<Stack spacing={0.5}>
|
|
<Typography
|
|
variant="overline"
|
|
sx={{
|
|
letterSpacing: 2,
|
|
fontWeight: 700,
|
|
color: "rgba(255,255,255,0.8)",
|
|
}}
|
|
>
|
|
Contenu éditorial
|
|
</Typography>
|
|
<Typography
|
|
variant="h3"
|
|
sx={{
|
|
color: "#ffffff",
|
|
fontWeight: 800,
|
|
letterSpacing: "-0.5px",
|
|
}}
|
|
>
|
|
Gestion des articles
|
|
</Typography>
|
|
<Typography
|
|
variant="body1"
|
|
sx={{
|
|
color: "rgba(255,255,255,0.85)",
|
|
maxWidth: 520,
|
|
}}
|
|
>
|
|
Consultez les publications WordPress, ajustez vos contenus et
|
|
maintenez un flux éditorial cohérent pour Octopus.
|
|
</Typography>
|
|
</Stack>
|
|
<Button
|
|
startIcon={<Add />}
|
|
variant="contained"
|
|
onClick={handleCreate}
|
|
sx={{ ...composeButtonSx("primary") }}
|
|
>
|
|
Nouvel article
|
|
</Button>
|
|
</Stack>
|
|
|
|
<Grid container spacing={2.5} sx={{ mb: 4 }}>
|
|
<Grid item xs={12} sm={4}>
|
|
<Card className="glass-subcard" sx={{ borderRadius: 3, p: 2.5 }}>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: "rgba(11, 26, 61, 0.6)", fontWeight: 600 }}
|
|
>
|
|
Total articles
|
|
</Typography>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
|
>
|
|
{stats.total}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
|
>
|
|
{filteredPosts.length} résultat(s) après filtre
|
|
</Typography>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={12} sm={4}>
|
|
<Card className="glass-subcard" sx={{ borderRadius: 3, p: 2.5 }}>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: "rgba(11, 26, 61, 0.6)", fontWeight: 600 }}
|
|
>
|
|
Articles illustrés
|
|
</Typography>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
|
>
|
|
{stats.withImage}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
|
>
|
|
{stats.withoutImage} sans visuel associé
|
|
</Typography>
|
|
</Card>
|
|
</Grid>
|
|
<Grid item xs={12} sm={4}>
|
|
<Card className="glass-subcard" sx={{ borderRadius: 3, p: 2.5 }}>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{ color: "rgba(11, 26, 61, 0.6)", fontWeight: 600 }}
|
|
>
|
|
Dernière publication
|
|
</Typography>
|
|
<Typography
|
|
variant="subtitle1"
|
|
sx={{ color: "#0b1a3d", fontWeight: 700, lineHeight: 1.4 }}
|
|
>
|
|
{stats.latestLabel}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
|
>
|
|
{stats.latestDate}
|
|
</Typography>
|
|
</Card>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Card
|
|
className="glass-subcard"
|
|
sx={{
|
|
borderRadius: 4,
|
|
p: { xs: 2.5, md: 3 },
|
|
mb: 4,
|
|
boxShadow: "0 24px 36px -28px rgba(12, 29, 74, 0.45)",
|
|
}}
|
|
>
|
|
<Stack
|
|
direction={{ xs: "column", md: "row" }}
|
|
spacing={2}
|
|
alignItems={{ xs: "stretch", md: "center" }}
|
|
justifyContent="space-between"
|
|
>
|
|
<Box>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{ color: "#0b1a3d", fontWeight: 700 }}
|
|
>
|
|
Rechercher un article
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
|
|
>
|
|
Filtrez par titre ou résumé pour retrouver un contenu en quelques
|
|
secondes.
|
|
</Typography>
|
|
</Box>
|
|
<TextField
|
|
label="Rechercher un article..."
|
|
variant="outlined"
|
|
fullWidth
|
|
value={searchTerm}
|
|
onChange={(event) => setSearchTerm(event.target.value)}
|
|
sx={{
|
|
maxWidth: 380,
|
|
"& .MuiOutlinedInput-root": {
|
|
borderRadius: 999,
|
|
backgroundColor: "rgba(255,255,255,0.92)",
|
|
},
|
|
}}
|
|
/>
|
|
</Stack>
|
|
</Card>
|
|
|
|
{loading ? (
|
|
<Box sx={{ textAlign: "center", py: 6 }}>
|
|
<CircularProgress color="inherit" />
|
|
</Box>
|
|
) : filteredPosts.length === 0 ? (
|
|
<Card
|
|
className="glass-subcard"
|
|
sx={{
|
|
borderRadius: 4,
|
|
p: 4,
|
|
textAlign: "center",
|
|
color: "#0b1a3d",
|
|
}}
|
|
>
|
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
|
|
Aucun article trouvé
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ color: "rgba(11, 26, 61, 0.65)" }}>
|
|
Ajustez votre recherche ou créez un nouvel article pour alimenter le blog.
|
|
</Typography>
|
|
<Button
|
|
startIcon={<Add />}
|
|
variant="contained"
|
|
onClick={handleCreate}
|
|
sx={{ ...composeButtonSx("primary"), mt: 3 }}
|
|
>
|
|
Créer un article
|
|
</Button>
|
|
</Card>
|
|
) : (
|
|
<Grid container spacing={2.5}>
|
|
{filteredPosts.map((post) => (
|
|
<Grid item xs={12} md={6} key={post.id}>
|
|
<Card
|
|
className="glass-article-card"
|
|
sx={{
|
|
borderRadius: 4,
|
|
overflow: "hidden",
|
|
position: "relative",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
height: "100%",
|
|
boxShadow: "0 32px 40px -28px rgba(12, 29, 74, 0.5)",
|
|
}}
|
|
>
|
|
<CardMedia
|
|
component="div"
|
|
image={
|
|
post.image ||
|
|
"https://images.unsplash.com/photo-1524995997946-a1c2e315a42f?auto=format&fit=crop&w=1200&q=80"
|
|
}
|
|
sx={{
|
|
height: 200,
|
|
backgroundSize: "cover",
|
|
backgroundPosition: "center",
|
|
}}
|
|
/>
|
|
<CardContent
|
|
sx={{
|
|
flexGrow: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 1.5,
|
|
p: { xs: 3, md: 3.5 },
|
|
}}
|
|
>
|
|
<Stack direction="row" spacing={1} alignItems="center">
|
|
<Chip
|
|
size="small"
|
|
label={`ID ${post.id}`}
|
|
sx={{
|
|
bgcolor: "rgba(49,83,151,0.12)",
|
|
color: "#315397",
|
|
fontWeight: 600,
|
|
}}
|
|
/>
|
|
<Tooltip title={stats.latestLabel === post.title.rendered ? "Dernière publication" : ""}>
|
|
<Chip
|
|
size="small"
|
|
label={formatDate(post.date)}
|
|
sx={{
|
|
bgcolor: "rgba(255,255,255,0.72)",
|
|
color: "#0b1a3d",
|
|
fontWeight: 600,
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
</Stack>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{ color: "#0b1a3d", fontWeight: 700, lineHeight: 1.4 }}
|
|
>
|
|
{post.title.rendered}
|
|
</Typography>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{ color: "rgba(11, 26, 61, 0.68)", lineHeight: 1.6 }}
|
|
>
|
|
{previewExcerpt(post.excerpt?.rendered)}
|
|
</Typography>
|
|
<Stack
|
|
direction={{ xs: "column", sm: "row" }}
|
|
spacing={1}
|
|
sx={{ mt: "auto" }}
|
|
>
|
|
<Button
|
|
startIcon={<Edit />}
|
|
variant="contained"
|
|
onClick={() => handleEdit(post.id)}
|
|
sx={{ ...composeButtonSx("outline", "small") }}
|
|
>
|
|
Modifier
|
|
</Button>
|
|
<Button
|
|
startIcon={<Delete />}
|
|
variant="contained"
|
|
onClick={() => handleDeletePost(post.id, post.imageId)}
|
|
sx={{ ...composeButtonSx("danger", "small") }}
|
|
>
|
|
Supprimer
|
|
</Button>
|
|
</Stack>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default GestionArticles;
|