ajout de la possibilité d'ajouter des couvertures aux chapitres
This commit is contained in:
parent
2a6ae05837
commit
3660e796ee
@ -41,6 +41,11 @@ try {
|
||||
$chapter['title'] = '';
|
||||
}
|
||||
|
||||
// S'assurer que le champ cover existe
|
||||
if (!isset($chapter['cover'])) {
|
||||
$chapter['cover'] = null;
|
||||
}
|
||||
|
||||
// Conversion du contenu HTML en format Delta de Quill si nécessaire
|
||||
if (!isset($chapter['content'])) {
|
||||
$chapter['content'] = '{"ops":[{"insert":"\n"}]}';
|
||||
|
171
admin/api/update-chapter-cover.php
Normal file
171
admin/api/update-chapter-cover.php
Normal file
@ -0,0 +1,171 @@
|
||||
<?php
|
||||
require_once '../../includes/config.php';
|
||||
require_once '../../includes/auth.php';
|
||||
require_once '../../includes/stories.php';
|
||||
|
||||
class ChapterCoverHandler {
|
||||
private $allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
private $maxFileSize = 5242880; // 5MB
|
||||
private $uploadDir;
|
||||
|
||||
public function __construct($storyId) {
|
||||
$this->uploadDir = __DIR__ . '/../../assets/images/chapters/' . $storyId . '/covers/';
|
||||
$this->ensureUploadDirectory();
|
||||
}
|
||||
|
||||
public function handleUpload($file, $chapterId) {
|
||||
try {
|
||||
// Vérifications de base
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
throw new Exception($this->getUploadErrorMessage($file['error']));
|
||||
}
|
||||
|
||||
// Vérification du 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, WEBP');
|
||||
}
|
||||
|
||||
// Vérification de la taille
|
||||
if ($file['size'] > $this->maxFileSize) {
|
||||
throw new Exception('Fichier trop volumineux. Taille maximum : 5MB');
|
||||
}
|
||||
|
||||
// Génération du nom de fichier
|
||||
$extension = $this->getExtensionFromMimeType($mimeType);
|
||||
$filename = $chapterId . '-cover.' . $extension;
|
||||
$targetPath = $this->uploadDir . $filename;
|
||||
|
||||
// Suppression de l'ancienne image si elle existe
|
||||
$this->removeOldCover($chapterId);
|
||||
|
||||
// Déplacement du 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/chapters/' . basename(dirname($this->uploadDir)) . '/covers/' . $filename;
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeCover($chapterId) {
|
||||
foreach (glob($this->uploadDir . $chapterId . '-cover.*') as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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($chapterId) {
|
||||
foreach (glob($this->uploadDir . $chapterId . '-cover.*') as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
|
||||
private function getExtensionFromMimeType($mimeType) {
|
||||
$map = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif',
|
||||
'image/webp' => 'webp'
|
||||
];
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// Vérification de l'authentification
|
||||
if (!Auth::check()) {
|
||||
http_response_code(401);
|
||||
exit(json_encode(['success' => false, 'error' => 'Non autorisé']));
|
||||
}
|
||||
|
||||
// Traitement de la requête
|
||||
try {
|
||||
// Récupérer les données selon la méthode
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if ($input) {
|
||||
// Cas d'une requête JSON (suppression)
|
||||
$storyId = $input['storyId'] ?? null;
|
||||
$chapterId = $input['chapterId'] ?? null;
|
||||
$isDelete = $input['delete'] ?? false;
|
||||
} else {
|
||||
// Cas d'un upload de fichier
|
||||
$storyId = $_POST['storyId'] ?? null;
|
||||
$chapterId = $_POST['chapterId'] ?? null;
|
||||
$isDelete = false;
|
||||
}
|
||||
|
||||
if (!$storyId || !$chapterId) {
|
||||
throw new Exception('Paramètres manquants');
|
||||
}
|
||||
|
||||
// Récupération du roman
|
||||
$story = Stories::get($storyId);
|
||||
if (!$story) {
|
||||
throw new Exception('Roman non trouvé');
|
||||
}
|
||||
|
||||
// Trouver le chapitre concerné
|
||||
$chapterFound = false;
|
||||
foreach ($story['chapters'] as &$chapter) {
|
||||
if ($chapter['id'] === $chapterId) {
|
||||
$chapterFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$chapterFound) {
|
||||
throw new Exception('Chapitre non trouvé');
|
||||
}
|
||||
|
||||
$handler = new ChapterCoverHandler($storyId);
|
||||
|
||||
// Traitement selon le type de requête
|
||||
if ($isDelete) {
|
||||
$handler->removeCover($chapterId);
|
||||
$chapter['cover'] = null;
|
||||
} else if (isset($_FILES['cover'])) {
|
||||
$chapter['cover'] = $handler->handleUpload($_FILES['cover'], $chapterId);
|
||||
}
|
||||
|
||||
// Sauvegarde des modifications
|
||||
Stories::save($story);
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
@ -136,6 +136,7 @@ function generateSlug($title) {
|
||||
<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 edit-cover">Couverture</button>
|
||||
<button type="button" class="button delete-chapter">Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -161,6 +162,31 @@ function generateSlug($title) {
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Modale pour la couverture de chapitre -->
|
||||
<div id="chapterCoverEditor" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Couverture du chapitre</h2>
|
||||
<p class="chapter-title-display"></p>
|
||||
</div>
|
||||
|
||||
<div class="cover-preview-container">
|
||||
<div class="current-cover-preview"></div>
|
||||
<div class="form-group">
|
||||
<label for="chapterCover">Nouvelle image de couverture</label>
|
||||
<input type="file" id="chapterCover" accept="image/*">
|
||||
<small>Formats acceptés : JPG, PNG, GIF. Taille maximum : 5MB</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="cancelCoverEdit" class="button dark">Annuler</button>
|
||||
<button type="button" id="deleteCover" class="button delete-story" style="display: none;">Supprimer</button>
|
||||
<button type="button" id="saveCover" class="button">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
|
||||
|
@ -306,6 +306,28 @@
|
||||
animation: modalFadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.cover-preview-container {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.current-cover-preview {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.current-cover-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 300px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.chapter-title-display {
|
||||
color: var(--text-secondary);
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Media queries */
|
||||
@media (max-width: 768px) {
|
||||
.story-cover {
|
||||
|
@ -127,6 +127,154 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const chapterTitleInput = document.getElementById('chapterTitle');
|
||||
const chaptersList = document.getElementById('chaptersList');
|
||||
|
||||
// Éléments de la modale de couverture
|
||||
const coverModal = document.getElementById('chapterCoverEditor');
|
||||
const coverPreview = document.querySelector('.current-cover-preview');
|
||||
const chapterCoverInput = document.getElementById('chapterCover');
|
||||
const saveCoverBtn = document.getElementById('saveCover');
|
||||
const cancelCoverBtn = document.getElementById('cancelCoverEdit');
|
||||
const deleteCoverBtn = document.getElementById('deleteCover');
|
||||
let currentChapterCover = null;
|
||||
|
||||
// Gestionnaire pour le bouton de couverture
|
||||
if (chaptersList) {
|
||||
chaptersList.addEventListener('click', async (e) => {
|
||||
if (e.target.matches('.edit-cover')) {
|
||||
const chapterItem = e.target.closest('.chapter-item');
|
||||
const chapterId = chapterItem.dataset.id;
|
||||
const chapterTitle = chapterItem.querySelector('.chapter-title').textContent;
|
||||
|
||||
// Mise à jour du titre dans la modale
|
||||
coverModal.querySelector('.chapter-title-display').textContent = chapterTitle;
|
||||
currentChapterCover = chapterId;
|
||||
|
||||
try {
|
||||
const response = await fetch(`api/get-chapter.php?storyId=${storyId}&chapterId=${chapterId}`);
|
||||
if (!response.ok) throw new Error('Erreur réseau');
|
||||
|
||||
const chapter = await response.json();
|
||||
|
||||
// Afficher l'image existante si elle existe
|
||||
if (chapter.cover) {
|
||||
coverPreview.innerHTML = `<img src="../${chapter.cover}" alt="Couverture actuelle">`;
|
||||
deleteCoverBtn.style.display = 'block';
|
||||
} else {
|
||||
coverPreview.innerHTML = '';
|
||||
deleteCoverBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
coverModal.style.display = 'block';
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
showNotification('Erreur lors du chargement de la couverture', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Prévisualisation de l'image
|
||||
if (chapterCoverInput) {
|
||||
chapterCoverInput.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
coverPreview.innerHTML = `<img src="${e.target.result}" alt="Prévisualisation">`;
|
||||
deleteCoverBtn.style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fermeture de la modale de couverture
|
||||
if (cancelCoverBtn) {
|
||||
cancelCoverBtn.addEventListener('click', () => {
|
||||
coverModal.style.display = 'none';
|
||||
chapterCoverInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Sauvegarde de la couverture
|
||||
if (saveCoverBtn) {
|
||||
saveCoverBtn.addEventListener('click', async () => {
|
||||
const file = chapterCoverInput.files[0];
|
||||
if (!file && !coverPreview.querySelector('img')) {
|
||||
showNotification('Veuillez sélectionner une image', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('storyId', storyId);
|
||||
formData.append('chapterId', currentChapterCover);
|
||||
if (file) {
|
||||
formData.append('cover', file);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('api/update-chapter-cover.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Erreur lors de l\'upload');
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification('Couverture mise à jour avec succès');
|
||||
coverModal.style.display = 'none';
|
||||
chapterCoverInput.value = '';
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur lors de la mise à jour');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
showNotification(error.message, 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Suppression de la couverture
|
||||
if (deleteCoverBtn) {
|
||||
deleteCoverBtn.addEventListener('click', () => {
|
||||
confirmDialog.show({
|
||||
title: 'Supprimer la couverture',
|
||||
message: 'Voulez-vous vraiment supprimer la couverture de ce chapitre ?',
|
||||
confirmText: 'Supprimer',
|
||||
confirmClass: 'danger',
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const response = await fetch('api/update-chapter-cover.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
storyId: storyId,
|
||||
chapterId: currentChapterCover,
|
||||
delete: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Erreur réseau');
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
showNotification('Couverture supprimée avec succès');
|
||||
coverPreview.innerHTML = '';
|
||||
coverModal.style.display = 'none';
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur lors de la suppression');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
showNotification(error.message, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Configuration de Sortable pour la réorganisation des chapitres
|
||||
if (chaptersList) {
|
||||
new Sortable(chaptersList, {
|
||||
|
@ -57,8 +57,11 @@ $config = Config::load();
|
||||
<meta name="description" content="<?= htmlspecialchars($story['title']) ?> - <?= htmlspecialchars($currentChapter['title']) ?>">
|
||||
</head>
|
||||
<body>
|
||||
<!-- En-tête simple -->
|
||||
<header class="novel-header" style="height: 180px;">
|
||||
<!-- En-tête avec image de couverture si disponible -->
|
||||
<header class="novel-header">
|
||||
<?php if (!empty($currentChapter['cover'])): ?>
|
||||
<div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($currentChapter['cover']) ?>');"></div>
|
||||
<?php endif; ?>
|
||||
<h1><?= htmlspecialchars($currentChapter['title']) ?></h1>
|
||||
</header>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user