User avatar (Google picture), avatar in mobile You tab, /account page
- 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>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { getJSON, postJSON, delJSON } from '$lib/api.js';
|
||||
import { clearLocal } from '$lib/auth.svelte.js';
|
||||
import { clearLocal, logout as authLogout } from '$lib/auth.svelte.js';
|
||||
|
||||
let { onclose } = $props();
|
||||
let info = $state(null);
|
||||
@@ -19,6 +19,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
async function signOut() {
|
||||
await authLogout();
|
||||
onclose?.();
|
||||
}
|
||||
|
||||
async function logoutEverywhere() {
|
||||
busy = 'logout';
|
||||
try {
|
||||
@@ -64,6 +69,7 @@
|
||||
<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'}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script>
|
||||
let { user, size = 30 } = $props();
|
||||
let initial = $derived(((user?.display_name || user?.email || '?').trim()[0] || '?').toUpperCase());
|
||||
let failed = $state(false);
|
||||
// Reset if the picture URL changes (or a different user signs in).
|
||||
$effect(() => {
|
||||
void user?.avatar_url;
|
||||
failed = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if user?.avatar_url && !failed}
|
||||
<img
|
||||
class="av"
|
||||
src={user.avatar_url}
|
||||
alt=""
|
||||
referrerpolicy="no-referrer"
|
||||
style="width:{size}px;height:{size}px"
|
||||
onerror={() => (failed = true)}
|
||||
/>
|
||||
{:else}
|
||||
<span class="av init" style="width:{size}px;height:{size}px;font-size:{size * 0.42}px">{initial}</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.av {
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: cover;
|
||||
flex: none;
|
||||
}
|
||||
.init {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-family: var(--label);
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script>
|
||||
// Mobile-only primary navigation. Today = the brief, Browse = mood/topic
|
||||
// discovery, You = personal controls (Boundaries, History).
|
||||
let { active = 'today', onToday, onBrowse, onYou } = $props();
|
||||
// discovery, You = account + personal controls (shows the user's avatar in).
|
||||
import Avatar from './Avatar.svelte';
|
||||
let { active = 'today', onToday, onBrowse, onYou, user = null } = $props();
|
||||
</script>
|
||||
|
||||
<nav class="bottomnav" aria-label="Primary">
|
||||
@@ -14,7 +15,11 @@
|
||||
<span>Browse</span>
|
||||
</button>
|
||||
<button class:active={active === 'you'} onclick={onYou}>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.8" /><path d="M5 20c0-3.6 3.1-5.5 7-5.5s7 1.9 7 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
|
||||
{#if user}
|
||||
<Avatar {user} size={23} />
|
||||
{:else}
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8.5" r="3.6" fill="none" stroke="currentColor" stroke-width="1.8" /><path d="M5 20c0-3.6 3.1-5.5 7-5.5s7 1.9 7 5.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
|
||||
{/if}
|
||||
<span>You</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Avatar from './Avatar.svelte';
|
||||
let { onBoundaries, onHistory, onaccount, user = null, filtersOn = false } = $props();
|
||||
let initial = $derived((user?.display_name || user?.email || '?').trim()[0].toUpperCase());
|
||||
</script>
|
||||
|
||||
<header class="appbar">
|
||||
@@ -23,7 +23,7 @@
|
||||
</button>
|
||||
{#if user}
|
||||
<button class="acct" onclick={onaccount} title={user.email} aria-label="Your account">
|
||||
<span class="avatar">{initial}</span>
|
||||
<Avatar {user} size={30} />
|
||||
</button>
|
||||
{:else}
|
||||
<button class="signin" onclick={onaccount}>Sign in</button>
|
||||
@@ -59,13 +59,7 @@
|
||||
.utils button.on { color: var(--accent-deep); }
|
||||
.utils .signin { border-color: var(--line); color: var(--accent-deep); }
|
||||
.utils .signin:hover { background: var(--accent-soft); }
|
||||
.acct { padding: 4px; }
|
||||
.avatar {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 30px; height: 30px; border-radius: 999px;
|
||||
background: var(--accent); color: #fff; font-weight: 600; font-size: 0.8rem;
|
||||
font-family: var(--label);
|
||||
}
|
||||
.acct { padding: 4px; display: inline-flex; }
|
||||
|
||||
/* On phones the utilities live in the bottom tab bar ("You") instead. */
|
||||
@media (max-width: 720px) {
|
||||
|
||||
Reference in New Issue
Block a user