15728c3bcb
- Capture the Google profile picture (picture claim) into users.avatar_url; an Avatar component shows it, falling back to the initial. Used in the desktop header and the mobile "You" tab (which now shows the user when signed in). - Move account/settings to its own route /account (robust + scrolls to top), reached by the desktop avatar and the mobile You tab; drop the inline "You" sheet. AccountPanel gains a Sign out action; the page links to Saved/History/ Boundaries via home intent params (?view= / ?open=). - db: users.avatar_url (schema + idempotent migration). 118 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
126 lines
4.2 KiB
Svelte
126 lines
4.2 KiB
Svelte
<script>
|
|
import { onMount } from 'svelte';
|
|
import { getJSON, postJSON, delJSON } from '$lib/api.js';
|
|
import { clearLocal, logout as authLogout } from '$lib/auth.svelte.js';
|
|
|
|
let { onclose } = $props();
|
|
let info = $state(null);
|
|
let error = $state('');
|
|
let confirmingDelete = $state(false);
|
|
let busy = $state('');
|
|
|
|
const PROVIDER_LABEL = { email: 'Email link', google: 'Google', apple: 'Apple' };
|
|
|
|
onMount(async () => {
|
|
try {
|
|
info = await getJSON('/api/account');
|
|
} catch {
|
|
error = "Couldn't load your account.";
|
|
}
|
|
});
|
|
|
|
async function signOut() {
|
|
await authLogout();
|
|
onclose?.();
|
|
}
|
|
|
|
async function logoutEverywhere() {
|
|
busy = 'logout';
|
|
try {
|
|
await postJSON('/api/account/logout-all', {});
|
|
clearLocal();
|
|
onclose?.();
|
|
} catch {
|
|
error = 'Could not sign out everywhere — try again.';
|
|
} finally {
|
|
busy = '';
|
|
}
|
|
}
|
|
|
|
async function deleteAccount() {
|
|
busy = 'delete';
|
|
try {
|
|
await delJSON('/api/account');
|
|
clearLocal();
|
|
onclose?.();
|
|
} catch {
|
|
error = 'Could not delete the account — try again.';
|
|
} finally {
|
|
busy = '';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<section class="panel rise">
|
|
<div class="phead">
|
|
<h2>Account</h2>
|
|
<button class="close" onclick={onclose}>done</button>
|
|
</div>
|
|
|
|
{#if error}<p class="err">{error}</p>{/if}
|
|
|
|
{#if info}
|
|
<div class="row"><span class="k">Email</span><span class="v">{info.user.email}</span></div>
|
|
<div class="row">
|
|
<span class="k">Sign-in methods</span>
|
|
<span class="v">{info.providers.map((p) => PROVIDER_LABEL[p] ?? p).join(', ')}</span>
|
|
</div>
|
|
<div class="row"><span class="k">Saved articles</span><span class="v">{info.saved_count}</span></div>
|
|
<div class="row"><span class="k">Active sessions</span><span class="v">{info.sessions}</span></div>
|
|
|
|
<div class="actions">
|
|
<button class="btn" onclick={signOut}>Sign out</button>
|
|
<a class="btn" href="/api/account/export">Export my data</a>
|
|
<button class="btn" onclick={logoutEverywhere} disabled={busy === 'logout'}>
|
|
{busy === 'logout' ? 'Signing out…' : 'Sign out everywhere'}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="danger">
|
|
{#if !confirmingDelete}
|
|
<button class="link-danger" onclick={() => (confirmingDelete = true)}>Delete my account</button>
|
|
{:else}
|
|
<p class="warn">This permanently removes your account, saved articles, and history. This can't be undone.</p>
|
|
<div class="actions">
|
|
<button class="btn danger-btn" onclick={deleteAccount} disabled={busy === 'delete'}>
|
|
{busy === 'delete' ? 'Deleting…' : 'Yes, delete everything'}
|
|
</button>
|
|
<button class="btn" onclick={() => (confirmingDelete = false)}>Cancel</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<p class="fine">Browsing without an account still works — signing out keeps your saved items safe on the server for next time.</p>
|
|
{:else if !error}
|
|
<p class="muted">Loading…</p>
|
|
{/if}
|
|
</section>
|
|
|
|
<style>
|
|
.row {
|
|
display: flex; justify-content: space-between; gap: 16px;
|
|
padding: 10px 2px; border-bottom: 1px solid var(--line); font-size: 0.92rem;
|
|
}
|
|
.row .k { color: var(--muted); }
|
|
.row .v { color: var(--ink); text-align: right; }
|
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0 6px; }
|
|
.btn {
|
|
font: inherit; font-size: 0.86rem; font-weight: 600;
|
|
border: 1px solid var(--line); border-radius: 999px; padding: 8px 14px;
|
|
background: var(--surface); color: var(--accent-deep); cursor: pointer; text-decoration: none;
|
|
}
|
|
.btn:hover { background: var(--accent-soft); }
|
|
.btn:disabled { opacity: 0.6; cursor: default; }
|
|
.danger { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); }
|
|
.link-danger {
|
|
background: none; border: none; color: #9a3b3b; font-size: 0.86rem;
|
|
text-decoration: underline; cursor: pointer; padding: 0;
|
|
}
|
|
.warn { color: #9a3b3b; font-size: 0.88rem; margin: 0 0 10px; }
|
|
.danger-btn { color: #fff; background: #9a3b3b; border-color: #9a3b3b; }
|
|
.danger-btn:hover { background: #843232; }
|
|
.err { color: #9a3b3b; font-size: 0.86rem; }
|
|
.fine { margin-top: 16px; color: var(--muted); font-size: 0.8rem; }
|
|
.muted { color: var(--muted); }
|
|
</style>
|