From 1d00a001461cfa09f95d2cc93d5aca584e13f42a Mon Sep 17 00:00:00 2001 From: Esenjin Date: Fri, 14 Feb 2025 19:57:45 +0100 Subject: [PATCH] =?UTF-8?q?am=C3=A9lioration=20de=20l'affichage=20et=20des?= =?UTF-8?q?=20comportements=20de=20la=20partie=20administration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/api/delete-chapter.php | 39 +++++ admin/api/get-chapter.php | 43 ++++++ admin/api/reorder-chapters.php | 48 +++++++ admin/index.php | 7 +- admin/story-edit.php | 15 +- assets/css/components.css | 189 ++++++++++++++++++++++-- assets/css/layout.css | 15 ++ assets/css/theme.css | 1 + assets/js/story-edit.js | 255 ++++++++++++++++++++------------- includes/stories.php | 42 ++---- 10 files changed, 502 insertions(+), 152 deletions(-) create mode 100644 admin/api/delete-chapter.php create mode 100644 admin/api/get-chapter.php create mode 100644 admin/api/reorder-chapters.php diff --git a/admin/api/delete-chapter.php b/admin/api/delete-chapter.php new file mode 100644 index 0000000..cb05dbb --- /dev/null +++ b/admin/api/delete-chapter.php @@ -0,0 +1,39 @@ + 'Non autorisé'])); +} + +$input = json_decode(file_get_contents('php://input'), true); +$storyId = $input['storyId'] ?? ''; +$chapterId = $input['chapterId'] ?? ''; + +if (!$storyId || !$chapterId) { + http_response_code(400); + exit(json_encode(['error' => 'Paramètres manquants'])); +} + +try { + $story = Stories::get($storyId); + if (!$story) { + throw new Exception('Roman non trouvé'); + } + + // Filtrer les chapitres pour retirer celui à supprimer + $story['chapters'] = array_values(array_filter($story['chapters'], function($chapter) use ($chapterId) { + return $chapter['id'] !== $chapterId; + })); + + // Sauvegarder les modifications + Stories::save($story); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/admin/api/get-chapter.php b/admin/api/get-chapter.php new file mode 100644 index 0000000..57edcb3 --- /dev/null +++ b/admin/api/get-chapter.php @@ -0,0 +1,43 @@ + 'Non autorisé'])); +} + +$storyId = $_GET['storyId'] ?? ''; +$chapterId = $_GET['chapterId'] ?? ''; + +if (!$storyId || !$chapterId) { + http_response_code(400); + exit(json_encode(['error' => 'Paramètres manquants'])); +} + +try { + $story = Stories::get($storyId); + if (!$story) { + throw new Exception('Roman non trouvé'); + } + + $chapter = null; + foreach ($story['chapters'] as $ch) { + if ($ch['id'] === $chapterId) { + $chapter = $ch; + break; + } + } + + if (!$chapter) { + throw new Exception('Chapitre non trouvé'); + } + + header('Content-Type: application/json'); + echo json_encode($chapter); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/admin/api/reorder-chapters.php b/admin/api/reorder-chapters.php new file mode 100644 index 0000000..417c613 --- /dev/null +++ b/admin/api/reorder-chapters.php @@ -0,0 +1,48 @@ + 'Non autorisé'])); +} + +$input = json_decode(file_get_contents('php://input'), true); +$storyId = $input['storyId'] ?? ''; +$newOrder = $input['chapters'] ?? []; + +if (!$storyId || empty($newOrder)) { + http_response_code(400); + exit(json_encode(['error' => 'Paramètres manquants'])); +} + +try { + $story = Stories::get($storyId); + if (!$story) { + throw new Exception('Roman non trouvé'); + } + + // Créer un tableau temporaire pour le nouvel ordre + $reorderedChapters = []; + foreach ($newOrder as $item) { + foreach ($story['chapters'] as $chapter) { + if ($chapter['id'] === $item['id']) { + $reorderedChapters[] = $chapter; + break; + } + } + } + + // Mettre à jour l'ordre des chapitres + $story['chapters'] = $reorderedChapters; + + // Sauvegarder les modifications + Stories::save($story); + + echo json_encode(['success' => true]); +} catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); +} \ No newline at end of file diff --git a/admin/index.php b/admin/index.php index 7f88b07..798b9cd 100644 --- a/admin/index.php +++ b/admin/index.php @@ -39,7 +39,10 @@ $stories = Stories::getAll();

-

Dernière modification :

+

+ chapitre 1 ? 's' : '' ?>
+ Dernière modification : +

Modifier @@ -52,4 +55,4 @@ $stories = Stories::getAll(); - + \ No newline at end of file diff --git a/admin/story-edit.php b/admin/story-edit.php index 472f338..886465d 100644 --- a/admin/story-edit.php +++ b/admin/story-edit.php @@ -74,7 +74,7 @@ function generateSlug($title) {
-

roman

+

roman

@@ -121,16 +121,13 @@ function generateSlug($title) {
$chapter): ?> -
-
- - +
+ +

+
+
-
-
-
diff --git a/assets/css/components.css b/assets/css/components.css index 2494ea3..ca0c746 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -16,21 +16,115 @@ } .story-cover { - width: 120px; - height: 180px; + width: 200px; + height: 120px; object-fit: cover; border-radius: var(--radius-sm); border: 1px solid var(--border-color); } +.current-cover { + max-height: 300px; + width: auto; + display: block; + margin-bottom: var(--spacing-sm); + border-radius: var(--radius-sm); + border: 1px solid var(--border-color); +} + .story-info { flex: 1; } +.story-info p { + color: var(--text-secondary); + margin: 0; + line-height: 1.5; +} + .story-info h2 { + color: var(--text-primary); margin: 0 0 var(--spacing-sm) 0; } +/* Cartes des chapitres */ +.chapters-section { + margin-top: var(--spacing-xl); +} + +.chapters-section h2 { + margin-bottom: var(--spacing-md); +} + +.chapters-list { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + margin-top: var(--spacing-lg); +} + +.chapter-item { + background: var(--bg-tertiary); + padding: var(--spacing-lg); + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + display: flex; + align-items: center; + gap: var(--spacing-lg); + transition: transform var(--transition-fast), box-shadow var(--transition-fast); + cursor: grab; +} + +.chapter-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.chapter-item.sortable-ghost { + opacity: 0.5; + background: var(--accent-primary); +} + +.chapter-number { + background: var(--accent-primary); + color: var(--text-primary); + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + font-weight: bold; +} + +.chapter-title { + flex: 1; + font-size: 1.1rem; + color: var(--text-primary); + margin: 0; +} + +.chapter-actions { + display: flex; + gap: var(--spacing-sm); +} + +.chapter-actions button { + padding: var(--spacing-sm) var(--spacing-md); +} + +.edit-chapter { + background-color: var(--accent-primary); +} + +.delete-chapter { + background-color: var(--bg-secondary); +} + +.delete-chapter:hover { + background-color: var(--error-color); +} + /* Messages système */ .error-message, .success-message { @@ -54,16 +148,19 @@ position: fixed; top: 0; left: 0; + right: 0; + bottom: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); - display: flex; - align-items: center; - justify-content: center; z-index: 1000; } .modal-content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); background: var(--bg-tertiary); padding: var(--spacing-xl); border-radius: var(--radius-md); @@ -74,17 +171,85 @@ overflow-y: auto; } +.modal-content h2 { + margin-top: 0; + margin-bottom: var(--spacing-lg); + color: var(--text-primary); +} + +#chapterTitle { + width: 35%; + padding: var(--spacing-sm); + background-color: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 1rem; + margin-bottom: var(--spacing-md); + transition: border-color var(--transition-fast); +} + +#chapterTitle:focus { + outline: none; + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px rgba(139, 69, 19, 0.2); +} + +.modal-actions { + margin-top: var(--spacing-lg); + display: flex; + gap: var(--spacing-md); + justify-content: flex-end; +} + +/* Animation de la modale */ +@keyframes modalFadeIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal-content { + animation: modalFadeIn 0.3s ease-out; +} + /* Media queries */ @media (max-width: 768px) { - .story-item { - flex-direction: column; - text-align: center; - padding: var(--spacing-md); - } - .story-cover { width: 100%; - max-width: 200px; + height: 200px; + max-width: none; + } + + .current-cover { + max-width: 100%; height: auto; } + + .modal-content { + padding: var(--spacing-md); + } + + .modal-actions { + flex-direction: column; + } + + .modal-actions button { + width: 100%; + } + + .chapter-item { + flex-wrap: wrap; + gap: var(--spacing-md); + } + + .chapter-actions { + width: 100%; + justify-content: flex-end; + } } \ No newline at end of file diff --git a/assets/css/layout.css b/assets/css/layout.css index 62d1dd2..76d2394 100644 --- a/assets/css/layout.css +++ b/assets/css/layout.css @@ -19,6 +19,21 @@ gap: var(--spacing-md); } +.logout-form button { + padding: var(--spacing-sm) var(--spacing-md); + background-color: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + cursor: pointer; + transition: background-color var(--transition-fast), border-color var(--transition-fast); +} + +.logout-form button:hover { + background-color: var(--error-color); + border-color: var(--error-color); +} + /* Conteneur principal */ .admin-main { padding: var(--spacing-xl); diff --git a/assets/css/theme.css b/assets/css/theme.css index d1fadf4..a64b027 100644 --- a/assets/css/theme.css +++ b/assets/css/theme.css @@ -58,6 +58,7 @@ button, input, textarea { border-radius: var(--radius-sm); cursor: pointer; transition: background-color var(--transition-fast); + text-decoration: none; } .button:hover { diff --git a/assets/js/story-edit.js b/assets/js/story-edit.js index f89f503..70d8d56 100644 --- a/assets/js/story-edit.js +++ b/assets/js/story-edit.js @@ -4,141 +4,202 @@ document.addEventListener('DOMContentLoaded', function() { theme: 'snow', modules: { toolbar: [ - [{ 'header': [1, 2, 3, false] }], ['bold', 'italic', 'underline'], [{ 'list': 'ordered'}, { 'list': 'bullet' }], - ['link', 'blockquote'], ['clean'] ] } }); - // Gestion du tri des chapitres + // Variables globales const chaptersList = document.getElementById('chaptersList'); + const modal = document.getElementById('chapterEditor'); + const addChapterBtn = document.getElementById('addChapter'); + let currentChapterId = null; + const storyId = document.querySelector('input[name="id"]')?.value; + + // Initialisation du tri par glisser-déposer if (chaptersList) { new Sortable(chaptersList, { animation: 150, - handle: '.chapter-number', - onEnd: updateChaptersOrder + ghostClass: 'sortable-ghost', + onEnd: async function() { + // Récupérer le nouvel ordre des chapitres + const chapters = Array.from(chaptersList.children).map((item, index) => ({ + id: item.dataset.id, + order: index + })); + + try { + const response = await fetch('api/reorder-chapters.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + storyId: storyId, + chapters: chapters + }) + }); + + if (!response.ok) { + throw new Error('Erreur lors de la réorganisation'); + } + + // Mettre à jour les numéros affichés + 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'); + } + } }); } - // Gestion des événements - const modal = document.getElementById('chapterEditor'); - const addChapterBtn = document.getElementById('addChapter'); - const saveChapterBtn = document.getElementById('saveChapter'); - const cancelEditBtn = document.getElementById('cancelEdit'); - let currentEditingChapter = null; - + // Gestion de l'ajout d'un nouveau chapitre if (addChapterBtn) { addChapterBtn.addEventListener('click', () => { - currentEditingChapter = null; + currentChapterId = null; document.getElementById('chapterTitle').value = ''; - quill.setContents([]); + quill.setContents([{ insert: '\n' }]); modal.style.display = 'block'; }); } - // Sauvegarde d'un chapitre - if (saveChapterBtn) { - saveChapterBtn.addEventListener('click', async () => { - const title = document.getElementById('chapterTitle').value; - const content = quill.root.innerHTML; - const storyId = document.querySelector('input[name="id"]').value; - + // 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/save-chapter.php', { + 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; + } + modal.style.display = 'block'; + } catch (error) { + console.error('Erreur:', error); + alert('Erreur lors de la récupération du chapitre'); + } + } + }); + + // 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; + + try { + const response = await fetch('api/delete-chapter.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - storyId, - chapterId: currentEditingChapter, - title, - content + storyId: storyId, + chapterId: chapterId }) }); - if (response.ok) { - window.location.reload(); - } else { - throw new Error('Erreur lors de la sauvegarde'); - } + 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; + }); } catch (error) { - alert('Erreur lors de la sauvegarde du chapitre'); + console.error('Erreur:', error); + alert('Erreur lors de la suppression du chapitre'); } - }); - } + } + }); - // Fermeture du modal - if (cancelEditBtn) { - cancelEditBtn.addEventListener('click', () => { - modal.style.display = 'none'; - }); - } + // 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; + } - // Écoute des clics sur la liste des chapitres - if (chaptersList) { - chaptersList.addEventListener('click', (e) => { - if (e.target.matches('.delete-chapter')) { - if (confirm('Voulez-vous vraiment supprimer ce chapitre ?')) { - const chapterId = e.target.closest('.chapter-item').dataset.id; - deleteChapter(chapterId); + 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; } - } else if (e.target.matches('.chapter-title')) { - const chapterItem = e.target.closest('.chapter-item'); - currentEditingChapter = chapterItem.dataset.id; - document.getElementById('chapterTitle').value = e.target.value; - quill.root.innerHTML = chapterItem.querySelector('.editor').innerHTML; - modal.style.display = 'block'; } - }); - } -}); -// Mise à jour de l'ordre des chapitres -async function updateChaptersOrder() { - const chapters = Array.from(document.querySelectorAll('.chapter-item')) - .map((item, index) => ({ - id: item.dataset.id, - order: index - })); - - try { - const response = await fetch('api/update-chapters-order.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(chapters) - }); - - if (!response.ok) { - throw new Error('Erreur lors de la mise à jour'); + modal.style.display = 'none'; + } catch (error) { + console.error('Erreur:', error); + alert('Erreur lors de la sauvegarde du chapitre'); } - } catch (error) { - alert('Erreur lors de la mise à jour de l\'ordre des chapitres'); - } -} + }); -// Suppression d'un chapitre -async function deleteChapter(chapterId) { - try { - const response = await fetch('api/delete-chapter.php', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ chapterId }) - }); + // Fermeture de la modale + document.getElementById('cancelEdit')?.addEventListener('click', () => { + modal.style.display = 'none'; + }); - if (response.ok) { - window.location.reload(); - } else { - throw new Error('Erreur lors de la suppression'); + // Fermeture de la modale en cliquant en dehors + modal?.addEventListener('click', (e) => { + if (e.target === modal) { + modal.style.display = 'none'; } - } catch (error) { - alert('Erreur lors de la suppression du chapitre'); - } -} \ No newline at end of file + }); +}); \ No newline at end of file diff --git a/includes/stories.php b/includes/stories.php index 496ce48..90f4f36 100644 --- a/includes/stories.php +++ b/includes/stories.php @@ -3,7 +3,6 @@ class Stories { private static $storiesDir = __DIR__ . '/../stories/'; public static function getAll() { - self::ensureDirectoryExists(); $stories = []; foreach (glob(self::$storiesDir . '*.json') as $file) { $story = json_decode(file_get_contents($file), true); @@ -13,7 +12,6 @@ class Stories { } public static function get($id) { - self::ensureDirectoryExists(); $file = self::$storiesDir . $id . '.json'; if (!file_exists($file)) { return null; @@ -22,40 +20,20 @@ class Stories { } public static function save($story) { - self::ensureDirectoryExists(); + // Ajout de l'heure à la date de mise à jour + $story['updated'] = date('Y-m-d H:i:s'); - // Vérifier que l'ID est valide - if (empty($story['id'])) { - throw new Exception('L\'ID du roman est requis'); - } - - // Créer le chemin complet du fichier $file = self::$storiesDir . $story['id'] . '.json'; - - // Encoder en JSON avec options de formatage - $jsonContent = json_encode($story, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); - if ($jsonContent === false) { - throw new Exception('Erreur lors de l\'encodage JSON: ' . json_last_error_msg()); - } - - // Écrire le fichier - $bytesWritten = file_put_contents($file, $jsonContent); - if ($bytesWritten === false) { - throw new Exception('Erreur lors de l\'écriture du fichier: ' . error_get_last()['message']); - } - - return $bytesWritten; + return file_put_contents($file, json_encode($story, JSON_PRETTY_PRINT)); } - private static function ensureDirectoryExists() { - if (!file_exists(self::$storiesDir)) { - if (!mkdir(self::$storiesDir, 0755, true)) { - throw new Exception('Impossible de créer le dossier stories/'); - } - } - - if (!is_writable(self::$storiesDir)) { - throw new Exception('Le dossier stories/ n\'est pas accessible en écriture'); + public static function formatDate($date) { + if (strlen($date) > 10) { // Si la date contient aussi l'heure + $datetime = DateTime::createFromFormat('Y-m-d H:i:s', $date); + return $datetime ? $datetime->format('d/m/Y H:i') : ''; + } else { // Pour les anciennes dates sans heure + $datetime = DateTime::createFromFormat('Y-m-d', $date); + return $datetime ? $datetime->format('d/m/Y') : ''; } } } \ No newline at end of file