Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
705860a5ec | |||
5912c7a0be | |||
7dfd600936 |
186
README.md
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
|
||||

|
||||
|
||||
## Fonctionnalités principales
|
||||
|
||||
### Zone Administrative
|
||||
- Interface sécurisée pour la gestion des contenus
|
||||
- Création et édition de romans avec éditeur WYSIWYG
|
||||
- Gestion flexible des chapitres avec réorganisation par glisser-déposer
|
||||
- Prévisualisation avant publication
|
||||
- Stockage JSON pour une maintenance simplifiée
|
||||
- Système d'upload d'images avec redimensionnement automatique
|
||||
- Gestion des métadonnées (date de création, mise à jour, etc.)
|
||||
- Import/Export des romans
|
||||
- Gestion du profil et des options du site
|
||||
|
||||

|
||||

|
||||

|
||||
- Création et édition de romans avec éditeur WYSIWYG (Quill.js)
|
||||
- Gestion des chapitres avec réorganisation par glisser-déposer
|
||||
- Mode brouillon pour les chapitres en cours de rédaction
|
||||
- Système d'upload et de gestion d'images
|
||||
- Gestion des accès utilisateurs (administrateurs, éditeurs)
|
||||
- Import/Export des romans au format ZIP
|
||||
- Personnalisation des options du site (logo, bannière, informations)
|
||||
|
||||
### Zone Publique
|
||||
- Interface de lecture épurée et confortable
|
||||
- Interface de lecture épurée aux tons bruns/ocre
|
||||
- Navigation intuitive entre les chapitres
|
||||
- Design responsive optimisé pour la lecture
|
||||
- Thème personnalisé aux tons bruns/ocre
|
||||
- Affichage adaptatif des images et du contenu
|
||||
- Design responsive optimisé pour tous les appareils
|
||||
- Section "À propos" personnalisable avec statistiques automatiques
|
||||
- Indicateurs pour nouveaux chapitres et contenus en cours de rédaction
|
||||
|
||||

|
||||

|
||||
|
||||
## 📋 Prérequis
|
||||
## Prérequis techniques
|
||||
|
||||
- PHP 8.0 ou supérieur
|
||||
- Serveur web (Apache/Nginx) avec mod_rewrite activé
|
||||
- Extensions PHP : GD ou Imagick pour le traitement des images
|
||||
- Permissions d'écriture sur les dossiers `stories/`, `assets/images/` et `admin/`
|
||||
- Extensions PHP : GD pour le traitement des images
|
||||
- Permissions d'écriture sur les dossiers `stories/` et `assets/images/`
|
||||
|
||||
## 🛠️ Installation
|
||||
## Installation rapide
|
||||
|
||||
1. Clonez le dépôt :
|
||||
```bash
|
||||
git clone https://github.com/votre-username/nom-du-projet.git
|
||||
cd nom-du-projet
|
||||
```
|
||||
1. Clonez le dépôt
|
||||
2. Copiez et modifiez `config.json` avec vos paramètres
|
||||
3. Définissez les permissions appropriées sur les dossiers
|
||||
4. Accédez à `/admin` pour commencer à gérer vos romans
|
||||
|
||||
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
|
||||
## Structure du projet
|
||||
|
||||
```
|
||||
/
|
||||
├── admin/ # Zone administrative sécurisée
|
||||
│ ├── 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
|
||||
├── admin/ # Zone administrative
|
||||
├── assets/ # Ressources statiques (CSS, JS, images)
|
||||
├── includes/ # Fichiers PHP réutilisables
|
||||
├── stories/ # Dossier contenant les récits (JSON)
|
||||
├── stories/ # Romans au format JSON
|
||||
├── 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
|
||||
- Protection contre les attaques XSS et CSRF
|
||||
- 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
|
||||
- Protection contre les injections et les attaques XSS
|
||||
- Validation des données et restrictions sur les uploads
|
||||
|
||||
## 🌐 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
|
||||
## License
|
||||
|
||||
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;
|
||||
foreach ($story['chapters'] as &$chapter) {
|
||||
if ($chapter['id'] === $chapterId) {
|
||||
$wasInDraft = $chapter['draft'] ?? false;
|
||||
$chapter['title'] = $title;
|
||||
$chapter['content'] = $content;
|
||||
$chapter['draft'] = $draft;
|
||||
$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;
|
||||
break;
|
||||
}
|
||||
|
@ -931,6 +931,30 @@ body {
|
||||
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);
|
||||
|
59
chapitre.php
59
chapitre.php
@ -120,21 +120,50 @@ $config = Config::load();
|
||||
<aside class="chapters-menu">
|
||||
<h2>Chapitres</h2>
|
||||
<ul class="chapters-list">
|
||||
<?php
|
||||
foreach ($visibleChapters as $chapter):
|
||||
$isDraft = $chapter['draft'] ?? false;
|
||||
?>
|
||||
<li>
|
||||
<a href="?story=<?= urlencode($storyId) ?>&chapter=<?= urlencode($chapter['id']) ?>"
|
||||
class="<?= $chapter['id'] === $chapterId ? 'current-chapter' : '' ?> <?= $isDraft ? 'draft-chapter' : '' ?>">
|
||||
<span class="chapter-title-text"><?= htmlspecialchars($chapter['title']) ?></span>
|
||||
<?php if ($isDraft && $canViewDrafts): ?>
|
||||
<span class="draft-label">Brouillon</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?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
|
||||
// ou les brouillons si l'utilisateur a les droits
|
||||
$visibleChapters = [];
|
||||
foreach ($chapters as $chapter) {
|
||||
if (!($chapter['draft'] ?? false) || $canViewDrafts) {
|
||||
$visibleChapters[] = $chapter;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($visibleChapters)): ?>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
@ -18,15 +18,7 @@ class Stories {
|
||||
}
|
||||
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) {
|
||||
if (strlen($date) > 10) { // Si la date contient aussi l'heure
|
||||
$datetime = DateTime::createFromFormat('Y-m-d H:i:s', $date);
|
||||
@ -36,4 +28,54 @@ class Stories {
|
||||
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']);
|
||||
});
|
||||
|
||||
// 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) {
|
||||
$updateTime = strtotime($story['updated']);
|
||||
$updateTime = strtotime($story['publicUpdated'] ?? $story['updated']);
|
||||
$weekAgo = strtotime('-1 week');
|
||||
return $updateTime > $weekAgo;
|
||||
}
|
||||
|
48
roman.php
48
roman.php
@ -63,24 +63,41 @@ $canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
|
||||
<?php if (!empty($story['chapters'])): ?>
|
||||
<ul class="chapters-list">
|
||||
<?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
|
||||
// ou les brouillons si l'utilisateur a les droits
|
||||
$visibleChapters = array_filter($story['chapters'], function($chapter) use ($canViewDrafts) {
|
||||
return !($chapter['draft'] ?? false) || $canViewDrafts;
|
||||
});
|
||||
$visibleChapters = [];
|
||||
foreach ($chapters as $chapter) {
|
||||
if (!($chapter['draft'] ?? false) || $canViewDrafts) {
|
||||
$visibleChapters[] = $chapter;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($visibleChapters)): ?>
|
||||
<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="<?= $isDraft ? 'draft-chapter' : '' ?>">
|
||||
<span class="chapter-title-text"><?= htmlspecialchars($chapter['title']) ?></span>
|
||||
class="<?= $isDraft ? 'draft-chapter' : ($isNew ? 'new-chapter' : '') ?>">
|
||||
<span class="chapter-title"><?= htmlspecialchars($chapter['title']) ?></span>
|
||||
<?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; ?>
|
||||
</a>
|
||||
</li>
|
||||
@ -99,25 +116,6 @@ $canViewDrafts = Auth::check() && Auth::canAccessStory($storyId);
|
||||
<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>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scrollTopBtn = document.querySelector('.scroll-top');
|
||||
|
@ -1 +1 @@
|
||||
1.3.1
|
||||
1.3.3
|
Loading…
x
Reference in New Issue
Block a user