Accounts Phase 1c: sign-in UI, magic-link landing, auth store
- Shared reactive auth store (auth.user) + postJSON helper (sends the cookie). - SignIn modal: email -> "check your inbox" (calm, no password); Google slots in here in Phase 2. - /auth/verify route exchanges the magic-link token for a session, then home. - Header shows "Sign in" or an account avatar; the You sheet gains "Signed in as …" + Sign out (or a Sign in row). Anonymous browsing is unchanged.
This commit is contained in:
@@ -7,6 +7,8 @@
|
||||
import MoodNav from '$lib/components/MoodNav.svelte';
|
||||
import ArticleCard from '$lib/components/ArticleCard.svelte';
|
||||
import BoundariesPanel from '$lib/components/BoundariesPanel.svelte';
|
||||
import SignIn from '$lib/components/SignIn.svelte';
|
||||
import { auth, refresh as refreshAuth, logout as authLogout } from '$lib/auth.svelte.js';
|
||||
|
||||
let moods = $state([]);
|
||||
let topics = $state([]);
|
||||
@@ -19,6 +21,17 @@
|
||||
let showBoundaries = $state(false);
|
||||
let showHistory = $state(false);
|
||||
let showYou = $state(false); // mobile "You" sheet
|
||||
let showSignIn = $state(false);
|
||||
|
||||
function openAccount() {
|
||||
showYou = false;
|
||||
if (auth.user) showYou = true; // account lives in the You sheet
|
||||
else showSignIn = true;
|
||||
}
|
||||
async function signOut() {
|
||||
await authLogout();
|
||||
showYou = false;
|
||||
}
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
|
||||
@@ -227,6 +240,7 @@
|
||||
seenIds = new Set(P.loadJSON(SEEN_KEY, []));
|
||||
dismissed = new Set(P.loadJSON(DISMISSED_KEY, []));
|
||||
history = P.loadJSON(HISTORY_KEY, []);
|
||||
refreshAuth(); // resolve any existing session (non-blocking)
|
||||
try {
|
||||
moods = await getJSON('/api/moods');
|
||||
topics = (await getJSON('/api/categories')).topics;
|
||||
@@ -242,7 +256,15 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Header onBoundaries={() => (showBoundaries = !showBoundaries)} onHistory={() => (showHistory = !showHistory)} {filtersOn} />
|
||||
<Header
|
||||
onBoundaries={() => (showBoundaries = !showBoundaries)}
|
||||
onHistory={() => (showHistory = !showHistory)}
|
||||
onaccount={openAccount}
|
||||
user={auth.user}
|
||||
{filtersOn}
|
||||
/>
|
||||
|
||||
{#if showSignIn}<SignIn onclose={() => (showSignIn = false)} />{/if}
|
||||
|
||||
<main class="container">
|
||||
{#if moods.length}
|
||||
@@ -278,12 +300,22 @@
|
||||
{#if showYou}
|
||||
<section class="panel rise youmenu">
|
||||
<div class="phead"><h2>You</h2><button class="close" onclick={() => (showYou = false)}>done</button></div>
|
||||
{#if auth.user}
|
||||
<div class="acctline">Signed in as <strong>{auth.user.email}</strong></div>
|
||||
{/if}
|
||||
<button class="yourow" onclick={() => { showYou = false; showBoundaries = true; }}>
|
||||
<span>Your boundaries</span>{#if filtersOn}<span class="dot">on</span>{/if}
|
||||
</button>
|
||||
<button class="yourow" onclick={() => { showYou = false; showHistory = true; }}>
|
||||
<span>What you've seen</span>{#if history.length}<span class="dot">{history.length}</span>{/if}
|
||||
</button>
|
||||
{#if auth.user}
|
||||
<button class="yourow" onclick={signOut}><span>Sign out</span></button>
|
||||
{:else}
|
||||
<button class="yourow" onclick={() => { showYou = false; showSignIn = true; }}>
|
||||
<span>Sign in</span><span class="dot">save & sync</span>
|
||||
</button>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -413,6 +445,7 @@
|
||||
}
|
||||
.youmenu .yourow:last-child { border-bottom: none; }
|
||||
.youmenu .dot { background: var(--accent-soft); color: var(--accent-deep); border-radius: 999px; padding: 1px 9px; font-size: 0.78rem; }
|
||||
.youmenu .acctline { color: var(--muted); font-size: 0.85rem; padding: 4px 2px 10px; border-bottom: 1px solid var(--line); }
|
||||
|
||||
.notice {
|
||||
text-align: center; color: var(--accent-deep); background: var(--accent-soft);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { verifyToken } from '$lib/auth.svelte.js';
|
||||
|
||||
let status = $state('verifying'); // verifying | error
|
||||
let error = $state('');
|
||||
|
||||
onMount(async () => {
|
||||
const token = new URLSearchParams(window.location.search).get('token');
|
||||
if (!token) {
|
||||
status = 'error';
|
||||
error = 'This sign-in link is missing its token.';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await verifyToken(token);
|
||||
goto('/', { replaceState: true }); // signed in — back home
|
||||
} catch (e) {
|
||||
status = 'error';
|
||||
error = e?.message || 'This sign-in link is invalid or has expired.';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="container verify">
|
||||
{#if status === 'verifying'}
|
||||
<p class="muted">Signing you in…</p>
|
||||
{:else}
|
||||
<h1>Couldn't sign you in</h1>
|
||||
<p class="muted">{error}</p>
|
||||
<a class="back" href="/">← Back to Upbeat Bytes</a>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.verify {
|
||||
min-height: 60vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 12px;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.back {
|
||||
color: var(--accent-deep);
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user