Files
RSS-Feed/templates/shell.php
T
Harwinder Sekhon 3c2d90fb7e Stream the page shell first so a loader shows while feeds fetch
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
2026-10-08 15:28:51 -04:00

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>