diff --git a/backend/src/routes/events.js b/backend/src/routes/events.js index 1acc4b2..53924fe 100644 --- a/backend/src/routes/events.js +++ b/backend/src/routes/events.js @@ -159,6 +159,7 @@ router.get('/:slug/messages', authenticate, async (req, res, next) => { id: true, username: true, avatar: true, + country: true, }, }, }, @@ -166,6 +167,24 @@ router.get('/:slug/messages', authenticate, async (req, res, next) => { take: parseInt(limit), }); + // Get competitor numbers for all users in this event + const userIds = [...new Set(messages.map(msg => msg.user.id))]; + const eventParticipants = await prisma.eventParticipant.findMany({ + where: { + eventId: event.id, + userId: { in: userIds }, + }, + select: { + userId: true, + competitorNumber: true, + }, + }); + + // Create a map of userId to competitorNumber + const competitorNumberMap = new Map( + eventParticipants.map(ep => [ep.userId, ep.competitorNumber]) + ); + // Return in chronological order (oldest first) res.json({ success: true, @@ -173,11 +192,20 @@ router.get('/:slug/messages', authenticate, async (req, res, next) => { id: msg.id, roomId: msg.roomId, userId: msg.user.id, - username: msg.user.username, - avatar: msg.user.avatar, content: msg.content, type: msg.type, createdAt: msg.createdAt, + // Nested user data for caching + user: { + id: msg.user.id, + username: msg.user.username, + avatar: msg.user.avatar, + country: msg.user.country, + }, + // Nested participant data for caching + participant: { + competitorNumber: competitorNumberMap.get(msg.user.id), + }, })), hasMore: messages.length === parseInt(limit), }); diff --git a/backend/src/socket/index.js b/backend/src/socket/index.js index d969956..d653cec 100644 --- a/backend/src/socket/index.js +++ b/backend/src/socket/index.js @@ -144,6 +144,7 @@ function initializeSocket(httpServer) { id: true, username: true, avatar: true, + country: true, }, }, }, @@ -151,16 +152,43 @@ function initializeSocket(httpServer) { take: 20, }); + // Get competitor numbers for all users in this event + const userIds = [...new Set(messages.map(msg => msg.user.id))]; + const eventParticipants = await prisma.eventParticipant.findMany({ + where: { + eventId: eventId, + userId: { in: userIds }, + }, + select: { + userId: true, + competitorNumber: true, + }, + }); + + // Create a map of userId to competitorNumber + const competitorNumberMap = new Map( + eventParticipants.map(ep => [ep.userId, ep.competitorNumber]) + ); + // Send message history to the joining user (reverse to chronological order) socket.emit('message_history', messages.reverse().map(msg => ({ id: msg.id, roomId: msg.roomId, userId: msg.user.id, - username: msg.user.username, - avatar: msg.user.avatar, content: msg.content, type: msg.type, createdAt: msg.createdAt, + // Nested user data for caching + user: { + id: msg.user.id, + username: msg.user.username, + avatar: msg.user.avatar, + country: msg.user.country, + }, + // Nested participant data for caching + participant: { + competitorNumber: competitorNumberMap.get(msg.user.id), + }, }))); } @@ -249,21 +277,44 @@ function initializeSocket(httpServer) { id: true, username: true, avatar: true, + country: true, }, }, }, }); + // Get competitor number for this user in this event + const eventParticipant = await prisma.eventParticipant.findUnique({ + where: { + userId_eventId: { + userId: socket.user.id, + eventId: eventId, + }, + }, + select: { + competitorNumber: true, + }, + }); + // Broadcast message to room io.to(roomName).emit('event_message', { id: message.id, roomId: message.roomId, userId: message.user.id, - username: message.user.username, - avatar: message.user.avatar, content: message.content, type: message.type, createdAt: message.createdAt, + // Nested user data for caching + user: { + id: message.user.id, + username: message.user.username, + avatar: message.user.avatar, + country: message.user.country, + }, + // Nested participant data for caching + participant: { + competitorNumber: eventParticipant?.competitorNumber, + }, }); console.log(`💬 Message in event ${socket.currentEventSlug} from ${socket.user.username}`); diff --git a/frontend/src/components/chat/ChatMessage.jsx b/frontend/src/components/chat/ChatMessage.jsx index 0774b83..28429fa 100644 --- a/frontend/src/components/chat/ChatMessage.jsx +++ b/frontend/src/components/chat/ChatMessage.jsx @@ -1,13 +1,82 @@ import Avatar from '../common/Avatar'; +// Helper function to convert country name to flag emoji +const getCountryFlag = (country) => { + if (!country) return null; + + // Map of country names to their ISO 3166-1 alpha-2 codes + const countryCodeMap = { + 'United States': 'US', + 'United Kingdom': 'GB', + 'Poland': 'PL', + 'Sweden': 'SE', + 'Germany': 'DE', + 'France': 'FR', + 'Spain': 'ES', + 'Italy': 'IT', + 'Netherlands': 'NL', + 'Belgium': 'BE', + 'Switzerland': 'CH', + 'Austria': 'AT', + 'Denmark': 'DK', + 'Norway': 'NO', + 'Finland': 'FI', + 'Portugal': 'PT', + 'Greece': 'GR', + 'Czech Republic': 'CZ', + 'Hungary': 'HU', + 'Romania': 'RO', + 'Bulgaria': 'BG', + 'Croatia': 'HR', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Lithuania': 'LT', + 'Latvia': 'LV', + 'Estonia': 'EE', + 'Ireland': 'IE', + 'Luxembourg': 'LU', + 'Canada': 'CA', + 'Australia': 'AU', + 'New Zealand': 'NZ', + 'Japan': 'JP', + 'South Korea': 'KR', + 'China': 'CN', + 'Brazil': 'BR', + 'Argentina': 'AR', + 'Mexico': 'MX', + 'Russia': 'RU', + 'Ukraine': 'UA', + 'Turkey': 'TR', + 'Israel': 'IL', + 'South Africa': 'ZA', + 'India': 'IN', + 'Singapore': 'SG', + 'Malaysia': 'MY', + 'Thailand': 'TH', + 'Indonesia': 'ID', + 'Philippines': 'PH', + 'Vietnam': 'VN', + }; + + const code = countryCodeMap[country]; + if (!code) return null; + + // Convert country code to flag emoji + // Flag emojis are made from regional indicator symbols (U+1F1E6 to U+1F1FF) + const codePoints = [...code].map(char => 127397 + char.charCodeAt(0)); + return String.fromCodePoint(...codePoints); +}; + /** * Individual Chat Message component * - * @param {object} message - Message object with { id, content, username, avatar, createdAt, userId/user_id } + * @param {object} message - Core message object with { id, content, createdAt, userId } + * @param {object} user - User data { id, username, avatar, country } + * @param {object} participant - Participant data { competitorNumber } * @param {boolean} isOwn - Whether this message belongs to the current user * @param {function} formatTime - Optional custom time formatter (default: toLocaleTimeString) */ -const ChatMessage = ({ message, isOwn, formatTime }) => { +const ChatMessage = ({ message, user, participant, isOwn, formatTime }) => { const defaultFormatTime = (timestamp) => { return new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', @@ -16,6 +85,12 @@ const ChatMessage = ({ message, isOwn, formatTime }) => { }; const timeFormatter = formatTime || defaultFormatTime; + const countryFlag = getCountryFlag(user?.country); + + // Fallback for missing user data + if (!user) { + return null; + } return (