From 5294e93989988f3f91811b753a2a84d210ab4e8a Mon Sep 17 00:00:00 2001 From: Esenjin Date: Sat, 15 Feb 2025 13:03:10 +0100 Subject: [PATCH] =?UTF-8?q?am=C3=A9lioration=20de=20l'=C3=A9diteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/api/get-chapter.php | 37 ++++- admin/api/upload-image.php | 207 ++++++++++++++++++++++++ assets/css/editor.css | 194 +++++++++++++++++++---- assets/js/story-edit.js | 311 ++++++++++++++++++++++--------------- 4 files changed, 594 insertions(+), 155 deletions(-) create mode 100644 admin/api/upload-image.php diff --git a/admin/api/get-chapter.php b/admin/api/get-chapter.php index 57edcb3..011455f 100644 --- a/admin/api/get-chapter.php +++ b/admin/api/get-chapter.php @@ -3,7 +3,8 @@ require_once '../../includes/config.php'; require_once '../../includes/auth.php'; require_once '../../includes/stories.php'; -// Vérification de l'authentification +header('Content-Type: application/json'); + if (!Auth::check()) { http_response_code(401); exit(json_encode(['error' => 'Non autorisé'])); @@ -35,9 +36,39 @@ try { throw new Exception('Chapitre non trouvé'); } - header('Content-Type: application/json'); + // Vérification et correction de la structure du chapitre + if (!isset($chapter['title'])) { + $chapter['title'] = ''; + } + + // Conversion du contenu HTML en format Delta de Quill si nécessaire + if (!isset($chapter['content'])) { + $chapter['content'] = '{"ops":[{"insert":"\n"}]}'; + } else { + // Si le contenu est une chaîne HTML + if (is_string($chapter['content']) && !isJson($chapter['content'])) { + // On envoie le HTML brut, Quill le convertira côté client + $chapter['html'] = $chapter['content']; + $chapter['content'] = null; + } + } + echo json_encode($chapter); } catch (Exception $e) { + error_log('Erreur dans get-chapter.php: ' . $e->getMessage()); http_response_code(500); - echo json_encode(['error' => $e->getMessage()]); + echo json_encode([ + 'error' => $e->getMessage(), + 'details' => [ + 'storyId' => $storyId, + 'chapterId' => $chapterId, + 'file' => __FILE__, + 'line' => __LINE__ + ] + ]); +} + +function isJson($string) { + json_decode($string); + return json_last_error() === JSON_ERROR_NONE; } \ No newline at end of file diff --git a/admin/api/upload-image.php b/admin/api/upload-image.php new file mode 100644 index 0000000..9517532 --- /dev/null +++ b/admin/api/upload-image.php @@ -0,0 +1,207 @@ +uploadDir = __DIR__ . '/../../assets/images/chapters/' . $storyId . '/'; + $this->ensureUploadDirectory(); + } + + public function handleUpload($file) { + try { + // Vérifications de base + if ($file['error'] !== UPLOAD_ERR_OK) { + throw new Exception($this->getUploadErrorMessage($file['error'])); + } + + // Vérification du type MIME + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->file($file['tmp_name']); + if (!in_array($mimeType, $this->allowedTypes)) { + throw new Exception('Type de fichier non autorisé. Types acceptés : JPG, PNG, GIF, WEBP'); + } + + // Vérification de la taille + if ($file['size'] > $this->maxFileSize) { + throw new Exception('Fichier trop volumineux. Taille maximum : 5MB'); + } + + // Vérification et redimensionnement de l'image + [$width, $height, $type] = getimagesize($file['tmp_name']); + $needsResize = $width > $this->maxWidth || $height > $this->maxHeight; + + // Génération d'un nom de fichier unique + $extension = $this->getExtensionFromMimeType($mimeType); + $filename = uniqid() . '.' . $extension; + $targetPath = $this->uploadDir . $filename; + + if ($needsResize) { + // Calcul des nouvelles dimensions en conservant le ratio + $ratio = min($this->maxWidth / $width, $this->maxHeight / $height); + $newWidth = round($width * $ratio); + $newHeight = round($height * $ratio); + + // Création de la nouvelle image + $sourceImage = $this->createImageFromFile($file['tmp_name'], $mimeType); + $newImage = imagecreatetruecolor($newWidth, $newHeight); + + // Préservation de la transparence pour PNG + if ($mimeType === 'image/png') { + imagealphablending($newImage, false); + imagesavealpha($newImage, true); + } + + // Redimensionnement + imagecopyresampled( + $newImage, $sourceImage, + 0, 0, 0, 0, + $newWidth, $newHeight, + $width, $height + ); + + // Sauvegarde de l'image redimensionnée + $this->saveImage($newImage, $targetPath, $mimeType); + + // Libération de la mémoire + imagedestroy($sourceImage); + imagedestroy($newImage); + } else { + // Déplacement du fichier original si pas besoin de redimensionnement + if (!move_uploaded_file($file['tmp_name'], $targetPath)) { + throw new Exception('Erreur lors du déplacement du fichier uploadé'); + } + } + + // Retourner le chemin relatif pour l'éditeur + return [ + 'success' => true, + 'url' => $this->getRelativePath($targetPath), + 'width' => $needsResize ? $newWidth : $width, + 'height' => $needsResize ? $newHeight : $height + ]; + + } catch (Exception $e) { + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } + } + + private function ensureUploadDirectory() { + if (!file_exists($this->uploadDir)) { + if (!mkdir($this->uploadDir, 0755, true)) { + throw new Exception('Impossible de créer le dossier d\'upload'); + } + } + + if (!is_writable($this->uploadDir)) { + throw new Exception('Le dossier d\'upload n\'est pas accessible en écriture'); + } + } + + private function createImageFromFile($file, $mimeType) { + switch ($mimeType) { + case 'image/jpeg': + return imagecreatefromjpeg($file); + case 'image/png': + return imagecreatefrompng($file); + case 'image/gif': + return imagecreatefromgif($file); + case 'image/webp': + return imagecreatefromwebp($file); + default: + throw new Exception('Type d\'image non supporté'); + } + } + + private function saveImage($image, $path, $mimeType) { + switch ($mimeType) { + case 'image/jpeg': + return imagejpeg($image, $path, 85); // Qualité 85% + case 'image/png': + return imagepng($image, $path, 8); // Compression 8 + case 'image/gif': + return imagegif($image, $path); + case 'image/webp': + return imagewebp($image, $path, 85); // Qualité 85% + default: + throw new Exception('Type d\'image non supporté pour la sauvegarde'); + } + } + + private function getExtensionFromMimeType($mimeType) { + $map = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp' + ]; + return $map[$mimeType] ?? 'jpg'; + } + + private function getRelativePath($absolutePath) { + $relativePath = str_replace(__DIR__ . '/../../', '', $absolutePath); + // Ajout de '../' car on est dans admin/api/ + return '../' . str_replace('\\', '/', $relativePath); // Pour la compatibilité Windows + } + + private function getUploadErrorMessage($error) { + $errors = [ + UPLOAD_ERR_INI_SIZE => 'Le fichier dépasse la taille maximale autorisée par PHP', + UPLOAD_ERR_FORM_SIZE => 'Le fichier dépasse la taille maximale autorisée par le formulaire', + UPLOAD_ERR_PARTIAL => 'Le fichier n\'a été que partiellement uploadé', + UPLOAD_ERR_NO_FILE => 'Aucun fichier n\'a été uploadé', + UPLOAD_ERR_NO_TMP_DIR => 'Dossier temporaire manquant', + UPLOAD_ERR_CANT_WRITE => 'Échec de l\'écriture du fichier sur le disque', + UPLOAD_ERR_EXTENSION => 'Une extension PHP a arrêté l\'upload' + ]; + return $errors[$error] ?? 'Erreur inconnue lors de l\'upload'; + } +} + +// Point d'entrée du script +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + exit(json_encode(['error' => 'Méthode non autorisée'])); +} + +// Vérification de l'authentification +if (!Auth::check()) { + http_response_code(401); + exit(json_encode(['error' => 'Non autorisé'])); +} + +// Récupération de l'ID du roman +$storyId = $_POST['storyId'] ?? null; +if (!$storyId) { + http_response_code(400); + exit(json_encode(['error' => 'ID du roman manquant'])); +} + +// Traitement de l'upload +try { + $handler = new ImageUploadHandler($storyId); + $result = $handler->handleUpload($_FILES['image']); + + if (!$result['success']) { + http_response_code(400); + } + + header('Content-Type: application/json'); + echo json_encode($result); +} catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => 'Erreur serveur : ' . $e->getMessage() + ]); +} \ No newline at end of file diff --git a/assets/css/editor.css b/assets/css/editor.css index df38b03..647b3fd 100644 --- a/assets/css/editor.css +++ b/assets/css/editor.css @@ -1,55 +1,197 @@ -/* Personnalisation de l'éditeur Quill */ -.ql-toolbar { - background: var(--bg-secondary) !important; - border-color: var(--border-color) !important; +/* Base de l'éditeur */ +.ql-toolbar.ql-snow { + background: var(--bg-secondary); + border-color: var(--border-color); border-radius: var(--radius-sm) var(--radius-sm) 0 0; + padding: var(--spacing-sm); + display: flex; + flex-wrap: wrap; + gap: 5px; } -.ql-container { - background: var(--input-bg) !important; - border-color: var(--border-color) !important; - color: var(--text-primary) !important; +.ql-container.ql-snow { + background: var(--input-bg); + border-color: var(--border-color); border-radius: 0 0 var(--radius-sm) var(--radius-sm); + color: var(--text-primary); min-height: 300px; + font-family: inherit; } +/* Contenu de l'éditeur */ .ql-editor { - color: var(--text-primary) !important; + color: var(--text-primary); + padding: var(--spacing-md); + font-size: 1rem; + line-height: 1.6; } -/* Boutons de la barre d'outils */ +.ql-editor.ql-blank::before { + color: var(--text-secondary); + font-style: italic; + left: var(--spacing-md); +} + +/* Styles des éléments dans l'éditeur */ +.ql-editor h1, .ql-editor h2, .ql-editor h3 { + color: var(--text-primary); + margin: 1em 0 0.5em; +} + +.ql-editor blockquote { + border-left: 4px solid var(--accent-primary); + margin: 1.5em 0; + padding-left: 1em; + color: var(--text-secondary); +} + +.ql-editor pre.ql-syntax { + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: var(--radius-sm); + padding: var(--spacing-md); +} + +/* Icônes de la barre d'outils */ .ql-snow .ql-stroke { - stroke: var(--text-secondary) !important; + stroke: var(--text-secondary); } .ql-snow .ql-fill { - fill: var(--text-secondary) !important; + fill: var(--text-secondary); } .ql-snow .ql-picker { - color: var(--text-secondary) !important; + color: var(--text-secondary); } -/* Menu déroulant */ -.ql-snow .ql-picker-options { - background-color: var(--bg-secondary) !important; - border-color: var(--border-color) !important; +.ql-snow.ql-toolbar button { + padding: 4px 6px; + height: 28px; + width: 28px; } -/* États au survol */ +/* États au survol et actif */ .ql-snow .ql-toolbar button:hover, -.ql-snow .ql-toolbar button.ql-active { - .ql-stroke { - stroke: var(--accent-primary) !important; - } - .ql-fill { - fill: var(--accent-primary) !important; - } +.ql-snow .ql-toolbar button:focus, +.ql-snow .ql-toolbar button.ql-active, +.ql-snow .ql-toolbar .ql-picker-label:hover, +.ql-snow .ql-toolbar .ql-picker-label.ql-active, +.ql-snow .ql-toolbar .ql-picker-item:hover, +.ql-snow .ql-toolbar .ql-picker-item.ql-selected { + color: var(--accent-primary); +} + +.ql-snow .ql-toolbar button:hover .ql-stroke, +.ql-snow .ql-toolbar button:focus .ql-stroke, +.ql-snow .ql-toolbar button.ql-active .ql-stroke, +.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke, +.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke { + stroke: var(--accent-primary); +} + +.ql-snow .ql-toolbar button:hover .ql-fill, +.ql-snow .ql-toolbar button:focus .ql-fill, +.ql-snow .ql-toolbar button.ql-active .ql-fill, +.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill, +.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill { + fill: var(--accent-primary); +} + +/* Menus déroulants */ +.ql-snow .ql-picker-options { + background-color: var(--bg-secondary); + border-color: var(--border-color); + border-radius: var(--radius-sm); + padding: var(--spacing-sm); +} + +.ql-snow .ql-picker-options .ql-picker-item { + color: var(--text-primary); + padding: 4px 8px; + border-radius: var(--radius-sm); +} + +.ql-snow .ql-picker-options .ql-picker-item:hover { + color: var(--accent-primary); + background-color: var(--bg-tertiary); +} + +/* Tooltip pour les liens */ +.ql-snow .ql-tooltip { + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + color: var(--text-primary); + border-radius: var(--radius-sm); + padding: var(--spacing-sm); + z-index: 1000; +} + +.ql-snow .ql-tooltip input[type=text] { + background-color: var(--input-bg); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 4px 8px; + border-radius: var(--radius-sm); + width: 170px; + margin: 0; +} + +.ql-snow .ql-tooltip input[type=text]:focus { + border-color: var(--accent-primary); + outline: none; +} + +.ql-snow .ql-tooltip a.ql-action, +.ql-snow .ql-tooltip a.ql-remove { + color: var(--accent-primary); + padding: 4px 8px; + margin: 0 4px; + cursor: pointer; + border-radius: var(--radius-sm); + display: inline-block; + text-decoration: none; +} + +.ql-snow .ql-tooltip a.ql-action:hover, +.ql-snow .ql-tooltip a.ql-remove:hover { + color: var(--accent-secondary); + background-color: var(--bg-tertiary); +} + +/* Styles des couleurs */ +.ql-snow .ql-color-picker .ql-picker-options { + padding: 5px; + width: 152px; +} + +.ql-snow .ql-color-picker .ql-picker-item { + border: 1px solid var(--border-color); + margin: 2px; + border-radius: var(--radius-sm); } /* Responsive */ @media (max-width: 768px) { - .ql-container { + .ql-snow.ql-toolbar { + padding: var(--spacing-xs); + } + + .ql-container.ql-snow { min-height: 200px; } + + .ql-snow .ql-tooltip { + left: 0 !important; + top: 100% !important; + width: 100%; + position: fixed; + transform: none !important; + } + + .ql-snow .ql-tooltip input[type=text] { + width: 100%; + margin: 4px 0; + } } \ No newline at end of file diff --git a/assets/js/story-edit.js b/assets/js/story-edit.js index 70d8d56..079583d 100644 --- a/assets/js/story-edit.js +++ b/assets/js/story-edit.js @@ -1,33 +1,66 @@ document.addEventListener('DOMContentLoaded', function() { - // Initialisation de l'éditeur Quill + let currentChapterId = null; + const storyId = document.querySelector('input[name="id"]')?.value; + + // Initialisation de l'éditeur Quill avec toutes les options const quill = new Quill('#editor', { theme: 'snow', modules: { toolbar: [ - ['bold', 'italic', 'underline'], + [{ 'header': [1, 2, 3, false] }], + ['bold', 'italic', 'underline', 'strike'], + [{ 'color': [] }, { 'background': [] }], + [{ 'font': [] }], + [{ 'align': [] }], + ['blockquote', 'code-block'], [{ 'list': 'ordered'}, { 'list': 'bullet' }], + [{ 'script': 'sub'}, { 'script': 'super' }], + [{ 'indent': '-1'}, { 'indent': '+1' }], + [{ 'direction': 'rtl' }], + ['link', 'image', 'video'], ['clean'] - ] - } + ], + keyboard: { + bindings: { + tab: false, + 'indent backwards': false + } + } + }, + placeholder: 'Commencez à écrire votre chapitre ici...', + formats: [ + 'header', + 'bold', 'italic', 'underline', 'strike', + 'color', 'background', + 'font', + 'align', + 'blockquote', 'code-block', + 'list', 'bullet', + 'script', + 'indent', + 'direction', + 'link', 'image', 'video' + ] }); - // Variables globales - const chaptersList = document.getElementById('chaptersList'); + // Gestion des chapitres const modal = document.getElementById('chapterEditor'); const addChapterBtn = document.getElementById('addChapter'); - let currentChapterId = null; - const storyId = document.querySelector('input[name="id"]')?.value; + const saveChapterBtn = document.getElementById('saveChapter'); + const cancelEditBtn = document.getElementById('cancelEdit'); + const chapterTitleInput = document.getElementById('chapterTitle'); + const chaptersList = document.getElementById('chaptersList'); - // Initialisation du tri par glisser-déposer + // Configuration de Sortable pour la réorganisation des chapitres if (chaptersList) { new Sortable(chaptersList, { animation: 150, + handle: '.chapter-number', // Utiliser le numéro comme poignée de drag ghostClass: 'sortable-ghost', - onEnd: async function() { - // Récupérer le nouvel ordre des chapitres + onEnd: async function(evt) { const chapters = Array.from(chaptersList.children).map((item, index) => ({ id: item.dataset.id, - order: index + position: index })); try { @@ -46,160 +79,186 @@ document.addEventListener('DOMContentLoaded', function() { throw new Error('Erreur lors de la réorganisation'); } - // Mettre à jour les numéros affichés + // Mise à jour des numéros de chapitres chapters.forEach((chapter, index) => { const element = document.querySelector(`[data-id="${chapter.id}"] .chapter-number`); if (element) { element.textContent = index + 1; } }); + } catch (error) { console.error('Erreur:', error); - alert('Erreur lors de la réorganisation des chapitres'); + showNotification('Erreur lors de la réorganisation des chapitres', 'error'); } } }); } - // Gestion de l'ajout d'un nouveau chapitre + // Gestionnaires d'événements pour l'édition des chapitres if (addChapterBtn) { addChapterBtn.addEventListener('click', () => { currentChapterId = null; - document.getElementById('chapterTitle').value = ''; - quill.setContents([{ insert: '\n' }]); + chapterTitleInput.value = ''; + quill.setContents([]); modal.style.display = 'block'; }); } - // Gestion de l'édition d'un chapitre - chaptersList?.addEventListener('click', async (e) => { - if (e.target.matches('.edit-chapter')) { - const chapterItem = e.target.closest('.chapter-item'); - currentChapterId = chapterItem.dataset.id; - - try { - const response = await fetch(`api/get-chapter.php?storyId=${storyId}&chapterId=${currentChapterId}`); - if (!response.ok) throw new Error('Erreur lors de la récupération du chapitre'); - - const chapter = await response.json(); - document.getElementById('chapterTitle').value = chapter.title; - try { - if (typeof chapter.content === 'string') { - quill.root.innerHTML = chapter.content; - } else { - quill.setContents(chapter.content); - } - } catch (error) { - console.error('Erreur lors du chargement du contenu:', error); - quill.root.innerHTML = chapter.content; + if (cancelEditBtn) { + cancelEditBtn.addEventListener('click', () => { + if (hasUnsavedChanges()) { + if (!confirm('Des modifications non sauvegardées seront perdues. Voulez-vous vraiment fermer ?')) { + return; } - modal.style.display = 'block'; - } catch (error) { - console.error('Erreur:', error); - alert('Erreur lors de la récupération du chapitre'); } + modal.style.display = 'none'; + }); + } + +// Gestion des clics sur la liste des chapitres + if (chaptersList) { + chaptersList.addEventListener('click', async (e) => { + const target = e.target; + + if (target.matches('.edit-chapter')) { + const chapterItem = target.closest('.chapter-item'); + currentChapterId = chapterItem.dataset.id; + + try { + const response = await fetch(`api/get-chapter.php?storyId=${storyId}&chapterId=${currentChapterId}`); + + if (!response.ok) { + throw new Error('Erreur réseau'); + } + + const chapter = await response.json(); + + // Mise à jour du titre + chapterTitleInput.value = chapter.title || ''; + + // Gestion du contenu + if (chapter.html) { + // Si on a du HTML, on le charge directement + quill.root.innerHTML = chapter.html; + } else if (chapter.content) { + // Si on a du contenu au format Delta + try { + const content = typeof chapter.content === 'string' + ? JSON.parse(chapter.content) + : chapter.content; + quill.setContents(content); + } catch (contentError) { + console.error('Erreur de parsing du contenu:', contentError); + quill.setText(chapter.content || ''); + } + } else { + // Contenu vide par défaut + quill.setContents([]); + } + + modal.style.display = 'block'; + } catch (error) { + console.error('Erreur détaillée:', error); + showNotification('Erreur lors du chargement du chapitre. Veuillez réessayer.', 'error'); + } + } + }); + } + + // Système de notification + function showNotification(message, type = 'success') { + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + + document.body.appendChild(notification); + + // Animation d'entrée + setTimeout(() => { + notification.style.opacity = '1'; + notification.style.transform = 'translateY(0)'; + }, 10); + + // Auto-suppression après 3 secondes + setTimeout(() => { + notification.style.opacity = '0'; + notification.style.transform = 'translateY(-100%)'; + setTimeout(() => notification.remove(), 300); + }, 3000); + } + + // Vérification des changements non sauvegardés + function hasUnsavedChanges() { + if (!currentChapterId) { + return chapterTitleInput.value !== '' || quill.getLength() > 1; } - }); - - // Gestion de la suppression d'un chapitre - chaptersList?.addEventListener('click', async (e) => { - if (e.target.matches('.delete-chapter')) { - if (!confirm('Voulez-vous vraiment supprimer ce chapitre ?')) return; - - const chapterItem = e.target.closest('.chapter-item'); - const chapterId = chapterItem.dataset.id; + // Pour les chapitres existants, il faudrait comparer avec le contenu original + return true; // Par défaut, on suppose qu'il y a des changements + } + // Sauvegarde d'un chapitre + if (saveChapterBtn) { + saveChapterBtn.addEventListener('click', async () => { + const title = chapterTitleInput.value.trim(); + if (!title) { + showNotification('Le titre est requis', 'error'); + return; + } + try { - const response = await fetch('api/delete-chapter.php', { + const response = await fetch('api/save-chapter.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ storyId: storyId, - chapterId: chapterId + chapterId: currentChapterId, + title: title, + content: JSON.stringify(quill.getContents()) }) }); - - if (!response.ok) throw new Error('Erreur lors de la suppression'); - - chapterItem.remove(); - - // Mettre à jour les numéros des chapitres - document.querySelectorAll('.chapter-number').forEach((num, index) => { - num.textContent = index + 1; - }); + + if (response.ok) { + showNotification('Chapitre sauvegardé avec succès'); + setTimeout(() => location.reload(), 500); + } else { + throw new Error('Erreur lors de la sauvegarde'); + } } catch (error) { console.error('Erreur:', error); - alert('Erreur lors de la suppression du chapitre'); + showNotification('Erreur lors de la sauvegarde du chapitre', 'error'); } - } - }); + }); + } - // Gestion de la sauvegarde d'un chapitre - document.getElementById('saveChapter')?.addEventListener('click', async () => { - const title = document.getElementById('chapterTitle').value; - if (!title.trim()) { - alert('Le titre du chapitre est requis'); - return; + // Ajouter le style des notifications + const style = document.createElement('style'); + style.textContent = ` + .notification { + position: fixed; + top: 20px; + right: 20px; + padding: 1rem 2rem; + border-radius: var(--radius-sm); + background: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + opacity: 0; + transform: translateY(-100%); + transition: opacity 0.3s ease, transform 0.3s ease; + z-index: 1000; } - try { - const response = await fetch('api/save-chapter.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - storyId: storyId, - chapterId: currentChapterId, - title: title, - content: quill.root.innerHTML - }) - }); - - if (!response.ok) throw new Error('Erreur lors de la sauvegarde'); - - const result = await response.json(); - - if (!currentChapterId) { - // Nouveau chapitre - ajouter à la liste - const newChapter = document.createElement('div'); - newChapter.className = 'chapter-item'; - newChapter.dataset.id = result.chapterId; - newChapter.innerHTML = ` - ${document.querySelectorAll('.chapter-item').length + 1} -

${title}

-
- - -
- `; - chaptersList.appendChild(newChapter); - } else { - // Mise à jour du titre dans la liste - const chapterItem = document.querySelector(`[data-id="${currentChapterId}"] .chapter-title`); - if (chapterItem) { - chapterItem.textContent = title; - } - } - - modal.style.display = 'none'; - } catch (error) { - console.error('Erreur:', error); - alert('Erreur lors de la sauvegarde du chapitre'); + .notification.success { + background-color: var(--success-color); + color: white; } - }); - // Fermeture de la modale - document.getElementById('cancelEdit')?.addEventListener('click', () => { - modal.style.display = 'none'; - }); - - // Fermeture de la modale en cliquant en dehors - modal?.addEventListener('click', (e) => { - if (e.target === modal) { - modal.style.display = 'none'; + .notification.error { + background-color: var(--error-color); + color: white; } - }); + `; + document.head.appendChild(style); }); \ No newline at end of file