Compare commits

...

3 Commits
v1.3.1 ... main

Author SHA1 Message Date
705860a5ec readme à jour et plus concis et correction du bouton d'accueil des romans 2025-02-27 00:07:05 +01:00
5912c7a0be Actualiser README.md
suppression des images obsolètes pour un gif plus à jour.
2025-02-26 19:02:42 +00:00
7dfd600936 amélioration de l'affichage des nouveaux chapitres
modification de la logique, la partie publique ne prend en compte que la date publication
2025-02-25 12:07:11 +01:00
8 changed files with 187 additions and 205 deletions

186
README.md

@ -1,179 +1,59 @@
# Plateforme de Publication de Romans en Ligne # Lectures d'Esenjin - Plateforme de Publication de Romans
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. Une plateforme web élégante pour la publication et la lecture de romans, développée en PHP avec stockage JSON.
## 🚀 Fonctionnalités ![Aperçu du site](https://concepts.esenjin.xyz/cyla/fichiers/67bf650a481c8_1740596490.gif)
## 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 - Création et édition de romans avec éditeur WYSIWYG (Quill.js)
- Gestion flexible des chapitres avec réorganisation par glisser-déposer - Gestion des chapitres avec réorganisation par glisser-déposer
- Prévisualisation avant publication - Mode brouillon pour les chapitres en cours de rédaction
- Stockage JSON pour une maintenance simplifiée - Système d'upload et de gestion d'images
- Système d'upload d'images avec redimensionnement automatique - Gestion des accès utilisateurs (administrateurs, éditeurs)
- Gestion des métadonnées (date de création, mise à jour, etc.) - Import/Export des romans au format ZIP
- Import/Export des romans - Personnalisation des options du site (logo, bannière, informations)
- 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 et confortable - Interface de lecture épurée aux tons bruns/ocre
- Navigation intuitive entre les chapitres - Navigation intuitive entre les chapitres
- Design responsive optimisé pour la lecture - Design responsive optimisé pour tous les appareils
- Thème personnalisé aux tons bruns/ocre - Section "À propos" personnalisable avec statistiques automatiques
- Affichage adaptatif des images et du contenu - Indicateurs pour nouveaux chapitres et contenus en cours de rédaction
![image](https://concepts.esenjin.xyz/cyla/fichiers/67b1db949863b_1739709332.png) ## Prérequis techniques
![image](https://concepts.esenjin.xyz/cyla/fichiers/67b1db9569d54_1739709333.png)
## 📋 Prérequis
- PHP 8.0 ou supérieur - PHP 8.0 ou supérieur
- Serveur web (Apache/Nginx) avec mod_rewrite activé - Extensions PHP : GD pour le traitement des images
- Extensions PHP : GD ou Imagick pour le traitement des images - Permissions d'écriture sur les dossiers `stories/` et `assets/images/`
- Permissions d'écriture sur les dossiers `stories/`, `assets/images/` et `admin/`
## 🛠️ Installation ## Installation rapide
1. Clonez le dépôt : 1. Clonez le dépôt
```bash 2. Copiez et modifiez `config.json` avec vos paramètres
git clone https://github.com/votre-username/nom-du-projet.git 3. Définissez les permissions appropriées sur les dossiers
cd nom-du-projet 4. Accédez à `/admin` pour commencer à gérer vos romans
```
2. Configurez votre serveur web pour pointer vers le dossier racine du projet ## Structure 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 sécurisée ├── admin/ # Zone administrative
│ ├── api/ # Endpoints API pour les opérations CRUD ├── assets/ # Ressources statiques (CSS, JS, images)
│ ├── 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/ # Dossier contenant les récits (JSON) ├── stories/ # Romans au format JSON
├── config.json # Configuration du site ├── config.json # Configuration du site
└── index.php # Page d'accueil publique └── index.php # Page d'accueil
``` ```
## 🔒 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 attaques XSS et CSRF - Protection contre les injections et les attaques XSS
- Validation des données entrantes - Validation des données et restrictions sur les uploads
- 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
## 🌐 Utilisation ## License
### 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.

@ -29,10 +29,19 @@ 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;
} }

@ -931,6 +931,30 @@ body {
word-break: break-word; 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) { @media (max-width: 768px) {
.chapters-list a { .chapters-list a {
padding-right: var(--spacing-xs); padding-right: var(--spacing-xs);

@ -120,21 +120,50 @@ $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
foreach ($visibleChapters as $chapter): // Vérifier que chapters est un tableau avant de le filtrer
$isDraft = $chapter['draft'] ?? false; $chapters = $story['chapters'] ?? [];
?>
<li> // Filtrer les chapitres pour n'afficher que les chapitres publiés
<a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($chapter['id']) ?>" // ou les brouillons si l'utilisateur a les droits
class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?> <?= $isDraft ? 'draft-chapter' : '' ?>"> $visibleChapters = [];
<span class="chapter-title-text"><?= htmlspecialchars($chapter['title']) ?></span> foreach ($chapters as $chapter) {
<?php if ($isDraft && $canViewDrafts): ?> if (!($chapter['draft'] ?? false) || $canViewDrafts) {
<span class="draft-label">Brouillon</span> $visibleChapters[] = $chapter;
<?php endif; ?> }
</a> }
</li>
<?php endforeach; ?> if (empty($visibleChapters)): ?>
</ul> <p>Aucun chapitre publié disponible pour le moment.</p>
<?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>

@ -18,15 +18,7 @@ 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);
@ -36,4 +28,54 @@ 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 (moins d'une semaine) // Fonction pour vérifier si un roman est une nouvelle parution
function isNewRelease($story) { function isNewRelease($story) {
$updateTime = strtotime($story['updated']); $updateTime = strtotime($story['publicUpdated'] ?? $story['updated']);
$weekAgo = strtotime('-1 week'); $weekAgo = strtotime('-1 week');
return $updateTime > $weekAgo; return $updateTime > $weekAgo;
} }

@ -63,24 +63,41 @@ $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 = array_filter($story['chapters'], function($chapter) use ($canViewDrafts) { $visibleChapters = [];
return !($chapter['draft'] ?? false) || $canViewDrafts; foreach ($chapters as $chapter) {
}); 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; $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' : '' ?>"> class="<?= $isDraft ? 'draft-chapter' : ($isNew ? 'new-chapter' : '') ?>">
<span class="chapter-title-text"><?= htmlspecialchars($chapter['title']) ?></span> <span class="chapter-title"><?= htmlspecialchars($chapter['title']) ?></span>
<?php if ($isDraft && $canViewDrafts): ?> <?php if ($isDraft && $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>
@ -99,25 +116,6 @@ $canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
<i class="fas fa-arrow-up"></i> <i class="fas fa-arrow-up"></i>
</button> </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() {
const scrollTopBtn = document.querySelector('.scroll-top'); const scrollTopBtn = document.querySelector('.scroll-top');

@ -1 +1 @@
1.3.1 1.3.3