feat: agrégateur RSS /flux + gestion feeds dans /profile

This commit is contained in:
Cedric Abonnel
2026-05-12 23:59:09 +02:00
parent 03177dc732
commit 2e8302dad4
7 changed files with 432 additions and 7 deletions
+3
View File
@@ -40,6 +40,9 @@ RewriteRule ^admin/?$ /index.php?action=admin [L,QSA]
RewriteRule ^categories/?$ /index.php?action=categories [L,QSA]
RewriteRule ^profile/?$ /index.php?action=profile [L,QSA]
RewriteRule ^search/?$ /index.php?action=search [L,QSA]
RewriteRule ^flux/?$ /index.php?action=flux [L,QSA]
RewriteRule ^feed/add/?$ /index.php?action=add_feed [L,QSA]
RewriteRule ^feed/delete/?$ /index.php?action=delete_feed [L,QSA]
# Profil public auteur
RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=author&slug=$1 [L,QSA]
+54
View File
@@ -1283,3 +1283,57 @@ footer.mt-5 { margin-top: 0 !important; }
color: var(--vl-muted);
line-height: 1.7;
}
/* ─── Agrégateur de flux ─────────────────── */
.flux-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 52rem;
}
.flux-item {
border-left: 3px solid var(--vl-border);
padding-left: 1rem;
}
.flux-item-meta {
display: flex;
align-items: center;
gap: .5rem;
flex-wrap: wrap;
font-size: .8rem;
color: var(--vl-muted);
margin-bottom: .25rem;
}
.flux-author {
font-weight: 600;
color: var(--vl-accent);
text-decoration: none;
}
.flux-author:hover { text-decoration: underline; }
.flux-feed-name::before { content: '·'; margin-right: .5rem; }
.flux-date::before { content: '·'; margin-right: .5rem; }
.flux-item-title {
font-size: 1rem;
font-weight: 600;
margin: 0 0 .25rem;
}
.flux-item-title a {
color: var(--vl-text);
text-decoration: none;
}
.flux-item-title a:hover { color: var(--vl-accent); }
.flux-item-summary {
font-size: .875rem;
color: var(--vl-muted);
margin: 0;
line-height: 1.6;
}
+86 -1
View File
@@ -22,7 +22,7 @@ $action = $_GET['action'] ?? 'list';
$uuid = $_GET['uuid'] ?? '';
$slug = $_GET['slug'] ?? '';
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found'];
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed'];
$metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null;
unset($_noindexActions);
@@ -881,6 +881,44 @@ switch ($action) {
include BASE_PATH . '/templates/author_profile.php';
break;
case 'flux':
require_once BASE_PATH . '/src/FeedFetcher.php';
$fetcher = new FeedFetcher(BASE_PATH . '/data/_cache/feeds');
$fluxItems = [];
$pdo = dbPdo();
if ($pdo) {
try {
$st = $pdo->query(
'SELECT f.user_email, f.feed_url, f.label,
p.display_name, p.profile_slug
FROM rss_feeds f
LEFT JOIN user_profiles p ON p.email = f.user_email
ORDER BY f.created_at'
);
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $_row) {
$data = $fetcher->get($_row['feed_url']);
if (!$data) {
continue;
}
$feedTitle = $_row['label'] !== '' ? $_row['label'] : $data['feed_title'];
$authorName = $_row['display_name'] ?? '';
$authorSlug = $_row['profile_slug'] ?? '';
foreach ($data['items'] as $_item) {
$fluxItems[] = array_merge($_item, [
'feed_title' => $feedTitle,
'feed_url' => $_row['feed_url'],
'author_name' => $authorName,
'author_slug' => $authorSlug,
]);
}
}
} catch (\Throwable) {
}
}
usort($fluxItems, static fn ($a, $b) => $b['date'] <=> $a['date']);
include BASE_PATH . '/templates/flux.php';
break;
case 'about':
include BASE_PATH . '/templates/about.php';
break;
@@ -1840,9 +1878,56 @@ switch ($action) {
if ($profileCurrentUrl === '' && $profileCurrentSlug !== '') {
$profileCurrentUrl = rtrim(APP_URL, '/') . '/profil/' . rawurlencode($profileCurrentSlug);
}
// Feeds RSS de l'utilisateur
$profileFeeds = [];
$pdo = dbPdo();
if ($pdo) {
try {
$st = $pdo->prepare('SELECT id, feed_url, label FROM rss_feeds WHERE user_email = :e ORDER BY created_at');
$st->execute([':e' => currentUserEmail()]);
$profileFeeds = $st->fetchAll(PDO::FETCH_ASSOC);
} catch (\Throwable) {
}
}
include BASE_PATH . '/templates/profile.php';
break;
case 'add_feed':
requireAuth();
$feedUrl = filter_var(trim($_POST['feed_url'] ?? ''), FILTER_VALIDATE_URL) ?: '';
$feedLabel = trim($_POST['feed_label'] ?? '');
if ($feedUrl !== '') {
$pdo = dbPdo();
if ($pdo) {
try {
$st = $pdo->prepare(
'INSERT INTO rss_feeds (user_email, feed_url, label) VALUES (:e, :u, :l)
ON CONFLICT (user_email, feed_url) DO UPDATE SET label = :l'
);
$st->execute([':e' => currentUserEmail(), ':u' => $feedUrl, ':l' => $feedLabel]);
} catch (\Throwable) {
}
}
}
header('Location: /profile#feeds');
exit;
case 'delete_feed':
requireAuth();
$feedId = (int)($_POST['feed_id'] ?? 0);
if ($feedId > 0) {
$pdo = dbPdo();
if ($pdo) {
try {
$st = $pdo->prepare('DELETE FROM rss_feeds WHERE id = :id AND user_email = :e');
$st->execute([':id' => $feedId, ':e' => currentUserEmail()]);
} catch (\Throwable) {
}
}
}
header('Location: /profile#feeds');
exit;
case 'search_files':
requireAuth();
header('Content-Type: application/json');