stammtisch-hersbruck/frontend/src/routes/dashboard/settings/+page.svelte
Daniel Michelberger 27c83946df feat: Erzeugte Avatare und Verfasser an Kommentaren
Ohne hinterlegtes Bild stehen Initialen auf einem Farbton, der sich aus der
User-ID errechnet: Derselbe Mensch bekommt ueberall dasselbe Bild, ohne dass
es irgendwo gespeichert werden muss. Bewusst kein externer Avatar-Dienst -
der wuerde bei jedem Seitenaufruf verraten, wer hier unterwegs ist.

Kommentare zeigen jetzt ihren Verfasser, aber nur als Bild; der Name steht
im Tooltip. In einer schmalen Spalte neben der Karte ist fuer mehr kein
Platz.

Verwendet wird der Avatar ausserdem in der Kopfzeile, im Profil und in der
Mitgliederliste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
2026-09-02 13:13:22 +02:00

185 lines
6.9 KiB
Svelte

<script lang="ts">
/**
* Eigenes Profil. Die users-Collection erlaubt Änderungen nur am eigenen
* Datensatz (updateRule `id = @request.auth.id`) — was hier steht, kann
* also niemand für jemand anderen ändern.
*/
import { api, auth } from '$lib/stores/pocketbase.svelte'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import Avatar from '$lib/components/Avatar.svelte'
import { KeyRound, User } from 'lucide-svelte'
let name = $state(auth.user?.name ?? '')
let avatarFile = $state<File | null>(null)
let savingProfile = $state(false)
let profileMessage = $state<string | null>(null)
let profileError = $state<string | null>(null)
async function saveProfile() {
if (!auth.user) return
savingProfile = true
profileMessage = null
profileError = null
try {
// Mit Datei als FormData, sonst als einfaches Objekt — ein leeres
// avatar-Feld im FormData würde ein vorhandenes Bild löschen.
let data: FormData | Record<string, unknown>
if (avatarFile) {
const fd = new FormData()
fd.append('name', name.trim())
fd.append('avatar', avatarFile)
data = fd
} else {
data = { name: name.trim() }
}
await api.collection('users').update(auth.user.id, data)
await auth.refresh()
avatarFile = null
profileMessage = 'Profil gespeichert.'
} catch (e: any) {
profileError = e.message ?? 'Das Profil konnte nicht gespeichert werden.'
} finally {
savingProfile = false
}
}
let oldPassword = $state('')
let newPassword = $state('')
let confirmPassword = $state('')
let savingPassword = $state(false)
let passwordMessage = $state<string | null>(null)
let passwordError = $state<string | null>(null)
async function savePassword() {
if (!auth.user) return
if (newPassword.length < 8) {
passwordError = 'Das neue Passwort braucht mindestens 8 Zeichen.'
return
}
if (newPassword !== confirmPassword) {
passwordError = 'Die Wiederholung stimmt nicht überein.'
return
}
savingPassword = true
passwordMessage = null
passwordError = null
try {
await api.collection('users').update(auth.user.id, {
oldPassword,
password: newPassword,
passwordConfirm: confirmPassword,
})
// PocketBase entwertet mit dem Passwortwechsel alle Tokens; ohne
// neue Anmeldung wäre die Sitzung ab hier tot.
await api.collection('users').authWithPassword(auth.user.email as string, newPassword)
if (auth.cookie) {
document.cookie = api.authStore.exportToCookie({ httpOnly: false })
}
oldPassword = newPassword = confirmPassword = ''
passwordMessage = 'Passwort geändert.'
} catch (e: any) {
passwordError = e.message ?? 'Das Passwort konnte nicht geändert werden.'
} finally {
savingPassword = false
}
}
</script>
<div class="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<User class="size-4 text-primary" />
Konto
</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="flex items-center gap-4">
<Avatar user={auth.user} size={64} />
<div class="space-y-1">
<Label for="avatar">Bild</Label>
<input
id="avatar"
type="file"
accept="image/*"
class="block w-full text-sm"
onchange={(e) => (avatarFile = e.currentTarget.files?.[0] ?? null)}
/>
<p class="text-xs text-muted-foreground">
Ohne Bild stehen deine Initialen da.
</p>
</div>
</div>
<div class="space-y-2">
<Label for="profile-name">Name</Label>
<Input id="profile-name" bind:value={name} placeholder="Wie du angezeigt wirst" />
</div>
<div class="space-y-2">
<Label for="profile-mail">E-Mail</Label>
<Input id="profile-mail" value={auth.user?.email ?? ''} readonly disabled />
<p class="text-xs text-muted-foreground">
Die E-Mail-Adresse ist der Anmeldename. Ändern kann sie nur die
Team-Administration über PocketBase.
</p>
</div>
{#if profileError}
<p class="text-sm text-destructive">{profileError}</p>
{:else if profileMessage}
<p class="text-sm text-muted-foreground">{profileMessage}</p>
{/if}
<Button onclick={saveProfile} disabled={savingProfile}>
{savingProfile ? 'Wird gespeichert …' : 'Speichern'}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2 text-base">
<KeyRound class="size-4 text-primary" />
Passwort ändern
</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label for="pw-old">Aktuelles Passwort</Label>
<Input id="pw-old" type="password" bind:value={oldPassword} autocomplete="current-password" />
</div>
<div class="space-y-2">
<Label for="pw-new">Neues Passwort</Label>
<Input id="pw-new" type="password" bind:value={newPassword} autocomplete="new-password" />
</div>
<div class="space-y-2">
<Label for="pw-confirm">Wiederholen</Label>
<Input id="pw-confirm" type="password" bind:value={confirmPassword} autocomplete="new-password" />
</div>
{#if passwordError}
<p class="text-sm text-destructive">{passwordError}</p>
{:else if passwordMessage}
<p class="text-sm text-muted-foreground">{passwordMessage}</p>
{/if}
<Button
onclick={savePassword}
disabled={savingPassword || !oldPassword || !newPassword}
>
{savingPassword ? 'Wird geändert …' : 'Passwort ändern'}
</Button>
</CardContent>
</Card>
</div>