21 Commits

Author SHA1 Message Date
sebvtl728 10c1f53d91 maj template page and pages 2025-02-03 03:53:44 +01:00
sebvtl728 555a63b61d maj template page 2025-02-03 02:02:18 +01:00
sebvtl728 f25b0fa6e4 gestion accueil MAJ avec gestion photo hero 2025-02-02 22:20:15 +01:00
sebvtl728 6eaa0fcf64 gestion accueil MAJ 2025-02-02 20:34:44 +01:00
sebvtl728 f70d21ae71 gestion accueil 2025-02-02 10:22:18 +01:00
sebvtl728 57f618a0e4 maj texte postdetail 2025-02-01 22:09:53 +01:00
sebvtl728 8bccf65623 maj texte postdetail 2025-02-01 22:07:50 +01:00
sebvtl728 55436deb95 maj du texte des articles 2025-01-31 22:40:52 +01:00
sebvtl728 827a6ac092 maj de la gestion des articles add mofif et suppr 2025-01-31 22:25:16 +01:00
sebvtl728 0d42ce54fc Maj gestion des articles 2025-01-31 20:05:03 +01:00
sebvtl728 841161cb9b add function creatPost and login wp 2025-01-31 08:22:48 +01:00
sebvtl728 81ac3d831e add bureauEtude 2025-01-23 23:27:53 +01:00
sebvtl728 c3ab2b54ca MAJ About 2025-01-22 04:05:01 +01:00
sebvtl728 1e4dd3b475 menu stiky, MAJ page Home add Testi... 2025-01-22 03:57:36 +01:00
sebvtl728 cc641c5f5d update Home add Testi 2025-01-21 23:43:32 +01:00
sebvtl728 e605d3915d update About 2025-01-21 17:48:24 +01:00
sebvtl728 ba6603fc6f reindexation serviceDeux 2025-01-21 11:55:01 +01:00
sebvtl728 9eca9041fb add about page 2025-01-21 11:45:50 +01:00
sebvtl728 dc3c6a566b 404 2025-01-18 17:42:28 +01:00
sebvtl728 2d4341311d Fusion de int-OG dans main 2025-01-16 23:16:36 +01:00
sebvtl728 476b227428 MAJ header 2025-01-15 10:13:04 +01:00
33 changed files with 3207 additions and 742 deletions
+6
View File
@@ -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>
RewriteEngine On
RewriteBase /
+474 -333
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,6 +21,7 @@
"react-helmet-async": "^2.0.5",
"react-router-dom": "^7.1.1",
"react-simple-lightbox": "^1.0.26",
"react-toastify": "^11.0.3",
"simple-react-lightbox": "^3.6.8",
"svgo": "^3.3.2",
"swiper": "^10.3.1"
+13 -6
View File
@@ -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({
baseURL: 'https://preprod.octopusdesign.fr/api-octopus/server/wp-json',
withCredentials: true,
baseURL: API_URL,
headers: {
"Content-Type": "application/json"
}
});
// 🔹 Fonction générique pour récupérer le token
export const getToken = () => {
return sessionStorage.getItem("custom_token");
};
// Exportation de l'instance Axios par défaut
// 🔹 Exporter l'instance Axios
export default api;
+35
View File
@@ -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 dapplication
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");
};
+17
View File
@@ -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;
+136
View File
@@ -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;
-3
View File
@@ -49,7 +49,6 @@ const Expertices = ({
<Grid item xs={12} md={4}>
<Card
sx={{
backgroundPosition: "center",
boxShadow: 3,
padding: 2,
textAlign: "center",
@@ -99,7 +98,6 @@ const Expertices = ({
<Grid item xs={12} md={4}>
<Card
sx={{
backgroundPosition: "center",
boxShadow: 3,
padding: 2,
textAlign: "center",
@@ -149,7 +147,6 @@ const Expertices = ({
<Grid item xs={12} md={4}>
<Card
sx={{
backgroundPosition: "center",
boxShadow: 3,
padding: 2,
textAlign: "center",
+26 -11
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import {
AppBar,
Toolbar,
@@ -84,13 +84,14 @@ const Header = () => {
return (
<AppBar
position="static"
position="fixed"
color="transparent"
elevation={0}
sx={{
background: "linear-gradient(to right, #294A9A, #3158b3)",
color: "white",
transition: "all 0.3s ease",
}}
>
<Toolbar>
@@ -170,7 +171,7 @@ const Header = () => {
</MenuItem>
<MenuItem
component={Link}
to="/services/formation-ia"
to="/services/structure-beton-charpente-metallique-bois"
onClick={handleMenuClose}
>
<WorkIcon sx={{ marginRight: 1 }} />
@@ -178,7 +179,7 @@ const Header = () => {
</MenuItem>
<MenuItem
component={Link}
to="/services/formation-video"
to="/services/electricite"
onClick={handleMenuClose}
>
<WorkIcon sx={{ marginRight: 1}} />
@@ -210,7 +211,20 @@ const Header = () => {
...commonButtonStyles,
}}
>
À propos
À Propos
</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
component={Link}
@@ -336,25 +350,26 @@ const Header = () => {
<WorkIcon sx={{ marginRight: 0.5 }} />
<ListItemText primary="Prestation maitrise oeuvre" />
</ListItem>
<ListItem
button
component={Link}
to="/services/formation-ia"
selected={location.pathname.includes("/services/formation-ia")}
to="/services/structure-beton-charpente-metallique-bois"
selected={location.pathname.includes("/services/structure-beton-charpente-metallique-bois")}
>
<WorkIcon sx={{ marginRight: 0.5 }} />
<ListItemText primary="Formation IA" />
<ListItemText primary="Structure beton..." />
</ListItem>
<ListItem
button
component={Link}
to="/services/formation-video"
to="/services/electricite"
selected={location.pathname.includes(
"/services/formation-video"
"/services/electricite"
)}
>
<WorkIcon sx={{ marginRight: 0.5 }} />
<ListItemText primary="Formation Video" />
<ListItemText primary="Electricité..." />
</ListItem>
<ListItem
button
+339 -5
View File
@@ -1,11 +1,345 @@
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 competences = [
{
icon: <BusinessIcon />,
title: "Bureau d’études et Maitrise d’œuvre",
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 (
<div>
<h1>À propos</h1>
<p>Bienvenue sur la page À propos.</p>
</div>
<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é quil a créée au
Mans (Sarthe). Lentreprise 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 dIN3 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>
);
};
@@ -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;
+3 -4
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, lazy, Suspense } from "react";
import React, { useState, useEffect, Suspense } from "react";
import {
Box,
TextField,
@@ -19,7 +19,7 @@ import {
import SEO from "../SEO";
import api from "../../api";
import Faq from "../Faq";
const FAQ = lazy(() => import('../Faq'));
const Contact = () => {
const [metaTitle, setMetaTitle] = useState("Contactez-nous");
@@ -356,10 +356,9 @@ const Contact = () => {
<Typography
variant="h2"
sx={{
fontSize: { xs: "2rem", md: "2rem", lg: "3rem" }, // Responsive
fontSize: { xs: "2rem", md: "2rem", lg: "2rem" }, // Responsive
fontWeight: "bold",
mb: 2,
fontSize: { xs: "1.5rem", md: "2rem" },
}}
>
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;
+150
View File
@@ -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(/&nbsp;/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(/&nbsp;/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 dune 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;
+45 -34
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import Hero from "../Hero";
import SEO from "../SEO";
import api from "../../api";
@@ -7,6 +7,7 @@ import Expertises from "../Expertises";
import ConstructSection from "../ConstructSection";
import { Box, Grid, Typography, Button, Card, CardContent } from "@mui/material";
import Logo from "../../assets/logo-in3-mobil.svg";
import Testi from "../Testi";
const Home = () => {
const [pageData, setPageData] = useState(null);
@@ -20,14 +21,15 @@ const Home = () => {
setIsLoading(true);
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;
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;
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);
} else {
setHeroImage(null);
@@ -61,6 +63,8 @@ const Home = () => {
);
}
const { acf, rank_math_title, rank_math_description } = pageData || {};
return (
@@ -106,39 +110,13 @@ const Home = () => {
expertise3Text={acf?.gdc_expert?.text_expertise_3 || "Text Expertise 3"}
/>
{/* 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" } }}>
Ce que disent nos clients
</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>
<Testi />
{/* Section Action */}
<Box sx={{ py: 6, px: 4, backgroundColor: "#0e467f", color: "#fff" }}>
<Typography variant="h2" align="center" gutterBottom sx={{ fontWeight: "bold", mb: 4 }}>
@@ -157,10 +135,43 @@ const Home = () => {
}}
href="/contact"
>
Contactez-nous dès aujourd'hui
Contactez-nous
</Button>
</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>
);
};
+125
View File
@@ -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;
@@ -0,0 +1,35 @@
.notfound-container {
text-align: center;
padding: 50px;
}
.notfound-container h1 {
font-size: 100px;
color: red;
margin-bottom: 10px;
}
.notfound-container h2 {
font-size: 30px;
margin-bottom: 10px;
}
.notfound-container p {
font-size: 18px;
margin-bottom: 20px;
}
.back-home {
display: inline-block;
padding: 10px 20px;
background-color: #007BFF;
color: white;
text-decoration: none;
border-radius: 5px;
font-size: 18px;
transition: background 0.3s ease;
}
.back-home:hover {
background-color: #0056b3;
}
@@ -0,0 +1,16 @@
import React from "react";
import { Link } from "react-router-dom";
import "./NotFound.css"; // Style dédié
const NotFound = () => {
return (
<div className="notfound-container">
<h1>404</h1>
<h2>Oups ! Page introuvable</h2>
<p>Il semble que la page que vous recherchez n'existe pas.</p>
<Link to="/" className="back-home">Retour à l'accueil</Link>
</div>
);
};
export default NotFound;
+69 -40
View File
@@ -1,57 +1,86 @@
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardContent, CardMedia, Typography, Box } from '@mui/material';
import api from '../../api'; // Adaptez le chemin vers l'instance Axios
import { useState, useEffect } from "react";
import { Box, Typography, Grid, Card, CardContent, CardMedia, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import api from "../../api";
const Post = () => {
const Posts = () => {
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
useEffect(() => {
api.get('wp/v2/posts?_embed')
.then((response) => {
setPosts(response.data);
const fetchPosts = async () => {
try {
const response = await api.get("wp/v2/posts?_fields=id,slug,title,excerpt,content,featured_media");
const postsData = response.data;
// Récupérer les images en vedette
const postsWithImages = await Promise.all(
postsData.map(async (post) => {
if (post.featured_media) {
try {
const mediaResponse = await api.get(`wp/v2/media/${post.featured_media}`);
return { ...post, image: mediaResponse.data.source_url };
} catch (error) {
console.error(`Erreur lors de la récupération de l'image pour l'article ${post.id}:`, error);
return { ...post, image: null };
}
}
return { ...post, image: null };
})
.catch((error) => {
console.error('Erreur lors de la récupération des articles :', error);
});
);
setPosts(postsWithImages);
} catch (error) {
console.error("Erreur lors de la récupération des articles :", error);
}
};
fetchPosts();
}, []);
return (
// Fonction pour nettoyer le HTML et récupérer un texte brut
const stripHtmlTags = (html) => {
return html.replace(/(<([^>]+)>)/gi, ""); // Supprime toutes les balises HTML
};
<Box sx={{ padding: 4 }}>
<Typography variant="h3" sx={{ marginBottom: 4, textAlign: 'center' }}>
Nos réalisations
// Fonction pour tronquer un texte à 100 caractères max
const truncateText = (text, maxLength) => {
const strippedText = stripHtmlTags(text
.replace(/<[^>]*>/g, "") // Supprime les balises HTML
.replace(/&nbsp;/g, " ") // Remplace les espaces non bris
); // Nettoyage HTML
return strippedText.length > maxLength ? `${strippedText.substring(0, maxLength)}...` : strippedText;
};
return (
<Box sx={{ padding: "40px 20px" }}>
<Typography variant="h2" sx={{ fontWeight: "bold", textAlign: "center", mb: 4, mt:5 }}>
Nos Articles
</Typography>
<Grid container spacing={4}>
<Grid container spacing={4} justifyContent="center">
{posts.map((post) => (
<Grid item xs={12} sm={6} md={4} key={post.id}>
<Card sx={{ maxWidth: 345,
boxShadow: 3,
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
'&:hover': {
transform: 'scale(1.05)',
boxShadow: 6,
},
}}>
{/* Image de l'article */}
{post._embedded?.['wp:featuredmedia']?.[0]?.source_url && (
<CardMedia
component="img"
height="200"
image={post._embedded['wp:featuredmedia'][0].source_url}
alt={post.title.rendered}
/>
<Card
sx={{
cursor: "pointer",
"&:hover": { backgroundColor: "#0e467f", color: "white", transform: "scale(1.05)", boxShadow: 6 },
}}
onClick={() => navigate(`/post/${post.slug}`)}
>
{post.image && (
<CardMedia component="img" height="200" image={post.image} alt={post.title.rendered} />
)}
<CardContent>
<Typography variant="h5" component="div" gutterBottom>
<Typography variant="h5" sx={{ fontWeight: "bold" }}>
{post.title.rendered}
</Typography>
<Typography
variant="body2"
color="text.secondary"
dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
/>
<Typography variant="body2" sx={{ mt: 1 }}>
{truncateText(post.excerpt.rendered || post.content.rendered, 100)}
</Typography>
<Button variant="outlined" sx={{ mt: 2, color: "white", borderColor: "white" }}>
Lire plus
</Button>
</CardContent>
</Card>
</Grid>
@@ -61,4 +90,4 @@ const Post = () => {
);
};
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;
+42 -30
View File
@@ -1,50 +1,62 @@
import React from 'react';
import ServicePageTemplate from '../ServicePageTemplate';
import React from "react";
import ServicePageTemplate from "../ServicePageTemplate";
const ServiceDeux = () => {
const serviceDetails = {
title: 'Formation IA',
subtitle: 'Dominez lintelligence artificielle.',
description:
'Plongez dans le futur! Notre formation IA vous prépare aux outils modernes dintelligence artificielle comme Python, TensorFlow et OpenAI API. Apprenez à créer des modèles IA performants.',
// Hero
title: "Structure béton charpente métallique & bois",
subtitle: "Dominez lintelligence artificielle.",
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é à limage 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: [
{
title: 'Fondamentaux de lIA',
description: 'Concepts, algorithmes et applications.',
modalText: 'Découvrez les bases de lIA, des algorithmes aux applications concrètes dans divers domaines.',
modalImage: 'https://picsum.photos/id/200/800/400.webp',
title: "Fondamentaux de lIA",
description: "Concepts, algorithmes et applications.",
modalText:
"Découvrez les bases de lIA, des algorithmes aux applications concrètes dans divers domaines.",
modalImage: "https://picsum.photos/id/200/800/400.webp",
},
{
title: 'Python pour lIA',
description: 'Bibliothèques populaires: TensorFlow, PyTorch.',
modalText: 'Apprenez à utiliser Python et ses bibliothèques pour concevoir des modèles dIA puissants.',
modalImage: 'https://picsum.photos/id/201/800/400.webp',
title: "Python pour lIA",
description: "Bibliothèques populaires: TensorFlow, PyTorch.",
modalText:
"Apprenez à utiliser Python et ses bibliothèques pour concevoir des modèles dIA puissants.",
modalImage: "https://picsum.photos/id/201/800/400.webp",
},
{
title: 'Projets Pratiques',
description: 'Créez vos propres modèles IA.',
modalText: 'Mettez vos compétences en pratique en développant des projets IA réels.',
modalImage: 'https://picsum.photos/id/202/800/400.webp',
title: "Projets Pratiques",
description: "Créez vos propres modèles IA.",
modalText:
"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: [
{
title: 'Site E-commerce',
description: 'Un projet e-commerce performant et moderne.',
image: 'https://picsum.photos/id/453/800/400.webp',
title: "Site E-commerce",
description: "Un projet e-commerce performant et moderne.",
image: "https://picsum.photos/id/453/800/400.webp",
},
{
title: 'Portfolio Personnel',
description: 'Montrez vos compétences avec un portfolio sur mesure.',
image: 'https://picsum.photos/id/454/800/400.webp',
title: "Portfolio Personnel",
description: "Montrez vos compétences avec un portfolio sur mesure.",
image: "https://picsum.photos/id/454/800/400.webp",
},
{
title: 'Blog Dynamique',
description: 'Créez un blog interactif et optimisé pour le SEO.',
image: 'https://picsum.photos/id/455/800/400.webp',
title: "Blog Dynamique",
description: "Créez un blog interactif et optimisé pour le SEO.",
image: "https://picsum.photos/id/455/800/400.webp",
},
],
};
+47 -30
View File
@@ -1,52 +1,69 @@
import React from 'react';
import ServicePageTemplate from '../ServicePageTemplate';
import React from "react";
import ServicePageTemplate from "../ServicePageTemplate";
const ServiceTrois = () => {
const serviceDetails = {
title: 'Électricité',
subtitle: 'IN3 est un bureau d’études technique spécialisé dans lingénierie électrique du bâtiment.',
description:
'Plongez dans le futur! Notre formation IA vous prépare aux outils modernes dintelligence artificielle comme Python, TensorFlow et OpenAI API. Apprenez à créer des modèles IA performants.',
// Hero
title: "Électricité", // Modifiable individuellement
subtitle: "IN3 est un bureau d’études technique spécialisé dans lingénierie électrique du bâtiment.",
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 douvrage ?",
description: "Notre activité est lassistance aux maîtres douvrage à 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 dimplantation du matériel et des canalisations schémas électriques unifilaires.\n Analyse des offres et assistance technique au choix de lentreprise d’électricité.",
// Section 2
desirTitle: "Réalisation et assistance électrique",
features: [
{
title: 'Chantier électricité industriel',
description: 'Réalisation dinstallations électriques.',
modalText: 'Réalisation dinstallations é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',
title: "Chantier électricité industriel",
description: "Réalisation dinstallations électriques.",
modalText: "Réalisation dinstallations é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",
},
{
title: 'Python pour lIA',
description: 'Bibliothèques populaires: TensorFlow, PyTorch.',
modalText: 'Apprenez à utiliser Python et ses bibliothèques pour concevoir des modèles dIA puissants.',
modalImage: 'https://picsum.photos/id/201/800/400.webp',
title: "Dossier de Consultation",
description: "Réalisation du Dossier de Consultation des entreprises (DCE)",
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",
},
{
title: 'Projets Pratiques',
description: 'Créez vos propres modèles IA.',
modalText: 'Mettez vos compétences en pratique en développant des projets IA réels.',
modalImage: 'https://picsum.photos/id/202/800/400.webp',
title: "L'électricité & Suivi de chantier",
description: "Assistance à lentreprise et vérification des documents",
modalText: "Assistance à lentreprise et vérification des documents dexécution. Réception des ouvrages exécutés. Nous pouvons également assister lentreprise titulaire du marché à la réalisation des documents dexécution (plans dimplantation, schématique électrique, notes de calculs réglementaires, …)",
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: [
{
title: 'Site E-commerce',
description: 'Un projet e-commerce performant et moderne.',
image: 'https://picsum.photos/id/453/800/400.webp',
title: "Site E-commerce",
description: "Un projet e-commerce performant et moderne.",
image: "https://picsum.photos/id/453/800/400.webp",
},
{
title: 'Portfolio Personnel',
description: 'Montrez vos compétences avec un portfolio sur mesure.',
image: 'https://picsum.photos/id/454/800/400.webp',
title: "Portfolio Personnel",
description: "Montrez vos compétences avec un portfolio sur mesure.",
image: "https://picsum.photos/id/454/800/400.webp",
},
{
title: 'Blog Dynamique',
description: 'Créez un blog interactif et optimisé pour le SEO.',
image: 'https://picsum.photos/id/455/800/400.webp',
title: "Blog Dynamique",
description: "Créez un blog interactif et optimisé pour le SEO.",
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} />;
+52 -26
View File
@@ -1,47 +1,73 @@
import React from 'react';
import ServicePageTemplate from '../ServicePageTemplate';
import React from "react";
import ServicePageTemplate from "../ServicePageTemplate";
const ServiceUn = () => {
const serviceDetails = {
title: 'Prestation-maitrise-oeuvre',
subtitle: 'Apprenez à maîtriser les technologies du web.',
// Hero
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 louvrage, de diriger lexé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:
'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 douvrage, nous lui proposons une solution technique et esthétique qui permet de réaliser son programme, dans lenveloppe budgétaire et les délais qui lui sont assignés. Une fois son projet validé par le maître douvrage, 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 lentrepreneur (ou des entrepreneurs) se fait à partir dune consultation formalisée où, sur la base dun cahier des charges (notamment le Cahier des Clauses Techniques Particulières), le titulaire faisant loffre la plus adaptée, est choisi par le maître douvrage 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: [
{
title: 'Frontend Development',
description: 'HTML, CSS, JavaScript, React et plus encore.',
modalText: 'Apprenez à construire des interfaces utilisateurs modernes avec les technologies frontend.',
modalImage: 'https://picsum.photos/id/300/800/400.webp',
title: "diagnostic \n (DIA)",
description: "Les études de diagnostic (DIA)...",
modalText:
"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',
description: 'Node.js, Express, MongoDB et bases de données.',
modalText: 'Développez des applications robustes avec des technologies backend performantes.',
modalImage: 'https://picsum.photos/id/301/800/400.webp',
title: "Les Études dEsquisse \n (ESQ)",
description: "Comprendre leur Importance dans un Projet Architectural",
modalText:
"Lobjectif principal de cette phase est de poser les bases du projet en définissant les grandes lignes de limplantation, de lorganisation des espaces et de lesthétique du bâtiment. Cela inclut : Lanalyse du site : étude de lenvironnement, des accès, de lorientation et des contraintes liées au terrain. L’évaluation des besoins du maître douvrage : 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 ladéquation entre les ambitions du projet et le budget disponible.",
modalImage: "https://picsum.photos/id/301/800/400.webp",
},
{
title: 'Responsive Design',
description: 'Apprenez à créer des designs modernes et adaptatifs.',
modalText: 'Maîtrisez les principes du responsive design pour des expériences utilisateur optimales.',
modalImage: 'https://picsum.photos/id/302/800/400.webp',
title: "Pourquoi réaliser une étude davant-projet ?",
description: "Les Études dAvant-Projet (AVP)...",
modalText:
"LAVP a pour objectif de définir les grandes lignes du projet en sappuyant 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: [
{
title: 'Portfolio Modern',
description: 'Montrez vos compétences avec un portfolio unique.',
image: 'https://picsum.photos/id/305/800/400.webp',
title: "Portfolio Modern",
description: "Montrez vos compétences avec un portfolio unique.",
image: "https://picsum.photos/id/305/800/400.webp",
},
{
title: 'Blog SEO',
description: 'Optimisez vos articles pour le référencement.',
image: 'https://picsum.photos/id/306/800/400.webp',
title: "Blog SEO",
description: "Optimisez vos articles pour le référencement.",
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} />;
+87
View File
@@ -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;
+83
View File
@@ -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 limage
const mediaId = await uploadImage(file);
if (!mediaId) {
setError("Échec de l'upload de l'image !");
setIsLoading(false);
return;
}
// 3 Création de larticle
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;
+77 -189
View File
@@ -1,40 +1,37 @@
import React, { useState } from "react";
import {
Box,
Typography,
Button,
Grid,
Card,
CardContent,
Modal,
Fade,
Backdrop,
} from "@mui/material";
import { useState } from "react";
import { Box, Typography, Button, Grid, Card, CardContent, Modal, Fade, Backdrop, CircularProgress } from "@mui/material";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination, Autoplay } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import PropTypes from "prop-types";
import { Helmet } from "react-helmet-async";
const ServicePageTemplate = ({
title,
subtitle,
interestTitle,
description,
desirTitle,
features,
image,
ctaText,
ctaLink,
carouselItems,
seo, // Ajout de l'objet SEO
}) => {
const [openModal, setOpenModal] = useState(false);
const [modalContent, setModalContent] = useState({});
const [loadingImage, setLoadingImage] = useState(true);
const handleCardClick = (feature) => {
setModalContent({
title: feature.title,
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);
};
@@ -43,16 +40,26 @@ const ServicePageTemplate = ({
};
return (
<Box sx={{ backgroundColor: "#f5f5f5", color: "#333", fontFamily: "Arial, sans-serif" }}>
{/* ✅ SEO META TAGS */}
<Helmet>
<title>{seo?.metaTitle || title}</title>
<meta name="description" content={seo?.metaDescription || ""} />
<meta name="keywords" content={seo?.keywords || ""} />
<meta property="og:title" content={seo?.metaTitle || title} />
<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
sx={{
backgroundColor: "#f5f5f5",
color: "#333",
fontFamily: "Arial, sans-serif",
}}
>
{/* Section Attention */}
<Box
sx={{
mt:5,
backgroundImage: `url(${image})`,
backgroundSize: "cover",
backgroundPosition: "center",
@@ -61,121 +68,66 @@ const ServicePageTemplate = ({
alignItems: "center",
justifyContent: "center",
textAlign: "center",
color: "white",
color: "black",
padding: "20px",
}}
>
<Box sx={{ maxWidth: "800px" }}>
<Typography
variant="h1"
sx={{
fontWeight: "bold",
mb: 2,
fontSize: { xs: '2rem', md: '3rem', lg: '4rem' }, // Responsive
}}
>
<Box sx={{
maxWidth: "800px",
backgroundColor:"rgba(255, 255, 255, 0.39)",
p:5,
borderRadius:5
}}>
<Typography variant="h1" sx={{ fontWeight: "bold", mb: 2, fontSize: { xs: "2rem", md: "3rem", lg: "3rem",whiteSpace: "pre-line" } }}>
{title}
</Typography>
<Typography
variant="body1"
sx={{
mb: 4
}}
>
<Typography variant="body1" sx={{ mb: 4 }}>
{subtitle}
</Typography>
<Button
href={ctaLink}
variant="contained"
color="secondary"
size="large"
sx={{
textTransform: "none",
fontWeight: "bold",
backgroundColor: " #0e467f",
"&:hover": { backgroundColor: "#00bcd4" },
}}
sx={{ textTransform: "none", fontWeight: "bold", backgroundColor: "#0e467f", "&:hover": { backgroundColor: "#00bcd4" } }}
>
{ctaText}
</Button>
</Box>
</Box>
{/* Section Interest */}
{/* Section numero 1 */}
<Box sx={{ padding: "40px 20px", textAlign: "center" }}>
<Typography variant="h2"
sx={{
fontWeight: "bold",
mb: 2,
fontSize: { xs: '2rem', md: '3rem', lg: '3rem' }, // Responsive
}}
>
Pourquoi choisir notre formation ?
<Typography variant="h2" sx={{ fontWeight: "bold", mb: 2, fontSize: { xs: "2rem", md: "2rem", lg: "2rem" }, whiteSpace: "pre-line" }}>
{interestTitle}
</Typography>
<Typography
variant="body1"
sx={{ maxWidth: "800px", margin: "0 auto", mb: 4 }}
>
<Typography variant="body1" sx={{ maxWidth: "1000px", margin: "0 auto", mb: 4, whiteSpace: "pre-line" }}>
{description}
</Typography>
</Box>
{/* Section Desire */}
{/* Section numero 2 */}
<Box sx={{ backgroundColor: "#ffffff", padding: "40px 20px" }}>
<Typography
variant="h2"
sx={{
fontWeight: "bold",
textAlign: "center",
mb: 4,
fontSize: { xs: '2rem', md: '3rem', lg: '3rem' }, // Responsive
}}
>
Ce que nous offrons
<Typography variant="h2" sx={{ fontWeight: "bold", textAlign: "center", mb: 4, fontSize: { xs: "2rem", md: "3rem", lg: "2rem" } }}>
{desirTitle}
</Typography>
<Grid container spacing={4} justifyContent="center">
{features.map((feature, index) => (
<Grid item xs={12} md={4} key={index}>
<Card
onClick={() => handleCardClick(feature)}
sx={{
boxShadow: 2,
borderRadius: 2,
cursor: "pointer",
textAlign: "center",
padding: "20px",
backgroundColor: "#f9f9f9",
cursor: "pointer",
transition: "transform 0.3s ease, box-shadow 0.3s ease",
"&: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,
},
transition: "0.3s",
"&:hover": { transform: "scale(1.05)", boxShadow: 5 },
}}
onClick={() => handleCardClick(feature)}
>
<CardContent>
<Typography
variant="h3"
sx={{
fontWeight: "bold",
mb: 2,
fontSize: 26,
}}
>
<Typography variant="h3" sx={{ fontWeight: "bold", mb: 2, fontSize: 26,whiteSpace: "pre-line" }}>
{feature.title}
</Typography>
<Typography variant="body2" sx={{ color: "#555" }}>
@@ -189,69 +141,23 @@ const ServicePageTemplate = ({
</Box>
{/* Modal */}
<Modal
open={openModal}
onClose={handleCloseModal}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Modal open={openModal} onClose={handleCloseModal} closeAfterTransition BackdropComponent={Backdrop} BackdropProps={{ timeout: 500 }}>
<Fade in={openModal}>
<Box
sx={{
position: "absolute",
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",
}}
/>
<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 }}>
{loadingImage && <CircularProgress sx={{ mb: 2 }} />}
<img src={modalContent.image} alt={modalContent.title} style={{ width: "100%", borderRadius: "8px", display: loadingImage ? "none" : "block" }} onLoad={() => setLoadingImage(false)} />
<Typography variant="h3" sx={{ fontWeight: "bold", mb: 2 }}>
{modalContent.title}
</Typography>
<Typography variant="body1" sx={{ mb: 4 }}>
{modalContent.text}
</Typography>
<Button
onClick={handleCloseModal}
variant="contained"
color="secondary"
sx={{
textTransform: "none",
fontWeight: "bold",
backgroundColor: "#f44336",
"&:hover": { backgroundColor: "#d32f2f" },
}}
>
Quitter
<Button onClick={handleCloseModal} variant="contained" color="secondary">
Retour
</Button>
</Box>
</Fade>
</Modal>
{/* Carousel Section */}
{carouselItems && carouselItems.length > 0 && (
<Box
@@ -319,47 +225,29 @@ const ServicePageTemplate = ({
</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 aujourdhui 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>
);
};
// 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;
+57
View File
@@ -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;
+91
View File
@@ -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;
+24 -3
View File
@@ -5,13 +5,24 @@ import { ThemeProvider } from "@mui/material/styles";
import theme from "./theme";
import { HelmetProvider } from "react-helmet-async";
import SimpleReactLightbox from "simple-react-lightbox";
import NotFound from "./components/Pages/NotFound.jsx";
import GestionArticles from "./components/Pages/GestionArticles";
import EditPost from "./components/EditPost";
import GestionPageAccueil from "./components/Pages/GestionPageAccueil";
// 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 Contact = lazy(() => import('./components/Pages/Contact'));
const Post = lazy(() => import('./components/Pages/Post'));
const PostDetails = lazy(() => import('./components/PostDetails')) ;
const Search = lazy(() => import('./components/Pages/Search')); // Nouveau composant pour la recherche
const BureauEtude = lazy(() => import('./components/Pages/BureauEtude'));
const CreatePost = lazy(() => import('./components/Pages/CreatePost'));
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 ServiceUn from "./components/Pages/ServiceUn.jsx";
@@ -30,13 +41,23 @@ function YourApp() {
<Routes>
<Route path="/" element={<Home />} />
<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="/contact" element={<Contact />} />
<Route path="/search" element={<Search />} /> {/* Route pour les recherches */}
{/* Routes pour les services */}
<Route path="/services/prestation-maitrise-oeuvre" element={<ServiceUn />} />
<Route path="/services/formation-ia" element={<ServiceDeux />} />
<Route path="/services/formation-video" element={<ServiceTrois />} />
<Route path="/services/structure-beton-charpente-metallique-bois" element={<ServiceDeux />} />
<Route path="/services/electricite" element={<ServiceTrois />} />
<Route path="/admin/create-post" element={<CreatePost />} />
<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 />} />
</Routes>
<Footer />
</Router>
+214
View File
@@ -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;
}
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,
]
);
});