10 Commits

Author SHA1 Message Date
sebvtl728 cf77437c89 MAJ Article 2025-01-31 17:28:21 +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
26 changed files with 2117 additions and 452 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 /
+469 -342
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from "react";
import { Box, Typography, Button, CircularProgress } from "@mui/material";
import { useParams, useNavigate } from "react-router-dom";
import api from "../api";
const PostDetails = () => {
const { id } = useParams();
const [post, setPost] = useState(null);
const [image, setImage] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const fetchPost = async () => {
try {
const response = await api.get(`wp/v2/posts/${id}?_fields=title,content,featured_media,date`);
setPost(response.data);
// Récupération de l'image
if (response.data.featured_media) {
const mediaResponse = await api.get(`wp/v2/media/${response.data.featured_media}`);
setImage(mediaResponse.data.source_url);
}
} catch (error) {
console.error("Erreur lors de la récupération de l'article :", error);
}
};
fetchPost();
}, [id]);
if (!post) {
return <CircularProgress sx={{ display: "block", margin: "auto", mt: 5 }} />;
}
return (
<Box sx={{ padding: "40px 20px",display:"flex", flexDirection:"column" }}>
<Button onClick={() => navigate(-1)} variant="outlined" sx={{ mt:5, mb: 3, maxWidth:"200px" }}>
Retour
</Button>
<Typography variant="h1" sx={{
fontWeight: "bold",
textAlign: "center",
mb: 4,
fontSize: { xs: '3rem', md: '3rem', lg: '3rem' }, // Responsive
}}>
{post.title.rendered}
</Typography>
<Box sx={{ display:"flex", alignItems:"center", justifyContent:"center", padding: "40px 20px",margin:"auto" }}>
{image && (
<Box
component="img"
src={image}
alt={post.title.rendered}
sx={{ width: "33%", maxHeight: "400px", objectFit: "cover", borderRadius: "8px", mb: 3 }}
/>
)}
<Typography variant="body1" sx={{ maxWidth: "800px", mx: "auto" }} dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</Box>
</Box>
);
};
export default PostDetails;
+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;
+34
View File
@@ -0,0 +1,34 @@
import { useState } from "react";
import { updatePost } from "../wordpress";
function EditPost({ post }) {
const [title, setTitle] = useState(post.title.rendered);
const [content, setContent] = useState(post.content.rendered);
const handleUpdate = async () => {
const success = await updatePost(post.id, title, content);
if (success) {
alert("Article mis à jour !");
} else {
alert("Échec de la mise à jour.");
}
};
return (
<div>
<h1>Modifier larticle</h1>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<button onClick={handleUpdate}>Mettre à jour</button>
</div>
);
}
export default EditPost;
+16 -2
View File
@@ -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 }} />
@@ -212,6 +213,19 @@ const Header = () => {
>
À 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}
to="/contact"
+341 -7
View File
@@ -1,12 +1,346 @@
import React from 'react';
import React from "react";
import {
Box,
Typography,
Grid,
Card,
CardContent,
Avatar,
} from "@mui/material";
import BusinessIcon from "@mui/icons-material/Business";
import BuildIcon from "@mui/icons-material/Build";
import FlashOnIcon from "@mui/icons-material/FlashOn";
import SecurityIcon from "@mui/icons-material/Security";
import EngineeringIcon from "@mui/icons-material/Engineering";
import AssessmentIcon from "@mui/icons-material/Assessment";
import ComputerIcon from "@mui/icons-material/Computer";
import LibraryBooksIcon from "@mui/icons-material/LibraryBooks";
import MoneyIcon from "@mui/icons-material/Money";
import PrintIcon from "@mui/icons-material/Print";
import StraightenIcon from "@mui/icons-material/Straighten";
import SettingsInputAntenna from "@mui/icons-material/SettingsInputAntenna";
import BookIcon from "@mui/icons-material/Book";
const About = () => {
return (
<div>
<h1>À propos</h1>
<p>Bienvenue sur la page À propos.</p>
</div>
);
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 (
<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>
);
};
export default About;
@@ -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;
@@ -0,0 +1,54 @@
import { useState } from "react";
import { uploadImage, createPost } from "../../wordpress";
function CreatePost() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [file, setFile] = useState(null);
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 !");
} catch (error) {
alert("❌ Erreur lors de la création du post.");
}
};
return (
<div>
<h2>Créer un post</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Titre du post"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<textarea
placeholder="Contenu du post"
value={content}
onChange={(e) => setContent(e.target.value)}
required
/>
<input
type="file"
accept="image/*"
onChange={(e) => setFile(e.target.files[0])}
required
/>
<button type="submit">Publier</button>
</form>
</div>
);
}
export default CreatePost;
@@ -0,0 +1,83 @@
import { useNavigate } from "react-router-dom";
import {
Box,
Typography,
Grid,
Card,
CardContent,
IconButton,
} from "@mui/material";
import ArticleIcon from "@mui/icons-material/Article";
import DashboardIcon from "@mui/icons-material/Dashboard";
function Dashboard() {
const navigate = useNavigate();
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",
}}
>
{/* En-tête */}
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 4 }}>
<DashboardIcon sx={{ fontSize: 40, color: "#0e467f", mr: 1 }} />
Tableau de Bord
</Typography>
{/* Cartes du Dashboard */}
<Grid container spacing={3} justifyContent="center">
<Grid item xs={12} sm={6} md={4}>
<Card
onClick={() => navigate("/posts")}
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;
+42 -32
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);
@@ -61,6 +62,8 @@ const Home = () => {
);
}
const { acf, rank_math_title, rank_math_description } = pageData || {};
return (
@@ -106,39 +109,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" } }}>
<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 +134,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>
);
};
+115
View File
@@ -0,0 +1,115 @@
import { useState } from "react";
import { loginWithAppPassword, getToken, logout } from "../../auth";
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 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 (
<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="error"
onClick={handleLogout}
sx={{ mt: 3, 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;
+77 -56
View File
@@ -1,64 +1,85 @@
import React, { useEffect, useState } from 'react';
import { Grid, Card, CardContent, CardMedia, Typography, Box } from '@mui/material';
import api from '../../api'; // Adaptez le chemin vers l'instance Axios
import React, { useState, useEffect } from "react";
import { Box, Typography, Grid, Card, CardContent, CardMedia, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import api from "../../api";
const Posts = () => {
const [posts, setPosts] = useState([]);
const navigate = useNavigate();
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await api.get("wp/v2/posts?_fields=id,title,excerpt,featured_media");
const postsData = response.data;
const Post = () => {
const [posts, setPosts] = useState([]);
// Récupérer les images en vedette
const postsWithImages = await Promise.all(
postsData.map(async (post) => {
if (post.featured_media) {
try {
const mediaResponse = await api.get(`wp/v2/media/${post.featured_media}`);
return { ...post, image: mediaResponse.data.source_url };
} catch (error) {
console.error(`Erreur lors de la récupération de l'image pour l'article ${post.id}:`, error);
return { ...post, image: null };
}
}
return { ...post, image: null };
})
);
useEffect(() => {
api.get('wp/v2/posts?_embed')
.then((response) => {
setPosts(response.data);
})
.catch((error) => {
console.error('Erreur lors de la récupération des articles :', error);
});
}, []);
setPosts(postsWithImages);
} catch (error) {
console.error("Erreur lors de la récupération des articles :", error);
}
};
return (
fetchPosts();
}, []);
<Box sx={{ padding: 4 }}>
<Typography variant="h3" sx={{ marginBottom: 4, textAlign: 'center' }}>
Nos réalisations
</Typography>
<Grid container spacing={4}>
{posts.map((post) => (
<Grid item xs={12} sm={6} md={4} key={post.id}>
<Card sx={{ maxWidth: 345,
boxShadow: 3,
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
'&:hover': {
transform: 'scale(1.05)',
boxShadow: 6,
},
}}>
{/* Image de l'article */}
{post._embedded?.['wp:featuredmedia']?.[0]?.source_url && (
<CardMedia
component="img"
height="200"
image={post._embedded['wp:featuredmedia'][0].source_url}
alt={post.title.rendered}
/>
)}
<CardContent>
<Typography variant="h5" component="div" gutterBottom>
{post.title.rendered}
</Typography>
<Typography
variant="body2"
color="text.secondary"
dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }}
/>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
);
// Fonction pour tronquer le texte à 70 caractères
const truncateText = (text, maxLength) => {
const strippedText = text.replace(/(<([^>]+)>)/gi, ""); // Suppression des balises HTML
return strippedText.length > maxLength ? `${strippedText.substring(0, maxLength)}...` : strippedText;
};
return (
<Box sx={{ padding: "40px 20px" }}>
<Typography variant="h2" sx={{ fontWeight: "bold", textAlign: "center", mb: 4 }}>
Nos Articles
</Typography>
<Grid container spacing={4} justifyContent="center">
{posts.map((post) => (
<Grid item xs={12} sm={6} md={4} key={post.id}>
<Card
sx={{
cursor: "pointer",
"&:hover": { backgroundColor: "#0e467f", color: "white", transform: "scale(1.05)", boxShadow: 6 },
}}
onClick={() => navigate(`/post/${post.id}`)}
>
{post.image && (
<CardMedia component="img" height="200" image={post.image} alt={post.title.rendered} />
)}
<CardContent>
<Typography variant="h5" sx={{ fontWeight: "bold" }}>
{post.title.rendered}
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
{truncateText(post.excerpt.rendered, 100)}
</Typography>
<Button variant="outlined" sx={{ mt: 2, color: "white", borderColor: "white" }}>
Lire plus
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Box>
);
};
export default Post;
export default Posts;
@@ -0,0 +1,27 @@
import { useEffect, useState } from "react";
import { fetchPosts, deletePost } from "../../wordpress";
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
async function loadPosts() {
const data = await fetchPosts();
setPosts(data);
}
loadPosts();
}, []);
return (
<div className="post-list">
<h2>Liste des Articles</h2>
{posts.map((post) => (
<div key={post.id} className="post-item">
<h3>{post.title.rendered}</h3>
<button onClick={() => deletePost(post.id)}>Supprimer</button>
</div>
))}
</div>
);
}
export default PostList;
@@ -3,7 +3,7 @@ import ServicePageTemplate from '../ServicePageTemplate';
const ServiceDeux = () => {
const serviceDetails = {
title: 'Formation IA',
title: 'Structure béton charpente métallique & bois',
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.',
+67
View File
@@ -0,0 +1,67 @@
import React, { useState, useEffect } from "react";
import { Box, Typography, Button, CircularProgress } from "@mui/material";
import { useParams, useNavigate } from "react-router-dom";
import api from "../api";
const PostDetails = () => {
const { id } = useParams();
const [post, setPost] = useState(null);
const [image, setImage] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const fetchPost = async () => {
try {
const response = await api.get(`wp/v2/posts/${id}?_fields=title,content,featured_media,date`);
setPost(response.data);
// Récupération de l'image
if (response.data.featured_media) {
const mediaResponse = await api.get(`wp/v2/media/${response.data.featured_media}`);
setImage(mediaResponse.data.source_url);
}
} catch (error) {
console.error("Erreur lors de la récupération de l'article :", error);
}
};
fetchPost();
}, [id]);
if (!post) {
return <CircularProgress sx={{ display: "block", margin: "auto", mt: 5 }} />;
}
return (
<Box sx={{ padding: "40px 20px",display:"flex", flexDirection:"column" }}>
<Button onClick={() => navigate(-1)} variant="outlined" sx={{ mt:5, mb: 3, maxWidth:"200px" }}>
Retour
</Button>
<Typography variant="h1" sx={{
fontWeight: "bold",
textAlign: "center",
mb: 4,
fontSize: { xs: '3rem', md: '3rem', lg: '3rem' }, // Responsive
}}>
{post.title.rendered}
</Typography>
<Box sx={{ display:"flex", alignItems:"center", justifyContent:"center", padding: "40px 20px",margin:"auto" }}>
{image && (
<Box
component="img"
src={image}
alt={post.title.rendered}
sx={{ width: "33%", maxHeight: "400px", objectFit: "cover", borderRadius: "8px", mb: 3 }}
/>
)}
<Typography variant="body1" sx={{ maxWidth: "800px", mx: "auto" }} dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
</Box>
</Box>
);
};
export default PostDetails;
+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;
+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;
+85
View File
@@ -0,0 +1,85 @@
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="h4"
sx={{
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="h6" sx={{ fontWeight: "bold" }}>
Marie Dupont
</Typography>
<Typography variant="body2">
Directrice Créative chez TechVision
</Typography>
</Box>
</Box>
</Grid>
</Box>
);
};
export default TestimonialSection;
+8 -2
View File
@@ -5,13 +5,16 @@ 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";
// 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'));
// Import des services
import ServiceUn from "./components/Pages/ServiceUn.jsx";
@@ -30,13 +33,16 @@ function YourApp() {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts" element={<Post />} />
<Route path="/post/:id" element={<PostDetails />} />
<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/structure-beton-charpente-metallique-bois" element={<ServiceDeux />} />
<Route path="/services/formation-video" element={<ServiceTrois />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Footer />
</Router>
+81
View File
@@ -0,0 +1,81 @@
import axios from "axios";
import { getToken } from "./auth"; // 🔥 Import du token depuis auth.js
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(); // 🔥 Récupération automatique du token
if (!token) {
console.error("❌ Aucun token trouvé ! L'utilisateur doit se reconnecter.");
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", "Nouvelle image");
formData.append("status", "publish");
try {
const response = await axios.post(
`${API_URL}/media`,
formData,
{
headers: {
"Authorization": `Basic ${token}`, // 🔥 Authentification Basic
}
}
);
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 ? error.response.data : error.message);
throw error;
}
}
/**
* 🔹 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(); // 🔥 Récupération automatique du token
if (!token) {
console.error("❌ Aucun token trouvé ! L'utilisateur doit se reconnecter.");
throw new Error("Utilisateur non authentifié.");
}
try {
const response = await axios.post(
`${API_URL}/posts`,
{
title: title,
content: content,
status: "publish",
featured_media: imageId, // 🔥 Associer l'image mise en avant
},
{
headers: {
"Authorization": `Basic ${token}`,
"Content-Type": "application/json"
}
}
);
console.log("✅ Post créé avec succès :", response.data);
return response.data;
} catch (error) {
console.error("❌ Erreur lors de la création du post :", error.response ? error.response.data : error.message);
throw error;
}
}
@@ -183,3 +183,90 @@ 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');