amélioration de l'affichage et des comportements de la partie administration

This commit is contained in:
Esenjin 2025-02-14 19:57:45 +01:00
parent c3f5e9afc0
commit 1d00a00146
10 changed files with 502 additions and 152 deletions

View File

@ -0,0 +1,39 @@
<?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);
exit(json_encode(['error' => '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()]);
}

43
admin/api/get-chapter.php Normal file
View File

@ -0,0 +1,43 @@
<?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);
exit(json_encode(['error' => '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()]);
}

View File

@ -0,0 +1,48 @@
<?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);
exit(json_encode(['error' => '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()]);
}

View File

@ -39,7 +39,10 @@ $stories = Stories::getAll();
<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>
<p>
<?= count($story['chapters'] ?? []) ?> chapitre<?= count($story['chapters'] ?? []) > 1 ? 's' : '' ?><br>
Dernière modification : <?= htmlspecialchars(Stories::formatDate($story['updated'])) ?>
</p>
</div>
<div class="story-actions">
<a href="story-edit.php?id=<?= htmlspecialchars($story['id']) ?>" class="button">Modifier</a>
@ -52,4 +55,4 @@ $stories = Stories::getAll();
<script src="../assets/js/admin.js"></script>
</body>
</html>
</html>

View File

@ -74,7 +74,7 @@ function generateSlug($title) {
</nav>
<main class="admin-main">
<h1><?= $story ? 'Modifier' : 'Nouveau' ?> roman</h1>
<h1><?= $story ? 'Modifier le' : 'Nouveau' ?> roman</h1>
<?php if ($error): ?>
<div class="error-message"><?= htmlspecialchars($error) ?></div>
@ -121,16 +121,13 @@ function generateSlug($title) {
<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']) ?>">
<div class="chapter-item" data-id="<?= htmlspecialchars($chapter['id']) ?>">
<span class="chapter-number"><?= $index + 1 ?></span>
<h3 class="chapter-title"><?= htmlspecialchars($chapter['title']) ?></h3>
<div class="chapter-actions">
<button type="button" class="button edit-chapter">Éditer</button>
<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>

View File

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

View File

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

View File

@ -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 {

View File

@ -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 = `
<span class="chapter-number">${document.querySelectorAll('.chapter-item').length + 1}</span>
<h3 class="chapter-title">${title}</h3>
<div class="chapter-actions">
<button type="button" class="button edit-chapter">Éditer</button>
<button type="button" class="button delete-chapter">Supprimer</button>
</div>
`;
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');
}
}
});
});

View File

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