Files
DiscordClone/packages/ui/src/Avatar.tsx
Bryan1029384756 b7a4cf4ce8
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
2026-04-14 09:02:14 -05:00

94 lines
2.5 KiB
TypeScript

import clsx from 'clsx';
import type { CSSProperties } from 'react';
import styles from './Avatar.module.css';
export interface AvatarProps {
src?: string | null;
alt?: string;
size?: number;
fallback?: string;
status?: 'online' | 'idle' | 'dnd' | 'offline';
className?: string;
style?: CSSProperties;
onClick?: () => void;
}
function getInitials(name: string): string {
return name
.split(/\s+/)
.map((w) => w[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
function stringToColor(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash % 360);
return `hsl(${hue}, 60%, 50%)`;
}
function getStatusGeometry(avatarSize: number): { dotSize: number; borderWidth: number; offset: number } {
if (avatarSize <= 16) return { dotSize: 8, borderWidth: 0, offset: -1 };
if (avatarSize <= 20) return { dotSize: 8, borderWidth: 0, offset: -1 };
if (avatarSize <= 24) return { dotSize: 10, borderWidth: 2, offset: -2 };
if (avatarSize <= 32) return { dotSize: 10, borderWidth: 3, offset: -3 };
if (avatarSize <= 36) return { dotSize: 10, borderWidth: 3, offset: -3 };
if (avatarSize <= 40) return { dotSize: 12, borderWidth: 3, offset: -3 };
if (avatarSize <= 48) return { dotSize: 14, borderWidth: 3, offset: -3 };
if (avatarSize <= 56) return { dotSize: 16, borderWidth: 3, offset: -3 };
if (avatarSize <= 80) return { dotSize: 16, borderWidth: 6, offset: -4 };
return { dotSize: 24, borderWidth: 8, offset: -6 };
}
export function Avatar({ src, alt = '', size = 40, fallback, status, className, style, onClick }: AvatarProps) {
const initials = fallback ? getInitials(fallback) : alt ? getInitials(alt) : '?';
const bgColor = stringToColor(fallback || alt || '?');
const geo = getStatusGeometry(size);
return (
<div
className={clsx(styles.wrapper, className)}
style={{
width: size,
height: size,
cursor: onClick ? 'pointer' : undefined,
...style,
}}
onClick={onClick}
>
{src ? (
<img src={src} alt={alt} className={styles.image} />
) : (
<div
className={styles.fallback}
style={{
width: size,
height: size,
backgroundColor: bgColor,
fontSize: size * 0.4,
userSelect: 'none',
}}
>
{initials}
</div>
)}
{status && (
<div
className={clsx(styles.statusBadge, styles[status])}
style={{
width: geo.dotSize,
height: geo.dotSize,
borderWidth: geo.borderWidth,
bottom: geo.offset,
right: geo.offset,
}}
/>
)}
</div>
);
}