From 3fabebeb857cd6393f25b8e7150bba733fe1cab0 Mon Sep 17 00:00:00 2001 From: sebvtl728 Date: Thu, 9 Apr 2026 16:37:09 +0200 Subject: [PATCH] feat: integrate Moodle API services, add RichTextEditor component, and configure environment variables for course management. --- .DS_Store | Bin 10244 -> 10244 bytes frontend/.gitignore | 2 + frontend/README.md | 17 + frontend/package-lock.json | 1 + frontend/package.json | 1 + .../src/components/Admin/CoursLecture.jsx | 97 +- .../src/components/Admin/GestionCours.jsx | 851 +++++++++++++- frontend/src/components/Admin/ListeCours.jsx | 1002 ++++++++++++++--- frontend/src/components/EditPost.jsx | 26 +- .../src/components/GestionBureauEtudeACF.jsx | 7 +- frontend/src/components/Pages/CreatePost.jsx | 7 +- frontend/src/components/Pages/EditPageACF.jsx | 9 +- .../components/Pages/GestionPageAccueil.jsx | 21 +- .../src/components/common/RichTextEditor.jsx | 119 ++ frontend/src/config/moodle.js | 5 + frontend/src/services/moodleApi.js | 193 ++++ frontend/stats.html | 2 +- server/.DS_Store | Bin 8196 -> 10244 bytes 18 files changed, 2059 insertions(+), 301 deletions(-) create mode 100644 frontend/src/components/common/RichTextEditor.jsx create mode 100644 frontend/src/config/moodle.js create mode 100644 frontend/src/services/moodleApi.js diff --git a/.DS_Store b/.DS_Store index 4eeb8ae686f5aa9f8a22fd958c5502447514f1c3..714bdfdb6f96326629a0efec1536bb06f5b32e97 100644 GIT binary patch delta 503 zcmZn(XbG6$FDS*pz`)4BAi$85ZWx@LpIfl8a2or>2Eonj94s95AXyd$J%)6KOokGe z3ZNJR!{M%aWVzgY7nh`*{3M_lNB8cjji0t0cSMy>!7E>o0kIvZhk@bW0uag0pjXdO z#!$?V%#hEJ!jOt$Jp-dQL0t%nW zJObZYg#Ya~o2)M=Hd$7Xi_Q9%G3VzklXV4^V9ZcKC1wRlfyq^Z-b|JUC(jm?5kYoS zYU0blB|^s?p%MsZAWQJ01O=DDd#UREJ}Zw?eGW@CT9rzSUT>aoeErDWmk_fm393=Nz4rGKz& SEZ|_=%&zc@WizK3Gcy26Dno$) diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..9572296 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -11,6 +11,8 @@ node_modules dist dist-ssr *.local +.env +.env.* # Editor directories and files .vscode/* diff --git a/frontend/README.md b/frontend/README.md index f768e33..fedb6d9 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -6,3 +6,20 @@ Currently, two official plugins are available: - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Configuration Moodle + +Les écrans d'administration (liste de cours, gestion des chapitres et lecture sécurisée) consomment l'API REST de Moodle. +Avant de lancer le projet : + +1. Copiez `frontend/.env.example` vers `frontend/.env.local`. +2. Renseignez vos propres valeurs pour : + +``` +VITE_MOODLE_API_URL=https://votre-instance/webservice/rest/server.php +VITE_MOODLE_TOKEN=token_personnel +``` + +> Ne versionnez jamais votre token : `.env` est ignoré par Git. + +Ensuite vous pouvez lancer l'application avec `npm install && npm run dev` depuis le dossier `frontend`. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a82b949..5d9876f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.7.9", "cloudinary": "^2.7.0", "pdf-lib": "^1.17.1", + "quill": "^1.3.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", diff --git a/frontend/package.json b/frontend/package.json index 52f2d27..23d594a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "axios": "^1.7.9", "cloudinary": "^2.7.0", "pdf-lib": "^1.17.1", + "quill": "^1.3.7", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet": "^6.1.0", diff --git a/frontend/src/components/Admin/CoursLecture.jsx b/frontend/src/components/Admin/CoursLecture.jsx index 43ae109..ff92e59 100644 --- a/frontend/src/components/Admin/CoursLecture.jsx +++ b/frontend/src/components/Admin/CoursLecture.jsx @@ -1,27 +1,10 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import axios from "axios"; import { Box, Typography, CircularProgress } from "@mui/material"; - -const API_URL = - "https://www.formations.octopusdesign.fr/webservice/rest/server.php"; -const TOKEN = "685e1b5d794b558b60e971581154c3b2"; +import { MoodleApi } from "../../services/moodleApi"; const PUBLIC_LINKS_STORAGE_KEY = "listeCoursPublicLinks"; const CoursLecture = () => { - // Empêche le clic droit - const handleContextMenu = (e) => e.preventDefault(); - document.addEventListener("contextmenu", handleContextMenu); - - // Empêche le copier/coller - const handleCopyPaste = (e) => e.preventDefault(); - document.addEventListener("copy", handleCopyPaste); - document.addEventListener("cut", handleCopyPaste); - document.addEventListener("paste", handleCopyPaste); - - // Empêche la sélection de texte - document.body.style.userSelect = "none"; - const { token } = useParams(); const [content, setContent] = useState(null); const [title, setTitle] = useState(""); @@ -30,19 +13,34 @@ const CoursLecture = () => { // ✅ Masquer Header/Footer pendant l'affichage useEffect(() => { + if (typeof document === "undefined") { + return () => {}; + } const header = document.querySelector("header"); const footer = document.querySelector("footer"); + const previousHeaderDisplay = header?.style.display; + const previousFooterDisplay = footer?.style.display; if (header) header.style.display = "none"; if (footer) footer.style.display = "none"; + const handleContextMenu = (event) => event.preventDefault(); + const handleCopyPaste = (event) => event.preventDefault(); + document.addEventListener("contextmenu", handleContextMenu); + document.addEventListener("copy", handleCopyPaste); + document.addEventListener("cut", handleCopyPaste); + document.addEventListener("paste", handleCopyPaste); + + const previousUserSelect = document.body.style.userSelect; + document.body.style.userSelect = "none"; + return () => { - if (header) header.style.display = ""; - if (footer) footer.style.display = ""; + if (header) header.style.display = previousHeaderDisplay || ""; + if (footer) footer.style.display = previousFooterDisplay || ""; document.removeEventListener("contextmenu", handleContextMenu); document.removeEventListener("copy", handleCopyPaste); document.removeEventListener("cut", handleCopyPaste); document.removeEventListener("paste", handleCopyPaste); - document.body.style.userSelect = "auto"; + document.body.style.userSelect = previousUserSelect || "auto"; }; }, []); @@ -77,7 +75,8 @@ const CoursLecture = () => { linkId, format: "json", }; - } catch (jsonError) { + } catch (parseError) { + console.warn("Lien public JSON invalide :", parseError); const parts = raw.split("_"); if (parts.length === 3) { const [courseId, itemId, expiry] = parts; @@ -103,7 +102,8 @@ const CoursLecture = () => { } return null; } - } catch (error) { + } catch (decodeError) { + console.warn("Impossible de décoder le lien sécurisé :", decodeError); return null; } }; @@ -175,29 +175,9 @@ const CoursLecture = () => { const fetchPage = async () => { try { - const res = await axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "mod_page_get_pages_by_courses", - moodlewsrestformat: "json", - courseids: [parseInt(courseId, 10)], - }, - paramsSerializer: (params) => { - const sp = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((v, i) => sp.append(`${key}[${i}]`, v)); - } else { - sp.append(key, value); - } - }); - return sp.toString(); - }, - }); - - const found = (res.data.pages || []).find( - (p) => p.id === parseInt(itemId, 10) - ); + const pages = await MoodleApi.getPagesByCourse(Number(courseId)); + const parsedItemId = parseInt(itemId, 10); + const found = pages.find((p) => p.id === parsedItemId); if (found) { setTitle(found.name || ""); setContent(found.content || ""); @@ -205,7 +185,8 @@ const CoursLecture = () => { setTokenValid(false); setContent("⛔ Page non trouvée."); } - } catch (err) { + } catch (error) { + console.error("Erreur lors du chargement de la page Moodle :", error); setTokenValid(false); setContent("❌ Erreur lors du chargement."); } finally { @@ -215,19 +196,11 @@ const CoursLecture = () => { const fetchAssignment = async () => { try { - const res = await axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "mod_assign_get_assignments", - moodlewsrestformat: "json", - }, - }); - const parsedCourseId = parseInt(courseId, 10); - const parsedAssignmentId = parseInt(itemId, 10); - const course = (res.data.courses || []).find( - (c) => c.id === parsedCourseId + const assignments = await MoodleApi.getAssignmentsByCourse( + Number(courseId) ); - const assignment = (course?.assignments || []).find( + const parsedAssignmentId = parseInt(itemId, 10); + const assignment = assignments.find( (a) => a.id === parsedAssignmentId ); if (assignment) { @@ -238,6 +211,10 @@ const CoursLecture = () => { setContent("⛔ Devoir introuvable."); } } catch (error) { + console.error( + "Erreur lors du chargement du devoir Moodle :", + error + ); setTokenValid(false); setContent("❌ Erreur lors du chargement du devoir."); } finally { diff --git a/frontend/src/components/Admin/GestionCours.jsx b/frontend/src/components/Admin/GestionCours.jsx index 624ec7c..93190dd 100644 --- a/frontend/src/components/Admin/GestionCours.jsx +++ b/frontend/src/components/Admin/GestionCours.jsx @@ -1,80 +1,817 @@ -import React, { useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { - Box, - Typography, - TextField, - Button, - CircularProgress, Alert, + Box, + Button, + Card, + CardContent, + CircularProgress, + Divider, + Grid, + IconButton, + List, + ListItem, + ListItemText, + Stack, + TextField, + Typography, } from "@mui/material"; -import ReactQuill from "react-quill"; -import "react-quill/dist/quill.snow.css"; -import axios from "axios"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import AddIcon from "@mui/icons-material/Add"; +import { MoodleApi } from "../../services/moodleApi"; +import { isMoodleConfigured } from "../../config/moodle"; +import RichTextEditor from "../common/RichTextEditor"; -const API_URL = "https://www.formations.octopusdesign.fr/webservice/rest/server.php"; -const TOKEN = "685e1b5d794b558b60e971581154c3b2"; +const defaultCourseForm = { + fullname: "", + shortname: "", + categoryid: "1", + summary: "", +}; -const EditeurCours = () => { - const [titre, setTitre] = useState(""); - const [contenu, setContenu] = useState(""); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); - const [error, setError] = useState(""); +const defaultSectionForm = { + name: "", + summary: "", + section: "", +}; - const handleSubmit = async () => { - if (!titre || !contenu) return; - setLoading(true); - setSuccess(false); - setError(""); +const formatSectionName = (section) => { + if (!section) { + return "Section sans titre"; + } + const number = + section.section !== null && section.section !== undefined + ? Number(section.section) + : null; + if (section.name?.trim()) { + return section.name.trim(); + } + if (Number.isFinite(number)) { + return `Chapitre ${number}`; + } + return "Section sans titre"; +}; - const params = new URLSearchParams(); - params.append("wstoken", TOKEN); - params.append("wsfunction", "core_course_create_courses"); - params.append("moodlewsrestformat", "json"); +const mapCourseToForm = (course) => ({ + fullname: course.fullname || "", + shortname: course.shortname || "", + categoryid: String(course.categoryid || "1"), + summary: course.summary || "", +}); - params.append("courses[0][fullname]", titre); - params.append("courses[0][shortname]", titre.replace(/\s+/g, "_").toLowerCase()); - params.append("courses[0][categoryid]", 1); - params.append("courses[0][summary]", contenu); - params.append("courses[0][summaryformat]", 1); +const GestionCours = () => { + const [courses, setCourses] = useState([]); + const [courseForm, setCourseForm] = useState(defaultCourseForm); + const [selectedCourse, setSelectedCourse] = useState(null); + const [sections, setSections] = useState([]); + const [sectionForm, setSectionForm] = useState(defaultSectionForm); + const [selectedSection, setSelectedSection] = useState(null); + const [loadingCourses, setLoadingCourses] = useState(true); + const [loadingSections, setLoadingSections] = useState(false); + const [courseActionLoading, setCourseActionLoading] = useState(false); + const [sectionActionLoading, setSectionActionLoading] = useState(false); + const [courseFeedback, setCourseFeedback] = useState(null); + const [sectionFeedback, setSectionFeedback] = useState(null); + const [searchValue, setSearchValue] = useState(""); + const handleResetSectionForm = useCallback(() => { + setSelectedSection(null); + setSectionForm(defaultSectionForm); + }, []); + + const handleResetCourseSelection = useCallback(() => { + setSelectedCourse(null); + setCourseForm(defaultCourseForm); + setSections([]); + handleResetSectionForm(); + }, [handleResetSectionForm]); + + const refreshCourses = useCallback(async (preserveCourseId) => { try { - await axios.post(API_URL, params); - setSuccess(true); - setTitre(""); - setContenu(""); - } catch (err) { - setError("Erreur lors de la création du cours."); - console.error(err); + if (!isMoodleConfigured()) { + return; + } + setLoadingCourses(true); + const data = await MoodleApi.listCourses(); + const sanitized = (Array.isArray(data) ? data : []).filter( + (course) => course.id !== 1 + ); + setCourses(sanitized); + if (preserveCourseId) { + const stillExists = sanitized.find((c) => c.id === preserveCourseId); + if (stillExists) { + setSelectedCourse(stillExists); + setCourseForm(mapCourseToForm(stillExists)); + } else { + handleResetCourseSelection(); + } + } + } catch (error) { + console.error("Erreur lors du chargement des cours :", error); + setCourseFeedback({ + type: "error", + message: + error?.message || "Impossible de récupérer la liste des cours.", + }); } finally { - setLoading(false); + setLoadingCourses(false); + } + }, [handleResetCourseSelection]); + + useEffect(() => { + if (!isMoodleConfigured()) { + setCourseFeedback({ + type: "error", + message: + "Configurez VITE_MOODLE_API_URL et VITE_MOODLE_TOKEN pour utiliser cette interface.", + }); + setLoadingCourses(false); + return; + } + refreshCourses(); + }, [refreshCourses]); + + const refreshSections = async (courseId) => { + if (!courseId) { + setSections([]); + return; + } + setLoadingSections(true); + try { + const data = await MoodleApi.getCourseContents(courseId); + const normalized = (Array.isArray(data) ? data : []).map((section) => ({ + id: section.id, + section: section.section, + name: formatSectionName(section), + summary: section.summary || "", + })); + setSections(normalized); + } catch (error) { + console.error("Erreur lors du chargement des sections :", error); + setSectionFeedback({ + type: "error", + message: + error?.message || "Impossible de récupérer les sections du cours.", + }); + } finally { + setLoadingSections(false); } }; + const handleSelectCourse = (course) => { + setSelectedCourse(course); + setCourseForm(mapCourseToForm(course)); + handleResetSectionForm(); + refreshSections(course.id); + }; + + const handleCourseInputChange = (field) => (event) => { + const { value } = event.target; + setCourseForm((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleSummaryChange = (value) => { + setCourseForm((prev) => ({ + ...prev, + summary: value, + })); + }; + + const slugify = (value) => + value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .replace(/[^\w\s-]/g, "") + .trim() + .replace(/\s+/g, "-") + .toLowerCase(); + + const buildCoursePayload = () => { + const fullname = courseForm.fullname.trim(); + const shortname = + courseForm.shortname.trim() || slugify(courseForm.fullname); + if (!fullname || !shortname) { + throw new Error( + "Le titre et l'identifiant court du cours sont obligatoires." + ); + } + const payload = { + fullname, + shortname, + summary: courseForm.summary || "", + summaryformat: 1, + }; + const normalizedCategory = + courseForm.categoryid && !Number.isNaN(Number(courseForm.categoryid)) + ? Number(courseForm.categoryid) + : null; + if (normalizedCategory !== null) { + payload.categoryid = normalizedCategory; + } + return payload; + }; + + const handleCreateCourse = async () => { + setCourseFeedback(null); + try { + setCourseActionLoading(true); + const payload = buildCoursePayload(); + await MoodleApi.createCourse(payload); + setCourseFeedback({ + type: "success", + message: "Cours créé avec succès.", + }); + setCourseForm(defaultCourseForm); + await refreshCourses(); + } catch (error) { + setCourseFeedback({ + type: "error", + message: error?.message || "Impossible de créer le cours.", + }); + } finally { + setCourseActionLoading(false); + } + }; + + const handleUpdateCourse = async () => { + if (!selectedCourse?.id) { + return; + } + setCourseFeedback(null); + try { + setCourseActionLoading(true); + const payload = buildCoursePayload(); + await MoodleApi.updateCourse({ + id: Number(selectedCourse.id), + ...payload, + }); + setCourseFeedback({ + type: "success", + message: "Cours mis à jour.", + }); + await refreshCourses(selectedCourse.id); + } catch (error) { + setCourseFeedback({ + type: "error", + message: error?.message || "Impossible de mettre à jour le cours.", + }); + } finally { + setCourseActionLoading(false); + } + }; + + const handleDeleteCourse = async (courseArg) => { + const targetCourse = courseArg || selectedCourse; + if (!targetCourse?.id) { + return; + } + const confirmed = window.confirm( + "Supprimer définitivement ce cours et tous ses contenus ?" + ); + if (!confirmed) { + return; + } + setCourseFeedback(null); + try { + setCourseActionLoading(true); + await MoodleApi.deleteCourses([targetCourse.id]); + setCourseFeedback({ + type: "success", + message: "Cours supprimé.", + }); + if (selectedCourse?.id === targetCourse.id) { + handleResetCourseSelection(); + } + await refreshCourses(); + } catch (error) { + setCourseFeedback({ + type: "error", + message: error?.message || "Impossible de supprimer le cours.", + }); + } finally { + setCourseActionLoading(false); + } + }; + + const handleSectionInputChange = (field) => (event) => { + const { value } = event.target; + setSectionForm((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const handleEditSection = (section) => { + setSelectedSection(section); + setSectionForm({ + name: section.name || "", + summary: section.summary || "", + section: + section.section !== null && section.section !== undefined + ? String(section.section) + : "", + }); + }; + + const handleCreateSection = async () => { + if (!selectedCourse?.id) { + return; + } + setSectionFeedback(null); + try { + setSectionActionLoading(true); + const payload = { + courseid: selectedCourse.id, + name: sectionForm.name?.trim() || undefined, + summary: sectionForm.summary || "", + }; + if (sectionForm.section) { + payload.section = Number(sectionForm.section); + } + await MoodleApi.createSection(payload); + setSectionFeedback({ + type: "success", + message: "Chapitre ajouté.", + }); + handleResetSectionForm(); + await refreshSections(selectedCourse.id); + } catch (error) { + setSectionFeedback({ + type: "error", + message: error?.message || "Impossible d'ajouter le chapitre.", + }); + } finally { + setSectionActionLoading(false); + } + }; + + const handleUpdateSection = async () => { + if (!selectedSection?.id || !selectedCourse?.id) { + return; + } + setSectionFeedback(null); + try { + setSectionActionLoading(true); + await MoodleApi.updateSection({ + id: selectedSection.id, + courseid: selectedCourse.id, + name: sectionForm.name?.trim() || undefined, + summary: sectionForm.summary || "", + section: + sectionForm.section !== "" + ? Number(sectionForm.section) + : undefined, + }); + setSectionFeedback({ + type: "success", + message: "Chapitre mis à jour.", + }); + handleResetSectionForm(); + await refreshSections(selectedCourse.id); + } catch (error) { + setSectionFeedback({ + type: "error", + message: error?.message || "Impossible de mettre à jour le chapitre.", + }); + } finally { + setSectionActionLoading(false); + } + }; + + const handleDeleteSection = async (section) => { + if (!selectedCourse?.id || !section?.id) { + return; + } + const confirmed = window.confirm( + "Supprimer définitivement ce chapitre ?" + ); + if (!confirmed) { + return; + } + setSectionFeedback(null); + try { + setSectionActionLoading(true); + await MoodleApi.deleteSections(selectedCourse.id, [section.id]); + setSectionFeedback({ + type: "success", + message: "Chapitre supprimé.", + }); + if (selectedSection?.id === section.id) { + handleResetSectionForm(); + } + await refreshSections(selectedCourse.id); + } catch (error) { + setSectionFeedback({ + type: "error", + message: error?.message || "Impossible de supprimer le chapitre.", + }); + } finally { + setSectionActionLoading(false); + } + }; + + const filteredCourses = useMemo(() => { + if (!searchValue.trim()) { + return courses; + } + const query = searchValue.trim().toLowerCase(); + return courses.filter((course) => + course.fullname?.toLowerCase().includes(query) + ); + }, [courses, searchValue]); + + const sectionList = useMemo(() => { + const clone = [...sections]; + clone.sort((a, b) => { + const valueA = + a.section === null || a.section === undefined ? 9999 : a.section; + const valueB = + b.section === null || b.section === undefined ? 9999 : b.section; + return valueA - valueB; + }); + return clone; + }, [sections]); + return ( - - - Ajouter un cours dans Moodle - + + + + + + Gestion des cours et chapitres + + + + + Créez, modifiez et supprimez vos cours Moodle directement depuis + cette interface. Les chapitres correspondent aux sections du cours. + + - {success && Cours enregistré avec succès !} - {error && {error}} + {!isMoodleConfigured() && ( + + Ajoutez vos identifiants Moodle dans .env.local pour + activer cette page. + + )} - setTitre(e.target.value)} - sx={{ mb: 3 }} - /> + {courseFeedback && ( + {courseFeedback.message} + )} - + + + + + + + + Liste des cours + + setSearchValue(event.target.value)} + /> + + + {loadingCourses ? ( + + + + ) : ( + + {filteredCourses.length === 0 && ( + + + + )} + {filteredCourses.map((course) => ( + handleDeleteCourse(course)} + disabled={courseActionLoading} + > + + + } + > + handleSelectCourse(course)} + /> + + ))} + + )} + + + + - + + + + + + + {selectedCourse ? "Modifier le cours" : "Nouveau cours"} + + {selectedCourse && ( + + )} + + + + + + + Résumé du cours + + + + + + + + + + + + + + + {selectedCourse && ( + + + + + + + Chapitres de « {selectedCourse.fullname} » + + + Gérez les sections du cours Moodle sélectionné. + + + + + + {sectionFeedback && ( + + {sectionFeedback.message} + + )} + + + + {loadingSections ? ( + + + + ) : ( + + {sectionList.length === 0 && ( + + + + )} + {sectionList.map((section) => ( + + handleEditSection(section)} + > + + + handleDeleteSection(section)} + disabled={sectionActionLoading} + > + + + + } + > + + + ))} + + )} + + + + + {selectedSection + ? "Modifier le chapitre" + : "Nouveau chapitre"} + + + + + + + {selectedSection && ( + + )} + + + + + + + + )} + ); }; -export default EditeurCours; \ No newline at end of file +export default GestionCours; diff --git a/frontend/src/components/Admin/ListeCours.jsx b/frontend/src/components/Admin/ListeCours.jsx index f3e4bcf..9402a6a 100644 --- a/frontend/src/components/Admin/ListeCours.jsx +++ b/frontend/src/components/Admin/ListeCours.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Box, Typography, @@ -27,21 +27,29 @@ import { Select, MenuItem, IconButton, + List, + ListItemButton, + ListItemIcon, + ListItemText, + Tabs, + Tab, } 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 ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import { Link, useNavigate } from "react-router-dom"; import { getToken } from "../../auth"; +import { MoodleApi } from "../../services/moodleApi"; +import { + isMoodleConfigured, + MOODLE_API_URL, + MOODLE_TOKEN, +} from "../../config/moodle"; import "../../assets/styleCours.css"; -const API_URL = - "https://www.formations.octopusdesign.fr/webservice/rest/server.php"; -const TOKEN = "685e1b5d794b558b60e971581154c3b2"; - const ITEM_TYPE_META = { page: { label: "Cours", chipColor: "primary" }, assignment: { label: "Devoir", chipColor: "secondary" }, @@ -223,6 +231,7 @@ const ListeCours = () => { const [h5pActivities, setH5pActivities] = useState([]); const [quizzes, setQuizzes] = useState([]); const [moduleSections, setModuleSections] = useState({}); + const [courseSections, setCourseSections] = useState([]); const [viewedPages, setViewedPages] = useState({}); const [viewedAssignments, setViewedAssignments] = useState({}); const [viewedH5P, setViewedH5P] = useState({}); @@ -231,6 +240,14 @@ const ListeCours = () => { const [linkDurationSelections, setLinkDurationSelections] = useState({}); const [progressByCourse, setProgressByCourse] = useState({}); const [courseSearch, setCourseSearch] = useState(""); + const [draggingItem, setDraggingItem] = useState(null); + const [hoveredSection, setHoveredSection] = useState(null); + const [unitActionLoading, setUnitActionLoading] = useState(false); + const [unitActionError, setUnitActionError] = useState(""); + const [unitActionSuccess, setUnitActionSuccess] = useState(""); + const [modalActiveTab, setModalActiveTab] = useState("programmes"); + const modalTabBeforeDrag = useRef(null); + const [configMissing, setConfigMissing] = useState(!isMoodleConfigured()); useEffect(() => { if (typeof window === "undefined") { return; @@ -243,7 +260,7 @@ const ListeCours = () => { } catch (error) { console.error("Impossible de charger les liens publics :", error); } - }, []); + }, [MOODLE_API_URL]); const normalizeSectionValue = React.useCallback((value) => { if (value === null || value === undefined) { return null; @@ -467,6 +484,9 @@ const ListeCours = () => { registerSection(activity.section, activity.sectionname) ); quizzes.forEach((quiz) => registerSection(quiz.section, quiz.sectionname)); + courseSections.forEach((section) => + registerSection(section.section, section.name) + ); return Array.from(optionsMap.values()); }, [ @@ -474,6 +494,7 @@ const ListeCours = () => { assignments, h5pActivities, quizzes, + courseSections, formatSectionLabel, normalizeSectionValue, ]); @@ -575,102 +596,39 @@ const ListeCours = () => { ); const fetchCours = async () => { + if (!isMoodleConfigured()) { + setConfigMissing(true); + setLoading(false); + return; + } try { - const res = await axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "core_course_get_courses", - moodlewsrestformat: "json", - }, - }); - setCours(res.data); + setConfigMissing(false); + const data = await MoodleApi.listCourses(); + setCours(Array.isArray(data) ? data : []); } catch (err) { console.error("Erreur chargement cours :", err); + setCours([]); } finally { setLoading(false); } }; const fetchPages = async (courseId) => { + if (!isMoodleConfigured()) { + setConfigMissing(true); + setLoadingPages(false); + return; + } setLoadingPages(true); try { const [pagesRes, sectionsRes, h5pRes, quizzesRes] = await Promise.all([ - axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "mod_page_get_pages_by_courses", - moodlewsrestformat: "json", - courseids: [courseId], - }, - paramsSerializer: (params) => { - const searchParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((v, i) => - searchParams.append(`${key}[${i}]`, v) - ); - } else { - searchParams.append(key, value); - } - }); - return searchParams.toString(); - }, - }), - axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "core_course_get_contents", - moodlewsrestformat: "json", - courseid: courseId, - }, - }), - axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "mod_h5pactivity_get_h5pactivities_by_courses", - moodlewsrestformat: "json", - courseids: [courseId], - }, - paramsSerializer: (params) => { - const searchParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((v, i) => - searchParams.append(`${key}[${i}]`, v) - ); - } else { - searchParams.append(key, value); - } - }); - return searchParams.toString(); - }, - }), - axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "mod_quiz_get_quizzes_by_courses", - moodlewsrestformat: "json", - courseids: [courseId], - }, - paramsSerializer: (params) => { - const searchParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((v, i) => - searchParams.append(`${key}[${i}]`, v) - ); - } else { - searchParams.append(key, value); - } - }); - return searchParams.toString(); - }, - }), + MoodleApi.getPagesByCourse(courseId), + MoodleApi.getCourseContents(courseId), + MoodleApi.getH5PActivitiesByCourse(courseId), + MoodleApi.getQuizzesByCourse(courseId), ]); - const sectionsData = Array.isArray(sectionsRes.data) - ? sectionsRes.data - : []; + const sectionsData = Array.isArray(sectionsRes) ? sectionsRes : []; const nameMap = sectionsData.reduce((acc, section) => { const sectionIndex = Number(section.section); if (Number.isNaN(sectionIndex)) { @@ -687,6 +645,33 @@ const ListeCours = () => { return acc; }, {}); setSectionNames(nameMap); + const formattedSections = sectionsData.map((section) => { + const sectionIndex = Number(section.section); + const normalizedSection = Number.isNaN(sectionIndex) + ? section.section ?? null + : sectionIndex; + const plainSummary = section.summary + ? section.summary.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim() + : ""; + const label = + (normalizedSection !== null && + normalizedSection !== undefined && + nameMap[normalizedSection]) || + (section.name && section.name.trim()) || + (plainSummary || null) || + (Number.isFinite(Number(section.section)) + ? `Chapitre ${section.section}` + : "Unité sans titre"); + return { + id: section.id, + section: normalizedSection, + name: label, + summary: section.summary || "", + summaryformat: section.summaryformat, + visible: section.visible, + }; + }); + setCourseSections(formattedSections); let globalModuleOrder = 0; const moduleMap = {}; @@ -707,7 +692,7 @@ const ListeCours = () => { }); }); - const pagesData = (pagesRes.data.pages || []).map((page) => ({ + const pagesData = (Array.isArray(pagesRes) ? pagesRes : []).map((page) => ({ ...page, sectionname: nameMap[page.section] || @@ -715,9 +700,7 @@ const ListeCours = () => { `Chapitre ${page.section}`, })); - const h5pActivitiesData = ( - (h5pRes.data && h5pRes.data.h5pactivities) || [] - ).map((activity) => { + const h5pActivitiesData = (Array.isArray(h5pRes) ? h5pRes : []).map((activity) => { const moduleInfo = moduleMap[activity.coursemodule] || {}; const section = moduleInfo.section ?? null; const sectionLabel = @@ -734,17 +717,24 @@ const ListeCours = () => { const normalizedPackageUrl = packageFileUrl ? packageFileUrl.replace("/webservice", "") : ""; - const embedUrl = normalizedPackageUrl - ? `${new URL(normalizedPackageUrl).origin}/h5p/embed.php?url=${encodeURIComponent( + let embedUrl = ""; + if (normalizedPackageUrl) { + try { + const parsedUrl = new URL(normalizedPackageUrl); + embedUrl = `${parsedUrl.origin}/h5p/embed.php?url=${encodeURIComponent( normalizedPackageUrl - )}&component=mod_h5pactivity` - : ""; + )}&component=mod_h5pactivity`; + } catch (error) { + console.error("URL H5P invalide :", error); + } + } - const downloadUrl = packageFileUrl - ? `${packageFileUrl}${ - packageFileUrl.includes("?") ? "&" : "?" - }token=${TOKEN}` - : null; + const downloadUrl = + packageFileUrl && MOODLE_TOKEN + ? `${packageFileUrl}${ + packageFileUrl.includes("?") ? "&" : "?" + }token=${MOODLE_TOKEN}` + : packageFileUrl || null; return { id: activity.id, @@ -762,7 +752,7 @@ const ListeCours = () => { setPages(pagesData); setModuleSections(moduleMap); setH5pActivities(h5pActivitiesData); - const quizzesData = (quizzesRes.data.quizzes || []).map((quiz) => { + const quizzesData = (Array.isArray(quizzesRes) ? quizzesRes : []).map((quiz) => { const moduleInfo = moduleMap[quiz.coursemodule] || {}; const section = moduleInfo.section ?? null; const sectionLabel = formatSectionLabel( @@ -781,27 +771,26 @@ const ListeCours = () => { }); setQuizzes(quizzesData); setCurrentPage(1); - } catch (err) { - console.error("Erreur chargement pages :", err); - setH5pActivities([]); - setModuleSections({}); - setQuizzes([]); - } finally { - setLoadingPages(false); - } - }; + } catch (err) { + console.error("Erreur chargement pages :", err); + setH5pActivities([]); + setModuleSections({}); + setCourseSections([]); + setQuizzes([]); + setPages([]); + } finally { + setLoadingPages(false); + } +}; const fetchAssignments = async (courseId) => { + if (!isMoodleConfigured()) { + setConfigMissing(true); + return; + } try { - const res = await axios.get(API_URL, { - params: { - wstoken: TOKEN, - wsfunction: "mod_assign_get_assignments", - moodlewsrestformat: "json", - }, - }); - const found = res.data.courses.find((c) => c.id === courseId); - setAssignmentsRaw(found?.assignments || []); + const assignments = await MoodleApi.getAssignmentsByCourse(courseId); + setAssignmentsRaw(assignments); } catch (err) { console.error("Erreur chargement devoirs :", err); setAssignmentsRaw([]); @@ -877,11 +866,19 @@ const ListeCours = () => { setSelectedH5P(null); setSelectedQuiz(null); setModuleSections({}); + setCourseSections([]); setQuizzes([]); setSearchQuery(""); setShowOnlyMedia(false); setAssignments([]); setAssignmentsRaw([]); + setUnitActionError(""); + setUnitActionSuccess(""); + setUnitActionLoading(false); + setDraggingItem(null); + setHoveredSection(null); + setModalActiveTab("programmes"); + modalTabBeforeDrag.current = null; loadProgress(cours.id); fetchPages(cours.id); fetchAssignments(cours.id); @@ -895,11 +892,19 @@ const ListeCours = () => { setH5pActivities([]); setSelectedH5P(null); setModuleSections({}); + setCourseSections([]); setAssignments([]); setAssignmentsRaw([]); setQuizzes([]); setSearchQuery(""); setShowOnlyMedia(false); + setUnitActionError(""); + setUnitActionSuccess(""); + setUnitActionLoading(false); + setDraggingItem(null); + setHoveredSection(null); + setModalActiveTab("programmes"); + modalTabBeforeDrag.current = null; }, []); const buildPdfContainer = (titleText, htmlContent) => { @@ -1075,17 +1080,18 @@ const ListeCours = () => { if (typeof window === "undefined") { return; } - if (!document.getElementById(scriptId)) { - try { - const h5pOrigin = new URL(API_URL).origin; - const script = document.createElement("script"); - script.id = scriptId; - script.src = `${h5pOrigin}/h5p/h5plib/v127/joubel/core/js/h5p-resizer.js`; - script.async = true; - document.body.appendChild(script); - } catch (error) { - console.error("Impossible de charger le script H5P resizer :", error); - } + if (!MOODLE_API_URL || document.getElementById(scriptId)) { + return; + } + try { + const h5pOrigin = new URL(MOODLE_API_URL).origin; + const script = document.createElement("script"); + script.id = scriptId; + script.src = `${h5pOrigin}/h5p/h5plib/v127/joubel/core/js/h5p-resizer.js`; + script.async = true; + document.body.appendChild(script); + } catch (error) { + console.error("Impossible de charger le script H5P resizer :", error); } }, []); @@ -1181,6 +1187,12 @@ const ListeCours = () => { key: `page-${page.id}`, id: page.id, name: page.name, + moduleId: + moduleInfo?.id ?? + moduleInfo?.cmid ?? + page.coursemodule ?? + page.cmid ?? + null, section: sectionValue, sectionLabel: formatSectionLabel(sectionValue, page.sectionname), data: page, @@ -1202,6 +1214,12 @@ const ListeCours = () => { key: `assignment-${assignment.id}`, id: assignment.id, name: assignment.name, + moduleId: + moduleInfo?.id ?? + moduleInfo?.cmid ?? + assignment.cmid ?? + assignment.coursemodule ?? + null, section: sectionValue, sectionLabel: formatSectionLabel(sectionValue, assignment.sectionname), data: assignment, @@ -1223,6 +1241,12 @@ const ListeCours = () => { key: `quiz-${quiz.id}`, id: quiz.id, name: quiz.name, + moduleId: + moduleInfo?.id ?? + moduleInfo?.cmid ?? + quiz.coursemodule ?? + quiz.cmid ?? + null, section: sectionValue, sectionLabel: formatSectionLabel(sectionValue, quiz.sectionname), data: quiz, @@ -1244,6 +1268,12 @@ const ListeCours = () => { key: `h5p-${activity.id}`, id: activity.id, name: activity.name, + moduleId: + moduleInfo?.id ?? + moduleInfo?.cmid ?? + activity.coursemodule ?? + activity.cmid ?? + null, section: sectionValue, sectionLabel: formatSectionLabel(sectionValue, activity.sectionname), data: activity, @@ -1351,6 +1381,84 @@ const ListeCours = () => { viewedQuizzes, ]); + const unitPanelData = React.useMemo(() => { + if (!Array.isArray(courseSections) || courseSections.length === 0) { + return []; + } + return courseSections + .map((section) => { + const normalizedSection = normalizeSectionValue(section.section); + const label = formatSectionLabel( + normalizedSection, + section.name || undefined + ); + const itemsForSection = programItems.filter( + (item) => + normalizeSectionValue(item.section) === normalizedSection + ); + const pagesForSection = itemsForSection.filter( + (item) => item.type === "page" + ); + const mappedItems = itemsForSection.map((item) => { + const meta = ITEM_TYPE_META[item.type] || { + label: "Contenu", + }; + return { + id: item.id, + moduleId: item.moduleId, + name: item.name, + type: item.type, + typeLabel: meta.label, + typeChipColor: meta.chipColor || "default", + hasMedia: + item.type === "page" && + pageHasMedia(item.data?.content || ""), + }; + }); + const stats = + normalizedSection !== null && normalizedSection !== undefined + ? chapterStats[normalizedSection] || { total: 0, viewed: 0 } + : { total: 0, viewed: 0 }; + return { + id: + section.id ?? + (normalizedSection !== null && normalizedSection !== undefined + ? `section-${normalizedSection}` + : `section-${Math.random().toString(36).slice(2)}`), + sectionNumber: normalizedSection, + label, + summary: section.summary || "", + totalItems: itemsForSection.length, + totalChapters: pagesForSection.length, + items: mappedItems, + progress: { + total: stats.total, + viewed: stats.viewed, + }, + }; + }) + .sort((a, b) => { + const sectionA = a.sectionNumber; + const sectionB = b.sectionNumber; + if (sectionA === sectionB) { + return 0; + } + if (sectionA === null || sectionA === undefined) { + return 1; + } + if (sectionB === null || sectionB === undefined) { + return -1; + } + return sectionA < sectionB ? -1 : 1; + }); + }, [ + courseSections, + chapterStats, + programItems, + formatSectionLabel, + normalizeSectionValue, + ]); + const isChapterComplete = React.useCallback( (sectionValue) => { const key = normalizeSectionValue(sectionValue); @@ -1490,6 +1598,188 @@ const ListeCours = () => { currentPage * itemsPerPage ); + const handleProgramItemDragStart = (item) => (event) => { + if (!item?.moduleId) { + return; + } + try { + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData( + "application/json", + JSON.stringify({ + moduleId: item.moduleId, + section: normalizeSectionValue(item.section), + label: item.name, + }) + ); + } catch (error) { + console.error("Impossible de préparer le glisser-déposer :", error); + } + setUnitActionError(""); + setUnitActionSuccess(""); + setDraggingItem({ + moduleId: item.moduleId, + section: normalizeSectionValue(item.section), + label: item.name, + }); + if (modalActiveTab !== "unites") { + modalTabBeforeDrag.current = modalActiveTab; + setModalActiveTab("unites"); + refreshSelectedCourse(); + } else { + modalTabBeforeDrag.current = "unites"; + } + }; + + const handleProgramItemDragEnd = () => { + setDraggingItem(null); + setHoveredSection(null); + if ( + modalTabBeforeDrag.current && + modalTabBeforeDrag.current !== "unites" + ) { + setModalActiveTab(modalTabBeforeDrag.current); + } + modalTabBeforeDrag.current = null; + }; + + const handleUnitDragOver = (sectionNumber) => (event) => { + if (unitActionLoading) { + return; + } + const hasData = + draggingItem || + (event.dataTransfer && + Array.from(event.dataTransfer.types || []).includes( + "application/json" + )); + if (!hasData) { + return; + } + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + setHoveredSection(normalizeSectionValue(sectionNumber)); + }; + + const handleUnitDragLeave = (sectionNumber) => () => { + setHoveredSection((current) => { + const normalizedSection = normalizeSectionValue(sectionNumber); + if (current === normalizedSection) { + return null; + } + return current; + }); + }; + + const handleUnitDrop = (sectionNumber) => async (event) => { + event.preventDefault(); + const normalizedTarget = normalizeSectionValue(sectionNumber); + setHoveredSection(null); + let payload = draggingItem; + if (!payload && event.dataTransfer) { + const raw = event.dataTransfer.getData("application/json"); + if (raw) { + try { + const parsed = JSON.parse(raw); + if (parsed && parsed.moduleId) { + payload = { + moduleId: parsed.moduleId, + section: normalizeSectionValue(parsed.section), + label: parsed.label, + }; + } + } catch (error) { + console.error("Impossible de lire la donnée déposée :", error); + } + } + } + if (!payload?.moduleId) { + return; + } + const currentSection = normalizeSectionValue(payload.section); + if ( + currentSection !== null && + currentSection !== undefined && + normalizedTarget !== null && + normalizedTarget !== undefined && + currentSection === normalizedTarget + ) { + setUnitActionError("Le chapitre se trouve déjà dans cette unité."); + setDraggingItem(null); + return; + } + await moveModuleToSection(payload.moduleId, normalizedTarget); + }; + + const moveModuleToSection = async (moduleId, targetSection) => { + if (!selected?.id) { + return; + } + if (!isMoodleConfigured()) { + setConfigMissing(true); + setUnitActionError( + "Configuration Moodle absente. Ajoutez VITE_MOODLE_API_URL et VITE_MOODLE_TOKEN." + ); + return; + } + if (!moduleId && moduleId !== 0) { + return; + } + const normalizedTarget = normalizeSectionValue(targetSection); + const numericSection = + normalizedTarget === null || normalizedTarget === undefined + ? null + : Number(normalizedTarget); + if (numericSection === null || Number.isNaN(numericSection)) { + setUnitActionError("Section cible invalide pour ce déplacement."); + return; + } + if (unitActionLoading) { + return; + } + setUnitActionLoading(true); + setUnitActionError(""); + setUnitActionSuccess(""); + + try { + await MoodleApi.moveModuleToSection(moduleId, numericSection); + setUnitActionSuccess("Chapitre déplacé avec succès."); + await Promise.all([ + fetchPages(selected.id), + fetchAssignments(selected.id), + ]); + } catch (error) { + console.error("Erreur lors du déplacement du chapitre :", error); + const serverMessage = + error?.response?.data?.message || + error?.response?.data?.error || + error?.message || + ""; + setUnitActionError( + `Impossible de déplacer le chapitre.${ + serverMessage ? ` ${serverMessage}` : "" + }` + ); + } finally { + setUnitActionLoading(false); + setDraggingItem(null); + setHoveredSection(null); + } + }; + + const refreshSelectedCourse = React.useCallback(() => { + if (!selected?.id) { + return; + } + setUnitActionLoading(true); + Promise.all([ + fetchPages(selected.id), + fetchAssignments(selected.id), + ]).finally(() => { + setUnitActionLoading(false); + }); + }, [selected?.id, fetchPages, fetchAssignments]); + const programmeInfoMessage = "Visualisez les cours, devoirs, tests et activités interactives depuis un seul espace."; @@ -1566,6 +1856,32 @@ const ListeCours = () => { ] ); + const openProgramItem = React.useCallback( + (itemId, itemType = "page") => { + if (!itemId) { + return; + } + const target = programItems.find( + (candidate) => + candidate && + candidate.id === itemId && + candidate.type === itemType + ); + if (target) { + handleItemTitleClick(target); + } + }, + [programItems, handleItemTitleClick] + ); + + const handleModalTabChange = (event, value) => { + modalTabBeforeDrag.current = null; + setModalActiveTab(value); + if (value === "unites") { + refreshSelectedCourse(); + } + }; + return ( { /> + {configMissing && ( + + Ajoutez vos identifiants Moodle dans .env.local, puis + relancez le serveur pour activer cette interface. + + )} + { - + + + + + + + + + + Programmes du cours @@ -2416,6 +2773,19 @@ const ListeCours = () => { { + if (!item.moduleId) { + event.preventDefault(); + return; + } + event.stopPropagation(); + handleProgramItemDragStart(item)(event); + }} + onDragEnd={(event) => { + event.stopPropagation(); + handleProgramItemDragEnd(); + }} sx={{ mb: 4, p: { xs: 2.5, md: 3 }, @@ -2424,6 +2794,7 @@ const ListeCours = () => { border: "1px solid rgba(255,255,255,0.6)", backdropFilter: "blur(14px)", boxShadow: "0 26px 34px -28px rgba(12, 29, 74, 0.55)", + cursor: item.moduleId ? "grab" : "default", }} > { + + + + + Unités de formation + + + Glissez un chapitre dans une unité pour réorganiser le cours. + + + + {unitActionSuccess ? ( + + {unitActionSuccess} + + ) : null} + {unitActionError ? ( + + {unitActionError} + + ) : null} + {unitActionLoading ? ( + + ) : null} + + + {unitPanelData.length > 0 ? ( + unitPanelData.map((unit) => { + const normalizedSection = + normalizeSectionValue(unit.sectionNumber); + const isHovered = + hoveredSection === normalizedSection; + const canDrop = Boolean(draggingItem?.moduleId); + const progressRatio = + unit.progress.total > 0 + ? Math.min( + 100, + Math.max( + 0, + (unit.progress.viewed / + unit.progress.total) * + 100 + ) + ) + : 0; + return ( + + + + {unit.label} + + + + {unit.progress.total > 0 ? ( + + + + {unit.progress.viewed}/{unit.progress.total}{" "} + éléments suivis + + + ) : null} + {unit.items.length > 0 ? ( + 6 ? 228 : "auto", + overflowY: + unit.items.length > 6 + ? "auto" + : "visible", + pr: 0.5, + }} + > + {unit.items.map((item) => { + let isViewed = false; + if (item.type === "page") { + isViewed = Boolean(viewedPages[item.id]); + } else if (item.type === "assignment") { + isViewed = Boolean( + viewedAssignments[item.id] + ); + } else if (item.type === "quiz") { + isViewed = Boolean(viewedQuizzes[item.id]); + } else if (item.type === "h5p") { + isViewed = Boolean(viewedH5P[item.id]); + } + const isActive = + (item.type === "page" && + selectedPage?.id === item.id) || + (item.type === "assignment" && + selectedAssignment?.id === item.id) || + (item.type === "quiz" && + selectedQuiz?.id === item.id) || + (item.type === "h5p" && + selectedH5P?.id === item.id); + return ( + + openProgramItem(item.id, item.type) + } + disabled={unitActionLoading} + sx={{ + borderRadius: 3, + mb: 0.75, + alignItems: "center", + bgcolor: isActive + ? "rgba(49,83,151,0.14)" + : "transparent", + color: "rgba(11, 26, 61, 0.82)", + boxShadow: isActive + ? "0 12px 20px -18px rgba(11,26,61,0.45)" + : "none", + border: isActive + ? "1px solid rgba(49,83,151,0.3)" + : "1px solid transparent", + transition: "all 0.2s ease", + px: 1.75, + "&:hover": { + bgcolor: "rgba(49,83,151,0.12)", + borderColor: "rgba(49,83,151,0.18)", + }, + }} + > + + {isViewed ? ( + + ) : ( + + )} + + + + {item.hasMedia ? ( + + ) : null} + + + + + ); + })} + + ) : ( + + Aucun contenu dans cette unité pour le moment. + + )} + + ); + }) + ) : ( + + Aucune unité détectée pour ce cours pour l’instant. + + )} + + + Astuce : créez de nouvelles unités depuis la page{" "} + + Gestion des cours + + , puis actualisez cette page pour les retrouver ici. + + + + + diff --git a/frontend/src/components/EditPost.jsx b/frontend/src/components/EditPost.jsx index f746f0b..589046e 100644 --- a/frontend/src/components/EditPost.jsx +++ b/frontend/src/components/EditPost.jsx @@ -17,8 +17,7 @@ import { 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 RichTextEditor from "./common/RichTextEditor"; import "../assets/styleCours.css"; const BUTTON_BASE_SX = Object.freeze({ @@ -157,15 +156,13 @@ function EditPost() { return new File([blob], "image-cloudinary.jpg", { type: blob.type }); }; - const modules = { - toolbar: [ - [{ header: [1, 2, 3, false] }], - ["bold", "italic", "underline", "strike"], - [{ list: "ordered" }, { list: "bullet" }], - ["link", "image"], // ✅ le bouton image est ici - ["clean"], - ], - }; + const toolbarOptions = [ + [{ header: [1, 2, 3, false] }], + ["bold", "italic", "underline", "strike"], + [{ list: "ordered" }, { list: "bullet" }], + ["link", "image"], + ["clean"], + ]; const canSubmit = title.trim().length > 0 && content.trim().length > 0 && Boolean(imageUrl); @@ -344,12 +341,11 @@ function EditPost() { backgroundColor: "rgba(255,255,255,0.92)", }} > - diff --git a/frontend/src/components/GestionBureauEtudeACF.jsx b/frontend/src/components/GestionBureauEtudeACF.jsx index 83e20ec..daf4daa 100644 --- a/frontend/src/components/GestionBureauEtudeACF.jsx +++ b/frontend/src/components/GestionBureauEtudeACF.jsx @@ -15,8 +15,7 @@ import { 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 RichTextEditor from "./common/RichTextEditor"; import "../assets/styleCours.css"; const ETUDE_PAGE_ID = 272; @@ -408,10 +407,10 @@ function EditPageEtude() { backgroundColor: "rgba(255,255,255,0.92)", }} > - handleFieldChange("introduction", value)} - style={{ height: 220 }} + minHeight={220} /> diff --git a/frontend/src/components/Pages/CreatePost.jsx b/frontend/src/components/Pages/CreatePost.jsx index ede5a34..7aa9dac 100644 --- a/frontend/src/components/Pages/CreatePost.jsx +++ b/frontend/src/components/Pages/CreatePost.jsx @@ -17,8 +17,7 @@ import { 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 RichTextEditor from "../common/RichTextEditor"; import "../../assets/styleCours.css"; const BUTTON_BASE_SX = Object.freeze({ @@ -272,10 +271,10 @@ function CreatePost() { backgroundColor: "rgba(255,255,255,0.92)", }} > - diff --git a/frontend/src/components/Pages/EditPageACF.jsx b/frontend/src/components/Pages/EditPageACF.jsx index 2fe1cfb..50f350d 100644 --- a/frontend/src/components/Pages/EditPageACF.jsx +++ b/frontend/src/components/Pages/EditPageACF.jsx @@ -21,8 +21,7 @@ import { } from "@mui/material"; import { Add, Delete, ArrowBack, Save } from "@mui/icons-material"; import CloseIcon from "@mui/icons-material/Close"; -import ReactQuill from "react-quill"; -import "react-quill/dist/quill.snow.css"; +import RichTextEditor from "../common/RichTextEditor"; import ImageUploaderCloudinary from "../ImageUploaderCloudinary"; import CloudinaryGallerySelector from "../CloudinaryGallerySelector"; import { @@ -718,11 +717,11 @@ function EditPageACF() { )} {richText ? ( <> - handleFieldChange(field, content)} - style={quillStyle} + minHeight={220} + sx={quillStyle} />