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
|
||||
|
||||
## 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 {
|
||||
padding: 30px 40px 32px 40px;
|
||||
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 { 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 {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Paper,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Stack,
|
||||
Chip,
|
||||
Divider,
|
||||
} 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 CloudinaryGallerySelector from "./CloudinaryGallerySelector";
|
||||
import ReactQuill from "react-quill";
|
||||
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() {
|
||||
const { id } = useParams();
|
||||
@ -23,6 +113,13 @@ function EditPost() {
|
||||
const [content, setContent] = useState("");
|
||||
const [imageUrl, setImageUrl] = useState(null); // preview
|
||||
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
|
||||
useEffect(() => {
|
||||
@ -33,13 +130,21 @@ function EditPost() {
|
||||
useEffect(() => {
|
||||
const fetchPost = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const post = await getPostById(id);
|
||||
setTitle(post.title.rendered);
|
||||
setContent(post.content.rendered);
|
||||
setImageUrl(post?.jetpack_featured_media_url || null); // preview actuelle
|
||||
} catch (err) {
|
||||
console.error("Erreur chargement article :", err);
|
||||
setImageUrl(post?.jetpack_featured_media_url || null);
|
||||
setPostMeta({
|
||||
date: post.date || null,
|
||||
modified: post.modified || null,
|
||||
status: post.status || "",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur chargement article :", error);
|
||||
navigate("/admin/gestion-articles");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPost();
|
||||
@ -62,119 +167,392 @@ function EditPost() {
|
||||
],
|
||||
};
|
||||
|
||||
const handleUpdate = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
let imageId = null;
|
||||
const canSubmit =
|
||||
title.trim().length > 0 && content.trim().length > 0 && Boolean(imageUrl);
|
||||
|
||||
if (imageFile) {
|
||||
// upload image vers WordPress
|
||||
const handleUpdate = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!canSubmit || submitting) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setSubmitting(true);
|
||||
let imageId = undefined;
|
||||
if (imageFile || imageUrl) {
|
||||
imageId = await uploadImageFromUrl(imageUrl);
|
||||
}
|
||||
|
||||
await updatePost(id, title, content, imageId);
|
||||
alert("✅ Article mis à jour !");
|
||||
navigate("/admin/gestion-articles");
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur update :", error);
|
||||
alert("❌ Erreur mise à jour.");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="glass-dashboard"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
px: { xs: 2, md: 6 },
|
||||
pt: { xs: 6, md: 8 },
|
||||
pb: { xs: 8, md: 10 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Paper elevation={6} sx={{ p: 4, borderRadius: 3 }}>
|
||||
<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
|
||||
onClick={() => navigate("/admin/gestion-articles")}
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
startIcon={<ArrowBack />}
|
||||
sx={composeButtonSx("ghost")}
|
||||
>
|
||||
Retour
|
||||
Retour aux articles
|
||||
</Button>
|
||||
|
||||
<Typography variant="h4" align="center" sx={{ mb: 3 }}>
|
||||
Modifier l'article
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleUpdate}>
|
||||
<TextField
|
||||
label="Titre"
|
||||
fullWidth
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Stack spacing={0.5}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{
|
||||
letterSpacing: 2,
|
||||
fontWeight: 700,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
}}
|
||||
>
|
||||
Publication Octopus
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
color: "#ffffff",
|
||||
fontWeight: 800,
|
||||
letterSpacing: "-0.5px",
|
||||
}}
|
||||
>
|
||||
Modifier l’article
|
||||
</Typography>
|
||||
<Typography
|
||||
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>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ height: 250, mb: 7 }}>
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
modules={modules}
|
||||
style={{ height: "100%" }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography fontWeight="bold">Image depuis ton ordi :</Typography>
|
||||
<ImageUploaderCloudinary
|
||||
onUploadSuccess={(url) => {
|
||||
setImageUrl(url);
|
||||
convertUrlToFile(url).then(setImageFile);
|
||||
<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
|
||||
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 }}>
|
||||
Ou choisir une image déjà envoyée :
|
||||
</Typography>
|
||||
<CloudinaryGallerySelector
|
||||
onSelect={async (url) => {
|
||||
setImageUrl(url);
|
||||
const file = await convertUrlToFile(url);
|
||||
setImageFile(file);
|
||||
}}
|
||||
/>
|
||||
{loading ? (
|
||||
<Typography variant="body2" sx={{ color: "rgba(11, 26, 61, 0.65)" }}>
|
||||
Chargement de l’article…
|
||||
</Typography>
|
||||
) : (
|
||||
<Box component="form" onSubmit={handleUpdate} 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)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{imageUrl && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2">Aperçu :</Typography>
|
||||
<img
|
||||
src={imageUrl}
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: 6,
|
||||
maxHeight: 200,
|
||||
objectFit: "cover",
|
||||
<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
|
||||
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"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
sx={{ mt: 3 }}
|
||||
startIcon={<Publish />}
|
||||
>
|
||||
Mettre à jour
|
||||
</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
>
|
||||
<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 illustrera l’article dans la liste des publications
|
||||
et sur les réseaux sociaux.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</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 { getToken } from "../auth";
|
||||
import { getPageById, updatePageACF } from "../wordpress";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Paper,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Stack,
|
||||
Divider,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { Add, Delete, ArrowBack, Save } from "@mui/icons-material";
|
||||
import ReactQuill from "react-quill";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
import "../assets/styleCours.css";
|
||||
|
||||
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() {
|
||||
const navigate = useNavigate();
|
||||
const [acfFields, setAcfFields] = useState({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [successMessage, setSuccessMessage] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Vérifie l'authentification
|
||||
useEffect(() => {
|
||||
@ -50,15 +150,22 @@ function EditPageEtude() {
|
||||
}, []);
|
||||
|
||||
// Mise à jour ACF
|
||||
const handleUpdateACF = async (e) => {
|
||||
e.preventDefault();
|
||||
const handleUpdateACF = async (event) => {
|
||||
event.preventDefault();
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await updatePageACF(ETUDE_PAGE_ID, acfFields);
|
||||
setSuccessMessage("✅ Modifications enregistrées !");
|
||||
setError(null);
|
||||
setTimeout(() => navigate("/admin/dashboard"), 2000);
|
||||
} catch {
|
||||
setTimeout(() => navigate("/admin/dashboard"), 1800);
|
||||
} catch (err) {
|
||||
console.error("⚠ Impossible de mettre à jour les champs ACF :", err);
|
||||
setError("⚠ Impossible de mettre à jour les champs ACF.");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -98,126 +205,466 @@ function EditPageEtude() {
|
||||
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 (
|
||||
<Box sx={{ py: 4, backgroundColor: "#f5f5f5" }}>
|
||||
<Container maxWidth="md">
|
||||
<Paper sx={{ p: 4, borderRadius: 3 }} elevation={4}>
|
||||
<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="outlined"
|
||||
onClick={() => navigate("/admin/dashboard")}
|
||||
sx={{ mb: 2 }}
|
||||
variant="contained"
|
||||
sx={composeButtonSx("ghost")}
|
||||
>
|
||||
Retour
|
||||
Retour au dashboard
|
||||
</Button>
|
||||
|
||||
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 3, textAlign: "center" }}>
|
||||
Modifier la Page Formations
|
||||
</Typography>
|
||||
<Stack spacing={0.5}>
|
||||
<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>}
|
||||
{successMessage && <Typography color="success.main" sx={{ mb: 2, textAlign: "center" }}>{successMessage}</Typography>}
|
||||
<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 }}
|
||||
>
|
||||
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}>
|
||||
<Grid container spacing={3}>
|
||||
{/* Introduction */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Introduction</Typography>
|
||||
<ReactQuill
|
||||
value={acfFields.introduction || ""}
|
||||
onChange={(val) => handleFieldChange("introduction", val)}
|
||||
/>
|
||||
</Grid>
|
||||
{error ? (
|
||||
<Card
|
||||
className="glass-subcard"
|
||||
sx={{ borderRadius: 4, p: 3, mb: 3, borderColor: "rgba(240,91,107,0.35) !important" }}
|
||||
>
|
||||
<Typography color="error" sx={{ fontWeight: 600 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
</Card>
|
||||
) : 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 */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Méthodologie</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={acfFields.methodologie || ""}
|
||||
onChange={(e) => handleFieldChange("methodologie", e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
<Box component="form" onSubmit={handleUpdateACF}>
|
||||
<Stack spacing={3.5}>
|
||||
<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 }}
|
||||
>
|
||||
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 */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Liste des compétences</Typography>
|
||||
{getRepeater("liste_des_competences").map((item, i) => (
|
||||
<Paper key={i} sx={{ p:2, mb:2 }}>
|
||||
<Divider sx={{ borderColor: "rgba(11, 26, 61, 0.08)" }} />
|
||||
|
||||
<Box>
|
||||
<Typography
|
||||
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
|
||||
label="Titre"
|
||||
fullWidth
|
||||
value={item.comp_titre}
|
||||
onChange={(e) => handleRepeaterChange("liste_des_competences", i, "comp_titre", e.target.value)}
|
||||
sx={{ mb:1 }}
|
||||
value={acfFields.methodologie || ""}
|
||||
onChange={(event) =>
|
||||
handleFieldChange("methodologie", event.target.value)
|
||||
}
|
||||
multiline
|
||||
minRows={3}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: 2,
|
||||
backgroundColor: "rgba(255,255,255,0.94)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
fullWidth
|
||||
multiline rows={3}
|
||||
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>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Expertises spécifiques */}
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Expertises spécifiques</Typography>
|
||||
{getRepeater("expertises_specifiques").map((item, i) => (
|
||||
<Paper key={i} sx={{ p:2, mb:2 }}>
|
||||
<TextField
|
||||
label="Nom"
|
||||
fullWidth
|
||||
value={item.expertise_nom}
|
||||
onChange={(e) => handleRepeaterChange("expertises_specifiques", i, "expertise_nom", e.target.value)}
|
||||
sx={{ mb:1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Détails"
|
||||
fullWidth multiline rows={2}
|
||||
value={item.expertise_details}
|
||||
onChange={(e) => handleRepeaterChange("expertises_specifiques", i, "expertise_details", e.target.value)}
|
||||
sx={{ mb:1 }}
|
||||
/>
|
||||
<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>
|
||||
<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 }}
|
||||
>
|
||||
Liste des compétences
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "rgba(11, 26, 61, 0.65)", mb: 1.5 }}
|
||||
>
|
||||
Ajoutez les compétences clés qui démontrent l’expertise du
|
||||
bureau d’étude. Chaque compétence peut contenir un titre et
|
||||
une description détaillée.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Save */}
|
||||
<Grid item xs={12}>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<Save />}
|
||||
sx={{ py:1.5 }}
|
||||
>
|
||||
Enregistrer
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
<Stack spacing={2}>
|
||||
{getRepeater("liste_des_competences").map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="glass-card"
|
||||
sx={{ borderRadius: 3, p: { xs: 2, md: 2.5 } }}
|
||||
>
|
||||
<Stack spacing={1.5}>
|
||||
<TextField
|
||||
label="Titre"
|
||||
fullWidth
|
||||
value={item.comp_titre}
|
||||
onChange={(event) =>
|
||||
handleRepeaterChange(
|
||||
"liste_des_competences",
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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 { getToken } from "../auth";
|
||||
import { fetchPages } from "../wordpress";
|
||||
@ -6,24 +6,109 @@ import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Grid,
|
||||
Card,
|
||||
TextField,
|
||||
Stack,
|
||||
CircularProgress,
|
||||
} from "@mui/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 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() {
|
||||
const navigate = useNavigate();
|
||||
const [pages, setPages] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
// ✅ Vérifier l'authentification
|
||||
useEffect(() => {
|
||||
@ -49,7 +134,6 @@ function GestionPagesACF() {
|
||||
return;
|
||||
}
|
||||
|
||||
// ✅ Exclure la page d'accueil
|
||||
const filteredPages = response.filter(
|
||||
(page) => page.id !== HOME_PAGE_ID && page.id !== ETUDE_PAGE_ID,
|
||||
);
|
||||
@ -65,88 +149,333 @@ function GestionPagesACF() {
|
||||
loadPages();
|
||||
}, []);
|
||||
|
||||
if (loading) return <Typography>Chargement...</Typography>;
|
||||
if (error) return <Typography color="error">{error}</Typography>;
|
||||
const filteredPages = useMemo(() => {
|
||||
const term = searchTerm.trim().toLowerCase();
|
||||
if (!term) {
|
||||
return pages;
|
||||
}
|
||||
return pages.filter((page) =>
|
||||
page.title?.rendered?.toLowerCase?.().includes(term),
|
||||
);
|
||||
}, [pages, searchTerm]);
|
||||
|
||||
return (
|
||||
<Box sx={{ padding: "40px 20px", mt: 6 }}>
|
||||
if (loading) {
|
||||
return (
|
||||
<Box
|
||||
className="glass-dashboard"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 4,
|
||||
mt: 5,
|
||||
justifyContent: "center",
|
||||
px: { xs: 2, md: 6 },
|
||||
pt: { xs: 6, md: 8 },
|
||||
pb: { xs: 8, md: 10 },
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
startIcon={<ArrowBack />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => navigate("/admin/dashboard")}
|
||||
>
|
||||
Retour au Dashboard
|
||||
</Button>
|
||||
<Typography
|
||||
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 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 des pages services…
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead sx={{ backgroundColor: "#0e467f" }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>
|
||||
Titre
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ color: "white", fontWeight: "bold", textAlign: "center" }}
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
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: 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
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{pages.length > 0 ? (
|
||||
pages.map((page) => (
|
||||
<TableRow key={page.id}>
|
||||
<TableCell sx={{ fontWeight: "bold" }}>
|
||||
{page.title.rendered}
|
||||
</TableCell>
|
||||
<TableCell sx={{ textAlign: "center" }}>
|
||||
<Button
|
||||
startIcon={<Edit />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
onClick={() => navigate(`/admin/edit-page/${page.id}`)}
|
||||
Pages référencées
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ color: "#0b1a3d", fontWeight: 800, letterSpacing: "-0.5px" }}
|
||||
>
|
||||
{pages.length}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "rgba(11, 26, 61, 0.65)", mt: 0.5 }}
|
||||
>
|
||||
Ensemble des pages service disposant de champs ACF personnalisés.
|
||||
</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 }}
|
||||
>
|
||||
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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={2} sx={{ textAlign: "center", py: 2 }}>
|
||||
Aucune page avec des champs ACF trouvée.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: "#0b1a3d", fontWeight: 700, lineHeight: 1.4 }}
|
||||
>
|
||||
{page.title.rendered}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "rgba(11, 26, 61, 0.68)", lineHeight: 1.5 }}
|
||||
>
|
||||
Cliquez sur "Modifier" pour ajuster les contenus
|
||||
de cette page service depuis l’éditeur ACF dédié.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,6 +22,12 @@ import Faq from "../Faq";
|
||||
|
||||
|
||||
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 [metaDescription, setMetaDescription] = useState(
|
||||
"Contactez notre équipe pour plus d'informations."
|
||||
@ -35,7 +41,11 @@ const Contact = () => {
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
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 [showFAQ, setShowFAQ] = useState(false); // État pour afficher/masquer la FAQ
|
||||
@ -96,7 +106,7 @@ const Contact = () => {
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://votre-site.com/wp-json/custom/v1/contact",
|
||||
CONTACT_API_URL,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -107,7 +117,11 @@ const Contact = () => {
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
setSuccessMessage(true);
|
||||
setAlertState({
|
||||
open: true,
|
||||
severity: "success",
|
||||
message: "Merci ! Votre message a bien été envoyé.",
|
||||
});
|
||||
setFormData({
|
||||
name: "",
|
||||
email: "",
|
||||
@ -120,7 +134,13 @@ const Contact = () => {
|
||||
throw new Error(errorData.message || "Une erreur est survenue.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
setAlertState({
|
||||
open: true,
|
||||
severity: "error",
|
||||
message:
|
||||
error.message ||
|
||||
"Impossible d'envoyer votre message. Veuillez réessayer.",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -135,6 +155,13 @@ const Contact = () => {
|
||||
setOpenLightbox(false);
|
||||
};
|
||||
|
||||
const handleCloseAlert = (_, reason) => {
|
||||
if (reason === "clickaway") {
|
||||
return;
|
||||
}
|
||||
setAlertState((prev) => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* SEO */}
|
||||
@ -490,12 +517,17 @@ const Contact = () => {
|
||||
</form>
|
||||
|
||||
<Snackbar
|
||||
open={successMessage}
|
||||
open={alertState.open}
|
||||
autoHideDuration={6000}
|
||||
onClose={() => setSuccessMessage(false)}
|
||||
onClose={handleCloseAlert}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
>
|
||||
<Alert onClose={() => setSuccessMessage(false)} severity="success">
|
||||
Message envoyé avec succès !
|
||||
<Alert
|
||||
onClose={handleCloseAlert}
|
||||
severity={alertState.severity}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
{alertState.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
|
||||
@ -4,22 +4,113 @@ import { uploadImageFromUrl, createPost } from "../../wordpress";
|
||||
import { getToken } from "../../auth";
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
TextField,
|
||||
Button,
|
||||
Paper,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
Stack,
|
||||
Chip,
|
||||
Divider,
|
||||
} 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 CloudinaryGallerySelector from "../CloudinaryGallerySelector";
|
||||
import ReactQuill from "react-quill";
|
||||
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() {
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [imageUrl, setImageUrl] = useState(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Vérification de l'authentification
|
||||
@ -29,126 +120,352 @@ function CreatePost() {
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const canPublish =
|
||||
title.trim().length > 0 && content.trim().length > 0 && Boolean(imageUrl);
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!canPublish || submitting) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const imageId = await uploadImageFromUrl(imageUrl);
|
||||
await createPost(title, content, imageId);
|
||||
alert("✅ Article publié !");
|
||||
navigate("/admin/gestion-articles");
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur création post :", error.response?.data || error.message);
|
||||
alert("❌ Erreur lors de la création du post.");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Erreur création post :",
|
||||
error?.response?.data || error?.message || error
|
||||
);
|
||||
alert("❌ Erreur lors de la création du post.");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="glass-dashboard"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundImage:
|
||||
"url('https://source.unsplash.com/1600x900/?office,writing')",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
position: "relative",
|
||||
px: { xs: 2, md: 6 },
|
||||
pt: { xs: 6, md: 8 },
|
||||
pb: { xs: 8, md: 10 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="sm">
|
||||
<Paper elevation={6} sx={{ padding: 4, borderRadius: 3 }}>
|
||||
<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="outlined"
|
||||
color="secondary"
|
||||
variant="contained"
|
||||
onClick={() => navigate("/admin/gestion-articles")}
|
||||
sx={{ mb: 2 }}
|
||||
sx={composeButtonSx("ghost")}
|
||||
>
|
||||
Retour
|
||||
Retour aux articles
|
||||
</Button>
|
||||
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ fontWeight: "bold", textAlign: "center", mb: 3 }}
|
||||
>
|
||||
Créer un article
|
||||
</Typography>
|
||||
|
||||
<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 }}
|
||||
<Stack spacing={0.5}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{
|
||||
letterSpacing: 2,
|
||||
fontWeight: 700,
|
||||
color: "rgba(255,255,255,0.8)",
|
||||
}}
|
||||
>
|
||||
Publier l'article
|
||||
</Button>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
Publication Octopus
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h3"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,18 +7,119 @@ import {
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
IconButton,
|
||||
Button,
|
||||
Stack,
|
||||
Chip,
|
||||
Divider,
|
||||
} 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 DashboardIcon from "@mui/icons-material/Dashboard";
|
||||
import HomeIcon from "@mui/icons-material/Home"; // Icône pour la gestion page d'accueil
|
||||
import HomeIcon from "@mui/icons-material/Home";
|
||||
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() {
|
||||
const navigate = useNavigate();
|
||||
const token = getToken();
|
||||
const token = getToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
@ -31,240 +132,390 @@ function Dashboard() {
|
||||
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 (
|
||||
<Box
|
||||
className="glass-dashboard"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
backgroundImage:
|
||||
"url('https://source.unsplash.com/1600x900/?technology,office')",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "20px",
|
||||
position: "relative",
|
||||
px: { xs: 2, md: 6 },
|
||||
pt: { xs: 6, md: 8 },
|
||||
pb: { xs: 8, md: 10 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "90%",
|
||||
maxWidth: "900px",
|
||||
padding: 4,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
||||
borderRadius: 3,
|
||||
boxShadow: 6,
|
||||
textAlign: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Bouton de déconnexion placé en haut à gauche */}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<LogoutIcon />}
|
||||
onClick={handleLogout}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
backgroundColor: "#d32f2f",
|
||||
"&:hover": { backgroundColor: "#b71c1c" },
|
||||
fontSize: "0.875rem",
|
||||
padding: "6px 12px",
|
||||
}}
|
||||
/>
|
||||
<Box className="glass-orb orb-1" />
|
||||
<Box className="glass-orb orb-2" />
|
||||
<Box className="glass-orb orb-3" />
|
||||
|
||||
{/* En-tête */}
|
||||
<Typography variant="h4" sx={{ fontWeight: "bold", mb: 4, mt: 4 }}>
|
||||
<DashboardIcon sx={{ fontSize: 40, color: "#0e467f", mr: 1 }} />
|
||||
Tableau de Bord
|
||||
<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 }}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Cartes du Dashboard */}
|
||||
<Grid container spacing={3} justifyContent="center">
|
||||
{/* ✅ Gestion Page d'Accueil - Première carte */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Card
|
||||
onClick={() => navigate("/admin/Gestion-Page-Accueil")}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
textAlign: "center",
|
||||
padding: 2,
|
||||
transition: "0.3s",
|
||||
"&:hover": {
|
||||
backgroundColor: "#0e467f",
|
||||
color: "#ffffff",
|
||||
transform: "scale(1.05)",
|
||||
boxShadow: 8,
|
||||
},
|
||||
"&:hover svg": {
|
||||
fill: "#ffffff",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<IconButton sx={{ fontSize: 40, color: "#0e467f" }}>
|
||||
<HomeIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||
Gestion Page d'Accueil
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Card
|
||||
onClick={() => navigate("/admin/tools")}
|
||||
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" }}>
|
||||
<BuildIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||
Tools
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* ✅ Gérer les Articles - Deuxième carte */}
|
||||
<Grid item xs={12} sm={6}>
|
||||
<Card
|
||||
onClick={() => navigate("/admin/gestion-articles")}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
textAlign: "center",
|
||||
padding: 2,
|
||||
transition: "0.3s",
|
||||
"&:hover": {
|
||||
backgroundColor: "#0e467f",
|
||||
color: "#ffffff",
|
||||
transform: "scale(1.05)",
|
||||
boxShadow: 8,
|
||||
},
|
||||
"&:hover svg": {
|
||||
fill: "#ffffff",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<IconButton sx={{ fontSize: 40, color: "#0e467f" }}>
|
||||
<ArticleIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||
Gérer les Articles
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* ✅ 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 container spacing={2.5}>
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<Grid item xs={12} md={6} key={item.title}>
|
||||
<Card
|
||||
onClick={() => navigate(item.navigateTo)}
|
||||
className="glass-subcard"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
borderRadius: 3,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
transition:
|
||||
"transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease",
|
||||
"&::before": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
borderRadius: "inherit",
|
||||
background: `radial-gradient(360px at 18% 24%, ${item.accent}, transparent 72%)`,
|
||||
opacity: 0.9,
|
||||
},
|
||||
"&:hover": {
|
||||
transform: "translateY(-8px)",
|
||||
borderColor: "rgba(49,83,151,0.35)",
|
||||
boxShadow: "0 38px 40px -30px rgba(12, 29, 74, 0.6)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
p: { xs: 3, md: 3.5 },
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 2,
|
||||
backgroundColor: "rgba(255,255,255,0.75)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "#315397",
|
||||
boxShadow: "0 16px 26px -20px rgba(11, 26, 61, 0.5)",
|
||||
fontSize: 28,
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ color: "#0b1a3d", fontWeight: 700 }}
|
||||
>
|
||||
{item.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: "rgba(11, 26, 61, 0.68)", lineHeight: 1.6 }}
|
||||
>
|
||||
{item.description}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => navigate(item.navigateTo)}
|
||||
endIcon={<ArrowForwardIcon fontSize="small" />}
|
||||
sx={{
|
||||
...composeButtonSx("primary", "small"),
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
>
|
||||
Ouvrir
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@ -1,18 +1,118 @@
|
||||
// ✅ Importations nécessaires
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Box, Typography, Button, Table, TableBody, TableCell,
|
||||
TableContainer, TableHead, TableRow, Paper, TextField, Avatar
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
TextField,
|
||||
Stack,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { Add, ArrowBack, Edit, Delete } from "@mui/icons-material";
|
||||
import api from "../../api";
|
||||
import { getToken } from "../../auth";
|
||||
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 [posts, setPosts] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// ✅ Vérifier si l'utilisateur est connecté
|
||||
@ -26,7 +126,10 @@ const GestionArticles = () => {
|
||||
useEffect(() => {
|
||||
const fetchPosts = async () => {
|
||||
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 postsWithImages = await Promise.all(
|
||||
@ -47,6 +150,8 @@ const GestionArticles = () => {
|
||||
setPosts(postsWithImages);
|
||||
} catch (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
|
||||
const handleDeletePost = async (postId, imageId) => {
|
||||
if (window.confirm("Es-tu sûr de vouloir supprimer cet article ?")) {
|
||||
try {
|
||||
await deletePost(postId, imageId); // ✅ Appel de la fonction deletePost
|
||||
setPosts(posts.filter((post) => post.id !== postId)); // ✅ Met à jour la liste après suppression
|
||||
alert("✅ Article supprimé avec succès !");
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur suppression article :", error);
|
||||
alert("⚠ Erreur : impossible de supprimer l'article.");
|
||||
}
|
||||
}
|
||||
};
|
||||
const stripHtml = (value = "") =>
|
||||
value.replace(/(<([^>]+)>)/gi, "").replace(/ /gi, " ").trim();
|
||||
|
||||
// ✅ Filtrage des articles
|
||||
const filteredPosts = posts.filter((post) =>
|
||||
post.title.rendered.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").replace(/ /g, " ").toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
const previewExcerpt = (value = "", limit = 160) => {
|
||||
const clean = stripHtml(value);
|
||||
return clean.length > limit ? `${clean.slice(0, limit)}…` : clean;
|
||||
};
|
||||
|
||||
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 (
|
||||
<Box sx={{ padding: "40px 20px" }}>
|
||||
{/* ✅ En-tête avec retour et création */}
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 4, mt:5 }}>
|
||||
<Button startIcon={<ArrowBack />} variant="outlined" color="secondary" onClick={() => navigate("/admin/dashboard")}>
|
||||
Retour au Dashboard
|
||||
</Button>
|
||||
<Typography variant="h4" sx={{ fontWeight: "bold", textAlign: "center", flexGrow: 1 }}>
|
||||
Gestion des Articles
|
||||
</Typography>
|
||||
<Button startIcon={<Add />} variant="contained" color="primary" onClick={() => navigate("/admin/create-post")}>
|
||||
Créer un article
|
||||
</Button>
|
||||
<Box
|
||||
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: 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>
|
||||
|
||||
{/* ✅ Recherche */}
|
||||
<TextField
|
||||
label="Rechercher un article..."
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
sx={{ mb: 3 }}
|
||||
/>
|
||||
|
||||
{/* ✅ Tableau des articles */}
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead sx={{ backgroundColor: "#0e467f" }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Image</TableCell>
|
||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Titre</TableCell>
|
||||
<TableCell sx={{ color: "white", fontWeight: "bold" }}>Résumé</TableCell>
|
||||
<TableCell sx={{ color: "white", fontWeight: "bold", textAlign: "center" }}>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredPosts.length > 0 ? (
|
||||
filteredPosts.map((post) => (
|
||||
<TableRow key={post.id}>
|
||||
{/* ✅ Image en vedette */}
|
||||
<TableCell>
|
||||
<Avatar
|
||||
src={post.image || "https://via.placeholder.com/100"}
|
||||
variant="rounded"
|
||||
sx={{ width: 80, height: 80 }}
|
||||
/>
|
||||
</TableCell>
|
||||
{/* ✅ Titre de l'article */}
|
||||
<TableCell sx={{ fontWeight: "bold" }}>{post.title.rendered}</TableCell>
|
||||
{/* ✅ Résumé avec 100 caractères max */}
|
||||
<TableCell>
|
||||
{post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").replace(/ /g, " ").substring(0, 100)}...
|
||||
</TableCell>
|
||||
{/* ✅ Boutons Modifier et Supprimer */}
|
||||
<TableCell sx={{ textAlign: "center" }}>
|
||||
<Button startIcon={<Edit />}
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
sx={{ mr: 1 }}
|
||||
onClick={() => navigate(`/admin/edit-post/${post.id}`)}
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
<Button
|
||||
startIcon={<Delete />}
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => handleDeletePost(post.id, post.imageId)}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} sx={{ textAlign: "center", py: 2 }}>
|
||||
Aucun article trouvé.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default GestionArticles;
|
||||
export default GestionArticles;
|
||||
|
||||
@ -4,29 +4,35 @@ import { useNavigate } from "react-router-dom"; // Import de useNavigate pour la
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
TextField,
|
||||
Typography,
|
||||
Container,
|
||||
Avatar,
|
||||
Stack,
|
||||
CircularProgress,
|
||||
Chip,
|
||||
} from "@mui/material";
|
||||
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
|
||||
import "../../assets/styleCours.css";
|
||||
|
||||
function TestLogin() {
|
||||
const [username, setUsername] = useState("");
|
||||
const [appPassword, setAppPassword] = useState("");
|
||||
const [token, setToken] = useState(getToken());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const navigate = useNavigate(); // Hook pour la navigation
|
||||
|
||||
const handleLogin = async (e) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
const newToken = await loginWithAppPassword(username, appPassword);
|
||||
if (newToken) {
|
||||
setToken(newToken);
|
||||
} else {
|
||||
alert("Échec de la connexion !");
|
||||
setError("Échec de la connexion. Vérifiez vos identifiants.");
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
@ -34,92 +40,232 @@ function TestLogin() {
|
||||
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 (
|
||||
<Box
|
||||
className="glass-dashboard"
|
||||
sx={{
|
||||
backgroundImage: "url('https://source.unsplash.com/random/1600x900?technology')",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
px: { xs: 2, md: 6 },
|
||||
pt: { xs: 8, md: 12 },
|
||||
pb: { xs: 8, md: 10 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="xs">
|
||||
<Card
|
||||
sx={{
|
||||
padding: 4,
|
||||
boxShadow: 6,
|
||||
borderRadius: 3,
|
||||
textAlign: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Avatar sx={{ margin: "auto", backgroundColor: "#0e467f" }}>
|
||||
<LockOutlinedIcon />
|
||||
</Avatar>
|
||||
<Typography variant="h5" sx={{ fontWeight: "bold", mt: 2 }}>
|
||||
{token ? "Bienvenue !" : "Connexion"}
|
||||
</Typography>
|
||||
<Box className="glass-orb orb-1" />
|
||||
<Box className="glass-orb orb-2" />
|
||||
<Box className="glass-orb orb-3" />
|
||||
|
||||
{token ? (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => navigate("/admin/dashboard")}
|
||||
sx={{ mt: 3, width: "100%" }}
|
||||
<Grid
|
||||
container
|
||||
spacing={4}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{ position: "relative", zIndex: 1, maxWidth: 1180, mx: "auto" }}
|
||||
>
|
||||
<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
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={handleLogout}
|
||||
sx={{ mt: 2, width: "100%" }}
|
||||
>
|
||||
Déconnexion
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<form onSubmit={handleLogin}>
|
||||
<TextField
|
||||
label="Nom d'utilisateur"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label="Mot de passe d'application"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
type="password"
|
||||
value={appPassword}
|
||||
onChange={(e) => setAppPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
sx={{ mt: 3, width: "100%" }}
|
||||
>
|
||||
Se connecter
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="Nom d’utilisateur"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255,255,255,0.95)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Mot de passe d’application"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
type="password"
|
||||
value={appPassword}
|
||||
onChange={(e) => setAppPassword(e.target.value)}
|
||||
required
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255,255,255,0.95)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{error ? (
|
||||
<Typography color="error" variant="body2">
|
||||
{error}
|
||||
</Typography>
|
||||
) : null}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
sx={{ ...composeButtonSx("primary"), width: "100%" }}
|
||||
>
|
||||
{loading ? (
|
||||
<CircularProgress size={20} sx={{ color: "#ffffff" }} />
|
||||
) : (
|
||||
"Se connecter"
|
||||
)}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default TestLogin;
|
||||
export default TestLogin;
|
||||
|
||||
@ -1,8 +1,17 @@
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
import { getToken } from "../../auth";
|
||||
import "../../assets/styleCours.css";
|
||||
|
||||
const toolsApps = [
|
||||
{
|
||||
@ -46,40 +55,172 @@ function Tools() {
|
||||
navigate(app.url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: "100vh", p: 9 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 3 }}>
|
||||
<Button
|
||||
startIcon={<ArrowBackIcon />}
|
||||
onClick={() => navigate("/admin/dashboard")}
|
||||
sx={{ mr: 2 }}
|
||||
>
|
||||
Retour au dashboard
|
||||
</Button>
|
||||
<Typography variant="h4" sx={{ fontWeight: "bold" }}>
|
||||
Mes applications
|
||||
</Typography>
|
||||
</Box>
|
||||
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)",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
<Grid container spacing={4}>
|
||||
{toolsApps.map((app) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={app.id}>
|
||||
<Card sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6">{app.name}</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{app.description}
|
||||
</Typography>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Button variant="contained" color="primary" onClick={() => handleOpenApp(app)}>
|
||||
Ouvrir
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
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={<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>
|
||||
);
|
||||
}
|
||||
|
||||
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 dotenv from "dotenv";
|
||||
import cloudinaryRoute from "./cloudinaryRoute.js";
|
||||
import cors from "cors";
|
||||
import cloudinaryRoute from "./cloudinaryRoute.js";
|
||||
import contactRoute from "./contactRoute.js";
|
||||
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
// origin: "https://octopusdesign.fr"
|
||||
origin: ["https://octopusdesign.fr", "http://localhost:3000"]
|
||||
}));
|
||||
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || "")
|
||||
.split(",")
|
||||
.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", contactRoute);
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
app.listen(PORT, () => {
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"axios": "^1.6.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2"
|
||||
"express": "^4.18.2",
|
||||
"nodemailer": "^6.9.11"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user