From a926e1825dbd8adfb244b862ac8c17ee0845f5cb Mon Sep 17 00:00:00 2001 From: Cedric Abonnel Date: Wed, 13 May 2026 00:57:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20filtres=20et=20suppression=20massive=20?= =?UTF-8?q?dans=20admin/articles,=20profil=20auteur=20am=C3=A9lior=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Admin articles : filtres auteur/catégorie/statut par GET, compteur de résultats - Admin articles : suppression massive avec checkboxes, confirmation JS, contrôle d'ownership - Profil auteur : bio tronquée à 3 lignes avec bouton 'plus', limite à 6 articles affichés - Profil auteur : bouton CTA pill 'Mes liens' vers /liens/{slug} - Page liens : boutons pill colorés (palette auto par index), fond gris clair, grand avatar --- data/private_cats.json | 2 +- public/.htaccess | 2 + public/assets/css/style.css | 27 ++++++++ public/assets/js/bio-toggle.js | 14 ++++ public/assets/js/links-sortable.js | 29 +++++++++ public/index.php | 101 +++++++++++++++++++++++++++++ templates/admin.php | 72 ++++++++++++++++++++ templates/author_articles.php | 97 +++++++++++++++++++++++++++ templates/author_profile.php | 18 +---- templates/post_list.php | 7 +- templates/profile.php | 32 +-------- 11 files changed, 350 insertions(+), 51 deletions(-) create mode 100644 public/assets/js/bio-toggle.js create mode 100644 public/assets/js/links-sortable.js create mode 100644 templates/author_articles.php diff --git a/data/private_cats.json b/data/private_cats.json index 141462c..9272d7d 100644 --- a/data/private_cats.json +++ b/data/private_cats.json @@ -1 +1 @@ -["scolaire"] \ No newline at end of file +["scolaire","perso","Produits","Journal personnel","Vie pratique"] \ No newline at end of file diff --git a/public/.htaccess b/public/.htaccess index 9575fa6..359b71d 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -45,6 +45,8 @@ RewriteRule ^feed/add/?$ /index.php?action=add_feed [L,QSA] RewriteRule ^feed/delete/?$ /index.php?action=delete_feed [L,QSA] # Profil public auteur + page liens +RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/article/cursor/([0-9a-f-]{36})/?$ /index.php?action=author_articles&slug=$1&cursor=$2 [L,QSA] +RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/article/?$ /index.php?action=author_articles&slug=$1 [L,QSA] RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=author&slug=$1 [L,QSA] RewriteRule ^liens/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=liens&slug=$1 [L,QSA] RewriteRule ^link/add/?$ /index.php?action=add_link [L,QSA] diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 850bcef..04bdc1b 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1479,3 +1479,30 @@ footer.mt-5 { margin-top: 0 !important; } margin: 0; line-height: 1.6; } + +/* Bouton flottant "+" — nouvel article */ +.fab-new { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 1050; + width: 3.25rem; + height: 3.25rem; + border-radius: 50%; + background: var(--vl-accent); + color: #fff; + font-size: 1.75rem; + line-height: 3.25rem; + text-align: center; + text-decoration: none; + box-shadow: 0 4px 16px rgba(79,70,229,.45); + transition: background 0.15s, box-shadow 0.15s, transform 0.15s; + user-select: none; +} +.fab-new:hover, +.fab-new:focus { + background: var(--vl-accent-dark); + color: #fff; + box-shadow: 0 6px 20px rgba(79,70,229,.55); + transform: scale(1.08); +} diff --git a/public/assets/js/bio-toggle.js b/public/assets/js/bio-toggle.js new file mode 100644 index 0000000..549bcad --- /dev/null +++ b/public/assets/js/bio-toggle.js @@ -0,0 +1,14 @@ +(function(){ + var bio = document.getElementById('author-bio'); + var btn = document.getElementById('bio-toggle'); + if (!bio || !btn) return; + requestAnimationFrame(function() { + if (bio.scrollHeight > bio.clientHeight + 2) { btn.hidden = false; } + }); + btn.addEventListener('click', function() { + var exp = btn.getAttribute('aria-expanded') === 'true'; + bio.classList.toggle('bio-clamped', exp); + btn.textContent = exp ? 'plus' : 'moins'; + btn.setAttribute('aria-expanded', exp ? 'false' : 'true'); + }); +})(); diff --git a/public/assets/js/links-sortable.js b/public/assets/js/links-sortable.js new file mode 100644 index 0000000..7a02393 --- /dev/null +++ b/public/assets/js/links-sortable.js @@ -0,0 +1,29 @@ +(function() { + const list = document.getElementById('links-sortable'); + if (!list) return; + let dragged = null; + list.querySelectorAll('li').forEach(li => { + li.setAttribute('draggable', true); + li.addEventListener('dragstart', () => { dragged = li; li.style.opacity = '.4'; }); + li.addEventListener('dragend', () => { dragged = null; li.style.opacity = ''; saveOrder(); }); + li.addEventListener('dragover', e => { e.preventDefault(); const after = getDragAfter(list, e.clientY); after ? list.insertBefore(dragged, after) : list.appendChild(dragged); }); + }); + function getDragAfter(container, y) { + return [...container.querySelectorAll('li:not([style*="opacity"])')].reduce((closest, el) => { + const box = el.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + return offset < 0 && offset > closest.offset ? { offset, element: el } : closest; + }, { offset: Number.NEGATIVE_INFINITY }).element; + } + function saveOrder() { + const form = document.getElementById('reorder-form'); + if (!form) return; + form.querySelectorAll('input').forEach(i => i.remove()); + list.querySelectorAll('li[data-id]').forEach(li => { + const inp = document.createElement('input'); + inp.type = 'hidden'; inp.name = 'order[]'; inp.value = li.dataset.id; + form.appendChild(inp); + }); + form.submit(); + } +})(); diff --git a/public/index.php b/public/index.php index 5f6abce..11b4809 100644 --- a/public/index.php +++ b/public/index.php @@ -881,6 +881,55 @@ switch ($action) { include BASE_PATH . '/templates/author_profile.php'; break; + case 'author_articles': + $authorSlug = trim($_GET['slug'] ?? ''); + $authorRow = profileBySlug($authorSlug); + if (!$authorRow) { + http_response_code(404); + $content = '

Profil introuvable.

'; + $title = 'Profil introuvable'; + include BASE_PATH . '/templates/layout.php'; + break; + } + $privateCats = $articles->getPrivateCategories(); + $allCats = $articles->getCategories(); + $authorArticles = array_values(array_filter( + $articles->getAll(publishedOnly: true), + static function (array $a) use ($authorRow, $privateCats): bool { + if (($a['author'] ?? '') !== $authorRow['email']) { + return false; + } + $cat = trim($a['category'] ?? ''); + if ($cat !== '' && in_array($cat, $privateCats, true) && !isLoggedIn()) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + return false; + } + return true; + } + )); + $perPage = postsPerPage(); + $cursor = trim($_GET['cursor'] ?? ''); + $offset = 0; + if ($cursor !== '') { + foreach ($authorArticles as $i => $a) { + if ($a['uuid'] === $cursor) { + $offset = $i + 1; + break; + } + } + } + $posts = array_slice($authorArticles, $offset, $perPage); + $nextCursor = count($posts) === $perPage ? end($posts)['uuid'] : null; + $prevCursor = null; + if ($offset > 0) { + $prevOffset = max(0, $offset - $perPage); + $prevCursor = $prevOffset > 0 ? $authorArticles[$prevOffset - 1]['uuid'] : ''; + } + include BASE_PATH . '/templates/author_articles.php'; + break; + case 'liens': $liensSlug = trim($_GET['slug'] ?? ''); $liensRow = profileBySlug($liensSlug); @@ -1622,6 +1671,34 @@ switch ($action) { $allArticles = array_values(array_filter($allArticles, fn ($a) => ($a['author'] ?? '') === $me)); } usort($allArticles, fn ($a, $b) => strcmp($b['updated_at'] ?? '', $a['updated_at'] ?? '')); + + $adminData['filter_authors'] = array_values(array_unique(array_filter(array_column($allArticles, 'author')))); + $adminData['filter_categories'] = array_values(array_unique(array_filter(array_column($allArticles, 'category')))); + sort($adminData['filter_authors']); + sort($adminData['filter_categories']); + + $filterAuthor = trim($_GET['filter_author'] ?? ''); + $filterCategory = trim($_GET['filter_category'] ?? ''); + $filterStatus = trim($_GET['filter_status'] ?? ''); + $adminData['filter_author'] = $filterAuthor; + $adminData['filter_category'] = $filterCategory; + $adminData['filter_status'] = $filterStatus; + + $nowTs = time(); + if ($filterAuthor !== '') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => ($a['author'] ?? '') === $filterAuthor)); + } + if ($filterCategory !== '') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => trim($a['category'] ?? '') === $filterCategory)); + } + if ($filterStatus === 'published') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) <= $nowTs)); + } elseif ($filterStatus === 'draft') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => !$a['published'])); + } elseif ($filterStatus === 'preview') { + $allArticles = array_values(array_filter($allArticles, fn ($a) => $a['published'] && strtotime((string)($a['published_at'] ?? '')) > $nowTs)); + } + $adminData['articles'] = $allArticles; } @@ -1706,6 +1783,30 @@ switch ($action) { include BASE_PATH . '/templates/admin.php'; break; + case 'admin_bulk_delete': + requireAuth(); + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $uuids = $_POST['uuids'] ?? []; + if (is_array($uuids)) { + $me = currentUserEmail() ?? ''; + foreach ($uuids as $uid) { + $uid = trim((string)$uid); + if ($uid === '') { + continue; + } + $art = $articles->getByUuid($uid); + if (!$art) { + continue; + } + if (isAdmin() || ($art['author'] ?? '') === $me) { + $articles->delete($uid); + } + } + } + } + header('Location: /admin/articles'); + exit; + case 'admin_grant_role': requireAuth(); if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { diff --git a/templates/admin.php b/templates/admin.php index b6d015f..39bc278 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -105,12 +105,74 @@ function adminStatusBadge(array $a, int $now): string + +
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ + + + Réinitialiser + +
+ +
+ résultat(s) +
+ +
+

Aucun article.

+
+
+
+ + +
+ +
+ @@ -122,6 +184,10 @@ function adminStatusBadge(array $a, int $now): string +
Titre Auteur Catégorie
+ + @@ -143,6 +209,12 @@ function adminStatusBadge(array $a, int $now): string
+
+ diff --git a/templates/author_articles.php b/templates/author_articles.php new file mode 100644 index 0000000..9f8d530 --- /dev/null +++ b/templates/author_articles.php @@ -0,0 +1,97 @@ + + +
+ +

Articles de

+
+ + +

Aucun article publié.

+ +
+text($post['content']); + $preview = mb_strimwidth(strip_tags($html), 0, 120, '…'); + $category = trim((string)($post['category'] ?? '')); + $gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []); + $postUrl = '/post/' . rawurlencode($post['slug']); + $coverFile = $post['cover'] ?? ''; + $coverStyle = $coverFile !== '' + ? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')' + : 'background: ' . $gradient; + $isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time(); + $isLocked = $isAvantPremiere && !hasCapability('view_previews'); + ?> +
+ +
Avant-première
+ +
+ + + +
+
+

+ + + + + +

+

+ +
+ + + +
+ +
+ + + + + + +

- + @@ -82,7 +68,7 @@ $_initials = mb_strtoupper(mb_substr($_apName, 0, 1, 'UTF-8'), 'UTF-8'); 6): ?>

- Voir tous les articles → + Voir tous les articles →

diff --git a/templates/post_list.php b/templates/post_list.php index 6308b7b..ad85eeb 100644 --- a/templates/post_list.php +++ b/templates/post_list.php @@ -55,9 +55,6 @@ ob_start(); - - modifier - → lire @@ -125,6 +122,10 @@ if (!empty($_tagCats)): + ++ + + "> - +