ajout de l'édition des romans
This commit is contained in:
parent
303194437c
commit
2584e75ba0
49
admin/api/save-chapter.php
Normal file
49
admin/api/save-chapter.php
Normal 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
161
admin/story-edit.php
Normal 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>
|
@ -138,3 +138,269 @@ 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;
|
||||
}
|
144
assets/js/story-edit.js
Normal file
144
assets/js/story-edit.js
Normal 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');
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user