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
185 lines
6.9 KiB
Svelte
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>
|