feat(dashboard): add unread count for match chats

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
This commit is contained in:
Radosław Gierwiało
2025-11-21 21:46:00 +01:00
parent 2c0620db6a
commit 78280ca8d8
6 changed files with 121 additions and 15 deletions

View File

@@ -356,7 +356,7 @@ const EventCard = ({ event }) => {
// Match Card Component
const MatchCard = ({ match }) => {
const navigate = useNavigate();
const { partner, event, videoExchange, ratings } = match;
const { partner, event, videoExchange, ratings, unreadCount } = match;
// Can rate when video exchange is complete and user hasn't rated yet
const canRate = videoExchange?.sentByMe && videoExchange?.receivedFromPartner && !ratings?.ratedByMe;
@@ -365,12 +365,17 @@ const MatchCard = ({ match }) => {
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
<div className="flex items-start gap-4">
{/* Avatar */}
<Link to={`/${partner.username}`} className="flex-shrink-0">
<Link to={`/${partner.username}`} className="flex-shrink-0 relative">
<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 hover:ring-2 hover:ring-primary-500 transition-all"
/>
{unreadCount > 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">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
</Link>
{/* Content */}

View File

@@ -507,6 +507,68 @@ describe('DashboardPage', () => {
});
});
describe('Unread Count Display', () => {
it('should display unread count badge when there are unread messages', async () => {
dashboardAPI.getData.mockResolvedValue({
activeEvents: [],
activeMatches: [
{
id: 1,
slug: 'match-123',
partner: {
id: 2,
username: 'sarah_dancer',
firstName: 'Sarah',
lastName: 'Martinez',
avatar: null,
},
event: { id: 1, name: 'Test Event' },
videoExchange: { sentByMe: false, receivedFromPartner: false },
ratings: { ratedByMe: false, ratedByPartner: false },
unreadCount: 5,
},
],
matchRequests: { incoming: [], outgoing: [] },
});
renderWithRouter(<DashboardPage />);
await waitFor(() => {
expect(screen.getByText('5')).toBeInTheDocument();
});
});
it('should show 9+ for more than 9 unread messages', async () => {
dashboardAPI.getData.mockResolvedValue({
activeEvents: [],
activeMatches: [
{
id: 1,
slug: 'match-123',
partner: {
id: 2,
username: 'sarah_dancer',
firstName: 'Sarah',
lastName: 'Martinez',
avatar: null,
},
event: { id: 1, name: 'Test Event' },
videoExchange: { sentByMe: false, receivedFromPartner: false },
ratings: { ratedByMe: false, ratedByPartner: false },
unreadCount: 15,
},
],
matchRequests: { incoming: [], outgoing: [] },
});
renderWithRouter(<DashboardPage />);
await waitFor(() => {
expect(screen.getByText('9+')).toBeInTheDocument();
});
});
});
describe('Rating Status Display', () => {
it('should show rating status indicators', async () => {
dashboardAPI.getData.mockResolvedValue({