feat(avatar) : add UserAvatar component with image/initials fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 21:55:52 +01:00
parent 5613a7c92b
commit cea22f977b

View File

@@ -0,0 +1,55 @@
<template>
<span
class="inline-flex shrink-0 items-center justify-center rounded-full"
:class="sizeClasses"
:title="user.username"
>
<img
v-if="user.avatarUrl && !imgError"
:src="user.avatarUrl"
:alt="user.username"
class="h-full w-full rounded-full object-cover"
@error="imgError = true"
/>
<span
v-else
class="flex h-full w-full items-center justify-center rounded-full bg-primary-500 font-bold text-white"
:class="textSizeClass"
>
{{ user.username.substring(0, 2).toUpperCase() }}
</span>
</span>
</template>
<script setup lang="ts">
const props = defineProps<{
user: { id?: number; username: string; avatarUrl?: string | null }
size?: 'xs' | 'sm' | 'md' | 'lg'
}>()
const imgError = ref(false)
watch(() => props.user.avatarUrl, () => {
imgError.value = false
})
const sizeClasses = computed(() => {
const map = {
xs: 'h-5 w-5',
sm: 'h-6 w-6',
md: 'h-8 w-8',
lg: 'h-12 w-12',
}
return map[props.size ?? 'sm']
})
const textSizeClass = computed(() => {
const map = {
xs: 'text-[10px]',
sm: 'text-xs',
md: 'text-sm',
lg: 'text-base',
}
return map[props.size ?? 'sm']
})
</script>