Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10c1f53d91 | |||
| 555a63b61d | |||
| f25b0fa6e4 | |||
| 6eaa0fcf64 | |||
| f70d21ae71 | |||
| 57f618a0e4 | |||
| 8bccf65623 | |||
| 55436deb95 | |||
| 827a6ac092 | |||
| 0d42ce54fc | |||
| 841161cb9b | |||
| 81ac3d831e | |||
| c3ab2b54ca | |||
| 1e4dd3b475 | |||
| cc641c5f5d | |||
| e605d3915d | |||
| ba6603fc6f | |||
| 9eca9041fb | |||
| dc3c6a566b |
@@ -22,5 +22,3 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
.env
|
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header set Access-Control-Allow-Origin "*"
|
||||||
|
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE"
|
||||||
|
Header set Access-Control-Allow-Headers "Content-Type, Authorization"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_rewrite.c>
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
RewriteBase /
|
RewriteBase /
|
||||||
|
|||||||
Generated
+474
-346
File diff suppressed because it is too large
Load Diff
@@ -15,13 +15,13 @@
|
|||||||
"@mui/icons-material": "^6.3.0",
|
"@mui/icons-material": "^6.3.0",
|
||||||
"@mui/material": "^6.3.0",
|
"@mui/material": "^6.3.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"dotenv": "^16.4.7",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-helmet-async": "^2.0.5",
|
"react-helmet-async": "^2.0.5",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
"react-simple-lightbox": "^1.0.26",
|
"react-simple-lightbox": "^1.0.26",
|
||||||
|
"react-toastify": "^11.0.3",
|
||||||
"simple-react-lightbox": "^3.6.8",
|
"simple-react-lightbox": "^3.6.8",
|
||||||
"svgo": "^3.3.2",
|
"svgo": "^3.3.2",
|
||||||
"swiper": "^10.3.1"
|
"swiper": "^10.3.1"
|
||||||
|
|||||||
+13
-6
@@ -1,12 +1,19 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
|
|
||||||
// Création de l'instance Axios
|
const API_URL = "https://preprod.octopusdesign.fr/api-octopus/server/wp-json";
|
||||||
|
|
||||||
|
// 🔹 Instance Axios pour éviter de répéter l'URL
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'https://preprod.octopusdesign.fr/api-octopus/server/wp-json',
|
baseURL: API_URL,
|
||||||
withCredentials: true,
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔹 Fonction générique pour récupérer le token
|
||||||
|
export const getToken = () => {
|
||||||
|
return sessionStorage.getItem("custom_token");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔹 Exporter l'instance Axios
|
||||||
// Exportation de l'instance Axios par défaut
|
|
||||||
export default api;
|
export default api;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const API_URL = "https://preprod.octopusdesign.fr/api-octopus/server/wp-json";
|
||||||
|
|
||||||
|
// ✅ Connexion avec le mot de passe d’application
|
||||||
|
export const loginWithAppPassword = async (username, appPassword) => {
|
||||||
|
try {
|
||||||
|
const credentials = `${username}:${appPassword}`;
|
||||||
|
const encodedCredentials = btoa(credentials); // Encodage en Base64
|
||||||
|
|
||||||
|
const response = await axios.get(`${API_URL}/wp/v2/users/me`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${encodedCredentials}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ Connexion réussie :", response.data);
|
||||||
|
sessionStorage.setItem("wp_app_token", encodedCredentials);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur lors de la connexion :", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Récupération du token stocké
|
||||||
|
export const getToken = () => {
|
||||||
|
return sessionStorage.getItem("wp_app_token") || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Déconnexion
|
||||||
|
export const logout = () => {
|
||||||
|
console.log("🚪 Déconnexion...");
|
||||||
|
sessionStorage.removeItem("wp_app_token");
|
||||||
|
};
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
.critical-alert-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.critical-alert-box {
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 0 10px rgba(255, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import "./CriticalAlert.css";
|
|
||||||
|
|
||||||
const CriticalAlert = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
const [isActive, setIsActive] = useState(false);
|
|
||||||
const [opacity, setOpacity] = useState(1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateOpacity = () => {
|
|
||||||
const alertStatus = localStorage.getItem("criticalAlertActive");
|
|
||||||
const startTime = localStorage.getItem("criticalAlertStartTime");
|
|
||||||
const totalDuration = parseInt(localStorage.getItem("totalDuration") || "15", 10);
|
|
||||||
|
|
||||||
if (location.pathname === "/backtime") {
|
|
||||||
setOpacity(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alertStatus === "yes" && startTime) {
|
|
||||||
setIsActive(true);
|
|
||||||
|
|
||||||
const startTimestamp = parseInt(startTime, 10);
|
|
||||||
const now = Date.now();
|
|
||||||
const elapsedTime = now - startTimestamp;
|
|
||||||
const totalTime = totalDuration * 24 * 60 * 60 * 1000;
|
|
||||||
const remainingTime = totalTime - elapsedTime;
|
|
||||||
|
|
||||||
if (remainingTime <= 0) {
|
|
||||||
setOpacity(0);
|
|
||||||
} else {
|
|
||||||
const newOpacity = 1 - elapsedTime / totalTime;
|
|
||||||
setOpacity(newOpacity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateOpacity();
|
|
||||||
const interval = setInterval(updateOpacity, 10000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
isActive && location.pathname !== "/backtime" && (
|
|
||||||
<style>
|
|
||||||
{`body { opacity: ${opacity}; transition: opacity 10s linear; }`}
|
|
||||||
</style>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CriticalAlert;
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { deletePost } from "../wordpress";
|
||||||
|
|
||||||
|
function DeletePost({ postId, onDelete }) {
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (window.confirm("Es-tu sûr de vouloir supprimer cet article ?")) {
|
||||||
|
const success = await deletePost(postId);
|
||||||
|
if (success) {
|
||||||
|
alert("Article supprimé avec succès !");
|
||||||
|
onDelete(); // Rafraîchir la liste
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <button onClick={handleDelete}>🗑️ Supprimer</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeletePost;
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { getPostById, updatePost, uploadImage } 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 EditPost() {
|
||||||
|
const { id } = useParams(); // ✅ Récupère l'ID de l'article depuis l'URL
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// ✅ Vérification de l'authentification
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getToken()) {
|
||||||
|
navigate("/admin/login");
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// ✅ Récupérer les données de l'article
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPost = async () => {
|
||||||
|
try {
|
||||||
|
const post = await getPostById(id);
|
||||||
|
setTitle(post.title.rendered);
|
||||||
|
setContent(post.content.rendered.replace(/(<([^>]+)>)/gi, "")); // Enlever le HTML
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur chargement article :", error);
|
||||||
|
navigate("/admin/gestion-articles"); // Redirige si erreur
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPost();
|
||||||
|
}, [id, navigate]);
|
||||||
|
|
||||||
|
const handleUpdate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const updatedPost = await updatePost(id, title, content, file);
|
||||||
|
alert("✅ Article mis à jour avec succès !");
|
||||||
|
navigate("/admin/gestion-articles"); // Redirige après modification
|
||||||
|
} catch (error) {
|
||||||
|
alert("❌ Erreur lors de la mise à jour de l'article.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <Typography>Chargement...</Typography>;
|
||||||
|
|
||||||
|
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 }}>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBack />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => navigate("/admin/gestion-articles")}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
Retour à la gestion des articles
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}>
|
||||||
|
Modifier l'article
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<form onSubmit={handleUpdate}>
|
||||||
|
<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 }}>
|
||||||
|
Nouvelle image (facultatif) :
|
||||||
|
</Typography>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => setFile(e.target.files[0])}
|
||||||
|
sx={{ display: "block", mb: 2 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
startIcon={<Publish />}
|
||||||
|
sx={{ mt: 3 }}
|
||||||
|
>
|
||||||
|
Mettre à jour
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditPost;
|
||||||
@@ -49,7 +49,6 @@ const Expertices = ({
|
|||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
backgroundPosition: "center",
|
|
||||||
boxShadow: 3,
|
boxShadow: 3,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
@@ -99,7 +98,6 @@ const Expertices = ({
|
|||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
backgroundPosition: "center",
|
|
||||||
boxShadow: 3,
|
boxShadow: 3,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
@@ -149,7 +147,6 @@ const Expertices = ({
|
|||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12} md={4}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
backgroundPosition: "center",
|
|
||||||
boxShadow: 3,
|
boxShadow: 3,
|
||||||
padding: 2,
|
padding: 2,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
@@ -84,13 +84,14 @@ const Header = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position="static"
|
position="fixed"
|
||||||
color="transparent"
|
color="transparent"
|
||||||
elevation={0}
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
background: "linear-gradient(to right, #294A9A, #3158b3)",
|
background: "linear-gradient(to right, #294A9A, #3158b3)",
|
||||||
color: "white",
|
color: "white",
|
||||||
transition: "all 0.3s ease",
|
transition: "all 0.3s ease",
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
@@ -170,7 +171,7 @@ const Header = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/services/formation-ia"
|
to="/services/structure-beton-charpente-metallique-bois"
|
||||||
onClick={handleMenuClose}
|
onClick={handleMenuClose}
|
||||||
>
|
>
|
||||||
<WorkIcon sx={{ marginRight: 1 }} />
|
<WorkIcon sx={{ marginRight: 1 }} />
|
||||||
@@ -178,7 +179,7 @@ const Header = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/services/formation-video"
|
to="/services/electricite"
|
||||||
onClick={handleMenuClose}
|
onClick={handleMenuClose}
|
||||||
>
|
>
|
||||||
<WorkIcon sx={{ marginRight: 1}} />
|
<WorkIcon sx={{ marginRight: 1}} />
|
||||||
@@ -212,6 +213,19 @@ const Header = () => {
|
|||||||
>
|
>
|
||||||
À Propos
|
À Propos
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
to="/bureauEtude"
|
||||||
|
color="inherit"
|
||||||
|
sx={{
|
||||||
|
fontWeight: location.pathname === "/bureauEtude" ? "bold" : "normal",
|
||||||
|
textDecoration:
|
||||||
|
location.pathname === "/bureauEtude" ? "underline" : "none",
|
||||||
|
...commonButtonStyles,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bureau d'étude
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/contact"
|
to="/contact"
|
||||||
@@ -336,25 +350,26 @@ const Header = () => {
|
|||||||
<WorkIcon sx={{ marginRight: 0.5 }} />
|
<WorkIcon sx={{ marginRight: 0.5 }} />
|
||||||
<ListItemText primary="Prestation maitrise oeuvre" />
|
<ListItemText primary="Prestation maitrise oeuvre" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
button
|
button
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/services/formation-ia"
|
to="/services/structure-beton-charpente-metallique-bois"
|
||||||
selected={location.pathname.includes("/services/formation-ia")}
|
selected={location.pathname.includes("/services/structure-beton-charpente-metallique-bois")}
|
||||||
>
|
>
|
||||||
<WorkIcon sx={{ marginRight: 0.5 }} />
|
<WorkIcon sx={{ marginRight: 0.5 }} />
|
||||||
<ListItemText primary="Formation IA" />
|
<ListItemText primary="Structure beton..." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
button
|
button
|
||||||
component={Link}
|
component={Link}
|
||||||
to="/services/formation-video"
|
to="/services/electricite"
|
||||||
selected={location.pathname.includes(
|
selected={location.pathname.includes(
|
||||||
"/services/formation-video"
|
"/services/electricite"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<WorkIcon sx={{ marginRight: 0.5 }} />
|
<WorkIcon sx={{ marginRight: 0.5 }} />
|
||||||
<ListItemText primary="Formation Video" />
|
<ListItemText primary="Electricité..." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem
|
<ListItem
|
||||||
button
|
button
|
||||||
|
|||||||
@@ -1,12 +1,346 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Avatar,
|
||||||
|
} from "@mui/material";
|
||||||
|
import BusinessIcon from "@mui/icons-material/Business";
|
||||||
|
import BuildIcon from "@mui/icons-material/Build";
|
||||||
|
import FlashOnIcon from "@mui/icons-material/FlashOn";
|
||||||
|
import SecurityIcon from "@mui/icons-material/Security";
|
||||||
|
import EngineeringIcon from "@mui/icons-material/Engineering";
|
||||||
|
import AssessmentIcon from "@mui/icons-material/Assessment";
|
||||||
|
import ComputerIcon from "@mui/icons-material/Computer";
|
||||||
|
import LibraryBooksIcon from "@mui/icons-material/LibraryBooks";
|
||||||
|
import MoneyIcon from "@mui/icons-material/Money";
|
||||||
|
import PrintIcon from "@mui/icons-material/Print";
|
||||||
|
import StraightenIcon from "@mui/icons-material/Straighten";
|
||||||
|
import SettingsInputAntenna from "@mui/icons-material/SettingsInputAntenna";
|
||||||
|
import BookIcon from "@mui/icons-material/Book";
|
||||||
|
|
||||||
const About = () => {
|
const About = () => {
|
||||||
return (
|
const competences = [
|
||||||
<div>
|
{
|
||||||
<h1>À propos</h1>
|
icon: <BusinessIcon />,
|
||||||
<p>Bienvenue sur la page À propos.</p>
|
title: "Bureau d’études et Maitrise d’œuvre",
|
||||||
</div>
|
link: "/services/maitrise-oeuvre",
|
||||||
);
|
},
|
||||||
|
{
|
||||||
|
icon: <BuildIcon />,
|
||||||
|
title: "La structure béton / métallique / bois",
|
||||||
|
link: "/services/structure-beton-charpente-metallique-bois",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FlashOnIcon />,
|
||||||
|
title: "L’électricité",
|
||||||
|
link: "/services/electricite",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <SecurityIcon />,
|
||||||
|
title: "Sécurité incendie (SSI)",
|
||||||
|
link: "/services/securite-incendie",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <EngineeringIcon />,
|
||||||
|
title: "Expertises TCE",
|
||||||
|
link: "/services/expertises-tce",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <AssessmentIcon />,
|
||||||
|
title: "Diagnostics technique",
|
||||||
|
link: "/services/diagnostics-technique",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const team = [
|
||||||
|
{
|
||||||
|
name: "Ingénieur consultant Génie Civil",
|
||||||
|
role: "Spécialiste Structures en béton armé, charpentes bois et métalliques",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ingénieur Électricité",
|
||||||
|
role: "courant Forts et Faibles, expert bâtiment TCE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Technicienne spécialiste Structures",
|
||||||
|
role: "Béton armé, TCE, DAO-CAO, dessinatrice TCE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Assistante de gestion",
|
||||||
|
role: "Comptable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "consultants",
|
||||||
|
role: "spécialités tel que thermique, SSI",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const infrastructure = [
|
||||||
|
{
|
||||||
|
icon: <ComputerIcon />,
|
||||||
|
title: "CAO-DAO",
|
||||||
|
description: "Autocad & ZWCAD Pro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ComputerIcon />,
|
||||||
|
title: "Modélisation",
|
||||||
|
description: "Sketchup 3D",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <LibraryBooksIcon />,
|
||||||
|
title: "Calculs techniques",
|
||||||
|
description: "Progiciel interne ou programme du CACT (tango, spot)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MoneyIcon />,
|
||||||
|
title: "Logiciel de comptabilité",
|
||||||
|
description: "EBP",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const materiel = [
|
||||||
|
{
|
||||||
|
icon: <ComputerIcon />,
|
||||||
|
title: "Parc informatique",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PrintIcon />,
|
||||||
|
title: "Traceur plans",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <StraightenIcon />,
|
||||||
|
title: "Pied à coulisse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <SettingsInputAntenna />,
|
||||||
|
title: "détecteurs de métaux",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BookIcon />,
|
||||||
|
title: "Bibliothèques techniques (DTU…)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <BookIcon />,
|
||||||
|
title: "Bibliothèques commerciales",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ padding: "40px 20px" }}>
|
||||||
|
{/* En-tête */}
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "3rem", md: "3rem", lg: "3rem" },
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
mb: 4, mt:5
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
La société IN3
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
mb: 6,
|
||||||
|
maxWidth: "800px",
|
||||||
|
mx: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Depuis 2001, Monsieur Stéphane Seillé gère la société qu’il a créée au
|
||||||
|
Mans (Sarthe). L’entreprise a su développer son expertise autour des
|
||||||
|
activités du bureau d’études et de la maîtrise d’œuvre.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Compétences */}
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "2rem", md: "2rem", lg: "2rem" },
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
mb: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nos Compétences
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={4} justifyContent="center">
|
||||||
|
{competences.map((competence, index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||||
|
<a href={competence.link} style={{ textDecoration: "none" }}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: 2,
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#0e467f",
|
||||||
|
color: "#ffffff",
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
boxShadow: 6,
|
||||||
|
},
|
||||||
|
"&:hover svg": { fill: "#ffffff" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="span" sx={{ mb: 1 }}>
|
||||||
|
{competence.icon}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "1.1rem", md: "1.1rem", lg: "1.1rem" },
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{competence.title}
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</a>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Équipe - Maintenant sur 2 colonnes */}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
background: "linear-gradient(to bottom, #0e467f, transparent)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
mt: 6,
|
||||||
|
mb: 3,
|
||||||
|
color: "#ffffff",
|
||||||
|
pt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Notre Équipe
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{ textAlign: "center", mt: 1, mb: 3, color: "#ffffff", pt: 2 }}
|
||||||
|
>
|
||||||
|
L’équipe d’IN3 est composée de collaborateurs experts dans leurs
|
||||||
|
domaines :
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={4} justifyContent="center">
|
||||||
|
{team.map((member, index) => (
|
||||||
|
<Grid item xs={12} sm={6} key={index}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
p: 2,
|
||||||
|
margin: 2,
|
||||||
|
height: "100",
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Avatar sx={{ width: 56, height: 56, margin: "auto" }} />
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "1.1rem", md: "1.1rem", lg: "1.1rem" },
|
||||||
|
fontWeight: "bold",
|
||||||
|
mt: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{member.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{member.role}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Infrastructure */}
|
||||||
|
<Typography
|
||||||
|
variant="h2"
|
||||||
|
sx={{ fontWeight: "bold", textAlign: "center", mt: 6, mb: 3 }}
|
||||||
|
>
|
||||||
|
Notre Infrastructure
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={4} justifyContent="center">
|
||||||
|
{infrastructure.map((infra, index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||||
|
<Card sx={{ textAlign: "center", padding: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="span" sx={{ mb: 1 }}>
|
||||||
|
{infra.icon}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "1.1rem", md: "1.1rem", lg: "1.1rem" },
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{infra.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{infra.description}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
background: "linear-gradient(to top, #6d6e6f, transparent)",
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "2rem", md: "2rem", lg: "2rem" },
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
mt: 6,
|
||||||
|
mb: 3,
|
||||||
|
p: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nous sommes équipés de matériel de dernière génération :
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={4} justifyContent="center">
|
||||||
|
{materiel.map((infra, index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||||
|
<Card sx={{ textAlign: "center", p: 2, margin: 2 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="span" sx={{ mb: 1 }}>
|
||||||
|
{infra.icon}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "1.1rem", md: "1.1rem", lg: "1.1rem" },
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{infra.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{infra.description}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default About;
|
export default About;
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
const AdminDashboard = () => {
|
|
||||||
const [isActive, setIsActive] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const alertStatus = localStorage.getItem("criticalAlertActive");
|
|
||||||
setIsActive(alertStatus === "yes");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const activateAlert = () => {
|
|
||||||
localStorage.setItem("criticalAlertActive", "yes");
|
|
||||||
localStorage.setItem("criticalAlertStartTime", Date.now().toString());
|
|
||||||
setIsActive(true);
|
|
||||||
window.location.reload(); // Recharge la page pour appliquer les changements
|
|
||||||
};
|
|
||||||
|
|
||||||
const deactivateAlert = () => {
|
|
||||||
localStorage.removeItem("criticalAlertActive");
|
|
||||||
localStorage.removeItem("criticalAlertStartTime");
|
|
||||||
setIsActive(false);
|
|
||||||
window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>Panneau Administrateur</h2>
|
|
||||||
<button
|
|
||||||
onClick={activateAlert}
|
|
||||||
style={{ backgroundColor: "red", color: "white", padding: "10px", marginRight: "10px" }}
|
|
||||||
disabled={isActive}
|
|
||||||
>
|
|
||||||
Activer l'Alerte Critique
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={deactivateAlert}
|
|
||||||
style={{ backgroundColor: "green", color: "white", padding: "10px" }}
|
|
||||||
disabled={!isActive}
|
|
||||||
>
|
|
||||||
Désactiver l'Alerte Critique
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdminDashboard;
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
/* 🔥 Design moderne pour la page Backtime */
|
|
||||||
.backtime-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100vh;
|
|
||||||
background: linear-gradient(135deg, #1e1e2f, #3b3b58);
|
|
||||||
color: white;
|
|
||||||
font-family: "Poppins", sans-serif;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-button {
|
|
||||||
background: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-button:hover {
|
|
||||||
background: #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown h3 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
color: #f1c40f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-setting {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-setting label {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-setting input {
|
|
||||||
width: 60px;
|
|
||||||
padding: 5px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
text-align: center;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch-container span {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🎚️ Switch */
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 60px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switch input {
|
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #ccc;
|
|
||||||
transition: 0.4s;
|
|
||||||
border-radius: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider:before {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
height: 22px;
|
|
||||||
width: 22px;
|
|
||||||
left: 4px;
|
|
||||||
bottom: 4px;
|
|
||||||
background-color: white;
|
|
||||||
transition: 0.4s;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked + .slider {
|
|
||||||
background-color: #2ecc71;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:checked + .slider:before {
|
|
||||||
transform: translateX(26px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🔐 Modernisation du formulaire de connexion */
|
|
||||||
.login-container {
|
|
||||||
background: rgba(0, 0, 0, 0.8); /* Fond semi-transparent pour un effet élégant */
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0px 10px 25px rgba(0, 0, 0, 0.3); /* Ombre plus profonde pour effet moderne */
|
|
||||||
backdrop-filter: blur(10px); /* Effet flou pour un look premium */
|
|
||||||
width: 320px;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container:hover {
|
|
||||||
transform: scale(1.02); /* Effet léger d’agrandissement au survol */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container h2 {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
color: #f1c40f; /* Couleur dorée pour mettre en avant le titre */
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container input,
|
|
||||||
.login-button {
|
|
||||||
width: 100%; /* Assure que les deux éléments prennent la même largeur */
|
|
||||||
max-width: 280px; /* Définit une largeur max pour éviter qu'ils deviennent trop grands */
|
|
||||||
display: block; /* Évite les décalages */
|
|
||||||
margin: 0 auto; /* Centre les éléments */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container input {
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 1rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container input::placeholder {
|
|
||||||
color: #ddd;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button {
|
|
||||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
|
||||||
color: white;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-top: 10px; /* Ajoute un petit espace entre le champ et le bouton */
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:hover {
|
|
||||||
background: linear-gradient(135deg, #2980b9, #1c6ea4);
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🎨 Design amélioré du menu déroulant */
|
|
||||||
.duration-selector {
|
|
||||||
margin-top: 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration-selector label {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #f0c040;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🌟 Style du select */
|
|
||||||
.duration-selector select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(135deg, #222831, #393e46);
|
|
||||||
border: 2px solid #f0c040;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🎨 Effet au survol */
|
|
||||||
.duration-selector select:hover {
|
|
||||||
background: linear-gradient(135deg, #393e46, #222831);
|
|
||||||
border-color: #ffcc00;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 🔽 Apparence des options */
|
|
||||||
.duration-selector select option {
|
|
||||||
background: #222831;
|
|
||||||
color: white;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Helmet } from "react-helmet";
|
|
||||||
import "./Backtime.css";
|
|
||||||
|
|
||||||
const PASSWORD_HASH = import.meta.env.VITE_PASSWORD_HASH;
|
|
||||||
|
|
||||||
const Backtime = () => {
|
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
||||||
const [inputPassword, setInputPassword] = useState("");
|
|
||||||
const [timeRemaining, setTimeRemaining] = useState(null);
|
|
||||||
const [selectedDuration, setSelectedDuration] = useState(() => {
|
|
||||||
return parseInt(localStorage.getItem("criticalAlertDuration")) || 15;
|
|
||||||
});
|
|
||||||
const [isActive, setIsActive] = useState(() => {
|
|
||||||
return localStorage.getItem("criticalAlertActive") === "yes";
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const storedAuth = localStorage.getItem("backtimeAuth");
|
|
||||||
if (storedAuth === "true") {
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTimeRemaining();
|
|
||||||
const interval = setInterval(updateTimeRemaining, 1000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isActive) {
|
|
||||||
localStorage.setItem("criticalAlertDuration", selectedDuration);
|
|
||||||
if (!localStorage.getItem("criticalAlertStartTime")) {
|
|
||||||
localStorage.setItem("criticalAlertStartTime", Date.now().toString());
|
|
||||||
}
|
|
||||||
updateTimeRemaining();
|
|
||||||
}
|
|
||||||
}, [selectedDuration]);
|
|
||||||
|
|
||||||
const hashPassword = async (password) => {
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
const data = encoder.encode(password);
|
|
||||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
||||||
return Array.from(new Uint8Array(hashBuffer))
|
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
|
||||||
.join("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
|
||||||
const hashedInput = await hashPassword(inputPassword);
|
|
||||||
if (hashedInput === PASSWORD_HASH) {
|
|
||||||
localStorage.setItem("backtimeAuth", "true");
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
} else {
|
|
||||||
alert("Mot de passe incorrect !");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
localStorage.removeItem("backtimeAuth");
|
|
||||||
setIsAuthenticated(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTimeRemaining = () => {
|
|
||||||
const startTime = localStorage.getItem("criticalAlertStartTime");
|
|
||||||
|
|
||||||
if (startTime) {
|
|
||||||
const startTimestamp = parseInt(startTime, 10);
|
|
||||||
const now = Date.now();
|
|
||||||
const elapsedTime = now - startTimestamp;
|
|
||||||
const totalTime = selectedDuration * 24 * 60 * 60 * 1000;
|
|
||||||
const remainingTime = totalTime - elapsedTime;
|
|
||||||
|
|
||||||
if (remainingTime <= 0) {
|
|
||||||
setTimeRemaining("Le site est complètement transparent !");
|
|
||||||
localStorage.setItem("criticalAlertActive", "no"); // Désactive le module si le temps est écoulé
|
|
||||||
} else {
|
|
||||||
const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
|
|
||||||
const hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
||||||
const minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
|
|
||||||
const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);
|
|
||||||
setTimeRemaining(`${days}j ${hours}h ${minutes}m ${seconds}s`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setTimeRemaining("Aucune alerte activée");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDurationChange = (event) => {
|
|
||||||
const newDuration = parseInt(event.target.value, 10);
|
|
||||||
setSelectedDuration(newDuration);
|
|
||||||
if (isActive) {
|
|
||||||
localStorage.setItem("criticalAlertDuration", newDuration);
|
|
||||||
if (!localStorage.getItem("criticalAlertStartTime")) {
|
|
||||||
localStorage.setItem("criticalAlertStartTime", Date.now().toString());
|
|
||||||
}
|
|
||||||
updateTimeRemaining();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="backtime-container">
|
|
||||||
<Helmet>
|
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
|
||||||
<title>Page introuvable</title>
|
|
||||||
<style>{`body { opacity: 1 !important; }`}</style>
|
|
||||||
</Helmet>
|
|
||||||
|
|
||||||
{isAuthenticated ? (
|
|
||||||
<>
|
|
||||||
<h2>Gestion du module d'alerte critique</h2>
|
|
||||||
<button onClick={handleLogout} className="logout-button">Déconnexion</button>
|
|
||||||
|
|
||||||
<div className="switch-container">
|
|
||||||
<SwitchControl isActive={isActive} setIsActive={setIsActive} selectedDuration={selectedDuration} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="duration-selector">
|
|
||||||
<label>Durée du processus :</label>
|
|
||||||
<select value={selectedDuration} onChange={handleDurationChange}>
|
|
||||||
{Array.from({ length: 15 }, (_, i) => i + 1).map((day) => (
|
|
||||||
<option key={day} value={day}>{day} jours</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="countdown">
|
|
||||||
<h3>Temps restant avant transparence totale :</h3>
|
|
||||||
<p>{timeRemaining}</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="login-container">
|
|
||||||
<h2>Accès Restreint</h2>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="Entrez le mot de passe"
|
|
||||||
value={inputPassword}
|
|
||||||
onChange={(e) => setInputPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button onClick={handleLogin} className="login-button">Valider</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SwitchControl = ({ isActive, setIsActive, selectedDuration }) => {
|
|
||||||
useEffect(() => {
|
|
||||||
const alertStatus = localStorage.getItem("criticalAlertActive");
|
|
||||||
setIsActive(alertStatus === "yes");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleAlert = () => {
|
|
||||||
if (!isActive) {
|
|
||||||
localStorage.setItem("criticalAlertActive", "yes");
|
|
||||||
if (!localStorage.getItem("criticalAlertStartTime")) {
|
|
||||||
localStorage.setItem("criticalAlertStartTime", Date.now().toString());
|
|
||||||
}
|
|
||||||
localStorage.setItem("criticalAlertDuration", selectedDuration);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem("criticalAlertActive", "no");
|
|
||||||
localStorage.removeItem("criticalAlertStartTime");
|
|
||||||
}
|
|
||||||
setIsActive(!isActive);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="switch-container">
|
|
||||||
<label className="switch">
|
|
||||||
<input type="checkbox" checked={isActive} onChange={toggleAlert} />
|
|
||||||
<span className="slider"></span>
|
|
||||||
</label>
|
|
||||||
<span className={`status-indicator ${isActive ? "active" : "inactive"}`}>
|
|
||||||
{isActive ? "Module Actif" : "Module Inactif"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Backtime;
|
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
} from "@mui/material";
|
||||||
|
import api from "../../api";
|
||||||
|
|
||||||
|
const BureauEtudes = () => {
|
||||||
|
const [pageData, setPageData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPageData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get("wp/v2/pages/272?_fields=acf"); // Remplace XXX par l'ID WordPress de la page
|
||||||
|
setPageData(response.data.acf);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Erreur lors de la récupération des données ACF :",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPageData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!pageData) {
|
||||||
|
return <Typography>Chargement...</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{}}>
|
||||||
|
{/* Section Héros avec Image de Fond */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundImage: "url('https://picsum.photos/1920/600.webp')",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
minHeight: "60vh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "white",
|
||||||
|
textAlign: "center",
|
||||||
|
px: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Présentation Générale */}
|
||||||
|
<Box sx={{ mt: 6, textAlign: "center", px: 4 }}>
|
||||||
|
<Typography variant="h1" sx={{
|
||||||
|
fontSize: { xs: "3rem", md: "3rem", lg: "3.5rem" }, // Responsive
|
||||||
|
fontWeight: "bold",
|
||||||
|
mb: 2,
|
||||||
|
}}>
|
||||||
|
Notre Bureau d'Études
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ maxWidth: "900px", mx: "auto" }}>
|
||||||
|
{pageData.introduction}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{/* Domaines d'Intervention */}
|
||||||
|
<Box sx={{ mt: 6, px: 4 }}>
|
||||||
|
<Typography variant="h2" sx={{
|
||||||
|
fontSize: { xs: "2.5rem", md: "2.5rem", lg: "2.5rem" }, // Responsive
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
mb: 3 }}>
|
||||||
|
Nos Domaines d'Intervention
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={4} justifyContent="center">
|
||||||
|
{pageData.liste_des_competences?.map((competence, index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
padding: 3,
|
||||||
|
transition: "transform 0.3s, box-shadow 0.3s",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
boxShadow: "0px 10px 20px rgba(0, 0, 0, 0.2)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h3" sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
mb: 1,
|
||||||
|
fontSize: { xs: "1.8rem", md: "1.8rem", lg: "1.8rem" }, // Responsive
|
||||||
|
}}>
|
||||||
|
{competence.comp_titre}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{
|
||||||
|
textAlign:"center"
|
||||||
|
}}>{competence.comp_description}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Compétences Spécifiques */}
|
||||||
|
<Box sx={{ mt: 6, textAlign: "center", px: 4 }}>
|
||||||
|
<Typography variant="h2" sx={{
|
||||||
|
fontSize: { xs: "2.5rem", md: "2.5rem", lg: "2.5rem" }, // Responsive
|
||||||
|
fontWeight: "bold",
|
||||||
|
mb: 3
|
||||||
|
}}>
|
||||||
|
Nos Compétences
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ maxWidth: "900px", mx: "auto" }}>
|
||||||
|
{pageData.methodologie}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Engagement et Accompagnement */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 6,
|
||||||
|
py: 6,
|
||||||
|
px: 4,
|
||||||
|
textAlign: "center",
|
||||||
|
background: "linear-gradient(to bottom, #0e467f, #ffffff)",
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h2" sx={{
|
||||||
|
fontSize: { xs: "2.5rem", md: "2.5rem", lg: "2.5rem" }, // Responsive
|
||||||
|
fontWeight: "bold",
|
||||||
|
mb: 3
|
||||||
|
}}>
|
||||||
|
Engagement et Accompagnement
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={4} justifyContent="center">
|
||||||
|
{pageData.expertises_specifiques?.map((expertise, index) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
padding: 3,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
transition: "transform 0.3s, box-shadow 0.3s",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
boxShadow: "0px 10px 20px rgba(255, 255, 255, 0.2)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h3" sx={{
|
||||||
|
fontSize: { xs: "1.8rem", md: "1.8rem", lg: "1.8rem" }, // Responsive
|
||||||
|
fontWeight: "bold",
|
||||||
|
mb: 1 }}>
|
||||||
|
{expertise.expertise_nom}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{expertise.expertise_details}</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Call-To-Action */}
|
||||||
|
<Box sx={{ mt: 6, textAlign: "center", mb: 6 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
href="/contact"
|
||||||
|
sx={{
|
||||||
|
padding: "12px 24px",
|
||||||
|
fontSize: "1.2rem",
|
||||||
|
fontWeight: "bold",
|
||||||
|
borderRadius: "8px",
|
||||||
|
transition: "background-color 0.3s, transform 0.3s",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#0a3b6b",
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Nous Contacter
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BureauEtudes;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect, lazy, Suspense } from "react";
|
import React, { useState, useEffect, Suspense } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import SEO from "../SEO";
|
import SEO from "../SEO";
|
||||||
import api from "../../api";
|
import api from "../../api";
|
||||||
import Faq from "../Faq";
|
import Faq from "../Faq";
|
||||||
const FAQ = lazy(() => import('../Faq'));
|
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
const [metaTitle, setMetaTitle] = useState("Contactez-nous");
|
const [metaTitle, setMetaTitle] = useState("Contactez-nous");
|
||||||
@@ -356,10 +356,9 @@ const Contact = () => {
|
|||||||
<Typography
|
<Typography
|
||||||
variant="h2"
|
variant="h2"
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: { xs: "2rem", md: "2rem", lg: "3rem" }, // Responsive
|
fontSize: { xs: "2rem", md: "2rem", lg: "2rem" }, // Responsive
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
mb: 2,
|
mb: 2,
|
||||||
fontSize: { xs: "1.5rem", md: "2rem" },
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Besoin d'une assistance immédiate ?
|
Besoin d'une assistance immédiate ?
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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 navigate = useNavigate();
|
||||||
|
|
||||||
|
// ✅ Vérification de l'authentification : Redirection vers /login si l'utilisateur n'est pas connecté
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getToken()) {
|
||||||
|
navigate("/admin/login");
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("📢 Tentative d'upload de l'image...");
|
||||||
|
const imageId = await uploadImage(file);
|
||||||
|
|
||||||
|
console.log("📢 Création du post avec l'image mise en avant...");
|
||||||
|
await createPost(title, content, imageId);
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { getToken, logout } from "../../auth"; // Importation de la fonction logout
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
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";
|
||||||
|
import HomeIcon from "@mui/icons-material/Home"; // Icône pour la gestion page d'accueil
|
||||||
|
|
||||||
|
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
|
||||||
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
backgroundImage: "url('https://source.unsplash.com/1600x900/?technology,office')",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "90%",
|
||||||
|
maxWidth: "900px",
|
||||||
|
padding: 4,
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: 6,
|
||||||
|
textAlign: "center",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Bouton de déconnexion placé en haut à gauche */}
|
||||||
|
<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",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* En-tête */}
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 4, mt: 4 }}>
|
||||||
|
<DashboardIcon sx={{ fontSize: 40, color: "#0e467f", mr: 1 }} />
|
||||||
|
Tableau de Bord
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* Cartes du Dashboard */}
|
||||||
|
<Grid container spacing={3} justifyContent="center">
|
||||||
|
{/* ✅ Gestion Page d'Accueil - Première carte */}
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Card
|
||||||
|
onClick={() => navigate("/admin/Gestion-Page-Accueil")}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: 2,
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#0e467f",
|
||||||
|
color: "#ffffff",
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
boxShadow: 8,
|
||||||
|
},
|
||||||
|
"&:hover svg": {
|
||||||
|
fill: "#ffffff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<IconButton sx={{ fontSize: 40, color: "#0e467f" }}>
|
||||||
|
<HomeIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||||
|
Gestion Page d'Accueil
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* ✅ Gérer les Articles - Deuxième carte */}
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Card
|
||||||
|
onClick={() => navigate("/admin/gestion-articles")}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: 2,
|
||||||
|
transition: "0.3s",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "#0e467f",
|
||||||
|
color: "#ffffff",
|
||||||
|
transform: "scale(1.05)",
|
||||||
|
boxShadow: 8,
|
||||||
|
},
|
||||||
|
"&:hover svg": {
|
||||||
|
fill: "#ffffff",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<IconButton sx={{ fontSize: 40, color: "#0e467f" }}>
|
||||||
|
<ArticleIcon fontSize="inherit" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||||
|
Gérer les Articles
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
// ✅ 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";
|
||||||
|
import { deletePost } from "../../wordpress";
|
||||||
|
|
||||||
|
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 ?")) {
|
||||||
|
try {
|
||||||
|
await deletePost(postId, imageId); // ✅ Appel de la fonction deletePost
|
||||||
|
setPosts(posts.filter((post) => post.id !== postId)); // ✅ Met à jour la liste après suppression
|
||||||
|
alert("✅ Article supprimé avec succès !");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur suppression article :", error);
|
||||||
|
alert("⚠ Erreur : impossible de supprimer l'article.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Filtrage des articles
|
||||||
|
const filteredPosts = posts.filter((post) =>
|
||||||
|
post.title.rendered.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").replace(/ /g, " ").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, "").replace(/ /g, " ").substring(0, 100)}...
|
||||||
|
</TableCell>
|
||||||
|
{/* ✅ Boutons Modifier et Supprimer */}
|
||||||
|
<TableCell sx={{ textAlign: "center" }}>
|
||||||
|
<Button startIcon={<Edit />}
|
||||||
|
variant="outlined"
|
||||||
|
color="warning"
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
onClick={() => navigate(`/admin/edit-post/${post.id}`)}
|
||||||
|
>
|
||||||
|
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;
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { updateHomePageACF, uploadImage } from "../../wordpress";
|
||||||
|
import { getToken } from "../../auth";
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
const GestionPageAccueil = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [heroTitle, setHeroTitle] = useState("");
|
||||||
|
const [heroText, setHeroText] = useState("");
|
||||||
|
const [heroImage, setHeroImage] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [successMessage, setSuccessMessage] = useState("");
|
||||||
|
|
||||||
|
// ✅ Vérification de l'authentification (Redirection si non connecté)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!getToken()) {
|
||||||
|
navigate("/admin/login");
|
||||||
|
}
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// ✅ Chargement des données ACF depuis WordPress
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPageData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/13?_fields=acf",
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur de chargement des données");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setHeroTitle(data.acf?.hero_title || "");
|
||||||
|
setHeroText(data.acf?.hero_text || "");
|
||||||
|
|
||||||
|
// ✅ Récupération de l'image Hero
|
||||||
|
if (data.acf?.img_hero) {
|
||||||
|
const mediaResponse = await fetch(
|
||||||
|
`https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/media/${data.acf.img_hero}`
|
||||||
|
);
|
||||||
|
const mediaData = await mediaResponse.json();
|
||||||
|
setHeroImage(mediaData.source_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur chargement ACF :", error);
|
||||||
|
setError("Impossible de charger les données.");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPageData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ✅ Mise à jour des champs ACF
|
||||||
|
const handleSave = async () => {
|
||||||
|
try {
|
||||||
|
const newData = {
|
||||||
|
hero_title: heroTitle,
|
||||||
|
hero_text: heroText,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("📢 Données envoyées pour mise à jour ACF :", newData);
|
||||||
|
await updateHomePageACF(13, newData);
|
||||||
|
|
||||||
|
setSuccessMessage("✅ Modifications enregistrées avec succès !");
|
||||||
|
setTimeout(() => setSuccessMessage(""), 3000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur mise à jour :", error);
|
||||||
|
setError("⚠ Impossible de mettre à jour les champs ACF.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Gérer le téléversement d’une nouvelle image Hero
|
||||||
|
const handleImageUpload = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
const imageId = await uploadImage(file);
|
||||||
|
setHeroImage(URL.createObjectURL(file)); // Affichage immédiat
|
||||||
|
await updateHomePageACF(13, { img_hero: imageId });
|
||||||
|
setSuccessMessage("✅ Image mise à jour !");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur lors de l'upload :", error);
|
||||||
|
setError("❌ Échec de l'upload de l'image.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="md" sx={{ mt: 16, mb:5 }}>
|
||||||
|
<Paper elevation={6} sx={{ padding: 4, borderRadius: 3, backgroundColor: "#f8f9fa" }}>
|
||||||
|
{/* ✅ En-tête */}
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", mb: 3, color: "#0e467f" }}>
|
||||||
|
🏠 Gestion de la Page d'Accueil
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* ✅ Affichage du statut de chargement */}
|
||||||
|
{loading ? (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center">
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : error ? (
|
||||||
|
<Typography textAlign="center" color="error">
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* ✅ Message de succès */}
|
||||||
|
{successMessage && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "#d4edda",
|
||||||
|
color: "#155724",
|
||||||
|
padding: "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "16px",
|
||||||
|
textAlign: "center",
|
||||||
|
boxShadow: "0px 2px 10px rgba(0,0,0,0.1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" fontWeight="bold">
|
||||||
|
{successMessage}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ✅ Affichage de l'image actuelle */}
|
||||||
|
{heroImage && (
|
||||||
|
<Box sx={{ textAlign: "center", mb: 2 }}>
|
||||||
|
<img
|
||||||
|
src={heroImage}
|
||||||
|
alt="Hero"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
maxHeight: "250px",
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ✅ Bouton pour changer l'image */}
|
||||||
|
<Button variant="contained" component="label" fullWidth sx={{ mb: 2 }}>
|
||||||
|
📸 Modifier l'Image Hero
|
||||||
|
<input type="file" hidden onChange={handleImageUpload} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* ✅ Formulaire de modification */}
|
||||||
|
<TextField
|
||||||
|
label="Titre Héros"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={heroTitle}
|
||||||
|
onChange={(e) => setHeroTitle(e.target.value)}
|
||||||
|
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Texte Héros"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={heroText}
|
||||||
|
onChange={(e) => setHeroText(e.target.value)}
|
||||||
|
sx={{ mb: 2, backgroundColor: "#fff", borderRadius: "5px" }}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ✅ Bouton d'enregistrement */}
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
mt: 3,
|
||||||
|
fontSize: "1.1rem",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color:"#ffffff",
|
||||||
|
backgroundColor: "#0e467f",
|
||||||
|
"&:hover": { backgroundColor: "#093a6b" },
|
||||||
|
}}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
💾 Enregistrer les modifications
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* ✅ Bouton retour au Dashboard */}
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
fullWidth
|
||||||
|
sx={{ mt: 2, fontSize: "1rem", fontWeight: "bold", borderColor: "#0e467f", color: "#0e467f" }}
|
||||||
|
onClick={() => navigate("/admin/dashboard")}
|
||||||
|
>
|
||||||
|
⬅ Retour au Dashboard
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GestionPageAccueil;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Hero from "../Hero";
|
import Hero from "../Hero";
|
||||||
import SEO from "../SEO";
|
import SEO from "../SEO";
|
||||||
import api from "../../api";
|
import api from "../../api";
|
||||||
@@ -7,6 +7,7 @@ import Expertises from "../Expertises";
|
|||||||
import ConstructSection from "../ConstructSection";
|
import ConstructSection from "../ConstructSection";
|
||||||
import { Box, Grid, Typography, Button, Card, CardContent } from "@mui/material";
|
import { Box, Grid, Typography, Button, Card, CardContent } from "@mui/material";
|
||||||
import Logo from "../../assets/logo-in3-mobil.svg";
|
import Logo from "../../assets/logo-in3-mobil.svg";
|
||||||
|
import Testi from "../Testi";
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const [pageData, setPageData] = useState(null);
|
const [pageData, setPageData] = useState(null);
|
||||||
@@ -20,14 +21,15 @@ const Home = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await api.get("wp/v2/pages/13?_fields=acf,rank_math_title,rank_math_description");
|
// 🔥 Ajout d'un timestamp pour éviter la mise en cache
|
||||||
|
const response = await api.get(`wp/v2/pages/13?_fields=acf,rank_math_title,rank_math_description&_=${new Date().getTime()}`);
|
||||||
const pageContent = response.data;
|
const pageContent = response.data;
|
||||||
setPageData(pageContent);
|
setPageData(pageContent);
|
||||||
|
|
||||||
// Vérification et récupération de l'image Hero depuis l'API media de WordPress
|
// Récupération de l'image Hero
|
||||||
const heroImageId = pageContent.acf?.img_hero;
|
const heroImageId = pageContent.acf?.img_hero;
|
||||||
if (heroImageId) {
|
if (heroImageId) {
|
||||||
const mediaResponse = await api.get(`wp/v2/media/${heroImageId}`);
|
const mediaResponse = await api.get(`wp/v2/media/${heroImageId}?_=${new Date().getTime()}`);
|
||||||
setHeroImage(mediaResponse.data.source_url);
|
setHeroImage(mediaResponse.data.source_url);
|
||||||
} else {
|
} else {
|
||||||
setHeroImage(null);
|
setHeroImage(null);
|
||||||
@@ -41,7 +43,7 @@ const Home = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchPageData();
|
fetchPageData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -61,6 +63,8 @@ const Home = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const { acf, rank_math_title, rank_math_description } = pageData || {};
|
const { acf, rank_math_title, rank_math_description } = pageData || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -106,39 +110,13 @@ const Home = () => {
|
|||||||
expertise3Text={acf?.gdc_expert?.text_expertise_3 || "Text Expertise 3"}
|
expertise3Text={acf?.gdc_expert?.text_expertise_3 || "Text Expertise 3"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Section Témoignages */}
|
{/* Section Témoignages */}
|
||||||
<Box sx={{ py: 6, px: 4 }}>
|
<Typography variant="h2" sx={{ fontWeight: "bold", mb: 2, textAlign: "center", fontSize: { xs: "2rem", md: "2rem", lg: "3rem" } }}>
|
||||||
<Typography variant="h2" sx={{ fontWeight: "bold", mb: 2, textAlign: "center", fontSize: { xs: "2rem", md: "2rem", lg: "3rem" } }}>
|
|
||||||
Ce que disent nos clients
|
Ce que disent nos clients
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={4}>
|
<Testi />
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Card sx={{ boxShadow: 3, padding: 2, backgroundColor: "#f9f9f9" }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="body1" sx={{ fontStyle: "italic" }}>
|
|
||||||
"Un service exceptionnel, une équipe formidable !"
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ mt: 2, textAlign: "right", fontWeight: "bold" }}>
|
|
||||||
- Client 1
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} md={6}>
|
|
||||||
<Card sx={{ boxShadow: 3, padding: 2, backgroundColor: "#f9f9f9" }}>
|
|
||||||
<CardContent>
|
|
||||||
<Typography variant="body1" sx={{ fontStyle: "italic" }}>
|
|
||||||
"Des résultats impressionnants pour notre projet."
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ mt: 2, textAlign: "right", fontWeight: "bold" }}>
|
|
||||||
- Client 2
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Section Action */}
|
{/* Section Action */}
|
||||||
<Box sx={{ py: 6, px: 4, backgroundColor: "#0e467f", color: "#fff" }}>
|
<Box sx={{ py: 6, px: 4, backgroundColor: "#0e467f", color: "#fff" }}>
|
||||||
<Typography variant="h2" align="center" gutterBottom sx={{ fontWeight: "bold", mb: 4 }}>
|
<Typography variant="h2" align="center" gutterBottom sx={{ fontWeight: "bold", mb: 4 }}>
|
||||||
@@ -157,10 +135,43 @@ const Home = () => {
|
|||||||
}}
|
}}
|
||||||
href="/contact"
|
href="/contact"
|
||||||
>
|
>
|
||||||
Contactez-nous dès aujourd'hui
|
Contactez-nous
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Section Témoignages */}
|
||||||
|
<Box sx={{ py: 6, px: 4 }}>
|
||||||
|
<Typography variant="h2" sx={{ fontWeight: "bold", mb: 2, textAlign: "center", fontSize: { xs: "2rem", md: "2rem", lg: "3rem" } }}>
|
||||||
|
Nos partenaires
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={4}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Card sx={{ boxShadow: 3, padding: 2, backgroundColor: "#f9f9f9" }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="body1" sx={{ fontStyle: "italic" }}>
|
||||||
|
Un service exceptionnel, une équipe formidable
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mt: 2, textAlign: "right", fontWeight: "bold" }}>
|
||||||
|
- Client 1
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Card sx={{ boxShadow: 3, padding: 2, backgroundColor: "#f9f9f9" }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="body1" sx={{ fontStyle: "italic" }}>
|
||||||
|
Des résultats impressionnants pour notre projet.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mt: 2, textAlign: "right", fontWeight: "bold" }}>
|
||||||
|
- Client 2
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Container,
|
||||||
|
Avatar,
|
||||||
|
} from "@mui/material";
|
||||||
|
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
alert("Échec de la connexion !");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
setToken(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundImage: "url('https://source.unsplash.com/random/1600x900?technology')",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container maxWidth="xs">
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
padding: 4,
|
||||||
|
boxShadow: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
textAlign: "center",
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Avatar sx={{ margin: "auto", backgroundColor: "#0e467f" }}>
|
||||||
|
<LockOutlinedIcon />
|
||||||
|
</Avatar>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: "bold", mt: 2 }}>
|
||||||
|
{token ? "Bienvenue !" : "Connexion"}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{token ? (
|
||||||
|
<>
|
||||||
|
<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
|
||||||
|
label="Nom d'utilisateur"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Mot de passe d'application"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
variant="outlined"
|
||||||
|
type="password"
|
||||||
|
value={appPassword}
|
||||||
|
onChange={(e) => setAppPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
sx={{ mt: 3, width: "100%" }}
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TestLogin;
|
||||||
@@ -1,64 +1,93 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { 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,slug,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
|
||||||
boxShadow: 3,
|
.replace(/<[^>]*>/g, "") // Supprime les balises HTML
|
||||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
.replace(/ /g, " ") // Remplace les espaces non bris
|
||||||
'&:hover': {
|
); // Nettoyage HTML
|
||||||
transform: 'scale(1.05)',
|
return strippedText.length > maxLength ? `${strippedText.substring(0, maxLength)}...` : strippedText;
|
||||||
boxShadow: 6,
|
};
|
||||||
},
|
|
||||||
}}>
|
return (
|
||||||
{/* Image de l'article */}
|
<Box sx={{ padding: "40px 20px" }}>
|
||||||
{post._embedded?.['wp:featuredmedia']?.[0]?.source_url && (
|
<Typography variant="h2" sx={{ fontWeight: "bold", textAlign: "center", mb: 4, mt:5 }}>
|
||||||
<CardMedia
|
Nos Articles
|
||||||
component="img"
|
</Typography>
|
||||||
height="200"
|
|
||||||
image={post._embedded['wp:featuredmedia'][0].source_url}
|
<Grid container spacing={4} justifyContent="center">
|
||||||
alt={post.title.rendered}
|
{posts.map((post) => (
|
||||||
/>
|
<Grid item xs={12} sm={6} md={4} key={post.id}>
|
||||||
)}
|
<Card
|
||||||
<CardContent>
|
sx={{
|
||||||
<Typography variant="h5" component="div" gutterBottom>
|
cursor: "pointer",
|
||||||
{post.title.rendered}
|
"&:hover": { backgroundColor: "#0e467f", color: "white", transform: "scale(1.05)", boxShadow: 6 },
|
||||||
</Typography>
|
}}
|
||||||
<Typography
|
onClick={() => navigate(`/post/${post.slug}`)}
|
||||||
variant="body2"
|
>
|
||||||
color="text.secondary"
|
{post.image && (
|
||||||
dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
|
<CardMedia component="img" height="200" image={post.image} alt={post.title.rendered} />
|
||||||
/>
|
)}
|
||||||
</CardContent>
|
<CardContent>
|
||||||
</Card>
|
<Typography variant="h5" sx={{ fontWeight: "bold" }}>
|
||||||
</Grid>
|
{post.title.rendered}
|
||||||
))}
|
</Typography>
|
||||||
</Grid>
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
</Box>
|
{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;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { fetchPosts, deletePost } from "../../wordpress";
|
||||||
|
import { Box, Typography, Button, CircularProgress, Paper, Grid } from "@mui/material";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
|
function PostList() {
|
||||||
|
const [posts, setPosts] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadPosts() {
|
||||||
|
try {
|
||||||
|
const data = await fetchPosts();
|
||||||
|
setPosts(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError("⚠️ Impossible de récupérer les articles.");
|
||||||
|
console.error("Erreur lors de la récupération des articles :", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadPosts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDelete = async (postId) => {
|
||||||
|
if (window.confirm("❌ Es-tu sûr de vouloir supprimer cet article ?")) {
|
||||||
|
try {
|
||||||
|
await deletePost(postId);
|
||||||
|
setPosts(posts.filter((post) => post.id !== postId));
|
||||||
|
toast.success("✅ Article supprimé avec succès !");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erreur suppression :", err);
|
||||||
|
toast.error("⚠️ Erreur lors de la suppression.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ maxWidth: "900px", margin: "auto", mt: 5 }}>
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}>
|
||||||
|
📋 Liste des Articles
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<Box sx={{ textAlign: "center", mt: 4 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Typography color="error" textAlign="center" sx={{ mb: 3 }}>
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && posts.length === 0 && (
|
||||||
|
<Typography textAlign="center" sx={{ mt: 3 }}>
|
||||||
|
Aucun article trouvé.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{posts.map((post) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={post.id}>
|
||||||
|
<Paper sx={{ padding: 3, boxShadow: 3, textAlign: "center" }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: "bold", mb: 2 }}>
|
||||||
|
{post.title.rendered}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
startIcon={<DeleteIcon />}
|
||||||
|
onClick={() => handleDelete(post.id)}
|
||||||
|
sx={{ fontWeight: "bold" }}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostList;
|
||||||
@@ -1,50 +1,62 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ServicePageTemplate from '../ServicePageTemplate';
|
import ServicePageTemplate from "../ServicePageTemplate";
|
||||||
|
|
||||||
const ServiceDeux = () => {
|
const ServiceDeux = () => {
|
||||||
const serviceDetails = {
|
const serviceDetails = {
|
||||||
title: 'Formation IA',
|
// Hero
|
||||||
subtitle: 'Dominez l’intelligence artificielle.',
|
title: "Structure béton charpente métallique & bois",
|
||||||
description:
|
subtitle: "Dominez l’intelligence artificielle.",
|
||||||
'Plongez dans le futur ! Notre formation IA vous prépare aux outils modernes d’intelligence artificielle comme Python, TensorFlow et OpenAI API. Apprenez à créer des modèles IA performants.',
|
|
||||||
|
image: "https://picsum.photos/id/239/1920/1080.webp",
|
||||||
|
ctaText: "Découvrir la formation IA",
|
||||||
|
ctaLink: "/contact",
|
||||||
|
|
||||||
|
// section 1
|
||||||
|
interestTitle: "Béton ou Charpente :\n Quel Matériau Choisir pour une Construction Durable ?", // ✅ Modifiable individuellement
|
||||||
|
description: "Le béton est le matériau de construction le plus répandu dans le monde. \nIl est présent dans tous les secteurs de la construction.\n\n Longtemps associé à l’image négative des grands ensembles, gris et vieillissants, le béton a réalisé des progrès spectaculaires ces dernières années, tant au niveau de ses performances techniques que des aspects esthétiques. \n\nIl commence même à entrer dans les foyers comme produit de décoration !\nUne charpente est un assemblage de pièces de bois et/ou de métal, servant à soutenir ou couvrir des constructions et faisant partie de la toiture.",
|
||||||
|
|
||||||
|
// section 2
|
||||||
|
desirTitle: "Ce que nous offrons",// ✅ Modifiable individuellement
|
||||||
|
|
||||||
features: [
|
features: [
|
||||||
{
|
{
|
||||||
title: 'Fondamentaux de l’IA',
|
title: "Fondamentaux de l’IA",
|
||||||
description: 'Concepts, algorithmes et applications.',
|
description: "Concepts, algorithmes et applications.",
|
||||||
modalText: 'Découvrez les bases de l’IA, des algorithmes aux applications concrètes dans divers domaines.',
|
modalText:
|
||||||
modalImage: 'https://picsum.photos/id/200/800/400.webp',
|
"Découvrez les bases de l’IA, des algorithmes aux applications concrètes dans divers domaines.",
|
||||||
|
modalImage: "https://picsum.photos/id/200/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Python pour l’IA',
|
title: "Python pour l’IA",
|
||||||
description: 'Bibliothèques populaires : TensorFlow, PyTorch.',
|
description: "Bibliothèques populaires : TensorFlow, PyTorch.",
|
||||||
modalText: 'Apprenez à utiliser Python et ses bibliothèques pour concevoir des modèles d’IA puissants.',
|
modalText:
|
||||||
modalImage: 'https://picsum.photos/id/201/800/400.webp',
|
"Apprenez à utiliser Python et ses bibliothèques pour concevoir des modèles d’IA puissants.",
|
||||||
|
modalImage: "https://picsum.photos/id/201/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Projets Pratiques',
|
title: "Projets Pratiques",
|
||||||
description: 'Créez vos propres modèles IA.',
|
description: "Créez vos propres modèles IA.",
|
||||||
modalText: 'Mettez vos compétences en pratique en développant des projets IA réels.',
|
modalText:
|
||||||
modalImage: 'https://picsum.photos/id/202/800/400.webp',
|
"Mettez vos compétences en pratique en développant des projets IA réels.",
|
||||||
|
modalImage: "https://picsum.photos/id/202/800/400.webp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
image: 'https://picsum.photos/id/239/1920/1080.webp',
|
|
||||||
ctaText: 'Découvrir la formation IA',
|
|
||||||
ctaLink: '/contact',
|
|
||||||
carouselItems: [
|
carouselItems: [
|
||||||
{
|
{
|
||||||
title: 'Site E-commerce',
|
title: "Site E-commerce",
|
||||||
description: 'Un projet e-commerce performant et moderne.',
|
description: "Un projet e-commerce performant et moderne.",
|
||||||
image: 'https://picsum.photos/id/453/800/400.webp',
|
image: "https://picsum.photos/id/453/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Portfolio Personnel',
|
title: "Portfolio Personnel",
|
||||||
description: 'Montrez vos compétences avec un portfolio sur mesure.',
|
description: "Montrez vos compétences avec un portfolio sur mesure.",
|
||||||
image: 'https://picsum.photos/id/454/800/400.webp',
|
image: "https://picsum.photos/id/454/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Blog Dynamique',
|
title: "Blog Dynamique",
|
||||||
description: 'Créez un blog interactif et optimisé pour le SEO.',
|
description: "Créez un blog interactif et optimisé pour le SEO.",
|
||||||
image: 'https://picsum.photos/id/455/800/400.webp',
|
image: "https://picsum.photos/id/455/800/400.webp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +1,69 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ServicePageTemplate from '../ServicePageTemplate';
|
import ServicePageTemplate from "../ServicePageTemplate";
|
||||||
|
|
||||||
const ServiceTrois = () => {
|
const ServiceTrois = () => {
|
||||||
const serviceDetails = {
|
const serviceDetails = {
|
||||||
title: 'Électricité',
|
// Hero
|
||||||
subtitle: 'IN3 est un bureau d’études technique spécialisé dans l’ingénierie électrique du bâtiment.',
|
title: "Électricité", // ✅ Modifiable individuellement
|
||||||
description:
|
subtitle: "IN3 est un bureau d’études technique spécialisé dans l’ingénierie électrique du bâtiment.",
|
||||||
'Plongez dans le futur ! Notre formation IA vous prépare aux outils modernes d’intelligence artificielle comme Python, TensorFlow et OpenAI API. Apprenez à créer des modèles IA performants.',
|
|
||||||
|
image: "https://picsum.photos/id/1018/1920/1080.webp",
|
||||||
|
ctaText: "En savoir plus",
|
||||||
|
ctaLink: "/contact",
|
||||||
|
|
||||||
|
// Section 1
|
||||||
|
interestTitle: "Comment réussir vos installations électriques \navec une assistance experte en maîtrise d’ouvrage ?",
|
||||||
|
description: "Notre activité est l’assistance aux maîtres d’ouvrage à la réalisation des installations électriques courants forts et courants faibles.\n Définition des besoins / Audit si installations existantes Estimation du coût des ouvrages.\n \n Plans d’implantation du matériel et des canalisations schémas électriques unifilaires.\n Analyse des offres et assistance technique au choix de l’entreprise d’électricité.",
|
||||||
|
|
||||||
|
// Section 2
|
||||||
|
desirTitle: "Réalisation et assistance électrique",
|
||||||
|
|
||||||
features: [
|
features: [
|
||||||
{
|
{
|
||||||
title: 'Chantier électricité industriel',
|
title: "Chantier électricité industriel",
|
||||||
description: 'Réalisation d’installations électriques.',
|
description: "Réalisation d’installations électriques.",
|
||||||
modalText: 'Réalisation d’installations électriques pour des sites industriels, incluant le câblage, la pose d’équipements, et la mise en conformité selon les normes en vigueur..',
|
modalText: "Réalisation d’installations électriques pour des sites industriels, incluant le câblage, la pose d’équipements, et la mise en conformité selon les normes en vigueur.",
|
||||||
modalImage: 'https://picsum.photos/id/200/800/400.webp',
|
modalImage: "https://picsum.photos/id/200/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Python pour l’IA',
|
title: "Dossier de Consultation",
|
||||||
description: 'Bibliothèques populaires : TensorFlow, PyTorch.',
|
description: "Réalisation du Dossier de Consultation des entreprises (DCE)",
|
||||||
modalText: 'Apprenez à utiliser Python et ses bibliothèques pour concevoir des modèles d’IA puissants.',
|
modalText: "Réalisation du Dossier de Consultation des entreprises (DCE) cahier des clauses techniques particulières (CCTP) bordereau de décomposition du prix global et forfaitaire (DPGF, servant de base à tous les devis).",
|
||||||
modalImage: 'https://picsum.photos/id/201/800/400.webp',
|
modalImage: "https://picsum.photos/id/201/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Projets Pratiques',
|
title: "L'électricité & Suivi de chantier",
|
||||||
description: 'Créez vos propres modèles IA.',
|
description: "Assistance à l’entreprise et vérification des documents",
|
||||||
modalText: 'Mettez vos compétences en pratique en développant des projets IA réels.',
|
modalText: "Assistance à l’entreprise et vérification des documents d’exécution. Réception des ouvrages exécutés. Nous pouvons également assister l’entreprise titulaire du marché à la réalisation des documents d’exécution (plans d’implantation, schématique électrique, notes de calculs réglementaires, …)",
|
||||||
modalImage: 'https://picsum.photos/id/202/800/400.webp',
|
modalImage: "https://picsum.photos/id/202/800/400.webp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
image: 'https://picsum.photos/id/239/1920/1080.webp',
|
|
||||||
ctaText: 'En savoir plus !',
|
|
||||||
ctaLink: '/contact',
|
|
||||||
carouselItems: [
|
carouselItems: [
|
||||||
{
|
{
|
||||||
title: 'Site E-commerce',
|
title: "Site E-commerce",
|
||||||
description: 'Un projet e-commerce performant et moderne.',
|
description: "Un projet e-commerce performant et moderne.",
|
||||||
image: 'https://picsum.photos/id/453/800/400.webp',
|
image: "https://picsum.photos/id/453/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Portfolio Personnel',
|
title: "Portfolio Personnel",
|
||||||
description: 'Montrez vos compétences avec un portfolio sur mesure.',
|
description: "Montrez vos compétences avec un portfolio sur mesure.",
|
||||||
image: 'https://picsum.photos/id/454/800/400.webp',
|
image: "https://picsum.photos/id/454/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Blog Dynamique',
|
title: "Blog Dynamique",
|
||||||
description: 'Créez un blog interactif et optimisé pour le SEO.',
|
description: "Créez un blog interactif et optimisé pour le SEO.",
|
||||||
image: 'https://picsum.photos/id/455/800/400.webp',
|
image: "https://picsum.photos/id/455/800/400.webp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// ✅ Métadonnées SEO
|
||||||
|
seo: {
|
||||||
|
metaTitle: "Électricité | Expertise en ingénierie électrique",
|
||||||
|
metaDescription: "Découvrez notre expertise en ingénierie électrique et maîtrisez l'installation et la gestion des systèmes électriques.",
|
||||||
|
keywords: "électricité, ingénierie électrique, installation électrique, suivi de chantier",
|
||||||
|
ogImage: "https://picsum.photos/1200/630",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ServicePageTemplate {...serviceDetails} />;
|
return <ServicePageTemplate {...serviceDetails} />;
|
||||||
|
|||||||
@@ -1,47 +1,73 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ServicePageTemplate from '../ServicePageTemplate';
|
import ServicePageTemplate from "../ServicePageTemplate";
|
||||||
|
|
||||||
const ServiceUn = () => {
|
const ServiceUn = () => {
|
||||||
const serviceDetails = {
|
const serviceDetails = {
|
||||||
title: 'Prestation-maitrise-oeuvre',
|
// Hero
|
||||||
subtitle: 'Apprenez à maîtriser les technologies du web.',
|
title:
|
||||||
|
"Pourquoi faire appel à un \n bureau d’études pour concevoir \n ou rénover votre bâtiment ?", // ✅ Modifiable individuellement
|
||||||
|
subtitle:
|
||||||
|
"Notre bureau d’études in3 au Mans conçoit des bâtiments à construire ou à rénover selon le programme fourni par le maître de l’ouvrage, de diriger l’exécution des marchés de travaux, de proposer le règlement des travaux et leur réception. Nos missions sont les suivantes :", // ✅ Modifiable individuellement
|
||||||
|
|
||||||
|
image: "https://picsum.photos/id/1018/1920/1080.webp",
|
||||||
|
ctaText: "En savoir plus",
|
||||||
|
ctaLink: "/contact",
|
||||||
|
|
||||||
|
// section 1
|
||||||
|
interestTitle:
|
||||||
|
"Comment le maître d’œuvre garantit-il \n la réussite de votre projet de construction ?", // ✅ Modifiable individuellement
|
||||||
description:
|
description:
|
||||||
'Notre formation web vous permet de développer vos compétences en développement frontend, backend et design. Rejoignez notre programme pour apprendre HTML, CSS, JavaScript et bien plus.',
|
"Véritable bras droit du maître d’ouvrage, nous lui proposons une solution technique et esthétique qui permet de réaliser son programme, dans l’enveloppe budgétaire et les délais qui lui sont assignés. Une fois son projet validé par le maître d’ouvrage, le maître d’œuvre est responsable du bon déroulement des travaux et joue un rôle de conseil dans le choix des entreprises qui vont les réaliser. Le choix de l’entrepreneur (ou des entrepreneurs) se fait à partir d’une consultation formalisée où, sur la base d’un cahier des charges (notamment le Cahier des Clauses Techniques Particulières), le titulaire faisant l’offre la plus adaptée, est choisi par le maître d’ouvrage sur proposition du maître d’œuvre compte tenu d’éléments matériels concrets.", // ✅ Modifiable individuellement
|
||||||
|
|
||||||
|
// section 2
|
||||||
|
desirTitle: "Titre Désir", // ✅ Modifiable individuellement
|
||||||
|
|
||||||
features: [
|
features: [
|
||||||
{
|
{
|
||||||
title: 'Frontend Development',
|
title: "diagnostic \n (DIA)",
|
||||||
description: 'HTML, CSS, JavaScript, React et plus encore.',
|
description: "Les études de diagnostic (DIA)...",
|
||||||
modalText: 'Apprenez à construire des interfaces utilisateurs modernes avec les technologies frontend.',
|
modalText:
|
||||||
modalImage: 'https://picsum.photos/id/300/800/400.webp',
|
"Les études de diagnostic (DIA), pour le cas de travaux sur un bâtiment existant)",
|
||||||
|
modalImage: "https://picsum.photos/id/300/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Backend Development',
|
title: "Les Études d’Esquisse \n (ESQ)",
|
||||||
description: 'Node.js, Express, MongoDB et bases de données.',
|
description: "Comprendre leur Importance dans un Projet Architectural",
|
||||||
modalText: 'Développez des applications robustes avec des technologies backend performantes.',
|
modalText:
|
||||||
modalImage: 'https://picsum.photos/id/301/800/400.webp',
|
"L’objectif principal de cette phase est de poser les bases du projet en définissant les grandes lignes de l’implantation, de l’organisation des espaces et de l’esthétique du bâtiment. Cela inclut : L’analyse du site : étude de l’environnement, des accès, de l’orientation et des contraintes liées au terrain. L’évaluation des besoins du maître d’ouvrage : prise en compte des attentes fonctionnelles et esthétiques. La proposition de plusieurs variantes : différentes approches sont étudiées pour identifier la meilleure solution. Une estimation préliminaire des coûts : pour vérifier l’adéquation entre les ambitions du projet et le budget disponible.",
|
||||||
|
modalImage: "https://picsum.photos/id/301/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Responsive Design',
|
title: "Pourquoi réaliser une étude d’avant-projet ?",
|
||||||
description: 'Apprenez à créer des designs modernes et adaptatifs.',
|
description: "Les Études d’Avant-Projet (AVP)...",
|
||||||
modalText: 'Maîtrisez les principes du responsive design pour des expériences utilisateur optimales.',
|
modalText:
|
||||||
modalImage: 'https://picsum.photos/id/302/800/400.webp',
|
"L’AVP a pour objectif de définir les grandes lignes du projet en s’appuyant sur une analyse approfondie des besoins, des contraintes et des attentes des parties prenantes. Elle permet de : Clarifier les objectifs : Identifier précisément les résultats attendus et les enjeux du projet. Évaluer la faisabilité : Étudier la viabilité technique, économique et réglementaire. Définir les solutions possibles : Comparer différentes approches et choisir la meilleure. Établir une première estimation des coûts et délais : Anticiper les ressources nécessaires et éviter les imprévus.",
|
||||||
|
modalImage: "https://picsum.photos/id/302/800/400.webp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
image: 'https://picsum.photos/id/1018/1920/1080.webp',
|
|
||||||
ctaText: 'En savoir plus',
|
|
||||||
ctaLink: '/contact',
|
|
||||||
carouselItems: [
|
carouselItems: [
|
||||||
{
|
{
|
||||||
title: 'Portfolio Modern',
|
title: "Portfolio Modern",
|
||||||
description: 'Montrez vos compétences avec un portfolio unique.',
|
description: "Montrez vos compétences avec un portfolio unique.",
|
||||||
image: 'https://picsum.photos/id/305/800/400.webp',
|
image: "https://picsum.photos/id/305/800/400.webp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Blog SEO',
|
title: "Blog SEO",
|
||||||
description: 'Optimisez vos articles pour le référencement.',
|
description: "Optimisez vos articles pour le référencement.",
|
||||||
image: 'https://picsum.photos/id/306/800/400.webp',
|
image: "https://picsum.photos/id/306/800/400.webp",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// ✅ Métadonnées SEO
|
||||||
|
seo: {
|
||||||
|
metaTitle:
|
||||||
|
"Pourquoi faire appel à un bureau d’études pour votre construction ou rénovation ? | in3",
|
||||||
|
metaDescription:
|
||||||
|
"Optimisez votre projet de construction ou de rénovation avec notre bureau d’études in3. Expertise, maîtrise d’œuvre et solutions adaptées pour garantir la réussite de votre chantier.",
|
||||||
|
keywords:
|
||||||
|
"bureau d'études, construction, rénovation, maîtrise d’œuvre, étude avant-projet, diagnostic DIA, architecture, travaux, bâtiment",
|
||||||
|
ogImage: "https://picsum.photos/id/1018/1920/1080.webp",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <ServicePageTemplate {...serviceDetails} />;
|
return <ServicePageTemplate {...serviceDetails} />;
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { 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 { slug } = useParams(); // ✅ Utilisation correcte du slug
|
||||||
|
const [post, setPost] = useState(null);
|
||||||
|
const [image, setImage] = useState(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchPost = async () => {
|
||||||
|
if (!slug) {
|
||||||
|
console.error("❌ Erreur : aucun slug fourni !");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`📢 Requête API pour le slug : ${slug}`);
|
||||||
|
|
||||||
|
// ✅ Requête API correcte
|
||||||
|
const response = await api.get(`wp/v2/posts?slug=${slug}&_fields=id,title,content,featured_media,date`);
|
||||||
|
|
||||||
|
if (response.data.length > 0) {
|
||||||
|
const article = response.data[0]; // ✅ Récupération du premier article trouvé
|
||||||
|
setPost(article);
|
||||||
|
console.log("✅ Article trouvé :", article);
|
||||||
|
|
||||||
|
// ✅ Récupération de l'image en vedette si elle existe
|
||||||
|
if (article.featured_media) {
|
||||||
|
console.log(`📢 Récupération de l'image ID : ${article.featured_media}`);
|
||||||
|
const mediaResponse = await api.get(`wp/v2/media/${article.featured_media}`);
|
||||||
|
setImage(mediaResponse.data.source_url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("❌ Aucun article trouvé avec ce slug !");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur lors de la récupération de l'article :", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPost();
|
||||||
|
}, [slug]); // ✅ Dépendance correcte
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return <CircularProgress sx={{ display: "block", margin: "auto", mt: 5 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ padding: "40px 20px", display: "flex", flexDirection: "column" }}>
|
||||||
|
{/* ✅ Bouton Retour */}
|
||||||
|
<Button onClick={() => navigate(-1)} variant="contained" sx={{ mt: 5, mb: 3, maxWidth: "200px" }}>
|
||||||
|
⬅ Retour
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* ✅ Titre de l'article */}
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
textAlign: "center",
|
||||||
|
mb: 4,
|
||||||
|
fontSize: { xs: "3rem", md: "3rem", lg: "3rem" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{post.title.rendered}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* ✅ Image + Contenu */}
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "40px 20px", margin: "auto" }}>
|
||||||
|
{image && (
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={image}
|
||||||
|
alt={post.title.rendered}
|
||||||
|
sx={{ width: "100%", 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;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { uploadImage, createPost } from "../wordpress";
|
||||||
|
|
||||||
|
function PostForm() {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [content, setContent] = useState("");
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
// 1️⃣ Vérification des champs
|
||||||
|
if (!title.trim() || !content.trim() || !file) {
|
||||||
|
setError("Tous les champs sont requis !");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2️⃣ Upload de l’image
|
||||||
|
const mediaId = await uploadImage(file);
|
||||||
|
if (!mediaId) {
|
||||||
|
setError("Échec de l'upload de l'image !");
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3️⃣ Création de l’article
|
||||||
|
const success = await createPost(title, content, mediaId);
|
||||||
|
if (success) {
|
||||||
|
alert("Article publié avec succès !");
|
||||||
|
setTitle("");
|
||||||
|
setContent("");
|
||||||
|
setFile(null);
|
||||||
|
} else {
|
||||||
|
setError("Échec de la publication !");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Erreur lors du traitement :", err);
|
||||||
|
setError("Une erreur est survenue, veuillez réessayer.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Créer un nouvel article</h2>
|
||||||
|
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Titre"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="Contenu"
|
||||||
|
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" disabled={isLoading}>
|
||||||
|
{isLoading ? "Publication..." : "Publier"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostForm;
|
||||||
@@ -1,40 +1,37 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { Box, Typography, Button, Grid, Card, CardContent, Modal, Fade, Backdrop, CircularProgress } from "@mui/material";
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
Grid,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Modal,
|
|
||||||
Fade,
|
|
||||||
Backdrop,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
import "swiper/css/pagination";
|
import "swiper/css/pagination";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { Helmet } from "react-helmet-async";
|
||||||
|
|
||||||
const ServicePageTemplate = ({
|
const ServicePageTemplate = ({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
interestTitle,
|
||||||
description,
|
description,
|
||||||
|
desirTitle,
|
||||||
features,
|
features,
|
||||||
image,
|
image,
|
||||||
ctaText,
|
ctaText,
|
||||||
ctaLink,
|
ctaLink,
|
||||||
carouselItems,
|
carouselItems,
|
||||||
|
seo, // ✅ Ajout de l'objet SEO
|
||||||
}) => {
|
}) => {
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const [modalContent, setModalContent] = useState({});
|
const [modalContent, setModalContent] = useState({});
|
||||||
|
const [loadingImage, setLoadingImage] = useState(true);
|
||||||
|
|
||||||
const handleCardClick = (feature) => {
|
const handleCardClick = (feature) => {
|
||||||
setModalContent({
|
setModalContent({
|
||||||
title: feature.title,
|
title: feature.title,
|
||||||
text: feature.modalText || "Texte non disponible.",
|
text: feature.modalText || "Texte non disponible.",
|
||||||
image: feature.modalImage || "https://picsum.photos/id/43/800/400.webp", // Image par défaut
|
image: feature.modalImage || "https://picsum.photos/id/43/800/400.webp",
|
||||||
});
|
});
|
||||||
|
setLoadingImage(true);
|
||||||
setOpenModal(true);
|
setOpenModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,16 +40,26 @@ const ServicePageTemplate = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box sx={{ backgroundColor: "#f5f5f5", color: "#333", fontFamily: "Arial, sans-serif" }}>
|
||||||
sx={{
|
{/* ✅ SEO META TAGS */}
|
||||||
backgroundColor: "#f5f5f5",
|
<Helmet>
|
||||||
color: "#333",
|
<title>{seo?.metaTitle || title}</title>
|
||||||
fontFamily: "Arial, sans-serif",
|
<meta name="description" content={seo?.metaDescription || ""} />
|
||||||
}}
|
<meta name="keywords" content={seo?.keywords || ""} />
|
||||||
>
|
<meta property="og:title" content={seo?.metaTitle || title} />
|
||||||
{/* Section Attention */}
|
<meta property="og:description" content={seo?.metaDescription || ""} />
|
||||||
|
<meta property="og:image" content={seo?.ogImage || image} />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta name="twitter:title" content={seo?.metaTitle || title} />
|
||||||
|
<meta name="twitter:description" content={seo?.metaDescription || ""} />
|
||||||
|
<meta name="twitter:image" content={seo?.ogImage || image} />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
{/* Section Hero */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
mt:5,
|
||||||
backgroundImage: `url(${image})`,
|
backgroundImage: `url(${image})`,
|
||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
backgroundPosition: "center",
|
backgroundPosition: "center",
|
||||||
@@ -61,121 +68,66 @@ const ServicePageTemplate = ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
color: "white",
|
color: "black",
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ maxWidth: "800px" }}>
|
<Box sx={{
|
||||||
<Typography
|
maxWidth: "800px",
|
||||||
variant="h1"
|
backgroundColor:"rgba(255, 255, 255, 0.39)",
|
||||||
sx={{
|
p:5,
|
||||||
fontWeight: "bold",
|
borderRadius:5
|
||||||
mb: 2,
|
}}>
|
||||||
fontSize: { xs: '2rem', md: '3rem', lg: '4rem' }, // Responsive
|
<Typography variant="h1" sx={{ fontWeight: "bold", mb: 2, fontSize: { xs: "2rem", md: "3rem", lg: "3rem",whiteSpace: "pre-line" } }}>
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography variant="body1" sx={{ mb: 4 }}>
|
||||||
variant="body1"
|
|
||||||
sx={{
|
|
||||||
mb: 4
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{subtitle}
|
{subtitle}
|
||||||
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
href={ctaLink}
|
href={ctaLink}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="large"
|
size="large"
|
||||||
sx={{
|
sx={{ textTransform: "none", fontWeight: "bold", backgroundColor: "#0e467f", "&:hover": { backgroundColor: "#00bcd4" } }}
|
||||||
textTransform: "none",
|
|
||||||
fontWeight: "bold",
|
|
||||||
backgroundColor: " #0e467f",
|
|
||||||
"&:hover": { backgroundColor: "#00bcd4" },
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{ctaText}
|
{ctaText}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Section Interest */}
|
{/* Section numero 1 */}
|
||||||
<Box sx={{ padding: "40px 20px", textAlign: "center" }}>
|
<Box sx={{ padding: "40px 20px", textAlign: "center" }}>
|
||||||
<Typography variant="h2"
|
<Typography variant="h2" sx={{ fontWeight: "bold", mb: 2, fontSize: { xs: "2rem", md: "2rem", lg: "2rem" }, whiteSpace: "pre-line" }}>
|
||||||
sx={{
|
{interestTitle}
|
||||||
fontWeight: "bold",
|
|
||||||
mb: 2,
|
|
||||||
fontSize: { xs: '2rem', md: '3rem', lg: '3rem' }, // Responsive
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Pourquoi choisir notre formation ?
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ maxWidth: "1000px", margin: "0 auto", mb: 4, whiteSpace: "pre-line" }}>
|
||||||
<Typography
|
|
||||||
variant="body1"
|
|
||||||
sx={{ maxWidth: "800px", margin: "0 auto", mb: 4 }}
|
|
||||||
>
|
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Section Desire */}
|
{/* Section numero 2 */}
|
||||||
<Box sx={{ backgroundColor: "#ffffff", padding: "40px 20px" }}>
|
<Box sx={{ backgroundColor: "#ffffff", padding: "40px 20px" }}>
|
||||||
<Typography
|
<Typography variant="h2" sx={{ fontWeight: "bold", textAlign: "center", mb: 4, fontSize: { xs: "2rem", md: "3rem", lg: "2rem" } }}>
|
||||||
variant="h2"
|
{desirTitle}
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
textAlign: "center",
|
|
||||||
mb: 4,
|
|
||||||
fontSize: { xs: '2rem', md: '3rem', lg: '3rem' }, // Responsive
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Ce que nous offrons
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid container spacing={4} justifyContent="center">
|
<Grid container spacing={4} justifyContent="center">
|
||||||
{features.map((feature, index) => (
|
{features.map((feature, index) => (
|
||||||
<Grid item xs={12} md={4} key={index}>
|
<Grid item xs={12} md={4} key={index}>
|
||||||
<Card
|
<Card
|
||||||
|
onClick={() => handleCardClick(feature)}
|
||||||
sx={{
|
sx={{
|
||||||
boxShadow: 2,
|
cursor: "pointer",
|
||||||
borderRadius: 2,
|
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
backgroundColor: "#f9f9f9",
|
backgroundColor: "#f9f9f9",
|
||||||
cursor: "pointer",
|
transition: "0.3s",
|
||||||
transition: "transform 0.3s ease, box-shadow 0.3s ease",
|
"&:hover": { transform: "scale(1.05)", boxShadow: 5 },
|
||||||
"&:hover": {
|
|
||||||
transform: {
|
|
||||||
xs: "none", // Pas d'hover sur mobile
|
|
||||||
md: "scale(1.05)", // Hover sur desktop uniquement
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
xs: 2, // Pas d'effet de hover sur mobile
|
|
||||||
md: 5, // Augmentation de l'ombre sur desktop
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"&:active": {
|
|
||||||
transform: "scale(0.98)", // Effet de clic pour le tactile
|
|
||||||
boxShadow: 3,
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
onClick={() => handleCardClick(feature)}
|
|
||||||
>
|
>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography
|
<Typography variant="h3" sx={{ fontWeight: "bold", mb: 2, fontSize: 26,whiteSpace: "pre-line" }}>
|
||||||
variant="h3"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
mb: 2,
|
|
||||||
fontSize: 26,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{feature.title}
|
{feature.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ color: "#555" }}>
|
<Typography variant="body2" sx={{ color: "#555" }}>
|
||||||
@@ -189,71 +141,25 @@ const ServicePageTemplate = ({
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Modal */}
|
||||||
<Modal
|
<Modal open={openModal} onClose={handleCloseModal} closeAfterTransition BackdropComponent={Backdrop} BackdropProps={{ timeout: 500 }}>
|
||||||
open={openModal}
|
|
||||||
onClose={handleCloseModal}
|
|
||||||
closeAfterTransition
|
|
||||||
BackdropComponent={Backdrop}
|
|
||||||
BackdropProps={{
|
|
||||||
timeout: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Fade in={openModal}>
|
<Fade in={openModal}>
|
||||||
<Box
|
<Box sx={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", width: "90%", maxWidth: "600px", p: 4, textAlign: "center", background: "white", borderRadius: 2, boxShadow: 3 }}>
|
||||||
sx={{
|
{loadingImage && <CircularProgress sx={{ mb: 2 }} />}
|
||||||
position: "absolute",
|
<img src={modalContent.image} alt={modalContent.title} style={{ width: "100%", borderRadius: "8px", display: loadingImage ? "none" : "block" }} onLoad={() => setLoadingImage(false)} />
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
width: "90%",
|
|
||||||
maxWidth: "600px",
|
|
||||||
p: 4,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
textAlign: "center",
|
|
||||||
background: "rgb(255, 255, 255)",
|
|
||||||
boxShadow: "0 4px 30px rgba(0, 0, 0, 0.1)",
|
|
||||||
backdropFilter: "blur(10px)",
|
|
||||||
borderRadius: 2,
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.3)",
|
|
||||||
color: "rgb(0, 0, 0)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={modalContent.image}
|
|
||||||
alt={modalContent.title}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
borderRadius: "8px",
|
|
||||||
marginBottom: "20px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography variant="h3" sx={{ fontWeight: "bold", mb: 2 }}>
|
<Typography variant="h3" sx={{ fontWeight: "bold", mb: 2 }}>
|
||||||
{modalContent.title}
|
{modalContent.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" sx={{ mb: 4 }}>
|
<Typography variant="body1" sx={{ mb: 4 }}>
|
||||||
{modalContent.text}
|
{modalContent.text}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button onClick={handleCloseModal} variant="contained" color="secondary">
|
||||||
onClick={handleCloseModal}
|
Retour
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
sx={{
|
|
||||||
textTransform: "none",
|
|
||||||
fontWeight: "bold",
|
|
||||||
backgroundColor: "#f44336",
|
|
||||||
"&:hover": { backgroundColor: "#d32f2f" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Quitter
|
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Fade>
|
</Fade>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
{/* Carousel Section */}
|
||||||
{/* Carousel Section */}
|
{carouselItems && carouselItems.length > 0 && (
|
||||||
{carouselItems && carouselItems.length > 0 && (
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: "40px 20px",
|
padding: "40px 20px",
|
||||||
@@ -319,47 +225,29 @@ const ServicePageTemplate = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Section Action */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
backgroundColor: "#0e467f",
|
|
||||||
color: "white",
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "40px 20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h2"
|
|
||||||
sx={{
|
|
||||||
fontWeight: "bold",
|
|
||||||
mb: 2,
|
|
||||||
fontSize: { xs: '2rem', md: '3rem', lg: '3rem' }, // Responsive
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Prêt à démarrer ?
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography variant="body1" sx={{ mb: 4 }}>
|
|
||||||
Rejoignez-nous dès aujourd’hui et profitez de nos services pour
|
|
||||||
booster votre activité.
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
href={ctaLink}
|
|
||||||
variant="contained"
|
|
||||||
color="secondary"
|
|
||||||
size="large"
|
|
||||||
sx={{
|
|
||||||
textTransform: "none",
|
|
||||||
fontWeight: "bold",
|
|
||||||
backgroundColor: "#00bcd4",
|
|
||||||
"&:hover": { backgroundColor: "#0288d1" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ctaText}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Définition des types des props avec PropTypes
|
||||||
|
ServicePageTemplate.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
subtitle: PropTypes.string.isRequired,
|
||||||
|
interestTitle: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string.isRequired,
|
||||||
|
desirTitle: PropTypes.string.isRequired,
|
||||||
|
features: PropTypes.arrayOf(PropTypes.shape({ title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, modalText: PropTypes.string, modalImage: PropTypes.string })).isRequired,
|
||||||
|
image: PropTypes.string.isRequired,
|
||||||
|
ctaText: PropTypes.string.isRequired,
|
||||||
|
ctaLink: PropTypes.string.isRequired,
|
||||||
|
carouselItems: PropTypes.arrayOf(PropTypes.shape({ title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, image: PropTypes.string.isRequired })),
|
||||||
|
seo: PropTypes.object, // ✅ Ajout du SEO en option
|
||||||
|
};
|
||||||
|
|
||||||
|
// ✅ Valeurs par défaut
|
||||||
|
ServicePageTemplate.defaultProps = {
|
||||||
|
carouselItems: [],
|
||||||
|
seo: {}, // ✅ SEO par défaut vide
|
||||||
|
};
|
||||||
|
|
||||||
export default ServicePageTemplate;
|
export default ServicePageTemplate;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
const SwitchControl = () => {
|
|
||||||
const [isActive, setIsActive] = useState(() => {
|
|
||||||
return localStorage.getItem("criticalAlertActive") === "yes";
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const alertStatus = localStorage.getItem("criticalAlertActive");
|
|
||||||
setIsActive(alertStatus === "yes");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleAlert = () => {
|
|
||||||
if (!isActive) {
|
|
||||||
localStorage.setItem("criticalAlertActive", "yes");
|
|
||||||
localStorage.setItem("criticalAlertStartTime", Date.now().toString());
|
|
||||||
} else {
|
|
||||||
localStorage.setItem("criticalAlertActive", "no");
|
|
||||||
localStorage.removeItem("criticalAlertStartTime");
|
|
||||||
}
|
|
||||||
setIsActive(!isActive);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="switch-container">
|
|
||||||
<label className="switch">
|
|
||||||
<input type="checkbox" checked={isActive} onChange={toggleAlert} />
|
|
||||||
<span className="slider"></span>
|
|
||||||
</label>
|
|
||||||
<span className={`status-indicator ${isActive ? "active" : "inactive"}`}>
|
|
||||||
{isActive ? "Module Actif" : "Module Inactif"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { loginWithAppPassword, getToken, logout } from "../auth";
|
||||||
|
|
||||||
|
function TestLogin() {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [appPassword, setAppPassword] = useState("");
|
||||||
|
const [token, setToken] = useState(getToken());
|
||||||
|
|
||||||
|
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 !");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
setToken(null);
|
||||||
|
alert("Déconnexion réussie !");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Connexion</h2>
|
||||||
|
{token ? (
|
||||||
|
<div>
|
||||||
|
<p>✅ Connecté avec le token : {token}</p>
|
||||||
|
<button onClick={handleLogout}>Déconnexion</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleLogin}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Nom d'utilisateur"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Mot de passe d'application"
|
||||||
|
value={appPassword}
|
||||||
|
onChange={(e) => setAppPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit">Se connecter</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TestLogin;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box, Typography, Grid, Avatar } from "@mui/material";
|
||||||
|
|
||||||
|
const TestimonialSection = () => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "50px 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Image à gauche */}
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src="https://picsum.photos/600/600.webp"
|
||||||
|
alt="Illustration"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: { xs: "15px 15px 0 0", md: "15px 0 0 15px" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Contenu Témoignage */}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
md={6}
|
||||||
|
sx={{
|
||||||
|
background: "linear-gradient(to right, #002F6C, #0E467F)",
|
||||||
|
color: "white",
|
||||||
|
padding: "40px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
fontSize: { xs: "2rem", md: "2rem", lg: "2rem" }, // Responsive
|
||||||
|
fontWeight: "bold",
|
||||||
|
mb: 2,
|
||||||
|
fontStyle: "italic",
|
||||||
|
position: "relative",
|
||||||
|
"&::before": {
|
||||||
|
content: '"“"',
|
||||||
|
fontSize: "50px",
|
||||||
|
position: "absolute",
|
||||||
|
top: "-10px",
|
||||||
|
left: "-5px",
|
||||||
|
opacity: 0.3,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
In3 a transformé notre vision en réalité avec une approche passionnée
|
||||||
|
et professionnelle.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", mt: 3 }}>
|
||||||
|
<Avatar
|
||||||
|
alt="Marie Dupont"
|
||||||
|
src="https://picsum.photos/100.webp"
|
||||||
|
sx={{ width: 56, height: 56, marginRight: 2 }}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="span"
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Marie Dupont
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Directrice Créative chez TechVision
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestimonialSection;
|
||||||
+22
-10
@@ -5,17 +5,24 @@ import { ThemeProvider } from "@mui/material/styles";
|
|||||||
import theme from "./theme";
|
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 CriticalAlert from "./components/CriticalAlert";
|
import NotFound from "./components/Pages/NotFound.jsx";
|
||||||
import AdminDashboard from "./components/Pages/AdminDashboard";
|
import GestionArticles from "./components/Pages/GestionArticles";
|
||||||
import Backtime from "./components/Pages/Backtime";
|
import EditPost from "./components/EditPost";
|
||||||
import NotFound from "./components/Pages/NotFound";
|
import GestionPageAccueil from "./components/Pages/GestionPageAccueil";
|
||||||
|
|
||||||
|
|
||||||
// Lazy loading des pages
|
// Lazy loading des pages
|
||||||
const Home = lazy(() => import('./components/Pages/Home.jsx'));
|
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 CreatePost = lazy(() => import('./components/Pages/CreatePost'));
|
||||||
|
const PostList = lazy(() => import("./components/Pages/PostList"));
|
||||||
|
const Login = lazy(() => import("./components/Pages/Login"));
|
||||||
|
const Dashboard = lazy(() => import("./components/Pages/Dashboard"));
|
||||||
|
|
||||||
// Import des services
|
// Import des services
|
||||||
import ServiceUn from "./components/Pages/ServiceUn.jsx";
|
import ServiceUn from "./components/Pages/ServiceUn.jsx";
|
||||||
@@ -31,19 +38,25 @@ function YourApp() {
|
|||||||
<Suspense fallback={<div>Chargement...</div>}>
|
<Suspense fallback={<div>Chargement...</div>}>
|
||||||
<Router>
|
<Router>
|
||||||
<Header />
|
<Header />
|
||||||
<CriticalAlert />
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/posts" element={<Post />} />
|
<Route path="/posts" element={<Post />} />
|
||||||
|
<Route path="/admin/edit-post/:id" element={<EditPost />} />
|
||||||
|
<Route path="/bureauEtude" element={<BureauEtude />} />
|
||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/contact" element={<Contact />} />
|
<Route path="/contact" element={<Contact />} />
|
||||||
<Route path="/search" element={<Search />} /> {/* Route pour les recherches */}
|
<Route path="/search" element={<Search />} /> {/* Route pour les recherches */}
|
||||||
{/* Routes pour les services */}
|
{/* Routes pour les services */}
|
||||||
<Route path="/services/prestation-maitrise-oeuvre" element={<ServiceUn />} />
|
<Route path="/services/prestation-maitrise-oeuvre" element={<ServiceUn />} />
|
||||||
<Route path="/services/formation-ia" element={<ServiceDeux />} />
|
<Route path="/services/structure-beton-charpente-metallique-bois" element={<ServiceDeux />} />
|
||||||
<Route path="/services/formation-video" element={<ServiceTrois />} />
|
<Route path="/services/electricite" element={<ServiceTrois />} />
|
||||||
<Route path="/backtime" element={<Backtime />} />
|
<Route path="/admin/create-post" element={<CreatePost />} />
|
||||||
<Route path="/admin" element={<AdminDashboard />} />
|
<Route path="/post/:slug" 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="/admin/gestion-page-accueil" element={<GestionPageAccueil />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Footer />
|
<Footer />
|
||||||
@@ -52,7 +65,6 @@ function YourApp() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import { getToken } from "./auth"; // 🔥 Import du token pour l'authentification
|
||||||
|
|
||||||
|
const API_URL = "https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Upload une image et retourne son ID
|
||||||
|
* @param {File} file - Le fichier image à uploader
|
||||||
|
* @returns {Promise<number>} - L'ID de l'image uploadée
|
||||||
|
*/
|
||||||
|
export async function uploadImage(file) {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error("❌ Aucun token trouvé !");
|
||||||
|
throw new Error("Utilisateur non authentifié.");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📢 Token utilisé pour l'upload :", token);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("title", file.name);
|
||||||
|
formData.append("status", "publish");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_URL}/media`, formData, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${token}`, // ✅ Garder `Basic` si ça fonctionnait avant
|
||||||
|
"Content-Type": "multipart/form-data"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ Image uploadée avec succès :", response.data);
|
||||||
|
return response.data.id; // ✅ Retourne l'ID de l'image
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur d'upload :", error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchPosts() {
|
||||||
|
const response = await fetch("https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/posts");
|
||||||
|
if (!response.ok) throw new Error("Erreur lors de la récupération des articles");
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Crée un post WordPress avec une image mise en avant
|
||||||
|
* @param {string} title - Le titre du post
|
||||||
|
* @param {string} content - Le contenu du post
|
||||||
|
* @param {number} imageId - L'ID de l'image à mettre en avant
|
||||||
|
* @returns {Promise<Object>} - Le post créé
|
||||||
|
*/
|
||||||
|
export async function createPost(title, content, imageId) {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error("❌ Aucun token trouvé !");
|
||||||
|
throw new Error("Utilisateur non authentifié.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_URL}/posts`, {
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
status: "publish",
|
||||||
|
featured_media: imageId,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${token}`, // ✅ Garder `Basic` si ça fonctionnait avant
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ Post créé avec succès :", response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur création post :", error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Récupère un article WordPress par son ID
|
||||||
|
* @param {number} postId - L'ID du post à récupérer
|
||||||
|
* @returns {Promise<Object>} - Données du post récupéré
|
||||||
|
*/
|
||||||
|
export async function getPostById(postId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_URL}/posts/${postId}`);
|
||||||
|
console.log("✅ Article récupéré :", response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur récupération article :", error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Met à jour un article WordPress
|
||||||
|
* @param {number} postId - L'ID de l'article à modifier
|
||||||
|
* @param {string} title - Le nouveau titre
|
||||||
|
* @param {string} content - Le nouveau contenu
|
||||||
|
* @returns {Promise<Object>} - L'article mis à jour
|
||||||
|
*/
|
||||||
|
export async function updatePost(postId, title, content) {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error("❌ Aucun token trouvé !");
|
||||||
|
throw new Error("Utilisateur non authentifié.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_URL}/posts/${postId}`, { // ✅ Rester sur `POST`
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
status: "publish",
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${token}`, // ✅ Garder `Basic` si ça fonctionnait avant
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ Article mis à jour avec succès :", response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur mise à jour article :", error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Supprime un article WordPress et son image associée
|
||||||
|
* @param {number} postId - L'ID de l'article à supprimer
|
||||||
|
* @param {number} imageId - L'ID de l'image en vedette à supprimer (facultatif)
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function deletePost(postId, imageId) {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error("❌ Aucun token trouvé !");
|
||||||
|
throw new Error("Utilisateur non authentifié.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`📢 Suppression article ID: ${postId}...`);
|
||||||
|
|
||||||
|
// ✅ Suppression du post
|
||||||
|
await axios.delete(`${API_URL}/posts/${postId}?force=true`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${token}`, // ✅ Garder `Basic` si ça fonctionnait avant
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Article ${postId} supprimé.`);
|
||||||
|
|
||||||
|
// ✅ Suppression de l'image associée si elle existe
|
||||||
|
if (imageId) {
|
||||||
|
console.log(`📢 Suppression image ID: ${imageId}...`);
|
||||||
|
await axios.delete(`${API_URL}/media/${imageId}?force=true`, {
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${token}`, // ✅ Garder `Basic` si ça fonctionnait avant
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`✅ Image ${imageId} supprimée.`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur suppression :", error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔹 Met à jour les champs ACF d'une page WordPress
|
||||||
|
* @param {number} pageId - L'ID de la page à modifier
|
||||||
|
* @param {object} acfData - Les données des champs ACF à mettre à jour
|
||||||
|
*/
|
||||||
|
export async function updateHomePageACF(pageId, newData) {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
console.error("❌ Aucun token trouvé !");
|
||||||
|
throw new Error("Utilisateur non authentifié.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`📢 Tentative de mise à jour des ACF pour la page ${pageId}...`, newData);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`https://preprod.octopusdesign.fr/api-octopus/server/wp-json/wp/v2/pages/${pageId}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ acf: newData }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Erreur API: ${JSON.stringify(responseData)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Mise à jour réussie :", responseData);
|
||||||
|
return responseData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur mise à jour ACF :", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -183,3 +183,110 @@ function ajouter_opengraph_meta_api($data, $post, $context) {
|
|||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
add_filter('rest_prepare_post', 'ajouter_opengraph_meta_api', 10, 3);
|
add_filter('rest_prepare_post', 'ajouter_opengraph_meta_api', 10, 3);
|
||||||
|
|
||||||
|
// ✅ Active les erreurs PHP pour le debug (désactive après les tests)
|
||||||
|
define('WP_DEBUG', true);
|
||||||
|
define('WP_DEBUG_LOG', true);
|
||||||
|
define('WP_DEBUG_DISPLAY', false);
|
||||||
|
@ini_set('log_errors', 1);
|
||||||
|
@ini_set('display_errors', 0);
|
||||||
|
|
||||||
|
// ✅ Charger la librairie JWT
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
|
||||||
|
// ✅ Configurer le secret JWT dans `wp-config.php`
|
||||||
|
define('JWT_AUTH_SECRET_KEY', 'change_this_secret_key'); // Change ce secret !
|
||||||
|
|
||||||
|
// ✅ Fonction pour générer un token JWT
|
||||||
|
function custom_generate_jwt_token($user_id) {
|
||||||
|
$issuedAt = time();
|
||||||
|
$expiration = $issuedAt + (60 * 60 * 24); // Expire en 24h
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'iss' => get_bloginfo('url'),
|
||||||
|
'iat' => $issuedAt,
|
||||||
|
'exp' => $expiration,
|
||||||
|
'user_id' => $user_id
|
||||||
|
];
|
||||||
|
|
||||||
|
return JWT::encode($payload, JWT_AUTH_SECRET_KEY, 'HS256');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Fonction pour vérifier un token JWT
|
||||||
|
function custom_authenticate_with_jwt($token) {
|
||||||
|
try {
|
||||||
|
$decoded = JWT::decode($token, new Key(JWT_AUTH_SECRET_KEY, 'HS256'));
|
||||||
|
return get_user_by('ID', $decoded->user_id);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Route API REST pour la connexion
|
||||||
|
function custom_authenticate_user(WP_REST_Request $request) {
|
||||||
|
$parameters = $request->get_json_params();
|
||||||
|
$username = sanitize_text_field($parameters['username']);
|
||||||
|
$password = sanitize_text_field($parameters['password']);
|
||||||
|
|
||||||
|
$user = wp_authenticate($username, $password);
|
||||||
|
|
||||||
|
if (is_wp_error($user)) {
|
||||||
|
return new WP_Error('authentication_failed', 'Identifiants incorrects.', ['status' => 401]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = custom_generate_jwt_token($user->ID);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'user_id' => $user->ID,
|
||||||
|
'token' => $token,
|
||||||
|
'email' => $user->user_email,
|
||||||
|
'role' => $user->roles
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Enregistrement des routes API
|
||||||
|
add_action('rest_api_init', function () {
|
||||||
|
register_rest_route('custom/v1', '/login', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'custom_authenticate_user',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ Autoriser les uploads et publications via l'API REST
|
||||||
|
function allow_admin_upload_and_post($allcaps, $cap, $args) {
|
||||||
|
if (in_array($cap[0], ['upload_files', 'edit_posts', 'publish_posts'])) {
|
||||||
|
$allcaps[$cap[0]] = true;
|
||||||
|
}
|
||||||
|
return $allcaps;
|
||||||
|
}
|
||||||
|
add_filter('map_meta_cap', 'allow_admin_upload_and_post', 10, 3);
|
||||||
|
|
||||||
|
// ✅ Autoriser CORS (utile pour React)
|
||||||
|
function custom_cors_headers() {
|
||||||
|
header("Access-Control-Allow-Origin: *");
|
||||||
|
header("Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE");
|
||||||
|
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
||||||
|
}
|
||||||
|
add_action('init', 'custom_cors_headers');
|
||||||
|
|
||||||
|
// 🔹 Autoriser la modification des champs ACF via l'API REST
|
||||||
|
add_action('rest_api_init', function () {
|
||||||
|
register_rest_field(
|
||||||
|
'page', // Type de post
|
||||||
|
'acf', // Champ ACF
|
||||||
|
[
|
||||||
|
'get_callback' => function ($object) {
|
||||||
|
return get_fields($object['id']); // ✅ Vérifie que l'ID est correct
|
||||||
|
},
|
||||||
|
'update_callback' => function ($value, $object) {
|
||||||
|
foreach ($value as $field_key => $field_value) {
|
||||||
|
update_field($field_key, $field_value, $object->ID); // ✅ Correction ici
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'schema' => null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user