feat(ui): unify avatars across navbar, profiles, event/match chat
- Add reusable Avatar with fallback, status dot, ring - Replace <img> uses in Navbar, Profile, PublicProfile - Use Avatar in MatchChatPage and EventChatPage messages and sidebars - Fix own-message detection for snake_case payloads
This commit is contained in:
71
frontend/src/components/common/Avatar.jsx
Normal file
71
frontend/src/components/common/Avatar.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
|
||||
const STATUS_COLORS = {
|
||||
online: 'bg-green-500',
|
||||
offline: 'bg-gray-400',
|
||||
busy: 'bg-yellow-500',
|
||||
};
|
||||
|
||||
const Avatar = ({
|
||||
src,
|
||||
username = '',
|
||||
alt,
|
||||
size = 32, // number (px) or string with Tailwind classes
|
||||
className = '', // applied to <img>
|
||||
containerClassName = '', // applied to wrapper
|
||||
rounded = true,
|
||||
ring = false,
|
||||
ringColor = 'ring-white',
|
||||
status = null, // 'online' | 'offline' | 'busy' | null
|
||||
title,
|
||||
onClick,
|
||||
}) => {
|
||||
const [imgSrc, setImgSrc] = React.useState(src || '');
|
||||
|
||||
const isNumericSize = typeof size === 'number';
|
||||
const style = isNumericSize ? { width: size, height: size } : undefined;
|
||||
const sizeClass = isNumericSize ? '' : size; // e.g. 'w-8 h-8'
|
||||
|
||||
const fallback = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(
|
||||
username || 'user'
|
||||
)}`;
|
||||
|
||||
const handleError = (e) => {
|
||||
if (e.currentTarget.src !== fallback) {
|
||||
setImgSrc(fallback);
|
||||
}
|
||||
};
|
||||
|
||||
const img = (
|
||||
<img
|
||||
src={imgSrc || fallback}
|
||||
alt={alt || username || 'avatar'}
|
||||
className={`${sizeClass} ${rounded ? 'rounded-full' : ''} bg-gray-100 object-cover ${
|
||||
ring ? `ring-2 ${ringColor}` : ''
|
||||
} ${className}`}
|
||||
style={style}
|
||||
loading="lazy"
|
||||
onError={handleError}
|
||||
title={title || username}
|
||||
/>
|
||||
);
|
||||
|
||||
// If no status, no need for wrapper; keep DOM minimal
|
||||
if (!status) {
|
||||
return img;
|
||||
}
|
||||
|
||||
const dotColor = STATUS_COLORS[status] || STATUS_COLORS.offline;
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${containerClassName}`} onClick={onClick}>
|
||||
{img}
|
||||
<span
|
||||
className={`absolute bottom-0 right-0 block h-3 w-3 ${dotColor} rounded-full ring-2 ring-white`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Avatar;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Video, LogOut, User, History, Users } from 'lucide-react';
|
||||
import Avatar from '../common/Avatar';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { matchesAPI } from '../../services/api';
|
||||
import { connectSocket, disconnectSocket, getSocket } from '../../services/socket';
|
||||
@@ -99,11 +100,7 @@ const Navbar = () => {
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.username}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<Avatar src={user?.avatar} username={user.username} size={32} />
|
||||
<span className="text-sm font-medium text-gray-700">{user.username}</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user