Compare commits
3 Commits
303194437c
...
bd59582428
Author | SHA1 | Date | |
---|---|---|---|
bd59582428 | |||
e4f1880870 | |||
2584e75ba0 |
64
admin/api/delete-story.php
Normal file
64
admin/api/delete-story.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
require_once '../../includes/config.php';
|
||||
require_once '../../includes/auth.php';
|
||||
require_once '../../includes/stories.php';
|
||||
|
||||
// Vérification de l'authentification
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'error' => 'Non autorisé']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Vérification de la méthode HTTP
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupération et validation des données
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$storyId = $input['id'] ?? null;
|
||||
|
||||
if (!$storyId) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'ID du roman manquant']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Récupération du roman
|
||||
$story = Stories::get($storyId);
|
||||
if (!$story) {
|
||||
throw new Exception('Roman non trouvé');
|
||||
}
|
||||
|
||||
// Suppression de l'image de couverture si elle existe
|
||||
if (!empty($story['cover'])) {
|
||||
$coverPath = __DIR__ . '/../../' . $story['cover'];
|
||||
if (file_exists($coverPath)) {
|
||||
unlink($coverPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Suppression du fichier JSON du roman
|
||||
$storyFile = __DIR__ . '/../../stories/' . $storyId . '.json';
|
||||
if (file_exists($storyFile)) {
|
||||
unlink($storyFile);
|
||||
|
||||
// Réponse de succès
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Roman supprimé avec succès'
|
||||
]);
|
||||
} else {
|
||||
throw new Exception('Fichier du roman introuvable');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Erreur lors de la suppression : ' . $e->getMessage()
|
||||
]);
|
||||
}
|
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()]);
|
||||
}
|
@ -36,7 +36,7 @@ $stories = Stories::getAll();
|
||||
<div class="stories-list">
|
||||
<?php foreach ($stories as $story): ?>
|
||||
<div class="story-item">
|
||||
<img src="<?= htmlspecialchars($story['cover']) ?>" alt="" class="story-cover">
|
||||
<img src="<?= htmlspecialchars('../' . $story['cover']) ?>" alt="" class="story-cover">
|
||||
<div class="story-info">
|
||||
<h2><?= htmlspecialchars($story['title']) ?></h2>
|
||||
<p>Dernière modification : <?= htmlspecialchars($story['updated']) ?></p>
|
||||
|
157
admin/story-edit.php
Normal file
157
admin/story-edit.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
require_once '../includes/config.php';
|
||||
require_once '../includes/auth.php';
|
||||
require_once '../includes/stories.php';
|
||||
require_once 'upload-handler.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'] ?? '',
|
||||
'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_NO_FILE) {
|
||||
$uploadHandler = new CoverUploadHandler();
|
||||
$storyData['cover'] = $uploadHandler->handleUpload($_FILES['cover'], $storyData['id']);
|
||||
}
|
||||
|
||||
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>
|
92
admin/upload-handler.php
Normal file
92
admin/upload-handler.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
class CoverUploadHandler {
|
||||
private $uploadDir;
|
||||
private $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
|
||||
private $maxFileSize = 5242880; // 5MB
|
||||
|
||||
public function __construct() {
|
||||
$this->uploadDir = __DIR__ . '/../assets/images/covers/';
|
||||
$this->ensureUploadDirectory();
|
||||
}
|
||||
|
||||
public function handleUpload($file, $storyId) {
|
||||
try {
|
||||
// Vérifications basiques
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception($this->getUploadErrorMessage($file['error']));
|
||||
}
|
||||
|
||||
// Vérifier le 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');
|
||||
}
|
||||
|
||||
// Vérifier la taille
|
||||
if ($file['size'] > $this->maxFileSize) {
|
||||
throw new Exception('Fichier trop volumineux. Taille maximum : 5MB');
|
||||
}
|
||||
|
||||
// Générer le nom de fichier
|
||||
$extension = $this->getExtensionFromMimeType($mimeType);
|
||||
$filename = $storyId . '.' . $extension;
|
||||
$targetPath = $this->uploadDir . $filename;
|
||||
|
||||
// Supprimer l'ancienne image si elle existe
|
||||
$this->removeOldCover($storyId);
|
||||
|
||||
// Déplacer le 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/covers/' . $filename;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log('Erreur upload couverture: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
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($storyId) {
|
||||
foreach (glob($this->uploadDir . $storyId . '.*') as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
private function getExtensionFromMimeType($mimeType) {
|
||||
$map = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif'
|
||||
];
|
||||
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';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
145
assets/js/admin.js
Normal file
145
assets/js/admin.js
Normal file
@ -0,0 +1,145 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Gestion de la suppression des romans
|
||||
const storyList = document.querySelector('.stories-list');
|
||||
if (storyList) {
|
||||
storyList.addEventListener('click', async (e) => {
|
||||
if (e.target.matches('.delete-story')) {
|
||||
const storyId = e.target.dataset.id;
|
||||
if (confirmDeletion(storyId)) {
|
||||
await deleteStory(storyId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Confirmation de suppression avec informations du roman
|
||||
function confirmDeletion(storyId) {
|
||||
const storyCard = document.querySelector(`[data-id="${storyId}"]`).closest('.story-item');
|
||||
const title = storyCard.querySelector('h2').textContent;
|
||||
return confirm(`Voulez-vous vraiment supprimer le roman "${title}" ? Cette action est irréversible.`);
|
||||
}
|
||||
|
||||
// Suppression d'un roman via l'API
|
||||
async function deleteStory(storyId) {
|
||||
try {
|
||||
const response = await fetch('api/delete-story.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ id: storyId })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
// Animation de suppression et retrait du DOM
|
||||
const storyCard = document.querySelector(`[data-id="${storyId}"]`).closest('.story-item');
|
||||
storyCard.style.opacity = '0';
|
||||
storyCard.style.transform = 'translateX(-100%)';
|
||||
setTimeout(() => {
|
||||
storyCard.remove();
|
||||
showNotification('Roman supprimé avec succès');
|
||||
}, 300);
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur lors de la suppression');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Erreur serveur');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Système de notification
|
||||
function showNotification(message, type = 'success') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animation d'entrée
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '1';
|
||||
notification.style.transform = 'translateY(0)';
|
||||
}, 10);
|
||||
|
||||
// Auto-suppression après 3 secondes
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
notification.style.transform = 'translateY(-100%)';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Gestion du formulaire de déconnexion
|
||||
const logoutForm = document.querySelector('.logout-form');
|
||||
if (logoutForm) {
|
||||
logoutForm.addEventListener('submit', (e) => {
|
||||
if (!confirm('Voulez-vous vraiment vous déconnecter ?')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fonction utilitaire pour détecter les changements non sauvegardés
|
||||
let hasUnsavedChanges = false;
|
||||
|
||||
function detectUnsavedChanges() {
|
||||
const forms = document.querySelectorAll('form');
|
||||
forms.forEach(form => {
|
||||
const initialState = new FormData(form).toString();
|
||||
|
||||
form.addEventListener('change', () => {
|
||||
const currentState = new FormData(form).toString();
|
||||
hasUnsavedChanges = initialState !== currentState;
|
||||
});
|
||||
});
|
||||
|
||||
// Avertissement avant de quitter la page avec des changements non sauvegardés
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
if (hasUnsavedChanges) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Style des notifications
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0;
|
||||
transform: translateY(-100%);
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.story-item {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Initialisation des fonctionnalités
|
||||
detectUnsavedChanges();
|
||||
});
|
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');
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ 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);
|
||||
@ -12,6 +13,7 @@ class Stories {
|
||||
}
|
||||
|
||||
public static function get($id) {
|
||||
self::ensureDirectoryExists();
|
||||
$file = self::$storiesDir . $id . '.json';
|
||||
if (!file_exists($file)) {
|
||||
return null;
|
||||
@ -20,7 +22,40 @@ class Stories {
|
||||
}
|
||||
|
||||
public static function save($story) {
|
||||
self::ensureDirectoryExists();
|
||||
|
||||
// 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';
|
||||
return file_put_contents($file, json_encode($story, JSON_PRETTY_PRINT));
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user