feat: agrégateur RSS /flux + gestion feeds dans /profile
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user