Customizable nav lanes: pin moods / topics / discovery tags
Readers can now choose which quick-access lanes sit above the feed; "Today" stays pinned. The pool (goodnews/lanes.py, served at /api/lanes) is one source of truth over three lane kinds the feed already renders: moods, primary topics, and high-volume Discovery tags. Selection lives in the existing prefs blob (localStorage + /api/prefs sync); the filter parser ignores the new `lanes` field, so it rides along harmlessly. Default = today's moods, unchanged. Food/Space stay grouping tags rather than primary topics (per review): `space` already existed; added `food` to the Mind & Craft family so the classifier assigns it, and seeded the Food lane by re-tagging the two food sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,17 +6,20 @@
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import BottomNav from '$lib/components/BottomNav.svelte';
|
||||
import MoodNav from '$lib/components/MoodNav.svelte';
|
||||
import LanePicker from '$lib/components/LanePicker.svelte';
|
||||
import ArticleCard from '$lib/components/ArticleCard.svelte';
|
||||
import SignIn from '$lib/components/SignIn.svelte';
|
||||
import SavedFlyout from '$lib/components/SavedFlyout.svelte';
|
||||
import { auth, refresh as refreshAuth } from '$lib/auth.svelte.js';
|
||||
import { prefs, initPrefs, active as prefsActive, applyPrefAction, syncPrefsOnLogin } from '$lib/prefs.svelte.js';
|
||||
import { prefs, initPrefs, active as prefsActive, applyPrefAction, persistPrefs, syncPrefsOnLogin } from '$lib/prefs.svelte.js';
|
||||
import { initHistory, deviceIds, record, loadServerHistory } from '$lib/history.svelte.js';
|
||||
import { trackVisit, track } from '$lib/analytics.js';
|
||||
|
||||
let moods = $state([]);
|
||||
let topics = $state([]);
|
||||
let families = $state([]);
|
||||
let lanePool = $state(null); // /api/lanes: { pinned, default, groups }
|
||||
let showLanes = $state(false);
|
||||
let selected = $state('today'); // 'today' | a mood key | a topic key | 'tag:<slug>'
|
||||
let brief = $state(null);
|
||||
let heroIdx = $state(0);
|
||||
@@ -92,6 +95,33 @@
|
||||
);
|
||||
let activeTab = $derived(selected === 'today' ? 'today' : 'browse');
|
||||
|
||||
// Customizable nav rail: Today is always first, then the reader's pinned
|
||||
// lanes (or the default set if they've never customized). Resolve each key
|
||||
// to its {label, description} from the pool.
|
||||
let laneMap = $derived(
|
||||
new Map(
|
||||
lanePool
|
||||
? [
|
||||
[lanePool.pinned.key, lanePool.pinned],
|
||||
...lanePool.groups.flatMap((g) => g.lanes.map((l) => [l.key, l])),
|
||||
]
|
||||
: []
|
||||
)
|
||||
);
|
||||
let pinnedLaneKeys = $derived(
|
||||
prefs.data.lanes?.length ? prefs.data.lanes : (lanePool?.default ?? [])
|
||||
);
|
||||
let navLanes = $derived(
|
||||
lanePool ? [lanePool.pinned, ...pinnedLaneKeys.map((k) => laneMap.get(k)).filter(Boolean)] : []
|
||||
);
|
||||
|
||||
function saveLanes(keys) {
|
||||
prefs.data.lanes = keys;
|
||||
persistPrefs();
|
||||
// If the reader unpinned the lane they're currently viewing, fall back home.
|
||||
if (selected !== 'today' && !keys.includes(selected)) select('today');
|
||||
}
|
||||
|
||||
// Hero is the only image slot; if its image won't load, promote the next one.
|
||||
let heroArticle = $derived(brief?.items?.[heroIdx] ?? null);
|
||||
let restArticles = $derived((brief?.items ?? []).filter((_, i) => i !== heroIdx));
|
||||
@@ -226,6 +256,7 @@
|
||||
try {
|
||||
moods = await getJSON('/api/moods');
|
||||
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');
|
||||
} catch (e) {
|
||||
@@ -239,10 +270,13 @@
|
||||
|
||||
{#if showSignIn}<SignIn onclose={() => (showSignIn = false)} />{/if}
|
||||
{#if showSaved && auth.user}<SavedFlyout onclose={() => (showSaved = false)} />{/if}
|
||||
{#if showLanes && lanePool}
|
||||
<LanePicker pool={lanePool} selected={pinnedLaneKeys} onsave={saveLanes} onclose={() => (showLanes = false)} />
|
||||
{/if}
|
||||
|
||||
<main class="container">
|
||||
{#if moods.length}
|
||||
<MoodNav {moods} {selected} onselect={select} />
|
||||
{#if navLanes.length}
|
||||
<MoodNav lanes={navLanes} {selected} onselect={select} oncustomize={() => (showLanes = true)} />
|
||||
{/if}
|
||||
|
||||
{#if notice}<p class="notice rise">{notice}</p>{/if}
|
||||
|
||||
Reference in New Issue
Block a user