20f4fd7095
The light lavender theme carried its color in decoration -- four colored background blobs, a conic-gradient brand mark, gradient-clipped wordmark, a four-color animated button, a spinning conic empty mark, and a five-color spectrum rail. All of that competed with the only color on screen that carries meaning: which source a story came from. - One indigo-slate ink ramp (bg / surface / line / ink) carries every neutral surface; the only saturated hues are the three sources, used strictly as signals -- filter pill, card spine, source chip, timestamp, feed health. - Rule for every filled state: bright fill, deep ink type. Source tokens kept in sync between :root and View::sourceColor(). - Signature "signal bars": a CSS brand mark of three ascending bars in the source hues, and the header rail re-cut into three wire segments that double as the palette legend. The empty state reuses the mark, unlit and static. - Bootstrap switched to data-bs-theme="dark" with matching form controls, alerts, and focus rings; --ink-faint recalibrated so small text clears WCAG 4.5:1 on every surface. Reduced-motion behavior unchanged. - README documents the two rules next to View::sourceColor().
199 lines
11 KiB
PHP
199 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\View;
|
|
|
|
/** @var array<string, mixed> $config */
|
|
/** @var App\ManageAuth $auth */
|
|
/** @var array<int, array<string, mixed>> $feeds */
|
|
/** @var array{type: string, text: string}|null $flash */
|
|
|
|
$csrf = View::e($auth->csrfToken());
|
|
|
|
$feedGroups = [];
|
|
foreach ($feeds as $feed) {
|
|
$feedGroups[(string) $feed['source']][] = $feed;
|
|
}
|
|
|
|
$actionUrl = 'manage.php';
|
|
?>
|
|
<!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>Manage feeds · RSS Feed Dashboard</title>
|
|
<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="manage.php">
|
|
<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">
|
|
<span class="mono manage-title">Manage feeds</span>
|
|
<a class="manage-link" href="index.php">← Dashboard</a>
|
|
</div>
|
|
</div>
|
|
<div class="wire-rail" aria-hidden="true"></div>
|
|
</header>
|
|
|
|
<main class="container-xxl py-4 manage-main">
|
|
<?php if (!$auth->enabled()): ?>
|
|
<div class="manage-panel manage-notice">
|
|
<h1 class="manage-heading">Management is disabled</h1>
|
|
<p class="mb-0">
|
|
Set <code class="mono">manage_password</code> in <code class="mono">config/app.php</code>
|
|
to enable this page.
|
|
</p>
|
|
</div>
|
|
<?php elseif (!$auth->authed()): ?>
|
|
<?php if ($flash !== null): ?>
|
|
<div class="alert alert-soft alert-dismissible fade show small" role="alert">
|
|
<?= View::e($flash['text']) ?>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="manage-panel manage-login">
|
|
<h1 class="manage-heading">Sign in to manage feeds</h1>
|
|
<p class="text-body-secondary small">This page configures which subreddits and YouTube channels the dashboard follows.</p>
|
|
<form method="post" action="<?= View::e($actionUrl) ?>" class="d-flex gap-2">
|
|
<input type="hidden" name="csrf" value="<?= $csrf ?>">
|
|
<input type="hidden" name="action" value="login">
|
|
<input type="password" name="input" class="form-control mono" placeholder="Password" required autofocus autocomplete="current-password">
|
|
<button type="submit" class="btn btn-tally btn-compact">Sign in</button>
|
|
</form>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php if ($flash !== null): ?>
|
|
<div class="alert alert-dismissible fade show small <?= $flash['type'] === 'ok' ? 'alert-ok-soft' : 'alert-soft' ?>" role="alert">
|
|
<?= View::e($flash['text']) ?>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-12 col-lg-6">
|
|
<div class="manage-panel">
|
|
<h2 class="manage-subheading">Add a subreddit</h2>
|
|
<form method="post" action="<?= View::e($actionUrl) ?>" class="d-flex gap-2">
|
|
<input type="hidden" name="csrf" value="<?= $csrf ?>">
|
|
<input type="hidden" name="action" value="add_reddit">
|
|
<input type="text" name="input" class="form-control mono" inputmode="url"
|
|
placeholder="r/homelab · homelab · reddit.com/r/homelab"
|
|
required maxlength="300" autocomplete="off">
|
|
<button type="submit" class="btn btn-tally btn-compact">Add</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6">
|
|
<div class="manage-panel">
|
|
<h2 class="manage-subheading">Add a YouTube channel</h2>
|
|
<form method="post" action="<?= View::e($actionUrl) ?>" class="d-flex gap-2">
|
|
<input type="hidden" name="csrf" value="<?= $csrf ?>">
|
|
<input type="hidden" name="action" value="add_youtube">
|
|
<input type="text" name="input" class="form-control mono" inputmode="url"
|
|
placeholder="@handle · UC… channel ID · channel URL"
|
|
required maxlength="300" autocomplete="off">
|
|
<button type="submit" class="btn btn-tally btn-compact">Add</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($feedGroups === []): ?>
|
|
<div class="manage-panel manage-notice">
|
|
<p class="mb-0">No feeds registered yet — add one above, or edit <code class="mono">config/feeds.php</code>.</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php foreach ($feedGroups as $source => $groupFeeds): ?>
|
|
<section class="manage-group">
|
|
<h2 class="manage-group-title" style="--src: <?= View::e(View::sourceColor($source)) ?>">
|
|
<span class="pill-dot" aria-hidden="true"></span>
|
|
<?= View::e(View::sourceLabel($source)) ?>
|
|
<span class="mono count"><?= count($groupFeeds) ?></span>
|
|
</h2>
|
|
|
|
<div class="feed-rows">
|
|
<?php foreach ($groupFeeds as $feed): ?>
|
|
<?php
|
|
$url = (string) $feed['url'];
|
|
$label = (string) ($feed['label'] ?? $url);
|
|
$isEnabled = (int) $feed['enabled'] === 1;
|
|
$isConfig = (string) $feed['origin'] === 'config';
|
|
$lastError = $feed['last_error'] ?? null;
|
|
$lastSuccess = $feed['last_success_at'] ?? null;
|
|
$confirm = $isConfig
|
|
? sprintf('Hide "%s" from the dashboard? Its cached stories are kept.', $label)
|
|
: sprintf('Remove "%s"? Its cached stories stay hidden until it is added again.', $label);
|
|
?>
|
|
<div class="feed-row <?= $isEnabled ? '' : 'is-off' ?>" style="--src: <?= View::e(View::sourceColor($source)) ?>">
|
|
<div class="feed-row-main">
|
|
<span class="feed-row-label"><?= View::e($label) ?></span>
|
|
<span class="origin-badge mono"><?= $isConfig ? 'config' : 'manual' ?></span>
|
|
<a class="feed-row-url mono text-truncate" href="<?= View::e($url) ?>"
|
|
target="_blank" rel="noopener noreferrer"><?= View::e($url) ?></a>
|
|
<?php if (is_string($lastError) && $lastError !== ''): ?>
|
|
<span class="health health-err mono" title="<?= View::e($lastError) ?>">
|
|
<span class="health-dot" aria-hidden="true"></span>failed
|
|
</span>
|
|
<?php elseif (is_string($lastSuccess) && $lastSuccess !== ''): ?>
|
|
<span class="health health-ok mono" title="Last success: <?= View::e($lastSuccess) ?>">
|
|
<span class="health-dot" aria-hidden="true"></span><?= View::e(View::timeAgo($lastSuccess)) ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="health health-idle mono"><span class="health-dot" aria-hidden="true"></span>not fetched yet</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="feed-row-actions">
|
|
<form method="post" action="<?= View::e($actionUrl) ?>">
|
|
<input type="hidden" name="csrf" value="<?= $csrf ?>">
|
|
<input type="hidden" name="action" value="toggle">
|
|
<input type="hidden" name="url" value="<?= View::e($url) ?>">
|
|
<input type="hidden" name="enabled" value="<?= $isEnabled ? '0' : '1' ?>">
|
|
<button type="submit" class="btn btn-mini <?= $isEnabled ? '' : 'btn-mini-on' ?>">
|
|
<?= $isEnabled ? 'Disable' : 'Enable' ?>
|
|
</button>
|
|
</form>
|
|
<form method="post" action="<?= View::e($actionUrl) ?>"
|
|
data-confirm="<?= View::e($confirm) ?>">
|
|
<input type="hidden" name="csrf" value="<?= $csrf ?>">
|
|
<input type="hidden" name="action" value="remove">
|
|
<input type="hidden" name="url" value="<?= View::e($url) ?>">
|
|
<button type="submit" class="btn btn-mini btn-mini-danger">
|
|
<?= $isConfig ? 'Hide' : 'Remove' ?>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</section>
|
|
<?php endforeach; ?>
|
|
|
|
<div class="d-flex justify-content-end mt-4">
|
|
<form method="post" action="<?= View::e($actionUrl) ?>">
|
|
<input type="hidden" name="csrf" value="<?= $csrf ?>">
|
|
<input type="hidden" name="action" value="logout">
|
|
<button type="submit" class="btn btn-mini">Sign out</button>
|
|
</form>
|
|
</div>
|
|
<?php endif; ?>
|
|
</main>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
|
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhykGkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
|
<script src="assets/manage.js" defer></script>
|
|
</body>
|
|
</html>
|