111 lines
2.6 KiB
TypeScript
111 lines
2.6 KiB
TypeScript
|
|
import { cn, generateAvatarColor, getInitials } from '../lib/utils';
|
||
|
|
|
||
|
|
interface AvatarProps {
|
||
|
|
seed: string;
|
||
|
|
name: string;
|
||
|
|
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||
|
|
className?: string;
|
||
|
|
showOnline?: boolean;
|
||
|
|
isOnline?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function Avatar({
|
||
|
|
seed,
|
||
|
|
name,
|
||
|
|
size = 'md',
|
||
|
|
className,
|
||
|
|
showOnline = false,
|
||
|
|
isOnline = false,
|
||
|
|
}: AvatarProps) {
|
||
|
|
const sizeClasses = {
|
||
|
|
sm: 'w-6 h-6 text-xs',
|
||
|
|
md: 'w-8 h-8 text-sm',
|
||
|
|
lg: 'w-10 h-10 text-base',
|
||
|
|
xl: 'w-12 h-12 text-lg',
|
||
|
|
};
|
||
|
|
|
||
|
|
const dotSizeClasses = {
|
||
|
|
sm: 'w-1.5 h-1.5 -bottom-0 -right-0',
|
||
|
|
md: 'w-2 h-2 -bottom-0.5 -right-0.5',
|
||
|
|
lg: 'w-2.5 h-2.5 -bottom-0.5 -right-0.5',
|
||
|
|
xl: 'w-3 h-3 -bottom-1 -right-1',
|
||
|
|
};
|
||
|
|
|
||
|
|
const bgColor = generateAvatarColor(seed);
|
||
|
|
const initials = getInitials(name);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={cn('relative flex-shrink-0', className)}>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'rounded-full flex items-center justify-center font-semibold text-white',
|
||
|
|
sizeClasses[size]
|
||
|
|
)}
|
||
|
|
style={{ backgroundColor: bgColor }}
|
||
|
|
>
|
||
|
|
{initials}
|
||
|
|
</div>
|
||
|
|
{showOnline && (
|
||
|
|
<span
|
||
|
|
className={cn(
|
||
|
|
'absolute rounded-full border-2 border-bg-secondary',
|
||
|
|
dotSizeClasses[size],
|
||
|
|
isOnline ? 'bg-accent-green shadow-[0_0_6px_#4ade80]' : 'bg-text-muted'
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
interface AvatarGroupProps {
|
||
|
|
users: Array<{ seed: string; name: string }>;
|
||
|
|
max?: number;
|
||
|
|
size?: 'sm' | 'md' | 'lg';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function AvatarGroup({ users, max = 4, size = 'md' }: AvatarGroupProps) {
|
||
|
|
const displayed = users.slice(0, max);
|
||
|
|
const remaining = users.length - max;
|
||
|
|
|
||
|
|
const overlapClasses = {
|
||
|
|
sm: '-ml-2',
|
||
|
|
md: '-ml-3',
|
||
|
|
lg: '-ml-4',
|
||
|
|
};
|
||
|
|
|
||
|
|
const sizeClasses = {
|
||
|
|
sm: 'w-6 h-6 text-xs',
|
||
|
|
md: 'w-8 h-8 text-sm',
|
||
|
|
lg: 'w-10 h-10 text-base',
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex items-center">
|
||
|
|
{displayed.map((user, index) => (
|
||
|
|
<div
|
||
|
|
key={user.seed}
|
||
|
|
className={cn(
|
||
|
|
'rounded-full border-2 border-bg-secondary',
|
||
|
|
index > 0 && overlapClasses[size]
|
||
|
|
)}
|
||
|
|
style={{ zIndex: displayed.length - index }}
|
||
|
|
>
|
||
|
|
<Avatar seed={user.seed} name={user.name} size={size} />
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{remaining > 0 && (
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'rounded-full bg-bg-tertiary flex items-center justify-center font-medium text-text-secondary border-2 border-bg-secondary',
|
||
|
|
overlapClasses[size],
|
||
|
|
sizeClasses[size]
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
+{remaining}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|