Consolidate Boundaries + History under the account; sectioned /account
The inline Boundaries/History panels lived on the home page, so opening them while scrolled left you stranded. Move everything "yours" behind the account icon: - Home header slims to: Saved (opens a right-side flyout, signed-in) · shield (Boundaries indicator — filled when active — linking to the Boundaries section) · avatar. The inline panels + the home "saved" view are gone. - /account is now a sectioned hub (left sidebar on desktop, top tabs on mobile), OPEN TO EVERYONE with each section self-gating: Profile (sign-in), Saved (sign-in), History (device/account), Boundaries (device/account), Admin (admins). This keeps Boundaries/History usable without an account (they're device-local) while consolidating the UI — and every section loads at the top, fixing the scroll bug. - Lift Calm Filters and History into shared stores (prefs.svelte.js, history.svelte.js) so the home feed (applies/records) and the account page (edits/manages) share one source of truth. New SavedFlyout component. Card boundary actions only render when a handler is provided. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,58 +1,156 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { getJSON } from '$lib/api.js';
|
||||
import { auth, savedIds, refresh } from '$lib/auth.svelte.js';
|
||||
import { prefs, initPrefs, persistPrefs } from '$lib/prefs.svelte.js';
|
||||
import { history, initHistory, loadServerHistory, removeOne, clearAll } from '$lib/history.svelte.js';
|
||||
import { track } from '$lib/analytics.js';
|
||||
import AccountPanel from '$lib/components/AccountPanel.svelte';
|
||||
import { auth, refresh } from '$lib/auth.svelte.js';
|
||||
import BoundariesPanel from '$lib/components/BoundariesPanel.svelte';
|
||||
import ArticleCard from '$lib/components/ArticleCard.svelte';
|
||||
|
||||
onMount(() => {
|
||||
if (!auth.ready) refresh();
|
||||
let section = $derived($page.url.searchParams.get('section') || 'profile');
|
||||
let historyItems = $derived(auth.user ? history.server : history.device);
|
||||
|
||||
let savedItems = $state([]);
|
||||
let savedReady = $state(false);
|
||||
|
||||
const SECTIONS = [
|
||||
{ key: 'profile', label: 'Profile' },
|
||||
{ key: 'saved', label: 'Saved' },
|
||||
{ key: 'history', label: 'History' },
|
||||
{ key: 'boundaries', label: 'Boundaries' },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
if (!auth.ready) await refresh();
|
||||
initPrefs();
|
||||
initHistory();
|
||||
if (auth.user) loadServerHistory();
|
||||
});
|
||||
// Once auth resolves, send anonymous visitors back home.
|
||||
|
||||
// Load the saved grid when entering that section while signed in.
|
||||
$effect(() => {
|
||||
if (auth.ready && !auth.user) goto('/', { replaceState: true });
|
||||
if (section === 'saved' && auth.user && !savedReady) {
|
||||
savedReady = true;
|
||||
getJSON('/api/saved').then((r) => (savedItems = r.items)).catch(() => {});
|
||||
}
|
||||
});
|
||||
let savedShown = $derived(savedItems.filter((a) => savedIds.has(a.id)));
|
||||
</script>
|
||||
|
||||
<header class="bar">
|
||||
<div class="container inner">
|
||||
<a class="brand" href="/" aria-label="Upbeat Bytes — home">
|
||||
<img class="logo" src="/logo.svg" alt="Upbeat Bytes" />
|
||||
</a>
|
||||
<a class="back" href="/">← Back</a>
|
||||
<a class="brand" href="/" aria-label="Upbeat Bytes — home"><img class="logo" src="/logo.svg" alt="Upbeat Bytes" /></a>
|
||||
<a class="back" href="/">← Back to news</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container page">
|
||||
{#if auth.user}
|
||||
<h1>You</h1>
|
||||
<nav class="quick">
|
||||
<a href="/?view=saved">Saved</a>
|
||||
<a href="/?open=history">History</a>
|
||||
<a href="/?open=boundaries">Boundaries</a>
|
||||
{#if auth.user.is_admin}<a href="/admin" class="admin">Admin dashboard</a>{/if}
|
||||
</nav>
|
||||
<AccountPanel onclose={() => goto('/')} />
|
||||
{/if}
|
||||
<h1>You</h1>
|
||||
|
||||
<nav class="tabs" aria-label="Account sections">
|
||||
{#each SECTIONS as s (s.key)}
|
||||
<a href={'/account?section=' + s.key} class:active={section === s.key}>{s.label}</a>
|
||||
{/each}
|
||||
{#if auth.user?.is_admin}<a href="/admin" class="admin">Admin dashboard</a>{/if}
|
||||
</nav>
|
||||
|
||||
<div class="content">
|
||||
{#if section === 'boundaries'}
|
||||
<BoundariesPanel prefs={prefs.data} onchange={persistPrefs} />
|
||||
|
||||
{:else if section === 'history'}
|
||||
<section class="panel">
|
||||
<div class="phead">
|
||||
<h2>History</h2>
|
||||
{#if historyItems.length}<button class="link" onclick={clearAll}>Clear all</button>{/if}
|
||||
</div>
|
||||
<p class="reassure">Stories you've opened or swapped away — so an accidental Replace stays
|
||||
recoverable. {auth.user ? 'Synced across your devices.' : 'Kept on this device.'}</p>
|
||||
{#if historyItems.length}
|
||||
<ul class="hist">
|
||||
{#each historyItems as a (a.id)}
|
||||
<li>
|
||||
<a href={a.url} target="_blank" rel="noopener" onclick={() => track('open', a.id)}>{a.title}</a>
|
||||
<span class="hsrc">{a.source}</span>
|
||||
<button class="hx" aria-label="Remove" onclick={() => removeOne(a.id)}>×</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="empty">Nothing yet — stories you open or swap away will appear here.</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{:else if section === 'saved'}
|
||||
{#if !auth.user}
|
||||
<p class="gate">Sign in to save articles and find them here.</p>
|
||||
{:else if savedShown.length}
|
||||
<div class="grid">
|
||||
{#each savedShown as a (a.id)}
|
||||
<ArticleCard article={a} onview={(x) => track('open', x.id)} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if savedReady}
|
||||
<p class="empty">Nothing saved yet — tap <strong>Save</strong> on a story to keep it here.</p>
|
||||
{:else}
|
||||
<p class="muted">Loading…</p>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<!-- profile -->
|
||||
{#if auth.user}
|
||||
<AccountPanel onclose={() => {}} />
|
||||
{:else}
|
||||
<p class="gate">Sign in from the home page to manage your profile, saved articles, and devices.</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.bar {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--line);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
}
|
||||
.bar { background: var(--surface); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 20; }
|
||||
.inner { display: flex; align-items: center; justify-content: space-between; height: 64px; }
|
||||
.logo { height: 40px; width: auto; display: block; }
|
||||
.logo { height: 40px; display: block; }
|
||||
.back { color: var(--accent-deep); font-size: 0.9rem; }
|
||||
.page { padding: 22px 20px 60px; }
|
||||
h1 { font-size: clamp(2rem, 5vw, 2.6rem); margin: 8px 0 14px; }
|
||||
.quick { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }
|
||||
.quick a {
|
||||
.page { padding: 20px 20px 70px; }
|
||||
h1 { font-size: clamp(2rem, 5vw, 2.6rem); margin: 6px 0 16px; }
|
||||
|
||||
/* Desktop: sidebar to the left of the content. Mobile: a top tab strip. */
|
||||
.tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 22px; }
|
||||
.tabs a {
|
||||
border: 1px solid var(--line); background: var(--surface); color: var(--ink);
|
||||
border-radius: 999px; padding: 7px 15px; font-size: 0.9rem;
|
||||
}
|
||||
.quick a:hover { border-color: var(--accent); color: var(--accent-deep); }
|
||||
.quick a.admin { border-color: var(--accent); color: var(--accent-deep); }
|
||||
.tabs a:hover { border-color: var(--accent); color: var(--accent-deep); }
|
||||
.tabs a.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
.tabs a.admin { margin-left: auto; border-color: var(--accent); color: var(--accent-deep); }
|
||||
|
||||
@media (min-width: 760px) {
|
||||
.page { display: grid; grid-template-columns: 180px 1fr; column-gap: 32px; align-items: start; }
|
||||
h1 { grid-column: 1 / -1; }
|
||||
.tabs { flex-direction: column; gap: 4px; position: sticky; top: 80px; }
|
||||
.tabs a { text-align: left; border-color: transparent; }
|
||||
.tabs a.admin { margin-left: 0; margin-top: 10px; }
|
||||
}
|
||||
|
||||
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: 20px 22px; }
|
||||
.phead { display: flex; align-items: baseline; justify-content: space-between; }
|
||||
.phead h2 { font-size: 1.3rem; }
|
||||
.link { background: none; border: none; color: var(--accent-deep); font-size: 0.85rem; text-decoration: underline; cursor: pointer; }
|
||||
.reassure { margin: 4px 0 14px; color: var(--muted); font-size: 0.85rem; }
|
||||
.hist { list-style: none; margin: 0; padding: 0; }
|
||||
.hist li { padding: 8px 0; border-bottom: 1px solid var(--line); display: flex; gap: 12px; align-items: baseline; }
|
||||
.hist li:last-child { border-bottom: none; }
|
||||
.hist a { color: var(--ink); }
|
||||
.hist a:hover { color: var(--accent-deep); }
|
||||
.hsrc { margin-left: auto; color: var(--muted); font-size: 0.78rem; white-space: nowrap; }
|
||||
.hx { background: none; border: none; color: var(--muted); font-size: 1.15rem; line-height: 1; cursor: pointer; padding: 0 2px; }
|
||||
.hx:hover { color: var(--accent-deep); }
|
||||
.empty { margin: 0; color: var(--muted); font-style: italic; font-size: 0.9rem; }
|
||||
.muted { color: var(--muted); }
|
||||
.gate { color: var(--muted); font-size: 1rem; padding: 8px 0; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(238px, 1fr)); gap: 18px; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user