feat : recherche titre, toggle à la une, date modif, retour sources (v1.6.15)
- admin/articles : champ filter_search (titre, insensible casse) cumulable avec auteur/catégorie/statut (#85) - admin/articles : colonne ★ avec toggle rapide featured + filtre filter_featured (#84) - post/ : date de modification sous la date de publication si modifié après mise en ligne (#81) - sources/ : bouton ← Retour à l'article vers post/<slug> au lieu de /edit/<uuid> (#83) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,18 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.6.15] - 2026-05-16
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- `admin/articles` : champ de recherche par titre (`filter_search`), cumulable avec les autres filtres (#85)
|
||||||
|
- `admin/articles` : colonne « ★ À la une » avec toggle rapide par ligne et filtre `filter_featured` (#84)
|
||||||
|
- `post/` : date de modification affichée sous la date de publication si l'article a été modifié après sa mise en ligne (#81)
|
||||||
|
|
||||||
|
### Modifié
|
||||||
|
- `sources/` : bouton « ← Modifier » remplacé par « ← Retour à l'article » pointant vers `post/<slug>` (#83)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.6.14] - 2026-05-15
|
## [1.6.14] - 2026-05-15
|
||||||
|
|
||||||
### Modifié
|
### Modifié
|
||||||
|
|||||||
@@ -2355,9 +2355,13 @@ switch ($action) {
|
|||||||
$filterAuthor = trim($_GET['filter_author'] ?? '');
|
$filterAuthor = trim($_GET['filter_author'] ?? '');
|
||||||
$filterCategory = trim($_GET['filter_category'] ?? '');
|
$filterCategory = trim($_GET['filter_category'] ?? '');
|
||||||
$filterStatus = trim($_GET['filter_status'] ?? '');
|
$filterStatus = trim($_GET['filter_status'] ?? '');
|
||||||
|
$filterSearch = trim($_GET['filter_search'] ?? '');
|
||||||
|
$filterFeatured = trim($_GET['filter_featured'] ?? '');
|
||||||
$adminData['filter_author'] = $filterAuthor;
|
$adminData['filter_author'] = $filterAuthor;
|
||||||
$adminData['filter_category'] = $filterCategory;
|
$adminData['filter_category'] = $filterCategory;
|
||||||
$adminData['filter_status'] = $filterStatus;
|
$adminData['filter_status'] = $filterStatus;
|
||||||
|
$adminData['filter_search'] = $filterSearch;
|
||||||
|
$adminData['filter_featured'] = $filterFeatured;
|
||||||
|
|
||||||
$nowTs = time();
|
$nowTs = time();
|
||||||
if ($filterAuthor !== '') {
|
if ($filterAuthor !== '') {
|
||||||
@@ -2373,6 +2377,12 @@ switch ($action) {
|
|||||||
} elseif ($filterStatus === 'preview') {
|
} elseif ($filterStatus === 'preview') {
|
||||||
$allArticles = array_values(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) > $nowTs));
|
$allArticles = array_values(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) > $nowTs));
|
||||||
}
|
}
|
||||||
|
if ($filterSearch !== '') {
|
||||||
|
$allArticles = array_values(array_filter($allArticles, fn ($a) => mb_stripos($a['title'] ?? '', $filterSearch) !== false));
|
||||||
|
}
|
||||||
|
if ($filterFeatured === 'yes') {
|
||||||
|
$allArticles = array_values(array_filter($allArticles, fn ($a) => !empty($a['featured'])));
|
||||||
|
}
|
||||||
|
|
||||||
$sortBy = in_array($_GET['sort'] ?? '', ['title', 'published', 'updated']) ? $_GET['sort'] : 'updated';
|
$sortBy = in_array($_GET['sort'] ?? '', ['title', 'published', 'updated']) ? $_GET['sort'] : 'updated';
|
||||||
$sortDir = ($_GET['dir'] ?? '') === 'asc' ? 'asc' : 'desc';
|
$sortDir = ($_GET['dir'] ?? '') === 'asc' ? 'asc' : 'desc';
|
||||||
@@ -2741,6 +2751,21 @@ switch ($action) {
|
|||||||
header('Location: /admin/smtp');
|
header('Location: /admin/smtp');
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
|
case 'admin_toggle_featured':
|
||||||
|
requireAuth();
|
||||||
|
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(403);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$uid = trim((string)($_POST['uuid'] ?? ''));
|
||||||
|
$art = $uid !== '' ? $articles->getByUuid($uid) : null;
|
||||||
|
if ($art) {
|
||||||
|
$articles->setFeatured($uid, !((bool)($art['featured'] ?? false)));
|
||||||
|
}
|
||||||
|
$back = $_POST['_back'] ?? '/admin/articles';
|
||||||
|
header('Location: ' . $back);
|
||||||
|
exit;
|
||||||
|
|
||||||
case 'admin_bulk_delete':
|
case 'admin_bulk_delete':
|
||||||
requireAuth();
|
requireAuth();
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.6.14
|
1.6.15
|
||||||
|
|||||||
+30
-1
@@ -214,6 +214,8 @@ function adminStatusBadge(array $a, int $now): string
|
|||||||
'filter_author' => $adminData['filter_author'] ?? '',
|
'filter_author' => $adminData['filter_author'] ?? '',
|
||||||
'filter_category' => $adminData['filter_category'] ?? '',
|
'filter_category' => $adminData['filter_category'] ?? '',
|
||||||
'filter_status' => $adminData['filter_status'] ?? '',
|
'filter_status' => $adminData['filter_status'] ?? '',
|
||||||
|
'filter_search' => $adminData['filter_search'] ?? '',
|
||||||
|
'filter_featured' => $adminData['filter_featured'] ?? '',
|
||||||
], fn ($v) => $v !== '');
|
], fn ($v) => $v !== '');
|
||||||
$p['sort'] = $col;
|
$p['sort'] = $col;
|
||||||
$p['dir'] = $dir;
|
$p['dir'] = $dir;
|
||||||
@@ -263,9 +265,19 @@ function adminStatusBadge(array $a, int $now): string
|
|||||||
<option value="preview" <?= ($adminData['filter_status'] ?? '') === 'preview' ? 'selected' : '' ?>>Avant-première</option>
|
<option value="preview" <?= ($adminData['filter_status'] ?? '') === 'preview' ? 'selected' : '' ?>>Avant-première</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<select name="filter_featured" class="form-select form-select-sm">
|
||||||
|
<option value="">Tous</option>
|
||||||
|
<option value="yes" <?= ($adminData['filter_featured'] ?? '') === 'yes' ? 'selected' : '' ?>>★ À la une</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<input type="text" name="filter_search" class="form-control form-control-sm"
|
||||||
|
placeholder="Rechercher…" value="<?= htmlspecialchars($adminData['filter_search'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
<div class="col-auto d-flex gap-2">
|
<div class="col-auto d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-secondary btn-sm">Filtrer</button>
|
<button type="submit" class="btn btn-secondary btn-sm">Filtrer</button>
|
||||||
<?php $hasFilter = ($adminData['filter_author'] ?? '') !== '' || ($adminData['filter_category'] ?? '') !== '' || ($adminData['filter_status'] ?? '') !== ''; ?>
|
<?php $hasFilter = ($adminData['filter_author'] ?? '') !== '' || ($adminData['filter_category'] ?? '') !== '' || ($adminData['filter_status'] ?? '') !== '' || ($adminData['filter_search'] ?? '') !== '' || ($adminData['filter_featured'] ?? '') !== ''; ?>
|
||||||
<?php if ($hasFilter): ?>
|
<?php if ($hasFilter): ?>
|
||||||
<a href="/admin/articles" class="btn btn-link btn-sm p-0">Réinitialiser</a>
|
<a href="/admin/articles" class="btn btn-link btn-sm p-0">Réinitialiser</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -304,6 +316,7 @@ function adminStatusBadge(array $a, int $now): string
|
|||||||
<?php if (isAdmin()): ?><th>Auteur</th><?php endif; ?>
|
<?php if (isAdmin()): ?><th>Auteur</th><?php endif; ?>
|
||||||
<th>Catégorie</th>
|
<th>Catégorie</th>
|
||||||
<th>Statut</th>
|
<th>Statut</th>
|
||||||
|
<th title="À la une">★</th>
|
||||||
<th>
|
<th>
|
||||||
<a href="<?= htmlspecialchars($_mkSortUrl('published')) ?>"
|
<a href="<?= htmlspecialchars($_mkSortUrl('published')) ?>"
|
||||||
class="text-decoration-none text-reset">
|
class="text-decoration-none text-reset">
|
||||||
@@ -330,6 +343,22 @@ function adminStatusBadge(array $a, int $now): string
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<td class="text-muted small"><?= htmlspecialchars($a['category'] ?? '–') ?></td>
|
<td class="text-muted small"><?= htmlspecialchars($a['category'] ?? '–') ?></td>
|
||||||
<td><?= adminStatusBadge($a, $now) ?></td>
|
<td><?= adminStatusBadge($a, $now) ?></td>
|
||||||
|
<td class="text-center">
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<?php $_isFeatured = !empty($a['featured']); ?>
|
||||||
|
<?php $_backUrl = '/admin/articles?' . http_build_query(array_filter(['filter_author' => $adminData['filter_author'] ?? '', 'filter_category' => $adminData['filter_category'] ?? '', 'filter_status' => $adminData['filter_status'] ?? '', 'filter_search' => $adminData['filter_search'] ?? '', 'filter_featured' => $adminData['filter_featured'] ?? '', 'sort' => $_sortBy, 'dir' => $_sortDir], fn ($v) => $v !== '')); ?>
|
||||||
|
<form method="post" action="/?action=admin_toggle_featured" class="d-inline m-0">
|
||||||
|
<input type="hidden" name="uuid" value="<?= htmlspecialchars($a['uuid']) ?>">
|
||||||
|
<input type="hidden" name="_back" value="<?= htmlspecialchars($_backUrl) ?>">
|
||||||
|
<button type="submit" class="btn btn-link p-0 border-0 lh-1 fs-6"
|
||||||
|
title="<?= $_isFeatured ? 'Retirer de la une' : 'Mettre à la une' ?>">
|
||||||
|
<?= $_isFeatured ? '★' : '<span class="text-muted">☆</span>' ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= !empty($a['featured']) ? '★' : '' ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td class="text-muted small text-nowrap">
|
<td class="text-muted small text-nowrap">
|
||||||
<?= htmlspecialchars(date('d/m/Y', strtotime((string)($a['published_at'] ?? $a['created_at'] ?? '')))) ?>
|
<?= htmlspecialchars(date('d/m/Y', strtotime((string)($a['published_at'] ?? $a['created_at'] ?? '')))) ?>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ $authorName = ($authorEmail !== '' && function_exists('authorDisplayName')
|
|||||||
$authorProfileUrl = ($authorEmail !== '' && function_exists('authorProfileUrl')) ? authorProfileUrl($authorEmail) : '';
|
$authorProfileUrl = ($authorEmail !== '' && function_exists('authorProfileUrl')) ? authorProfileUrl($authorEmail) : '';
|
||||||
$authorSlugVal = ($authorEmail !== '' && function_exists('authorSlug')) ? authorSlug($authorEmail) : '';
|
$authorSlugVal = ($authorEmail !== '' && function_exists('authorSlug')) ? authorSlug($authorEmail) : '';
|
||||||
$pubDate = htmlspecialchars(date('d/m/Y', strtotime((string)($article['published_at'] ?? $article['created_at'] ?? ''))));
|
$pubDate = htmlspecialchars(date('d/m/Y', strtotime((string)($article['published_at'] ?? $article['created_at'] ?? ''))));
|
||||||
|
$modDate = '';
|
||||||
|
$_updatedTs = strtotime((string)($article['updated_at'] ?? ''));
|
||||||
|
$_publishedTs = strtotime((string)($article['published_at'] ?? $article['created_at'] ?? ''));
|
||||||
|
if ($_updatedTs > 0 && $_publishedTs > 0 && $_updatedTs > $_publishedTs) {
|
||||||
|
$_frMonths = ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'];
|
||||||
|
$modDate = 'Modifié le ' . (int)date('j', $_updatedTs) . ' ' . $_frMonths[(int)date('n', $_updatedTs) - 1]
|
||||||
|
. ' ' . date('Y', $_updatedTs) . ' à ' . date('H', $_updatedTs) . 'h' . date('i', $_updatedTs);
|
||||||
|
}
|
||||||
$hasCover = $coverFile !== '';
|
$hasCover = $coverFile !== '';
|
||||||
$heroExtraClass = $hasCover ? '' : ' article-cover--gradient';
|
$heroExtraClass = $hasCover ? '' : ' article-cover--gradient';
|
||||||
$heroStyle = $hasCover ? '' : ' style="background:' . htmlspecialchars($gradient) . '"';
|
$heroStyle = $hasCover ? '' : ' style="background:' . htmlspecialchars($gradient) . '"';
|
||||||
@@ -137,6 +145,9 @@ $hasSources = (!empty($externalLinks) || !empty($files))
|
|||||||
<span class="mx-1 opacity-50">·</span>
|
<span class="mx-1 opacity-50">·</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?= $pubDate ?>
|
<?= $pubDate ?>
|
||||||
|
<?php if ($modDate !== ''): ?>
|
||||||
|
<br><small class="opacity-75"><?= htmlspecialchars($modDate) ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="article-hero-right">
|
<div class="article-hero-right">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function renderMetaCell(string $key, mixed $val, array $row = []): string
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="d-flex align-items-center gap-3 mb-1">
|
<div class="d-flex align-items-center gap-3 mb-1">
|
||||||
<a href="/edit/<?= rawurlencode($article['uuid']) ?>" class="btn btn-secondary btn-sm">← Modifier</a>
|
<a href="/post/<?= rawurlencode($article['slug'] ?? $article['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour à l'article</a>
|
||||||
<h1 class="h4 mb-0">Sources & médias</h1>
|
<h1 class="h4 mb-0">Sources & médias</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small mb-4"><?= htmlspecialchars($article['title']) ?></p>
|
<p class="text-muted small mb-4"><?= htmlspecialchars($article['title']) ?></p>
|
||||||
|
|||||||
Reference in New Issue
Block a user