first commit
This commit is contained in:
72
views/admin/categories/index.twig
Normal file
72
views/admin/categories/index.twig
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Tableau de bord – Catégories{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Gestion des catégories</h2>
|
||||
|
||||
{% include 'partials/_admin_nav.twig' %}
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert--success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="category-create">
|
||||
<h3 class="category-create__title">Ajouter une catégorie</h3>
|
||||
|
||||
<form method="post" action="/admin/categories/create" class="category-create__form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
|
||||
<label for="name" class="category-create__label">
|
||||
Nom
|
||||
<input type="text" id="name" name="name" required maxlength="100"
|
||||
class="form-container__input category-create__input" placeholder="ex : Développement Web">
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn--primary">Créer</button>
|
||||
</form>
|
||||
|
||||
<p class="category-create__hint">Le slug URL est généré automatiquement depuis le nom.</p>
|
||||
</div>
|
||||
|
||||
{% if categories is not empty %}
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Slug</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for category in categories %}
|
||||
<tr>
|
||||
<td data-label="Nom"><strong>{{ category.name }}</strong></td>
|
||||
<td data-label="Slug"><code>{{ category.slug }}</code></td>
|
||||
<td data-label="Actions">
|
||||
<div class="u-inline-actions">
|
||||
<form method="post" action="/admin/categories/delete/{{ category.id }}" class="u-inline-form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
<button type="submit" class="btn btn--sm btn--danger"
|
||||
onclick="return confirm('Supprimer la catégorie « {{ category.name }} » ?
|
||||
|
||||
Cette action est impossible si des articles lui sont rattachés.')">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>Aucune catégorie créée.</em></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
70
views/admin/media/index.twig
Normal file
70
views/admin/media/index.twig
Normal file
@@ -0,0 +1,70 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Tableau de bord – Médias{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Gestion des médias</h2>
|
||||
|
||||
{% include 'partials/_admin_nav.twig' %}
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert--success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if media is not empty %}
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Aperçu</th>
|
||||
<th>URL</th>
|
||||
<th>Uploadé le</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in media %}
|
||||
<tr>
|
||||
<td data-label="Aperçu">
|
||||
<div class="upload">
|
||||
<a href="{{ item.url }}" target="_blank" rel="noopener noreferrer" class="upload__thumb-link">
|
||||
<img src="{{ item.url }}" alt="" class="upload__thumb">
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="URL">
|
||||
<div class="upload">
|
||||
<code class="upload__url">{{ item.url }}</code>
|
||||
<div class="upload__actions">
|
||||
<button type="button" class="btn btn--sm btn--secondary"
|
||||
onclick="navigator.clipboard.writeText('{{ item.url }}').then(function() {
|
||||
var btn = this; btn.textContent = 'Copié !';
|
||||
setTimeout(function() { btn.textContent = 'Copier l\'URL'; }, 1500);
|
||||
}.bind(this))">Copier l'URL</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Uploadé le">{{ item.createdAt|date("d/m/Y H:i") }}</td>
|
||||
<td data-label="Actions">
|
||||
<div class="u-inline-actions">
|
||||
<form method="post" action="/admin/media/delete/{{ item.id }}" class="u-inline-form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
<button type="submit" class="btn btn--sm btn--danger"
|
||||
onclick="return confirm('Supprimer ce fichier ?\n\nAttention : s\'il est utilisé dans un article, l\'image n\'apparaîtra plus.')">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>Aucun fichier uploadé.</em></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
139
views/admin/posts/form.twig
Normal file
139
views/admin/posts/form.twig
Normal file
@@ -0,0 +1,139 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}
|
||||
{% if post is defined and post is not null and post.id > 0 %}Éditer l'article{% else %}Créer un article{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="/assets/vendor/trumbowyg/ui/trumbowyg.min.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container">
|
||||
<div class="form-container__header">
|
||||
<h2 class="form-container__title">
|
||||
{% if post is defined and post is not null and post.id > 0 %}Éditer l'article{% else %}Créer un article{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{% include 'partials/_admin_nav.twig' %}
|
||||
|
||||
<div class="form-container__panel">
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ action }}" class="form-container__form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
|
||||
{% if post is defined and post is not null %}
|
||||
<p class="form-container__field">
|
||||
<label class="form-container__label">
|
||||
<span>Auteur</span>
|
||||
<input type="text" value="{{ post.authorUsername ?? 'inconnu' }}" disabled
|
||||
class="form-container__input form-container__input--disabled">
|
||||
</label>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="title" class="form-container__label">
|
||||
<span>Titre</span>
|
||||
<input type="text" id="title" name="title" value="{{ post.title|default('') }}" required maxlength="255"
|
||||
class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
{% if post is defined and post is not null and post.id > 0 %}
|
||||
<p class="form-container__field">
|
||||
<label for="slug" class="form-container__label">
|
||||
<span>Slug URL</span>
|
||||
<input type="text" id="slug" name="slug" value="{{ post.storedSlug }}" pattern="[a-z0-9]+(-[a-z0-9]+)*"
|
||||
maxlength="255" title="Lettres minuscules, chiffres et tirets uniquement"
|
||||
class="form-container__input">
|
||||
</label>
|
||||
<small class="form-container__hint">(URL actuelle : <a href="/article/{{ post.storedSlug }}" target="_blank">/article/{{ post.storedSlug }}</a>)</small>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="category_id" class="form-container__label">
|
||||
<span>Catégorie</span>
|
||||
<select id="category_id" name="category_id" class="form-container__select">
|
||||
<option value="">— Sans catégorie —</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}" {% if post is not null and post.categoryId==category.id %}selected{% endif %}>
|
||||
{{ category.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="editor" class="form-container__label">
|
||||
<span>Contenu</span>
|
||||
<textarea id="editor" name="content" required class="form-container__textarea">{{ post.content|default('') }}</textarea>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<div class="form-container__actions">
|
||||
<div class="form-container__action">
|
||||
<button type="submit" class="btn btn--primary btn--lg btn--full">
|
||||
{% if post is defined and post is not null and post.id > 0 %}Mettre à jour{% else %}Enregistrer{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-container__action">
|
||||
<a href="/admin/posts" class="btn btn--secondary btn--lg btn--full">Annuler</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if post is defined and post is not null and post.id > 0 %}
|
||||
<div class="form-container__footer">
|
||||
<small>
|
||||
Créé le : {{ post.createdAt|date("d/m/Y à H:i") }}<br>
|
||||
Modifié le : {{ post.updatedAt|date("d/m/Y à H:i") }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/assets/vendor/jquery.min.js"></script>
|
||||
<script src="/assets/vendor/trumbowyg/trumbowyg.min.js"></script>
|
||||
<script src="/assets/vendor/trumbowyg/langs/fr.min.js"></script>
|
||||
<script src="/assets/vendor/trumbowyg/plugins/upload/trumbowyg.upload.min.js"></script>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
$('#editor').trumbowyg({
|
||||
lang: 'fr',
|
||||
btns: [
|
||||
['viewHTML'],
|
||||
['undo', 'redo'],
|
||||
['formatting'],
|
||||
['strong', 'em', 'underline'],
|
||||
['unorderedList', 'orderedList'],
|
||||
['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
|
||||
['link'],
|
||||
['upload', 'insertImage'],
|
||||
['removeformat']
|
||||
],
|
||||
semantic: false,
|
||||
imageWidthModalEdit: true,
|
||||
plugins: {
|
||||
upload: {
|
||||
serverPath: '/admin/media/upload',
|
||||
fileFieldName: 'file',
|
||||
urlPropertyName: 'url',
|
||||
statusPropertyName: 'success'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
107
views/admin/posts/index.twig
Normal file
107
views/admin/posts/index.twig
Normal file
@@ -0,0 +1,107 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Tableau de bord – Articles{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Gestion des articles</h2>
|
||||
|
||||
{% include 'partials/_admin_nav.twig' %}
|
||||
|
||||
<p>
|
||||
<a href="/admin/posts/edit/0" class="btn btn--primary">+ Ajouter un article</a>
|
||||
</p>
|
||||
|
||||
<form method="get" action="/admin/posts" class="search-bar">
|
||||
{% if activeCategory %}
|
||||
<input type="hidden" name="categorie" value="{{ activeCategory.slug }}">
|
||||
{% endif %}
|
||||
<input type="search" name="q" value="{{ searchQuery }}"
|
||||
placeholder="Rechercher un article…" class="search-bar__input" aria-label="Recherche">
|
||||
<button type="submit" class="search-bar__btn">Rechercher</button>
|
||||
{% if searchQuery %}
|
||||
<a href="/admin/posts{% if activeCategory %}?categorie={{ activeCategory.slug }}{% endif %}" class="search-bar__reset">✕</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if searchQuery %}
|
||||
<p class="search-bar__info">
|
||||
{% if posts is not empty %}
|
||||
{{ posts|length }} résultat{{ posts|length > 1 ? 's' : '' }} pour « {{ searchQuery }} »
|
||||
{% else %}
|
||||
Aucun résultat pour « {{ searchQuery }} »
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if categories is not empty %}
|
||||
<nav class="category-filter">
|
||||
<a href="/admin/posts"
|
||||
class="category-filter__item{% if activeCategory is null %} category-filter__item--active{% endif %}">
|
||||
Tous
|
||||
</a>
|
||||
{% for category in categories %}
|
||||
<a href="/admin/posts?categorie={{ category.slug }}"
|
||||
class="category-filter__item{% if activeCategory and activeCategory.id == category.id %} category-filter__item--active{% endif %}">
|
||||
{{ category.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert--success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if posts is not empty %}
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titre</th>
|
||||
<th>Catégorie</th>
|
||||
<th>Auteur</th>
|
||||
<th>Créé le</th>
|
||||
<th>Modifié le</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for post in posts %}
|
||||
<tr>
|
||||
<td data-label="Titre"><strong>{{ post.title }}</strong></td>
|
||||
<td data-label="Catégorie">
|
||||
{% if post.categoryName %}
|
||||
<a href="/admin/posts?categorie={{ post.categorySlug }}"
|
||||
class="badge badge--category">{{ post.categoryName }}</a>
|
||||
{% else %}
|
||||
<span class="admin-table__muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-label="Auteur">{{ post.authorUsername ?? 'inconnu' }}</td>
|
||||
<td data-label="Créé le">{{ post.createdAt|date("d/m/Y H:i") }}</td>
|
||||
<td data-label="Modifié le">{{ post.updatedAt|date("d/m/Y H:i") }}</td>
|
||||
<td data-label="Actions">
|
||||
<div class="u-inline-actions">
|
||||
<a href="/admin/posts/edit/{{ post.id }}" class="btn btn--sm btn--secondary">Éditer</a>
|
||||
|
||||
<form method="post" action="/admin/posts/delete/{{ post.id }}" class="u-inline-form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
<button type="submit" class="btn btn--sm btn--danger"
|
||||
onclick="return confirm('Supprimer cet article ?')">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>{% if searchQuery %}Aucun résultat pour « {{ searchQuery }} ».{% else %}Aucun article à gérer.{% endif %}</em></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
74
views/admin/users/form.twig
Normal file
74
views/admin/users/form.twig
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Tableau de bord – Créer un utilisateur{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Créer un utilisateur</h2>
|
||||
|
||||
{% include 'partials/_admin_nav.twig' %}
|
||||
|
||||
<div class="form-container form-container--narrow">
|
||||
<div class="form-container__panel">
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/admin/users/create" class="form-container__form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="username" class="form-container__label">
|
||||
<span>Nom d'utilisateur</span>
|
||||
<input type="text" id="username" name="username" required minlength="3" maxlength="50" autofocus
|
||||
class="form-container__input">
|
||||
</label>
|
||||
<small class="form-container__hint">Minimum 3 caractères</small>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="email" class="form-container__label">
|
||||
<span>Email</span>
|
||||
<input type="email" id="email" name="email" required class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="password" class="form-container__label">
|
||||
<span>Mot de passe</span>
|
||||
<input type="password" id="password" name="password" required minlength="8"
|
||||
class="form-container__input">
|
||||
</label>
|
||||
<small class="form-container__hint">Minimum 8 caractères</small>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="password_confirm" class="form-container__label">
|
||||
<span>Confirmer le mot de passe</span>
|
||||
<input type="password" id="password_confirm" name="password_confirm" required minlength="8"
|
||||
class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="role" class="form-container__label">
|
||||
<span>Rôle</span>
|
||||
<select id="role" name="role" class="form-container__select">
|
||||
<option value="user">Utilisateur</option>
|
||||
<option value="editor">Éditeur</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<div class="form-container__actions">
|
||||
<div class="form-container__action">
|
||||
<button type="submit" class="btn btn--primary btn--lg btn--full">Créer l'utilisateur</button>
|
||||
</div>
|
||||
<div class="form-container__action">
|
||||
<a href="/admin/users" class="btn btn--secondary btn--lg btn--full">Annuler</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
95
views/admin/users/index.twig
Normal file
95
views/admin/users/index.twig
Normal file
@@ -0,0 +1,95 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Tableau de bord – Utilisateurs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Gestion des utilisateurs</h2>
|
||||
|
||||
{% include 'partials/_admin_nav.twig' %}
|
||||
|
||||
<p>
|
||||
<a href="/admin/users/create" class="btn btn--primary">+ Ajouter un utilisateur</a>
|
||||
</p>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert--success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if users is not empty %}
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom d'utilisateur</th>
|
||||
<th>Email</th>
|
||||
<th>Rôle</th>
|
||||
<th>Inscrit le</th>
|
||||
<th>Modifier le rôle</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td data-label="Nom d'utilisateur">
|
||||
<strong>{{ user.username }}</strong>
|
||||
{% if user.id == currentUserId %}
|
||||
<em class="admin-table__self">(vous)</em>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-label="Email">{{ user.email }}</td>
|
||||
<td data-label="Rôle">
|
||||
{% if user.isAdmin() %}
|
||||
<span class="badge badge--admin">Admin</span>
|
||||
{% elseif user.isEditor() %}
|
||||
<span class="badge badge--editor">Éditeur</span>
|
||||
{% else %}
|
||||
<span class="badge badge--user">Utilisateur</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-label="Inscrit le">{{ user.createdAt|date("d/m/Y") }}</td>
|
||||
<td data-label="Modifier le rôle">
|
||||
{% if not user.isAdmin() and user.id != currentUserId %}
|
||||
<form method="post" action="/admin/users/role/{{ user.id }}" class="u-inline-form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
<div class="u-inline-actions">
|
||||
<select name="role" class="admin-table__role-select">
|
||||
<option value="user" {% if user.role == 'user' %}selected{% endif %}>Utilisateur</option>
|
||||
<option value="editor" {% if user.role == 'editor' %}selected{% endif %}>Éditeur</option>
|
||||
<option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn--sm btn--secondary">Modifier</button>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="admin-table__muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
{% if not user.isAdmin() and user.id != currentUserId %}
|
||||
<div class="u-inline-actions">
|
||||
<form method="post" action="/admin/users/delete/{{ user.id }}" class="u-inline-form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
<button type="submit" class="btn btn--sm btn--danger"
|
||||
onclick="return confirm('Supprimer l\'utilisateur « {{ user.username }} » ?')">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="admin-table__muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>Aucun utilisateur.</em></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
82
views/emails/password-reset.twig
Normal file
82
views/emails/password-reset.twig
Normal file
@@ -0,0 +1,82 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Réinitialisation de mot de passe</title>
|
||||
<style>
|
||||
/* CSS inline — requis pour la compatibilité avec les clients mail */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
color: #212529;
|
||||
background: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.wrapper {
|
||||
max-width: 560px;
|
||||
margin: 40px auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 40px;
|
||||
}
|
||||
.logo {
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 24px;
|
||||
color: #212529;
|
||||
}
|
||||
p {
|
||||
line-height: 1.6;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: #007bff;
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
margin: 8px 0 24px;
|
||||
}
|
||||
.url {
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
word-break: break-all;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 32px;
|
||||
font-size: 13px;
|
||||
color: #6c757d;
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="logo">Slim Blog</div>
|
||||
|
||||
<p>Bonjour <strong>{{ username }}</strong>,</p>
|
||||
|
||||
<p>Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :</p>
|
||||
|
||||
<p>
|
||||
<a href="{{ resetUrl }}" class="btn">Réinitialiser mon mot de passe</a>
|
||||
</p>
|
||||
|
||||
<p class="url">Si le bouton ne fonctionne pas, copiez ce lien dans votre navigateur :<br>{{ resetUrl }}</p>
|
||||
|
||||
<p>Ce lien est valable <strong>{{ ttlMinutes }} minutes</strong>. Passé ce délai, vous devrez faire une nouvelle demande.</p>
|
||||
|
||||
<p>Si vous n'êtes pas à l'origine de cette demande, ignorez simplement cet email. Votre mot de passe ne sera pas modifié.</p>
|
||||
|
||||
<div class="footer">
|
||||
Cet email a été envoyé automatiquement, merci de ne pas y répondre.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
30
views/layout.twig
Normal file
30
views/layout.twig
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Slim Blog{% endblock %}</title>
|
||||
{% block meta %}
|
||||
<meta name="description" content="Un blog propulsé par Slim 4.">
|
||||
{% endblock %}
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
{% block styles %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
{% include 'partials/_header.twig' %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% include 'partials/_footer.twig' %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
60
views/pages/account/password-change.twig
Normal file
60
views/pages/account/password-change.twig
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Mon compte – Changer le mot de passe{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container form-container--narrow">
|
||||
<div class="form-container__panel">
|
||||
<div class="form-container__header">
|
||||
<h2 class="form-container__title">Changer le mot de passe</h2>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert--success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/account/password" class="form-container__form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="current_password" class="form-container__label">
|
||||
<span>Mot de passe actuel</span>
|
||||
<input type="password" id="current_password" name="current_password" required autofocus
|
||||
class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="new_password" class="form-container__label">
|
||||
<span>Nouveau mot de passe</span>
|
||||
<input type="password" id="new_password" name="new_password" required minlength="8"
|
||||
class="form-container__input">
|
||||
</label>
|
||||
<small class="form-container__hint">Minimum 8 caractères</small>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="new_password_confirm" class="form-container__label">
|
||||
<span>Confirmer le nouveau mot de passe</span>
|
||||
<input type="password" id="new_password_confirm" name="new_password_confirm" required minlength="8"
|
||||
class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<div class="form-container__actions">
|
||||
<div class="form-container__action">
|
||||
<button type="submit" class="btn btn--primary btn--lg btn--full">Mettre à jour</button>
|
||||
</div>
|
||||
<div class="form-container__action">
|
||||
<a href="{{ back_url }}" class="btn btn--secondary btn--lg btn--full">Annuler</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
50
views/pages/auth/login.twig
Normal file
50
views/pages/auth/login.twig
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Connexion – Slim Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container form-container--narrow">
|
||||
<div class="form-container__panel">
|
||||
<div class="form-container__header">
|
||||
<h2 class="form-container__title">Connexion</h2>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert--success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/auth/login" class="form-container__form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="username" class="form-container__label">
|
||||
<span>Nom d'utilisateur</span>
|
||||
<input type="text" id="username" name="username" required autofocus class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="password" class="form-container__label">
|
||||
<span>Mot de passe</span>
|
||||
<input type="password" id="password" name="password" required class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<div class="form-container__actions">
|
||||
<div class="form-container__action">
|
||||
<button type="submit" class="btn btn--primary btn--lg btn--full">Se connecter</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form-container__footer">
|
||||
<a href="/password/forgot">Mot de passe oublié ?</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
44
views/pages/auth/password-forgot.twig
Normal file
44
views/pages/auth/password-forgot.twig
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Mot de passe oublié – Slim Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container form-container--narrow">
|
||||
<div class="form-container__panel">
|
||||
<div class="form-container__header">
|
||||
<h2 class="form-container__title">Mot de passe oublié</h2>
|
||||
<p class="form-container__intro">Saisissez votre adresse email. Si elle est associée à un compte, vous recevrez un lien de réinitialisation.</p>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if success %}
|
||||
<div class="alert alert--success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/password/forgot" class="form-container__form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="email" class="form-container__label">
|
||||
<span>Adresse email</span>
|
||||
<input type="email" id="email" name="email" required autofocus class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<div class="form-container__actions">
|
||||
<div class="form-container__action">
|
||||
<button type="submit" class="btn btn--primary btn--lg btn--full">Envoyer le lien</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form-container__footer">
|
||||
<a href="/auth/login">← Retour à la connexion</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
46
views/pages/auth/password-reset.twig
Normal file
46
views/pages/auth/password-reset.twig
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Réinitialisation du mot de passe – Slim Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container form-container--narrow">
|
||||
<div class="form-container__panel">
|
||||
<div class="form-container__header">
|
||||
<h2 class="form-container__title">Nouveau mot de passe</h2>
|
||||
</div>
|
||||
|
||||
{% if error %}
|
||||
<div class="alert alert--danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/password/reset" class="form-container__form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="new_password" class="form-container__label">
|
||||
<span>Nouveau mot de passe</span>
|
||||
<input type="password" id="new_password" name="new_password"
|
||||
required minlength="8" autofocus class="form-container__input">
|
||||
</label>
|
||||
<small class="form-container__hint">Minimum 8 caractères</small>
|
||||
</p>
|
||||
|
||||
<p class="form-container__field">
|
||||
<label for="new_password_confirm" class="form-container__label">
|
||||
<span>Confirmer le mot de passe</span>
|
||||
<input type="password" id="new_password_confirm" name="new_password_confirm"
|
||||
required minlength="8" class="form-container__input">
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<div class="form-container__actions">
|
||||
<div class="form-container__action">
|
||||
<button type="submit" class="btn btn--primary btn--lg btn--full">Réinitialiser</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
11
views/pages/error.twig
Normal file
11
views/pages/error.twig
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}{{ status }} – Slim Blog{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="error-page">
|
||||
<h2 class="error-page__code">{{ status }}</h2>
|
||||
<p class="error-page__message">{{ message }}</p>
|
||||
<p><a href="/">← Retour à l'accueil</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
94
views/pages/home.twig
Normal file
94
views/pages/home.twig
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}Slim Blog{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
<meta name="description" content="{% if activeCategory %}Articles de la catégorie {{ activeCategory.name }} — {% endif %}Slim Blog, un blog propulsé par Slim 4.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Slim Blog">
|
||||
<meta property="og:description" content="Un blog propulsé par Slim 4.">
|
||||
<meta property="og:url" content="{{ app_url }}/">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form method="get" action="/" class="search-bar">
|
||||
{% if activeCategory %}
|
||||
<input type="hidden" name="categorie" value="{{ activeCategory.slug }}">
|
||||
{% endif %}
|
||||
<input type="search" name="q" value="{{ searchQuery }}"
|
||||
placeholder="Rechercher un article…" class="search-bar__input" aria-label="Recherche">
|
||||
<button type="submit" class="search-bar__btn">Rechercher</button>
|
||||
{% if searchQuery %}
|
||||
<a href="/{% if activeCategory %}?categorie={{ activeCategory.slug }}{% endif %}" class="search-bar__reset">✕</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if searchQuery %}
|
||||
<p class="search-bar__info">
|
||||
{% if posts is not empty %}
|
||||
{{ posts|length }} résultat{{ posts|length > 1 ? 's' : '' }} pour « {{ searchQuery }} »
|
||||
{% else %}
|
||||
Aucun résultat pour « {{ searchQuery }} »
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if categories is not empty %}
|
||||
<nav class="category-filter">
|
||||
<a href="/"
|
||||
class="category-filter__item{% if activeCategory is null %} category-filter__item--active{% endif %}">
|
||||
Tous
|
||||
</a>
|
||||
{% for category in categories %}
|
||||
<a href="/?categorie={{ category.slug }}"
|
||||
class="category-filter__item{% if activeCategory and activeCategory.id == category.id %} category-filter__item--active{% endif %}">
|
||||
{{ category.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-list card-list--contained">
|
||||
{% for post in posts %}
|
||||
{% set thumb = post_thumbnail(post) %}
|
||||
<article class="card">
|
||||
|
||||
<a href="{{ post_url(post) }}" class="card__thumb-link" tabindex="-1" aria-hidden="true">
|
||||
{% if thumb %}
|
||||
<img class="card__thumb" src="{{ thumb }}" alt="">
|
||||
{% else %}
|
||||
<span class="card__initials" aria-hidden="true">{{ post_initials(post) }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<div class="card__content">
|
||||
<div class="card__body">
|
||||
<h2 class="card__title">
|
||||
<a href="{{ post_url(post) }}" class="card__title-link">{{ post.title }}</a>
|
||||
</h2>
|
||||
|
||||
<div class="card__meta">
|
||||
<small>
|
||||
Publié le {{ post.createdAt|date("d/m/Y à H:i") }}
|
||||
par <strong>{{ post.authorUsername ?? 'inconnu' }}</strong>
|
||||
</small>
|
||||
{% if post.categoryName %}
|
||||
<a href="/?categorie={{ post.categorySlug }}" class="badge badge--category">{{ post.categoryName }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p class="card__excerpt">{{ post_excerpt(post) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card__actions">
|
||||
<a href="{{ post_url(post) }}" class="card__actions-link">Lire la suite →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</article>
|
||||
{% else %}
|
||||
<p>Aucun article publié{% if searchQuery %} pour « {{ searchQuery }} »{% elseif activeCategory %} dans cette catégorie{% endif %}.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
48
views/pages/post/detail.twig
Normal file
48
views/pages/post/detail.twig
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}{{ post.title }} – Slim Blog{% endblock %}
|
||||
|
||||
{% block meta %}
|
||||
{% set excerpt = post_excerpt(post, 160) %}
|
||||
{% set thumb = post_thumbnail(post) %}
|
||||
<meta name="description" content="{{ excerpt }}">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:title" content="{{ post.title }}">
|
||||
<meta property="og:description" content="{{ excerpt }}">
|
||||
<meta property="og:url" content="{{ app_url }}{{ post_url(post) }}">
|
||||
{% if thumb %}
|
||||
<meta property="og:image" content="{{ app_url }}{{ thumb }}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article class="post">
|
||||
<h1>{{ post.title }}</h1>
|
||||
|
||||
<div class="post__meta">
|
||||
<small>
|
||||
Publié le {{ post.createdAt|date("d/m/Y à H:i") }}
|
||||
par <strong>{{ post.authorUsername ?? 'inconnu' }}</strong>
|
||||
</small>
|
||||
{% if post.categoryName %}
|
||||
<a href="/?categorie={{ post.categorySlug }}" class="badge badge--category">{{ post.categoryName }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if post.updatedAt != post.createdAt %}
|
||||
<div class="post__updated">
|
||||
<small><em>Mis à jour le {{ post.updatedAt|date("d/m/Y à H:i") }}</em></small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="post__content">
|
||||
{# Le contenu est déjà sanitisé par HtmlSanitizer via PostService #}
|
||||
{{ post.content|raw }}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<p>
|
||||
<a href="/">← Retour aux articles</a>
|
||||
</p>
|
||||
</article>
|
||||
{% endblock %}
|
||||
10
views/partials/_admin_nav.twig
Normal file
10
views/partials/_admin_nav.twig
Normal file
@@ -0,0 +1,10 @@
|
||||
<nav class="admin-nav">
|
||||
<a href="/admin/posts">Articles</a>
|
||||
| <a href="/admin/media">Médias</a>
|
||||
{% if session.role is defined and session.role in ['admin', 'editor'] %}
|
||||
| <a href="/admin/categories">Catégories</a>
|
||||
{% endif %}
|
||||
{% if session.role is defined and session.role == 'admin' %}
|
||||
| <a href="/admin/users">Utilisateurs</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
11
views/partials/_footer.twig
Normal file
11
views/partials/_footer.twig
Normal file
@@ -0,0 +1,11 @@
|
||||
<footer class="site-footer">
|
||||
<p>
|
||||
© {{ "now"|date("Y") }} Slim Blog – Made with ❤️ by <a href="https://netig.net">NETig</a> –
|
||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/deed.fr" rel="license">CC BY-SA 4.0</a>
|
||||
</p>
|
||||
<nav>
|
||||
{# TODO : remplacer par les vraies URLs une fois les pages créées #}
|
||||
<a href="">À propos</a> |
|
||||
<a href="">Contact</a>
|
||||
</nav>
|
||||
</footer>
|
||||
24
views/partials/_header.twig
Normal file
24
views/partials/_header.twig
Normal file
@@ -0,0 +1,24 @@
|
||||
<header class="site-header">
|
||||
<div class="site-header__inner">
|
||||
<h1 class="site-header__logo">
|
||||
<a href="/" class="site-header__logo-link">Slim Blog</a>
|
||||
</h1>
|
||||
|
||||
<nav class="site-header__nav">
|
||||
{% if session.user_id is defined and session.user_id %}
|
||||
<span class="site-header__user">
|
||||
Connecté en tant que : <strong>{{ session.username }}</strong>
|
||||
</span>
|
||||
<a href="/admin/posts" class="site-header__action">Tableau de bord</a>
|
||||
<a href="/account/password" class="site-header__action">Mon compte</a>
|
||||
<form method="post" action="/auth/logout" class="u-inline-form">
|
||||
<input type="hidden" name="{{ csrf.keys.name }}" value="{{ csrf.name }}">
|
||||
<input type="hidden" name="{{ csrf.keys.value }}" value="{{ csrf.value }}">
|
||||
<button type="submit" class="site-header__action">Déconnexion</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="/auth/login" class="site-header__action">Connexion</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
Reference in New Issue
Block a user