In-feed Back button + clickable source on the article page
* Back button on feed views: drilling into a tag or source from a card now remembers where you came from (a small history stack), and a "← Back" appears in the view header to return there — chains of drill-ins included. Top-level nav (rail/bottom bar) resets the history. * Article page: the source name is now a link into that source's in-app feed (/?source=<id>); the SPA reads the param on load and opens the source view (label falls back to the loaded feed's source name). Completes the "cards-only v1" — source is clickable on /a/ too now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,7 +99,7 @@
|
||||
);
|
||||
let viewLabel = $derived(
|
||||
selected === 'today' ? 'Highlights from Today'
|
||||
: selected.startsWith('source:') ? (currentSource?.name ?? 'Source')
|
||||
: selected.startsWith('source:') ? (currentSource?.name ?? feed[0]?.source ?? 'Source')
|
||||
: selected === 'latest' ? 'Latest'
|
||||
: currentTag ? humanize(currentTag)
|
||||
: (currentMood?.label ?? cap(currentTopic?.key) ?? '')
|
||||
@@ -208,13 +208,26 @@
|
||||
return `/api/feed?limit=${PAGE}&offset=${offset}${q ? '&' + q : ''}${exq}`;
|
||||
}
|
||||
|
||||
// Clicking a source name opens its publication feed; stash the name for the header.
|
||||
function selectSource(id, name) {
|
||||
currentSource = { id, name };
|
||||
select('source:' + id);
|
||||
// Drill into a tag or source view from a card, remembering where we came from
|
||||
// so the in-feed Back button can return there (chains of drill-ins too).
|
||||
let navStack = $state([]); // [{ key, source }] views to return to
|
||||
function drill(key, source = null) {
|
||||
navStack = [...navStack, { key: selected, source: currentSource }];
|
||||
if (source) currentSource = source;
|
||||
select(key);
|
||||
}
|
||||
function goBack() {
|
||||
const prev = navStack[navStack.length - 1];
|
||||
if (!prev) return;
|
||||
navStack = navStack.slice(0, -1);
|
||||
currentSource = prev.source;
|
||||
select(prev.key);
|
||||
}
|
||||
|
||||
async function select(key, fresh = false) {
|
||||
// Top-level navigation (nav rail / bottom bar) resets the drill-in history;
|
||||
// only tag/source drill-ins build it up.
|
||||
if (!key.startsWith('tag:') && !key.startsWith('source:')) navStack = [];
|
||||
selected = key;
|
||||
error = '';
|
||||
feedDone = false;
|
||||
@@ -324,7 +337,13 @@
|
||||
topics = (await getJSON('/api/categories')).topics;
|
||||
try { lanePool = await getJSON('/api/lanes'); } catch { lanePool = null; }
|
||||
try { families = await getJSON('/api/families'); } catch { families = []; }
|
||||
await select('today');
|
||||
// Deep link from an article page's source name (/?source=<id>) → its feed.
|
||||
const srcParam = typeof location !== 'undefined' && new URLSearchParams(location.search).get('source');
|
||||
if (srcParam && /^\d+$/.test(srcParam)) {
|
||||
await select('source:' + srcParam);
|
||||
} else {
|
||||
await select('today');
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Could not reach Upbeat Bytes.';
|
||||
}
|
||||
@@ -354,6 +373,12 @@
|
||||
{:else}
|
||||
{#key selected}
|
||||
<header class="view-head rise">
|
||||
{#if navStack.length}
|
||||
<button class="viewback" onclick={goBack} aria-label="Go back">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Back
|
||||
</button>
|
||||
{/if}
|
||||
<h1>{viewLabel}</h1>
|
||||
{#if viewSubtitle}<p class="sub">{viewSubtitle}</p>{/if}
|
||||
</header>
|
||||
@@ -361,11 +386,11 @@
|
||||
{#if selected === 'today'}
|
||||
{#if brief?.items?.length}
|
||||
<section class="rise">
|
||||
<ArticleCard article={heroArticle} hero onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onsource={selectSource} onview={record} onimageerror={heroImageFailed} />
|
||||
<ArticleCard article={heroArticle} hero onaction={applyAction} onreplace={replaceArticle} ontag={(t) => drill('tag:' + t)} onsource={(id, name) => drill('source:' + id, { id, name })} onview={record} onimageerror={heroImageFailed} />
|
||||
{#if restArticles.length}
|
||||
<div class="grid rest">
|
||||
{#each restArticles as a (a.id)}
|
||||
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onsource={selectSource} onview={record} />
|
||||
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => drill('tag:' + t)} onsource={(id, name) => drill('source:' + id, { id, name })} onview={record} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -377,7 +402,7 @@
|
||||
{:else if feed.length}
|
||||
<div class="grid rise">
|
||||
{#each feed as a (a.id)}
|
||||
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onsource={selectSource} onview={record} />
|
||||
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => drill('tag:' + t)} onsource={(id, name) => drill('source:' + id, { id, name })} onview={record} />
|
||||
{/each}
|
||||
</div>
|
||||
{#if !feedDone}
|
||||
@@ -406,7 +431,7 @@
|
||||
<p class="fdesc">{f.description}</p>
|
||||
<div class="chips">
|
||||
{#each tags as t (t.key)}
|
||||
<button class="chip" class:active={selected === 'tag:' + t.key} onclick={() => select('tag:' + t.key)}>{humanize(t.key)}</button>
|
||||
<button class="chip" class:active={selected === 'tag:' + t.key} onclick={() => drill('tag:' + t.key)}>{humanize(t.key)}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -424,6 +449,14 @@
|
||||
main.container { padding-top: 6px; padding-bottom: 40px; min-height: 60vh; }
|
||||
|
||||
.view-head { margin: 18px 0 18px; }
|
||||
.viewback {
|
||||
display: inline-flex; align-items: center; gap: 5px; margin-bottom: 10px;
|
||||
background: none; border: 1px solid var(--line); color: var(--accent-deep);
|
||||
border-radius: 999px; padding: 6px 14px; font-size: 0.85rem; cursor: pointer;
|
||||
transition: border-color 0.14s ease;
|
||||
}
|
||||
.viewback:hover { border-color: var(--accent); }
|
||||
.viewback svg { width: 16px; height: 16px; display: block; }
|
||||
.view-head h1 { font-size: clamp(2.1rem, 5.5vw, 2.8rem); line-height: 1.05; text-transform: capitalize; }
|
||||
.view-head .sub { margin: 8px 0 0; color: var(--muted); font-size: 1.02rem; }
|
||||
.view-head::after {
|
||||
|
||||
Reference in New Issue
Block a user