diff --git a/.DS_Store b/.DS_Store
index 1e7e613..4eeb8ae 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/README.md b/README.md
index 9c2b6b4..d75e959 100644
--- a/README.md
+++ b/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.
diff --git a/frontend/.env.local.example b/frontend/.env.local.example
new file mode 100644
index 0000000..6a7becd
--- /dev/null
+++ b/frontend/.env.local.example
@@ -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
diff --git a/frontend/src/assets/styleCours.css b/frontend/src/assets/styleCours.css
index 66edccf..b9b8fff 100644
--- a/frontend/src/assets/styleCours.css
+++ b/frontend/src/assets/styleCours.css
@@ -119,4 +119,256 @@ ul li {
.c4lv-readingcontext {
padding: 30px 40px 32px 40px;
font-family: sans-serif;
-}
\ No newline at end of file
+}
+
+/* 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;
+}
diff --git a/frontend/src/components/Admin/ListeCours.jsx b/frontend/src/components/Admin/ListeCours.jsx
index a147405..f3e4bcf 100644
--- a/frontend/src/components/Admin/ListeCours.jsx
+++ b/frontend/src/components/Admin/ListeCours.jsx
@@ -26,11 +26,13 @@ import {
InputLabel,
Select,
MenuItem,
+ IconButton,
} from "@mui/material";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
import DoneAllIcon from "@mui/icons-material/DoneAll";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
+import CloseIcon from "@mui/icons-material/Close";
import axios from "axios";
import { useNavigate } from "react-router-dom";
import { getToken } from "../../auth";
@@ -47,6 +49,148 @@ const ITEM_TYPE_META = {
h5p: { label: "Activité H5P", chipColor: "info" },
};
+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.2,
+ py: 0.65,
+ fontSize: "0.78rem",
+ minHeight: 32,
+ },
+ medium: {
+ px: 2.9,
+ py: 0.85,
+ fontSize: "0.86rem",
+ minHeight: 38,
+ },
+ large: {
+ px: 3.6,
+ py: 1.05,
+ fontSize: "0.94rem",
+ minHeight: 44,
+ },
+});
+
+const BUTTON_VARIANT_SX = Object.freeze({
+ primary: {
+ background: "linear-gradient(135deg, #315397, #7bc0ff)",
+ color: "#ffffff",
+ boxShadow: "0 18px 28px -18px rgba(11, 26, 61, 0.6)",
+ border: "1px solid rgba(255,255,255,0.35)",
+ "&:hover": {
+ background: "linear-gradient(135deg, #26467d, #6fb6ff)",
+ boxShadow: "0 24px 34px -18px rgba(11, 26, 61, 0.62)",
+ },
+ "&.Mui-disabled": {
+ color: "rgba(255,255,255,0.7)",
+ 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)",
+ },
+ },
+ secondary: {
+ background: "rgba(49,83,151,0.12)",
+ color: "#315397",
+ border: "1px solid rgba(49,83,151,0.2)",
+ backdropFilter: "blur(8px)",
+ "&:hover": {
+ background: "rgba(49,83,151,0.18)",
+ borderColor: "rgba(49,83,151,0.35)",
+ boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.4)",
+ },
+ "&.Mui-disabled": {
+ color: "rgba(49,83,151,0.45)",
+ borderColor: "rgba(49,83,151,0.12)",
+ background: "rgba(49,83,151,0.08)",
+ },
+ },
+ outline: {
+ background: "rgba(255,255,255,0.78)",
+ color: "#315397",
+ border: "1px solid rgba(49,83,151,0.35)",
+ "&:hover": {
+ background: "rgba(255,255,255,0.92)",
+ borderColor: "rgba(49,83,151,0.55)",
+ boxShadow: "0 14px 22px -18px rgba(11, 26, 61, 0.45)",
+ },
+ "&.Mui-disabled": {
+ color: "rgba(49,83,151,0.4)",
+ borderColor: "rgba(49,83,151,0.18)",
+ },
+ },
+ ghost: {
+ background: "rgba(255,255,255,0.14)",
+ 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.22)",
+ borderColor: "rgba(255,255,255,0.35)",
+ boxShadow: "0 18px 28px -18px rgba(6, 18, 38, 0.45)",
+ },
+ "&.Mui-disabled": {
+ color: "rgba(255,255,255,0.55)",
+ borderColor: "rgba(255,255,255,0.18)",
+ },
+ },
+ success: {
+ background: "linear-gradient(135deg, #2dc897, #6ef7c4)",
+ color: "#0f2c36",
+ border: "1px solid rgba(45,200,151,0.35)",
+ boxShadow: "0 18px 28px -18px rgba(26, 87, 76, 0.42)",
+ "&:hover": {
+ background: "linear-gradient(135deg, #27b486, #60e0b0)",
+ boxShadow: "0 24px 34px -18px rgba(26, 87, 76, 0.48)",
+ },
+ "&.Mui-disabled": {
+ color: "rgba(15,44,54,0.48)",
+ background: "rgba(45,200,151,0.28)",
+ boxShadow: "none",
+ border: "1px solid rgba(45,200,151,0.18)",
+ },
+ },
+ 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)",
+ },
+ "&.Mui-disabled": {
+ color: "rgba(58,10,15,0.5)",
+ background: "rgba(240,91,107,0.28)",
+ border: "1px solid rgba(240,91,107,0.18)",
+ boxShadow: "none",
+ },
+ },
+});
+
+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 PUBLIC_LINKS_STORAGE_KEY = "listeCoursPublicLinks";
const DEFAULT_LINK_DURATION_HOURS = 24;
const LINK_DURATION_PRESETS = [
@@ -86,6 +230,7 @@ const ListeCours = () => {
const [publicLinks, setPublicLinks] = useState({});
const [linkDurationSelections, setLinkDurationSelections] = useState({});
const [progressByCourse, setProgressByCourse] = useState({});
+ const [courseSearch, setCourseSearch] = useState("");
useEffect(() => {
if (typeof window === "undefined") {
return;
@@ -742,26 +887,145 @@ const ListeCours = () => {
fetchAssignments(cours.id);
};
+ const handleCloseModal = React.useCallback(() => {
+ setSelected(null);
+ setPages([]);
+ setSectionNames({});
+ setSelectedSection(null);
+ setH5pActivities([]);
+ setSelectedH5P(null);
+ setModuleSections({});
+ setAssignments([]);
+ setAssignmentsRaw([]);
+ setQuizzes([]);
+ setSearchQuery("");
+ setShowOnlyMedia(false);
+ }, []);
+
const buildPdfContainer = (titleText, htmlContent) => {
- const now = new Date().toLocaleDateString();
+ const now = new Date().toLocaleDateString("fr-FR");
+ const parser = new DOMParser();
+ const parsed = parser.parseFromString(
+ `
${htmlContent || ""}
`,
+ "text/html"
+ );
+ const content = parsed.body;
+
+ content.querySelectorAll("script, style").forEach((node) => node.remove());
+ content.querySelectorAll("iframe").forEach((iframe) => {
+ const link = document.createElement("p");
+ link.innerHTML = `Média externe : ${iframe.src}`;
+ iframe.replaceWith(link);
+ });
+ content.querySelectorAll("video, audio").forEach((media) => {
+ const info = document.createElement("p");
+ info.innerHTML = "Média intégré : disponible uniquement en ligne.";
+ media.replaceWith(info);
+ });
+ content.querySelectorAll("img").forEach((img) => {
+ img.removeAttribute("height");
+ img.removeAttribute("width");
+ img.style.maxWidth = "100%";
+ img.style.height = "auto";
+ img.style.display = "block";
+ img.style.margin = "12px auto";
+ const parent = img.parentElement;
+ if (parent && parent.tagName.toLowerCase() === "p") {
+ parent.style.textAlign = "center";
+ }
+ });
+ content.querySelectorAll("table").forEach((table) => {
+ table.style.width = "100%";
+ table.style.borderCollapse = "collapse";
+ table.style.margin = "16px 0";
+ table.querySelectorAll("td, th").forEach((cell) => {
+ cell.style.border = "1px solid #ccd3e0";
+ cell.style.padding = "8px 10px";
+ });
+ });
+
const container = document.createElement("div");
- container.style.padding = "20px";
- container.style.fontFamily = "Nunito, sans-serif";
- container.style.width = "100%";
- container.style.maxWidth = "800px";
- container.style.boxSizing = "border-box";
- container.style.overflowWrap = "break-word";
-
- const title = document.createElement("h3");
- title.textContent = titleText;
- container.appendChild(title);
-
- const body = document.createElement("div");
- body.innerHTML = htmlContent;
- container.appendChild(body);
+ container.className = "pdf-export-container";
+ container.appendChild(content);
const style = document.createElement("style");
- style.textContent = `img { max-width: 100%; height: auto; } h1, h2, h3, h4, p { page-break-inside: avoid; }`;
+ style.textContent = `
+ @page {
+ size: A4 portrait;
+ margin: 18mm;
+ }
+ * {
+ box-sizing: border-box;
+ }
+ body {
+ font-family: "Nunito", "Helvetica Neue", Arial, sans-serif;
+ color: #0b1a3d;
+ }
+ .pdf-export-container {
+ width: 100%;
+ max-width: 720px;
+ padding: 0 24px 24px;
+ font-family: "Nunito", "Helvetica Neue", Arial, sans-serif;
+ font-size: 13px;
+ line-height: 1.6;
+ color: #0b1a3d;
+ }
+ .pdf-export-container h1,
+ .pdf-export-container h2,
+ .pdf-export-container h3,
+ .pdf-export-container h4 {
+ color: #315397;
+ font-weight: 700;
+ margin: 18px 0 12px;
+ page-break-after: avoid;
+ }
+ .pdf-export-container p {
+ margin: 12px 0;
+ page-break-inside: avoid;
+ }
+ .pdf-export-container ul,
+ .pdf-export-container ol {
+ margin: 12px 0 12px 18px;
+ padding: 0;
+ }
+ .pdf-export-container li {
+ margin-bottom: 6px;
+ }
+ .pdf-export-container blockquote {
+ border-left: 4px solid #315397;
+ padding-left: 12px;
+ margin: 16px 0;
+ font-style: italic;
+ background: rgba(49, 83, 151, 0.06);
+ }
+ .pdf-export-container a {
+ color: #315397;
+ text-decoration: underline;
+ word-break: break-word;
+ }
+ .pdf-header {
+ text-align: center;
+ margin-bottom: 24px;
+ }
+ .pdf-header h2 {
+ font-size: 22px;
+ margin: 0;
+ }
+ .pdf-header p {
+ font-size: 12px;
+ color: #5b6b91;
+ margin: 4px 0 0;
+ }
+ `;
+
+ const header = document.createElement("div");
+ header.className = "pdf-header";
+ header.innerHTML = `
+ ${titleText || "Cours Octopus"}
+ Exporté le ${now}
+ `;
+
+ container.prepend(header);
container.appendChild(style);
return { container, now };
@@ -770,11 +1034,11 @@ const ListeCours = () => {
const exportPagePDF = (page) => {
const { container, now } = buildPdfContainer(page.name, page.content);
const opt = {
- margin: 0.5,
+ margin: [18, 15, 20, 15],
filename: `${page.name}_${now}.pdf`,
- image: { type: "jpeg", quality: 0.98 },
- html2canvas: { scale: 2, useCORS: true },
- jsPDF: { unit: "in", format: "letter", orientation: "portrait" },
+ image: { type: "jpeg", quality: 0.95 },
+ html2canvas: { scale: 2, useCORS: true, scrollX: 0, scrollY: 0 },
+ jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};
window.html2pdf().set(opt).from(container).save();
};
@@ -786,11 +1050,11 @@ const ListeCours = () => {
htmlContent
);
const opt = {
- margin: 0.5,
+ margin: [18, 15, 20, 15],
filename: `${assignment.name}_${now}.pdf`,
- image: { type: "jpeg", quality: 0.98 },
- html2canvas: { scale: 2, useCORS: true },
- jsPDF: { unit: "in", format: "letter", orientation: "portrait" },
+ image: { type: "jpeg", quality: 0.95 },
+ html2canvas: { scale: 2, useCORS: true, scrollX: 0, scrollY: 0 },
+ jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};
window.html2pdf().set(opt).from(container).save();
};
@@ -1166,6 +1430,39 @@ const ListeCours = () => {
buildProgressKey,
]);
+ const courseStats = React.useMemo(() => {
+ const total = cours.length;
+ if (total === 0) {
+ return { total: 0, completed: 0, avgProgress: 0 };
+ }
+ let completed = 0;
+ let progressSum = 0;
+ cours.forEach((course) => {
+ const ratio = progressByCourse[course.id]?.progressRatio ?? 0;
+ progressSum += ratio;
+ if (ratio >= 0.999) {
+ completed += 1;
+ }
+ });
+ return {
+ total,
+ completed,
+ avgProgress: progressSum / total,
+ };
+ }, [cours, progressByCourse]);
+
+ const filteredCourses = React.useMemo(() => {
+ const query = courseSearch.trim().toLowerCase();
+ if (!query) {
+ return cours;
+ }
+ return cours.filter((course) =>
+ course.fullname?.toLowerCase?.().includes(query)
+ );
+ }, [cours, courseSearch]);
+
+ const filteredCoursesCount = filteredCourses.length;
+
const totalProgramItems = programItems.length;
const filteredProgramCount = filteredProgramItems.length;
const filteredLabel =
@@ -1196,6 +1493,30 @@ const ListeCours = () => {
const programmeInfoMessage =
"Visualisez les cours, devoirs, tests et activités interactives depuis un seul espace.";
+ const modalSteps = React.useMemo(
+ () => [
+ {
+ id: "01",
+ title: "Explorer les chapitres",
+ description:
+ "Consultez l’ensemble des contenus du parcours : cours, devoirs, tests et activités interactives.",
+ },
+ {
+ id: "02",
+ title: "Affiner les résultats",
+ description:
+ "Filtrez par chapitre, recherchez un mot-clé ou limitez-vous aux cours riches en médias.",
+ },
+ {
+ id: "03",
+ title: "Ouvrir un contenu",
+ description:
+ "Sélectionnez un programme pour consulter le contenu, générer un lien public ou exporter en PDF.",
+ },
+ ],
+ []
+ );
+
const handleItemTitleClick = React.useCallback(
(item) => {
if (!item) {
@@ -1246,178 +1567,633 @@ const ListeCours = () => {
);
return (
-
- }
- onClick={() => navigate("/admin/dashboard")}
- sx={{ mb: 3 }}
- >
- Retour au dashboard
-
-
- Cours
-
- {loading ? (
-
-
-
- ) : (
-
- {cours.map((course) => {
- const courseProgress = progressByCourse[course.id];
- const progressRatio = courseProgress?.progressRatio ?? null;
- const progressLabel = courseProgress
- ? `${courseProgress.viewedItems}/${courseProgress.totalItems} éléments`
- : null;
- return (
-
- handleSelect(course)}
+
+
+
+
+
+
+ }
+ onClick={() => navigate("/admin/dashboard")}
+ sx={{
+ ...composeButtonSx("ghost"),
+ alignSelf: { xs: "stretch", md: "flex-start" },
+ "& .MuiButton-startIcon": {
+ color: "inherit",
+ },
+ }}
+ >
+ Retour au dashboard
+
+
+
+
+
+
+
+ Espace pédagogique
+
+
+ Explorer vos cours Octopus
+
+
+ Accédez aux contenus, devoirs, tests et activités interactives
+ dans une interface fluide. Filtrez vos programmes et reprenez
+ exactement là où vous vous êtes arrêté.
+
+
+
+
+
+ Progression moyenne des cours
+
+
+
+
-
-
- {course.fullname}
-
- {progressRatio !== null ? (
-
-
- {progressLabel ? (
-
- {progressLabel}
-
+ {`${courseStats.completed}/${courseStats.total} cours terminés`}
+
+
+ {`${Math.round(courseStats.avgProgress * 100)}%`}
+
+
+
+
+
+
+
+
+
+
+
+
+ setCourseSearch(event.target.value)}
+ InputProps={{
+ sx: {
+ borderRadius: 3,
+ bgcolor: "rgba(255,255,255,0.72)",
+ backdropFilter: "blur(8px)",
+ },
+ }}
+ />
+
+ Astuce : ouvrez un cours pour accéder aux chapitres, créer des liens
+ de partage temporaires et suivre votre progression.
+
+
+
+ {loading ? (
+
+
+
+ ) : filteredCoursesCount > 0 ? (
+
+ {filteredCourses.map((course) => {
+ const courseProgress = progressByCourse[course.id];
+ const progressRatio = courseProgress?.progressRatio ?? 0;
+ const progressLabel = courseProgress
+ ? `${courseProgress.viewedItems}/${courseProgress.totalItems} éléments`
+ : "Progression en attente";
+ const completionPercent = Math.min(
+ 100,
+ Math.max(0, progressRatio * 100)
+ );
+ return (
+
+ handleSelect(course)}
+ sx={{
+ cursor: "pointer",
+ borderRadius: 4,
+ px: 2,
+ py: 3,
+ display: "flex",
+ flexDirection: "column",
+ gap: 2,
+ position: "relative",
+ overflow: "hidden",
+ transition:
+ "transform 0.35s ease, box-shadow 0.35s ease, border-color 0.35s ease",
+ background: "rgba(255,255,255,0.26)",
+ border: "1px solid rgba(255,255,255,0.35)",
+ backdropFilter: "blur(18px)",
+ boxShadow:
+ "0 28px 36px -24px rgba(8, 22, 68, 0.78)",
+ "&:hover": {
+ transform: "translateY(-6px)",
+ borderColor: "rgba(255,255,255,0.7)",
+ boxShadow:
+ "0 36px 48px -22px rgba(9, 29, 92, 0.86)",
+ },
+ }}
+ >
+
+
+
+ {course.fullname}
+
+ {progressRatio >= 0.999 ? (
+
) : null}
+
+
+ {course.shortname || "Programme Octopus"}
+
+
+
+
+ {progressLabel}
+
- ) : null}
-
-
-
- );
- })}
-
- )}
+
+
+
+
+ );
+ })}
+
+ ) : (
+
+ Aucun cours ne correspond à votre recherche. Essayez un autre mot-clé.
+
+ )}
+
{
- setSelected(null);
- setPages([]);
- setSectionNames({});
- setSelectedSection(null);
- setH5pActivities([]);
- setSelectedH5P(null);
- setModuleSections({});
- setAssignments([]);
- setAssignmentsRaw([]);
- setQuizzes([]);
- setSearchQuery("");
- setShowOnlyMedia(false);
- }}
+ onClose={handleCloseModal}
closeAfterTransition
BackdropComponent={Backdrop}
- BackdropProps={{ timeout: 300 }}
+ BackdropProps={{
+ timeout: 300,
+ sx: {
+ backdropFilter: "blur(12px)",
+ backgroundColor: "rgba(6, 18, 38, 0.55)",
+ },
+ }}
>
-
+
+
+
+
+
- {selected?.fullname}
-
-
-
-
-
-
+
-
- {overallStats.viewedItems}/{overallStats.totalItems} éléments
-
-
- {overallStats.completedChapters}/{overallStats.totalChapters} chapitres
-
-
+ Parcours sélectionné
+
+
+ {selected?.fullname}
+
+
+ Retrouvez tous les contenus du parcours et reprenez le fil de
+ votre apprentissage en un seul coup d’œil.
+
-
-
-
-
-
- 1. Explorer les Programmes
-
-
- Accédez aux cours, devoirs, tests et activités interactives liés
- à ce parcours.
-
+
+
+
+
+
+
+
+
+ {overallStats.viewedItems}/{overallStats.totalItems} éléments
+
+
+ {Math.round(overallStats.progressRatio * 100)}%
+
+
+
+
+
+
-
-
+ {modalSteps.map((step) => (
+
+
+ {step.id}
+
+
+ {step.title}
+
+
+ {step.description}
+
+
+ ))}
+
+
+
+
- 2. Affiner les résultats
-
+
+
+ Affiner votre sélection
+
+
+ Combinez les filtres pour accéder rapidement au chapitre ou au
+ contenu recherché.
+
+
+
{
if (!option || typeof option === "string") {
@@ -1485,7 +2261,13 @@ const ListeCours = () => {
{
{hasPageItems ? (
{
label={`Total ${totalProgramItems}`}
/>
-
+
{programmeInfoMessage}
-
- 3. Sélectionner un programme
-
+
+ Programmes du cours
+
+
+ Cliquez sur un programme pour afficher son contenu détaillé.
+
+
{loadingPages ? (
@@ -1604,7 +2413,19 @@ const ListeCours = () => {
);
return (
-
+
{
>
{sectionLabel}
@@ -1708,6 +2533,7 @@ const ListeCours = () => {
{activeLinkEntry ? (
-
- Lien actif jusqu'au {publicLinkExpiryLabel}
+
+ Lien actif jusqu'au {publicLinkExpiryLabel}
) : linkDisabled ? (
{
)}
- setSelected(null)}>
+
Fermer
@@ -1914,6 +2751,12 @@ const ListeCours = () => {
BackdropProps={{
timeout: 300,
invisible: previewHasH5P || Boolean(selectedQuiz),
+ sx: previewHasH5P || Boolean(selectedQuiz)
+ ? {}
+ : {
+ backdropFilter: "blur(10px)",
+ backgroundColor: "rgba(6, 18, 38, 0.45)",
+ },
}}
>
{
left: "8%",
width: "80%",
height: "100vh",
- bgcolor: "background.paper",
- p: 4,
+ background: "rgba(255,255,255,0.95)",
+ backdropFilter: "blur(14px)",
+ p: { xs: 3, md: 4 },
overflowY: "auto",
fontFamily: "Nunito, sans-serif",
- color: "#315397",
+ color: "#0b1a3d",
+ boxShadow: "0 44px 60px -45px rgba(7, 16, 45, 0.65)",
+ borderRadius: { xs: 0, md: 4 },
}}
>
{
selectedQuiz?.name}
{
setSelectedPage(null);
setSelectedAssignment(null);
diff --git a/frontend/src/components/EditPost.jsx b/frontend/src/components/EditPost.jsx
index ffb1e73..f746f0b 100644
--- a/frontend/src/components/EditPost.jsx
+++ b/frontend/src/components/EditPost.jsx
@@ -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 (
-
-
+
+
+
+
+
+
navigate("/admin/gestion-articles")}
- variant="outlined"
+ variant="contained"
startIcon={}
+ sx={composeButtonSx("ghost")}
>
- Retour
+ Retour aux articles
-
-
- Modifier l'article
-
-
-
-
-
-
- Image depuis ton ordi :
- {
- setImageUrl(url);
- convertUrlToFile(url).then(setImageFile);
+
+
+
+ >
+
+
+
+ }
+ 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,
+ }}
+ />
+
+
-
- Ou choisir une image déjà envoyée :
-
- {
- setImageUrl(url);
- const file = await convertUrlToFile(url);
- setImageFile(file);
- }}
- />
+ {loading ? (
+
+ Chargement de l’article…
+
+ ) : (
+
+ setTitle(event.target.value)}
+ required
+ sx={{
+ mb: 3,
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 2,
+ backgroundColor: "rgba(255,255,255,0.94)",
+ },
+ }}
+ />
- {imageUrl && (
-
- Aperçu :
-
+
+
+
+
+
+
+ Remplacer le visuel
+
+
+ Importez une nouvelle image optimisée pour vos
+ contenus.
+
+
+ {
+ setImageUrl(url);
+ convertUrlToFile(url).then(setImageFile);
+ }}
+ />
+
+
+
+
+
+
+
+ Sélectionner depuis la médiathèque
+
+
+ Choisissez un visuel existant dans Cloudinary.
+
+ {
+ setImageUrl(url);
+ const file = await convertUrlToFile(url);
+ setImageFile(file);
+ }}
+ />
+
+
+
+ }
+ disabled={!canSubmit || submitting}
+ sx={{
+ ...composeButtonSx("primary"),
+ minWidth: 220,
+ }}
+ >
+ {submitting ? "Enregistrement…" : "Mettre à jour"}
+
+ {
+ setTitle("");
+ setContent("");
+ setImageUrl(null);
+ setImageFile(null);
+ }}
+ >
+ Réinitialiser
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Rappels éditoriaux
+
+
+ • Vérifiez la cohérence des titres et sous-titres.
+ • Ajoutez des liens internes vers vos formations Octopus.
+ • N’oubliez pas de relire la mise en forme dans l’aperçu
+ WordPress.
+
+
+
+
+
+
+
-
- )}
-
- }
- >
- Mettre à jour
-
-
-
-
+ >
+
+ Aperçu visuel
+
+ {imageUrl ? (
+
+ ) : (
+
+ Aucun visuel sélectionné pour le moment
+
+ )}
+
+ Ce visuel illustrera l’article dans la liste des publications
+ et sur les réseaux sociaux.
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/components/GestionBureauEtudeACF.jsx b/frontend/src/components/GestionBureauEtudeACF.jsx
index baa13cd..83e20ec 100644
--- a/frontend/src/components/GestionBureauEtudeACF.jsx
+++ b/frontend/src/components/GestionBureauEtudeACF.jsx
@@ -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 Chargement...;
+ 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 (
+
+
+
+
+
+
+
+ Chargement de la page Bureau d’étude…
+
+
+
+ );
+ }
return (
-
-
-
+
+
+
+
+
+
+
}
- variant="outlined"
onClick={() => navigate("/admin/dashboard")}
- sx={{ mb: 2 }}
+ variant="contained"
+ sx={composeButtonSx("ghost")}
>
- Retour
+ Retour au dashboard
-
- Modifier la Page Formations
-
+
+
+ Bureau d’étude Octopus
+
+
+ Gestion des contenus ACF
+
+
+ Mettez à jour l’introduction, la méthodologie et les expertises du
+ bureau d’étude pour garder des informations pertinentes et à jour.
+
+
+
- {error && {error}}
- {successMessage && {successMessage}}
+
+
+
+
+ Compétences listées
+
+
+ {stats.competencies}
+
+
+ Ajoutez des compétences pour valoriser l’expertise Octopus.
+
+
+
+
+
+
+ Expertises spécifiques
+
+
+ {stats.expertises}
+
+
+ Développez vos expertises pour renforcer la crédibilité de l’équipe.
+
+
+
+
-
-
-
+
+ {getRepeater("liste_des_competences").map((item, index) => (
+
+
+
+ handleRepeaterChange(
+ "liste_des_competences",
+ index,
+ "comp_titre",
+ event.target.value
+ )
+ }
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 2,
+ backgroundColor: "rgba(255,255,255,0.9)",
+ },
+ }}
+ />
+
+ handleRepeaterChange(
+ "liste_des_competences",
+ index,
+ "comp_description",
+ event.target.value
+ )
+ }
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 2,
+ backgroundColor: "rgba(255,255,255,0.9)",
+ },
+ }}
+ />
+ }
+ onClick={() =>
+ deleteRepeaterItem("liste_des_competences", index)
+ }
+ variant="contained"
+ sx={composeButtonSx("danger", "small")}
+ >
+ Supprimer
+
+
+
+ ))}
+
+
+ }
+ variant="contained"
+ onClick={() =>
+ addRepeaterItem("liste_des_competences", {
+ comp_titre: "",
+ comp_description: "",
+ })
+ }
+ sx={composeButtonSx("primary", "small")}
+ >
+ Ajouter une compétence
+
+
+
+
+
+
+
+
+
+
+ Expertises spécifiques
+
+
+ Décrivez les expertises métiers ou sectorielles qui
+ distinguent votre bureau d’étude.
+
+
+
+
+ {getRepeater("expertises_specifiques").map((item, index) => (
+
+
+
+ handleRepeaterChange(
+ "expertises_specifiques",
+ index,
+ "expertise_nom",
+ event.target.value
+ )
+ }
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 2,
+ backgroundColor: "rgba(255,255,255,0.9)",
+ },
+ }}
+ />
+
+ handleRepeaterChange(
+ "expertises_specifiques",
+ index,
+ "expertise_details",
+ event.target.value
+ )
+ }
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 2,
+ backgroundColor: "rgba(255,255,255,0.9)",
+ },
+ }}
+ />
+ }
+ onClick={() =>
+ deleteRepeaterItem("expertises_specifiques", index)
+ }
+ variant="contained"
+ sx={composeButtonSx("danger", "small")}
+ >
+ Supprimer
+
+
+
+ ))}
+
+
+ }
+ variant="contained"
+ onClick={() =>
+ addRepeaterItem("expertises_specifiques", {
+ expertise_nom: "",
+ expertise_details: "",
+ })
+ }
+ sx={composeButtonSx("primary", "small")}
+ >
+ Ajouter une expertise
+
+
+
+
+
+ }
+ disabled={submitting}
+ sx={{ ...composeButtonSx("primary"), alignSelf: "flex-end", minWidth: 220 }}
+ >
+ {submitting ? "Enregistrement…" : "Enregistrer les modifications"}
+
+
+
+
);
}
-export default EditPageEtude;
\ No newline at end of file
+export default EditPageEtude;
diff --git a/frontend/src/components/GestionPagesACF.jsx b/frontend/src/components/GestionPagesACF.jsx
index 5be97ef..464ed3b 100644
--- a/frontend/src/components/GestionPagesACF.jsx
+++ b/frontend/src/components/GestionPagesACF.jsx
@@ -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 Chargement...;
- if (error) return {error};
+ 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 (
-
+ if (loading) {
+ return (
- }
- variant="outlined"
- color="secondary"
- onClick={() => navigate("/admin/dashboard")}
- >
- Retour au Dashboard
-
-
- 📄 Gestion des Pages Services
-
- }
- variant="contained"
- color="primary"
- onClick={() =>
- window.open("https://it.sveitl.synology.me/", "_blank")
- }
- >
- Ticket
-
+
+
+
+
+
+
+ Chargement des pages services…
+
+
-
-
-
-
-
- Titre
-
-
+
+
+
+
+
+
+ {error}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ }
+ variant="contained"
+ onClick={() => navigate("/admin/dashboard")}
+ sx={composeButtonSx("ghost")}
+ >
+ Retour au dashboard
+
+
+
+
+ Gestion des pages services
+
+
+ Contenus ACF disponibles
+
+
+ Accédez rapidement aux pages service Octopus équipées de champs
+ ACF pour les mettre à jour en quelques clics.
+
+
+
+ }
+ variant="contained"
+ onClick={() =>
+ window.open("https://it.sveitl.synology.me/", "_blank")
+ }
+ sx={composeButtonSx("primary")}
+ >
+ Ouvrir un ticket
+
+
+
+
+
+
+
- Actions
-
-
-
-
- {pages.length > 0 ? (
- pages.map((page) => (
-
-
- {page.title.rendered}
-
-
- }
- variant="outlined"
- color="warning"
- onClick={() => navigate(`/admin/edit-page/${page.id}`)}
+ Pages référencées
+
+
+ {pages.length}
+
+
+ Ensemble des pages service disposant de champs ACF personnalisés.
+
+
+
+
+
+
+ Pages visibles
+
+
+ {filteredPages.length}
+
+
+ Résultats actuels après filtrage par titre.
+
+
+
+
+
+
+
+
+
+ Rechercher une page
+
+
+ Filtrez par titre pour trouver une page service en un instant.
+
+
+ setSearchTerm(event.target.value)}
+ sx={{
+ maxWidth: 360,
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 999,
+ backgroundColor: "rgba(255,255,255,0.94)",
+ },
+ }}
+ />
+
+
+
+ {filteredPages.length === 0 ? (
+
+
+ Aucune page trouvée
+
+
+ Ajustez votre recherche ou vérifiez que la page dispose bien de
+ champs ACF activés.
+
+
+ ) : (
+
+ {filteredPages.map((page, index) => (
+ = 1 ? 2 : 0,
+ md: index >= 3 ? 4 : 0,
+ },
+ }}
+ >
+
+
+
- Modifier
-
-
-
- ))
- ) : (
-
-
- Aucune page avec des champs ACF trouvée.
-
-
- )}
-
-
-
+
+
+
+ {page.title.rendered}
+
+
+ Cliquez sur "Modifier" pour ajuster les contenus
+ de cette page service depuis l’éditeur ACF dédié.
+
+
+ }
+ variant="contained"
+ sx={composeButtonSx("outline", "small")}
+ onClick={() => navigate(`/admin/edit-page/${page.id}`)}
+ >
+ Modifier
+
+
+
+ ))}
+
+ )}
+
);
}
diff --git a/frontend/src/components/Pages/Contact.jsx b/frontend/src/components/Pages/Contact.jsx
index 3352755..ef1397e 100644
--- a/frontend/src/components/Pages/Contact.jsx
+++ b/frontend/src/components/Pages/Contact.jsx
@@ -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 (
{/* SEO */}
@@ -490,12 +517,17 @@ const Contact = () => {
setSuccessMessage(false)}
+ onClose={handleCloseAlert}
+ anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
>
- setSuccessMessage(false)} severity="success">
- Message envoyé avec succès !
+
+ {alertState.message}
diff --git a/frontend/src/components/Pages/CreatePost.jsx b/frontend/src/components/Pages/CreatePost.jsx
index 73893d4..ede5a34 100644
--- a/frontend/src/components/Pages/CreatePost.jsx
+++ b/frontend/src/components/Pages/CreatePost.jsx
@@ -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 (
-
-
+
+
+
+
+
+
}
- variant="outlined"
- color="secondary"
+ variant="contained"
onClick={() => navigate("/admin/gestion-articles")}
- sx={{ mb: 2 }}
+ sx={composeButtonSx("ghost")}
>
- Retour
+ Retour aux articles
-
-
- Créer un article
-
-
-
-
-
+ Publication Octopus
+
+
+ Rédiger un nouvel article
+
+
+ Structurez votre contenu, ajoutez une image mise en avant et
+ publiez directement sur le blog WordPress Octopus.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setTitle(event.target.value)}
+ required
+ sx={{
+ mb: 3,
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 2,
+ backgroundColor: "rgba(255,255,255,0.94)",
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+ Importer une image
+
+
+ Ajoutez un visuel optimisé pour les réseaux sociaux
+ (format 1600x900 recommandé).
+
+
+ setImageUrl(url)}
+ />
+
+
+
+
+
+
+
+ Sélectionner depuis la médiathèque
+
+
+ Retrouver un visuel déjà téléversé dans Cloudinary.
+
+ setImageUrl(url)}
+ />
+
+
+
+ }
+ disabled={!canPublish || submitting}
+ sx={{ ...composeButtonSx("primary"), minWidth: 200 }}
+ >
+ {submitting ? "Publication..." : "Publier l'article"}
+
+ {
+ setTitle("");
+ setContent("");
+ setImageUrl(null);
+ }}
+ sx={composeButtonSx("outline")}
+ >
+ Réinitialiser
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Conseils de rédaction
+
+
+ • Introduisez le sujet dans les 250 premiers caractères pour
+ optimiser le SEO.
+ • Utilisez des sous-titres (<h2>) pour
+ clarifier la structure.
+ • Ajoutez des appels à l’action vers vos formations Octopus.
+
+
+
+
+
+
+
+
+ Aperçu visuel
+
+ {imageUrl ? (
+
+ ) : (
+
+ Aucun visuel sélectionné pour le moment
+
+ )}
+
+ Ce visuel sera utilisé comme image de couverture sur la liste
+ des articles et les réseaux sociaux.
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/components/Pages/Dashboard.jsx b/frontend/src/components/Pages/Dashboard.jsx
index ef34ef3..654f096 100644
--- a/frontend/src/components/Pages/Dashboard.jsx
+++ b/frontend/src/components/Pages/Dashboard.jsx
@@ -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: ,
+ accent: "rgba(123,192,255,0.35)",
+ },
+ {
+ title: "Tools studio",
+ description: "Centralisez vos outils internes et automatisez les tâches.",
+ navigateTo: "/admin/tools",
+ icon: ,
+ accent: "rgba(255,214,150,0.32)",
+ },
+ {
+ title: "Articles",
+ description: "Rédigez, programmez et optimisez les publications du blog.",
+ navigateTo: "/admin/gestion-articles",
+ icon: ,
+ accent: "rgba(255,172,236,0.28)",
+ },
+ {
+ title: "Pages services",
+ description: "Actualisez les contenus ACF pour chaque offre Octopus.",
+ navigateTo: "/admin/pages-acf",
+ icon: ,
+ 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: ,
+ 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: ,
+ 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 (
-
- {/* Bouton de déconnexion placé en haut à gauche */}
- }
- onClick={handleLogout}
- sx={{
- position: "absolute",
- top: 10,
- left: 10,
- backgroundColor: "#d32f2f",
- "&:hover": { backgroundColor: "#b71c1c" },
- fontSize: "0.875rem",
- padding: "6px 12px",
- }}
- />
+
+
+
- {/* En-tête */}
-
-
- Tableau de Bord
+
+
+
+
+ Octopus Admin
+
+
+ Tableau de bord centralisé
+
+
+ Accédez à l’ensemble des outils de gestion : contenus éditoriaux,
+ pages services, ressources pédagogiques et automatisations.
+
+
+ }
+ onClick={handleLogout}
+ sx={composeButtonSx("ghost")}
+ >
+ Déconnexion
+
+
+
+
+
+
+
+ Priorités du jour
+
+
+ Suivez la progression des mises en ligne, synchronisez Moodle
+ et gardez un œil sur vos contenus essentiels.
+
+
+ }
+ label="Contenus pédagogiques à jour"
+ sx={{
+ bgcolor: "rgba(123,192,255,0.16)",
+ color: "#0b1a3d",
+ fontWeight: 600,
+ }}
+ />
+ }
+ label="Octopusdesign.fr en production"
+ sx={{
+ bgcolor: "rgba(255,214,150,0.18)",
+ color: "#0b1a3d",
+ fontWeight: 600,
+ }}
+ />
+
+
+
+
+
+ Actions rapides
+
+
+ {QUICK_ACTIONS.map((action) => (
+ }
+ sx={{
+ ...composeButtonSx("subtle", "large"),
+ justifyContent: "space-between",
+ width: "100%",
+ }}
+ >
+
+
+ {action.label}
+
+
+ {action.hint}
+
+
+
+ ))}
+
+
+
+
+
+
+ {KPI_CARDS.map((card) => (
+
+
+
+ {card.label.toUpperCase()}
+
+
+ {card.value}
+
+
+ {card.delta}
+
+
+
+ ))}
+
+
+
+ Espaces de gestion
- {/* Cartes du Dashboard */}
-
- {/* ✅ Gestion Page d'Accueil - Première carte */}
-
- 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",
- },
- }}
- >
-
-
-
-
-
- Gestion Page d'Accueil
-
-
-
-
-
- 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",
- },
- }}
- >
-
-
-
-
-
- Tools
-
-
-
-
-
- {/* ✅ Gérer les Articles - Deuxième carte */}
-
- 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",
- },
- }}
- >
-
-
-
-
-
- Gérer les Articles
-
-
-
-
-
- {/* ✅ Gérer les Pages ACF - Nouvelle carte */}
-
- 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",
- },
- }}
- >
-
-
-
-
-
- Gérer les Pages Services
-
-
-
-
-
- {/* ✅ Gérer les Pages ACF - Nouvelle carte */}
-
- 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",
- },
- }}
- >
-
-
-
-
-
- Gérer la page bureau d'étude
-
-
-
-
- {/* ✅ Gérer les Cours - Nouvelle carte */}
-
- 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",
- },
- }}
- >
-
-
-
-
-
- Cours
-
-
-
-
- {/* ✅ Gérer les Cours - Nouvelle carte */}
+
+ {NAV_ITEMS.map((item) => (
+
+ 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)",
+ },
+ }}
+ >
+
+
+ {item.icon}
+
+
+ {item.title}
+
+
+ {item.description}
+
+ navigate(item.navigateTo)}
+ endIcon={}
+ sx={{
+ ...composeButtonSx("primary", "small"),
+ alignSelf: "flex-start",
+ }}
+ >
+ Ouvrir
+
+
+
+
+ ))}
diff --git a/frontend/src/components/Pages/GestionArticles.jsx b/frontend/src/components/Pages/GestionArticles.jsx
index b1dce05..29232e8 100644
--- a/frontend/src/components/Pages/GestionArticles.jsx
+++ b/frontend/src/components/Pages/GestionArticles.jsx
@@ -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 (
-
- {/* ✅ En-tête avec retour et création */}
-
- } variant="outlined" color="secondary" onClick={() => navigate("/admin/dashboard")}>
- Retour au Dashboard
-
-
- Gestion des Articles
-
- } variant="contained" color="primary" onClick={() => navigate("/admin/create-post")}>
- Créer un article
-
+
+
+
+
+
+
+
+ }
+ variant="contained"
+ onClick={handleBack}
+ sx={{ ...composeButtonSx("ghost") }}
+ >
+ Retour
+
+
+
+ Contenu éditorial
+
+
+ Gestion des articles
+
+
+ Consultez les publications WordPress, ajustez vos contenus et
+ maintenez un flux éditorial cohérent pour Octopus.
+
+
+ }
+ variant="contained"
+ onClick={handleCreate}
+ sx={{ ...composeButtonSx("primary") }}
+ >
+ Nouvel article
+
+
+
+
+
+
+
+ Total articles
+
+
+ {stats.total}
+
+
+ {filteredPosts.length} résultat(s) après filtre
+
+
+
+
+
+
+ Articles illustrés
+
+
+ {stats.withImage}
+
+
+ {stats.withoutImage} sans visuel associé
+
+
+
+
+
+
+ Dernière publication
+
+
+ {stats.latestLabel}
+
+
+ {stats.latestDate}
+
+
+
+
+
+
+
+
+
+ Rechercher un article
+
+
+ Filtrez par titre ou résumé pour retrouver un contenu en quelques
+ secondes.
+
+
+ setSearchTerm(event.target.value)}
+ sx={{
+ maxWidth: 380,
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 999,
+ backgroundColor: "rgba(255,255,255,0.92)",
+ },
+ }}
+ />
+
+
+
+ {loading ? (
+
+
+
+ ) : filteredPosts.length === 0 ? (
+
+
+ Aucun article trouvé
+
+
+ Ajustez votre recherche ou créez un nouvel article pour alimenter le blog.
+
+ }
+ variant="contained"
+ onClick={handleCreate}
+ sx={{ ...composeButtonSx("primary"), mt: 3 }}
+ >
+ Créer un article
+
+
+ ) : (
+
+ {filteredPosts.map((post) => (
+
+
+
+
+
+
+
+
+
+
+
+ {post.title.rendered}
+
+
+ {previewExcerpt(post.excerpt?.rendered)}
+
+
+ }
+ variant="contained"
+ onClick={() => handleEdit(post.id)}
+ sx={{ ...composeButtonSx("outline", "small") }}
+ >
+ Modifier
+
+ }
+ variant="contained"
+ onClick={() => handleDeletePost(post.id, post.imageId)}
+ sx={{ ...composeButtonSx("danger", "small") }}
+ >
+ Supprimer
+
+
+
+
+
+ ))}
+
+ )}
-
- {/* ✅ Recherche */}
- setSearchTerm(e.target.value)}
- sx={{ mb: 3 }}
- />
-
- {/* ✅ Tableau des articles */}
-
-
-
-
- Image
- Titre
- Résumé
- Actions
-
-
-
- {filteredPosts.length > 0 ? (
- filteredPosts.map((post) => (
-
- {/* ✅ Image en vedette */}
-
-
-
- {/* ✅ Titre de l'article */}
- {post.title.rendered}
- {/* ✅ Résumé avec 100 caractères max */}
-
- {post.excerpt.rendered.replace(/(<([^>]+)>)/gi, "").replace(/ /g, " ").substring(0, 100)}...
-
- {/* ✅ Boutons Modifier et Supprimer */}
-
- }
- variant="outlined"
- color="warning"
- sx={{ mr: 1 }}
- onClick={() => navigate(`/admin/edit-post/${post.id}`)}
- >
- Modifier
-
- }
- variant="outlined"
- color="error"
- onClick={() => handleDeletePost(post.id, post.imageId)}
- >
- Supprimer
-
-
-
- ))
- ) : (
-
-
- Aucun article trouvé.
-
-
- )}
-
-
-
);
};
-export default GestionArticles;
\ No newline at end of file
+export default GestionArticles;
diff --git a/frontend/src/components/Pages/Login.jsx b/frontend/src/components/Pages/Login.jsx
index 4ff4e00..aaf4952 100644
--- a/frontend/src/components/Pages/Login.jsx
+++ b/frontend/src/components/Pages/Login.jsx
@@ -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 (
-
-
-
-
-
-
-
- {token ? "Bienvenue !" : "Connexion"}
-
+
+
+
- {token ? (
- <>
- navigate("/admin/dashboard")}
- sx={{ mt: 3, width: "100%" }}
+
+
+
+
+ Octopus Admin
+
+
+ Accédez à votre espace sécurisé
+
+
+ Connectez-vous avec vos identifiants d’application pour gérer les
+ contenus Octopus : articles, pages services, formations et plus
+ encore.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {token ? "Bienvenue dans l’espace Octopus" : "Connexion"}
+
+
+ {token ? (
+
+ navigate("/admin/dashboard")}
+ sx={{ ...composeButtonSx("primary"), width: "100%" }}
+ >
+ Accéder au tableau de bord
+
+
+ Déconnexion
+
+
+ ) : (
+
- Accéder au tableau de bord
-
-
- Déconnexion
-
- >
- ) : (
-
- )}
-
-
-
+
+ setUsername(e.target.value)}
+ required
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 999,
+ backgroundColor: "rgba(255,255,255,0.95)",
+ },
+ }}
+ />
+ setAppPassword(e.target.value)}
+ required
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ borderRadius: 999,
+ backgroundColor: "rgba(255,255,255,0.95)",
+ },
+ }}
+ />
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+ {loading ? (
+
+ ) : (
+ "Se connecter"
+ )}
+
+
+
+ )}
+
+
+
+
);
}
-export default TestLogin;
\ No newline at end of file
+export default TestLogin;
diff --git a/frontend/src/components/Pages/Tools.jsx b/frontend/src/components/Pages/Tools.jsx
index cd42dcc..3b0b35c 100644
--- a/frontend/src/components/Pages/Tools.jsx
+++ b/frontend/src/components/Pages/Tools.jsx
@@ -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 (
-
-
- }
- onClick={() => navigate("/admin/dashboard")}
- sx={{ mr: 2 }}
- >
- Retour au dashboard
-
-
- Mes applications
-
-
+ 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)",
+ },
+ }),
+ });
-
- {toolsApps.map((app) => (
-
-
-
- {app.name}
-
- {app.description}
-
-
- handleOpenApp(app)}>
- Ouvrir
-
-
-
-
-
- ))}
-
+ return (
+
+
+
+
+
+
+
+ }
+ onClick={() => navigate("/admin/dashboard")}
+ sx={composeButtonSx("outline")}
+ >
+ Retour au dashboard
+
+
+
+ Hub applicatif Octopus
+
+
+ Mes outils métiers
+
+
+ Centralisez vos applications internes : réseau, bilans, générateur
+ de photos et futurs modules d’automatisation Octopus.
+
+
+
+
+
+
+
+
+
+
+ {toolsApps.map((app) => (
+
+
+
+
+ {app.external ? "Application externe" : "Module interne"}
+
+
+ {app.name}
+
+
+ {app.description}
+
+
+ handleOpenApp(app)}
+ >
+ Ouvrir
+
+
+
+ ))}
+
+
);
}
diff --git a/server/cloudinary-backend/.DS_Store b/server/cloudinary-backend/.DS_Store
new file mode 100644
index 0000000..f2ea631
Binary files /dev/null and b/server/cloudinary-backend/.DS_Store differ
diff --git a/server/cloudinary-backend/.env.example b/server/cloudinary-backend/.env.example
new file mode 100644
index 0000000..5679128
--- /dev/null
+++ b/server/cloudinary-backend/.env.example
@@ -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=
diff --git a/server/cloudinary-backend/contactRoute.js b/server/cloudinary-backend/contactRoute.js
new file mode 100644
index 0000000..df5515d
--- /dev/null
+++ b/server/cloudinary-backend/contactRoute.js
@@ -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 = `
+ Nom : ${name}
+ E-mail : ${email}
+ Consentement : ${consentLabel}
+ Message :
+ ${message.replace(/\n/g, "
")}
+ `;
+
+ // 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;
diff --git a/server/cloudinary-backend/main.js b/server/cloudinary-backend/main.js
index 4c5bf54..651ad36 100644
--- a/server/cloudinary-backend/main.js
+++ b/server/cloudinary-backend/main.js
@@ -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, () => {
diff --git a/server/cloudinary-backend/package.json b/server/cloudinary-backend/package.json
index c1ac5c1..be07cfb 100644
--- a/server/cloudinary-backend/package.json
+++ b/server/cloudinary-backend/package.json
@@ -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"
}
}