mise a jour structure visuel BO
This commit is contained in:
parent
1890d830e1
commit
a264f9b430
28
README.md
28
README.md
@ -1,2 +1,30 @@
|
|||||||
# Octopus-React-Wp
|
# Octopus-React-Wp
|
||||||
|
|
||||||
|
## Formulaire de contact – mise en route
|
||||||
|
|
||||||
|
Le formulaire (`frontend/src/components/Pages/Contact.jsx`) envoie désormais une requête POST vers l’API de contact.
|
||||||
|
Suivez ces étapes pour activer l’envoi d’e-mails :
|
||||||
|
|
||||||
|
1. **Configuration backend**
|
||||||
|
- Copiez `server/cloudinary-backend/.env.example` vers `server/cloudinary-backend/.env`.
|
||||||
|
- Renseignez vos identifiants SMTP (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, etc.).
|
||||||
|
- Facultatif : ajustez `CONTACT_RECIPIENT`, `CONTACT_FROM`, ou ajoutez des domaines supplémentaires dans `CORS_ALLOWED_ORIGINS`.
|
||||||
|
|
||||||
|
2. **Installer et lancer le serveur contact**
|
||||||
|
```bash
|
||||||
|
cd server/cloudinary-backend
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
Par défaut, l’API est disponible sur `http://localhost:3001/api/contact`.
|
||||||
|
|
||||||
|
3. **Configuration du front-end**
|
||||||
|
- Copiez `frontend/.env.local.example` vers `frontend/.env.local`.
|
||||||
|
- Ajustez `VITE_CONTACT_API_URL` si nécessaire (par défaut `http://localhost:3001/api/contact` en dev).
|
||||||
|
- Relancez l’application React :
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
En production, assurez-vous que l’URL définie dans `VITE_CONTACT_API_URL` pointe vers votre API accessible publiquement (ex. `https://octopusdesign.fr/api/contact`) et que le serveur backend est déployé avec les mêmes variables d’environnement SMTP.
|
||||||
|
|||||||
3
frontend/.env.local.example
Normal file
3
frontend/.env.local.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# URL of the contact API (backend server).
|
||||||
|
# In development, point to the local Express server.
|
||||||
|
VITE_CONTACT_API_URL=http://localhost:3001/api/contact
|
||||||
@ -119,4 +119,256 @@ ul li {
|
|||||||
.c4lv-readingcontext {
|
.c4lv-readingcontext {
|
||||||
padding: 30px 40px 32px 40px;
|
padding: 30px 40px 32px 40px;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Liquid glass dashboard */
|
||||||
|
.glass-dashboard {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(180px at 4% 2%, rgba(123, 192, 255, 0.35), transparent 65%),
|
||||||
|
radial-gradient(220px at 94% 8%, rgba(255, 180, 250, 0.32), transparent 70%),
|
||||||
|
radial-gradient(280px at 50% 100%, rgba(102, 204, 255, 0.22), transparent 72%),
|
||||||
|
linear-gradient(135deg, #0b1a3d 0%, #1e2c58 45%, #5a3fb6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-orb {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.58;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(0);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-orb.orb-1 {
|
||||||
|
width: 420px;
|
||||||
|
height: 420px;
|
||||||
|
top: -180px;
|
||||||
|
left: -120px;
|
||||||
|
background: rgba(118, 161, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-orb.orb-2 {
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
top: 22%;
|
||||||
|
right: -140px;
|
||||||
|
background: rgba(255, 172, 236, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-orb.orb-3 {
|
||||||
|
width: 460px;
|
||||||
|
height: 460px;
|
||||||
|
bottom: -220px;
|
||||||
|
left: 32%;
|
||||||
|
background: rgba(125, 215, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||||
|
box-shadow: 0 32px 48px -28px rgba(14, 21, 52, 0.68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-panel {
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.16),
|
||||||
|
rgba(255, 255, 255, 0.08)
|
||||||
|
) !important;
|
||||||
|
backdrop-filter: blur(22px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(22px) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.28) !important;
|
||||||
|
box-shadow: 0 28px 40px -32px rgba(12, 29, 74, 0.48) !important;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.3s ease, border-color 0.3s ease,
|
||||||
|
background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 20% 20%,
|
||||||
|
rgba(255, 255, 255, 0.28),
|
||||||
|
transparent 55%
|
||||||
|
);
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-card:hover {
|
||||||
|
box-shadow: 0 36px 50px -28px rgba(12, 29, 74, 0.56) !important;
|
||||||
|
border-color: rgba(49, 83, 151, 0.35) !important;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.22),
|
||||||
|
rgba(255, 255, 255, 0.12)
|
||||||
|
) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-subcard {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.6),
|
||||||
|
rgba(255, 255, 255, 0.42)
|
||||||
|
) !important;
|
||||||
|
backdrop-filter: blur(20px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(20px) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.35) !important;
|
||||||
|
box-shadow: 0 32px 46px -30px rgba(12, 29, 74, 0.5) !important;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-subcard::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(
|
||||||
|
320px at 18% 30%,
|
||||||
|
rgba(123, 192, 255, 0.12),
|
||||||
|
transparent 70%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
320px at 82% 12%,
|
||||||
|
rgba(255, 180, 250, 0.1),
|
||||||
|
transparent 68%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__hero::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: radial-gradient(
|
||||||
|
260px at 8% 18%,
|
||||||
|
rgba(255, 255, 255, 0.35),
|
||||||
|
transparent 65%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
320px at 92% 12%,
|
||||||
|
rgba(250, 233, 255, 0.3),
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
opacity: 0.35;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__hero-glow {
|
||||||
|
position: absolute;
|
||||||
|
width: 420px;
|
||||||
|
height: 420px;
|
||||||
|
right: -120px;
|
||||||
|
bottom: -140px;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle,
|
||||||
|
rgba(255, 255, 255, 0.55) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 70%
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__close {
|
||||||
|
box-shadow: 0 16px 24px -18px rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__steps {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__step-card {
|
||||||
|
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__step-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 32px 36px -28px rgba(12, 29, 74, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__filters {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__filters::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(123, 192, 255, 0.12),
|
||||||
|
rgba(255, 180, 250, 0.08)
|
||||||
|
);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-modal__section-header {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-article-card {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(255, 255, 255, 0.68),
|
||||||
|
rgba(255, 255, 255, 0.5)
|
||||||
|
) !important;
|
||||||
|
backdrop-filter: blur(24px) !important;
|
||||||
|
-webkit-backdrop-filter: blur(24px) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4) !important;
|
||||||
|
box-shadow: 0 36px 50px -30px rgba(12, 29, 74, 0.54) !important;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.35s ease, box-shadow 0.35s ease,
|
||||||
|
border-color 0.35s ease;
|
||||||
|
will-change: transform, box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-article-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(
|
||||||
|
480px at 20% 10%,
|
||||||
|
rgba(255, 255, 255, 0.32),
|
||||||
|
transparent 65%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
360px at 82% 12%,
|
||||||
|
rgba(255, 214, 150, 0.2),
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
opacity: 0.6;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-article-card:hover {
|
||||||
|
transform: translate3d(0, -6px, 0);
|
||||||
|
box-shadow: 0 44px 56px -30px rgba(12, 29, 74, 0.62);
|
||||||
|
border-color: rgba(49, 83, 151, 0.42);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,20 +1,110 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { getPostById, updatePost, uploadImageFromUrl } from "../wordpress"; // utilise uploadImageFromUrl
|
import { getPostById, updatePost, uploadImageFromUrl } from "../wordpress";
|
||||||
import { getToken } from "../auth";
|
import { getToken } from "../auth";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
|
||||||
Typography,
|
Typography,
|
||||||
TextField,
|
TextField,
|
||||||
Button,
|
Button,
|
||||||
Paper,
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
Divider,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { ArrowBack, Height, Publish } from "@mui/icons-material";
|
import { ArrowBack, Publish, Image as ImageIcon, History } from "@mui/icons-material";
|
||||||
import ImageUploaderCloudinary from "./ImageUploaderCloudinary";
|
import ImageUploaderCloudinary from "./ImageUploaderCloudinary";
|
||||||
import CloudinaryGallerySelector from "./CloudinaryGallerySelector";
|
import CloudinaryGallerySelector from "./CloudinaryGallerySelector";
|
||||||
import ReactQuill from "react-quill";
|
import ReactQuill from "react-quill";
|
||||||
import "react-quill/dist/quill.snow.css";
|
import "react-quill/dist/quill.snow.css";
|
||||||
|
import "../assets/styleCours.css";
|
||||||
|
|
||||||
|
const BUTTON_BASE_SX = Object.freeze({
|
||||||
|
borderRadius: 999,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0.75,
|
||||||
|
transition:
|
||||||
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_SIZE_SX = Object.freeze({
|
||||||
|
small: {
|
||||||
|
px: 2.1,
|
||||||
|
py: 0.55,
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
minHeight: 30,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
px: 2.8,
|
||||||
|
py: 0.8,
|
||||||
|
fontSize: "0.86rem",
|
||||||
|
minHeight: 36,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
px: 3.4,
|
||||||
|
py: 1,
|
||||||
|
fontSize: "0.94rem",
|
||||||
|
minHeight: 42,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_VARIANT_SX = Object.freeze({
|
||||||
|
primary: {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.35)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11, 26, 61, 0.6)",
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "rgba(255,255,255,0.72)",
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, rgba(49,83,151,0.35), rgba(123,192,255,0.35))",
|
||||||
|
boxShadow: "none",
|
||||||
|
border: "1px solid rgba(49,83,151,0.18)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
color: "rgba(255,255,255,0.92)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.24)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.26)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.96)",
|
||||||
|
borderColor: "rgba(49,83,151,0.46)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const composeButtonSx = (variant, size = "medium") => ({
|
||||||
|
...BUTTON_BASE_SX,
|
||||||
|
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
|
||||||
|
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
|
||||||
|
});
|
||||||
|
|
||||||
function EditPost() {
|
function EditPost() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@ -23,6 +113,13 @@ function EditPost() {
|
|||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [imageUrl, setImageUrl] = useState(null); // preview
|
const [imageUrl, setImageUrl] = useState(null); // preview
|
||||||
const [imageFile, setImageFile] = useState(null); // pour upload
|
const [imageFile, setImageFile] = useState(null); // pour upload
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [postMeta, setPostMeta] = useState({
|
||||||
|
date: null,
|
||||||
|
modified: null,
|
||||||
|
status: "",
|
||||||
|
});
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -33,13 +130,21 @@ function EditPost() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPost = async () => {
|
const fetchPost = async () => {
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
const post = await getPostById(id);
|
const post = await getPostById(id);
|
||||||
setTitle(post.title.rendered);
|
setTitle(post.title.rendered);
|
||||||
setContent(post.content.rendered);
|
setContent(post.content.rendered);
|
||||||
setImageUrl(post?.jetpack_featured_media_url || null); // preview actuelle
|
setImageUrl(post?.jetpack_featured_media_url || null);
|
||||||
} catch (err) {
|
setPostMeta({
|
||||||
console.error("Erreur chargement article :", err);
|
date: post.date || null,
|
||||||
|
modified: post.modified || null,
|
||||||
|
status: post.status || "",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur chargement article :", error);
|
||||||
navigate("/admin/gestion-articles");
|
navigate("/admin/gestion-articles");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchPost();
|
fetchPost();
|
||||||
@ -62,119 +167,392 @@ function EditPost() {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdate = async (e) => {
|
const canSubmit =
|
||||||
e.preventDefault();
|
title.trim().length > 0 && content.trim().length > 0 && Boolean(imageUrl);
|
||||||
try {
|
|
||||||
let imageId = null;
|
|
||||||
|
|
||||||
if (imageFile) {
|
const handleUpdate = async (event) => {
|
||||||
// upload image vers WordPress
|
event.preventDefault();
|
||||||
|
if (!canSubmit || submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
let imageId = undefined;
|
||||||
|
if (imageFile || imageUrl) {
|
||||||
imageId = await uploadImageFromUrl(imageUrl);
|
imageId = await uploadImageFromUrl(imageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updatePost(id, title, content, imageId);
|
await updatePost(id, title, content, imageId);
|
||||||
alert("✅ Article mis à jour !");
|
alert("✅ Article mis à jour !");
|
||||||
navigate("/admin/gestion-articles");
|
navigate("/admin/gestion-articles");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erreur update :", error);
|
console.error("❌ Erreur update :", error);
|
||||||
alert("❌ Erreur mise à jour.");
|
alert("❌ Erreur mise à jour.");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
className="glass-dashboard"
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
display: "flex",
|
position: "relative",
|
||||||
justifyContent: "center",
|
px: { xs: 2, md: 6 },
|
||||||
alignItems: "center",
|
pt: { xs: 6, md: 8 },
|
||||||
|
pb: { xs: 8, md: 10 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container maxWidth="lg">
|
<Box className="glass-orb orb-1" />
|
||||||
<Paper elevation={6} sx={{ p: 4, borderRadius: 3 }}>
|
<Box className="glass-orb orb-2" />
|
||||||
|
<Box className="glass-orb orb-3" />
|
||||||
|
|
||||||
|
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
spacing={2}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate("/admin/gestion-articles")}
|
onClick={() => navigate("/admin/gestion-articles")}
|
||||||
variant="outlined"
|
variant="contained"
|
||||||
startIcon={<ArrowBack />}
|
startIcon={<ArrowBack />}
|
||||||
|
sx={composeButtonSx("ghost")}
|
||||||
>
|
>
|
||||||
Retour
|
Retour aux articles
|
||||||
</Button>
|
</Button>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
<Typography variant="h4" align="center" sx={{ mb: 3 }}>
|
<Typography
|
||||||
Modifier l'article
|
variant="overline"
|
||||||
</Typography>
|
sx={{
|
||||||
|
letterSpacing: 2,
|
||||||
<form onSubmit={handleUpdate}>
|
fontWeight: 700,
|
||||||
<TextField
|
color: "rgba(255,255,255,0.8)",
|
||||||
label="Titre"
|
}}
|
||||||
fullWidth
|
>
|
||||||
value={title}
|
Publication Octopus
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
</Typography>
|
||||||
sx={{ mb: 2 }}
|
<Typography
|
||||||
/>
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Modifier l’article
|
||||||
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body1"
|
variant="body1"
|
||||||
sx={{ mt: 2, mb: 2, fontWeight: "bold" }}
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
maxWidth: 520,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Contenu de l'article :
|
Ajustez votre contenu, remplacez le visuel mis en avant et publiez
|
||||||
|
les mises à jour directement sur le site Octopus.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Box sx={{ height: 250, mb: 7 }}>
|
<Grid container spacing={3.5}>
|
||||||
<ReactQuill
|
<Grid item xs={12} lg={7}>
|
||||||
theme="snow"
|
<Card
|
||||||
value={content}
|
className="glass-subcard"
|
||||||
onChange={setContent}
|
sx={{
|
||||||
modules={modules}
|
borderRadius: 4,
|
||||||
style={{ height: "100%" }}
|
p: { xs: 2.5, md: 3 },
|
||||||
/>
|
boxShadow: "0 28px 36px -28px rgba(12, 29, 74, 0.5)",
|
||||||
</Box>
|
|
||||||
<Typography fontWeight="bold">Image depuis ton ordi :</Typography>
|
|
||||||
<ImageUploaderCloudinary
|
|
||||||
onUploadSuccess={(url) => {
|
|
||||||
setImageUrl(url);
|
|
||||||
convertUrlToFile(url).then(setImageFile);
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<CardContent sx={{ p: 0 }}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
alignItems="center"
|
||||||
|
sx={{ flexWrap: "wrap" }}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
icon={<History fontSize="small" />}
|
||||||
|
label={
|
||||||
|
postMeta.modified
|
||||||
|
? `Modifié le ${new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(new Date(postMeta.modified))}`
|
||||||
|
: "Modification en cours"
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(255,255,255,0.78)",
|
||||||
|
color: "#0b1a3d",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={
|
||||||
|
postMeta.status === "publish"
|
||||||
|
? "Publiée"
|
||||||
|
: postMeta.status === "draft"
|
||||||
|
? "Brouillon"
|
||||||
|
: postMeta.status || "Statut inconnu"
|
||||||
|
}
|
||||||
|
color={
|
||||||
|
postMeta.status === "publish"
|
||||||
|
? "success"
|
||||||
|
: postMeta.status === "draft"
|
||||||
|
? "warning"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
sx={{ fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Typography fontWeight="bold" sx={{ mt: 3 }}>
|
{loading ? (
|
||||||
Ou choisir une image déjà envoyée :
|
<Typography variant="body2" sx={{ color: "rgba(11, 26, 61, 0.65)" }}>
|
||||||
</Typography>
|
Chargement de l’article…
|
||||||
<CloudinaryGallerySelector
|
</Typography>
|
||||||
onSelect={async (url) => {
|
) : (
|
||||||
setImageUrl(url);
|
<Box component="form" onSubmit={handleUpdate} noValidate>
|
||||||
const file = await convertUrlToFile(url);
|
<TextField
|
||||||
setImageFile(file);
|
label="Titre de l'article"
|
||||||
}}
|
fullWidth
|
||||||
/>
|
variant="outlined"
|
||||||
|
value={title}
|
||||||
|
onChange={(event) => setTitle(event.target.value)}
|
||||||
|
required
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.94)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{imageUrl && (
|
<Box
|
||||||
<Box sx={{ mt: 2 }}>
|
sx={{
|
||||||
<Typography variant="body2">Aperçu :</Typography>
|
mb: 3,
|
||||||
<img
|
borderRadius: 2,
|
||||||
src={imageUrl}
|
overflow: "hidden",
|
||||||
style={{
|
border: "1px solid rgba(49,83,151,0.15)",
|
||||||
width: "100%",
|
backgroundColor: "rgba(255,255,255,0.92)",
|
||||||
borderRadius: 6,
|
}}
|
||||||
maxHeight: 200,
|
>
|
||||||
objectFit: "cover",
|
<ReactQuill
|
||||||
|
theme="snow"
|
||||||
|
value={content}
|
||||||
|
onChange={setContent}
|
||||||
|
modules={modules}
|
||||||
|
style={{ height: 280 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack spacing={2.5}>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||||||
|
>
|
||||||
|
Remplacer le visuel
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
|
||||||
|
>
|
||||||
|
Importez une nouvelle image optimisée pour vos
|
||||||
|
contenus.
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<ImageUploaderCloudinary
|
||||||
|
onUploadSuccess={(url) => {
|
||||||
|
setImageUrl(url);
|
||||||
|
convertUrlToFile(url).then(setImageFile);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider
|
||||||
|
sx={{ borderColor: "rgba(11, 26, 61, 0.08)" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||||||
|
>
|
||||||
|
Sélectionner depuis la médiathèque
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 2 }}
|
||||||
|
>
|
||||||
|
Choisissez un visuel existant dans Cloudinary.
|
||||||
|
</Typography>
|
||||||
|
<CloudinaryGallerySelector
|
||||||
|
onSelect={async (url) => {
|
||||||
|
setImageUrl(url);
|
||||||
|
const file = await convertUrlToFile(url);
|
||||||
|
setImageFile(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", sm: "row" }}
|
||||||
|
spacing={1.5}
|
||||||
|
sx={{ pt: 1 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Publish />}
|
||||||
|
disabled={!canSubmit || submitting}
|
||||||
|
sx={{
|
||||||
|
...composeButtonSx("primary"),
|
||||||
|
minWidth: 220,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{submitting ? "Enregistrement…" : "Mettre à jour"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={composeButtonSx("outline")}
|
||||||
|
onClick={() => {
|
||||||
|
setTitle("");
|
||||||
|
setContent("");
|
||||||
|
setImageUrl(null);
|
||||||
|
setImageFile(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} lg={5}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
boxShadow: "0 28px 36px -28px rgba(12, 29, 74, 0.45)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="flex-start">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: "rgba(123,192,255,0.22)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#315397",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageIcon />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d", mb: 1 }}
|
||||||
|
>
|
||||||
|
Rappels éditoriaux
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.68)" }}
|
||||||
|
>
|
||||||
|
• Vérifiez la cohérence des titres et sous-titres.<br />
|
||||||
|
• Ajoutez des liens internes vers vos formations Octopus.<br />
|
||||||
|
• N’oubliez pas de relire la mise en forme dans l’aperçu
|
||||||
|
WordPress.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className="glass-article-card"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: "hidden",
|
||||||
|
minHeight: 220,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
gap: 1.5,
|
||||||
}}
|
}}
|
||||||
alt="preview"
|
>
|
||||||
/>
|
<Typography
|
||||||
</Box>
|
variant="subtitle1"
|
||||||
)}
|
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||||||
|
>
|
||||||
<Button
|
Aperçu visuel
|
||||||
type="submit"
|
</Typography>
|
||||||
variant="contained"
|
{imageUrl ? (
|
||||||
fullWidth
|
<Box
|
||||||
sx={{ mt: 3 }}
|
component="img"
|
||||||
startIcon={<Publish />}
|
src={imageUrl}
|
||||||
>
|
alt="Aperçu de l’image sélectionnée"
|
||||||
Mettre à jour
|
sx={{
|
||||||
</Button>
|
width: "100%",
|
||||||
</form>
|
borderRadius: 3,
|
||||||
</Paper>
|
height: 200,
|
||||||
</Container>
|
objectFit: "cover",
|
||||||
|
boxShadow: "0 20px 28px -24px rgba(12, 29, 74, 0.48)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
borderRadius: 3,
|
||||||
|
border: "1px dashed rgba(49,83,151,0.35)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "rgba(49,83,151,0.6)",
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aucun visuel sélectionné pour le moment
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
|
||||||
|
>
|
||||||
|
Ce visuel illustrera l’article dans la liste des publications
|
||||||
|
et sur les réseaux sociaux.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,128 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { getToken } from "../auth";
|
import { getToken } from "../auth";
|
||||||
import { getPageById, updatePageACF } from "../wordpress";
|
import { getPageById, updatePageACF } from "../wordpress";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
|
||||||
Typography,
|
Typography,
|
||||||
TextField,
|
TextField,
|
||||||
Button,
|
Button,
|
||||||
Paper,
|
|
||||||
Grid,
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Stack,
|
||||||
|
Divider,
|
||||||
|
CircularProgress,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Add, Delete, ArrowBack, Save } from "@mui/icons-material";
|
import { Add, Delete, ArrowBack, Save } from "@mui/icons-material";
|
||||||
import ReactQuill from "react-quill";
|
import ReactQuill from "react-quill";
|
||||||
import "react-quill/dist/quill.snow.css";
|
import "react-quill/dist/quill.snow.css";
|
||||||
|
import "../assets/styleCours.css";
|
||||||
|
|
||||||
const ETUDE_PAGE_ID = 272;
|
const ETUDE_PAGE_ID = 272;
|
||||||
|
|
||||||
|
const BUTTON_BASE_SX = Object.freeze({
|
||||||
|
borderRadius: 999,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0.75,
|
||||||
|
transition:
|
||||||
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_SIZE_SX = Object.freeze({
|
||||||
|
small: {
|
||||||
|
px: 2.1,
|
||||||
|
py: 0.55,
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
minHeight: 30,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
px: 2.8,
|
||||||
|
py: 0.8,
|
||||||
|
fontSize: "0.86rem",
|
||||||
|
minHeight: 36,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
px: 3.4,
|
||||||
|
py: 1,
|
||||||
|
fontSize: "0.94rem",
|
||||||
|
minHeight: 42,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_VARIANT_SX = Object.freeze({
|
||||||
|
primary: {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11, 26, 61, 0.62)",
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "rgba(255,255,255,0.72)",
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, rgba(49,83,151,0.35), rgba(123,192,255,0.35))",
|
||||||
|
boxShadow: "none",
|
||||||
|
border: "1px solid rgba(49,83,151,0.18)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
color: "rgba(255,255,255,0.92)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.24)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.24)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.26)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.95)",
|
||||||
|
borderColor: "rgba(49,83,151,0.46)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
background: "linear-gradient(135deg, #f05b6b, #ff8a80)",
|
||||||
|
color: "#3a0a0f",
|
||||||
|
border: "1px solid rgba(240,91,107,0.35)",
|
||||||
|
boxShadow: "0 18px 26px -18px rgba(105, 21, 33, 0.45)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #e34659, #ff7575)",
|
||||||
|
boxShadow: "0 22px 30px -18px rgba(105, 21, 33, 0.52)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const composeButtonSx = (variant, size = "medium") => ({
|
||||||
|
...BUTTON_BASE_SX,
|
||||||
|
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
|
||||||
|
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
|
||||||
|
});
|
||||||
|
|
||||||
function EditPageEtude() {
|
function EditPageEtude() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [acfFields, setAcfFields] = useState({});
|
const [acfFields, setAcfFields] = useState({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [successMessage, setSuccessMessage] = useState("");
|
const [successMessage, setSuccessMessage] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
// Vérifie l'authentification
|
// Vérifie l'authentification
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -50,15 +150,22 @@ function EditPageEtude() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Mise à jour ACF
|
// Mise à jour ACF
|
||||||
const handleUpdateACF = async (e) => {
|
const handleUpdateACF = async (event) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
|
if (submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
await updatePageACF(ETUDE_PAGE_ID, acfFields);
|
await updatePageACF(ETUDE_PAGE_ID, acfFields);
|
||||||
setSuccessMessage("✅ Modifications enregistrées !");
|
setSuccessMessage("✅ Modifications enregistrées !");
|
||||||
setError(null);
|
setError(null);
|
||||||
setTimeout(() => navigate("/admin/dashboard"), 2000);
|
setTimeout(() => navigate("/admin/dashboard"), 1800);
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error("⚠ Impossible de mettre à jour les champs ACF :", err);
|
||||||
setError("⚠ Impossible de mettre à jour les champs ACF.");
|
setError("⚠ Impossible de mettre à jour les champs ACF.");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -98,126 +205,466 @@ function EditPageEtude() {
|
|||||||
setAcfFields(prev => ({ ...prev, [field]: updated }));
|
setAcfFields(prev => ({ ...prev, [field]: updated }));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) return <Typography>Chargement...</Typography>;
|
const stats = useMemo(() => {
|
||||||
|
const toArray = (value) =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: value
|
||||||
|
? Object.values(value)
|
||||||
|
: [];
|
||||||
|
const competencies = toArray(acfFields.liste_des_competences);
|
||||||
|
const expertises = toArray(acfFields.expertises_specifiques);
|
||||||
|
return {
|
||||||
|
competencies: competencies.length,
|
||||||
|
expertises: expertises.length,
|
||||||
|
};
|
||||||
|
}, [acfFields]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="glass-dashboard"
|
||||||
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
px: { xs: 2, md: 6 },
|
||||||
|
pt: { xs: 6, md: 8 },
|
||||||
|
pb: { xs: 8, md: 10 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="glass-orb orb-1" />
|
||||||
|
<Box className="glass-orb orb-2" />
|
||||||
|
<Box className="glass-orb orb-3" />
|
||||||
|
<Stack spacing={2} alignItems="center" sx={{ position: "relative", zIndex: 1 }}>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
<Typography variant="body1" sx={{ color: "#ffffff" }}>
|
||||||
|
Chargement de la page Bureau d’étude…
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ py: 4, backgroundColor: "#f5f5f5" }}>
|
<Box
|
||||||
<Container maxWidth="md">
|
className="glass-dashboard"
|
||||||
<Paper sx={{ p: 4, borderRadius: 3 }} elevation={4}>
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
position: "relative",
|
||||||
|
px: { xs: 2, md: 6 },
|
||||||
|
pt: { xs: 6, md: 8 },
|
||||||
|
pb: { xs: 8, md: 10 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="glass-orb orb-1" />
|
||||||
|
<Box className="glass-orb orb-2" />
|
||||||
|
<Box className="glass-orb orb-3" />
|
||||||
|
|
||||||
|
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
spacing={2}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<ArrowBack />}
|
startIcon={<ArrowBack />}
|
||||||
variant="outlined"
|
|
||||||
onClick={() => navigate("/admin/dashboard")}
|
onClick={() => navigate("/admin/dashboard")}
|
||||||
sx={{ mb: 2 }}
|
variant="contained"
|
||||||
|
sx={composeButtonSx("ghost")}
|
||||||
>
|
>
|
||||||
Retour
|
Retour au dashboard
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 3, textAlign: "center" }}>
|
<Stack spacing={0.5}>
|
||||||
Modifier la Page Formations
|
<Typography
|
||||||
</Typography>
|
variant="overline"
|
||||||
|
sx={{
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Bureau d’étude Octopus
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Gestion des contenus ACF
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
maxWidth: 520,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Mettez à jour l’introduction, la méthodologie et les expertises du
|
||||||
|
bureau d’étude pour garder des informations pertinentes et à jour.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
{error && <Typography color="error" sx={{ mb: 2 }}>{error}</Typography>}
|
<Grid container spacing={2.5} sx={{ mb: 4 }}>
|
||||||
{successMessage && <Typography color="success.main" sx={{ mb: 2, textAlign: "center" }}>{successMessage}</Typography>}
|
<Grid item xs={12} md={6}>
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 4, p: 2.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Compétences listées
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
||||||
|
>
|
||||||
|
{stats.competencies}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
|
>
|
||||||
|
Ajoutez des compétences pour valoriser l’expertise Octopus.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 4, p: 2.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Expertises spécifiques
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
||||||
|
>
|
||||||
|
{stats.expertises}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
|
>
|
||||||
|
Développez vos expertises pour renforcer la crédibilité de l’équipe.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<form onSubmit={handleUpdateACF}>
|
{error ? (
|
||||||
<Grid container spacing={3}>
|
<Card
|
||||||
{/* Introduction */}
|
className="glass-subcard"
|
||||||
<Grid item xs={12}>
|
sx={{ borderRadius: 4, p: 3, mb: 3, borderColor: "rgba(240,91,107,0.35) !important" }}
|
||||||
<Typography variant="h6">Introduction</Typography>
|
>
|
||||||
<ReactQuill
|
<Typography color="error" sx={{ fontWeight: 600 }}>
|
||||||
value={acfFields.introduction || ""}
|
{error}
|
||||||
onChange={(val) => handleFieldChange("introduction", val)}
|
</Typography>
|
||||||
/>
|
</Card>
|
||||||
</Grid>
|
) : null}
|
||||||
|
{successMessage ? (
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{ borderRadius: 4, p: 3, mb: 3, borderColor: "rgba(123,192,255,0.45) !important" }}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#0b8043", fontWeight: 600 }}>
|
||||||
|
{successMessage}
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Méthodologie */}
|
<Box component="form" onSubmit={handleUpdateACF}>
|
||||||
<Grid item xs={12}>
|
<Stack spacing={3.5}>
|
||||||
<Typography variant="h6">Méthodologie</Typography>
|
<Card className="glass-subcard" sx={{ borderRadius: 4 }}>
|
||||||
<TextField
|
<CardContent sx={{ p: { xs: 2.5, md: 3 } }}>
|
||||||
fullWidth
|
<Stack spacing={2.5}>
|
||||||
value={acfFields.methodologie || ""}
|
<Box>
|
||||||
onChange={(e) => handleFieldChange("methodologie", e.target.value)}
|
<Typography
|
||||||
/>
|
variant="h6"
|
||||||
</Grid>
|
sx={{ color: "#0b1a3d", fontWeight: 700, mb: 1 }}
|
||||||
|
>
|
||||||
|
Introduction
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 1.5 }}
|
||||||
|
>
|
||||||
|
Présentez la vision globale du bureau d’étude et son rôle
|
||||||
|
au sein des formations Octopus.
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderRadius: 2,
|
||||||
|
border: "1px solid rgba(49,83,151,0.15)",
|
||||||
|
backgroundColor: "rgba(255,255,255,0.92)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReactQuill
|
||||||
|
value={acfFields.introduction || ""}
|
||||||
|
onChange={(value) => handleFieldChange("introduction", value)}
|
||||||
|
style={{ height: 220 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Liste des compétences */}
|
<Divider sx={{ borderColor: "rgba(11, 26, 61, 0.08)" }} />
|
||||||
<Grid item xs={12}>
|
|
||||||
<Typography variant="h6">Liste des compétences</Typography>
|
<Box>
|
||||||
{getRepeater("liste_des_competences").map((item, i) => (
|
<Typography
|
||||||
<Paper key={i} sx={{ p:2, mb:2 }}>
|
variant="h6"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 700, mb: 1 }}
|
||||||
|
>
|
||||||
|
Méthodologie
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 1.5 }}
|
||||||
|
>
|
||||||
|
Décrivez en quelques lignes la manière dont le bureau
|
||||||
|
d’étude accompagne les clients et partenaires.
|
||||||
|
</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
label="Titre"
|
|
||||||
fullWidth
|
fullWidth
|
||||||
value={item.comp_titre}
|
value={acfFields.methodologie || ""}
|
||||||
onChange={(e) => handleRepeaterChange("liste_des_competences", i, "comp_titre", e.target.value)}
|
onChange={(event) =>
|
||||||
sx={{ mb:1 }}
|
handleFieldChange("methodologie", event.target.value)
|
||||||
|
}
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.94)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
</Box>
|
||||||
label="Description"
|
</Stack>
|
||||||
fullWidth
|
</CardContent>
|
||||||
multiline rows={3}
|
</Card>
|
||||||
value={item.comp_description}
|
|
||||||
onChange={(e) => handleRepeaterChange("liste_des_competences", i, "comp_description", e.target.value)}
|
|
||||||
sx={{ mb:1 }}
|
|
||||||
/>
|
|
||||||
<Button startIcon={<Delete />} variant="outlined" color="error" onClick={() => deleteRepeaterItem("liste_des_competences", i)}>
|
|
||||||
Supprimer
|
|
||||||
</Button>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
<Button startIcon={<Add />} variant="contained" onClick={() => addRepeaterItem("liste_des_competences", { comp_titre: "", comp_description: "" })}>
|
|
||||||
Ajouter une compétence
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Expertises spécifiques */}
|
<Card className="glass-subcard" sx={{ borderRadius: 4 }}>
|
||||||
<Grid item xs={12}>
|
<CardContent sx={{ p: { xs: 2.5, md: 3 } }}>
|
||||||
<Typography variant="h6">Expertises spécifiques</Typography>
|
<Stack spacing={2.5}>
|
||||||
{getRepeater("expertises_specifiques").map((item, i) => (
|
<Box>
|
||||||
<Paper key={i} sx={{ p:2, mb:2 }}>
|
<Typography
|
||||||
<TextField
|
variant="h6"
|
||||||
label="Nom"
|
sx={{ color: "#0b1a3d", fontWeight: 700, mb: 1 }}
|
||||||
fullWidth
|
>
|
||||||
value={item.expertise_nom}
|
Liste des compétences
|
||||||
onChange={(e) => handleRepeaterChange("expertises_specifiques", i, "expertise_nom", e.target.value)}
|
</Typography>
|
||||||
sx={{ mb:1 }}
|
<Typography
|
||||||
/>
|
variant="body2"
|
||||||
<TextField
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 1.5 }}
|
||||||
label="Détails"
|
>
|
||||||
fullWidth multiline rows={2}
|
Ajoutez les compétences clés qui démontrent l’expertise du
|
||||||
value={item.expertise_details}
|
bureau d’étude. Chaque compétence peut contenir un titre et
|
||||||
onChange={(e) => handleRepeaterChange("expertises_specifiques", i, "expertise_details", e.target.value)}
|
une description détaillée.
|
||||||
sx={{ mb:1 }}
|
</Typography>
|
||||||
/>
|
</Box>
|
||||||
<Button startIcon={<Delete />} variant="outlined" color="error" onClick={() => deleteRepeaterItem("expertises_specifiques", i)}>
|
|
||||||
Supprimer
|
|
||||||
</Button>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
<Button startIcon={<Add />} variant="contained" onClick={() => addRepeaterItem("expertises_specifiques", { expertise_nom: "", expertise_details: "" })}>
|
|
||||||
Ajouter une expertise
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* Save */}
|
<Stack spacing={2}>
|
||||||
<Grid item xs={12}>
|
{getRepeater("liste_des_competences").map((item, index) => (
|
||||||
<Button
|
<Card
|
||||||
type="submit"
|
key={index}
|
||||||
fullWidth
|
className="glass-card"
|
||||||
variant="contained"
|
sx={{ borderRadius: 3, p: { xs: 2, md: 2.5 } }}
|
||||||
startIcon={<Save />}
|
>
|
||||||
sx={{ py:1.5 }}
|
<Stack spacing={1.5}>
|
||||||
>
|
<TextField
|
||||||
Enregistrer
|
label="Titre"
|
||||||
</Button>
|
fullWidth
|
||||||
</Grid>
|
value={item.comp_titre}
|
||||||
</Grid>
|
onChange={(event) =>
|
||||||
</form>
|
handleRepeaterChange(
|
||||||
</Paper>
|
"liste_des_competences",
|
||||||
</Container>
|
index,
|
||||||
|
"comp_titre",
|
||||||
|
event.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Description"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
minRows={3}
|
||||||
|
value={item.comp_description}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleRepeaterChange(
|
||||||
|
"liste_des_competences",
|
||||||
|
index,
|
||||||
|
"comp_description",
|
||||||
|
event.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
startIcon={<Delete />}
|
||||||
|
onClick={() =>
|
||||||
|
deleteRepeaterItem("liste_des_competences", index)
|
||||||
|
}
|
||||||
|
variant="contained"
|
||||||
|
sx={composeButtonSx("danger", "small")}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
startIcon={<Add />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
addRepeaterItem("liste_des_competences", {
|
||||||
|
comp_titre: "",
|
||||||
|
comp_description: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sx={composeButtonSx("primary", "small")}
|
||||||
|
>
|
||||||
|
Ajouter une compétence
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 4 }}>
|
||||||
|
<CardContent sx={{ p: { xs: 2.5, md: 3 } }}>
|
||||||
|
<Stack spacing={2.5}>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 700, mb: 1 }}
|
||||||
|
>
|
||||||
|
Expertises spécifiques
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 1.5 }}
|
||||||
|
>
|
||||||
|
Décrivez les expertises métiers ou sectorielles qui
|
||||||
|
distinguent votre bureau d’étude.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{getRepeater("expertises_specifiques").map((item, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
className="glass-card"
|
||||||
|
sx={{ borderRadius: 3, p: { xs: 2, md: 2.5 } }}
|
||||||
|
>
|
||||||
|
<Stack spacing={1.5}>
|
||||||
|
<TextField
|
||||||
|
label="Nom de l’expertise"
|
||||||
|
fullWidth
|
||||||
|
value={item.expertise_nom}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleRepeaterChange(
|
||||||
|
"expertises_specifiques",
|
||||||
|
index,
|
||||||
|
"expertise_nom",
|
||||||
|
event.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Détails"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
minRows={2}
|
||||||
|
value={item.expertise_details}
|
||||||
|
onChange={(event) =>
|
||||||
|
handleRepeaterChange(
|
||||||
|
"expertises_specifiques",
|
||||||
|
index,
|
||||||
|
"expertise_details",
|
||||||
|
event.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
startIcon={<Delete />}
|
||||||
|
onClick={() =>
|
||||||
|
deleteRepeaterItem("expertises_specifiques", index)
|
||||||
|
}
|
||||||
|
variant="contained"
|
||||||
|
sx={composeButtonSx("danger", "small")}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
startIcon={<Add />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
addRepeaterItem("expertises_specifiques", {
|
||||||
|
expertise_nom: "",
|
||||||
|
expertise_details: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sx={composeButtonSx("primary", "small")}
|
||||||
|
>
|
||||||
|
Ajouter une expertise
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Save />}
|
||||||
|
disabled={submitting}
|
||||||
|
sx={{ ...composeButtonSx("primary"), alignSelf: "flex-end", minWidth: 220 }}
|
||||||
|
>
|
||||||
|
{submitting ? "Enregistrement…" : "Enregistrer les modifications"}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditPageEtude;
|
export default EditPageEtude;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { getToken } from "../auth";
|
import { getToken } from "../auth";
|
||||||
import { fetchPages } from "../wordpress";
|
import { fetchPages } from "../wordpress";
|
||||||
@ -6,24 +6,109 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
Table,
|
Grid,
|
||||||
TableBody,
|
Card,
|
||||||
TableCell,
|
TextField,
|
||||||
TableContainer,
|
Stack,
|
||||||
TableHead,
|
CircularProgress,
|
||||||
TableRow,
|
|
||||||
Paper,
|
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Edit, ArrowBack, Add } from "@mui/icons-material";
|
import { Edit, ArrowBack, Add } from "@mui/icons-material";
|
||||||
|
import "../assets/styleCours.css";
|
||||||
|
|
||||||
const HOME_PAGE_ID = 13; // Remplace 13 par l'ID réel de la page d'accueil
|
const HOME_PAGE_ID = 13; // Remplace 13 par l'ID réel de la page d'accueil
|
||||||
const ETUDE_PAGE_ID = 272; // Remplace 13 par l'ID réel de la page d'accueil
|
const ETUDE_PAGE_ID = 272; // Remplace 13 par l'ID réel de la page d'accueil
|
||||||
|
|
||||||
|
const BUTTON_BASE_SX = Object.freeze({
|
||||||
|
borderRadius: 999,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0.75,
|
||||||
|
transition:
|
||||||
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_SIZE_SX = Object.freeze({
|
||||||
|
small: {
|
||||||
|
px: 2.1,
|
||||||
|
py: 0.55,
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
minHeight: 30,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
px: 2.8,
|
||||||
|
py: 0.8,
|
||||||
|
fontSize: "0.86rem",
|
||||||
|
minHeight: 36,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
px: 3.4,
|
||||||
|
py: 1,
|
||||||
|
fontSize: "0.94rem",
|
||||||
|
minHeight: 42,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_VARIANT_SX = Object.freeze({
|
||||||
|
primary: {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11, 26, 61, 0.62)",
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "rgba(255,255,255,0.72)",
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, rgba(49,83,151,0.35), rgba(123,192,255,0.35))",
|
||||||
|
boxShadow: "none",
|
||||||
|
border: "1px solid rgba(49,83,151,0.18)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
color: "rgba(255,255,255,0.92)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.24)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.24)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.26)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.95)",
|
||||||
|
borderColor: "rgba(49,83,151,0.46)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const composeButtonSx = (variant, size = "medium") => ({
|
||||||
|
...BUTTON_BASE_SX,
|
||||||
|
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
|
||||||
|
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
|
||||||
|
});
|
||||||
|
|
||||||
function GestionPagesACF() {
|
function GestionPagesACF() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [pages, setPages] = useState([]);
|
const [pages, setPages] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
// ✅ Vérifier l'authentification
|
// ✅ Vérifier l'authentification
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -49,7 +134,6 @@ function GestionPagesACF() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Exclure la page d'accueil
|
|
||||||
const filteredPages = response.filter(
|
const filteredPages = response.filter(
|
||||||
(page) => page.id !== HOME_PAGE_ID && page.id !== ETUDE_PAGE_ID,
|
(page) => page.id !== HOME_PAGE_ID && page.id !== ETUDE_PAGE_ID,
|
||||||
);
|
);
|
||||||
@ -65,88 +149,333 @@ function GestionPagesACF() {
|
|||||||
loadPages();
|
loadPages();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) return <Typography>Chargement...</Typography>;
|
const filteredPages = useMemo(() => {
|
||||||
if (error) return <Typography color="error">{error}</Typography>;
|
const term = searchTerm.trim().toLowerCase();
|
||||||
|
if (!term) {
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
return pages.filter((page) =>
|
||||||
|
page.title?.rendered?.toLowerCase?.().includes(term),
|
||||||
|
);
|
||||||
|
}, [pages, searchTerm]);
|
||||||
|
|
||||||
return (
|
if (loading) {
|
||||||
<Box sx={{ padding: "40px 20px", mt: 6 }}>
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
className="glass-dashboard"
|
||||||
sx={{
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
position: "relative",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
mb: 4,
|
justifyContent: "center",
|
||||||
mt: 5,
|
px: { xs: 2, md: 6 },
|
||||||
|
pt: { xs: 6, md: 8 },
|
||||||
|
pb: { xs: 8, md: 10 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Box className="glass-orb orb-1" />
|
||||||
startIcon={<ArrowBack />}
|
<Box className="glass-orb orb-2" />
|
||||||
variant="outlined"
|
<Box className="glass-orb orb-3" />
|
||||||
color="secondary"
|
<Stack spacing={2} alignItems="center" sx={{ position: "relative", zIndex: 1 }}>
|
||||||
onClick={() => navigate("/admin/dashboard")}
|
<CircularProgress color="inherit" />
|
||||||
>
|
<Typography variant="body1" sx={{ color: "#ffffff" }}>
|
||||||
Retour au Dashboard
|
Chargement des pages services…
|
||||||
</Button>
|
</Typography>
|
||||||
<Typography
|
</Stack>
|
||||||
variant="h4"
|
|
||||||
sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}
|
|
||||||
>
|
|
||||||
📄 Gestion des Pages Services
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
startIcon={<Add />}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() =>
|
|
||||||
window.open("https://it.sveitl.synology.me/", "_blank")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Ticket
|
|
||||||
</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
<TableContainer component={Paper}>
|
);
|
||||||
<Table>
|
}
|
||||||
<TableHead sx={{ backgroundColor: "#0e467f" }}>
|
|
||||||
<TableRow>
|
if (error) {
|
||||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>
|
return (
|
||||||
Titre
|
<Box
|
||||||
</TableCell>
|
className="glass-dashboard"
|
||||||
<TableCell
|
sx={{
|
||||||
sx={{ color: "white", fontWeight: "bold", textAlign: "center" }}
|
minHeight: "100vh",
|
||||||
|
position: "relative",
|
||||||
|
px: { xs: 2, md: 6 },
|
||||||
|
pt: { xs: 6, md: 8 },
|
||||||
|
pb: { xs: 8, md: 10 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="glass-orb orb-1" />
|
||||||
|
<Box className="glass-orb orb-2" />
|
||||||
|
<Box className="glass-orb orb-3" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
zIndex: 1,
|
||||||
|
maxWidth: 720,
|
||||||
|
mx: "auto",
|
||||||
|
mt: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: 3,
|
||||||
|
borderColor: "rgba(240,91,107,0.32) !important",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography color="error" sx={{ fontWeight: 600 }}>
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className="glass-dashboard"
|
||||||
|
sx={{
|
||||||
|
minHeight: "100vh",
|
||||||
|
position: "relative",
|
||||||
|
px: { xs: 2, md: 6 },
|
||||||
|
pt: { xs: 6, md: 8 },
|
||||||
|
pb: { xs: 8, md: 10 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box className="glass-orb orb-1" />
|
||||||
|
<Box className="glass-orb orb-2" />
|
||||||
|
<Box className="glass-orb orb-3" />
|
||||||
|
|
||||||
|
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
spacing={2}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBack />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => navigate("/admin/dashboard")}
|
||||||
|
sx={composeButtonSx("ghost")}
|
||||||
|
>
|
||||||
|
Retour au dashboard
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
sx={{
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Gestion des pages services
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Contenus ACF disponibles
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
maxWidth: 520,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Accédez rapidement aux pages service Octopus équipées de champs
|
||||||
|
ACF pour les mettre à jour en quelques clics.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
startIcon={<Add />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() =>
|
||||||
|
window.open("https://it.sveitl.synology.me/", "_blank")
|
||||||
|
}
|
||||||
|
sx={composeButtonSx("primary")}
|
||||||
|
>
|
||||||
|
Ouvrir un ticket
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Grid container spacing={2.5} sx={{ mb: 4 }}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 4, p: 2.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
Actions
|
Pages référencées
|
||||||
</TableCell>
|
</Typography>
|
||||||
</TableRow>
|
<Typography
|
||||||
</TableHead>
|
variant="h4"
|
||||||
<TableBody>
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
||||||
{pages.length > 0 ? (
|
>
|
||||||
pages.map((page) => (
|
{pages.length}
|
||||||
<TableRow key={page.id}>
|
</Typography>
|
||||||
<TableCell sx={{ fontWeight: "bold" }}>
|
<Typography
|
||||||
{page.title.rendered}
|
variant="body2"
|
||||||
</TableCell>
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
<TableCell sx={{ textAlign: "center" }}>
|
>
|
||||||
<Button
|
Ensemble des pages service disposant de champs ACF personnalisés.
|
||||||
startIcon={<Edit />}
|
</Typography>
|
||||||
variant="outlined"
|
</Card>
|
||||||
color="warning"
|
</Grid>
|
||||||
onClick={() => navigate(`/admin/edit-page/${page.id}`)}
|
<Grid item xs={12} md={6}>
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 4, p: 2.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Pages visibles
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
||||||
|
>
|
||||||
|
{filteredPages.length}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
|
>
|
||||||
|
Résultats actuels après filtrage par titre.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
mb: 4,
|
||||||
|
boxShadow: "0 28px 36px -28px rgba(12, 29, 74, 0.48)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
spacing={2}
|
||||||
|
alignItems={{ xs: "stretch", md: "center" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Rechercher une page
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
|
||||||
|
>
|
||||||
|
Filtrez par titre pour trouver une page service en un instant.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
label="Rechercher..."
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(event) => setSearchTerm(event.target.value)}
|
||||||
|
sx={{
|
||||||
|
maxWidth: 360,
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.94)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{filteredPages.length === 0 ? (
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 4, p: 3 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, color: "#0b1a3d" }}>
|
||||||
|
Aucune page trouvée
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 1 }}
|
||||||
|
>
|
||||||
|
Ajustez votre recherche ou vérifiez que la page dispose bien de
|
||||||
|
champs ACF activés.
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Grid container spacing={2.5}>
|
||||||
|
{filteredPages.map((page, index) => (
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs={12}
|
||||||
|
md={6}
|
||||||
|
lg={4}
|
||||||
|
key={page.id}
|
||||||
|
sx={{
|
||||||
|
mb:2,
|
||||||
|
mt: {
|
||||||
|
xs: index >= 1 ? 2 : 0,
|
||||||
|
md: index >= 3 ? 4 : 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className="glass-card"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
boxShadow: "0 32px 44px -30px rgba(12, 29, 74, 0.5)",
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={1.8}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", letterSpacing: 1, }}
|
||||||
>
|
>
|
||||||
Modifier
|
|
||||||
</Button>
|
</Typography>
|
||||||
</TableCell>
|
<Typography
|
||||||
</TableRow>
|
variant="h6"
|
||||||
))
|
sx={{ color: "#0b1a3d", fontWeight: 700, lineHeight: 1.4 }}
|
||||||
) : (
|
>
|
||||||
<TableRow>
|
{page.title.rendered}
|
||||||
<TableCell colSpan={2} sx={{ textAlign: "center", py: 2 }}>
|
</Typography>
|
||||||
Aucune page avec des champs ACF trouvée.
|
<Typography
|
||||||
</TableCell>
|
variant="body2"
|
||||||
</TableRow>
|
sx={{ color: "rgba(11, 26, 61, 0.68)", lineHeight: 1.5 }}
|
||||||
)}
|
>
|
||||||
</TableBody>
|
Cliquez sur "Modifier" pour ajuster les contenus
|
||||||
</Table>
|
de cette page service depuis l’éditeur ACF dédié.
|
||||||
</TableContainer>
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
startIcon={<Edit />}
|
||||||
|
variant="contained"
|
||||||
|
sx={composeButtonSx("outline", "small")}
|
||||||
|
onClick={() => navigate(`/admin/edit-page/${page.id}`)}
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,12 @@ import Faq from "../Faq";
|
|||||||
|
|
||||||
|
|
||||||
const Contact = () => {
|
const Contact = () => {
|
||||||
|
const CONTACT_API_URL =
|
||||||
|
import.meta.env.VITE_CONTACT_API_URL ||
|
||||||
|
(import.meta.env.DEV
|
||||||
|
? "http://localhost:3001/api/contact"
|
||||||
|
: "https://octopusdesign.fr/api/contact");
|
||||||
|
|
||||||
const [metaTitle, setMetaTitle] = useState("Contactez-nous");
|
const [metaTitle, setMetaTitle] = useState("Contactez-nous");
|
||||||
const [metaDescription, setMetaDescription] = useState(
|
const [metaDescription, setMetaDescription] = useState(
|
||||||
"Contactez notre équipe pour plus d'informations."
|
"Contactez notre équipe pour plus d'informations."
|
||||||
@ -35,7 +41,11 @@ const Contact = () => {
|
|||||||
});
|
});
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [successMessage, setSuccessMessage] = useState(false);
|
const [alertState, setAlertState] = useState({
|
||||||
|
open: false,
|
||||||
|
severity: "success",
|
||||||
|
message: "",
|
||||||
|
});
|
||||||
const [openLightbox, setOpenLightbox] = useState(false);
|
const [openLightbox, setOpenLightbox] = useState(false);
|
||||||
|
|
||||||
const [showFAQ, setShowFAQ] = useState(false); // État pour afficher/masquer la FAQ
|
const [showFAQ, setShowFAQ] = useState(false); // État pour afficher/masquer la FAQ
|
||||||
@ -96,7 +106,7 @@ const Contact = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://votre-site.com/wp-json/custom/v1/contact",
|
CONTACT_API_URL,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -107,7 +117,11 @@ const Contact = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
setSuccessMessage(true);
|
setAlertState({
|
||||||
|
open: true,
|
||||||
|
severity: "success",
|
||||||
|
message: "Merci ! Votre message a bien été envoyé.",
|
||||||
|
});
|
||||||
setFormData({
|
setFormData({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
@ -120,7 +134,13 @@ const Contact = () => {
|
|||||||
throw new Error(errorData.message || "Une erreur est survenue.");
|
throw new Error(errorData.message || "Une erreur est survenue.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
setAlertState({
|
||||||
|
open: true,
|
||||||
|
severity: "error",
|
||||||
|
message:
|
||||||
|
error.message ||
|
||||||
|
"Impossible d'envoyer votre message. Veuillez réessayer.",
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -135,6 +155,13 @@ const Contact = () => {
|
|||||||
setOpenLightbox(false);
|
setOpenLightbox(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseAlert = (_, reason) => {
|
||||||
|
if (reason === "clickaway") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAlertState((prev) => ({ ...prev, open: false }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
{/* SEO */}
|
{/* SEO */}
|
||||||
@ -490,12 +517,17 @@ const Contact = () => {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={successMessage}
|
open={alertState.open}
|
||||||
autoHideDuration={6000}
|
autoHideDuration={6000}
|
||||||
onClose={() => setSuccessMessage(false)}
|
onClose={handleCloseAlert}
|
||||||
|
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||||
>
|
>
|
||||||
<Alert onClose={() => setSuccessMessage(false)} severity="success">
|
<Alert
|
||||||
Message envoyé avec succès !
|
onClose={handleCloseAlert}
|
||||||
|
severity={alertState.severity}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{alertState.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -4,22 +4,113 @@ import { uploadImageFromUrl, createPost } from "../../wordpress";
|
|||||||
import { getToken } from "../../auth";
|
import { getToken } from "../../auth";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Container,
|
|
||||||
Typography,
|
Typography,
|
||||||
TextField,
|
TextField,
|
||||||
Button,
|
Button,
|
||||||
Paper,
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
Divider,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { ArrowBack, Publish } from "@mui/icons-material";
|
import { ArrowBack, Publish, Image as ImageIcon } from "@mui/icons-material";
|
||||||
import ImageUploaderCloudinary from "../ImageUploaderCloudinary";
|
import ImageUploaderCloudinary from "../ImageUploaderCloudinary";
|
||||||
import CloudinaryGallerySelector from "../CloudinaryGallerySelector";
|
import CloudinaryGallerySelector from "../CloudinaryGallerySelector";
|
||||||
import ReactQuill from "react-quill";
|
import ReactQuill from "react-quill";
|
||||||
import "react-quill/dist/quill.snow.css";
|
import "react-quill/dist/quill.snow.css";
|
||||||
|
import "../../assets/styleCours.css";
|
||||||
|
|
||||||
|
const BUTTON_BASE_SX = Object.freeze({
|
||||||
|
borderRadius: 999,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0.75,
|
||||||
|
transition:
|
||||||
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_SIZE_SX = Object.freeze({
|
||||||
|
small: {
|
||||||
|
px: 2.1,
|
||||||
|
py: 0.55,
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
minHeight: 30,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
px: 2.8,
|
||||||
|
py: 0.8,
|
||||||
|
fontSize: "0.86rem",
|
||||||
|
minHeight: 36,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
px: 3.4,
|
||||||
|
py: 1,
|
||||||
|
fontSize: "0.94rem",
|
||||||
|
minHeight: 42,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_VARIANT_SX = Object.freeze({
|
||||||
|
primary: {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.35)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11, 26, 61, 0.6)",
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "rgba(255,255,255,0.75)",
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, rgba(49,83,151,0.35), rgba(123,192,255,0.35))",
|
||||||
|
boxShadow: "none",
|
||||||
|
border: "1px solid rgba(49,83,151,0.18)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
color: "rgba(255,255,255,0.92)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.24)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.26)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.96)",
|
||||||
|
borderColor: "rgba(49,83,151,0.46)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const composeButtonSx = (variant, size = "medium") => ({
|
||||||
|
...BUTTON_BASE_SX,
|
||||||
|
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
|
||||||
|
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
|
||||||
|
});
|
||||||
|
|
||||||
function CreatePost() {
|
function CreatePost() {
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [imageUrl, setImageUrl] = useState(null);
|
const [imageUrl, setImageUrl] = useState(null);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Vérification de l'authentification
|
// Vérification de l'authentification
|
||||||
@ -29,126 +120,352 @@ function CreatePost() {
|
|||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const canPublish =
|
||||||
e.preventDefault();
|
title.trim().length > 0 && content.trim().length > 0 && Boolean(imageUrl);
|
||||||
|
|
||||||
|
const handleSubmit = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!canPublish || submitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
const imageId = await uploadImageFromUrl(imageUrl);
|
const imageId = await uploadImageFromUrl(imageUrl);
|
||||||
await createPost(title, content, imageId);
|
await createPost(title, content, imageId);
|
||||||
alert("✅ Article publié !");
|
alert("✅ Article publié !");
|
||||||
navigate("/admin/gestion-articles");
|
navigate("/admin/gestion-articles");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erreur création post :", error.response?.data || error.message);
|
console.error(
|
||||||
alert("❌ Erreur lors de la création du post.");
|
"❌ Erreur création post :",
|
||||||
}
|
error?.response?.data || error?.message || error
|
||||||
|
);
|
||||||
|
alert("❌ Erreur lors de la création du post.");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
className="glass-dashboard"
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
display: "flex",
|
position: "relative",
|
||||||
alignItems: "center",
|
px: { xs: 2, md: 6 },
|
||||||
justifyContent: "center",
|
pt: { xs: 6, md: 8 },
|
||||||
backgroundImage:
|
pb: { xs: 8, md: 10 },
|
||||||
"url('https://source.unsplash.com/1600x900/?office,writing')",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container maxWidth="sm">
|
<Box className="glass-orb orb-1" />
|
||||||
<Paper elevation={6} sx={{ padding: 4, borderRadius: 3 }}>
|
<Box className="glass-orb orb-2" />
|
||||||
|
<Box className="glass-orb orb-3" />
|
||||||
|
|
||||||
|
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
spacing={2}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<ArrowBack />}
|
startIcon={<ArrowBack />}
|
||||||
variant="outlined"
|
variant="contained"
|
||||||
color="secondary"
|
|
||||||
onClick={() => navigate("/admin/gestion-articles")}
|
onClick={() => navigate("/admin/gestion-articles")}
|
||||||
sx={{ mb: 2 }}
|
sx={composeButtonSx("ghost")}
|
||||||
>
|
>
|
||||||
Retour
|
Retour aux articles
|
||||||
</Button>
|
</Button>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h4"
|
variant="overline"
|
||||||
sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}
|
sx={{
|
||||||
>
|
letterSpacing: 2,
|
||||||
Créer un article
|
fontWeight: 700,
|
||||||
</Typography>
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
}}
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<TextField
|
|
||||||
label="Titre de l'article"
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
variant="outlined"
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box sx={{ height: 250, mb: 7 }}>
|
|
||||||
<ReactQuill
|
|
||||||
label="Contenu"
|
|
||||||
fullWidth
|
|
||||||
margin="normal"
|
|
||||||
variant="outlined"
|
|
||||||
multiline
|
|
||||||
rows={4}
|
|
||||||
value={content}
|
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
required
|
|
||||||
style={{height:"100%"}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Upload via Cloudinary */}
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
|
||||||
Importer une image depuis ton ordi :
|
|
||||||
</Typography>
|
|
||||||
<ImageUploaderCloudinary onUploadSuccess={(url) => setImageUrl(url)} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Sélection galerie Cloudinary */}
|
|
||||||
<Box sx={{ mt: 4 }}>
|
|
||||||
<Typography variant="body1" sx={{ fontWeight: "bold", mb: 1 }}>
|
|
||||||
Ou choisir une image déjà envoyée :
|
|
||||||
</Typography>
|
|
||||||
<CloudinaryGallerySelector onSelect={(url) => setImageUrl(url)} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Aperçu */}
|
|
||||||
{imageUrl && (
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Typography variant="body2">Aperçu de l’image :</Typography>
|
|
||||||
<img
|
|
||||||
src={imageUrl}
|
|
||||||
alt="Image sélectionnée"
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
maxHeight: 200,
|
|
||||||
objectFit: "cover",
|
|
||||||
borderRadius: 8,
|
|
||||||
marginTop: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
fullWidth
|
|
||||||
startIcon={<Publish />}
|
|
||||||
sx={{ mt: 4 }}
|
|
||||||
>
|
>
|
||||||
Publier l'article
|
Publication Octopus
|
||||||
</Button>
|
</Typography>
|
||||||
</form>
|
<Typography
|
||||||
</Paper>
|
variant="h3"
|
||||||
</Container>
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Rédiger un nouvel article
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
maxWidth: 520,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Structurez votre contenu, ajoutez une image mise en avant et
|
||||||
|
publiez directement sur le blog WordPress Octopus.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Grid container spacing={3.5}>
|
||||||
|
<Grid item xs={12} lg={7}>
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
boxShadow: "0 28px 36px -28px rgba(12, 29, 74, 0.5)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 0 }}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
alignItems="center"
|
||||||
|
sx={{ flexWrap: "wrap" }}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
label="Brouillon"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(49,83,151,0.12)",
|
||||||
|
color: "#315397",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label={
|
||||||
|
canPublish
|
||||||
|
? "Prêt pour publication"
|
||||||
|
: "Complétez les champs requis"
|
||||||
|
}
|
||||||
|
color={canPublish ? "success" : "warning"}
|
||||||
|
sx={{ fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Box component="form" onSubmit={handleSubmit} noValidate>
|
||||||
|
<TextField
|
||||||
|
label="Titre de l'article"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={title}
|
||||||
|
onChange={(event) => setTitle(event.target.value)}
|
||||||
|
required
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.94)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
borderRadius: 2,
|
||||||
|
overflow: "hidden",
|
||||||
|
border: "1px solid rgba(49,83,151,0.15)",
|
||||||
|
backgroundColor: "rgba(255,255,255,0.92)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReactQuill
|
||||||
|
value={content}
|
||||||
|
onChange={setContent}
|
||||||
|
style={{ height: 280 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack spacing={2.5}>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||||||
|
>
|
||||||
|
Importer une image
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
|
||||||
|
>
|
||||||
|
Ajoutez un visuel optimisé pour les réseaux sociaux
|
||||||
|
(format 1600x900 recommandé).
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<ImageUploaderCloudinary
|
||||||
|
onUploadSuccess={(url) => setImageUrl(url)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ borderColor: "rgba(11, 26, 61, 0.08)" }} />
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||||||
|
>
|
||||||
|
Sélectionner depuis la médiathèque
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 2 }}
|
||||||
|
>
|
||||||
|
Retrouver un visuel déjà téléversé dans Cloudinary.
|
||||||
|
</Typography>
|
||||||
|
<CloudinaryGallerySelector
|
||||||
|
onSelect={(url) => setImageUrl(url)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", sm: "row" }}
|
||||||
|
spacing={1.5}
|
||||||
|
sx={{ pt: 1 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Publish />}
|
||||||
|
disabled={!canPublish || submitting}
|
||||||
|
sx={{ ...composeButtonSx("primary"), minWidth: 200 }}
|
||||||
|
>
|
||||||
|
{submitting ? "Publication..." : "Publier l'article"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setTitle("");
|
||||||
|
setContent("");
|
||||||
|
setImageUrl(null);
|
||||||
|
}}
|
||||||
|
sx={composeButtonSx("outline")}
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} lg={5}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
boxShadow: "0 28px 36px -28px rgba(12, 29, 74, 0.45)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="flex-start">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: 2,
|
||||||
|
bgcolor: "rgba(123,192,255,0.22)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#315397",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ImageIcon />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d", mb: 1 }}
|
||||||
|
>
|
||||||
|
Conseils de rédaction
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.68)" }}
|
||||||
|
>
|
||||||
|
• Introduisez le sujet dans les 250 premiers caractères pour
|
||||||
|
optimiser le SEO.<br />
|
||||||
|
• Utilisez des sous-titres (<code><h2></code>) pour
|
||||||
|
clarifier la structure.<br />
|
||||||
|
• Ajoutez des appels à l’action vers vos formations Octopus.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className="glass-article-card"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: "hidden",
|
||||||
|
minHeight: 220,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: "rgba(255,255,255,0.9)",
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||||||
|
>
|
||||||
|
Aperçu visuel
|
||||||
|
</Typography>
|
||||||
|
{imageUrl ? (
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={imageUrl}
|
||||||
|
alt="Aperçu de l’image sélectionnée"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
borderRadius: 3,
|
||||||
|
height: 200,
|
||||||
|
objectFit: "cover",
|
||||||
|
boxShadow: "0 20px 28px -24px rgba(12, 29, 74, 0.48)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
borderRadius: 3,
|
||||||
|
border: "1px dashed rgba(49,83,151,0.35)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "rgba(49,83,151,0.6)",
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aucun visuel sélectionné pour le moment
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
|
||||||
|
>
|
||||||
|
Ce visuel sera utilisé comme image de couverture sur la liste
|
||||||
|
des articles et les réseaux sociaux.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,18 +7,119 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
IconButton,
|
|
||||||
Button,
|
Button,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
Divider,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import LogoutIcon from "@mui/icons-material/Logout"; // Icône de déconnexion
|
import LogoutIcon from "@mui/icons-material/Logout";
|
||||||
import ArticleIcon from "@mui/icons-material/Article";
|
import ArticleIcon from "@mui/icons-material/Article";
|
||||||
import DashboardIcon from "@mui/icons-material/Dashboard";
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
import HomeIcon from "@mui/icons-material/Home"; // Icône pour la gestion page d'accueil
|
|
||||||
import BuildIcon from "@mui/icons-material/Build";
|
import BuildIcon from "@mui/icons-material/Build";
|
||||||
|
import SchoolIcon from "@mui/icons-material/School";
|
||||||
|
import InsightsIcon from "@mui/icons-material/Insights";
|
||||||
|
import SettingsSuggestIcon from "@mui/icons-material/SettingsSuggest";
|
||||||
|
import AutoStoriesIcon from "@mui/icons-material/AutoStories";
|
||||||
|
import PublicIcon from "@mui/icons-material/Public";
|
||||||
|
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
|
||||||
|
|
||||||
|
const BUTTON_BASE_SX = Object.freeze({
|
||||||
|
borderRadius: 999,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0.75,
|
||||||
|
transition:
|
||||||
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_SIZE_SX = Object.freeze({
|
||||||
|
small: {
|
||||||
|
px: 2.1,
|
||||||
|
py: 0.55,
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
minHeight: 30,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
px: 2.8,
|
||||||
|
py: 0.8,
|
||||||
|
fontSize: "0.86rem",
|
||||||
|
minHeight: 36,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
px: 3.4,
|
||||||
|
py: 1,
|
||||||
|
fontSize: "0.94rem",
|
||||||
|
minHeight: 42,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_VARIANT_SX = Object.freeze({
|
||||||
|
primary: {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.35)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11, 26, 61, 0.6)",
|
||||||
|
},
|
||||||
|
"&.Mui-disabled": {
|
||||||
|
color: "rgba(255,255,255,0.72)",
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, rgba(49,83,151,0.35), rgba(123,192,255,0.35))",
|
||||||
|
boxShadow: "none",
|
||||||
|
border: "1px solid rgba(49,83,151,0.18)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
color: "rgba(255,255,255,0.92)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.24)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.32)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.96)",
|
||||||
|
borderColor: "rgba(49,83,151,0.48)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subtle: {
|
||||||
|
background: "rgba(49,83,151,0.08)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.12)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(49,83,151,0.14)",
|
||||||
|
borderColor: "rgba(49,83,151,0.24)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const composeButtonSx = (variant, size = "medium") => ({
|
||||||
|
...BUTTON_BASE_SX,
|
||||||
|
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
|
||||||
|
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
|
||||||
|
});
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@ -31,240 +132,390 @@ function Dashboard() {
|
|||||||
navigate("/admin/login"); // Redirige vers la page de connexion
|
navigate("/admin/login"); // Redirige vers la page de connexion
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NAV_ITEMS = [
|
||||||
|
{
|
||||||
|
title: "Page d’accueil",
|
||||||
|
description: "Mettez à jour les blocs héros, témoignages et sections clés.",
|
||||||
|
navigateTo: "/admin/Gestion-Page-Accueil",
|
||||||
|
icon: <HomeIcon fontSize="inherit" />,
|
||||||
|
accent: "rgba(123,192,255,0.35)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tools studio",
|
||||||
|
description: "Centralisez vos outils internes et automatisez les tâches.",
|
||||||
|
navigateTo: "/admin/tools",
|
||||||
|
icon: <BuildIcon fontSize="inherit" />,
|
||||||
|
accent: "rgba(255,214,150,0.32)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Articles",
|
||||||
|
description: "Rédigez, programmez et optimisez les publications du blog.",
|
||||||
|
navigateTo: "/admin/gestion-articles",
|
||||||
|
icon: <ArticleIcon fontSize="inherit" />,
|
||||||
|
accent: "rgba(255,172,236,0.28)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pages services",
|
||||||
|
description: "Actualisez les contenus ACF pour chaque offre Octopus.",
|
||||||
|
navigateTo: "/admin/pages-acf",
|
||||||
|
icon: <SettingsSuggestIcon fontSize="inherit" />,
|
||||||
|
accent: "rgba(158,241,209,0.32)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Bureau d’étude",
|
||||||
|
description: "Administrez les informations du pôle bureau d’étude.",
|
||||||
|
navigateTo: "/admin/bureau-etude-acf",
|
||||||
|
icon: <InsightsIcon fontSize="inherit" />,
|
||||||
|
accent: "rgba(255,199,186,0.34)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Cours & formations",
|
||||||
|
description: "Gérez le catalogue Moodle et vos ressources pédagogiques.",
|
||||||
|
navigateTo: "/admin/liste-cours",
|
||||||
|
icon: <SchoolIcon fontSize="inherit" />,
|
||||||
|
accent: "rgba(178,219,255,0.34)",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const QUICK_ACTIONS = [
|
||||||
|
{
|
||||||
|
label: "Nouvel article",
|
||||||
|
hint: "Créez un contenu en un clic",
|
||||||
|
onClick: () => navigate("/admin/gestion-articles"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Synchroniser Moodle",
|
||||||
|
hint: "Actualisez vos cours et stats",
|
||||||
|
onClick: () => navigate("/admin/liste-cours"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Voir le site",
|
||||||
|
hint: "Ouvrir octopusdesign.fr",
|
||||||
|
onClick: () => window.open("https://www.octopusdesign.fr", "_blank"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const KPI_CARDS = [
|
||||||
|
{
|
||||||
|
label: "Articles publiés",
|
||||||
|
value: "128",
|
||||||
|
delta: "+4 ce mois-ci",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Cours actifs",
|
||||||
|
value: "36",
|
||||||
|
delta: "8 en mise à jour",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pages services",
|
||||||
|
value: "12",
|
||||||
|
delta: "2 révisions en cours",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
className="glass-dashboard"
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
backgroundImage:
|
position: "relative",
|
||||||
"url('https://source.unsplash.com/1600x900/?technology,office')",
|
px: { xs: 2, md: 6 },
|
||||||
backgroundSize: "cover",
|
pt: { xs: 6, md: 8 },
|
||||||
backgroundPosition: "center",
|
pb: { xs: 8, md: 10 },
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
padding: "20px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box className="glass-orb orb-1" />
|
||||||
sx={{
|
<Box className="glass-orb orb-2" />
|
||||||
width: "90%",
|
<Box className="glass-orb orb-3" />
|
||||||
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 */}
|
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1240, mx: "auto" }}>
|
||||||
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 4, mt: 4 }}>
|
<Stack
|
||||||
<DashboardIcon sx={{ fontSize: 40, color: "#0e467f", mr: 1 }} />
|
direction={{ xs: "column", md: "row" }}
|
||||||
Tableau de Bord
|
justifyContent="space-between"
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
spacing={2}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
sx={{
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Octopus Admin
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tableau de bord centralisé
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
maxWidth: 540,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Accédez à l’ensemble des outils de gestion : contenus éditoriaux,
|
||||||
|
pages services, ressources pédagogiques et automatisations.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<LogoutIcon />}
|
||||||
|
onClick={handleLogout}
|
||||||
|
sx={composeButtonSx("ghost")}
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
className="glass-panel hero-panel"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 3, md: 4 },
|
||||||
|
mb: 4,
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
spacing={{ xs: 3, md: 4 }}
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{
|
||||||
|
color: "#0b1a3d",
|
||||||
|
fontWeight: 700,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Priorités du jour
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.7)", lineHeight: 1.6 }}
|
||||||
|
>
|
||||||
|
Suivez la progression des mises en ligne, synchronisez Moodle
|
||||||
|
et gardez un œil sur vos contenus essentiels.
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", sm: "row" }}
|
||||||
|
spacing={1}
|
||||||
|
sx={{ mt: 2, flexWrap: "wrap" }}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
icon={<AutoStoriesIcon fontSize="small" />}
|
||||||
|
label="Contenus pédagogiques à jour"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(123,192,255,0.16)",
|
||||||
|
color: "#0b1a3d",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
icon={<PublicIcon fontSize="small" />}
|
||||||
|
label="Octopusdesign.fr en production"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(255,214,150,0.18)",
|
||||||
|
color: "#0b1a3d",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<Divider
|
||||||
|
orientation="vertical"
|
||||||
|
flexItem
|
||||||
|
sx={{
|
||||||
|
display: { xs: "none", md: "block" },
|
||||||
|
borderColor: "rgba(11, 26, 61, 0.08)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexBasis: { xs: "100%", md: "40%" },
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Actions rapides
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={1.2}>
|
||||||
|
{QUICK_ACTIONS.map((action) => (
|
||||||
|
<Button
|
||||||
|
key={action.label}
|
||||||
|
variant="contained"
|
||||||
|
onClick={action.onClick}
|
||||||
|
endIcon={<ArrowForwardIcon fontSize="small" />}
|
||||||
|
sx={{
|
||||||
|
...composeButtonSx("subtle", "large"),
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "flex-start" }}>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 700 }}>
|
||||||
|
{action.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(49,83,151,0.7)", fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
{action.hint}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={2.5} sx={{ mb: 4 }}>
|
||||||
|
{KPI_CARDS.map((card) => (
|
||||||
|
<Grid item xs={12} sm={4} key={card.label}>
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 3,
|
||||||
|
p: 2.5,
|
||||||
|
boxShadow: "0 28px 34px -30px rgba(12, 29, 74, 0.52)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.6)", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
{card.label.toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
color: "#0b1a3d",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{card.value}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
|
>
|
||||||
|
{card.delta}
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ color: "#ffffff", fontWeight: 700, mb: 2 }}
|
||||||
|
>
|
||||||
|
Espaces de gestion
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{/* Cartes du Dashboard */}
|
<Grid container spacing={2.5}>
|
||||||
<Grid container spacing={3} justifyContent="center">
|
{NAV_ITEMS.map((item) => (
|
||||||
{/* ✅ Gestion Page d'Accueil - Première carte */}
|
<Grid item xs={12} md={6} key={item.title}>
|
||||||
<Grid item xs={12} sm={6}>
|
<Card
|
||||||
<Card
|
onClick={() => navigate(item.navigateTo)}
|
||||||
onClick={() => navigate("/admin/Gestion-Page-Accueil")}
|
className="glass-subcard"
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
textAlign: "center",
|
borderRadius: 3,
|
||||||
padding: 2,
|
overflow: "hidden",
|
||||||
transition: "0.3s",
|
position: "relative",
|
||||||
"&:hover": {
|
transition:
|
||||||
backgroundColor: "#0e467f",
|
"transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease",
|
||||||
color: "#ffffff",
|
"&::before": {
|
||||||
transform: "scale(1.05)",
|
content: '""',
|
||||||
boxShadow: 8,
|
position: "absolute",
|
||||||
},
|
inset: 0,
|
||||||
"&:hover svg": {
|
borderRadius: "inherit",
|
||||||
fill: "#ffffff",
|
background: `radial-gradient(360px at 18% 24%, ${item.accent}, transparent 72%)`,
|
||||||
},
|
opacity: 0.9,
|
||||||
}}
|
},
|
||||||
>
|
"&:hover": {
|
||||||
<CardContent>
|
transform: "translateY(-8px)",
|
||||||
<IconButton sx={{ fontSize: 40, color: "#0e467f" }}>
|
borderColor: "rgba(49,83,151,0.35)",
|
||||||
<HomeIcon fontSize="inherit" />
|
boxShadow: "0 38px 40px -30px rgba(12, 29, 74, 0.6)",
|
||||||
</IconButton>
|
},
|
||||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
}}
|
||||||
Gestion Page d'Accueil
|
>
|
||||||
</Typography>
|
<CardContent
|
||||||
</CardContent>
|
sx={{
|
||||||
</Card>
|
position: "relative",
|
||||||
</Grid>
|
zIndex: 1,
|
||||||
<Grid item xs={12} sm={6}>
|
display: "flex",
|
||||||
<Card
|
flexDirection: "column",
|
||||||
onClick={() => navigate("/admin/tools")}
|
gap: 1.5,
|
||||||
sx={{
|
p: { xs: 3, md: 3.5 },
|
||||||
cursor: "pointer",
|
}}
|
||||||
textAlign: "center",
|
>
|
||||||
padding: 2,
|
<Box
|
||||||
transition: "0.3s",
|
sx={{
|
||||||
"&:hover": {
|
width: 52,
|
||||||
backgroundColor: "#0e467f",
|
height: 52,
|
||||||
color: "#ffffff",
|
borderRadius: 2,
|
||||||
transform: "scale(1.05)",
|
backgroundColor: "rgba(255,255,255,0.75)",
|
||||||
boxShadow: 8,
|
display: "flex",
|
||||||
},
|
alignItems: "center",
|
||||||
"&:hover svg": {
|
justifyContent: "center",
|
||||||
fill: "#ffffff",
|
color: "#315397",
|
||||||
},
|
boxShadow: "0 16px 26px -20px rgba(11, 26, 61, 0.5)",
|
||||||
}}
|
fontSize: 28,
|
||||||
>
|
}}
|
||||||
<CardContent>
|
>
|
||||||
<IconButton sx={{ fontSize: 40, color: "#0e467f" }}>
|
{item.icon}
|
||||||
<BuildIcon fontSize="inherit" />
|
</Box>
|
||||||
</IconButton>
|
<Typography
|
||||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
variant="h6"
|
||||||
Tools
|
sx={{ color: "#0b1a3d", fontWeight: 700 }}
|
||||||
</Typography>
|
>
|
||||||
</CardContent>
|
{item.title}
|
||||||
</Card>
|
</Typography>
|
||||||
</Grid>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
{/* ✅ Gérer les Articles - Deuxième carte */}
|
sx={{ color: "rgba(11, 26, 61, 0.68)", lineHeight: 1.6 }}
|
||||||
<Grid item xs={12} sm={6}>
|
>
|
||||||
<Card
|
{item.description}
|
||||||
onClick={() => navigate("/admin/gestion-articles")}
|
</Typography>
|
||||||
sx={{
|
<Button
|
||||||
cursor: "pointer",
|
variant="contained"
|
||||||
textAlign: "center",
|
onClick={() => navigate(item.navigateTo)}
|
||||||
padding: 2,
|
endIcon={<ArrowForwardIcon fontSize="small" />}
|
||||||
transition: "0.3s",
|
sx={{
|
||||||
"&:hover": {
|
...composeButtonSx("primary", "small"),
|
||||||
backgroundColor: "#0e467f",
|
alignSelf: "flex-start",
|
||||||
color: "#ffffff",
|
}}
|
||||||
transform: "scale(1.05)",
|
>
|
||||||
boxShadow: 8,
|
Ouvrir
|
||||||
},
|
</Button>
|
||||||
"&:hover svg": {
|
</CardContent>
|
||||||
fill: "#ffffff",
|
</Card>
|
||||||
},
|
</Grid>
|
||||||
}}
|
))}
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* ✅ Gérer les Pages ACF - Nouvelle carte */}
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Card
|
|
||||||
onClick={() => navigate("/admin/pages-acf")}
|
|
||||||
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 Pages Services
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* ✅ Gérer les Pages ACF - Nouvelle carte */}
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Card
|
|
||||||
onClick={() => navigate("/admin/bureau-etude-acf")}
|
|
||||||
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 la page bureau d'étude
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
{/* ✅ Gérer les Cours - Nouvelle carte */}
|
|
||||||
<Grid item xs={12} sm={6}>
|
|
||||||
<Card
|
|
||||||
onClick={() => navigate("/admin/liste-cours")}
|
|
||||||
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" }}>
|
|
||||||
<DashboardIcon fontSize="inherit" />
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
|
||||||
Cours
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
{/* ✅ Gérer les Cours - Nouvelle carte */}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,18 +1,118 @@
|
|||||||
// ✅ Importations nécessaires
|
// ✅ Importations nécessaires
|
||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Box, Typography, Button, Table, TableBody, TableCell,
|
Box,
|
||||||
TableContainer, TableHead, TableRow, Paper, TextField, Avatar
|
Typography,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardMedia,
|
||||||
|
TextField,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Add, ArrowBack, Edit, Delete } from "@mui/icons-material";
|
import { Add, ArrowBack, Edit, Delete } from "@mui/icons-material";
|
||||||
import api from "../../api";
|
import api from "../../api";
|
||||||
import { getToken } from "../../auth";
|
import { getToken } from "../../auth";
|
||||||
import { deletePost } from "../../wordpress";
|
import { deletePost } from "../../wordpress";
|
||||||
|
import "../../assets/styleCours.css";
|
||||||
|
|
||||||
|
const BUTTON_BASE_SX = Object.freeze({
|
||||||
|
borderRadius: 999,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 0.75,
|
||||||
|
transition:
|
||||||
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
|
"&:hover": {
|
||||||
|
transform: "translateY(-1px)",
|
||||||
|
},
|
||||||
|
"&:active": {
|
||||||
|
transform: "translateY(0)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_SIZE_SX = Object.freeze({
|
||||||
|
small: {
|
||||||
|
px: 2.1,
|
||||||
|
py: 0.55,
|
||||||
|
fontSize: "0.78rem",
|
||||||
|
minHeight: 30,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
px: 2.8,
|
||||||
|
py: 0.8,
|
||||||
|
fontSize: "0.86rem",
|
||||||
|
minHeight: 36,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
px: 3.4,
|
||||||
|
py: 1,
|
||||||
|
fontSize: "0.94rem",
|
||||||
|
minHeight: 42,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BUTTON_VARIANT_SX = Object.freeze({
|
||||||
|
primary: {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.35)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11, 26, 61, 0.6)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.26)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.96)",
|
||||||
|
borderColor: "rgba(49,83,151,0.46)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ghost: {
|
||||||
|
background: "rgba(255,255,255,0.16)",
|
||||||
|
color: "rgba(255,255,255,0.92)",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.24)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
background: "linear-gradient(135deg, #f05b6b, #ff8a80)",
|
||||||
|
color: "#3a0a0f",
|
||||||
|
border: "1px solid rgba(240,91,107,0.35)",
|
||||||
|
boxShadow: "0 18px 26px -18px rgba(105, 21, 33, 0.45)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #e34659, #ff7575)",
|
||||||
|
boxShadow: "0 22px 30px -18px rgba(105, 21, 33, 0.52)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const composeButtonSx = (variant, size = "medium") => ({
|
||||||
|
...BUTTON_BASE_SX,
|
||||||
|
...(BUTTON_SIZE_SX[size] || BUTTON_SIZE_SX.medium),
|
||||||
|
...(BUTTON_VARIANT_SX[variant] || BUTTON_VARIANT_SX.primary),
|
||||||
|
});
|
||||||
|
|
||||||
const GestionArticles = () => {
|
const GestionArticles = () => {
|
||||||
const [posts, setPosts] = useState([]);
|
const [posts, setPosts] = useState([]);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// ✅ Vérifier si l'utilisateur est connecté
|
// ✅ Vérifier si l'utilisateur est connecté
|
||||||
@ -26,7 +126,10 @@ const GestionArticles = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPosts = async () => {
|
const fetchPosts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get("wp/v2/posts?_fields=id,title,excerpt,featured_media");
|
setLoading(true);
|
||||||
|
const response = await api.get(
|
||||||
|
"wp/v2/posts?_fields=id,title,excerpt,featured_media,date,status"
|
||||||
|
);
|
||||||
const postsData = response.data;
|
const postsData = response.data;
|
||||||
|
|
||||||
const postsWithImages = await Promise.all(
|
const postsWithImages = await Promise.all(
|
||||||
@ -47,6 +150,8 @@ const GestionArticles = () => {
|
|||||||
setPosts(postsWithImages);
|
setPosts(postsWithImages);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erreur chargement des articles :", error);
|
console.error("❌ Erreur chargement des articles :", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,112 +159,408 @@ const GestionArticles = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ✅ Supprimer un article et son image associée
|
// ✅ Supprimer un article et son image associée
|
||||||
const handleDeletePost = async (postId, imageId) => {
|
const stripHtml = (value = "") =>
|
||||||
if (window.confirm("Es-tu sûr de vouloir supprimer cet article ?")) {
|
value.replace(/(<([^>]+)>)/gi, "").replace(/ /gi, " ").trim();
|
||||||
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 previewExcerpt = (value = "", limit = 160) => {
|
||||||
const filteredPosts = posts.filter((post) =>
|
const clean = stripHtml(value);
|
||||||
post.title.rendered.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
return clean.length > limit ? `${clean.slice(0, limit)}…` : clean;
|
||||||
post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").replace(/ /g, " ").toLowerCase().includes(searchTerm.toLowerCase())
|
};
|
||||||
);
|
|
||||||
|
const formatDate = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return "Date inconnue";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat("fr-FR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(new Date(value));
|
||||||
|
} catch {
|
||||||
|
return "Date inconnue";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeletePost = async (postId, imageId) => {
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
"Supprimer cet article et son visuel associé ?"
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await deletePost(postId, imageId);
|
||||||
|
setPosts((prev) => prev.filter((post) => post.id !== postId));
|
||||||
|
alert("✅ Article supprimé avec succès !");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Erreur suppression article :", error);
|
||||||
|
alert("⚠ Erreur : impossible de supprimer l'article.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = () => navigate("/admin/create-post");
|
||||||
|
const handleBack = () => navigate("/admin/dashboard");
|
||||||
|
const handleEdit = (postId) => navigate(`/admin/edit-post/${postId}`);
|
||||||
|
|
||||||
|
const filteredPosts = useMemo(() => {
|
||||||
|
const term = searchTerm.trim().toLowerCase();
|
||||||
|
if (!term) {
|
||||||
|
return posts;
|
||||||
|
}
|
||||||
|
return posts.filter((post) => {
|
||||||
|
const title = post.title?.rendered?.toLowerCase?.() || "";
|
||||||
|
const excerpt = stripHtml(post.excerpt?.rendered || "").toLowerCase();
|
||||||
|
return title.includes(term) || excerpt.includes(term);
|
||||||
|
});
|
||||||
|
}, [posts, searchTerm]);
|
||||||
|
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
const total = posts.length;
|
||||||
|
const withImage = posts.reduce(
|
||||||
|
(acc, post) => (post.image ? acc + 1 : acc),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const latest = posts.reduce((current, candidate) => {
|
||||||
|
if (!candidate?.date) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
if (!current) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
return new Date(candidate.date) > new Date(current.date)
|
||||||
|
? candidate
|
||||||
|
: current;
|
||||||
|
}, null);
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
withImage,
|
||||||
|
withoutImage: total - withImage,
|
||||||
|
latestLabel: latest?.title?.rendered || "Aucun article",
|
||||||
|
latestDate: latest?.date ? formatDate(latest.date) : "—",
|
||||||
|
};
|
||||||
|
}, [posts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ padding: "40px 20px" }}>
|
<Box
|
||||||
{/* ✅ En-tête avec retour et création */}
|
className="glass-dashboard"
|
||||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 4, mt:5 }}>
|
sx={{
|
||||||
<Button startIcon={<ArrowBack />} variant="outlined" color="secondary" onClick={() => navigate("/admin/dashboard")}>
|
minHeight: "100vh",
|
||||||
Retour au Dashboard
|
position: "relative",
|
||||||
</Button>
|
px: { xs: 2, md: 6 },
|
||||||
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", flexGrow: 1 }}>
|
pt: { xs: 6, md: 8 },
|
||||||
Gestion des Articles
|
pb: { xs: 8, md: 10 },
|
||||||
</Typography>
|
}}
|
||||||
<Button startIcon={<Add />} variant="contained" color="primary" onClick={() => navigate("/admin/create-post")}>
|
>
|
||||||
Créer un article
|
<Box className="glass-orb orb-1" />
|
||||||
</Button>
|
<Box className="glass-orb orb-2" />
|
||||||
|
<Box className="glass-orb orb-3" />
|
||||||
|
|
||||||
|
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1240, mx: "auto" }}>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
spacing={2}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBack />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleBack}
|
||||||
|
sx={{ ...composeButtonSx("ghost") }}
|
||||||
|
>
|
||||||
|
Retour
|
||||||
|
</Button>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
sx={{
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Contenu éditorial
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Gestion des articles
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
maxWidth: 520,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Consultez les publications WordPress, ajustez vos contenus et
|
||||||
|
maintenez un flux éditorial cohérent pour Octopus.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
startIcon={<Add />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleCreate}
|
||||||
|
sx={{ ...composeButtonSx("primary") }}
|
||||||
|
>
|
||||||
|
Nouvel article
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Grid container spacing={2.5} sx={{ mb: 4 }}>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 3, p: 2.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.6)", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Total articles
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
||||||
|
>
|
||||||
|
{stats.total}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
|
>
|
||||||
|
{filteredPosts.length} résultat(s) après filtre
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 3, p: 2.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.6)", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Articles illustrés
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
||||||
|
>
|
||||||
|
{stats.withImage}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
|
>
|
||||||
|
{stats.withoutImage} sans visuel associé
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={4}>
|
||||||
|
<Card className="glass-subcard" sx={{ borderRadius: 3, p: 2.5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.6)", fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
Dernière publication
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 700, lineHeight: 1.4 }}
|
||||||
|
>
|
||||||
|
{stats.latestLabel}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||||
|
>
|
||||||
|
{stats.latestDate}
|
||||||
|
</Typography>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
mb: 4,
|
||||||
|
boxShadow: "0 24px 36px -28px rgba(12, 29, 74, 0.45)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
spacing={2}
|
||||||
|
alignItems={{ xs: "stretch", md: "center" }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
Rechercher un article
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)" }}
|
||||||
|
>
|
||||||
|
Filtrez par titre ou résumé pour retrouver un contenu en quelques
|
||||||
|
secondes.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
label="Rechercher un article..."
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(event) => setSearchTerm(event.target.value)}
|
||||||
|
sx={{
|
||||||
|
maxWidth: 380,
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.92)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<Box sx={{ textAlign: "center", py: 6 }}>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</Box>
|
||||||
|
) : filteredPosts.length === 0 ? (
|
||||||
|
<Card
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: 4,
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#0b1a3d",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
Aucun article trouvé
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: "rgba(11, 26, 61, 0.65)" }}>
|
||||||
|
Ajustez votre recherche ou créez un nouvel article pour alimenter le blog.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
startIcon={<Add />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleCreate}
|
||||||
|
sx={{ ...composeButtonSx("primary"), mt: 3 }}
|
||||||
|
>
|
||||||
|
Créer un article
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Grid container spacing={2.5}>
|
||||||
|
{filteredPosts.map((post) => (
|
||||||
|
<Grid item xs={12} md={6} key={post.id}>
|
||||||
|
<Card
|
||||||
|
className="glass-article-card"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
boxShadow: "0 32px 40px -28px rgba(12, 29, 74, 0.5)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardMedia
|
||||||
|
component="div"
|
||||||
|
image={
|
||||||
|
post.image ||
|
||||||
|
"https://images.unsplash.com/photo-1524995997946-a1c2e315a42f?auto=format&fit=crop&w=1200&q=80"
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
height: 200,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 1.5,
|
||||||
|
p: { xs: 3, md: 3.5 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={`ID ${post.id}`}
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(49,83,151,0.12)",
|
||||||
|
color: "#315397",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip title={stats.latestLabel === post.title.rendered ? "Dernière publication" : ""}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={formatDate(post.date)}
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(255,255,255,0.72)",
|
||||||
|
color: "#0b1a3d",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Stack>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 700, lineHeight: 1.4 }}
|
||||||
|
>
|
||||||
|
{post.title.rendered}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.68)", lineHeight: 1.6 }}
|
||||||
|
>
|
||||||
|
{previewExcerpt(post.excerpt?.rendered)}
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
direction={{ xs: "column", sm: "row" }}
|
||||||
|
spacing={1}
|
||||||
|
sx={{ mt: "auto" }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<Edit />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => handleEdit(post.id)}
|
||||||
|
sx={{ ...composeButtonSx("outline", "small") }}
|
||||||
|
>
|
||||||
|
Modifier
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<Delete />}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => handleDeletePost(post.id, post.imageId)}
|
||||||
|
sx={{ ...composeButtonSx("danger", "small") }}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* ✅ Recherche */}
|
|
||||||
<TextField
|
|
||||||
label="Rechercher un article..."
|
|
||||||
variant="outlined"
|
|
||||||
fullWidth
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
sx={{ mb: 3 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* ✅ Tableau des articles */}
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table>
|
|
||||||
<TableHead sx={{ backgroundColor: "#0e467f" }}>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Image</TableCell>
|
|
||||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Titre</TableCell>
|
|
||||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Résumé</TableCell>
|
|
||||||
<TableCell sx={{ color: "white", fontWeight: "bold", textAlign: "center" }}>Actions</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{filteredPosts.length > 0 ? (
|
|
||||||
filteredPosts.map((post) => (
|
|
||||||
<TableRow key={post.id}>
|
|
||||||
{/* ✅ Image en vedette */}
|
|
||||||
<TableCell>
|
|
||||||
<Avatar
|
|
||||||
src={post.image || "https://via.placeholder.com/100"}
|
|
||||||
variant="rounded"
|
|
||||||
sx={{ width: 80, height: 80 }}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
{/* ✅ Titre de l'article */}
|
|
||||||
<TableCell sx={{ fontWeight: "bold" }}>{post.title.rendered}</TableCell>
|
|
||||||
{/* ✅ Résumé avec 100 caractères max */}
|
|
||||||
<TableCell>
|
|
||||||
{post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").replace(/ /g, " ").substring(0, 100)}...
|
|
||||||
</TableCell>
|
|
||||||
{/* ✅ Boutons Modifier et Supprimer */}
|
|
||||||
<TableCell sx={{ textAlign: "center" }}>
|
|
||||||
<Button startIcon={<Edit />}
|
|
||||||
variant="outlined"
|
|
||||||
color="warning"
|
|
||||||
sx={{ mr: 1 }}
|
|
||||||
onClick={() => navigate(`/admin/edit-post/${post.id}`)}
|
|
||||||
>
|
|
||||||
Modifier
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<Delete />}
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={() => handleDeletePost(post.id, post.imageId)}
|
|
||||||
>
|
|
||||||
Supprimer
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={4} sx={{ textAlign: "center", py: 2 }}>
|
|
||||||
Aucun article trouvé.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GestionArticles;
|
export default GestionArticles;
|
||||||
|
|||||||
@ -4,29 +4,35 @@ import { useNavigate } from "react-router-dom"; // Import de useNavigate pour la
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Grid,
|
||||||
CardContent,
|
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
Container,
|
Stack,
|
||||||
Avatar,
|
CircularProgress,
|
||||||
|
Chip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||||
|
import "../../assets/styleCours.css";
|
||||||
|
|
||||||
function TestLogin() {
|
function TestLogin() {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [appPassword, setAppPassword] = useState("");
|
const [appPassword, setAppPassword] = useState("");
|
||||||
const [token, setToken] = useState(getToken());
|
const [token, setToken] = useState(getToken());
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
const navigate = useNavigate(); // Hook pour la navigation
|
const navigate = useNavigate(); // Hook pour la navigation
|
||||||
|
|
||||||
const handleLogin = async (e) => {
|
const handleLogin = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
const newToken = await loginWithAppPassword(username, appPassword);
|
const newToken = await loginWithAppPassword(username, appPassword);
|
||||||
if (newToken) {
|
if (newToken) {
|
||||||
setToken(newToken);
|
setToken(newToken);
|
||||||
} else {
|
} else {
|
||||||
alert("Échec de la connexion !");
|
setError("Échec de la connexion. Vérifiez vos identifiants.");
|
||||||
}
|
}
|
||||||
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
@ -34,92 +40,232 @@ function TestLogin() {
|
|||||||
setToken(null);
|
setToken(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const composeButtonSx = (variant = "primary") => ({
|
||||||
|
borderRadius: 999,
|
||||||
|
textTransform: "none",
|
||||||
|
fontWeight: 600,
|
||||||
|
letterSpacing: 0.3,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 0.75,
|
||||||
|
transition:
|
||||||
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
|
px: 3,
|
||||||
|
py: 0.85,
|
||||||
|
minHeight: 42,
|
||||||
|
...(variant === "primary"
|
||||||
|
? {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11,26,61,0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11,26,61,0.62)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.26)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.95)",
|
||||||
|
borderColor: "rgba(49,83,151,0.46)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11,26,61,0.4)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
className="glass-dashboard"
|
||||||
sx={{
|
sx={{
|
||||||
backgroundImage: "url('https://source.unsplash.com/random/1600x900?technology')",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
display: "flex",
|
position: "relative",
|
||||||
alignItems: "center",
|
px: { xs: 2, md: 6 },
|
||||||
justifyContent: "center",
|
pt: { xs: 8, md: 12 },
|
||||||
|
pb: { xs: 8, md: 10 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container maxWidth="xs">
|
<Box className="glass-orb orb-1" />
|
||||||
<Card
|
<Box className="glass-orb orb-2" />
|
||||||
sx={{
|
<Box className="glass-orb orb-3" />
|
||||||
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 ? (
|
<Grid
|
||||||
<>
|
container
|
||||||
<Button
|
spacing={4}
|
||||||
variant="contained"
|
alignItems="center"
|
||||||
color="primary"
|
justifyContent="center"
|
||||||
onClick={() => navigate("/admin/dashboard")}
|
sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}
|
||||||
sx={{ mt: 3, width: "100%" }}
|
>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Stack spacing={2.5}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
sx={{
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Octopus Admin
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
lineHeight: 1.1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Accédez à votre espace sécurisé
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
lineHeight: 1.6,
|
||||||
|
maxWidth: 420,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Connectez-vous avec vos identifiants d’application pour gérer les
|
||||||
|
contenus Octopus : articles, pages services, formations et plus
|
||||||
|
encore.
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||||
|
<Chip
|
||||||
|
label="Authentification sécurisée"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(255,255,255,0.18)",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label="Accès réservé Octopus Team"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(123,192,255,0.18)",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Box
|
||||||
|
className="glass-subcard"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
p: { xs: 3, md: 4 },
|
||||||
|
boxShadow: "0 32px 46px -30px rgba(12, 29, 74, 0.5)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={3} alignItems="center">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
borderRadius: "50%",
|
||||||
|
bgcolor: "rgba(49,83,151,0.14)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#315397",
|
||||||
|
boxShadow: "0 18px 28px -20px rgba(12, 29, 74, 0.45)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LockOutlinedIcon fontSize="large" />
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{ fontWeight: 700, color: "#0b1a3d" }}
|
||||||
|
>
|
||||||
|
{token ? "Bienvenue dans l’espace Octopus" : "Connexion"}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{token ? (
|
||||||
|
<Stack spacing={1.5} sx={{ width: "100%" }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => navigate("/admin/dashboard")}
|
||||||
|
sx={{ ...composeButtonSx("primary"), width: "100%" }}
|
||||||
|
>
|
||||||
|
Accéder au tableau de bord
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleLogout}
|
||||||
|
sx={{ ...composeButtonSx("outline"), width: "100%" }}
|
||||||
|
>
|
||||||
|
Déconnexion
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
onSubmit={handleLogin}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
>
|
>
|
||||||
Accéder au tableau de bord
|
<Stack spacing={2}>
|
||||||
</Button>
|
<TextField
|
||||||
<Button
|
label="Nom d’utilisateur"
|
||||||
variant="contained"
|
fullWidth
|
||||||
color="error"
|
variant="outlined"
|
||||||
onClick={handleLogout}
|
value={username}
|
||||||
sx={{ mt: 2, width: "100%" }}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
>
|
required
|
||||||
Déconnexion
|
sx={{
|
||||||
</Button>
|
"& .MuiOutlinedInput-root": {
|
||||||
</>
|
borderRadius: 999,
|
||||||
) : (
|
backgroundColor: "rgba(255,255,255,0.95)",
|
||||||
<form onSubmit={handleLogin}>
|
},
|
||||||
<TextField
|
}}
|
||||||
label="Nom d'utilisateur"
|
/>
|
||||||
fullWidth
|
<TextField
|
||||||
margin="normal"
|
label="Mot de passe d’application"
|
||||||
variant="outlined"
|
fullWidth
|
||||||
value={username}
|
variant="outlined"
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
type="password"
|
||||||
required
|
value={appPassword}
|
||||||
/>
|
onChange={(e) => setAppPassword(e.target.value)}
|
||||||
<TextField
|
required
|
||||||
label="Mot de passe d'application"
|
sx={{
|
||||||
fullWidth
|
"& .MuiOutlinedInput-root": {
|
||||||
margin="normal"
|
borderRadius: 999,
|
||||||
variant="outlined"
|
backgroundColor: "rgba(255,255,255,0.95)",
|
||||||
type="password"
|
},
|
||||||
value={appPassword}
|
}}
|
||||||
onChange={(e) => setAppPassword(e.target.value)}
|
/>
|
||||||
required
|
{error ? (
|
||||||
/>
|
<Typography color="error" variant="body2">
|
||||||
<Button
|
{error}
|
||||||
type="submit"
|
</Typography>
|
||||||
variant="contained"
|
) : null}
|
||||||
color="primary"
|
<Button
|
||||||
sx={{ mt: 3, width: "100%" }}
|
type="submit"
|
||||||
>
|
variant="contained"
|
||||||
Se connecter
|
disabled={loading}
|
||||||
</Button>
|
sx={{ ...composeButtonSx("primary"), width: "100%" }}
|
||||||
</form>
|
>
|
||||||
)}
|
{loading ? (
|
||||||
</CardContent>
|
<CircularProgress size={20} sx={{ color: "#ffffff" }} />
|
||||||
</Card>
|
) : (
|
||||||
</Container>
|
"Se connecter"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TestLogin;
|
export default TestLogin;
|
||||||
|
|||||||
@ -1,8 +1,17 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Box, Typography, Grid, Card, CardContent, Button } from "@mui/material";
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Stack,
|
||||||
|
Chip,
|
||||||
|
} from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { getToken } from "../../auth";
|
import { getToken } from "../../auth";
|
||||||
|
import "../../assets/styleCours.css";
|
||||||
|
|
||||||
const toolsApps = [
|
const toolsApps = [
|
||||||
{
|
{
|
||||||
@ -46,40 +55,172 @@ function Tools() {
|
|||||||
navigate(app.url);
|
navigate(app.url);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const composeButtonSx = (variant = "primary") => ({
|
||||||
<Box sx={{ minHeight: "100vh", p: 9 }}>
|
borderRadius: 999,
|
||||||
<Box sx={{ display: "flex", alignItems: "center", mb: 3 }}>
|
textTransform: "none",
|
||||||
<Button
|
fontWeight: 600,
|
||||||
startIcon={<ArrowBackIcon />}
|
letterSpacing: 0.3,
|
||||||
onClick={() => navigate("/admin/dashboard")}
|
display: "inline-flex",
|
||||||
sx={{ mr: 2 }}
|
alignItems: "center",
|
||||||
>
|
justifyContent: "center",
|
||||||
Retour au dashboard
|
gap: 0.75,
|
||||||
</Button>
|
transition:
|
||||||
<Typography variant="h4" sx={{ fontWeight: "bold" }}>
|
"transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease",
|
||||||
Mes applications
|
px: 3,
|
||||||
</Typography>
|
py: 0.85,
|
||||||
</Box>
|
minHeight: 42,
|
||||||
|
...(variant === "primary"
|
||||||
|
? {
|
||||||
|
background: "linear-gradient(135deg, #315397, #7bc0ff)",
|
||||||
|
color: "#ffffff",
|
||||||
|
border: "1px solid rgba(255,255,255,0.28)",
|
||||||
|
boxShadow: "0 18px 28px -18px rgba(11,26,61,0.55)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "linear-gradient(135deg, #26467d, #6fb6ff)",
|
||||||
|
boxShadow: "0 22px 32px -18px rgba(11,26,61,0.62)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
background: "rgba(255,255,255,0.84)",
|
||||||
|
color: "#315397",
|
||||||
|
border: "1px solid rgba(49,83,151,0.26)",
|
||||||
|
"&:hover": {
|
||||||
|
background: "rgba(255,255,255,0.95)",
|
||||||
|
borderColor: "rgba(49,83,151,0.46)",
|
||||||
|
boxShadow: "0 14px 22px -18px rgba(11,26,61,0.4)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
<Grid container spacing={4}>
|
return (
|
||||||
{toolsApps.map((app) => (
|
<Box
|
||||||
<Grid item xs={12} sm={6} md={4} key={app.id}>
|
className="glass-dashboard"
|
||||||
<Card sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
sx={{
|
||||||
<CardContent>
|
minHeight: "100vh",
|
||||||
<Typography variant="h6">{app.name}</Typography>
|
position: "relative",
|
||||||
<Typography variant="body2" color="text.secondary">
|
px: { xs: 2, md: 6 },
|
||||||
{app.description}
|
pt: { xs: 6, md: 8 },
|
||||||
</Typography>
|
pb: { xs: 8, md: 10 },
|
||||||
<Box sx={{ mt: 2 }}>
|
}}
|
||||||
<Button variant="contained" color="primary" onClick={() => handleOpenApp(app)}>
|
>
|
||||||
Ouvrir
|
<Box className="glass-orb orb-1" />
|
||||||
</Button>
|
<Box className="glass-orb orb-2" />
|
||||||
</Box>
|
<Box className="glass-orb orb-3" />
|
||||||
</CardContent>
|
|
||||||
</Card>
|
<Box sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}>
|
||||||
</Grid>
|
<Stack
|
||||||
))}
|
direction={{ xs: "column", md: "row" }}
|
||||||
</Grid>
|
justifyContent="space-between"
|
||||||
|
alignItems={{ xs: "flex-start", md: "center" }}
|
||||||
|
spacing={2}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
onClick={() => navigate("/admin/dashboard")}
|
||||||
|
sx={composeButtonSx("outline")}
|
||||||
|
>
|
||||||
|
Retour au dashboard
|
||||||
|
</Button>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
sx={{
|
||||||
|
letterSpacing: 2,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "rgba(255,255,255,0.8)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Hub applicatif Octopus
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: "-0.5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Mes outils métiers
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
color: "rgba(255,255,255,0.85)",
|
||||||
|
maxWidth: 520,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Centralisez vos applications internes : réseau, bilans, générateur
|
||||||
|
de photos et futurs modules d’automatisation Octopus.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||||
|
<Chip
|
||||||
|
label="Accès sécurisé"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(255,255,255,0.2)",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
label="Applications internes Octopus"
|
||||||
|
sx={{
|
||||||
|
bgcolor: "rgba(123,192,255,0.2)",
|
||||||
|
color: "#ffffff",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{toolsApps.map((app) => (
|
||||||
|
<Grid item xs={12} md={6} lg={4} key={app.id}>
|
||||||
|
<Card
|
||||||
|
className="glass-card"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 4,
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
p: { xs: 2.5, md: 3 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={1.8}>
|
||||||
|
<Typography
|
||||||
|
variant="overline"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.65)", letterSpacing: 1 }}
|
||||||
|
>
|
||||||
|
{app.external ? "Application externe" : "Module interne"}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ color: "#0b1a3d", fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
{app.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ color: "rgba(11, 26, 61, 0.68)", lineHeight: 1.5 }}
|
||||||
|
>
|
||||||
|
{app.description}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={composeButtonSx("primary")}
|
||||||
|
onClick={() => handleOpenApp(app)}
|
||||||
|
>
|
||||||
|
Ouvrir
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
server/cloudinary-backend/.DS_Store
vendored
Normal file
BIN
server/cloudinary-backend/.DS_Store
vendored
Normal file
Binary file not shown.
28
server/cloudinary-backend/.env.example
Normal file
28
server/cloudinary-backend/.env.example
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#
|
||||||
|
# Backend configuration for contact form emails.
|
||||||
|
# Copy this file to `.env` and fill in your SMTP details.
|
||||||
|
#
|
||||||
|
|
||||||
|
# SMTP server host (e.g. smtp.gmail.com, smtp.mailgun.org, etc.)
|
||||||
|
SMTP_HOST=mail.sebvtl.com
|
||||||
|
|
||||||
|
# SMTP port (465 for SSL, 587 for TLS)
|
||||||
|
SMTP_PORT=465
|
||||||
|
|
||||||
|
# Use "true" if your provider requires SSL (port 465)
|
||||||
|
# Otherwise leave to "false" for TLS (port 587)
|
||||||
|
SMTP_SECURE=true
|
||||||
|
|
||||||
|
# SMTP credentials
|
||||||
|
SMTP_USER=contact@sebvtl.com
|
||||||
|
SMTP_PASS=Lightwave9.0**
|
||||||
|
|
||||||
|
# Optional: override the recipient (defaults to sebastien@octopusdesign.fr)
|
||||||
|
CONTACT_RECIPIENT=sebastien@octopusdesign.fr
|
||||||
|
|
||||||
|
# Optional: override the "from" address visible in the email client
|
||||||
|
CONTACT_FROM=no-reply@octopusdesign.fr
|
||||||
|
|
||||||
|
# Optional: add extra allowed origins for CORS (comma separated)
|
||||||
|
# Example: CORS_ALLOWED_ORIGINS=https://preprod.octopusdesign.fr,https://admin.octopusdesign.fr
|
||||||
|
CORS_ALLOWED_ORIGINS=
|
||||||
123
server/cloudinary-backend/contactRoute.js
Normal file
123
server/cloudinary-backend/contactRoute.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import express from "express";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const buildTransporterConfig = () => {
|
||||||
|
const host = process.env.SMTP_HOST;
|
||||||
|
const port = Number(process.env.SMTP_PORT || 587);
|
||||||
|
const secure =
|
||||||
|
typeof process.env.SMTP_SECURE === "string"
|
||||||
|
? process.env.SMTP_SECURE === "true"
|
||||||
|
: port === 465;
|
||||||
|
|
||||||
|
const user = process.env.SMTP_USER;
|
||||||
|
const pass = process.env.SMTP_PASS;
|
||||||
|
|
||||||
|
const baseConfig = {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
secure,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (user && pass) {
|
||||||
|
baseConfig.auth = { user, pass };
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post("/contact", async (req, res) => {
|
||||||
|
const { name, email, subject, message, consent } = req.body || {};
|
||||||
|
|
||||||
|
console.info("📨 Requête contact reçue", {
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
subject,
|
||||||
|
consent,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!name || !email || !subject || !message) {
|
||||||
|
return res.status(400).json({
|
||||||
|
message:
|
||||||
|
"Merci de compléter tous les champs requis avant d'envoyer votre message.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipient =
|
||||||
|
process.env.CONTACT_RECIPIENT || "sebastien@octopusdesign.fr";
|
||||||
|
const fromAddress =
|
||||||
|
process.env.CONTACT_FROM ||
|
||||||
|
process.env.SMTP_FROM ||
|
||||||
|
process.env.SMTP_USER ||
|
||||||
|
"no-reply@octopusdesign.fr";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transporter = nodemailer.createTransport(buildTransporterConfig());
|
||||||
|
|
||||||
|
const mailSubject = subject.trim()
|
||||||
|
? `[Site Octopus Design] ${subject.trim()}`
|
||||||
|
: "Nouveau message depuis le site Octopus Design";
|
||||||
|
|
||||||
|
const consentLabel = consent ? "✅ Consentement donné" : "❌ Consentement non fourni";
|
||||||
|
|
||||||
|
const textBody = `
|
||||||
|
Nom : ${name}
|
||||||
|
E-mail : ${email}
|
||||||
|
Consentement : ${consentLabel}
|
||||||
|
|
||||||
|
Message :
|
||||||
|
${message}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const htmlBody = `
|
||||||
|
<p><strong>Nom :</strong> ${name}</p>
|
||||||
|
<p><strong>E-mail :</strong> ${email}</p>
|
||||||
|
<p><strong>Consentement :</strong> ${consentLabel}</p>
|
||||||
|
<p><strong>Message :</strong></p>
|
||||||
|
<p>${message.replace(/\n/g, "<br />")}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Vérifie la connexion SMTP avant d'essayer d'envoyer
|
||||||
|
await transporter.verify();
|
||||||
|
|
||||||
|
const mailOptions = {
|
||||||
|
from: `"Octopus Design" <${fromAddress}>`,
|
||||||
|
to: recipient,
|
||||||
|
replyTo: email,
|
||||||
|
subject: mailSubject,
|
||||||
|
text: textBody,
|
||||||
|
html: htmlBody,
|
||||||
|
};
|
||||||
|
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
"✅ Email de contact envoyé avec succès",
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
to: recipient,
|
||||||
|
replyTo: email,
|
||||||
|
subject: mailSubject,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
message: "Votre message a bien été envoyé. Merci pour votre confiance !",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"❌ Erreur lors de l'envoi du mail de contact :",
|
||||||
|
error?.stack || error?.message || error
|
||||||
|
);
|
||||||
|
return res.status(500).json({
|
||||||
|
message:
|
||||||
|
"Impossible d'envoyer votre message pour le moment. Merci de réessayer plus tard.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -1,17 +1,37 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import cloudinaryRoute from "./cloudinaryRoute.js";
|
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
|
import cloudinaryRoute from "./cloudinaryRoute.js";
|
||||||
|
import contactRoute from "./contactRoute.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors({
|
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || "")
|
||||||
// origin: "https://octopusdesign.fr"
|
.split(",")
|
||||||
origin: ["https://octopusdesign.fr", "http://localhost:3000"]
|
.map((origin) => origin.trim())
|
||||||
}));
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const defaultOrigins = [
|
||||||
|
"https://octopusdesign.fr",
|
||||||
|
"https://www.octopusdesign.fr",
|
||||||
|
"https://preprod.octopusdesign.fr",
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:5173",
|
||||||
|
];
|
||||||
|
|
||||||
|
const origins = [...new Set([...defaultOrigins, ...allowedOrigins])];
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
origin: origins,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
app.use("/api", cloudinaryRoute);
|
app.use("/api", cloudinaryRoute);
|
||||||
|
app.use("/api", contactRoute);
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2"
|
"express": "^4.18.2",
|
||||||
|
"nodemailer": "^6.9.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user