ajout de l'édition des romans

This commit is contained in:
Esenjin 2025-02-14 18:15:23 +01:00
parent 303194437c
commit 2584e75ba0
4 changed files with 620 additions and 0 deletions

View File

@ -0,0 +1,49 @@
<?php
require_once '../../includes/config.php';
require_once '../../includes/auth.php';
require_once '../../includes/stories.php';
if (!Auth::check()) {
http_response_code(401);
exit('Non autorisé');
}
$input = json_decode(file_get_contents('php://input'), true);
$storyId = $input['storyId'] ?? null;
$chapterId = $input['chapterId'] ?? null;
$title = $input['title'] ?? '';
$content = $input['content'] ?? '';
try {
$story = Stories::get($storyId);
if (!$story) {
throw new Exception('Roman non trouvé');
}
if ($chapterId) {
// Mise à jour d'un chapitre existant
foreach ($story['chapters'] as &$chapter) {
if ($chapter['id'] == $chapterId) {
$chapter['title'] = $title;
$chapter['content'] = $content;
$chapter['updated'] = date('Y-m-d');
break;
}
}
} else {
// Nouveau chapitre
$story['chapters'][] = [
'id' => 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()]);
}

161
admin/story-edit.php Normal file
View File

@ -0,0 +1,161 @@
<?php
require_once '../includes/config.php';
require_once '../includes/auth.php';
require_once '../includes/stories.php';
if (!Auth::check()) {
header('Location: login.php');
exit;
}
$story = null;
$error = '';
$success = '';
// Chargement du roman existant si ID fourni
if (isset($_GET['id'])) {
$story = Stories::get($_GET['id']);
if (!$story) {
header('Location: index.php');
exit;
}
}
// Traitement de la sauvegarde
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$storyData = [
'id' => $_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, '-');
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $story ? 'Modifier' : 'Nouveau' ?> roman - Administration</title>
<link rel="stylesheet" href="../assets/css/admin.css">
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
</head>
<body>
<nav class="admin-nav">
<div class="nav-brand">Administration</div>
<div class="nav-menu">
<a href="index.php" class="button">Retour</a>
</div>
</nav>
<main class="admin-main">
<h1><?= $story ? 'Modifier' : 'Nouveau' ?> roman</h1>
<?php if ($error): ?>
<div class="error-message"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="success-message"><?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<form method="POST" enctype="multipart/form-data" class="story-form">
<?php if ($story): ?>
<input type="hidden" name="id" value="<?= htmlspecialchars($story['id']) ?>">
<?php endif; ?>
<div class="form-group">
<label for="title">Titre</label>
<input type="text" id="title" name="title" required
value="<?= htmlspecialchars($story['title'] ?? '') ?>">
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" rows="4" required><?=
htmlspecialchars($story['description'] ?? '')
?></textarea>
</div>
<div class="form-group">
<label for="cover">Image de couverture</label>
<?php if (isset($story['cover'])): ?>
<img src="<?= htmlspecialchars('../' . $story['cover']) ?>"
alt="Couverture actuelle" class="current-cover">
<?php endif; ?>
<input type="file" id="cover" name="cover" accept="image/*">
</div>
<button type="submit" class="button">Enregistrer</button>
</form>
<?php if ($story): ?>
<section class="chapters-section">
<h2>Chapitres</h2>
<button type="button" id="addChapter" class="button">Ajouter un chapitre</button>
<div id="chaptersList" class="chapters-list">
<?php foreach ($story['chapters'] ?? [] as $index => $chapter): ?>
<div class="chapter-item" data-id="<?= $chapter['id'] ?>">
<div class="chapter-header">
<span class="chapter-number"><?= $index + 1 ?></span>
<input type="text" class="chapter-title"
value="<?= htmlspecialchars($chapter['title']) ?>">
<button type="button" class="button delete-chapter">Supprimer</button>
</div>
<div class="chapter-content">
<div class="editor"><?= $chapter['content'] ?></div>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
<div id="chapterEditor" class="modal" style="display: none;">
<div class="modal-content">
<h2>Éditer le chapitre</h2>
<input type="text" id="chapterTitle" placeholder="Titre du chapitre">
<div id="editor"></div>
<div class="modal-actions">
<button type="button" class="button" id="saveChapter">Enregistrer</button>
<button type="button" class="button" id="cancelEdit">Annuler</button>
</div>
</div>
</div>
<?php endif; ?>
</main>
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="../assets/js/story-edit.js"></script>
</body>
</html>

View File

@ -138,3 +138,269 @@ body {
.delete-story:hover { .delete-story:hover {
background-color: #b71c1c; 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;
}

144
assets/js/story-edit.js Normal file
View File

@ -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');
}
}