Maj gestion des articles

This commit is contained in:
sebvtl728
2025-01-31 20:05:03 +01:00
parent 841161cb9b
commit 0d42ce54fc
7 changed files with 518 additions and 113 deletions
+122 -43
View File
@@ -1,54 +1,133 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { uploadImage, createPost } from "../../wordpress";
import { getToken } from "../../auth";
import {
Box,
Container,
Typography,
TextField,
Button,
Paper,
Input,
IconButton,
} from "@mui/material";
import { ArrowBack, Publish } from "@mui/icons-material";
function CreatePost() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [file, setFile] = useState(null);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [file, setFile] = useState(null);
const navigate = useNavigate();
const handleSubmit = async (e) => {
e.preventDefault();
// ✅ Vérification de l'authentification : Redirection vers /login si l'utilisateur n'est pas connecté
useEffect(() => {
if (!getToken()) {
navigate("/admin/login");
}
}, [navigate]);
try {
console.log("📢 Tentative d'upload de l'image...");
const imageId = await uploadImage(file);
const handleSubmit = async (e) => {
e.preventDefault();
console.log("📢 Création du post avec l'image mise en avant...");
await createPost(title, content, imageId);
try {
console.log("📢 Tentative d'upload de l'image...");
const imageId = await uploadImage(file);
alert("✅ Post créé avec succès !");
} catch (error) {
alert("❌ Erreur lors de la création du post.");
}
};
console.log("📢 Création du post avec l'image mise en avant...");
await createPost(title, content, imageId);
return (
<div>
<h2>Créer un post</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Titre du post"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<textarea
placeholder="Contenu du post"
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
<input
type="file"
accept="image/*"
onChange={(e) => setFile(e.target.files[0])}
required
/>
<button type="submit">Publier</button>
</form>
</div>
);
alert("✅ Post créé avec succès !");
navigate("/admin/gestion-articles"); // Redirection après la création
} catch (error) {
alert("❌ Erreur lors de la création du post.");
}
};
return (
<Box
sx={{
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundImage: "url('https://source.unsplash.com/1600x900/?office,writing')",
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
<Container maxWidth="sm">
<Paper elevation={6} sx={{ padding: 4, borderRadius: 3 }}>
{/* ✅ Bouton Retour à la Gestion des Articles */}
<Button
startIcon={<ArrowBack />}
variant="outlined"
color="secondary"
onClick={() => navigate("/admin/gestion-articles")}
sx={{ mb: 2 }}
>
Retour à la gestion des articles
</Button>
{/* ✅ Titre de la page */}
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}>
Créer un article
</Typography>
{/* ✅ Formulaire */}
<form onSubmit={handleSubmit}>
<TextField
label="Titre de l'article"
fullWidth
margin="normal"
variant="outlined"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<TextField
label="Contenu"
fullWidth
margin="normal"
variant="outlined"
multiline
rows={4}
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
{/* ✅ Upload de l'image */}
<Box sx={{ mt: 2 }}>
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
Image en vedette :
</Typography>
<Input
type="file"
accept="image/*"
onChange={(e) => setFile(e.target.files[0])}
required
sx={{ display: "block", mb: 2 }}
/>
</Box>
{/* ✅ Bouton Publier */}
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
startIcon={<Publish />}
sx={{ mt: 3 }}
>
Publier l'article
</Button>
</form>
</Paper>
</Container>
</Box>
);
}
export default CreatePost;
+38 -2
View File
@@ -1,4 +1,6 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { getToken, logout } from "../../auth"; // Importation de la fonction logout
import {
Box,
Typography,
@@ -6,12 +8,26 @@ import {
Card,
CardContent,
IconButton,
Button,
} from "@mui/material";
import LogoutIcon from "@mui/icons-material/Logout"; // Icône de déconnexion
import ArticleIcon from "@mui/icons-material/Article";
import DashboardIcon from "@mui/icons-material/Dashboard";
function Dashboard() {
const navigate = useNavigate();
const token = getToken();
useEffect(() => {
if (!token) {
navigate("/admin/login"); // Redirige vers la page de connexion si non authentifié
}
}, [token, navigate]);
const handleLogout = () => {
logout(); // Déconnecte l'utilisateur
navigate("/admin/login"); // Redirige vers la page de connexion
};
return (
<Box
@@ -35,10 +51,30 @@ function Dashboard() {
borderRadius: 3,
boxShadow: 6,
textAlign: "center",
position: "relative", // Permet de positionner des éléments enfants
}}
>
{/* Bouton de déconnexion placé en haut à gauche de la boîte blanche */}
<Button
variant="contained"
color="error"
startIcon={<LogoutIcon />}
onClick={handleLogout}
sx={{
position: "absolute",
top: 10,
left: 10,
backgroundColor: "#d32f2f",
"&:hover": { backgroundColor: "#b71c1c" },
fontSize: "0.875rem",
padding: "6px 12px",
}}
>
</Button>
{/* En-tête */}
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 4 }}>
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 4, mt: 4 }}>
<DashboardIcon sx={{ fontSize: 40, color: "#0e467f", mr: 1 }} />
Tableau de Bord
</Typography>
@@ -47,7 +83,7 @@ function Dashboard() {
<Grid container spacing={3} justifyContent="center">
<Grid item xs={12} sm={6} md={4}>
<Card
onClick={() => navigate("/posts")}
onClick={() => navigate("/admin/gestion-articles")}
sx={{
cursor: "pointer",
textAlign: "center",
@@ -0,0 +1,183 @@
// ✅ Importations nécessaires
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Box, Typography, Button, Table, TableBody, TableCell,
TableContainer, TableHead, TableRow, Paper, TextField, Avatar
} from "@mui/material";
import { Add, ArrowBack, Edit, Delete } from "@mui/icons-material";
import api from "../../api";
import { getToken } from "../../auth";
const GestionArticles = () => {
const [posts, setPosts] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
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 {
const response = await api.get("wp/v2/posts?_fields=id,title,excerpt,featured_media");
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);
}
};
fetchPosts();
}, []);
// ✅ Supprimer un article et son image associée
const handleDeletePost = async (postId, imageId) => {
if (!window.confirm("⚠ Es-tu sûr de vouloir supprimer cet article ?")) return;
try {
const token = getToken(); // 🔑 Récupérer le token JWT
if (!token) {
alert("❌ Erreur : utilisateur non authentifié !");
navigate("/admin/login");
return;
}
const headers = {
Authorization: `Bearer ${token}`, // ✅ Ajout du token dans l'en-tête
"Content-Type": "application/json",
};
console.log(`📢 Suppression de l'article ID: ${postId}`);
// ✅ Suppression de l'article (avec force=true)
await api.delete(`wp/v2/posts/${postId}?force=true`, { headers });
// ✅ Suppression de l'image associée si elle existe
if (imageId) {
console.log(`📢 Suppression de l'image ID: ${imageId}`);
await api.delete(`wp/v2/media/${imageId}?force=true`, { headers });
}
// ✅ Mise à jour de la liste après suppression
setPosts(posts.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.\n\nDétails: ${error.response?.data?.message || error.message}`);
}
};
// ✅ Filtrage des articles
const filteredPosts = posts.filter((post) =>
post.title.rendered.toLowerCase().includes(searchTerm.toLowerCase()) ||
post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<Box sx={{ padding: "40px 20px" }}>
{/* ✅ En-tête avec retour et création */}
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 4, mt:5 }}>
<Button startIcon={<ArrowBack />} variant="outlined" color="secondary" onClick={() => navigate("/admin/dashboard")}>
Retour au Dashboard
</Button>
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", flexGrow: 1 }}>
Gestion des Articles
</Typography>
<Button startIcon={<Add />} variant="contained" color="primary" onClick={() => navigate("/admin/create-post")}>
Créer un article
</Button>
</Box>
{/* ✅ Recherche */}
<TextField
label="Rechercher un article..."
variant="outlined"
fullWidth
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
sx={{ mb: 3 }}
/>
{/* ✅ Tableau des articles */}
<TableContainer component={Paper}>
<Table>
<TableHead sx={{ backgroundColor: "#0e467f" }}>
<TableRow>
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Image</TableCell>
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Titre</TableCell>
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Résumé</TableCell>
<TableCell sx={{ color: "white", fontWeight: "bold", textAlign: "center" }}>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredPosts.length > 0 ? (
filteredPosts.map((post) => (
<TableRow key={post.id}>
{/* ✅ Image en vedette */}
<TableCell>
<Avatar
src={post.image || "https://via.placeholder.com/100"}
variant="rounded"
sx={{ width: 80, height: 80 }}
/>
</TableCell>
{/* ✅ Titre de l'article */}
<TableCell sx={{ fontWeight: "bold" }}>{post.title.rendered}</TableCell>
{/* ✅ Résumé avec 100 caractères max */}
<TableCell>
{post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").substring(0, 100)}...
</TableCell>
{/* ✅ Boutons Modifier et Supprimer */}
<TableCell sx={{ textAlign: "center" }}>
<Button startIcon={<Edit />} variant="outlined" color="warning" sx={{ mr: 1 }} >
Modifier
</Button>
<Button
startIcon={<Delete />}
variant="outlined"
color="error"
onClick={() => handleDeletePost(post.id, post.imageId)}
>
Supprimer
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={4} sx={{ textAlign: "center", py: 2 }}>
Aucun article trouvé.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};
export default GestionArticles;
+20 -10
View File
@@ -1,5 +1,6 @@
import { useState } from "react";
import { loginWithAppPassword, getToken, logout } from "../../auth";
import { useNavigate } from "react-router-dom"; // Import de useNavigate pour la redirection
import {
Box,
Button,
@@ -16,13 +17,13 @@ function TestLogin() {
const [username, setUsername] = useState("");
const [appPassword, setAppPassword] = useState("");
const [token, setToken] = useState(getToken());
const navigate = useNavigate(); // Hook pour la navigation
const handleLogin = async (e) => {
e.preventDefault();
const newToken = await loginWithAppPassword(username, appPassword);
if (newToken) {
setToken(newToken);
// alert("Connexion réussie !");
} else {
alert("Échec de la connexion !");
}
@@ -31,7 +32,6 @@ function TestLogin() {
const handleLogout = () => {
logout();
setToken(null);
// alert("Déconnexion réussie !");
};
return (
@@ -66,14 +66,24 @@ function TestLogin() {
</Typography>
{token ? (
<Button
variant="contained"
color="error"
onClick={handleLogout}
sx={{ mt: 3, width: "100%" }}
>
Déconnexion
</Button>
<>
<Button
variant="contained"
color="primary"
onClick={() => navigate("/admin/dashboard")}
sx={{ mt: 3, width: "100%" }}
>
Accéder au tableau de bord
</Button>
<Button
variant="contained"
color="error"
onClick={handleLogout}
sx={{ mt: 2, width: "100%" }}
>
Déconnexion
</Button>
</>
) : (
<form onSubmit={handleLogin}>
<TextField
+82 -56
View File
@@ -1,64 +1,90 @@
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardContent, CardMedia, Typography, Box } from '@mui/material';
import api from '../../api'; // Adaptez le chemin vers l'instance Axios
import React, { useState, useEffect } from "react";
import { Box, Typography, Grid, Card, CardContent, CardMedia, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import api from "../../api";
const Posts = () => {
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await api.get("wp/v2/posts?_fields=id,title,excerpt,content,featured_media");
const postsData = response.data;
const Post = () => {
const [posts, setPosts] = useState([]);
// Récupérer les images en vedette
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 };
} catch (error) {
console.error(`Erreur lors de la récupération de l'image pour l'article ${post.id}:`, error);
return { ...post, image: null };
}
}
return { ...post, image: null };
})
);
useEffect(() => {
api.get('wp/v2/posts?_embed')
.then((response) => {
setPosts(response.data);
})
.catch((error) => {
console.error('Erreur lors de la récupération des articles :', error);
});
}, []);
setPosts(postsWithImages);
} catch (error) {
console.error("Erreur lors de la récupération des articles :", error);
}
};
return (
fetchPosts();
}, []);
<Box sx={{ padding: 4 }}>
<Typography variant="h3" sx={{ marginBottom: 4, textAlign: 'center' }}>
Nos réalisations
</Typography>
<Grid container spacing={4}>
{posts.map((post) => (
<Grid item xs={12} sm={6} md={4} key={post.id}>
<Card sx={{ maxWidth: 345,
boxShadow: 3,
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
'&:hover': {
transform: 'scale(1.05)',
boxShadow: 6,
},
}}>
{/* Image de l'article */}
{post._embedded?.['wp:featuredmedia']?.[0]?.source_url && (
<CardMedia
component="img"
height="200"
image={post._embedded['wp:featuredmedia'][0].source_url}
alt={post.title.rendered}
/>
)}
<CardContent>
<Typography variant="h5" component="div" gutterBottom>
{post.title.rendered}
</Typography>
<Typography
variant="body2"
color="text.secondary"
dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
/>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
);
// Fonction pour nettoyer le HTML et récupérer un texte brut
const stripHtmlTags = (html) => {
return html.replace(/(<([^>]+)>)/gi, ""); // Supprime toutes les balises HTML
};
// Fonction pour tronquer un texte à 100 caractères max
const truncateText = (text, maxLength) => {
const strippedText = stripHtmlTags(text); // Nettoyage HTML
return strippedText.length > maxLength ? `${strippedText.substring(0, maxLength)}...` : strippedText;
};
return (
<Box sx={{ padding: "40px 20px" }}>
<Typography variant="h2" sx={{ fontWeight: "bold", textAlign: "center", mb: 4 }}>
Nos Articles
</Typography>
<Grid container spacing={4} justifyContent="center">
{posts.map((post) => (
<Grid item xs={12} sm={6} md={4} key={post.id}>
<Card
sx={{
cursor: "pointer",
"&:hover": { backgroundColor: "#0e467f", color: "white", transform: "scale(1.05)", boxShadow: 6 },
}}
onClick={() => navigate(`/post/${post.id}`)}
>
{post.image && (
<CardMedia component="img" height="200" image={post.image} alt={post.title.rendered} />
)}
<CardContent>
<Typography variant="h5" sx={{ fontWeight: "bold" }}>
{post.title.rendered}
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
{truncateText(post.excerpt.rendered || post.content.rendered, 100)}
</Typography>
<Button variant="outlined" sx={{ mt: 2, color: "white", borderColor: "white" }}>
Lire plus
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
);
};
export default Post;
export default Posts;
+67
View File
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from "react";
import { Box, Typography, Button, CircularProgress } from "@mui/material";
import { useParams, useNavigate } from "react-router-dom";
import api from "../api";
const PostDetails = () => {
const { id } = useParams();
const [post, setPost] = useState(null);
const [image, setImage] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const fetchPost = async () => {
try {
const response = await api.get(`wp/v2/posts/${id}?_fields=title,content,featured_media,date`);
setPost(response.data);
// Récupération de l'image
if (response.data.featured_media) {
const mediaResponse = await api.get(`wp/v2/media/${response.data.featured_media}`);
setImage(mediaResponse.data.source_url);
}
} catch (error) {
console.error("Erreur lors de la récupération de l'article :", error);
}
};
fetchPost();
}, [id]);
if (!post) {
return <CircularProgress sx={{ display: "block", margin: "auto", mt: 5 }} />;
}
return (
<Box sx={{ padding: "40px 20px",display:"flex", flexDirection:"column" }}>
<Button onClick={() => navigate(-1)} variant="outlined" sx={{ mt:5, mb: 3, maxWidth:"200px" }}>
Retour
</Button>
<Typography variant="h1" sx={{
fontWeight: "bold",
textAlign: "center",
mb: 4,
fontSize: { xs: '3rem', md: '3rem', lg: '3rem' }, // Responsive
}}>
{post.title.rendered}
</Typography>
<Box sx={{ display:"flex", alignItems:"center", justifyContent:"center", padding: "40px 20px",margin:"auto" }}>
{image && (
<Box
component="img"
src={image}
alt={post.title.rendered}
sx={{ width: "33%", maxHeight: "400px", objectFit: "cover", borderRadius: "8px", mb: 3 }}
/>
)}
<Typography variant="body1" sx={{ maxWidth: "800px", mx: "auto" }} dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</Box>
</Box>
);
};
export default PostDetails;
+5 -1
View File
@@ -6,6 +6,7 @@ import theme from "./theme";
import { HelmetProvider } from "react-helmet-async";
import SimpleReactLightbox from "simple-react-lightbox";
import NotFound from "./components/Pages/NotFound.jsx";
import GestionArticles from "./components/Pages/GestionArticles";
// Lazy loading des pages
@@ -13,6 +14,7 @@ const Home = lazy(() => import('./components/Pages/Home.jsx'));
const About = lazy(() => import('./components/Pages/About'));
const Contact = lazy(() => import('./components/Pages/Contact'));
const Post = lazy(() => import('./components/Pages/Post'));
const PostDetails = lazy(() => import('./components/PostDetails')) ;
const Search = lazy(() => import('./components/Pages/Search')); // Nouveau composant pour la recherche
const BureauEtude = lazy(() => import('./components/Pages/BureauEtude'));
const CreatePost = lazy(() => import('./components/Pages/CreatePost'));
@@ -45,10 +47,12 @@ function YourApp() {
<Route path="/services/prestation-maitrise-oeuvre" element={<ServiceUn />} />
<Route path="/services/structure-beton-charpente-metallique-bois" element={<ServiceDeux />} />
<Route path="/services/formation-video" element={<ServiceTrois />} />
<Route path="/create-post" element={<CreatePost />} />
<Route path="/admin/create-post" element={<CreatePost />} />
<Route path="/post/:id" element={<PostDetails />} />
<Route path="/admin/posts" element={<PostList />} />
<Route path="/admin/login" element={<Login />} />
<Route path="/admin/dashboard" element={<Dashboard />} />
<Route path="/admin/gestion-articles" element={<GestionArticles />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Footer />