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:
Radosław Gierwiało
2025-11-15 23:08:00 +01:00
parent 6a17143ce1
commit 38adf1e5a5
6 changed files with 118 additions and 42 deletions

View File

@@ -6,6 +6,7 @@ import { Send, UserPlus, Loader2, LogOut, AlertTriangle, QrCode, Edit2, Filter,
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
import { eventsAPI, heatsAPI, matchesAPI } from '../services/api';
import HeatsBanner from '../components/heats/HeatsBanner';
import Avatar from '../components/common/Avatar';
const EventChatPage = () => {
const { slug } = useParams();
@@ -536,20 +537,13 @@ const EventChatPage = () => {
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
>
<div className="flex items-center space-x-2 flex-1 min-w-0">
<div className="relative flex-shrink-0">
<img
src={displayUser.avatar}
alt={displayUser.username}
className="w-8 h-8 rounded-full"
/>
{/* Online/Offline indicator */}
<div
className={`absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full border-2 border-gray-50 ${
displayUser.isOnline ? 'bg-green-500' : 'bg-gray-400'
}`}
title={displayUser.isOnline ? 'Online' : 'Offline'}
/>
</div>
<Avatar
src={displayUser.avatar}
username={displayUser.username}
size={32}
status={displayUser.isOnline ? 'online' : 'offline'}
title={displayUser.username}
/>
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium truncate ${
displayUser.isOnline ? 'text-gray-900' : 'text-gray-500'
@@ -611,17 +605,18 @@ const EventChatPage = () => {
</div>
)}
{messages.map((message) => {
const isOwnMessage = message.userId === user.id;
const isOwnMessage = (message.userId ?? message.user_id) === user.id;
return (
<div
key={message.id}
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}
>
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
<img
<Avatar
src={message.avatar}
alt={message.username}
className="w-8 h-8 rounded-full"
username={message.username}
size={32}
title={message.username}
/>
<div>
<div className="flex items-baseline space-x-2 mb-1">

View File

@@ -8,6 +8,7 @@ import { connectSocket, getSocket } from '../services/socket';
import { useWebRTC } from '../hooks/useWebRTC';
import { detectWebRTCSupport } from '../utils/webrtcDetection';
import WebRTCWarning from '../components/WebRTCWarning';
import Avatar from '../components/common/Avatar';
const MatchChatPage = () => {
const { slug } = useParams();
@@ -286,10 +287,12 @@ const MatchChatPage = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Link to={`/${partner.username}`} className="flex-shrink-0">
<img
src={partner.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${partner.username}`}
alt={partner.username}
className="w-12 h-12 rounded-full border-2 border-white hover:ring-2 hover:ring-white hover:ring-offset-2 hover:ring-offset-primary-600 transition-all"
<Avatar
src={partner.avatar}
username={partner.username}
size={48}
className="border-2 border-white transition-all hover:ring-2 hover:ring-white hover:ring-offset-2 hover:ring-offset-primary-600"
title={partner.username}
/>
</Link>
<div>
@@ -355,17 +358,18 @@ const MatchChatPage = () => {
</div>
)}
{messages.map((message) => {
const isOwnMessage = message.userId === user.id;
const isOwnMessage = (message.userId ?? message.user_id) === user.id;
return (
<div
key={message.id}
className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'}`}
>
<div className={`flex items-start space-x-2 max-w-md ${isOwnMessage ? 'flex-row-reverse space-x-reverse' : ''}`}>
<img
src={message.avatar}
alt={message.username}
className="w-8 h-8 rounded-full"
<Avatar
src={message.avatar || (isOwnMessage ? user?.avatar : partner?.avatar)}
username={message.username || (isOwnMessage ? user?.username : partner?.username)}
size={32}
title={message.username || (isOwnMessage ? user?.username : partner?.username)}
/>
<div>
<div className="flex items-baseline space-x-2 mb-1">

View File

@@ -4,6 +4,7 @@ import { authAPI } from '../services/api';
import Layout from '../components/layout/Layout';
import { User, Mail, Lock, Save, AlertCircle, CheckCircle, Loader2, Hash, Youtube, Instagram, Facebook, MapPin, Globe } from 'lucide-react';
import { COUNTRIES } from '../data/countries';
import Avatar from '../components/common/Avatar';
const ProfilePage = () => {
const { user, updateUser } = useAuth();
@@ -138,10 +139,13 @@ const ProfilePage = () => {
{/* Header */}
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
<div className="flex items-center gap-4">
<img
<Avatar
src={user?.avatar}
alt={user?.username}
className="w-20 h-20 rounded-full"
username={user?.username}
size={80}
ring
ringColor="ring-white"
title={user?.username}
/>
<div>
<h1 className="text-2xl font-bold text-gray-900">

View File

@@ -3,6 +3,7 @@ import { useParams, Link } from 'react-router-dom';
import { authAPI, ratingsAPI } from '../services/api';
import Layout from '../components/layout/Layout';
import { User, MapPin, Globe, Hash, Youtube, Instagram, Facebook, Award, Users, Star, Calendar, Loader2, AlertCircle, ThumbsUp } from 'lucide-react';
import Avatar from '../components/common/Avatar';
const PublicProfilePage = () => {
const { username } = useParams();
@@ -86,10 +87,12 @@ const PublicProfilePage = () => {
{/* Profile Header */}
<div className="bg-white rounded-lg shadow-sm p-8 mb-6">
<div className="flex items-start gap-6">
<img
<Avatar
src={profile.avatar}
alt={profile.username}
className="w-32 h-32 rounded-full"
username={profile.username}
size={128}
className="transition-all hover:ring-2 hover:ring-primary-500"
title={profile.username}
/>
<div className="flex-1">
<h1 className="text-3xl font-bold text-gray-900 mb-1">
@@ -263,10 +266,12 @@ const PublicProfilePage = () => {
<div className="flex items-start gap-4">
{/* Rater Info */}
<Link to={`/${rating.rater.username}`} className="flex-shrink-0">
<img
src={rating.rater.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${rating.rater.username}`}
alt={rating.rater.username}
className="w-12 h-12 rounded-full hover:ring-2 hover:ring-primary-500 transition-all"
<Avatar
src={rating.rater.avatar}
username={rating.rater.username}
size={48}
className="hover:ring-2 hover:ring-primary-500 transition-all"
title={rating.rater.username}
/>
</Link>