User feedback + expanded privacy-respecting admin stats
Feedback: - feedback table; POST /api/feedback (anonymous-ok, optional category/email, honeypot + per-day flood cap) stores + emails the admin; GET /api/admin/feedback. - Shared feedback store + FeedbackModal; a speech-bubble opens it from the desktop header, the mobile top bar (logo moves left), the footer, and /account. Feedback section in /admin. Stats (additive, same privacy model — no IP/UA/referrer/raw terms): - Event vocab: summary_viewed (fired on /a load), full_story (card → source), not_today/less_like_this/hide_topic, replace_used/replace_none, paywall_replace, paywalled_source_open. Card title/image opens /a (no double-count); history records via keepalive so it survives the nav. - Dashboard: Accounts card (counts only), reading funnel (summary→source rate), emotional-mix & friction, paywall, returning-visitor buckets. (Health metrics deferred to a future monitoring dashboard.) 131 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 @@
|
||||
<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 to news</a>
|
||||
<div class="baractions">
|
||||
<button class="fb" onclick={openFeedback} aria-label="Share feedback" title="Share feedback">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M4 5h16v11H8l-4 3z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" /></svg>
|
||||
</button>
|
||||
<a class="back" href="/">← Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -73,7 +79,7 @@
|
||||
<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>
|
||||
<a href={a.url} target="_blank" rel="noopener" onclick={() => track('full_story', a.id)}>{a.title}</a>
|
||||
<span class="hsrc">{a.source}</span>
|
||||
<button class="hx" aria-label="Remove" onclick={() => removeOne(a.id)}>×</button>
|
||||
</li>
|
||||
@@ -90,7 +96,7 @@
|
||||
{:else if savedShown.length}
|
||||
<div class="grid">
|
||||
{#each savedShown as a (a.id)}
|
||||
<ArticleCard article={a} onview={(x) => track('open', x.id)} />
|
||||
<ArticleCard article={a} onview={record} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if savedReady}
|
||||
@@ -115,6 +121,8 @@
|
||||
.inner { display: flex; align-items: center; justify-content: space-between; height: 64px; }
|
||||
.logo { height: 40px; display: block; }
|
||||
.back { color: var(--accent-deep); font-size: 0.9rem; }
|
||||
.baractions { display: flex; align-items: center; gap: 14px; }
|
||||
.baractions .fb { background: none; border: none; color: var(--accent-deep); cursor: pointer; display: inline-flex; padding: 2px; }
|
||||
.page { padding: 20px 20px 70px; }
|
||||
h1 { font-size: clamp(2rem, 5vw, 2.6rem); margin: 6px 0 16px; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user