feat: filtres et suppression massive dans admin/articles, profil auteur amélioré
- 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
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
})();
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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 = '<div class="container py-5"><p class="text-muted">Profil introuvable.</p></div>';
|
||||
$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') {
|
||||
|
||||
Reference in New Issue
Block a user