feat: implement My Account page
Add /account route showing display name (from localStorage), role badge, session expiry decoded from JWT, and recovery PIN display with copy button. Join and recover flows now persist display_name to localStorage via setAuth(). Feed header logout button replaced with person-icon link to /account; logout is available from the account page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
137
frontend/src/routes/account/+page.svelte
Normal file
137
frontend/src/routes/account/+page.svelte
Normal file
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { getToken, getPin, getDisplayName, getExpiry, getRole, clearAuth } from '$lib/auth';
|
||||
import { api } from '$lib/api';
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let pin = $state<string | null>(null);
|
||||
let displayName = $state<string | null>(null);
|
||||
let role = $state<'guest' | 'host' | 'admin' | null>(null);
|
||||
let expiry = $state<Date | null>(null);
|
||||
let copied = $state(false);
|
||||
let pinCopied = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
if (!getToken()) {
|
||||
goto('/join');
|
||||
return;
|
||||
}
|
||||
pin = getPin();
|
||||
displayName = getDisplayName();
|
||||
role = getRole();
|
||||
expiry = getExpiry();
|
||||
});
|
||||
|
||||
function copyPin() {
|
||||
if (!pin) return;
|
||||
navigator.clipboard.writeText(pin);
|
||||
pinCopied = true;
|
||||
setTimeout(() => (pinCopied = false), 2000);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
try { await api.delete('/session'); } catch { /* ignore */ }
|
||||
clearAuth();
|
||||
goto('/join');
|
||||
}
|
||||
|
||||
function formatDate(d: Date): string {
|
||||
return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
function roleLabel(r: string | null): string {
|
||||
switch (r) {
|
||||
case 'admin': return 'Admin';
|
||||
case 'host': return 'Gastgeber';
|
||||
default: return 'Gast';
|
||||
}
|
||||
}
|
||||
|
||||
function roleColor(r: string | null): string {
|
||||
switch (r) {
|
||||
case 'admin': return 'bg-red-100 text-red-700';
|
||||
case 'host': return 'bg-purple-100 text-purple-700';
|
||||
default: return 'bg-blue-100 text-blue-700';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<!-- Header -->
|
||||
<div class="border-b border-gray-200 bg-white">
|
||||
<div class="mx-auto flex max-w-lg items-center justify-between px-4 py-4">
|
||||
<h1 class="text-xl font-bold text-gray-900">Mein Konto</h1>
|
||||
<a href="/feed" class="text-sm text-blue-600 hover:underline">Zur Galerie</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-lg space-y-4 p-4">
|
||||
<!-- Profile card -->
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 text-xl font-bold text-blue-600">
|
||||
{#if displayName}
|
||||
{displayName[0].toUpperCase()}
|
||||
{:else}
|
||||
?
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900">{displayName ?? 'Unbekannt'}</p>
|
||||
<span class="inline-block rounded-full px-2 py-0.5 text-xs font-medium {roleColor(role)}">
|
||||
{roleLabel(role)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if expiry}
|
||||
<p class="mt-3 text-xs text-gray-400">Sitzung gültig bis {formatDate(expiry)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- PIN card -->
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50 p-5">
|
||||
<h2 class="mb-1 font-semibold text-amber-900">Wiederherstellungs-PIN</h2>
|
||||
<p class="mb-3 text-sm text-amber-700">
|
||||
Du brauchst diesen PIN, um dein Konto auf einem anderen Gerät wiederherzustellen. Schreib ihn auf!
|
||||
</p>
|
||||
{#if pin}
|
||||
<div class="flex items-center justify-between rounded-lg bg-white px-4 py-3 shadow-sm">
|
||||
<span class="font-mono text-4xl font-bold tracking-widest text-gray-900">{pin}</span>
|
||||
<button
|
||||
onclick={copyPin}
|
||||
class="rounded-md bg-amber-100 px-3 py-1.5 text-sm font-medium text-amber-800 transition hover:bg-amber-200"
|
||||
>
|
||||
{pinCopied ? 'Kopiert!' : 'Kopieren'}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="rounded-lg bg-white px-4 py-3 text-sm text-gray-400 shadow-sm">
|
||||
PIN nicht gespeichert. Nutze die Wiederherstellungs-Seite, um dich mit deinem PIN anzumelden.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Recovery hint -->
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-5">
|
||||
<h2 class="mb-1 font-semibold text-gray-900">Gerät wechseln?</h2>
|
||||
<p class="text-sm text-gray-600">
|
||||
Auf einem anderen Gerät kannst du dein Konto mit deinem Namen und PIN wiederherstellen.
|
||||
</p>
|
||||
<a
|
||||
href="/recover"
|
||||
class="mt-3 inline-block text-sm font-medium text-blue-600 hover:underline"
|
||||
>
|
||||
Zur Wiederherstellungs-Seite →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Logout -->
|
||||
<button
|
||||
onclick={handleLogout}
|
||||
class="w-full rounded-xl border border-red-200 bg-white py-3 text-sm font-medium text-red-600 transition hover:bg-red-50"
|
||||
>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user