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
78 lines
2.4 KiB
Svelte
78 lines
2.4 KiB
Svelte
<script lang="ts">
|
|
/**
|
|
* Bild eines Nutzers — und wenn keins hinterlegt ist, ein erzeugtes aus
|
|
* Initialen und einer Farbe.
|
|
*
|
|
* Die Farbe kommt aus einer Prüfsumme über die ID: Derselbe Mensch bekommt
|
|
* damit überall dasselbe Bild, ohne dass es irgendwo gespeichert werden
|
|
* müsste. Bewusst kein externer Avatar-Dienst — der würde bei jedem
|
|
* Seitenaufruf verraten, wer hier gerade unterwegs ist.
|
|
*/
|
|
import { getFileURL } from '$lib/stores/pocketbase.svelte'
|
|
|
|
let {
|
|
user = null,
|
|
size = 32,
|
|
class: className = '',
|
|
}: {
|
|
user?: { id?: string; name?: string; email?: string; avatar?: string } | null
|
|
size?: number
|
|
class?: string
|
|
} = $props()
|
|
|
|
const label = $derived(user?.name?.trim() || user?.email?.trim() || '')
|
|
|
|
/** Erste Buchstaben der ersten beiden Wörter, sonst der erste des Namens. */
|
|
const initials = $derived.by(() => {
|
|
if (!label) return '?'
|
|
|
|
const words = label.split(/[\s.@_-]+/).filter(Boolean)
|
|
if (words.length === 0) return '?'
|
|
if (words.length === 1) return words[0].slice(0, 2).toUpperCase()
|
|
|
|
return (words[0][0] + words[1][0]).toUpperCase()
|
|
})
|
|
|
|
/**
|
|
* Farbton aus der ID, hilfsweise aus dem Namen. Sättigung und Helligkeit
|
|
* bleiben fest, damit weiße Schrift auf jedem Ton lesbar bleibt — auch im
|
|
* Dunkelmodus, wo eine zufällige Helligkeit schnell blendet.
|
|
*/
|
|
const hue = $derived.by(() => {
|
|
const seed = user?.id || label || '?'
|
|
let h = 0
|
|
for (let i = 0; i < seed.length; i++) {
|
|
h = (h * 31 + seed.charCodeAt(i)) % 360
|
|
}
|
|
return h
|
|
})
|
|
|
|
const src = $derived(
|
|
user?.avatar
|
|
? getFileURL(user as object, user.avatar, { thumb: `${size * 2}x${size * 2}` })
|
|
: null,
|
|
)
|
|
</script>
|
|
|
|
{#if src}
|
|
<img
|
|
{src}
|
|
alt={label}
|
|
width={size}
|
|
height={size}
|
|
class="shrink-0 rounded-full object-cover {className}"
|
|
style:width="{size}px"
|
|
style:height="{size}px"
|
|
/>
|
|
{:else}
|
|
<span
|
|
aria-hidden="true"
|
|
class="grid shrink-0 place-items-center rounded-full font-medium text-white select-none {className}"
|
|
style:width="{size}px"
|
|
style:height="{size}px"
|
|
style:background="hsl({hue} 55% 42%)"
|
|
style:font-size="{Math.round(size * 0.4)}px"
|
|
>
|
|
{initials}
|
|
</span>
|
|
{/if}
|