Files
folio/templates/admin.php
T
cedricAbonnel dbd76556fb feat : notion de livre — grouper des pages en série ordonnée
Ajoute un concept de "livre" (série de pages ordonnées) avec :
- BookManager : CRUD JSON dans data/books/<slug>.json
- Route /book/<slug> → page de sommaire (table des matières)
- Navigation chapitre ← → en bas de chaque article membre du livre
- Bandeau "Chapitre X/N — Nom du livre" en haut de l'article
- Admin → onglet Livres : créer, éditer, supprimer un livre, ajouter/ordonner les pages via textarea slug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:47:41 +02:00

1317 lines
67 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
ob_start();
$now = time();
function adminStatusBadge(array $a, int $now): string
{
if (!$a['published']) {
return '<span class="badge bg-secondary">Brouillon</span>';
}
if (strtotime((string)($a['published_at'] ?? '')) > $now) {
return '<span class="badge bg-warning text-dark">Avant-première</span>';
}
return '<span class="badge bg-success">Publié</span>';
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Administration</h1>
<a href="/new" class="btn btn-primary btn-sm">+ Nouvel article</a>
</div>
<!-- Onglets -->
<ul class="nav nav-tabs mb-4">
<?php if (isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'dashboard' ? 'active' : '' ?>"
href="/admin/dashboard">Tableau de bord</a>
</li>
<?php endif; ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'articles' ? 'active' : '' ?>"
href="/admin/articles"><?= isAdmin() ? 'Articles' : 'Mes articles' ?></a>
</li>
<?php if (isAdmin()): ?>
<li class="nav-item">
<a class="nav-link <?= $tab === 'users' ? 'active' : '' ?>"
href="/admin/users">Utilisateurs</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'roles' ? 'active' : '' ?>"
href="/admin/roles">Rôles</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'categories' ? 'active' : '' ?>"
href="/admin/categories">Catégories &amp; Tags</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'site' ? 'active' : '' ?>"
href="/admin/site">Site</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'comments' ? 'active' : '' ?>"
href="/admin/comments">Commentaires</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'emails' ? 'active' : '' ?>"
href="/admin/emails">Emails</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'smtp' ? 'active' : '' ?>"
href="/admin/smtp">SMTP</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'searches' ? 'active' : '' ?>"
href="/admin/searches">Recherches</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'books' ? 'active' : '' ?>"
href="/admin/books">Livres</a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'stats' ? 'active' : '' ?>"
href="/admin/stats">Statistiques</a>
</li>
<?php endif; ?>
</ul>
<!-- ─────────────────────────── DASHBOARD ─────────────────────────── -->
<?php if ($tab === 'dashboard' && isAdmin()): ?>
<div class="row g-3 mb-4">
<?php
$stats = [
['label' => 'Publiés', 'value' => $adminData['published'], 'color' => 'success'],
['label' => 'Avant-premières', 'value' => $adminData['previews'], 'color' => 'warning'],
['label' => 'Brouillons', 'value' => $adminData['drafts'], 'color' => 'secondary'],
['label' => 'Total', 'value' => $adminData['total'], 'color' => 'primary'],
];
foreach ($stats as $s): ?>
<div class="col-6 col-md-3">
<div class="card text-center">
<div class="card-body">
<div class="display-6 fw-bold text-<?= $s['color'] ?>"><?= $s['value'] ?></div>
<div class="text-muted small"><?= $s['label'] ?></div>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Version Folio ──────────────────────────────────────────────────────── -->
<?php
$_deployedVer = trim((string) @file_get_contents(BASE_PATH . '/public/version.txt'));
$_deployedLabel = $_deployedVer !== '' ? $_deployedVer : '—';
$_notices = isset($_updateChecker) ? $_updateChecker->adminNotices() : [];
$_remoteLabel = '—';
foreach ($_notices as $_n) {
if ($_n['type'] === 'info' && preg_match('/v([\d]+\.[\d]+\.[\d]+)/', $_n['message'], $_m)) {
$_remoteLabel = $_m[1];
}
}
?>
<div class="card mb-4">
<div class="card-header bg-transparent py-2 small fw-semibold">Moteur Folio</div>
<div class="card-body py-2">
<table class="table table-sm table-borderless mb-0 small">
<tbody>
<tr>
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap" style="width:160px">Version déployée</th>
<td><?= htmlspecialchars($_deployedLabel) ?></td>
</tr>
<tr>
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Dernière version disponible</th>
<td><?= htmlspecialchars($_remoteLabel) ?><?= $_remoteLabel !== '—' && $_remoteLabel !== $_deployedLabel ? ' <span class="badge bg-warning text-dark ms-1">Mise à jour disponible</span>' : '' ?></td>
</tr>
<?php if (!empty($_notices)): ?>
<tr>
<th class="text-muted fw-normal ps-0 pe-2 align-top">Actions requises</th>
<td class="d-flex flex-wrap gap-2 align-items-center">
<?php foreach ($_notices as $_n): ?>
<?php if ($_n['type'] === 'warning'): ?>
<form method="POST" action="/?action=run_content_migrations">
<button type="submit" class="btn btn-warning btn-sm">Mettre à jour le contenu</button>
</form>
<?php endif; ?>
<?php endforeach; ?>
</td>
</tr>
<?php endif; ?>
<?php if (($_GET['notice'] ?? '') === 'migrated'): ?>
<tr><td colspan="2"><div class="alert alert-success py-1 mb-0 small">Migrations appliquées avec succès.</div></td></tr>
<?php elseif (($_GET['notice'] ?? '') === 'migration_error'): ?>
<tr><td colspan="2"><div class="alert alert-danger py-1 mb-0 small">Une erreur est survenue pendant la migration.</div></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<h5>Activité récente</h5>
<table class="table table-sm table-hover">
<thead>
<tr>
<th>Titre</th>
<th>Auteur</th>
<th>Statut</th>
<th>Modifié le</th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['recent'] as $a): ?>
<tr>
<td>
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
<?= htmlspecialchars($a['title']) ?>
</a>
</td>
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '') ?></td>
<td><?= adminStatusBadge($a, $now) ?></td>
<td class="text-muted small">
<?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($a['updated_at'] ?? $a['created_at'] ?? '')))) ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- ─────────────────────────── ARTICLES ─────────────────────────── -->
<?php elseif ($tab === 'articles'): ?>
<!-- Filtres -->
<form class="row g-2 align-items-center mb-3" method="get" action="/admin/articles">
<?php if (isAdmin() && !empty($adminData['filter_authors'])): ?>
<div class="col-auto">
<select name="filter_author" class="form-select form-select-sm">
<option value="">Tous les auteurs</option>
<?php foreach ($adminData['filter_authors'] as $_fa): ?>
<option value="<?= htmlspecialchars($_fa) ?>"
<?= ($adminData['filter_author'] ?? '') === $_fa ? 'selected' : '' ?>>
<?= htmlspecialchars($_fa) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<?php if (!empty($adminData['filter_categories'])): ?>
<div class="col-auto">
<select name="filter_category" class="form-select form-select-sm">
<option value="">Toutes les catégories</option>
<?php foreach ($adminData['filter_categories'] as $_fc): ?>
<option value="<?= htmlspecialchars($_fc) ?>"
<?= ($adminData['filter_category'] ?? '') === $_fc ? 'selected' : '' ?>>
<?= htmlspecialchars($_fc) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-auto">
<select name="filter_status" class="form-select form-select-sm">
<option value="">Tous les statuts</option>
<option value="published" <?= ($adminData['filter_status'] ?? '') === 'published' ? 'selected' : '' ?>>Publié</option>
<option value="draft" <?= ($adminData['filter_status'] ?? '') === 'draft' ? 'selected' : '' ?>>Brouillon</option>
<option value="preview" <?= ($adminData['filter_status'] ?? '') === 'preview' ? 'selected' : '' ?>>Avant-première</option>
</select>
</div>
<div class="col-auto d-flex gap-2">
<button type="submit" class="btn btn-secondary btn-sm">Filtrer</button>
<?php $hasFilter = ($adminData['filter_author'] ?? '') !== '' || ($adminData['filter_category'] ?? '') !== '' || ($adminData['filter_status'] ?? '') !== ''; ?>
<?php if ($hasFilter): ?>
<a href="/admin/articles" class="btn btn-link btn-sm p-0">Réinitialiser</a>
<?php endif; ?>
</div>
<?php if ($hasFilter): ?>
<div class="col-auto">
<span class="text-muted small"><?= count($adminData['articles']) ?> résultat(s)</span>
</div>
<?php endif; ?>
</form>
<?php if (empty($adminData['articles'])): ?>
<p class="text-muted">Aucun article.</p>
<?php else: ?>
<form method="post" action="/?action=admin_bulk_delete" id="bulk-form">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="form-check mb-0">
<input class="form-check-input" type="checkbox" id="check-all">
<label class="form-check-label small text-muted" for="check-all">Tout sélectionner</label>
</div>
<button type="submit" class="btn btn-danger btn-sm"
onclick="return document.querySelectorAll('.bulk-check:checked').length > 0 && confirm('Supprimer les articles sélectionnés ? Cette action est irréversible.')">
Supprimer la sélection
</button>
</div>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th style="width:2rem"></th>
<th>Titre</th>
<?php if (isAdmin()): ?><th>Auteur</th><?php endif; ?>
<th>Catégorie</th>
<th>Statut</th>
<th>Date</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['articles'] as $a): ?>
<tr>
<td>
<input class="form-check-input bulk-check" type="checkbox"
name="uuids[]" value="<?= htmlspecialchars($a['uuid']) ?>">
</td>
<td>
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
<?= htmlspecialchars($a['title']) ?>
</a>
</td>
<?php if (isAdmin()): ?>
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '') ?></td>
<?php endif; ?>
<td class="text-muted small"><?= htmlspecialchars($a['category'] ?? '') ?></td>
<td><?= adminStatusBadge($a, $now) ?></td>
<td class="text-muted small text-nowrap">
<?= htmlspecialchars(date('d/m/Y', strtotime((string)($a['published_at'] ?? $a['created_at'] ?? '')))) ?>
</td>
<td class="text-end text-nowrap">
<a href="/edit/<?= htmlspecialchars($a['uuid']) ?>"
class="btn btn-outline-secondary btn-sm">Modifier</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</form>
<script src="/assets/js/admin.js" defer></script>
<?php endif; ?>
<!-- ─────────────────────────── UTILISATEURS ─────────────────────────── -->
<?php elseif ($tab === 'users' && isAdmin()): ?>
<?php if (($_GET['error'] ?? '') === 'last_admin'): ?>
<div class="alert alert-danger py-2 mb-3">
Impossible de retirer le rôle Administrateur : il doit rester au moins un administrateur.
</div>
<?php endif; ?>
<!-- Ajouter / attribuer un rôle -->
<div class="card mb-4">
<div class="card-header">Attribuer un rôle</div>
<div class="card-body">
<form method="post" action="/?action=admin_grant_role" class="row g-2 align-items-end">
<div class="col-md-5">
<label class="form-label small">Email</label>
<input type="email" name="email" class="form-control form-control-sm"
placeholder="utilisateur@exemple.fr" required>
</div>
<div class="col-md-4">
<label class="form-label small">Rôle</label>
<select name="role" class="form-select form-select-sm" required>
<?php foreach ($adminData['roles'] as $r): ?>
<option value="<?= htmlspecialchars($r['name']) ?>">
<?= htmlspecialchars($r['label']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary btn-sm w-100">Attribuer</button>
</div>
</form>
</div>
</div>
<!-- Liste des utilisateurs -->
<?php if (empty($adminData['users'])): ?>
<p class="text-muted">Aucun utilisateur.</p>
<?php else: ?>
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Email</th>
<th>Statut</th>
<th>Rôles</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['users'] as $u): ?>
<tr>
<td class="small"><?= htmlspecialchars($u['email']) ?></td>
<td>
<?php if ($u['is_active'] === null): ?>
<span class="badge bg-light text-muted">Pré-inscrit</span>
<?php elseif ($u['is_active']): ?>
<span class="badge bg-success">Actif</span>
<?php else: ?>
<span class="badge bg-danger">Inactif</span>
<?php endif; ?>
</td>
<td>
<?php foreach ($u['roles'] as $role): ?>
<span class="badge bg-primary me-1"><?= htmlspecialchars($role['label']) ?></span>
<form method="post" action="/?action=admin_revoke_role"
class="d-inline"
data-confirm="Retirer le rôle «<?= htmlspecialchars($role['label']) ?>» à <?= htmlspecialchars($u['email']) ?> ?">
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
<input type="hidden" name="role" value="<?= htmlspecialchars($role['name']) ?>">
<button type="submit" class="btn btn-link btn-sm p-0 text-danger" title="Retirer">×</button>
</form>
<?php endforeach; ?>
<?php if (empty($u['roles'])): ?>
<span class="text-muted small"></span>
<?php endif; ?>
</td>
<td class="text-end">
<!-- Ajout rapide d'un rôle existant -->
<?php
$currentRoleNames = array_column($u['roles'], 'name');
$missing = array_filter($adminData['roles'], fn ($r) => !in_array($r['name'], $currentRoleNames, true));
?>
<?php if ($missing): ?>
<form method="post" action="/?action=admin_grant_role" class="d-inline-flex gap-1">
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
<select name="role" class="form-select form-select-sm" style="width:auto">
<?php foreach ($missing as $r): ?>
<option value="<?= htmlspecialchars($r['name']) ?>">
<?= htmlspecialchars($r['label']) ?>
</option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-outline-primary btn-sm">+</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<!-- ─────────────────────────── RÔLES ─────────────────────────── -->
<?php elseif ($tab === 'roles' && isAdmin()): ?>
<div class="row g-4">
<!-- Tableau des rôles existants -->
<div class="col-lg-8">
<?php if (empty($adminData['roles'])): ?>
<p class="text-muted">Aucun rôle défini.</p>
<?php else: ?>
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Rôle</th>
<th class="text-center" style="width:100px">Utilisateurs</th>
<th style="width:110px"></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['roles'] as $r): ?>
<tr>
<td>
<?= htmlspecialchars($r['label']) ?>
<?php if ($r['name'] !== 'admin'): ?>
<div class="text-muted small">
<?php
$capLabels = array_map(
fn ($c) => KNOWN_CAPABILITIES[$c] ?? $c,
$r['capabilities']
);
echo htmlspecialchars(implode(', ', $capLabels) ?: '');
?>
</div>
<?php else: ?>
<div class="text-muted small">Toutes les permissions</div>
<?php endif; ?>
</td>
<td class="text-center">
<span class="badge bg-secondary"><?= (int)$r['user_count'] ?></span>
</td>
<td class="text-end d-flex gap-1 justify-content-end">
<a href="/admin/role/<?= rawurlencode($r['name']) ?>"
class="btn btn-outline-secondary btn-sm">Éditer</a>
<?php if ((int)$r['user_count'] === 0 && $r['name'] !== 'admin'): ?>
<form method="post" action="/?action=admin_delete_role"
data-confirm="Supprimer le rôle «<?= htmlspecialchars($r['name']) ?>» ?">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">×</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Créer un rôle -->
<div class="col-lg-4">
<div class="card">
<div class="card-header">Nouveau rôle</div>
<div class="card-body">
<form method="post" action="/?action=admin_create_role">
<div class="mb-3">
<label for="role-label" class="form-label small fw-semibold">Nom du rôle</label>
<input type="text" id="role-label" name="label" class="form-control form-control-sm"
placeholder="ex : Modérateur" required autocomplete="off">
</div>
<input type="hidden" id="role-name" name="name">
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
</form>
</div>
</div>
</div>
</div>
<?php elseif ($tab === 'site' && isAdmin()): ?>
<?php if (!empty($siteSettingsSaved)): ?>
<div class="alert alert-success py-2 mb-3">Paramètres enregistrés.</div>
<?php elseif (!empty($siteSettingsError)): ?>
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
<?php endif; ?>
<div class="card" style="max-width:540px">
<div class="card-header">Paramètres du site</div>
<div class="card-body">
<form method="post" action="/?action=admin_save_site">
<div class="mb-3">
<label for="site-title" class="form-label small fw-semibold">Titre du site</label>
<input type="text" id="site-title" name="site_title"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteTitle()) ?>"
required maxlength="80">
<div class="form-text">Affiché dans la barre de navigation et les onglets.</div>
</div>
<div class="mb-3">
<label for="site-claim" class="form-label small fw-semibold">Claim / accroche</label>
<input type="text" id="site-claim" name="site_claim"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteClaim()) ?>"
required maxlength="200">
<div class="form-text">Affiché sous le titre dans la navbar et dans le pied de page.</div>
</div>
<div class="mb-3">
<label for="site-lang" class="form-label small fw-semibold">Langue du site</label>
<input type="text" id="site-lang" name="site_lang"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteLang()) ?>"
maxlength="20" placeholder="ex : fr-FR">
<div class="form-text">Format BCP 47 (ex&nbsp;: <code>fr</code>, <code>fr-FR</code>). Utilisé dans <code>&lt;html lang&gt;</code>, og:locale, RSS et JSON-LD.</div>
</div>
<div class="mb-3">
<label for="posts-per-page" class="form-label small fw-semibold">Articles par page</label>
<input type="number" id="posts-per-page" name="posts_per_page"
class="form-control form-control-sm"
value="<?= postsPerPage() ?>"
min="1" max="100">
</div>
<div class="mb-3">
<label for="site-license-label" class="form-label small fw-semibold">Licence (libellé)</label>
<input type="text" id="site-license-label" name="site_license_label"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteLicenseLabel()) ?>"
maxlength="80" placeholder="ex : CC BY 4.0">
</div>
<div class="mb-3">
<label for="site-license-url" class="form-label small fw-semibold">Licence (URL)</label>
<input type="url" id="site-license-url" name="site_license_url"
class="form-control form-control-sm"
value="<?= htmlspecialchars(siteLicenseUrl()) ?>"
maxlength="200">
<div class="form-text">Affiché dans le footer.</div>
</div>
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
</form>
</div>
</div>
<?php endif; ?>
<!-- ─────────────────────────── CATÉGORIES & TAGS ─────────────────── -->
<?php if ($tab === 'categories'): ?>
<?php
$_cats = $adminData['cats'] ?? [];
$_privateCats = $adminData['privateCats'] ?? [];
$_tagTypes = $adminData['tagTypes'] ?? [];
?>
<div class="row g-4 mb-5">
<!-- Liste des catégories -->
<div class="col-lg-9">
<h5 class="mb-3">Catégories existantes</h5>
<?php if (empty($_cats)): ?>
<p class="text-muted">Aucune catégorie définie.</p>
<?php else: ?>
<div class="d-flex flex-column gap-2">
<?php foreach ($_cats as $_cat => $_count):
$_gradient = coverGradient($_cat, $_cats); ?>
<div class="card">
<div class="card-body py-2 px-3 d-flex align-items-center gap-3">
<div style="width:40px;height:40px;border-radius:8px;flex-shrink:0;background:<?= htmlspecialchars($_gradient) ?>"></div>
<div style="min-width:140px">
<strong><?= htmlspecialchars($_cat) ?></strong>
<small class="text-muted ms-2"><?= $_count ?> article<?= $_count > 1 ? 's' : '' ?></small>
</div>
<form method="POST" action="/?action=rename_category"
class="d-flex align-items-center gap-2 flex-grow-1"
data-confirm="Renommer « <?= htmlspecialchars($_cat) ?> » ?">
<input type="hidden" name="old" value="<?= htmlspecialchars($_cat) ?>">
<input type="text" name="new" class="form-control form-control-sm"
placeholder="Nouveau nom" required>
<button type="submit" class="btn btn-outline-primary btn-sm text-nowrap">Renommer</button>
</form>
<?php $_isPriv = in_array($_cat, $_privateCats, true); ?>
<form method="POST" action="/?action=toggle_private_category">
<input type="hidden" name="category" value="<?= htmlspecialchars($_cat) ?>">
<button type="submit"
class="btn btn-sm text-nowrap <?= $_isPriv ? 'btn-secondary' : 'btn-outline-secondary' ?>">
🔒 <?= $_isPriv ? 'Privée' : 'Publique' ?>
</button>
</form>
<form method="POST" action="/?action=delete_category"
data-confirm="Retirer la catégorie « <?= htmlspecialchars($_cat) ?> » de tous les articles ?">
<input type="hidden" name="category" value="<?= htmlspecialchars($_cat) ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">Supprimer</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Palette -->
<div class="col-lg-3">
<h5 class="mb-3">Prochaine couleur</h5>
<div class="card">
<div class="card-body">
<p class="text-muted small mb-3">
La prochaine catégorie reçoit la couleur n°<?= count($_cats) % 16 + 1 ?>.
</p>
<div class="d-flex flex-wrap gap-2">
<?php
$_nextIdx = count($_cats) % 16;
foreach (COLOR_PALETTE_16 as $_i => $_rgb):
$_g = _paletteGradient($_rgb, 0);
$_active = $_i === $_nextIdx;
?>
<div title="Couleur <?= $_i + 1 ?>"
style="width:28px;height:28px;border-radius:6px;background:<?= htmlspecialchars($_g) ?>;
<?= $_active ? 'outline:2px solid #0d6efd;outline-offset:2px' : 'opacity:.75' ?>">
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<?php if (isAdmin()): ?>
<hr class="my-4">
<div class="row g-4">
<div class="col-lg-9">
<h5 class="mb-3">Types de tags</h5>
<?php if (empty($_tagTypes)): ?>
<p class="text-muted">Aucun type de tag défini.</p>
<?php else: ?>
<div class="d-flex flex-column gap-2">
<?php foreach ($_tagTypes as $_key => $_label): ?>
<div class="card">
<div class="card-body py-2 px-3 d-flex align-items-center gap-3">
<code class="text-muted small" style="min-width:8rem"><?= htmlspecialchars($_key) ?></code>
<strong class="flex-grow-1"><?= htmlspecialchars($_label) ?></strong>
<form method="POST" action="/?action=delete_tag_type"
data-confirm="Supprimer le type «<?= htmlspecialchars($_label) ?>» ? Les tags des articles ne sont pas supprimés.">
<input type="hidden" name="type_key" value="<?= htmlspecialchars($_key) ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">Supprimer</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="col-lg-3">
<h5 class="mb-3">Nouveau type</h5>
<div class="card">
<div class="card-body">
<form method="POST" action="/?action=create_tag_type">
<div class="mb-2">
<label class="form-label small fw-semibold">Identifiant</label>
<input type="text" name="type_key" class="form-control form-control-sm font-monospace"
placeholder="ex : logiciels" required
pattern="[a-z0-9_]+" title="Minuscules, chiffres et _ uniquement">
<div class="form-text">Minuscules, chiffres, _</div>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Libellé</label>
<input type="text" name="type_label" class="form-control form-control-sm"
placeholder="ex : Logiciels" required>
</div>
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
<script src="/assets/js/admin.js" defer></script>
<?php endif; ?>
<!-- ─────────────────────────── SMTP ─────────────────────────────── -->
<?php if ($tab === 'smtp' && isAdmin()): ?>
<?php $sc = $adminData['smtp_config']; ?>
<?php if (isset($_GET['saved'])): ?>
<div class="alert alert-success py-2 mb-3">Paramètres SMTP enregistrés.</div>
<?php elseif (($_GET['error'] ?? '') === 'write'): ?>
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
<?php endif; ?>
<div class="row g-4">
<!-- Formulaire config + actions -->
<div class="col-lg-5">
<div class="card">
<div class="card-header">Configuration SMTP</div>
<div class="card-body">
<form method="post" id="smtp-config-form">
<div class="mb-3">
<label for="smtp-host" class="form-label small fw-semibold">Serveur</label>
<input type="text" id="smtp-host" name="smtp_host"
class="form-control form-control-sm font-monospace"
value="<?= htmlspecialchars($sc['host']) ?>"
placeholder="smtp.exemple.fr">
</div>
<div class="row g-2 mb-3">
<div class="col-5">
<label for="smtp-port" class="form-label small fw-semibold">Port</label>
<input type="number" id="smtp-port" name="smtp_port"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['port']) ?>"
placeholder="587" min="1" max="65535">
</div>
<div class="col-7">
<label for="smtp-secure" class="form-label small fw-semibold">Chiffrement</label>
<select id="smtp-secure" name="smtp_secure" class="form-select form-select-sm">
<option value="" <?= $sc['secure'] === '' ? 'selected' : '' ?>>Aucun</option>
<option value="tls" <?= strtolower($sc['secure']) === 'tls' ? 'selected' : '' ?>>STARTTLS (587)</option>
<option value="ssl" <?= strtolower($sc['secure']) === 'ssl' ? 'selected' : '' ?>>SSL/TLS (465)</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="smtp-user" class="form-label small fw-semibold">Utilisateur</label>
<input type="text" id="smtp-user" name="smtp_user"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['user']) ?>"
placeholder="user@exemple.fr"
autocomplete="off">
</div>
<div class="mb-3">
<label for="smtp-pass" class="form-label small fw-semibold">Mot de passe</label>
<input type="password" id="smtp-pass" name="smtp_pass"
class="form-control form-control-sm"
placeholder="<?= $sc['has_pass'] ? '(inchangé si vide)' : '' ?>"
autocomplete="new-password">
<?php if ($sc['has_pass']): ?>
<div class="form-text">Laisser vide pour conserver le mot de passe actuel.</div>
<?php endif; ?>
</div>
<div class="mb-3">
<label for="smtp-from" class="form-label small fw-semibold">Email expéditeur</label>
<input type="email" id="smtp-from" name="smtp_from"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['from']) ?>"
placeholder="no-reply@exemple.fr">
</div>
<div class="mb-3">
<label for="smtp-from-name" class="form-label small fw-semibold">Nom expéditeur</label>
<input type="text" id="smtp-from-name" name="smtp_from_name"
class="form-control form-control-sm"
value="<?= htmlspecialchars($sc['from_name']) ?>"
placeholder="Mon Site">
</div>
<div class="d-flex flex-wrap gap-2">
<button type="submit" formaction="/?action=admin_smtp_save"
class="btn btn-primary btn-sm" id="smtp-save-btn">
Enregistrer
</button>
<button type="submit" formaction="/?action=admin_smtp_test"
name="mode" value="connect"
class="btn btn-outline-secondary btn-sm">
Tester la connexion
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Envoi de test + résultats -->
<div class="col-lg-7">
<div class="card mb-3">
<div class="card-body">
<form method="post" action="/?action=admin_smtp_test" id="smtp-test-form">
<input type="hidden" name="mode" value="send">
<label for="smtp-email" class="form-label small fw-semibold">Envoyer un email de test</label>
<div class="d-flex gap-2">
<input type="email" id="smtp-email" name="test_email"
class="form-control form-control-sm"
placeholder="destinataire@exemple.fr"
value="<?= htmlspecialchars($adminData['smtp_test']['email'] ?? '') ?>"
required>
<button type="submit" class="btn btn-outline-secondary btn-sm text-nowrap" id="smtp-send-btn">
Envoyer
</button>
</div>
</form>
</div>
</div>
<?php if (isset($adminData['smtp_test'])): ?>
<?php $st = $adminData['smtp_test']; ?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
<?php if ($st['success']): ?>
<span class="text-success fw-semibold">✓ Succès</span>
<?php else: ?>
<span class="text-danger fw-semibold">✗ Échec</span>
<?php endif; ?>
<?= $st['mode'] === 'send'
? 'Envoi vers ' . htmlspecialchars($st['email'])
: 'Test de connexion' ?>
</span>
<span class="text-muted small"><?= htmlspecialchars($st['ts']) ?></span>
</div>
<?php if (!$st['success'] && $st['error'] !== ''): ?>
<div class="alert alert-danger mb-0 rounded-0 border-0 border-bottom py-2 px-3 small">
<?= htmlspecialchars($st['error']) ?>
</div>
<?php endif; ?>
<?php if (!empty($st['logs'])): ?>
<pre class="p-3 mb-0 small" style="max-height:420px;overflow-y:auto;font-size:0.75rem;background:var(--vl-code-bg,#f8f9fa);border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)"><?php
foreach ($st['logs'] as $line) {
echo htmlspecialchars($line) . "\n";
}
?></pre>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<script src="/assets/js/admin.js" defer></script>
<?php endif; ?>
<!-- ─────────────────────────── EMAILS ───────────────────────────── -->
<?php if ($tab === 'emails' && isAdmin()): ?>
<?php
$emlFilter = $adminData['eml_filter'] ?? '';
$emlCounts = $adminData['eml_counts'] ?? ['all' => 0, 'sent' => 0, 'error' => 0, 'queued' => 0];
$emlFilters = [
'' => ['label' => 'Tous', 'count' => $emlCounts['all']],
'sent' => ['label' => 'Envoyés', 'count' => $emlCounts['sent']],
'error' => ['label' => 'Erreurs', 'count' => $emlCounts['error']],
'queued' => ['label' => 'En file', 'count' => $emlCounts['queued']],
];
?>
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
<h5 class="mb-0">Logs emails
<span class="badge bg-secondary ms-1"><?= (int)$emlCounts['all'] ?></span>
</h5>
<div class="btn-group btn-group-sm" role="group">
<?php foreach ($emlFilters as $fVal => $fInfo): ?>
<a href="/admin/emails<?= $fVal !== '' ? '?filter=' . rawurlencode($fVal) : '' ?>"
class="btn <?= $emlFilter === $fVal ? 'btn-primary' : 'btn-outline-secondary' ?>">
<?= htmlspecialchars($fInfo['label']) ?>
<span class="badge <?= $emlFilter === $fVal ? 'bg-light text-primary' : 'bg-secondary' ?> ms-1"><?= (int)$fInfo['count'] ?></span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php if (empty($adminData['emails'])): ?>
<p class="text-muted">Aucun email enregistré.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Date</th>
<th>Destinataire</th>
<th>Sujet</th>
<th>Statut</th>
<th>Contenu</th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['emails'] as $em): ?>
<?php
$emDate = date('d/m/Y H:i', strtotime((string)$em['created_at']));
$emSentAt = $em['sent_at'] ? date('d/m/Y H:i', strtotime((string)$em['sent_at'])) : null;
if ($em['status'] === 'sent') {
$emBadge = '<span class="badge bg-success">Envoyé' . ($emSentAt ? ' ' . $emSentAt : '') . '</span>';
} elseif ($em['status'] === 'error') {
$emBadge = '<span class="badge bg-danger" title="' . htmlspecialchars((string)($em['error_message'] ?? '')) . '">Erreur</span>';
} else {
$emBadge = '<span class="badge bg-warning text-dark">En file</span>';
}
?>
<tr>
<td class="text-muted small text-nowrap"><?= $emDate ?></td>
<td class="small"><?= htmlspecialchars((string)$em['to_email']) ?></td>
<td class="small"><?= htmlspecialchars((string)$em['subject']) ?></td>
<td><?= $emBadge ?></td>
<td>
<details>
<summary class="btn btn-outline-secondary btn-sm" style="display:inline;cursor:pointer">Voir</summary>
<div class="mt-2 p-2 border rounded bg-light" style="max-width:600px">
<?php if (!empty($em['error_message'])): ?>
<p class="text-danger small mb-2"><strong>Erreur :</strong> <?= htmlspecialchars((string)$em['error_message']) ?></p>
<?php endif; ?>
<?php if (!empty($em['content_text'])): ?>
<pre class="mb-0 small" style="white-space:pre-wrap;font-size:0.75rem"><?= htmlspecialchars((string)$em['content_text']) ?></pre>
<?php endif; ?>
</div>
</details>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($adminData['eml_page'] > 0 || count($adminData['emails']) === 50): ?>
<nav class="d-flex gap-2 mt-3">
<?php if ($adminData['eml_page'] > 0): ?>
<a href="/admin/emails?<?= $emlFilter ? 'filter=' . rawurlencode($emlFilter) . '&' : '' ?>page=<?= $adminData['eml_page'] - 1 ?>"
class="btn btn-outline-secondary btn-sm">← Plus récents</a>
<?php endif; ?>
<?php if (count($adminData['emails']) === 50): ?>
<a href="/admin/emails?<?= $emlFilter ? 'filter=' . rawurlencode($emlFilter) . '&' : '' ?>page=<?= $adminData['eml_page'] + 1 ?>"
class="btn btn-outline-secondary btn-sm ms-auto">Plus anciens →</a>
<?php endif; ?>
</nav>
<?php endif; ?>
<?php endif; ?>
<?php endif; ?>
<!-- ─────────────────────────── COMMENTAIRES ──────────────────────── -->
<?php if ($tab === 'comments' && isAdmin()): ?>
<?php
$cmtFilter = $adminData['cmt_filter_status'] ?? '';
$cmtCounts = $adminData['cmt_counts'] ?? ['all' => 0, 'pending' => 0, 'verified' => 0, 'hidden' => 0];
$cmtFilters = [
'' => ['label' => 'Tous', 'count' => $cmtCounts['all']],
'pending' => ['label' => 'En attente', 'count' => $cmtCounts['pending']],
'verified' => ['label' => 'Vérifiés', 'count' => $cmtCounts['verified']],
'hidden' => ['label' => 'Masqués', 'count' => $cmtCounts['hidden']],
];
?>
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
<h5 class="mb-0">Commentaires
<span class="badge bg-secondary ms-1"><?= count($adminData['comments']) ?></span>
</h5>
<div class="btn-group btn-group-sm" role="group" aria-label="Filtres commentaires">
<?php foreach ($cmtFilters as $fVal => $fInfo): ?>
<a href="/admin/comments<?= $fVal !== '' ? '?filter_status=' . rawurlencode($fVal) : '' ?>"
class="btn <?= $cmtFilter === $fVal ? 'btn-primary' : 'btn-outline-secondary' ?>">
<?= htmlspecialchars($fInfo['label']) ?>
<span class="badge <?= $cmtFilter === $fVal ? 'bg-light text-primary' : 'bg-secondary' ?> ms-1"><?= (int)$fInfo['count'] ?></span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php if (empty($adminData['comments'])): ?>
<p class="text-muted">Aucun commentaire pour ce filtre.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th>Article</th>
<th>Auteur</th>
<th>Commentaire</th>
<th>Date</th>
<th>Email</th>
<th>Statut</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($adminData['comments'] as $c): ?>
<?php
$cSlug = $adminData['articleSlugs'][$c['article_uuid']] ?? null;
$cDate = date('d/m/Y H:i', strtotime((string)$c['created_at']));
$cVerified = (bool)($c['verified'] === true || $c['verified'] === 't' || $c['verified'] === '1');
$cPublished = (bool)($c['published'] === true || $c['published'] === 't' || $c['published'] === '1');
// Badge statut email
$mailStatus = $c['mail_status'] ?? null;
if ($mailStatus === 'sent') {
$mailBadge = '<span class="badge bg-success" title="Envoyé le ' . htmlspecialchars((string)($c['mail_sent_at'] ?? '')) . '">Envoyé</span>';
} elseif ($mailStatus === 'error') {
$mailBadge = '<span class="badge bg-danger" title="' . htmlspecialchars((string)($c['mail_error'] ?? '')) . '">Erreur</span>';
} elseif ($mailStatus === 'queued') {
$mailBadge = '<span class="badge bg-warning text-dark">En file</span>';
} else {
$mailBadge = '<span class="badge bg-light text-muted border">-</span>';
}
?>
<tr>
<td class="small" style="max-width:18ch">
<?php if ($cSlug): ?>
<a href="/post/<?= rawurlencode($cSlug) ?>#comments"
title="<?= htmlspecialchars($cSlug) ?>">
<?= htmlspecialchars(mb_strimwidth($cSlug, 0, 28, '…')) ?>
</a>
<?php else: ?>
<span class="text-muted"><?= htmlspecialchars(substr($c['article_uuid'], 0, 8)) ?>…</span>
<?php endif; ?>
</td>
<td class="small">
<div><?= htmlspecialchars($c['author_name']) ?></div>
<div class="text-muted" style="font-size:.8em"><?= htmlspecialchars($c['author_email']) ?></div>
</td>
<td class="small" style="max-width:28ch">
<span title="<?= htmlspecialchars($c['content']) ?>">
<?= htmlspecialchars(mb_strimwidth($c['content'], 0, 100, '…')) ?>
</span>
<?php if (!empty($c['verification_code'])): ?>
<br><span class="text-muted" style="font-size:.8em">Code : <?= htmlspecialchars($c['verification_code']) ?></span>
<?php endif; ?>
</td>
<td class="text-muted small text-nowrap"><?= htmlspecialchars($cDate) ?></td>
<td><?= $mailBadge ?></td>
<td>
<?php if (!$cVerified): ?>
<span class="badge bg-warning text-dark">En attente</span>
<?php else: ?>
<span class="badge bg-success">Vérifié</span>
<?php if ($cPublished): ?>
<span class="badge bg-primary">Publié</span>
<?php else: ?>
<span class="badge bg-secondary">Masqué</span>
<?php endif; ?>
<?php endif; ?>
</td>
<td class="text-nowrap">
<?php if (!$cVerified): ?>
<form method="post" action="/?action=comment_resend" class="d-inline">
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline-info">Renvoyer</button>
</form>
<?php elseif ($cPublished): ?>
<form method="post" action="/comment-moderate" class="d-inline">
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<input type="hidden" name="pub" value="0">
<button type="submit" class="btn btn-sm btn-outline-danger">Masquer</button>
</form>
<?php else: ?>
<form method="post" action="/comment-moderate" class="d-inline">
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<input type="hidden" name="pub" value="1">
<button type="submit" class="btn btn-sm btn-outline-success">Publier</button>
</form>
<?php endif; ?>
<form method="post" action="/?action=comment_delete" class="d-inline"
data-confirm="Supprimer ce commentaire définitivement ?">
<input type="hidden" name="id" value="<?= (int)$c['id'] ?>">
<button type="submit" class="btn btn-sm btn-danger">✕</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<script src="/assets/js/admin.js" defer></script>
<?php endif; ?>
<!-- ─────────────────────────── RECHERCHES ─────────────────────────── -->
<?php if ($tab === 'searches' && isAdmin()): ?>
<?php if (isset($_GET['saved'])): ?>
<div class="alert alert-success py-2 mb-3">Configuration enregistrée.</div>
<?php elseif (($_GET['error'] ?? '') === 'write'): ?>
<div class="alert alert-danger py-2 mb-3">Impossible d'enregistrer : le fichier n'est pas accessible en écriture.</div>
<?php endif; ?>
<div class="card mb-4" style="max-width:540px">
<div class="card-header bg-transparent py-2 small fw-semibold">Configuration des logs</div>
<div class="card-body py-3">
<form method="post" action="/?action=admin_save_searches_config">
<div class="mb-3">
<label for="apache-access-log" class="form-label small fw-semibold">Pattern des logs d'accès</label>
<input type="text" id="apache-access-log" name="apache_access_log"
class="form-control form-control-sm font-monospace"
value="<?= htmlspecialchars(apacheAccessLog()) ?>"
maxlength="200" placeholder="ex : *-access.log">
<div class="form-text">Pattern glob dans <code>/var/log/apache2/</code>. Les rotations (<code>.gz</code>, <code>.tar.gz</code>) sont automatiquement incluses.</div>
</div>
<button type="submit" class="btn btn-primary btn-sm">Enregistrer</button>
</form>
</div>
</div>
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
<h5 class="mb-0">Termes recherchés
<span class="badge bg-secondary ms-1"><?= count($adminData['search_terms'] ?? []) ?></span>
</h5>
<span class="text-muted small">Derniers 14 jours de logs · cache 10 min</span>
</div>
<?php if (!($adminData['search_log_readable'] ?? false)): ?>
<div class="alert alert-warning py-2">
Les logs Apache ne sont pas lisibles par PHP. Vérifiez que <code>www-data</code> appartient au groupe <code>adm</code>.
</div>
<?php elseif (empty($adminData['search_terms'])): ?>
<p class="text-muted">Aucune recherche trouvée dans les logs.</p>
<?php else: ?>
<?php
$terms = $adminData['search_terms'];
$maxCount = max($terms);
$rank = 0;
?>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle">
<thead>
<tr>
<th style="width:3rem">#</th>
<th>Terme recherché</th>
<th style="width:6rem" class="text-end">Fois</th>
<th style="width:12rem"></th>
</tr>
</thead>
<tbody>
<?php foreach ($terms as $term => $count): ?>
<?php $rank++;
$pct = round($count / $maxCount * 100); ?>
<tr>
<td class="text-muted small"><?= $rank ?></td>
<td>
<a href="/search?q=<?= rawurlencode($term) ?>"
class="text-decoration-none fw-medium">
<?= htmlspecialchars($term) ?>
</a>
</td>
<td class="text-end fw-semibold"><?= $count ?></td>
<td>
<div class="progress" style="height:6px">
<div class="progress-bar" style="width:<?= $pct ?>%"></div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
<!-- ─────────────────────────── LIVRES ─────────────────────────── -->
<?php if ($tab === 'books' && isAdmin()): ?>
<?php if (($_GET['deleted'] ?? '') === '1'): ?>
<div class="alert alert-success py-2 small mb-3">Livre supprimé.</div>
<?php endif; ?>
<div class="row g-4">
<!-- Liste des livres -->
<div class="col-md-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
Livres
<span class="badge bg-secondary ms-1"><?= count($adminData['books']) ?></span>
</h5>
<a href="/admin/books?new=1" class="btn btn-sm btn-primary">+ Nouveau</a>
</div>
<?php if (empty($adminData['books'])): ?>
<p class="text-muted small">Aucun livre pour l'instant.</p>
<?php else: ?>
<div class="list-group">
<?php foreach ($adminData['books'] as $bk):
$isEdited = ($adminData['edit_book']['slug'] ?? '') === $bk['slug'];
?>
<a href="/admin/books?edit=<?= rawurlencode($bk['slug']) ?>"
class="list-group-item list-group-item-action<?= $isEdited ? ' active' : '' ?>">
<div class="fw-medium"><?= htmlspecialchars($bk['title']) ?></div>
<div class="small <?= $isEdited ? 'text-white-50' : 'text-muted' ?>">
<?= count($bk['articles'] ?? []) ?> page<?= count($bk['articles'] ?? []) > 1 ? 's' : '' ?>
· <a href="/book/<?= rawurlencode($bk['slug']) ?>" target="_blank"
class="<?= $isEdited ? 'text-white-50' : 'text-muted' ?>">Voir ↗</a>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Formulaire édition / création -->
<div class="col-md-8">
<?php if (($adminData['edit_book'] ?? null) !== null): ?>
<?php $eb = $adminData['edit_book']; ?>
<h5>Modifier le livre</h5>
<?php if (($_GET['saved'] ?? '') === '1'): ?>
<div class="alert alert-success py-2 small">Livre sauvegardé.</div>
<?php endif; ?>
<form method="POST" action="/?action=book_save">
<input type="hidden" name="slug" value="<?= htmlspecialchars($eb['slug']) ?>">
<div class="mb-3">
<label class="form-label small fw-medium">Slug (identifiant URL)</label>
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($eb['slug']) ?>" readonly>
<div class="form-text">Le slug ne peut pas être modifié après création.</div>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Titre</label>
<input type="text" name="title" class="form-control" required
value="<?= htmlspecialchars($eb['title'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Description</label>
<textarea name="description" class="form-control" rows="2"><?= htmlspecialchars($eb['description'] ?? '') ?></textarea>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Pages (slugs dans l'ordre, un par ligne)</label>
<textarea name="articles" class="form-control font-monospace"
id="book-articles-ta"
rows="<?= max(6, count($eb['articles'] ?? []) + 2) ?>"><?= htmlspecialchars(implode("\n", $eb['articles'] ?? [])) ?></textarea>
<div class="form-text">Un slug par ligne. L'ordre définit la navigation précédent/suivant.</div>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Ajouter une page existante</label>
<select class="form-select" onchange="bookAddArticle(this)">
<option value="">— Choisir un article —</option>
<?php
$alreadyIn = $eb['articles'] ?? [];
foreach ($adminData['all_articles'] as $aa):
if (in_array($aa['slug'] ?? '', $alreadyIn, true)) {
continue;
}
?>
<option value="<?= htmlspecialchars($aa['slug'] ?? '') ?>">
<?= htmlspecialchars($aa['title']) ?>
<?= !$aa['published'] ? ' (brouillon)' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Sauvegarder</button>
<a href="/book/<?= rawurlencode($eb['slug']) ?>" target="_blank"
class="btn btn-outline-secondary">Voir le livre ↗</a>
</div>
</form>
<hr class="my-4">
<form method="POST" action="/?action=book_delete"
data-confirm="Supprimer le livre « <?= htmlspecialchars($eb['title']) ?> » ? Les pages resteront intactes.">
<input type="hidden" name="slug" value="<?= htmlspecialchars($eb['slug']) ?>">
<button type="submit" class="btn btn-outline-danger btn-sm">🗑 Supprimer ce livre</button>
</form>
<script>
function bookAddArticle(sel) {
var slug = sel.value;
if (!slug) return;
var ta = document.getElementById('book-articles-ta');
var lines = ta.value.split('\n').map(function(s) { return s.trim(); }).filter(Boolean);
if (lines.indexOf(slug) === -1) {
lines.push(slug);
ta.value = lines.join('\n');
}
sel.value = '';
}
</script>
<?php elseif (isset($_GET['new'])): ?>
<h5>Nouveau livre</h5>
<form method="POST" action="/?action=book_save">
<div class="mb-3">
<label class="form-label small fw-medium">Slug (identifiant URL)</label>
<input type="text" name="slug" class="form-control" required
placeholder="mon-livre" pattern="[a-z0-9][a-z0-9-]*">
<div class="form-text">Minuscules, chiffres, tirets. Exemple : <code>esp8266</code></div>
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Titre</label>
<input type="text" name="title" class="form-control" required placeholder="Titre du livre">
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Description (optionnelle)</label>
<textarea name="description" class="form-control" rows="2" placeholder="Courte description…"></textarea>
</div>
<input type="hidden" name="articles" value="">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Créer le livre</button>
<a href="/admin/books" class="btn btn-outline-secondary">Annuler</a>
</div>
</form>
<?php else: ?>
<p class="text-muted mt-2">
<?php if (!empty($adminData['books'])): ?>
Sélectionnez un livre à gauche pour le modifier, ou créez-en un nouveau.
<?php else: ?>
Cliquez sur <strong>+ Nouveau</strong> pour créer votre premier livre.
<?php endif; ?>
</p>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php if ($tab === 'stats' && isAdmin()): ?>
<?php include __DIR__ . '/admin_stats.php'; ?>
<?php endif; ?>
<?php
$content = ob_get_clean();
$title = 'Administration — ' . siteTitle();
include __DIR__ . '/layout.php';