3c2d90fb7e
Opening the page used to block on refreshStale() before a single byte went out, so the browser sat on a blank tab for the whole fetch. index.php now renders templates/shell.php (head, header, filters, skeleton loader) and flushes it, then refreshes and appends templates/content.php, which removes the loader as the feed HTML arrives. - templates/layout.php is split into shell.php + content.php - refresh chip moved to a right-aligned row above the feed: the toolbar paints before the refresh report exists - header story count shows the cached total, then corrects to the real one - skeleton styles match the card grid and keep the sources' chroma; a noscript style hides the loader for clients without JS - README: file map and page-load lifecycle updated
128 lines
5.4 KiB
PHP
128 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\View;
|
|
|
|
/** @var array<string, mixed> $config */
|
|
/** @var array<int, string> $sources */
|
|
/** @var string|null $activeSource */
|
|
/** @var string|null $activeRange */
|
|
/** @var int $cachedTotal */
|
|
|
|
$tabs = ['' => 'All'];
|
|
foreach ($sources as $source) {
|
|
$tabs[$source] = View::sourceLabel($source);
|
|
}
|
|
|
|
$activeRange = $activeRange ?? '';
|
|
$ranges = $config['ranges'] ?? [];
|
|
$rangeOptions = ['' => 'All time'];
|
|
foreach (array_keys($ranges) as $rangeKey) {
|
|
$rangeOptions[$rangeKey] = View::rangeLabel($rangeKey);
|
|
}
|
|
|
|
$linkFor = static function (string $source, string $range): string {
|
|
$params = [];
|
|
if ($source !== '') {
|
|
$params['source'] = $source;
|
|
}
|
|
if ($range !== '') {
|
|
$params['range'] = $range;
|
|
}
|
|
|
|
return $params === [] ? '?' : '?' . http_build_query($params);
|
|
};
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en" data-bs-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>RSS Feed Dashboard</title>
|
|
<noscript><style>#feed-loader{display:none}</style></noscript>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
|
|
rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
|
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
|
<link href="assets/style.css" rel="stylesheet">
|
|
</head>
|
|
<body class="app-body">
|
|
<header class="site-header">
|
|
<div class="container-xxl d-flex align-items-center justify-content-between gap-3 py-3">
|
|
<a class="brand" href="?">
|
|
<span class="brand-mark" aria-hidden="true"></span>
|
|
<span class="brand-text">RSS <em>Feed</em> Dashboard</span>
|
|
</a>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="header-meta d-none d-md-flex align-items-center">
|
|
<span class="pulse" aria-hidden="true"></span>
|
|
<span class="mono" id="story-count"><?= $cachedTotal ?> stories</span>
|
|
<span class="sep">/</span>
|
|
<span class="mono">YouTube · Reddit · RT</span>
|
|
</div>
|
|
<a class="manage-link" href="manage.php">Manage</a>
|
|
</div>
|
|
</div>
|
|
<div class="wire-rail" aria-hidden="true"></div>
|
|
</header>
|
|
|
|
<main class="container-xxl py-4">
|
|
<div class="toolbar mb-4">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
|
|
<ul class="nav filter-pills flex-nowrap overflow-auto">
|
|
<?php foreach ($tabs as $key => $label): ?>
|
|
<?php
|
|
$isActive = ($activeSource ?? '') === $key;
|
|
$color = $key === '' ? 'var(--accent)' : View::sourceColor($key);
|
|
?>
|
|
<li class="nav-item">
|
|
<a class="filter-pill <?= $isActive ? 'active' : '' ?>"
|
|
style="--pill: <?= View::e($color) ?>"
|
|
href="<?= $linkFor($key, $activeRange) ?>">
|
|
<span class="pill-dot" aria-hidden="true"></span><?= View::e($label) ?>
|
|
</a>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap align-items-center gap-2 mt-3">
|
|
<span class="toolbar-label mono">Time</span>
|
|
<ul class="nav range-pills flex-nowrap overflow-auto">
|
|
<?php foreach ($rangeOptions as $key => $label): ?>
|
|
<li class="nav-item">
|
|
<a class="range-pill <?= $activeRange === $key ? 'active' : '' ?>"
|
|
href="<?= $linkFor($activeSource ?? '', $key) ?>"><?= View::e($label) ?></a>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="feed-loader" class="row g-4" role="status">
|
|
<span class="visually-hidden">Loading feeds…</span>
|
|
<?php for ($placeholder = 0; $placeholder < 3; $placeholder++): ?>
|
|
<div class="col-12 col-md-6 col-xl-4">
|
|
<div class="card h-100 feed-card skeleton-card" aria-hidden="true">
|
|
<div class="skeleton-thumb"></div>
|
|
<div class="card-body d-flex flex-column">
|
|
<div class="d-flex align-items-center gap-2 mb-2">
|
|
<span class="skeleton-chip"></span>
|
|
<span class="skeleton-line w-50 mb-0"></span>
|
|
</div>
|
|
<span class="skeleton-line skeleton-title w-100"></span>
|
|
<span class="skeleton-line w-100"></span>
|
|
<span class="skeleton-line w-75"></span>
|
|
<div class="card-footer-meta mt-auto d-flex justify-content-between align-items-center pt-3">
|
|
<span class="skeleton-line w-25 mb-0"></span>
|
|
<span class="skeleton-line skeleton-timestamp"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endfor; ?>
|
|
</div>
|