Compare commits

..

No commits in common. "main" and "v.1.3.0" have entirely different histories.

22 changed files with 289 additions and 1310 deletions

186
README.md

@ -1,59 +1,179 @@
# Lectures d'Esenjin - Plateforme de Publication de Romans # Plateforme de Publication de Romans en Ligne
Une plateforme web élégante pour la publication et la lecture de romans, développée en PHP avec stockage JSON. Une plateforme web simple et élégante pour la publication et la lecture de romans en ligne, développée en PHP avec stockage JSON.
![Aperçu du site](https://concepts.esenjin.xyz/cyla/fichiers/67bf650a481c8_1740596490.gif) ## 🚀 Fonctionnalités
## Fonctionnalités principales
### Zone Administrative ### Zone Administrative
- Interface sécurisée pour la gestion des contenus - Interface sécurisée pour la gestion des contenus
- Création et édition de romans avec éditeur WYSIWYG (Quill.js) - Création et édition de romans avec éditeur WYSIWYG
- Gestion des chapitres avec réorganisation par glisser-déposer - Gestion flexible des chapitres avec réorganisation par glisser-déposer
- Mode brouillon pour les chapitres en cours de rédaction - Prévisualisation avant publication
- Système d'upload et de gestion d'images - Stockage JSON pour une maintenance simplifiée
- Gestion des accès utilisateurs (administrateurs, éditeurs) - Système d'upload d'images avec redimensionnement automatique
- Import/Export des romans au format ZIP - Gestion des métadonnées (date de création, mise à jour, etc.)
- Personnalisation des options du site (logo, bannière, informations) - Import/Export des romans
- Gestion du profil et des options du site
![image](https://concepts.esenjin.xyz/cyla/fichiers/67b1db9462f60_1739709332.png)
![image](https://concepts.esenjin.xyz/cyla/fichiers/67b1db949a6d2_1739709332.png)
![image](https://concepts.esenjin.xyz/cyla/fichiers/67b1db947d0d0_1739709332.png)
### Zone Publique ### Zone Publique
- Interface de lecture épurée aux tons bruns/ocre - Interface de lecture épurée et confortable
- Navigation intuitive entre les chapitres - Navigation intuitive entre les chapitres
- Design responsive optimisé pour tous les appareils - Design responsive optimisé pour la lecture
- Section "À propos" personnalisable avec statistiques automatiques - Thème personnalisé aux tons bruns/ocre
- Indicateurs pour nouveaux chapitres et contenus en cours de rédaction - Affichage adaptatif des images et du contenu
## Prérequis techniques ![image](https://concepts.esenjin.xyz/cyla/fichiers/67b1db949863b_1739709332.png)
![image](https://concepts.esenjin.xyz/cyla/fichiers/67b1db9569d54_1739709333.png)
## 📋 Prérequis
- PHP 8.0 ou supérieur - PHP 8.0 ou supérieur
- Extensions PHP : GD pour le traitement des images - Serveur web (Apache/Nginx) avec mod_rewrite activé
- Permissions d'écriture sur les dossiers `stories/` et `assets/images/` - Extensions PHP : GD ou Imagick pour le traitement des images
- Permissions d'écriture sur les dossiers `stories/`, `assets/images/` et `admin/`
## Installation rapide ## 🛠️ Installation
1. Clonez le dépôt 1. Clonez le dépôt :
2. Copiez et modifiez `config.json` avec vos paramètres ```bash
3. Définissez les permissions appropriées sur les dossiers git clone https://github.com/votre-username/nom-du-projet.git
4. Accédez à `/admin` pour commencer à gérer vos romans cd nom-du-projet
```
## Structure du projet 2. Configurez votre serveur web pour pointer vers le dossier racine du projet
3. Copiez et modifiez le fichier de configuration :
```bash
cp config.example.json config.json
```
4. Modifiez `config.json` avec vos paramètres :
```json
{
"site": {
"name": "Votre Site",
"url": "https://votre-domaine.com",
"description": "Description de votre site",
"logo": "assets/images/logo.png"
},
"users": [
{
"id": "admin",
"password": "votre_mot_de_passe_hashé"
}
]
}
```
5. Définissez les permissions appropriées :
```bash
chmod 755 .
chmod 644 config.json
chmod -R 755 stories/
chmod -R 755 assets/images/
```
## 📁 Structure du Projet
``` ```
/ /
├── admin/ # Zone administrative ├── admin/ # Zone administrative sécurisée
├── assets/ # Ressources statiques (CSS, JS, images) │ ├── api/ # Endpoints API pour les opérations CRUD
│ ├── index.php # Dashboard administratif
│ └── login.php # Page de connexion
├── assets/ # Ressources statiques
│ ├── css/ # Styles CSS
│ ├── js/ # Scripts JavaScript
│ └── images/ # Images uploadées
│ ├── covers/ # Couvertures des romans
│ └── chapters/ # Images des chapitres
├── includes/ # Fichiers PHP réutilisables ├── includes/ # Fichiers PHP réutilisables
├── stories/ # Romans au format JSON ├── stories/ # Dossier contenant les récits (JSON)
├── config.json # Configuration du site ├── config.json # Configuration du site
└── index.php # Page d'accueil └── index.php # Page d'accueil publique
``` ```
## Sécurité ## 🔒 Sécurité
- Authentification sécurisée avec hashage des mots de passe - Authentification sécurisée avec hashage des mots de passe
- Protection contre les injections et les attaques XSS - Protection contre les attaques XSS et CSRF
- Validation des données et restrictions sur les uploads - Validation des données entrantes
- Sanitization des sorties HTML
- Restrictions sur les types de fichiers uploadés
- Redimensionnement automatique des images
- Sessions sécurisées avec paramètres renforcés
## License ## 🌐 Utilisation
### Interface Administrative
1. Accédez à `/admin` et connectez-vous
2. Utilisez le menu pour gérer vos romans et chapitres
3. L'éditeur WYSIWYG permet une mise en forme riche du texte
4. Uploadez des images directement dans l'éditeur
5. Réorganisez les chapitres par glisser-déposer
6. Prévisualisez vos modifications avant publication
### Interface Publique
- La page d'accueil liste tous les romans disponibles
- Chaque roman a sa page dédiée avec description et chapitres
- Navigation fluide entre les chapitres
- Interface adaptative pour une lecture confortable sur tous les appareils
- Optimisation des images selon la taille d'écran
## 💾 Structure des Données
### Configuration (config.json)
```json
{
"site": {
"name": "Nom du Site",
"description": "Description du site",
"logo": "path/to/logo.png"
},
"users": [
{
"id": "admin",
"password": "hashed_password"
}
]
}
```
### Romans (stories/roman-id.json)
```json
{
"id": "roman-id",
"title": "Titre du Roman",
"description": "Description complète",
"cover": "assets/images/covers/cover.jpg",
"created": "2025-02-14",
"updated": "2025-02-14",
"chapters": [
{
"id": "chapitre-1",
"title": "Titre du Chapitre",
"content": "Contenu au format Delta/HTML",
"created": "2025-02-14",
"updated": "2025-02-14"
}
]
}
```
## 🤝 Contribution
Les contributions sont les bienvenues ! Pour contribuer :
1. Forkez le projet
2. Créez une branche pour votre fonctionnalité (`git checkout -b feature/AmazingFeature`)
3. Committez vos changements (`git commit -m 'Add some AmazingFeature'`)
4. Poussez vers la branche (`git push origin feature/AmazingFeature`)
5. Ouvrez une Pull Request
## 📝 License
Ce projet est sous licence MIT - voir le fichier [LICENSE.md](LICENSE.md) pour plus de détails. Ce projet est sous licence MIT - voir le fichier [LICENSE.md](LICENSE.md) pour plus de détails.

@ -34,11 +34,6 @@ $siteStats = $stats->getStats();
<div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($about['background']) ?>');"></div> <div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($about['background']) ?>');"></div>
<?php endif; ?> <?php endif; ?>
<h1><?= htmlspecialchars($about['title']) ?></h1> <h1><?= htmlspecialchars($about['title']) ?></h1>
<div class="header-actions">
<a href="index.php" class="about-button">
<i class="fas fa-home"></i> Accueil
</a>
</div>
</header> </header>
<!-- Contenu principal --> <!-- Contenu principal -->
@ -136,9 +131,11 @@ $siteStats = $stats->getStats();
</aside> </aside>
</div> </div>
<button class="scroll-top" aria-label="Retour en haut de page"> <div class="back-to-home">
<i class="fas fa-arrow-up"></i> <a href="index.php">&larr; Retour à l'accueil</a>
</button> </div>
<button class="scroll-top" aria-label="Retour en haut de page"></button>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

@ -29,19 +29,10 @@ try {
$chapterUpdated = false; $chapterUpdated = false;
foreach ($story['chapters'] as &$chapter) { foreach ($story['chapters'] as &$chapter) {
if ($chapter['id'] === $chapterId) { if ($chapter['id'] === $chapterId) {
$wasInDraft = $chapter['draft'] ?? false;
$chapter['title'] = $title; $chapter['title'] = $title;
$chapter['content'] = $content; $chapter['content'] = $content;
$chapter['draft'] = $draft; $chapter['draft'] = $draft;
$chapter['updated'] = date('Y-m-d'); $chapter['updated'] = date('Y-m-d');
// Si le chapitre passe de brouillon à publié, ajouter la date de publication
if ($wasInDraft && !$draft) {
$chapter['published'] = date('Y-m-d H:i:s');
// Marquer le roman comme ayant du nouveau contenu
$story['publicUpdated'] = date('Y-m-d H:i:s');
}
$chapterUpdated = true; $chapterUpdated = true;
break; break;
} }

@ -49,9 +49,7 @@ $stories = Stories::getAll();
<span>Administration</span> <span>Administration</span>
</div> </div>
<div class="nav-menu"> <div class="nav-menu">
<a href="index.php" class="button tooltip" data-tooltip="Retour"> <a href="index.php" class="button">Retour</a>
<i class="fas fa-arrow-left"></i>
</a>
</div> </div>
</nav> </nav>
@ -82,14 +80,8 @@ $stories = Stories::getAll();
</div> </div>
<div class="form-actions"> <div class="form-actions">
<button type="button" class="button tooltip select-all" data-tooltip="Tout sélectionner"> <button type="button" class="button select-all">Tout sélectionner</button>
<i class="fas fa-check-square"></i> <button type="submit" class="button">Exporter la sélection</button>
<span class="tooltip-text">Tout sélectionner</span>
</button>
<button type="submit" class="button tooltip" data-tooltip="Exporter">
<i class="fas fa-upload"></i>
<span class="tooltip-text">Exporter</span>
</button>
</div> </div>
</form> </form>
</section> </section>
@ -110,10 +102,7 @@ $stories = Stories::getAll();
</label> </label>
</div> </div>
<button type="submit" class="button tooltip" data-tooltip="Importer"> <button type="submit" class="button">Importer</button>
<i class="fas fa-download"></i>
<span class="tooltip-text">Importer</span>
</button>
</form> </form>
</section> </section>
</main> </main>
@ -125,17 +114,10 @@ $stories = Stories::getAll();
const checkboxes = document.querySelectorAll('input[name="stories[]"]'); const checkboxes = document.querySelectorAll('input[name="stories[]"]');
selectAllBtn.addEventListener('click', function() { selectAllBtn.addEventListener('click', function() {
const allChecked = Array.from(checkboxes).every(cb => cb.checked); const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked); checkboxes.forEach(cb => cb.checked = !allChecked);
this.textContent = allChecked ? 'Tout sélectionner' : 'Tout désélectionner';
// Mise à jour du texte de la tooltip et non du contenu du bouton });
const tooltipText = this.querySelector('.tooltip-text');
if (tooltipText) {
tooltipText.textContent = allChecked ? 'Tout sélectionner' : 'Tout désélectionner';
}
// Mise à jour de l'attribut data-tooltip pour que le survol affiche le bon texte
this.setAttribute('data-tooltip', allChecked ? 'Tout sélectionner' : 'Tout désélectionner');
});
// Validation du formulaire d'export // Validation du formulaire d'export
document.getElementById('exportForm').addEventListener('submit', function(e) { document.getElementById('exportForm').addEventListener('submit', function(e) {

@ -58,31 +58,18 @@ $users = Auth::getAllUsers(false);
<?php endif; ?> <?php endif; ?>
<span>Administration</span> <span>Administration</span>
</div> </div>
<!-- Le bouton hamburger sera inséré par JS -->
<div class="nav-menu"> <div class="nav-menu">
<a href="../index.php" target="_blank" class="button tooltip" data-tooltip="Visiter le site"> <a href="../index.php" target="_blank" class="button">Visiter le site</a>
<i class="fa-solid fa-house"></i> <a href="profile.php" class="button">Profil</a>
</a>
<a href="profile.php" class="button tooltip" data-tooltip="Profil">
<i class="fas fa-user"></i>
</a>
<?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?> <?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?>
<a href="users.php" class="button tooltip" data-tooltip="Utilisateurs"> <a href="users.php" class="button">Utilisateurs</a>
<i class="fas fa-users"></i>
</a>
<?php endif; ?> <?php endif; ?>
<a href="story-edit.php" class="button tooltip" data-tooltip="Nouveau roman"> <a href="story-edit.php" class="button">Nouveau roman</a>
<i class="fa-solid fa-book"></i> <a href="options.php" class="button">Options</a>
</a> <a href="export-import.php" class="button">Import/Export</a>
<a href="options.php" class="button tooltip" data-tooltip="Options">
<i class="fas fa-cog"></i>
</a>
<a href="export-import.php" class="button tooltip" data-tooltip="Import/Export">
<i class="fa-solid fa-upload"></i>
</a>
<form method="POST" action="logout.php" class="logout-form"> <form method="POST" action="logout.php" class="logout-form">
<button type="submit" class="tooltip" data-tooltip="Déconnexion"> <button type="submit">Déconnexion</button>
<i class="fas fa-sign-out-alt"></i>
</button>
</form> </form>
</div> </div>
</nav> </nav>
@ -105,17 +92,11 @@ $users = Auth::getAllUsers(false);
</p> </p>
</div> </div>
<div class="story-actions"> <div class="story-actions">
<a href="story-edit.php?id=<?= htmlspecialchars($story['id']) ?>" class="button tooltip" data-tooltip="Modifier"> <a href="story-edit.php?id=<?= htmlspecialchars($story['id']) ?>" class="button">Modifier</a>
<i class="fas fa-edit"></i>
</a>
<?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?> <?php if (Auth::isAdmin() || Auth::hasAdminRole()): ?>
<button type="button" class="button tooltip manage-access" data-tooltip="Accès" data-id="<?= htmlspecialchars($story['id']) ?>"> <button type="button" class="button manage-access" data-id="<?= htmlspecialchars($story['id']) ?>">Accès</button>
<i class="fas fa-users-cog"></i>
</button>
<?php endif; ?> <?php endif; ?>
<button type="button" class="button tooltip delete-story" data-tooltip="Supprimer" data-id="<?= htmlspecialchars($story['id']) ?>"> <button type="button" class="button delete-story" data-id="<?= htmlspecialchars($story['id']) ?>">Supprimer</button>
<i class="fas fa-trash-alt"></i>
</button>
</div> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
@ -150,14 +131,8 @@ $users = Auth::getAllUsers(false);
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="button dark" id="cancelAccess"> <button type="button" class="button dark" id="cancelAccess">Annuler</button>
<i class="fas fa-times"></i> <button type="button" class="button" id="saveAccess">Enregistrer</button>
<span class="button-text">Annuler</span>
</button>
<button type="button" class="button" id="saveAccess">
<i class="fas fa-save"></i>
<span class="button-text">Enregistrer</span>
</button>
</div> </div>
</div> </div>
</div> </div>

@ -90,9 +90,7 @@ $config = Config::load();
<span>Administration</span> <span>Administration</span>
</div> </div>
<div class="nav-menu"> <div class="nav-menu">
<a href="index.php" class="button tooltip" data-tooltip="Retour"> <a href="index.php" class="button">Retour</a>
<i class="fas fa-arrow-left"></i>
</a>
</div> </div>
</nav> </nav>
@ -206,30 +204,20 @@ $config = Config::load();
Ouvrir dans un nouvel onglet Ouvrir dans un nouvel onglet
</label> </label>
</div> </div>
<button type="button" class="button dark tooltip remove-link" data-tooltip="Supprimer ce lien"> <button type="button" class="button delete-story">Supprimer ce lien</button>
<i class="fas fa-trash-alt"></i>
</button>
</div> </div>
<?php <?php
} }
} }
?> ?>
</div> </div>
<button type="button" id="addLink" class="button tooltip" data-tooltip="Ajouter un lien"> <button type="button" id="addLink" class="button">Ajouter un lien</button>
<i class="fas fa-plus"></i>
</button>
<br /> <br />
<button type="submit" class="button tooltip submit-button" data-tooltip="Enregistrer"> <button type="submit" class="button submit-button">Enregistrer les modifications</button>
<i class="fas fa-save"></i>
<span class="tooltip-text">Enregistrer les modifications</span>
</button>
<!-- Section Nettoyage des médias --> <!-- Section Nettoyage des médias -->
<h2>Maintenance</h2> <h2>Maintenance</h2>
<div class="maintenance-actions"> <div class="maintenance-actions">
<button type="button" id="cleanMedia" class="button tooltip" data-tooltip="Nettoyer"> <button type="button" id="cleanMedia" class="button">Nettoyer les médias inutilisés</button>
<i class="fas fa-broom"></i>
<span class="tooltip-text">Nettoyer les médias inutilisés</span>
</button>
<small>Supprime les images qui ne sont plus utilisées dans les romans et chapitres.</small> <small>Supprime les images qui ne sont plus utilisées dans les romans et chapitres.</small>
</div> </div>
</form> </form>

@ -96,9 +96,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<span>Administration</span> <span>Administration</span>
</div> </div>
<div class="nav-menu"> <div class="nav-menu">
<a href="index.php" class="button tooltip" data-tooltip="Retour"> <a href="index.php" class="button">Retour</a>
<i class="fas fa-arrow-left"></i>
</a>
</div> </div>
</nav> </nav>
@ -137,10 +135,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<input type="password" id="confirm_password" name="confirm_password"> <input type="password" id="confirm_password" name="confirm_password">
</div> </div>
<button type="submit" class="button tooltip" data-tooltip="Enregistrer"> <button type="submit" class="button">Enregistrer les modifications</button>
<i class="fas fa-save"></i>
<span class="tooltip-text">Enregistrer</span>
</button>
</form> </form>
</main> </main>
<link rel="stylesheet" href="../assets/css/dialog.css"> <link rel="stylesheet" href="../assets/css/dialog.css">

@ -102,9 +102,7 @@ function generateSlug($title) {
<span>Administration</span> <span>Administration</span>
</div> </div>
<div class="nav-menu"> <div class="nav-menu">
<a href="index.php" class="button tooltip" data-tooltip="Retour"> <a href="index.php" class="button">Retour</a>
<i class="fas fa-arrow-left"></i>
</a>
</div> </div>
</nav> </nav>
@ -145,19 +143,13 @@ function generateSlug($title) {
<input type="file" id="cover" name="cover" accept="image/*"> <input type="file" id="cover" name="cover" accept="image/*">
</div> </div>
<button type="submit" class="button tooltip" data-tooltip="Enregistrer"> <button type="submit" class="button">Enregistrer</button>
<i class="fas fa-save"></i>
<span class="tooltip-text">Enregistrer</span>
</button>
</form> </form>
<?php if ($story): ?> <?php if ($story): ?>
<section class="chapters-section"> <section class="chapters-section">
<h2>Chapitres</h2> <h2>Chapitres</h2>
<button type="button" id="addChapter" class="button"> <button type="button" id="addChapter" class="button">Ajouter un chapitre</button>
<i class="fas fa-plus"></i>
<span class="button-text">Ajouter un chapitre</span>
</button>
<div id="chaptersList" class="chapters-list"> <div id="chaptersList" class="chapters-list">
<?php foreach ($story['chapters'] ?? [] as $index => $chapter): ?> <?php foreach ($story['chapters'] ?? [] as $index => $chapter): ?>
@ -165,18 +157,9 @@ function generateSlug($title) {
<span class="chapter-number"><?= $index + 1 ?></span> <span class="chapter-number"><?= $index + 1 ?></span>
<h3 class="chapter-title"><?= htmlspecialchars($chapter['title']) ?></h3> <h3 class="chapter-title"><?= htmlspecialchars($chapter['title']) ?></h3>
<div class="chapter-actions"> <div class="chapter-actions">
<button type="button" class="edit-chapter"> <button type="button" class="button edit-chapter">Éditer</button>
<i class="fas fa-edit"></i> <button type="button" class="button edit-cover">Couverture</button>
<span class="button-text">Éditer</span> <button type="button" class="button delete-chapter">Supprimer</button>
</button>
<button type="button" class="edit-cover">
<i class="fas fa-image"></i>
<span class="button-text">Couverture</span>
</button>
<button type="button" class="delete-chapter">
<i class="fas fa-trash-alt"></i>
<span class="button-text">Supprimer</span>
</button>
</div> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
@ -199,14 +182,8 @@ function generateSlug($title) {
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="cancelEdit" class="button dark"> <button type="button" id="cancelEdit" class="button dark">Annuler</button>
<i class="fas fa-times"></i> <button type="button" id="saveChapter" class="button">Enregistrer</button>
<span class="button-text">Annuler</span>
</button>
<button type="button" id="saveChapter" class="button">
<i class="fas fa-save"></i>
<span class="button-text">Enregistrer</span>
</button>
</div> </div>
</div> </div>
</div> </div>
@ -230,18 +207,9 @@ function generateSlug($title) {
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="cancelCoverEdit" class="button dark"> <button type="button" id="cancelCoverEdit" class="button dark">Annuler</button>
<i class="fas fa-times"></i> <button type="button" id="deleteCover" class="button delete-story" style="display: none;">Supprimer</button>
<span class="button-text">Annuler</span> <button type="button" id="saveCover" class="button">Enregistrer</button>
</button>
<button type="button" id="deleteCover" class="button delete-story" style="display: none;">
<i class="fas fa-trash-alt"></i>
<span class="button-text">Supprimer</span>
</button>
<button type="button" id="saveCover" class="button">
<i class="fas fa-save"></i>
<span class="button-text">Enregistrer</span>
</button>
</div> </div>
</div> </div>
</div> </div>

@ -175,9 +175,7 @@ $config = Config::load();
<span>Administration</span> <span>Administration</span>
</div> </div>
<div class="nav-menu"> <div class="nav-menu">
<a href="index.php" class="button tooltip" data-tooltip="Retour"> <a href="index.php" class="button">Retour</a>
<i class="fas fa-arrow-left"></i>
</a>
</div> </div>
</nav> </nav>
@ -208,23 +206,16 @@ $config = Config::load();
<?php endif; ?> <?php endif; ?>
</div> </div>
<div class="user-actions"> <div class="user-actions">
<button type="button" class="button tooltip edit-user" data-tooltip="Modifier" data-user-id="<?= htmlspecialchars($user['id']) ?>" data-user-comment="<?= htmlspecialchars($user['comment'] ?? '') ?>" data-user-role="<?= htmlspecialchars($user['role'] ?? 'editor') ?>"> <button type="button" class="button edit-user" data-user-id="<?= htmlspecialchars($user['id']) ?>" data-user-comment="<?= htmlspecialchars($user['comment'] ?? '') ?>" data-user-role="<?= htmlspecialchars($user['role'] ?? 'editor') ?>">Modifier</button>
<i class="fas fa-edit"></i>
</button>
<?php if ($index !== 0): ?> <?php if ($index !== 0): ?>
<button type="button" class="button tooltip delete-user" data-tooltip="Supprimer" data-user-id="<?= htmlspecialchars($user['id']) ?>"> <button type="button" class="button delete-user" data-user-id="<?= htmlspecialchars($user['id']) ?>">Supprimer</button>
<i class="fas fa-trash-alt"></i>
</button>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<button type="button" id="addUserBtn" class="button tooltip" data-tooltip="Ajouter un utilisateur"> <button type="button" id="addUserBtn" class="button">Ajouter un utilisateur</button>
<i class="fas fa-user-plus"></i>
<span class="tooltip-text">Ajouter un utilisateur</span>
</button>
</section> </section>
<!-- Formulaire d'ajout d'utilisateur (modal) --> <!-- Formulaire d'ajout d'utilisateur (modal) -->
@ -306,14 +297,8 @@ $config = Config::load();
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="button dark cancel-btn"> <button type="button" class="button dark cancel-btn">Annuler</button>
<i class="fas fa-times"></i> <button type="submit" class="button">Enregistrer</button>
<span class="button-text">Annuler</span>
</button>
<button type="submit" class="button">
<i class="fas fa-save"></i>
<span class="button-text">Enregistrer</span>
</button>
</div> </div>
</form> </form>
</div> </div>

@ -1,377 +0,0 @@
@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css";
/* buttons.css - Styles pour les boutons, tooltips et menu */
/* Styles pour les boutons avec icônes */
.button.tooltip,
.tooltip {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border-radius: var(--radius-sm);
transition: var(--transition-fast);
}
.button.tooltip i,
.tooltip i {
font-size: 1.2rem;
}
/* Style pour le bouton de déconnexion */
.logout-form button {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
padding: 0;
border: 1px solid var(--border-color);
background-color: var(--bg-tertiary);
color: var(--text-primary);
position: relative;
}
.logout-form button:hover {
background-color: var(--error-color);
}
.logout-form button i {
font-size: 1.2rem;
}
/* Tooltip styles */
.tooltip::after {
content: attr(data-tooltip);
position: absolute;
bottom: -35px;
left: 50%;
transform: translateX(-50%);
background-color: var(--bg-primary);
color: var(--text-primary);
padding: 5px 10px;
border-radius: var(--radius-sm);
font-size: 0.8rem;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s, visibility 0.3s;
z-index: 100;
pointer-events: none;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-color);
}
.tooltip:hover::after {
opacity: 1;
visibility: visible;
}
/* Styles pour le texte à côté des icônes (mobile) */
.tooltip-text {
display: none;
margin-left: var(--spacing-md);
}
/* Menu toggle button */
.menu-toggle {
display: none;
background: none;
border: none;
color: var(--text-primary);
font-size: 1.5rem;
cursor: pointer;
padding: var(--spacing-sm);
}
/* Styles pour la barre de navigation */
.admin-nav {
position: relative;
}
.nav-menu {
display: flex;
gap: var(--spacing-md);
justify-content: flex-end;
}
/* Styles pour les boutons d'action des romans */
.story-actions {
display: flex;
gap: var(--spacing-sm);
}
/* Style spécifique pour le bouton d'édition */
.story-actions a.button {
background-color: var(--accent-primary);
color: var(--text-tertiary);
}
.story-actions a.button:hover {
background-color: var(--accent-secondary);
}
/* Style spécifique pour le bouton d'accès */
.story-actions .manage-access {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.story-actions .manage-access:hover {
background-color: var(--accent-primary);
color: var(--text-tertiary);
}
/* Style spécifique pour le bouton de suppression */
.story-actions .delete-story {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.story-actions .delete-story:hover {
background-color: var(--error-color);
color: var(--text-tertiary);
}
/* Styles pour les boutons modaux */
.modal-footer .button {
width: auto;
padding: var(--spacing-sm) var(--spacing-md);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.modal-footer .button i,
.modal-footer .button .button-text {
pointer-events: none; /* Solution au problème de clic */
}
/* Styles pour afficher du texte dans les boutons */
.button-text {
margin-left: var(--spacing-sm);
}
/* Styles pour les boutons avec texte intégré */
.button.with-text {
width: auto;
padding: var(--spacing-sm) var(--spacing-md);
justify-content: flex-start;
}
.button.with-text i {
margin-right: var(--spacing-sm);
}
/* Styles pour les boutons de formulaire */
.submit-button,
#cleanMedia,
#addUserBtn {
width: auto;
padding: var(--spacing-sm) var(--spacing-md);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* Solution globale pour les problèmes de clic sur les boutons */
button i,
button span,
.button i,
.button span {
pointer-events: none;
}
/* Styles pour les boutons d'action de chapitre */
.chapter-actions .button {
padding: 0;
width: 40px;
height: 40px;
}
.chapter-actions .edit-chapter {
background-color: var(--accent-primary);
}
.chapter-actions .edit-cover {
background-color: var(--bg-secondary);
}
.chapter-actions .delete-chapter {
background-color: var(--bg-secondary);
}
.chapter-actions .delete-chapter:hover {
background-color: var(--error-color);
}
/* Styles pour les boutons de formulaire */
.form-actions .button {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm);
}
.form-actions .button i {
font-size: 1.2rem;
}
/* Bouton d'ajout */
.button.add-button {
background-color: var(--accent-primary);
color: var(--text-tertiary);
}
.button.add-button:hover {
background-color: var(--accent-secondary);
}
.chapter-actions .edit-chapter,
.chapter-actions .edit-cover,
.chapter-actions .delete-chapter {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
border-radius: var(--radius-sm);
transition: var(--transition-fast);
position: relative;
border: none;
cursor: pointer;
}
.chapter-actions .edit-chapter i,
.chapter-actions .edit-cover i,
.chapter-actions .delete-chapter i {
font-size: 1.2rem;
}
.chapter-actions .button-text {
display: none;
}
.chapter-actions .edit-chapter {
background-color: var(--accent-primary);
color: var(--text-tertiary);
}
.chapter-actions .edit-cover,
.chapter-actions .delete-chapter {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.chapter-actions .delete-chapter:hover {
background-color: var(--error-color);
}
.chapter-actions .edit-chapter i,
.chapter-actions .edit-cover i,
.chapter-actions .delete-chapter i,
.chapter-actions .button-text {
pointer-events: none;
}
/* Media queries pour le menu mobile */
@media (max-width: 768px) {
.menu-toggle {
display: block;
position: absolute;
right: var(--spacing-md);
top: 50%;
transform: translateY(-50%);
z-index: 101;
}
.nav-menu {
flex-direction: column;
position: absolute;
top: 100%;
right: 0;
background-color: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: var(--spacing-md);
z-index: 100;
width: 200px;
gap: var(--spacing-sm);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
/* État caché par défaut */
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all var(--transition-fast);
}
.nav-menu.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
/* Style des boutons en mode mobile */
.nav-menu.active .button,
.nav-menu.active .logout-form button {
width: 100%;
justify-content: flex-start;
padding: var(--spacing-sm) var(--spacing-md);
gap: var(--spacing-md);
}
/* Afficher le texte des tooltips en mode mobile */
.nav-menu.active .tooltip-text {
display: inline-block;
}
/* Cacher les tooltips standards sur mobile */
.tooltip::after {
display: none;
}
.logout-form {
width: 100%;
}
/* Afficher le texte des tooltips en mode mobile pour les actions des romans */
.story-actions.active .tooltip-text {
display: inline-block;
margin-left: var(--spacing-sm);
}
.story-actions.active .button {
width: auto;
padding: var(--spacing-xs) var(--spacing-sm);
}
.chapter-actions.active .edit-chapter,
.chapter-actions.active .edit-cover,
.chapter-actions.active .delete-chapter {
width: 100%;
justify-content: flex-start;
padding: var(--spacing-sm) var(--spacing-md);
}
.chapter-actions.active .button-text {
display: inline-block;
margin-left: var(--spacing-sm);
}
}
/* Pour les très petits écrans */
@media (max-width: 480px) {
.story-actions {
justify-content: space-between;
}
.story-actions .button {
flex: 1;
}
}

@ -5,10 +5,9 @@
border-radius: var(--radius-md); border-radius: var(--radius-md);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
display: flex; display: flex;
align-items: flex-start; align-items: center;
gap: var(--spacing-lg); gap: var(--spacing-lg);
transition: transform var(--transition-fast), box-shadow var(--transition-fast); transition: transform var(--transition-fast), box-shadow var(--transition-fast);
position: relative;
} }
.story-item:hover { .story-item:hover {
@ -17,7 +16,7 @@
} }
.story-cover { .story-cover {
width: 180px; width: 200px;
height: 120px; height: 120px;
object-fit: cover; object-fit: cover;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@ -36,10 +35,6 @@
.story-info { .story-info {
flex: 1; flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 120px;
} }
.story-info p { .story-info p {
@ -93,7 +88,7 @@
.chapter-number { .chapter-number {
background: var(--accent-primary); background: var(--accent-primary);
color: var(--text-tertiary); color: var(--text-primary);
width: 40px; width: 40px;
height: 40px; height: 40px;
display: flex; display: flex;

@ -1,182 +0,0 @@
/* Correctifs pour les problèmes d'affichage des champs de texte */
/* 1. Règles globales pour tous les conteneurs de formulaire */
.form-group {
margin-bottom: var(--spacing-lg);
width: 100%;
position: relative;
overflow: visible;
}
/* 2. Correction pour les champs de texte */
.form-group input[type="text"],
.form-group input[type="password"],
.form-group textarea,
.form-group input[type="file"],
.form-group select {
width: 100%;
box-sizing: border-box;
max-width: 100%;
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;
}
/* 3. Correction pour les modales et leurs contenus */
.modal-content {
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
padding: var(--spacing-md);
width: auto;
box-sizing: border-box;
}
.modal-header,
.modal-body,
.modal-footer {
width: 100%;
box-sizing: border-box;
padding: var(--spacing-md);
}
/* 4. Correction pour l'éditeur Quill */
.ql-container {
overflow: auto;
max-width: 100%;
}
.ql-editor {
min-height: 200px;
max-width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
/* 5. Gestion spécifique pour les grilles et les flex containers */
.story-item,
.chapter-item,
.user-item {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-md);
width: 100%;
box-sizing: border-box;
}
.story-info,
.chapter-info {
flex: 1;
min-width: 0; /* Empêche le dépassement */
overflow: hidden;
}
/* 6. Meilleures media queries pour la responsivité */
@media (max-width: 768px) {
.story-item,
.chapter-item,
.user-item {
flex-direction: column;
}
.story-actions,
.chapter-actions,
.user-actions {
width: 100%;
flex-wrap: wrap;
justify-content: flex-start;
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group textarea {
font-size: 16px; /* Pour éviter le zoom sur mobile */
}
#chapterTitle {
width: 100%;
}
}
/* 7. Correction pour les boutons qui peuvent déborder */
.button,
button {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* 8. Meilleure gestion des tables et des listes */
table {
width: 100%;
max-width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
td, th {
word-break: break-word;
overflow-wrap: break-word;
padding: var(--spacing-sm);
}
/* 9. Amélioration du contenu affichable */
.novel-description,
.chapter-content,
.about-description {
width: 100%;
max-width: 100%;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
/* 10. Flexibilité des images dans l'éditeur et le contenu */
img {
max-width: 100%;
height: auto;
}
/* 11. Correction pour les éléments de navigation */
.chapters-list a {
display: block;
width: 100%;
box-sizing: border-box;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 12. Correction pour la page options.php */
.options-section {
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
#aboutEditor {
max-width: 100%;
}
.cover-preview-container {
max-width: 100%;
overflow: hidden;
}
.link-item {
width: 100%;
box-sizing: border-box;
}
/* 13. Empêcher les textes de déborder */
h1, h2, h3, h4, h5, h6, p {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
max-width: 100%;
}

@ -204,69 +204,6 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
/* Style pour les sélecteurs (dropdowns) */
select {
width: 100%;
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;
transition: border-color var(--transition-fast);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='6'%3E%3Cpath d='M0 0h12L6 6 0 0z' fill='%23d2a679'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right var(--spacing-sm) center;
background-size: 12px;
padding-right: 30px;
}
select:focus {
border-color: var(--accent-primary);
outline: none;
box-shadow: 0 0 0 2px rgba(210, 166, 121, 0.2);
}
select option {
background-color: var(--bg-secondary);
color: var(--text-primary);
padding: var(--spacing-sm);
}
/* Style pour les sélecteurs dans les formulaires modaux */
.modal-content select {
max-width: 100%;
margin-bottom: var(--spacing-md);
}
/* Style pour le sélecteur désactivé (pour le premier utilisateur admin) */
select:disabled {
opacity: 0.7;
cursor: not-allowed;
background-color: var(--bg-secondary);
}
/* Styles spécifiques pour les sélecteurs dans users.php */
#role, #edit_role {
width: auto;
min-width: 180px;
background-color: var(--input-bg);
}
/* Style visuel différent pour les options */
#role option[value="admin"],
#edit_role option[value="admin"] {
font-weight: bold;
color: var(--accent-primary);
}
#role option[value="editor"],
#edit_role option[value="editor"] {
font-weight: normal;
color: var(--text-secondary);
}
/* Responsive */ /* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.options-section { .options-section {
@ -276,9 +213,4 @@ select:disabled {
.link-item { .link-item {
padding: var(--spacing-md); padding: var(--spacing-md);
} }
select {
width: 100%;
max-width: none;
}
} }

@ -3,6 +3,4 @@
@import 'layout.css'; @import 'layout.css';
@import 'components.css'; @import 'components.css';
@import 'forms.css'; @import 'forms.css';
@import 'editor.css'; @import 'editor.css';
@import 'buttons.css';
@import 'fixes.css';

@ -4,9 +4,6 @@
/* Import Google Fonts */ /* Import Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Parisienne&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Parisienne&display=swap');
/* Import Font Awesome */
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css');
/* Variables globales */ /* Variables globales */
:root { :root {
/* Couleurs de fond */ /* Couleurs de fond */
@ -762,207 +759,4 @@ body {
.stats-value { .stats-value {
font-size: 1.1rem; font-size: 1.1rem;
} }
}
/* Styles pour les boutons avec icônes */
.about-button {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--accent-primary);
color: var(--text-tertiary);
text-decoration: none;
border-radius: var(--radius-sm);
transition: var(--transition);
font-weight: normal;
}
.about-button i {
font-size: 1.1rem;
}
.about-button:hover {
background-color: var(--accent-secondary);
}
/* Navigation entre chapitres */
.chapter-nav {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
text-decoration: none;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-sm);
transition: var(--transition);
}
.chapter-nav:hover {
background-color: var(--bg-secondary);
color: var(--accent-primary);
}
/* Bouton retour en haut */
.scroll-top {
position: fixed;
bottom: 2rem;
right: 2rem;
background-color: var(--bg-tertiary);
color: var(--text-primary);
width: 3rem;
height: 3rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
border: 1px solid var(--border-color);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
cursor: pointer;
z-index: 100;
}
.scroll-top i {
font-size: 1.2rem;
}
.scroll-top.visible {
opacity: 1;
visibility: visible;
}
.scroll-top:hover {
background-color: var(--accent-primary);
color: var(--text-tertiary);
}
/* Lien de retour */
.back-to-home a {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
text-decoration: none;
transition: var(--transition);
padding: var(--spacing-sm) 0;
}
.back-to-home a:hover {
color: var(--accent-primary);
}
/* Icônes dans la liste des chapitres */
.chapters-list a {
position: relative;
padding-left: calc(var(--spacing-md) + 1.2rem);
}
.chapters-list a::before {
content: "\f15c"; /* Icône de document */
font-family: "Font Awesome 6 Free";
font-weight: 900;
position: absolute;
left: var(--spacing-sm);
color: var(--accent-primary);
}
.chapters-list .current-chapter::before {
content: "\f02e"; /* Icône de marque-page */
color: var(--text-tertiary);
}
/* Pour les petits écrans */
@media (max-width: 768px) {
.header-actions {
display: flex;
gap: var(--spacing-sm);
}
.about-button {
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.9rem;
}
}
/* Modification des styles pour les chapitres en brouillon */
.chapters-list .draft-chapter {
opacity: 0.7;
border-left: 3px solid var(--accent-primary);
padding-left: calc(var(--spacing-sm) + 1.5rem) !important;
}
.chapters-list .draft-chapter::before {
content: "\f249";
left: calc(var(--spacing-sm) + 0.3rem);
}
.draft-label {
font-size: 0.8em;
background-color: var(--accent-primary);
color: var(--text-tertiary);
padding: 2px 6px;
border-radius: 10px;
margin-left: 8px;
display: inline-block;
vertical-align: middle;
position: relative;
z-index: 1;
}
/* S'assurer que l'étiquette de brouillon ne chevauche pas l'icône */
.chapters-list a {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: var(--spacing-sm);
padding-left: calc(var(--spacing-md) + 1.2rem);
gap: var(--spacing-xs);
line-height: 1.4;
}
/* Pour conserver l'espace correct en cas de texte long */
.chapter-title-text {
flex: 1;
min-width: 0;
white-space: normal;
word-break: break-word;
}
/* Modification des styles pour les chapitres en brouillon */
.new-chapter {
border-left: 3px solid #5a8c54;
padding-left: calc(var(--spacing-sm) + 1.5rem) !important;
}
.chapters-list .draft-chapter::before {
content: "\f249";
left: calc(var(--spacing-sm) + 0.3rem);
}
.new-label {
font-size: 0.8em;
background-color: #5a8c54;
color: var(--text-primary);
padding: 2px 6px;
border-radius: 10px;
margin-left: 8px;
display: inline-block;
vertical-align: middle;
position: relative;
z-index: 1;
}
@media (max-width: 768px) {
.chapters-list a {
padding-right: var(--spacing-xs);
}
.draft-label {
font-size: 0.7em;
padding: 1px 4px;
margin-left: 4px;
}
} }

@ -133,126 +133,33 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
// Gestion du menu mobile // Gestion du menu mobile
const navMenu = document.querySelector('.nav-menu'); const navMenu = document.querySelector('.nav-menu');
const navBrand = document.querySelector('.nav-brand');
function initializeTooltips() {
// Ajouter le texte des tooltips à côté des icônes pour mobile
document.querySelectorAll('.tooltip:not(.tooltip-initialized)').forEach(button => {
if (!button.querySelector('.tooltip-text')) {
const tooltip = button.getAttribute('data-tooltip');
if (tooltip) {
const tooltipSpan = document.createElement('span');
tooltipSpan.className = 'tooltip-text';
tooltipSpan.textContent = tooltip;
button.appendChild(tooltipSpan);
button.classList.add('tooltip-initialized');
}
}
});
}
// Observer les changements du DOM pour initialiser les nouveaux tooltips
const tooltipObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
initializeTooltips();
}
});
});
// Initialiser les tooltips existants et commencer l'observation
document.addEventListener('DOMContentLoaded', function() {
initializeTooltips();
tooltipObserver.observe(document.body, { childList: true, subtree: true });
// Vérifier le mode mobile au chargement et au redimensionnement // Créer le bouton hamburger s'il n'existe pas déjà
function checkMobileMode() { if (!document.querySelector('.menu-toggle')) {
const isMobile = window.innerWidth <= 768; const menuToggle = document.createElement('button');
menuToggle.className = 'menu-toggle';
menuToggle.innerHTML = '☰';
menuToggle.setAttribute('aria-label', 'Menu');
// Gestion des conteneurs d'actions // Insérer le bouton avant le menu
const actionContainers = document.querySelectorAll('.story-actions, .chapter-actions, .user-actions'); navMenu.parentNode.insertBefore(menuToggle, navMenu);
actionContainers.forEach(container => {
if (isMobile) {
container.classList.add('active');
} else {
container.classList.remove('active');
}
});
}
checkMobileMode();
window.addEventListener('resize', checkMobileMode);
});
// Créer le bouton hamburger s'il n'existe pas déjà
if (!document.querySelector('.menu-toggle')) {
const menuToggle = document.createElement('button');
menuToggle.className = 'menu-toggle';
menuToggle.innerHTML = '☰';
menuToggle.setAttribute('aria-label', 'Menu');
// Insérer le bouton avant le menu
navMenu.parentNode.insertBefore(menuToggle, navMenu);
// Ajouter le texte des tooltips à côté des icônes pour mobile
document.querySelectorAll('.nav-menu .tooltip').forEach(button => {
const tooltip = button.getAttribute('data-tooltip');
const tooltipSpan = document.createElement('span');
tooltipSpan.className = 'tooltip-text';
tooltipSpan.textContent = tooltip;
button.appendChild(tooltipSpan);
});
// Gérer les clics sur le bouton
menuToggle.addEventListener('click', function() {
navMenu.classList.toggle('active');
});
// Fermer le menu au clic en dehors
document.addEventListener('click', function(e) {
if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) {
navMenu.classList.remove('active');
}
});
}
// Ajouter le texte des tooltips à côté des icônes pour mobile - pour tous les tooltips
function addTooltipText() {
document.querySelectorAll('.tooltip').forEach(button => {
// Vérifier si le tooltip-text n'existe pas déjà
if (!button.querySelector('.tooltip-text')) {
const tooltip = button.getAttribute('data-tooltip');
const tooltipSpan = document.createElement('span');
tooltipSpan.className = 'tooltip-text';
tooltipSpan.textContent = tooltip;
button.appendChild(tooltipSpan);
}
});
}
// Appliquer à tous les tooltips du site
addTooltipText();
// Détection de la taille d'écran pour activer le mode mobile
function checkMobileMode() {
const isMobile = window.innerWidth <= 768;
// Gestion des actions pour les romans // Gérer les clics sur le bouton
const storyActions = document.querySelectorAll('.story-actions'); menuToggle.addEventListener('click', function() {
storyActions.forEach(action => { navMenu.classList.toggle('active');
if (isMobile) { });
action.classList.add('active');
} else { // Fermer le menu au clic en dehors
action.classList.remove('active'); document.addEventListener('click', function(e) {
if (!navMenu.contains(e.target) && !menuToggle.contains(e.target)) {
navMenu.classList.remove('active');
} }
}); });
} }
// Vérifier au chargement et au redimensionnement
checkMobileMode();
window.addEventListener('resize', checkMobileMode);
// Gestion de la suppression des romans // Gestion de la suppression des romans
const storyList = document.querySelector('.stories-list'); const storyList = document.querySelector('.stories-list');
if (storyList) { if (storyList) {

@ -480,11 +480,4 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialisation // Initialisation
detectUnsavedChanges(); detectUnsavedChanges();
});
document.addEventListener('DOMContentLoaded', function() {
// S'assurer que les clics sur les icônes se propagent correctement
document.querySelectorAll('.chapter-actions button i, .chapter-actions button .button-text').forEach(element => {
element.style.pointerEvents = 'none';
});
}); });

@ -79,12 +79,8 @@ $config = Config::load();
<div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($currentChapter['cover']) ?>');"></div> <div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($currentChapter['cover']) ?>');"></div>
<?php endif; ?> <?php endif; ?>
<div class="header-actions"> <div class="header-actions">
<a href="index.php" class="about-button"> <a href="index.php" class="about-button">Accueil</a>
<i class="fas fa-home"></i> Accueil <a href="roman.php?id=<?= urlencode($storyId) ?>" class="about-button">Roman</a>
</a>
<a href="roman.php?id=<?= urlencode($storyId) ?>" class="about-button">
<i class="fas fa-book"></i> Roman
</a>
</div> </div>
<h1> <h1>
<?= htmlspecialchars($currentChapter['title']) ?> <?= htmlspecialchars($currentChapter['title']) ?>
@ -103,15 +99,15 @@ $config = Config::load();
<div class="chapter-navigation"> <div class="chapter-navigation">
<?php if ($prevChapter): ?> <?php if ($prevChapter): ?>
<a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($prevChapter['id']) ?>" <a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($prevChapter['id']) ?>"
class="chapter-nav prev-chapter"> class="chapter-nav prev-chapter">
<i class="fas fa-chevron-left"></i> <?= htmlspecialchars($prevChapter['title']) ?> &larr; <?= htmlspecialchars($prevChapter['title']) ?>
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($nextChapter): ?> <?php if ($nextChapter): ?>
<a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($nextChapter['id']) ?>" <a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($nextChapter['id']) ?>"
class="chapter-nav next-chapter"> class="chapter-nav next-chapter">
<?= htmlspecialchars($nextChapter['title']) ?> <i class="fas fa-chevron-right"></i> <?= htmlspecialchars($nextChapter['title']) ?> &rarr;
</a> </a>
<?php endif; ?> <?php endif; ?>
</div> </div>
@ -120,56 +116,25 @@ $config = Config::load();
<aside class="chapters-menu"> <aside class="chapters-menu">
<h2>Chapitres</h2> <h2>Chapitres</h2>
<ul class="chapters-list"> <ul class="chapters-list">
<?php <?php
// Vérifier que chapters est un tableau avant de le filtrer foreach ($visibleChapters as $chapter):
$chapters = $story['chapters'] ?? []; $isDraft = $chapter['draft'] ?? false;
?>
// Filtrer les chapitres pour n'afficher que les chapitres publiés <li>
// ou les brouillons si l'utilisateur a les droits <a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($chapter['id']) ?>"
$visibleChapters = []; class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?> <?= $isDraft ? 'draft-chapter' : '' ?>">
foreach ($chapters as $chapter) { <?= htmlspecialchars($chapter['title']) ?>
if (!($chapter['draft'] ?? false) || $canViewDrafts) { <?php if ($isDraft && $canViewDrafts): ?>
$visibleChapters[] = $chapter; <span class="draft-label">(Brouillon)</span>
} <?php endif; ?>
} </a>
</li>
if (empty($visibleChapters)): ?> <?php endforeach; ?>
<p>Aucun chapitre publié disponible pour le moment.</p> </ul>
<?php else:
foreach ($visibleChapters as $chapter):
$isDraft = $chapter['draft'] ?? false;
$isNew = false;
// Vérifier si le chapitre est nouveau (publié il y a moins d'une semaine)
// Utiliser 'created' puisque c'est ce champ qui est présent dans vos données
if (isset($chapter['created'])) {
$publishedTime = strtotime($chapter['created']);
$weekAgo = strtotime('-1 week');
$isNew = $publishedTime > $weekAgo && !$isDraft;
}
?>
<li>
<a href="chapitre.php?story=<?= urlencode($story['id']) ?>&chapter=<?= urlencode($chapter['id']) ?>"
class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?> <?= $isDraft ? 'draft-chapter' : ($isNew ? 'new-chapter' : '') ?>">
<span class="chapter-title"><?= htmlspecialchars($chapter['title']) ?></span>
<?php if ($isDraft && $canViewDrafts): ?>
<span class="draft-label">(Brouillon)</span>
<?php elseif ($isNew): ?>
<span class="new-label">(Nouveau)</span>
<?php endif; ?>
</a>
</li>
<?php
endforeach;
endif;
?>
</ul>
</aside> </aside>
</div> </div>
<button class="scroll-top" aria-label="Retour en haut de page"> <button class="scroll-top" aria-label="Retour en haut de page"></button>
<i class="fas fa-arrow-up"></i>
</button>
<style> <style>
.draft-header::after { .draft-header::after {

@ -18,7 +18,15 @@ class Stories {
} }
return json_decode(file_get_contents($file), true); return json_decode(file_get_contents($file), true);
} }
public static function save($story) {
// Ajout de l'heure à la date de mise à jour
$story['updated'] = date('Y-m-d H:i:s');
$file = self::$storiesDir . $story['id'] . '.json';
return file_put_contents($file, json_encode($story, JSON_PRETTY_PRINT));
}
public static function formatDate($date) { public static function formatDate($date) {
if (strlen($date) > 10) { // Si la date contient aussi l'heure if (strlen($date) > 10) { // Si la date contient aussi l'heure
$datetime = DateTime::createFromFormat('Y-m-d H:i:s', $date); $datetime = DateTime::createFromFormat('Y-m-d H:i:s', $date);
@ -28,54 +36,4 @@ class Stories {
return $datetime ? $datetime->format('d/m/Y') : ''; return $datetime ? $datetime->format('d/m/Y') : '';
} }
} }
public static function save($story) {
// Mise à jour de la date standard pour tout changement
$story['updated'] = date('Y-m-d H:i:s');
// Vérification si c'est une création initiale
$isNew = !file_exists(self::$storiesDir . $story['id'] . '.json');
// Vérification si un nouveau chapitre a été ajouté ou un chapitre est passé de draft à publié
$existingStory = $isNew ? null : self::get($story['id']);
$updatePublicDate = $isNew; // Déjà true si c'est un nouveau roman
if (!$isNew && isset($existingStory['chapters']) && isset($story['chapters'])) {
// Vérifier les nouveaux chapitres ou changements de statut
foreach ($story['chapters'] as $chapter) {
$found = false;
foreach ($existingStory['chapters'] as $oldChapter) {
if ($oldChapter['id'] === $chapter['id']) {
$found = true;
// Chapitre passé de brouillon à publié
if (($oldChapter['draft'] ?? false) && !($chapter['draft'] ?? false)) {
$updatePublicDate = true;
// Ajouter une date de publication au chapitre
$chapter['published'] = date('Y-m-d H:i:s');
}
break;
}
}
// Nouveau chapitre
if (!$found && !($chapter['draft'] ?? false)) {
$updatePublicDate = true;
// S'assurer que le nouveau chapitre a une date de publication
if (!isset($chapter['published'])) {
$chapter['published'] = date('Y-m-d H:i:s');
}
}
}
}
// Mettre à jour la date publique si nécessaire
if ($updatePublicDate) {
$story['publicUpdated'] = date('Y-m-d H:i:s');
} else if (!isset($story['publicUpdated'])) {
// S'assurer que le champ existe
$story['publicUpdated'] = $story['created'] ?? date('Y-m-d H:i:s');
}
$file = self::$storiesDir . $story['id'] . '.json';
return file_put_contents($file, json_encode($story, JSON_PRETTY_PRINT));
}
} }

@ -11,9 +11,9 @@ usort($stories, function($a, $b) {
return strtotime($b['updated']) - strtotime($a['updated']); return strtotime($b['updated']) - strtotime($a['updated']);
}); });
// Fonction pour vérifier si un roman est une nouvelle parution // Fonction pour vérifier si un roman est une nouvelle parution (moins d'une semaine)
function isNewRelease($story) { function isNewRelease($story) {
$updateTime = strtotime($story['publicUpdated'] ?? $story['updated']); $updateTime = strtotime($story['updated']);
$weekAgo = strtotime('-1 week'); $weekAgo = strtotime('-1 week');
return $updateTime > $weekAgo; return $updateTime > $weekAgo;
} }
@ -64,9 +64,7 @@ function formatDate($date) {
</div> </div>
<div class="header-actions"> <div class="header-actions">
<a href="about.php" class="about-button"> <a href="about.php" class="about-button">À propos</a>
<i class="fas fa-info-circle"></i> À propos
</a>
</div> </div>
</header> </header>
</div> </div>

@ -45,9 +45,7 @@ $canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
<div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($story['cover']) ?>');"></div> <div class="novel-header-background" style="background-image: url('<?= htmlspecialchars($story['cover']) ?>');"></div>
<?php endif; ?> <?php endif; ?>
<div class="header-actions"> <div class="header-actions">
<a href="index.php" class="about-button"> <a href="index.php" class="about-button">Accueil</a>
<i class="fas fa-home"></i> Accueil
</a>
</div> </div>
<h1><?= htmlspecialchars($story['title']) ?></h1> <h1><?= htmlspecialchars($story['title']) ?></h1>
</header> </header>
@ -63,41 +61,23 @@ $canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
<?php if (!empty($story['chapters'])): ?> <?php if (!empty($story['chapters'])): ?>
<ul class="chapters-list"> <ul class="chapters-list">
<?php <?php
// Vérifier que chapters est un tableau avant de le filtrer
$chapters = $story['chapters'] ?? [];
// Filtrer les chapitres pour n'afficher que les chapitres publiés // Filtrer les chapitres pour n'afficher que les chapitres publiés
// ou les brouillons si l'utilisateur a les droits // ou les brouillons si l'utilisateur a les droits
$visibleChapters = []; $visibleChapters = array_filter($story['chapters'], function($chapter) use ($canViewDrafts) {
foreach ($chapters as $chapter) { return !($chapter['draft'] ?? false) || $canViewDrafts;
if (!($chapter['draft'] ?? false) || $canViewDrafts) { });
$visibleChapters[] = $chapter;
}
}
if (empty($visibleChapters)): ?> if (empty($visibleChapters)): ?>
<p>Aucun chapitre publié disponible pour le moment.</p> <p>Aucun chapitre publié disponible pour le moment.</p>
<?php else: <?php else:
foreach ($visibleChapters as $chapter): foreach ($visibleChapters as $chapter):
$isDraft = $chapter['draft'] ?? false;
$isNew = false;
// Vérifier si le chapitre est nouveau (publié il y a moins d'une semaine)
// Utiliser 'created' puisque c'est ce champ qui est présent dans vos données
if (isset($chapter['created'])) {
$publishedTime = strtotime($chapter['created']);
$weekAgo = strtotime('-1 week');
$isNew = $publishedTime > $weekAgo && !$isDraft;
}
?> ?>
<li> <li>
<a href="chapitre.php?story=<?= urlencode($story['id']) ?>&chapter=<?= urlencode($chapter['id']) ?>" <a href="chapitre.php?story=<?= urlencode($story['id']) ?>&chapter=<?= urlencode($chapter['id']) ?>"
class="<?= $isDraft ? 'draft-chapter' : ($isNew ? 'new-chapter' : '') ?>"> class="<?= ($chapter['draft'] ?? false) ? 'draft-chapter' : '' ?>">
<span class="chapter-title"><?= htmlspecialchars($chapter['title']) ?></span> <?= htmlspecialchars($chapter['title']) ?>
<?php if ($isDraft && $canViewDrafts): ?> <?php if (($chapter['draft'] ?? false) && $canViewDrafts): ?>
<span class="draft-label">(Brouillon)</span> <span class="draft-label">(Brouillon)</span>
<?php elseif ($isNew): ?>
<span class="new-label">(Nouveau)</span>
<?php endif; ?> <?php endif; ?>
</a> </a>
</li> </li>
@ -112,9 +92,26 @@ $canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
</aside> </aside>
</div> </div>
<button class="scroll-top" aria-label="Retour en haut de page"> <button class="scroll-top" aria-label="Retour en haut de page"></button>
<i class="fas fa-arrow-up"></i>
</button> <style>
.draft-chapter {
opacity: 0.7;
border-left: 3px solid var(--accent-primary);
padding-left: 8px !important;
}
.draft-label {
font-size: 0.8em;
background-color: var(--accent-primary);
color: var(--text-tertiary);
padding: 2px 6px;
border-radius: 10px;
margin-left: 8px;
display: inline-block;
vertical-align: middle;
}
</style>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

@ -1 +1 @@
1.3.3 1.3.0