- Updated all profile links to use /@username format
- Made usernames clickable in chat messages
- Added URL parameter sanitization to strip @ when fetching user data
- Ensures consistent profile URL format across the application
Implemented display of country flags and competitor numbers in event chat messages:
- Country flags displayed as emoji (🇸🇪, 🇵🇱, etc.) with proper emoji font support
- Competitor numbers shown in #123 format next to usernames
- Normalized data architecture with user and participant caches on frontend
- User data (username, avatar, country) and participant data (competitorNumber) cached separately
- Messages store only core data (id, content, userId, createdAt)
- Prevents data inconsistency when users update profile information
- Fixed duplicate message keys React warning with deduplication logic
- Backend sends nested user/participant objects for cache population
- Auto-updates across all messages when user changes avatar or country
Backend changes:
- Socket.IO event_message and message_history include nested user/participant data
- API /events/:slug/messages endpoint restructured with same nested format
- Batch lookup of competitor numbers for efficiency
Frontend changes:
- useEventChat hook maintains userCache and participantCache
- ChatMessage component accepts separate user/participant props
- ChatMessageList performs cache lookups during render
- Emoji font family support for cross-platform flag rendering
- Save vertical space on mobile by showing page title in navbar
- Mobile: "spotlight.cam - {PageTitle}" instead of separate h1
- Desktop: unchanged - page titles remain as separate headings
- Saves ~60-80px vertical space on mobile devices
Pages updated:
- Dashboard, Events, Matches, History, Profile
- Add w-full to MatchCard for consistent card width
- Use Layout noPadding to avoid padding conflicts
- Add explicit padding and width control to main container
- Ensure All, Pending, and Active tabs have identical width
- Show error alert when login fails instead of console-only logging
- Display user-friendly error message: "Invalid email or password"
- Clear error state before new login attempt
- Use existing Alert component for consistency with RegisterPage
- Replace "Edit heats" button with icon-only version
- Remove connection status indicator (show "Disconnected" warning only when offline)
- Remove event location from header
- Hide Participants tab on desktop (sidebar already visible)
- Remove "Participants" header from sidebar on desktop
- Add fullWidth prop to Layout component for chat pages
- Redesign EventChatPage and MatchChatPage with fixed layout:
- Navbar sticky to top (no gaps)
- Event/partner header directly below navbar
- Chat content fills available space (flex-1)
- Input area fixed to bottom
- Full screen width on mobile (no margins)
- Translate RecordingTab UI strings to English
- Move Leave Event button to header
- Remove unnecessary margins and max-width constraints
This provides a better mobile experience with full-screen chat
interface similar to native messaging apps.
- Add CONNECTION_STATE (disconnected, connecting, connected, failed)
- Add SUGGESTION_TYPE (toBeRecorded, toRecord)
- Update useWebRTC.js to use CONNECTION_STATE
- Update MatchChatPage.jsx to use CONNECTION_STATE
- Update RecordingTab.jsx to use SUGGESTION_TYPE
- Create constants/statuses.js with MATCH_STATUS, SUGGESTION_STATUS, MATCH_FILTER
- Update MatchCard, MatchesPage, HistoryPage, RatePartnerPage to use MATCH_STATUS
- Update RecordingTab to use SUGGESTION_STATUS
- Update Navbar to use MATCH_STATUS for API calls
Split DashboardPage (578 lines) into focused components:
- DashboardEventCard: event card with chat access
- DashboardMatchCard: match card with status indicators
- MatchRequestCards: incoming/outgoing request cards
- EmptyState: reusable empty state component (in common/)
DashboardPage now 295 lines (-49%)
Split large EventDetailsPage (545 lines) into smaller, focused components:
- QRCodeSection: QR code display and copy link functionality
- ParticipantsSection: participants list with avatars
- MatchingConfigSection: deadline and matching controls
- ScheduleConfigSection: division slot configuration
EventDetailsPage now 148 lines (-73%)
Allow event organizers to configure which divisions run in parallel
(same time slot) for accurate collision detection in the auto-matching
algorithm. Divisions in the same slot will collide with each other.
- Add scheduleConfig JSON field to Event model
- Add PUT /events/:slug/schedule-config API endpoint
- Update matching algorithm to use slot-based collision detection
- Add UI in EventDetailsPage for managing division slots
- Add unit tests for schedule-based collision detection
Add frontend components for auto-matching recording partners:
- RecordingTab component with suggestions list and opt-out toggle
- Tab navigation in EventChatPage (Chat, Uczestnicy, Nagrywanie)
- Matching configuration in EventDetailsPage (deadline, run matching)
- matchingAPI functions in api.js
- Return registrationDeadline and matchingRunAt in GET /events/:slug/details
UI allows users to:
- View who will record their heats
- View heats they need to record
- Accept/reject suggestions
- Opt-out from being a recorder
- Set registration deadline (admin)
- Manually trigger matching (admin)
Allow participants to set their bib/competitor number per event.
Display as badge next to username in participant lists.
- Add competitorNumber field to EventParticipant model
- Add PUT /events/:slug/competitor-number endpoint
- Include competitorNumber in heats/me and heats/all responses
- Add input field in HeatsBanner component
- Display badge in UserListItem component
- Add unit tests for competitor number feature
Track unread messages in match chats and display count badge:
- Schema: Add user1LastReadAt/user2LastReadAt to Match model
- Backend: Calculate unreadCount in dashboard API
- Socket: Update lastReadAt when user joins match room
- Frontend: Display red badge with unread count on match avatar
Show real-time count of users currently in each event chat room.
- Backend: Export getEventsOnlineCounts from socket module
- Dashboard API: Include onlineCount for each active event
- Frontend: Display online count with animated green dot indicator
Replace simple spinner with skeleton loading placeholders that match
the dashboard layout structure, providing better visual feedback during
data loading.
- Install react-hot-toast library
- Add Toaster component to App.jsx
- Show success/error toasts for match accept/reject/cancel
- Show toasts for real-time match events
- Update tests with toast mocks
- Create DashboardPage with active events, matches, and requests
- Add dashboardAPI.getData() to services/api.js
- Add /dashboard route as default landing after login
- Update Navbar with Dashboard and Events links
- Show video exchange and rating status for matches
- Handle match accept/reject/cancel actions
Bug: EventChatPage referenced formatHeat() function in header's heat display
(line 365) but the function was removed during Phase 3 refactoring when
creating the HeatBadges component.
Solution:
1. Enhanced HeatBadges component with badgeClassName prop to support
custom styling (needed for dark header background)
2. Replaced manual heat rendering in EventChatPage header with
HeatBadges component
3. Passed custom badge styling to match the dark primary-700 header
Changes:
- HeatBadges.jsx: Added badgeClassName prop for style customization
- EventChatPage.jsx: Replaced manual heat map with HeatBadges component
Fixes: "Uncaught ReferenceError: formatHeat is not defined" error
Separate concerns - move Socket.IO and form logic from components to reusable hooks
New Hooks:
- useForm: Generic form state management with handleChange/handleSubmit/reset
- useEventChat: Extract Socket.IO logic from EventChatPage (156 lines)
* Manages messages, active users, connection state
* Handles send message, load older messages with scroll preservation
* Real-time updates via Socket.IO event listeners
- useMatchChat: Extract Socket.IO logic from MatchChatPage (115 lines)
* Manages 1:1 chat messages and connection
* Loads message history from API
* Real-time message sync via Socket.IO
Pages Refactored:
- EventChatPage: 661 → 564 lines (-97 lines, -15%)
- MatchChatPage: 517 → 446 lines (-71 lines, -14%)
Benefits:
- Cleaner component code - UI separated from business logic
- Reusable hooks can be used in other components
- Easier to test - hooks can be unit tested independently
- Better code organization - single responsibility principle
- 168 lines eliminated from pages, moved to 271 lines of reusable hooks
Phase 2 Total: -168 lines
Grand Total (Phase 1+2): -389 lines (-12%)
- Add Alert component with 4 variants (success/error/warning/info)
- Add LoadingSpinner and LoadingButton components
- Add FormInput and FormSelect components with icon support
- Add Modal and ConfirmationModal components
- Add ChatMessage, ChatMessageList, and ChatInput components
- Add EventCard component
These components will eliminate ~450 lines of duplicated code across pages.
Part of Phase 1 (Quick Wins) frontend refactoring.
- 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
- Add openrelay.metered.ca TURN servers for testing
- Support connections through symmetric NAT (mobile networks)
- Add TCP transport fallback for strict firewalls
- Enables P2P file transfer across different networks
- Change API_URL from hardcoded 'http://localhost:8080/api' to '/api'
- Change SOCKET_URL from hardcoded to window.location.origin
- Fixes production build connecting to wrong port
- Now works correctly in both dev (localhost:8080) and prod (localhost)
Create new HomePage component with:
- Hero section with CTAs for registration and login
- Features showcase highlighting WebRTC, matching, chat, security
- How it works section with 3-step process
- CTA section and footer with links
- Responsive design with gradient backgrounds
Update routing to show HomePage at / instead of redirecting to /events
Changed WebRTC config from localhost-only to STUN servers:
- Removed rtcConfigLocalhost (no longer needed)
- Using rtcConfig with Google STUN servers
- Enables NAT traversal for users on different networks
- Removed unnecessary iceCandidatePoolSize config
- Link sharing remains as fallback for blocked users
Implemented complete WebRTC peer-to-peer file transfer system for match chat:
**Core WebRTC Implementation:**
- Created useWebRTC hook with RTCPeerConnection and RTCDataChannel
- P2P file transfer with 16KB chunking for large files (tested up to 700MB)
- Real-time progress monitoring for sender and receiver
- Automatic file download on receiver side
- End-to-end encryption via DTLS (native WebRTC)
- ICE candidate exchange via Socket.IO signaling
- Support for host candidates (localhost testing)
**WebRTC Detection & User Experience:**
- Automatic WebRTC capability detection on page load
- Detects if ICE candidates can be generated (fails in Opera, privacy-focused browsers, VPNs)
- User-friendly warning component with fix suggestions
- Graceful degradation: disables WebRTC button when blocked
- Suggests alternative methods (video links via Google Drive/Dropbox)
**Socket.IO Improvements:**
- Fixed multiple socket instance creation issue
- Implemented socket instance reuse pattern
- Disabled React.StrictMode to prevent reconnection loops in development
**Technical Details:**
- RTCPeerConnection with configurable STUN servers (currently using localhost config)
- RTCDataChannel with ordered delivery
- Comprehensive logging for debugging (ICE gathering, connection states, signaling)
- Match room-based signaling relay via Socket.IO
- Authorization checks for all WebRTC signaling events
**Files Changed:**
- frontend/src/hooks/useWebRTC.js - Complete WebRTC implementation
- frontend/src/utils/webrtcDetection.js - WebRTC capability detection
- frontend/src/components/WebRTCWarning.jsx - User warning component
- frontend/src/pages/MatchChatPage.jsx - WebRTC integration
- frontend/src/services/socket.js - Socket instance reuse
- frontend/src/main.jsx - Disabled StrictMode for Socket.IO stability
**Testing:**
- ✅ Verified working in Chrome (ICE candidates generated)
- ✅ Tested with 700MB file transfer
- ✅ Detection working in Opera (shows warning when WebRTC blocked)
- ✅ P2P connection establishment and DataChannel opening
- ✅ File chunking and progress monitoring
**TODO:**
- Add STUN server configuration for production (NAT traversal)
- Consider server-based upload fallback for blocked users
Add complete WebRTC peer-to-peer file transfer functionality:
Backend changes:
- Add WebRTC signaling events to Socket.IO (offer, answer, ICE candidates)
- Implement authorization checks for match participants
- Add signaling relay between matched users
Frontend changes:
- Create useWebRTC hook for RTCPeerConnection management
- Implement RTCDataChannel with 16KB chunking for large files
- Add real-time progress monitoring for sender and receiver
- Implement automatic file download on receiver side
- Add connection state tracking and error handling
- Integrate WebRTC with MatchChatPage (replace mockup)
Configuration:
- Add Vite allowed hosts configuration via VITE_ALLOWED_HOSTS env var
- Support comma-separated host list or 'all' for development
- Add .env.example with configuration examples
- Update docker-compose.yml with default allowed hosts
Documentation:
- Add comprehensive WebRTC testing guide with troubleshooting
- Add quick test checklist for manual testing
- Document WebRTC flow, requirements, and success criteria
Features:
- End-to-end encrypted P2P transfer (DTLS)
- 16KB chunk size optimized for DataChannel
- Buffer management to prevent overflow
- Automatic connection establishment with 30s timeout
- Support for files of any size
- Real-time progress tracking
- Clean connection lifecycle management
- Add comprehensive ratings section to PublicProfilePage showing average rating, individual reviews with comments, and collaboration preferences
- Make partner avatars and names clickable in MatchesPage and MatchChatPage to navigate to their public profiles
- Add hover effects on profile links for better UX
- Fetch and display ratings using ratingsAPI endpoint
Complete the match lifecycle with partner rating functionality.
Backend changes:
- Add POST /api/matches/:slug/ratings endpoint to create ratings
* Validate score range (1-5)
* Prevent duplicate ratings (unique constraint per match+rater+rated)
* Auto-complete match when both users have rated
* Return detailed rating data with user and event info
- Add GET /api/users/:username/ratings endpoint to fetch user ratings
* Calculate and return average rating
* Include rater details and event context for each rating
* Limit to last 50 ratings
- Add hasRated field to GET /api/matches/:slug response
* Check if current user has already rated the match
* Enable frontend to prevent duplicate rating attempts
Frontend changes:
- Update RatePartnerPage to use real API instead of mocks
* Load match data and partner info
* Submit ratings with score, comment, and wouldCollaborateAgain
* Check hasRated flag and redirect if already rated
* Validate match status before allowing rating
* Show loading state and proper error handling
- Update MatchChatPage to show rating status
* Replace "Rate Partner" button with "✓ Rated" badge when user has rated
* Improve button text from "End & rate" to "Rate Partner"
- Add ratings API functions
* matchesAPI.createRating(slug, ratingData)
* ratingsAPI.getUserRatings(username)
User flow:
1. After match is accepted, users can rate each other
2. Click "Rate Partner" in chat to navigate to rating page
3. Submit 1-5 star rating with optional comment
4. Rating saved and user redirected to matches list
5. Chat shows "✓ Rated" badge instead of rating button
6. Match marked as 'completed' when both users have rated
7. Users cannot rate the same match twice
Security improvements:
- Add random CUID slugs to Match model to prevent ID enumeration attacks
- Update all match URLs from /matches/:id to /matches/:slug
- Keep numeric IDs for internal Socket.IO operations only
Backend changes:
- Add slug field to matches table with unique index
- Update all match endpoints to use slug-based lookups (GET, PUT, DELETE)
- Add GET /api/matches/:slug/messages endpoint to fetch message history
- Include matchSlug in all Socket.IO notifications
Frontend changes:
- Update all match routes to use slug parameter
- Update MatchesPage to use slug for accept/reject/navigate operations
- Update MatchChatPage to fetch match data by slug and load message history
- Update RatePartnerPage to use slug parameter
- Add matchesAPI.getMatchMessages() function
Bug fixes:
- Fix MatchChatPage not loading message history from database on mount
- Messages now persist and display correctly when users reconnect
Backend changes:
- Add matches API routes (POST, GET, PUT, DELETE)
- Create/accept/reject match requests
- Auto-create private chat rooms on match acceptance
- Socket.IO notifications for match events (received, accepted, cancelled)
- Users join personal rooms (user_{id}) for notifications
Frontend changes:
- Add MatchesPage component with inbox UI
- Matches navigation link with notification badge
- Real-time match request count updates
- Accept/reject match functionality
- Filter matches by status (all/pending/accepted)
- Integrate match requests in EventChatPage (UserPlus button)
Features:
- Send match requests to event participants
- Accept incoming match requests
- Real-time notifications via Socket.IO
- Automatic private chat room creation
- Match status tracking (pending/accepted/completed)
- Authorization checks (only participants can match)
- Duplicate match prevention
- Show current user's heats in header next to connection status
- Display format: 'Your heats: J&J NOV 1 L, STR INT 2 L'
- Always visible - no need to click Edit Heats to see them
- Styled as badges matching sidebar heat badges
- Add existingHeats prop to HeatsBanner component
- Load and format existing heats into form fields
- Pass myHeats to HeatsBanner in edit modal
- Users can now edit their heats instead of starting from scratch
- Fix participant data structure mapping (use p.userId instead of p.user.id)
- Backend returns flat participant objects, not nested user objects
- Remove debug console.log statements
- Participants list now correctly displays all checked-in users