Files
MindSift/frontend/src/components/UserAvatar.tsx
T
sandrodach 444442fdf0
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
feat: integrate user profile pictures with circular avatar display and upload in settings
2026-10-25 21:37:06 +01:00

158 lines
4.9 KiB
TypeScript

import React, { useState } from 'react';
export interface UserAvatarProps {
user?: {
displayName?: string;
email?: string;
avatarUrl?: string;
} | null;
name?: string;
avatarUrl?: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
className?: string;
alt?: string;
}
const SIZE_MAP = {
xs: 'w-5 h-5 text-[10px]',
sm: 'w-7 h-7 text-xs',
md: 'w-9 h-9 text-sm',
lg: 'w-12 h-12 text-base',
xl: 'w-20 h-20 text-2xl',
};
const BG_COLORS = [
'bg-primary/20 text-primary border-primary/30',
'bg-secondary/20 text-secondary border-secondary/30',
'bg-accent/20 text-accent border-accent/30',
'bg-info/20 text-info border-info/30',
'bg-success/20 text-success border-success/30',
'bg-warning/20 text-warning border-warning/30',
];
function getInitials(name?: string, email?: string): string {
const cleanName = (name || '').trim();
if (cleanName) {
const parts = cleanName.split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return cleanName.slice(0, 2).toUpperCase();
}
const cleanEmail = (email || '').trim();
if (cleanEmail) {
return cleanEmail.slice(0, 2).toUpperCase();
}
return '?';
}
function getColorIndex(str: string): number {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
return Math.abs(hash) % BG_COLORS.length;
}
export function isEmoji(str?: string): boolean {
if (!str) return false;
const trimmed = str.trim();
// Simple heuristic: length <= 8 and not a URL or base64 image
return !trimmed.startsWith('data:') && !trimmed.startsWith('http') && !trimmed.startsWith('/') && trimmed.length <= 8;
}
export const UserAvatar: React.FC<UserAvatarProps> = ({
user,
name,
avatarUrl,
size = 'md',
className = '',
alt,
}) => {
const [imageError, setImageError] = useState(false);
const effectiveAvatarUrl = avatarUrl !== undefined ? avatarUrl : user?.avatarUrl;
const effectiveName = name !== undefined ? name : (user?.displayName || user?.email || '');
const effectiveEmail = user?.email || '';
const sizeClass = SIZE_MAP[size] || SIZE_MAP.md;
const initials = getInitials(effectiveName, effectiveEmail);
const colorIndex = getColorIndex(effectiveName || effectiveEmail || 'user');
const colorClass = BG_COLORS[colorIndex];
const hasImage = Boolean(
effectiveAvatarUrl &&
!imageError &&
(effectiveAvatarUrl.startsWith('data:image/') ||
effectiveAvatarUrl.startsWith('http://') ||
effectiveAvatarUrl.startsWith('https://') ||
effectiveAvatarUrl.startsWith('/'))
);
const isEmojiAvatar = Boolean(effectiveAvatarUrl && isEmoji(effectiveAvatarUrl));
return (
<div
className={`inline-flex items-center justify-center rounded-full aspect-square shrink-0 select-none overflow-hidden font-semibold transition-all ${sizeClass} ${className}`}
title={effectiveName || alt}
>
{hasImage ? (
<img
src={effectiveAvatarUrl}
alt={alt || effectiveName || 'Avatar'}
onError={() => setImageError(true)}
className="w-full h-full object-cover rounded-full"
/>
) : isEmojiAvatar ? (
<div className="w-full h-full flex items-center justify-center bg-base-300/80 rounded-full">
<span className="leading-none">{effectiveAvatarUrl}</span>
</div>
) : (
<div className={`w-full h-full flex items-center justify-center rounded-full border ${colorClass}`}>
<span className="leading-none tracking-tight">{initials}</span>
</div>
)}
</div>
);
};
/**
* Resizes and center-crops an uploaded image file to a compact base64 Data URL.
*/
export async function resizeImageToDataUrl(file: File, maxSize = 256): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error('Fehler beim Lesen der Datei'));
reader.onload = (e) => {
const img = new Image();
img.onerror = () => reject(new Error('Fehler beim Laden des Bildes'));
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = maxSize;
canvas.height = maxSize;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Canvas Context konnte nicht erstellt werden'));
return;
}
// Center crop calculation
const minDim = Math.min(img.width, img.height);
const sx = (img.width - minDim) / 2;
const sy = (img.height - minDim) / 2;
ctx.drawImage(img, sx, sy, minDim, minDim, 0, 0, maxSize, maxSize);
// Try webp first, fallback to jpeg
let dataUrl = canvas.toDataURL('image/webp', 0.85);
if (!dataUrl.startsWith('data:image/webp')) {
dataUrl = canvas.toDataURL('image/jpeg', 0.85);
}
resolve(dataUrl);
};
img.src = e.target?.result as string;
};
reader.readAsDataURL(file);
});
}