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) {
= htmlspecialchars($chapter['title']) ?>
+
@@ -161,6 +162,31 @@ function generateSlug($title) {
+
+
+
+
+
+
+
+
+
+
+
+ Formats acceptés : JPG, PNG, GIF. Taille maximum : 5MB
+
+
+
+
+
+
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 = `
`;
+ 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 = `
`;
+ 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();
-
-