From 3660e796ee147bb188a3e323c56675dabe3b3606 Mon Sep 17 00:00:00 2001 From: Esenjin Date: Mon, 17 Feb 2025 17:27:45 +0100 Subject: [PATCH] =?UTF-8?q?ajout=20de=20la=20possibilit=C3=A9=20d'ajouter?= =?UTF-8?q?=20des=20couvertures=20aux=20chapitres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/api/get-chapter.php | 5 + admin/api/update-chapter-cover.php | 171 +++++++++++++++++++++++++++++ admin/story-edit.php | 26 +++++ assets/css/components.css | 22 ++++ assets/js/story-edit.js | 148 +++++++++++++++++++++++++ chapitre.php | 7 +- 6 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 admin/api/update-chapter-cover.php diff --git a/admin/api/get-chapter.php b/admin/api/get-chapter.php index 011455f..bfad448 100644 --- a/admin/api/get-chapter.php +++ b/admin/api/get-chapter.php @@ -41,6 +41,11 @@ try { $chapter['title'] = ''; } + // S'assurer que le champ cover existe + if (!isset($chapter['cover'])) { + $chapter['cover'] = null; + } + // Conversion du contenu HTML en format Delta de Quill si nécessaire if (!isset($chapter['content'])) { $chapter['content'] = '{"ops":[{"insert":"\n"}]}'; diff --git a/admin/api/update-chapter-cover.php b/admin/api/update-chapter-cover.php new file mode 100644 index 0000000..e3757a4 --- /dev/null +++ b/admin/api/update-chapter-cover.php @@ -0,0 +1,171 @@ +uploadDir = __DIR__ . '/../../assets/images/chapters/' . $storyId . '/covers/'; + $this->ensureUploadDirectory(); + } + + public function handleUpload($file, $chapterId) { + 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'); + } + + // Génération du nom de fichier + $extension = $this->getExtensionFromMimeType($mimeType); + $filename = $chapterId . '-cover.' . $extension; + $targetPath = $this->uploadDir . $filename; + + // Suppression de l'ancienne image si elle existe + $this->removeOldCover($chapterId); + + // Déplacement du fichier + if (!move_uploaded_file($file['tmp_name'], $targetPath)) { + throw new Exception('Erreur lors du déplacement du fichier uploadé'); + } + + // Retourner le chemin relatif pour stockage en BDD + return 'assets/images/chapters/' . basename(dirname($this->uploadDir)) . '/covers/' . $filename; + + } catch (Exception $e) { + throw $e; + } + } + + public function removeCover($chapterId) { + foreach (glob($this->uploadDir . $chapterId . '-cover.*') as $file) { + unlink($file); + } + return true; + } + + 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 removeOldCover($chapterId) { + foreach (glob($this->uploadDir . $chapterId . '-cover.*') as $file) { + unlink($file); + } + } + + private function getExtensionFromMimeType($mimeType) { + $map = [ + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp' + ]; + return $map[$mimeType] ?? 'jpg'; + } + + 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'; + } +} + +// Vérification de l'authentification +if (!Auth::check()) { + http_response_code(401); + exit(json_encode(['success' => false, 'error' => 'Non autorisé'])); +} + +// Traitement de la requête +try { + // Récupérer les données selon la méthode + $input = json_decode(file_get_contents('php://input'), true); + if ($input) { + // Cas d'une requête JSON (suppression) + $storyId = $input['storyId'] ?? null; + $chapterId = $input['chapterId'] ?? null; + $isDelete = $input['delete'] ?? false; + } else { + // Cas d'un upload de fichier + $storyId = $_POST['storyId'] ?? null; + $chapterId = $_POST['chapterId'] ?? null; + $isDelete = false; + } + + if (!$storyId || !$chapterId) { + throw new Exception('Paramètres manquants'); + } + + // Récupération du roman + $story = Stories::get($storyId); + if (!$story) { + throw new Exception('Roman non trouvé'); + } + + // Trouver le chapitre concerné + $chapterFound = false; + foreach ($story['chapters'] as &$chapter) { + if ($chapter['id'] === $chapterId) { + $chapterFound = true; + break; + } + } + + if (!$chapterFound) { + throw new Exception('Chapitre non trouvé'); + } + + $handler = new ChapterCoverHandler($storyId); + + // Traitement selon le type de requête + if ($isDelete) { + $handler->removeCover($chapterId); + $chapter['cover'] = null; + } else if (isset($_FILES['cover'])) { + $chapter['cover'] = $handler->handleUpload($_FILES['cover'], $chapterId); + } + + // Sauvegarde des modifications + Stories::save($story); + + echo json_encode(['success' => true]); + +} catch (Exception $e) { + http_response_code(500); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} \ No newline at end of file diff --git a/admin/story-edit.php b/admin/story-edit.php index c2ef8d9..625b292 100644 --- a/admin/story-edit.php +++ b/admin/story-edit.php @@ -136,6 +136,7 @@ function generateSlug($title) {

+
@@ -161,6 +162,31 @@ function generateSlug($title) { + + + diff --git a/assets/css/components.css b/assets/css/components.css index 4e74362..68b6b92 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -306,6 +306,28 @@ animation: modalFadeIn 0.3s ease-out; } +.cover-preview-container { + padding: var(--spacing-lg); +} + +.current-cover-preview { + margin-bottom: var(--spacing-md); +} + +.current-cover-preview img { + max-width: 100%; + max-height: 300px; + display: block; + margin: 0 auto; + border-radius: var(--radius-sm); + border: 1px solid var(--border-color); +} + +.chapter-title-display { + color: var(--text-secondary); + margin-top: var(--spacing-sm); +} + /* Media queries */ @media (max-width: 768px) { .story-cover { diff --git a/assets/js/story-edit.js b/assets/js/story-edit.js index 77a8aa4..8975376 100644 --- a/assets/js/story-edit.js +++ b/assets/js/story-edit.js @@ -127,6 +127,154 @@ document.addEventListener('DOMContentLoaded', function() { const chapterTitleInput = document.getElementById('chapterTitle'); const chaptersList = document.getElementById('chaptersList'); + // Éléments de la modale de couverture + const coverModal = document.getElementById('chapterCoverEditor'); + const coverPreview = document.querySelector('.current-cover-preview'); + const chapterCoverInput = document.getElementById('chapterCover'); + const saveCoverBtn = document.getElementById('saveCover'); + const cancelCoverBtn = document.getElementById('cancelCoverEdit'); + const deleteCoverBtn = document.getElementById('deleteCover'); + let currentChapterCover = null; + + // Gestionnaire pour le bouton de couverture + if (chaptersList) { + chaptersList.addEventListener('click', async (e) => { + if (e.target.matches('.edit-cover')) { + const chapterItem = e.target.closest('.chapter-item'); + const chapterId = chapterItem.dataset.id; + const chapterTitle = chapterItem.querySelector('.chapter-title').textContent; + + // Mise à jour du titre dans la modale + coverModal.querySelector('.chapter-title-display').textContent = chapterTitle; + currentChapterCover = chapterId; + + try { + const response = await fetch(`api/get-chapter.php?storyId=${storyId}&chapterId=${chapterId}`); + if (!response.ok) throw new Error('Erreur réseau'); + + const chapter = await response.json(); + + // Afficher l'image existante si elle existe + if (chapter.cover) { + coverPreview.innerHTML = `Couverture actuelle`; + deleteCoverBtn.style.display = 'block'; + } else { + coverPreview.innerHTML = ''; + deleteCoverBtn.style.display = 'none'; + } + + coverModal.style.display = 'block'; + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors du chargement de la couverture', 'error'); + } + } + }); + } + + // Prévisualisation de l'image + if (chapterCoverInput) { + chapterCoverInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + coverPreview.innerHTML = `Prévisualisation`; + deleteCoverBtn.style.display = 'none'; + }; + reader.readAsDataURL(file); + } + }); + } + + // Fermeture de la modale de couverture + if (cancelCoverBtn) { + cancelCoverBtn.addEventListener('click', () => { + coverModal.style.display = 'none'; + chapterCoverInput.value = ''; + }); + } + + // Sauvegarde de la couverture + if (saveCoverBtn) { + saveCoverBtn.addEventListener('click', async () => { + const file = chapterCoverInput.files[0]; + if (!file && !coverPreview.querySelector('img')) { + showNotification('Veuillez sélectionner une image', 'error'); + return; + } + + const formData = new FormData(); + formData.append('storyId', storyId); + formData.append('chapterId', currentChapterCover); + if (file) { + formData.append('cover', file); + } + + try { + const response = await fetch('api/update-chapter-cover.php', { + method: 'POST', + body: formData + }); + + if (!response.ok) throw new Error('Erreur lors de l\'upload'); + + const result = await response.json(); + if (result.success) { + showNotification('Couverture mise à jour avec succès'); + coverModal.style.display = 'none'; + chapterCoverInput.value = ''; + } else { + throw new Error(result.error || 'Erreur lors de la mise à jour'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification(error.message, 'error'); + } + }); + } + + // Suppression de la couverture + if (deleteCoverBtn) { + deleteCoverBtn.addEventListener('click', () => { + confirmDialog.show({ + title: 'Supprimer la couverture', + message: 'Voulez-vous vraiment supprimer la couverture de ce chapitre ?', + confirmText: 'Supprimer', + confirmClass: 'danger', + onConfirm: async () => { + try { + const response = await fetch('api/update-chapter-cover.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + storyId: storyId, + chapterId: currentChapterCover, + delete: true + }) + }); + + if (!response.ok) throw new Error('Erreur réseau'); + + const result = await response.json(); + if (result.success) { + showNotification('Couverture supprimée avec succès'); + coverPreview.innerHTML = ''; + coverModal.style.display = 'none'; + } else { + throw new Error(result.error || 'Erreur lors de la suppression'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification(error.message, 'error'); + } + } + }); + }); + } + // Configuration de Sortable pour la réorganisation des chapitres if (chaptersList) { new Sortable(chaptersList, { diff --git a/chapitre.php b/chapitre.php index 388edcd..0f932f3 100644 --- a/chapitre.php +++ b/chapitre.php @@ -57,8 +57,11 @@ $config = Config::load(); - -
+ +
+ +
+