feat: implement Phase 2 - Matches API with real-time notifications
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
This commit is contained in:
@@ -1,16 +1,60 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { Video, LogOut, User, History } from 'lucide-react';
|
||||
import { Video, LogOut, User, History, Users } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { matchesAPI } from '../../services/api';
|
||||
import { connectSocket, disconnectSocket, getSocket } from '../../services/socket';
|
||||
|
||||
const Navbar = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [pendingMatchesCount, setPendingMatchesCount] = useState(0);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadPendingMatches();
|
||||
|
||||
// Connect to socket for real-time updates
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
connectSocket(token, user.id);
|
||||
|
||||
const socket = getSocket();
|
||||
if (socket) {
|
||||
// Listen for match notifications
|
||||
socket.on('match_request_received', () => loadPendingMatches());
|
||||
socket.on('match_accepted', () => loadPendingMatches());
|
||||
socket.on('match_cancelled', () => loadPendingMatches());
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
const socket = getSocket();
|
||||
if (socket) {
|
||||
socket.off('match_request_received');
|
||||
socket.off('match_accepted');
|
||||
socket.off('match_cancelled');
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const loadPendingMatches = async () => {
|
||||
try {
|
||||
const result = await matchesAPI.getMatches(null, 'pending');
|
||||
// Only count incoming requests (where user is not the initiator)
|
||||
const incomingCount = (result.data || []).filter(m => !m.isInitiator).length;
|
||||
setPendingMatchesCount(incomingCount);
|
||||
} catch (error) {
|
||||
console.error('Failed to load pending matches:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -25,6 +69,19 @@ const Navbar = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
to="/matches"
|
||||
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-primary-600 hover:bg-gray-100 relative"
|
||||
>
|
||||
<Users className="w-4 h-4" />
|
||||
<span>Matches</span>
|
||||
{pendingMatchesCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-semibold">
|
||||
{pendingMatchesCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/history"
|
||||
className="flex items-center space-x-1 px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-primary-600 hover:bg-gray-100"
|
||||
|
||||
Reference in New Issue
Block a user