diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index ff5334e..5d00cdd 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -5,12 +5,20 @@ export async function getJSON(url) { } export async function postJSON(url, body) { - const r = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body ?? {}), - credentials: 'same-origin', // send/receive the session cookie - }); + return sendJSON('POST', url, body); +} + +export async function delJSON(url) { + return sendJSON('DELETE', url); +} + +async function sendJSON(method, url, body) { + const opts = { method, credentials: 'same-origin' }; + if (body !== undefined) { + opts.headers = { 'Content-Type': 'application/json' }; + opts.body = JSON.stringify(body); + } + const r = await fetch(url, opts); if (!r.ok) { let msg = r.statusText; try { diff --git a/frontend/src/lib/auth.svelte.js b/frontend/src/lib/auth.svelte.js index fdefa6a..e702a4a 100644 --- a/frontend/src/lib/auth.svelte.js +++ b/frontend/src/lib/auth.svelte.js @@ -1,8 +1,9 @@ -// Shared, reactive auth state (Svelte 5 runes in a module). Components read -// `auth.user` and it stays reactive across the app. -import { getJSON, postJSON } from './api.js'; +// Shared, reactive auth + saved-articles state (Svelte 5 runes in a module). +import { SvelteSet } from 'svelte/reactivity'; +import { getJSON, postJSON, delJSON } from './api.js'; export const auth = $state({ user: null, ready: false }); +export const savedIds = new SvelteSet(); // reactive set of saved article ids export async function refresh() { try { @@ -12,6 +13,34 @@ export async function refresh() { } finally { auth.ready = true; } + if (auth.user) await loadSaved(); + else savedIds.clear(); +} + +export async function loadSaved() { + try { + const ids = await getJSON('/api/saved/ids'); + savedIds.clear(); + for (const id of ids) savedIds.add(id); + } catch { + /* not signed in / transient — leave as-is */ + } +} + +// Optimistic toggle; reverts on failure. +export async function toggleSave(id) { + if (!auth.user) return false; + const willSave = !savedIds.has(id); + if (willSave) savedIds.add(id); + else savedIds.delete(id); + try { + if (willSave) await postJSON(`/api/saved/${id}`, {}); + else await delJSON(`/api/saved/${id}`); + } catch { + if (willSave) savedIds.delete(id); + else savedIds.add(id); + } + return savedIds.has(id); } // Request a magic link. Reply is intentionally identical for any address. @@ -19,10 +48,10 @@ export function startEmail(email) { return postJSON('/api/auth/email/start', { email }); } -// Exchange a magic-link token for a session (cookie set server-side). export async function verifyToken(token) { const res = await postJSON('/api/auth/email/verify', { token }); auth.user = res.user; + await loadSaved(); return res; } @@ -31,5 +60,6 @@ export async function logout() { await postJSON('/api/auth/logout', {}); } finally { auth.user = null; + savedIds.clear(); } } diff --git a/frontend/src/lib/components/ArticleCard.svelte b/frontend/src/lib/components/ArticleCard.svelte index abfa14e..68f7d34 100644 --- a/frontend/src/lib/components/ArticleCard.svelte +++ b/frontend/src/lib/components/ArticleCard.svelte @@ -1,5 +1,8 @@