89 lines
2.1 KiB
TypeScript
89 lines
2.1 KiB
TypeScript
// components/ui/UserAvatar.tsx
|
||
'use client';
|
||
|
||
import * as React from 'react';
|
||
import clsx from 'clsx';
|
||
|
||
const AVATAR_COLORS = [
|
||
'bg-orange-500',
|
||
'bg-indigo-500',
|
||
'bg-emerald-500',
|
||
'bg-sky-500',
|
||
'bg-rose-500',
|
||
'bg-amber-500',
|
||
'bg-violet-500',
|
||
];
|
||
|
||
function getAvatarColor(seed: string) {
|
||
let hash = 0;
|
||
for (let i = 0; i < seed.length; i++) {
|
||
hash = (hash * 31 + seed.charCodeAt(i)) | 0;
|
||
}
|
||
const index = Math.abs(hash) % AVATAR_COLORS.length;
|
||
return AVATAR_COLORS[index];
|
||
}
|
||
|
||
type Size = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
||
|
||
const sizeClasses: Record<Size, string> = {
|
||
sm: 'h-6 w-6 text-[10px]',
|
||
md: 'h-8 w-8 text-xs',
|
||
lg: 'h-10 w-10 text-sm',
|
||
xl: 'h-12 w-12 text-md',
|
||
'2xl': 'h-14 w-14 text-lg',
|
||
'3xl': 'h-16 w-16 text-xl',
|
||
};
|
||
|
||
export type UserAvatarProps = {
|
||
/** Für Initialen + Farbberechnung, z.B. Arbeitsname */
|
||
name?: string | null;
|
||
/** Optionales Bild – wenn gesetzt, wird das Bild angezeigt */
|
||
avatarUrl?: string | null;
|
||
/** Größe des Avatars */
|
||
size?: Size;
|
||
};
|
||
|
||
export default function UserAvatar({
|
||
name,
|
||
avatarUrl,
|
||
size = 'md',
|
||
}: UserAvatarProps) {
|
||
const displayName = (name ?? '').trim();
|
||
const initial = displayName.charAt(0)?.toUpperCase() || '?';
|
||
const colorClass = getAvatarColor(displayName || initial || 'x');
|
||
|
||
// Wenn Bild-URL gesetzt, aber das Laden fehlschlägt → Fallback auf Initialen
|
||
const [hasImageError, setHasImageError] = React.useState(false);
|
||
|
||
React.useEffect(() => {
|
||
// Wenn sich avatarUrl ändert, Fehlerzustand zurücksetzen
|
||
setHasImageError(false);
|
||
}, [avatarUrl]);
|
||
|
||
const showImage = !!avatarUrl && !hasImageError;
|
||
|
||
if (showImage) {
|
||
return (
|
||
<img
|
||
src={avatarUrl!}
|
||
alt={displayName || 'Avatar'}
|
||
onError={() => setHasImageError(true)}
|
||
className={clsx('rounded-full object-cover', sizeClasses[size])}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// Fallback: Initialen mit pseudo-zufälliger Hintergrundfarbe
|
||
return (
|
||
<div
|
||
className={clsx(
|
||
'flex items-center justify-center rounded-full font-semibold text-white',
|
||
sizeClasses[size],
|
||
colorClass,
|
||
)}
|
||
>
|
||
{initial}
|
||
</div>
|
||
);
|
||
}
|