diff --git a/admin/api/save-chapter.php b/admin/api/save-chapter.php new file mode 100644 index 0000000..dd05f0e --- /dev/null +++ b/admin/api/save-chapter.php @@ -0,0 +1,49 @@ + uniqid(), + 'title' => $title, + 'content' => $content, + 'created' => date('Y-m-d'), + 'updated' => date('Y-m-d') + ]; + } + + 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/story-edit.php b/admin/story-edit.php new file mode 100644 index 0000000..df9f88d --- /dev/null +++ b/admin/story-edit.php @@ -0,0 +1,161 @@ + $_POST['id'] ?? generateSlug($_POST['title']), + 'title' => $_POST['title'], + 'description' => $_POST['description'], + 'cover' => $story['cover'] ?? '', // Géré séparément par l'upload + 'created' => $story['created'] ?? date('Y-m-d'), + 'updated' => date('Y-m-d'), + 'chapters' => $story['chapters'] ?? [] + ]; + + // Gestion de l'upload de couverture + if (isset($_FILES['cover']) && $_FILES['cover']['error'] === UPLOAD_ERR_OK) { + $uploadDir = '../assets/images/covers/'; + $extension = strtolower(pathinfo($_FILES['cover']['name'], PATHINFO_EXTENSION)); + $filename = $storyData['id'] . '.' . $extension; + + if (move_uploaded_file($_FILES['cover']['tmp_name'], $uploadDir . $filename)) { + $storyData['cover'] = 'assets/images/covers/' . $filename; + } + } + + Stories::save($storyData); + $success = 'Roman sauvegardé avec succès'; + $story = $storyData; + } catch (Exception $e) { + $error = 'Erreur lors de la sauvegarde : ' . $e->getMessage(); + } +} + +function generateSlug($title) { + $slug = strtolower($title); + $slug = preg_replace('/[^a-z0-9-]/', '-', $slug); + $slug = preg_replace('/-+/', '-', $slug); + return trim($slug, '-'); +} +?> + + + + + + <?= $story ? 'Modifier' : 'Nouveau' ?> roman - Administration + + + + + + +
+

roman

+ + +
+ + + +
+ + +
+ + + + +
+ + +
+ +
+ + +
+ +
+ + + Couverture actuelle + + +
+ + +
+ + +
+

Chapitres

+ + +
+ $chapter): ?> +
+
+ + + +
+
+
+
+
+ +
+
+ + + +
+ + + + + + \ No newline at end of file diff --git a/assets/css/admin.css b/assets/css/admin.css index e9b3ae9..88a5cbf 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -137,4 +137,270 @@ body { .delete-story:hover { background-color: #b71c1c; +} + +/* Formulaire d'édition du roman */ +.story-form { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 2rem; +} + +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + color: #2c1810; + font-weight: 500; +} + +.form-group input[type="text"], +.form-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.form-group textarea { + resize: vertical; + min-height: 100px; +} + +.current-cover { + display: block; + max-width: 200px; + margin: 0.5rem 0; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Section des chapitres */ +.chapters-section { + background: white; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.chapters-section h2 { + margin-top: 0; + color: #2c1810; +} + +.chapters-list { + margin-top: 1.5rem; +} + +.chapter-item { + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 1rem; + background: #f9f9f9; +} + +.chapter-header { + display: flex; + align-items: center; + padding: 1rem; + background: #fff; + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; +} + +.chapter-number { + width: 30px; + height: 30px; + background: #8b4513; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + cursor: move; + margin-right: 1rem; +} + +.chapter-title { + flex: 1; + padding: 0.5rem; + margin-right: 1rem; + border: 1px solid #ddd; + border-radius: 4px; +} + +.chapter-content { + padding: 1rem; + background: white; +} + +.chapter-content .editor { + min-height: 100px; + cursor: pointer; +} + +/* Modal d'édition */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal-content { + background: white; + padding: 2rem; + border-radius: 8px; + width: 90%; + max-width: 800px; + max-height: 90vh; + overflow-y: auto; +} + +.modal-content h2 { + margin-top: 0; + color: #2c1810; +} + +#chapterTitle { + width: 100%; + padding: 0.75rem; + margin-bottom: 1rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +/* Éditeur Quill personnalisé */ +.ql-container { + min-height: 300px; + font-size: 1rem; + margin-bottom: 1rem; +} + +.ql-toolbar { + border-radius: 4px 4px 0 0; + background: #f9f9f9; +} + +.ql-container { + border-radius: 0 0 4px 4px; +} + +/* Boutons et actions */ +.modal-actions { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.success-message { + background-color: #e8f5e9; + color: #2e7d32; + padding: 1rem; + border-radius: 4px; + margin-bottom: 1rem; +} + +/* Styles pour le drag & drop */ +.chapter-item.sortable-ghost { + opacity: 0.4; +} + +.chapter-item.sortable-drag { + cursor: move; + opacity: 0.9; +} + +/* Responsive */ +@media (max-width: 768px) { + .story-form, + .chapters-section { + padding: 1rem; + } + + .chapter-header { + flex-direction: column; + align-items: stretch; + } + + .chapter-number { + margin-bottom: 0.5rem; + } + + .chapter-title { + margin: 0.5rem 0; + } + + .modal-content { + width: 95%; + padding: 1rem; + } + + .ql-container { + min-height: 200px; + } +} + +/* Thème personnalisé pour l'éditeur */ +.ql-snow .ql-toolbar button:hover, +.ql-snow .ql-toolbar button.ql-active { + color: #8b4513; +} + +.ql-snow .ql-toolbar button:hover .ql-stroke, +.ql-snow .ql-toolbar button.ql-active .ql-stroke { + stroke: #8b4513; +} + +.ql-snow .ql-toolbar button:hover .ql-fill, +.ql-snow .ql-toolbar button.ql-active .ql-fill { + fill: #8b4513; +} + +/* Animation de transition */ +.chapter-item { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.chapter-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.button { + transition: background-color 0.2s ease; +} + +/* États de survol pour les éléments interactifs */ +.chapter-title:hover, +.chapter-title:focus { + border-color: #8b4513; + outline: none; +} + +.delete-chapter { + background-color: #dc3545; + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.delete-chapter:hover { + background-color: #c82333; } \ No newline at end of file diff --git a/assets/js/story-edit.js b/assets/js/story-edit.js new file mode 100644 index 0000000..f89f503 --- /dev/null +++ b/assets/js/story-edit.js @@ -0,0 +1,144 @@ +document.addEventListener('DOMContentLoaded', function() { + // Initialisation de l'éditeur Quill + const quill = new Quill('#editor', { + theme: 'snow', + modules: { + toolbar: [ + [{ 'header': [1, 2, 3, false] }], + ['bold', 'italic', 'underline'], + [{ 'list': 'ordered'}, { 'list': 'bullet' }], + ['link', 'blockquote'], + ['clean'] + ] + } + }); + + // Gestion du tri des chapitres + const chaptersList = document.getElementById('chaptersList'); + if (chaptersList) { + new Sortable(chaptersList, { + animation: 150, + handle: '.chapter-number', + onEnd: updateChaptersOrder + }); + } + + // 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; + + if (addChapterBtn) { + addChapterBtn.addEventListener('click', () => { + currentEditingChapter = null; + document.getElementById('chapterTitle').value = ''; + quill.setContents([]); + 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; + + try { + const response = await fetch('api/save-chapter.php', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + storyId, + chapterId: currentEditingChapter, + title, + content + }) + }); + + if (response.ok) { + window.location.reload(); + } else { + throw new Error('Erreur lors de la sauvegarde'); + } + } catch (error) { + alert('Erreur lors de la sauvegarde du chapitre'); + } + }); + } + + // Fermeture du modal + if (cancelEditBtn) { + cancelEditBtn.addEventListener('click', () => { + modal.style.display = 'none'; + }); + } + + // É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); + } + } 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'); + } + } 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 }) + }); + + if (response.ok) { + window.location.reload(); + } else { + throw new Error('Erreur lors de la suppression'); + } + } catch (error) { + alert('Erreur lors de la suppression du chapitre'); + } +} \ No newline at end of file