first commit

This commit is contained in:
julien
2026-03-16 01:47:07 +01:00
commit 8f7e61bda0
185 changed files with 27731 additions and 0 deletions

View 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 %}

View 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
View 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 %}

View 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 %}

View 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 %}

View 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 %}