Maj gestion des articles
This commit is contained in:
@@ -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 { 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() {
|
function CreatePost() {
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [file, setFile] = useState(null);
|
const [file, setFile] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
// ✅ Vérification de l'authentification : Redirection vers /login si l'utilisateur n'est pas connecté
|
||||||
e.preventDefault();
|
useEffect(() => {
|
||||||
|
if (!getToken()) {
|
||||||
|
navigate("/admin/login");
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
try {
|
const handleSubmit = async (e) => {
|
||||||
console.log("📢 Tentative d'upload de l'image...");
|
e.preventDefault();
|
||||||
const imageId = await uploadImage(file);
|
|
||||||
|
|
||||||
console.log("📢 Création du post avec l'image mise en avant...");
|
try {
|
||||||
await createPost(title, content, imageId);
|
console.log("📢 Tentative d'upload de l'image...");
|
||||||
|
const imageId = await uploadImage(file);
|
||||||
|
|
||||||
alert("✅ Post créé avec succès !");
|
console.log("📢 Création du post avec l'image mise en avant...");
|
||||||
} catch (error) {
|
await createPost(title, content, imageId);
|
||||||
alert("❌ Erreur lors de la création du post.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
alert("✅ Post créé avec succès !");
|
||||||
<div>
|
navigate("/admin/gestion-articles"); // Redirection après la création
|
||||||
<h2>Créer un post</h2>
|
} catch (error) {
|
||||||
<form onSubmit={handleSubmit}>
|
alert("❌ Erreur lors de la création du post.");
|
||||||
<input
|
}
|
||||||
type="text"
|
};
|
||||||
placeholder="Titre du post"
|
|
||||||
value={title}
|
return (
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
<Box
|
||||||
required
|
sx={{
|
||||||
/>
|
minHeight: "100vh",
|
||||||
<textarea
|
display: "flex",
|
||||||
placeholder="Contenu du post"
|
alignItems: "center",
|
||||||
value={content}
|
justifyContent: "center",
|
||||||
onChange={(e) => setContent(e.target.value)}
|
backgroundImage: "url('https://source.unsplash.com/1600x900/?office,writing')",
|
||||||
required
|
backgroundSize: "cover",
|
||||||
/>
|
backgroundPosition: "center",
|
||||||
<input
|
}}
|
||||||
type="file"
|
>
|
||||||
accept="image/*"
|
<Container maxWidth="sm">
|
||||||
onChange={(e) => setFile(e.target.files[0])}
|
<Paper elevation={6} sx={{ padding: 4, borderRadius: 3 }}>
|
||||||
required
|
{/* ✅ Bouton Retour à la Gestion des Articles */}
|
||||||
/>
|
<Button
|
||||||
<button type="submit">Publier</button>
|
startIcon={<ArrowBack />}
|
||||||
</form>
|
variant="outlined"
|
||||||
</div>
|
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;
|
export default CreatePost;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { getToken, logout } from "../../auth"; // Importation de la fonction logout
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -6,12 +8,26 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Button,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import LogoutIcon from "@mui/icons-material/Logout"; // Icône de déconnexion
|
||||||
import ArticleIcon from "@mui/icons-material/Article";
|
import ArticleIcon from "@mui/icons-material/Article";
|
||||||
import DashboardIcon from "@mui/icons-material/Dashboard";
|
import DashboardIcon from "@mui/icons-material/Dashboard";
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const navigate = useNavigate();
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -35,10 +51,30 @@ function Dashboard() {
|
|||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
boxShadow: 6,
|
boxShadow: 6,
|
||||||
textAlign: "center",
|
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 */}
|
{/* 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 }} />
|
<DashboardIcon sx={{ fontSize: 40, color: "#0e467f", mr: 1 }} />
|
||||||
Tableau de Bord
|
Tableau de Bord
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -47,7 +83,7 @@ function Dashboard() {
|
|||||||
<Grid container spacing={3} justifyContent="center">
|
<Grid container spacing={3} justifyContent="center">
|
||||||
<Grid item xs={12} sm={6} md={4}>
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
<Card
|
<Card
|
||||||
onClick={() => navigate("/posts")}
|
onClick={() => navigate("/admin/gestion-articles")}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
textAlign: "center",
|
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;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { loginWithAppPassword, getToken, logout } from "../../auth";
|
import { loginWithAppPassword, getToken, logout } from "../../auth";
|
||||||
|
import { useNavigate } from "react-router-dom"; // Import de useNavigate pour la redirection
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -16,13 +17,13 @@ function TestLogin() {
|
|||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [appPassword, setAppPassword] = useState("");
|
const [appPassword, setAppPassword] = useState("");
|
||||||
const [token, setToken] = useState(getToken());
|
const [token, setToken] = useState(getToken());
|
||||||
|
const navigate = useNavigate(); // Hook pour la navigation
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
const handleLogin = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newToken = await loginWithAppPassword(username, appPassword);
|
const newToken = await loginWithAppPassword(username, appPassword);
|
||||||
if (newToken) {
|
if (newToken) {
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
// alert("Connexion réussie !");
|
|
||||||
} else {
|
} else {
|
||||||
alert("Échec de la connexion !");
|
alert("Échec de la connexion !");
|
||||||
}
|
}
|
||||||
@@ -31,7 +32,6 @@ function TestLogin() {
|
|||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout();
|
||||||
setToken(null);
|
setToken(null);
|
||||||
// alert("Déconnexion réussie !");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -66,14 +66,24 @@ function TestLogin() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{token ? (
|
{token ? (
|
||||||
<Button
|
<>
|
||||||
variant="contained"
|
<Button
|
||||||
color="error"
|
variant="contained"
|
||||||
onClick={handleLogout}
|
color="primary"
|
||||||
sx={{ mt: 3, width: "100%" }}
|
onClick={() => navigate("/admin/dashboard")}
|
||||||
>
|
sx={{ mt: 3, width: "100%" }}
|
||||||
Déconnexion
|
>
|
||||||
</Button>
|
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}>
|
<form onSubmit={handleLogin}>
|
||||||
<TextField
|
<TextField
|
||||||
|
|||||||
@@ -1,64 +1,90 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState, useEffect } from "react";
|
||||||
import { Grid, Card, CardContent, CardMedia, Typography, Box } from '@mui/material';
|
import { Box, Typography, Grid, Card, CardContent, CardMedia, Button } from "@mui/material";
|
||||||
import api from '../../api'; // Adaptez le chemin vers l'instance Axios
|
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 = () => {
|
// Récupérer les images en vedette
|
||||||
const [posts, setPosts] = useState([]);
|
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(() => {
|
setPosts(postsWithImages);
|
||||||
api.get('wp/v2/posts?_embed')
|
} catch (error) {
|
||||||
.then((response) => {
|
console.error("Erreur lors de la récupération des articles :", error);
|
||||||
setPosts(response.data);
|
}
|
||||||
})
|
};
|
||||||
.catch((error) => {
|
|
||||||
console.error('Erreur lors de la récupération des articles :', error);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
fetchPosts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
<Box sx={{ padding: 4 }}>
|
// Fonction pour nettoyer le HTML et récupérer un texte brut
|
||||||
<Typography variant="h3" sx={{ marginBottom: 4, textAlign: 'center' }}>
|
const stripHtmlTags = (html) => {
|
||||||
Nos réalisations
|
return html.replace(/(<([^>]+)>)/gi, ""); // Supprime toutes les balises HTML
|
||||||
</Typography>
|
};
|
||||||
<Grid container spacing={4}>
|
|
||||||
{posts.map((post) => (
|
// Fonction pour tronquer un texte à 100 caractères max
|
||||||
<Grid item xs={12} sm={6} md={4} key={post.id}>
|
const truncateText = (text, maxLength) => {
|
||||||
<Card sx={{ maxWidth: 345,
|
const strippedText = stripHtmlTags(text); // Nettoyage HTML
|
||||||
boxShadow: 3,
|
return strippedText.length > maxLength ? `${strippedText.substring(0, maxLength)}...` : strippedText;
|
||||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
};
|
||||||
'&:hover': {
|
|
||||||
transform: 'scale(1.05)',
|
return (
|
||||||
boxShadow: 6,
|
<Box sx={{ padding: "40px 20px" }}>
|
||||||
},
|
<Typography variant="h2" sx={{ fontWeight: "bold", textAlign: "center", mb: 4 }}>
|
||||||
}}>
|
Nos Articles
|
||||||
{/* Image de l'article */}
|
</Typography>
|
||||||
{post._embedded?.['wp:featuredmedia']?.[0]?.source_url && (
|
|
||||||
<CardMedia
|
<Grid container spacing={4} justifyContent="center">
|
||||||
component="img"
|
{posts.map((post) => (
|
||||||
height="200"
|
<Grid item xs={12} sm={6} md={4} key={post.id}>
|
||||||
image={post._embedded['wp:featuredmedia'][0].source_url}
|
<Card
|
||||||
alt={post.title.rendered}
|
sx={{
|
||||||
/>
|
cursor: "pointer",
|
||||||
)}
|
"&:hover": { backgroundColor: "#0e467f", color: "white", transform: "scale(1.05)", boxShadow: 6 },
|
||||||
<CardContent>
|
}}
|
||||||
<Typography variant="h5" component="div" gutterBottom>
|
onClick={() => navigate(`/post/${post.id}`)}
|
||||||
{post.title.rendered}
|
>
|
||||||
</Typography>
|
{post.image && (
|
||||||
<Typography
|
<CardMedia component="img" height="200" image={post.image} alt={post.title.rendered} />
|
||||||
variant="body2"
|
)}
|
||||||
color="text.secondary"
|
<CardContent>
|
||||||
dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
|
<Typography variant="h5" sx={{ fontWeight: "bold" }}>
|
||||||
/>
|
{post.title.rendered}
|
||||||
</CardContent>
|
</Typography>
|
||||||
</Card>
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
</Grid>
|
{truncateText(post.excerpt.rendered || post.content.rendered, 100)}
|
||||||
))}
|
</Typography>
|
||||||
</Grid>
|
<Button variant="outlined" sx={{ mt: 2, color: "white", borderColor: "white" }}>
|
||||||
</Box>
|
Lire plus
|
||||||
);
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Post;
|
export default Posts;
|
||||||
@@ -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;
|
||||||
@@ -6,6 +6,7 @@ import theme from "./theme";
|
|||||||
import { HelmetProvider } from "react-helmet-async";
|
import { HelmetProvider } from "react-helmet-async";
|
||||||
import SimpleReactLightbox from "simple-react-lightbox";
|
import SimpleReactLightbox from "simple-react-lightbox";
|
||||||
import NotFound from "./components/Pages/NotFound.jsx";
|
import NotFound from "./components/Pages/NotFound.jsx";
|
||||||
|
import GestionArticles from "./components/Pages/GestionArticles";
|
||||||
|
|
||||||
|
|
||||||
// Lazy loading des pages
|
// Lazy loading des pages
|
||||||
@@ -13,6 +14,7 @@ const Home = lazy(() => import('./components/Pages/Home.jsx'));
|
|||||||
const About = lazy(() => import('./components/Pages/About'));
|
const About = lazy(() => import('./components/Pages/About'));
|
||||||
const Contact = lazy(() => import('./components/Pages/Contact'));
|
const Contact = lazy(() => import('./components/Pages/Contact'));
|
||||||
const Post = lazy(() => import('./components/Pages/Post'));
|
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 Search = lazy(() => import('./components/Pages/Search')); // Nouveau composant pour la recherche
|
||||||
const BureauEtude = lazy(() => import('./components/Pages/BureauEtude'));
|
const BureauEtude = lazy(() => import('./components/Pages/BureauEtude'));
|
||||||
const CreatePost = lazy(() => import('./components/Pages/CreatePost'));
|
const CreatePost = lazy(() => import('./components/Pages/CreatePost'));
|
||||||
@@ -45,10 +47,12 @@ function YourApp() {
|
|||||||
<Route path="/services/prestation-maitrise-oeuvre" element={<ServiceUn />} />
|
<Route path="/services/prestation-maitrise-oeuvre" element={<ServiceUn />} />
|
||||||
<Route path="/services/structure-beton-charpente-metallique-bois" element={<ServiceDeux />} />
|
<Route path="/services/structure-beton-charpente-metallique-bois" element={<ServiceDeux />} />
|
||||||
<Route path="/services/formation-video" element={<ServiceTrois />} />
|
<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/posts" element={<PostList />} />
|
||||||
<Route path="/admin/login" element={<Login />} />
|
<Route path="/admin/login" element={<Login />} />
|
||||||
<Route path="/admin/dashboard" element={<Dashboard />} />
|
<Route path="/admin/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/admin/gestion-articles" element={<GestionArticles />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
Reference in New Issue
Block a user