diff --git a/frontend/src/lib/analytics.js b/frontend/src/lib/analytics.js index bff4f44..81f724c 100644 --- a/frontend/src/lib/analytics.js +++ b/frontend/src/lib/analytics.js @@ -4,7 +4,7 @@ const VISITOR_KEY = 'goodnews:visitor'; const VISITDAY_KEY = 'goodnews:visitday'; -function visitorId() { +export function visitorId() { try { let v = localStorage.getItem(VISITOR_KEY); if (!v) { diff --git a/frontend/src/lib/components/ArticleCard.svelte b/frontend/src/lib/components/ArticleCard.svelte index 9f1f528..492e66e 100644 --- a/frontend/src/lib/components/ArticleCard.svelte +++ b/frontend/src/lib/components/ArticleCard.svelte @@ -5,8 +5,8 @@ let { article, onaction, onreplace, ontag, onimageerror, onview, hero = false } = $props(); function opened() { + // Records history; the /a page itself fires the summary_viewed event on load. onview?.(article); - track('open', article.id); } let isSaved = $derived(savedIds.has(article.id)); @@ -47,7 +47,8 @@ let summaryHref = $derived('/a/' + article.id); function openedSource() { onview?.(article); - track('open', article.id); + track('full_story', article.id); + if (article.paywalled) track('paywalled_source_open', article.id); } function act(kind, value) { diff --git a/frontend/src/lib/components/FeedbackModal.svelte b/frontend/src/lib/components/FeedbackModal.svelte new file mode 100644 index 0000000..784da61 --- /dev/null +++ b/frontend/src/lib/components/FeedbackModal.svelte @@ -0,0 +1,103 @@ + + + + + + + diff --git a/frontend/src/lib/components/Header.svelte b/frontend/src/lib/components/Header.svelte index 0430256..2af6030 100644 --- a/frontend/src/lib/components/Header.svelte +++ b/frontend/src/lib/components/Header.svelte @@ -1,5 +1,6 @@ @@ -11,24 +12,29 @@ @@ -64,10 +70,11 @@ } .signin:hover { background: var(--accent-soft); } - /* On phones the bottom tab bar handles navigation; keep the bar to the logo. */ + /* On phones the bottom tab bar handles navigation; the top bar keeps just the + logo (left) and the feedback bubble (right). Other utils move to bottom nav. */ @media (max-width: 720px) { - .bar { height: 66px; justify-content: center; } - .utils { display: none; } + .bar { height: 66px; } + .utils .desk { display: none; } .logo { height: 46px; } } diff --git a/frontend/src/lib/feedback.svelte.js b/frontend/src/lib/feedback.svelte.js new file mode 100644 index 0000000..2af8be1 --- /dev/null +++ b/frontend/src/lib/feedback.svelte.js @@ -0,0 +1,9 @@ +// App-wide feedback modal state, openable from any route (header, footer, account). +export const fb = $state({ open: false }); + +export function openFeedback() { + fb.open = true; +} +export function closeFeedback() { + fb.open = false; +} diff --git a/frontend/src/lib/history.svelte.js b/frontend/src/lib/history.svelte.js index 55a2677..e299b1f 100644 --- a/frontend/src/lib/history.svelte.js +++ b/frontend/src/lib/history.svelte.js @@ -3,7 +3,7 @@ // the account when signed in. Only deliberate events live here β€” opened or // replaced-away articles β€” never everything merely shown. import { auth } from './auth.svelte.js'; -import { getJSON, postJSON, delJSON } from './api.js'; +import { getJSON, delJSON } from './api.js'; const KEY = 'goodnews:history'; const CAP = 200; @@ -46,7 +46,15 @@ export function record(article) { if (!history.server.some((h) => h.id === article.id)) { history.server = [article, ...history.server]; } - postJSON('/api/history', { ids: [article.id] }).catch(() => {}); + // keepalive: this often fires on a click that navigates to /a, so it must + // survive the page transition. + fetch('/api/history', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ids: [article.id] }), + credentials: 'same-origin', + keepalive: true, + }).catch(() => {}); } } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index dd3fc69..abd592a 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,12 +1,18 @@ {@render children()} +{#if fb.open}{/if} +
+ + Β· Upbeat Bytes Β· metadata & links only, no stored articles Β· API
@@ -20,6 +26,8 @@ border-top: 1px solid var(--line); } footer.site a { color: var(--accent-deep); } + footer.site .fb { background: none; border: none; color: var(--accent-deep); font: inherit; font-size: 0.82rem; cursor: pointer; text-decoration: underline; padding: 0; } + footer.site .dot { margin: 0 4px; } /* room for the mobile bottom tab bar */ @media (max-width: 720px) { footer.site { padding-bottom: calc(34px + 64px + env(safe-area-inset-bottom)); } diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 39f08dc..55339f3 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -12,7 +12,7 @@ import { auth, refresh as refreshAuth } from '$lib/auth.svelte.js'; import { prefs, initPrefs, active as prefsActive, applyPrefAction, syncPrefsOnLogin } from '$lib/prefs.svelte.js'; import { initHistory, deviceIds, record, loadServerHistory } from '$lib/history.svelte.js'; - import { trackVisit } from '$lib/analytics.js'; + import { trackVisit, track } from '$lib/analytics.js'; let moods = $state([]); let topics = $state([]); @@ -156,8 +156,10 @@ if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' }); } + const MIX_EVENT = { notToday: 'not_today', lessLikeThis: 'less_like_this', alwaysHide: 'hide_topic' }; function applyAction(kind, value) { applyPrefAction(kind, value); // updates + persists + syncs to account + if (MIX_EVENT[kind]) track(MIX_EVENT[kind]); // emotional-mix signal (no value stored) select(selected, true); // re-filter the feed } @@ -182,8 +184,11 @@ } if (!repl) { flash("That's everything fresh for now β€” nothing new to swap in."); + track('replace_none'); return; } + track('replace_used', article.id); + if (article.paywalled) track('paywall_replace', article.id); dismissed.add(article.id); seenIds.add(article.id); markDisplayed([repl]); diff --git a/frontend/src/routes/account/+page.svelte b/frontend/src/routes/account/+page.svelte index dd2c4fe..54da7ee 100644 --- a/frontend/src/routes/account/+page.svelte +++ b/frontend/src/routes/account/+page.svelte @@ -4,8 +4,9 @@ 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 { history, initHistory, loadServerHistory, removeOne, clearAll, record } from '$lib/history.svelte.js'; import { track } from '$lib/analytics.js'; + import { openFeedback } from '$lib/feedback.svelte.js'; import AccountPanel from '$lib/components/AccountPanel.svelte'; import BoundariesPanel from '$lib/components/BoundariesPanel.svelte'; import ArticleCard from '$lib/components/ArticleCard.svelte'; @@ -43,7 +44,12 @@
@@ -73,7 +79,7 @@