feat: integrate heats system into EventChatPage
- Add state management for heats (myHeats, userHeats Map, showHeatsBanner, hideMyHeats, showHeatsModal) - Load user's heats and all users' heats on component mount - Display HeatsBanner when user has no heats declared - Add "Edit Heats" button in header for users with declared heats - Add modal for editing heats via HeatsBanner component - Display heat badges under usernames in sidebar (format: J&J NOV 1 L) - Show max 3 badges per user, with "+N" indicator for more - Add filter checkbox to hide users from same heats - Implement filter logic (hide if ANY heat matches: division + competition_type + heat_number) - Disable UserPlus (match) button for users without declared heats - Add Socket.IO heats_updated listener for real-time updates - Update todo list to mark EventChatPage integration as completed
This commit is contained in:
189
docs/TODO.md
189
docs/TODO.md
@@ -137,34 +137,191 @@
|
|||||||
- [ ] Update active_users event to include heats data
|
- [ ] Update active_users event to include heats data
|
||||||
|
|
||||||
### Step 4: Frontend Components (2-3h) ⏳
|
### Step 4: Frontend Components (2-3h) ⏳
|
||||||
- [ ] Create HeatsBanner component (sticky between header and chat):
|
- [x] Create HeatsBanner component (sticky between header and chat) - ✅ DONE
|
||||||
- Show only if user has no heats declared
|
- [x] Show only if user has no heats declared
|
||||||
- Form with dynamic heat entries (add/remove)
|
- [x] Form with dynamic heat entries (add/remove)
|
||||||
- Fields per entry: Competition Type (select), Division (select), Heat Number (1-9), Role (optional: Leader/Follower)
|
- [x] Fields per entry: Competition Type (select), Division (select), Heat Number (1-9), Role (optional: Leader/Follower)
|
||||||
- "Save Heats" button → POST /api/events/:slug/heats
|
- [x] "Save Heats" button → POST /api/events/:slug/heats
|
||||||
- On save success: hide banner, show success message
|
- [x] On save success: hide banner, show success message
|
||||||
- [ ] Add "Edit Heats" button in EventChatPage header (next to "Leave Event")
|
- [ ] Add "Edit Heats" button in EventChatPage header (next to "Leave Event") - ⏳ TODO
|
||||||
- Opens modal with same form as banner
|
- Opens modal with same form as banner
|
||||||
- Pre-fill with existing heats
|
- Pre-fill with existing heats
|
||||||
- "Update Heats" button
|
- "Update Heats" button
|
||||||
- [ ] Update EventChatPage sidebar (Active Users):
|
- [ ] Update EventChatPage sidebar (Active Users) - ⏳ TODO
|
||||||
- Display heat badges under username
|
- Display heat badges under username
|
||||||
- Format: "J&J NOV 1 L", "STR ADV 3" (no role if NULL)
|
- Format: "J&J NOV 1 L", "STR ADV 3" (no role if NULL)
|
||||||
- Max 3 visible badges, "+" indicator if more
|
- Max 3 visible badges, "+" indicator if more
|
||||||
- Add checkbox: "Hide users from my heats"
|
- Add checkbox: "Hide users from my heats"
|
||||||
- Logic: Hide users with ANY matching (division + competition_type + heat_number)
|
- Logic: Hide users with ANY matching (division + competition_type + heat_number)
|
||||||
- Disable UserPlus icon if user has no heats declared
|
- Disable UserPlus icon if user has no heats declared
|
||||||
- [ ] Create frontend API methods in services/api.js:
|
- [x] Create frontend API methods in services/api.js - ✅ DONE
|
||||||
- divisionsAPI.getAll()
|
- [x] divisionsAPI.getAll()
|
||||||
- competitionTypesAPI.getAll()
|
- [x] competitionTypesAPI.getAll()
|
||||||
- heatsAPI.saveHeats(slug, heats[])
|
- [x] heatsAPI.saveHeats(slug, heats[])
|
||||||
- heatsAPI.getMyHeats(slug)
|
- [x] heatsAPI.getMyHeats(slug)
|
||||||
- heatsAPI.getAllHeats(slug)
|
- [x] heatsAPI.getAllHeats(slug)
|
||||||
- heatsAPI.deleteHeat(slug, heatId)
|
- [x] heatsAPI.deleteHeat(slug, heatId)
|
||||||
- [ ] Socket.IO integration:
|
- [ ] Socket.IO integration - ⏳ TODO
|
||||||
- Listen to `heats_updated` event
|
- Listen to `heats_updated` event
|
||||||
- Update active users list in real-time
|
- Update active users list in real-time
|
||||||
|
|
||||||
|
### Step 4.1: EventChatPage Integration - ⏳ IN PROGRESS (Remaining work)
|
||||||
|
|
||||||
|
**What needs to be done:**
|
||||||
|
|
||||||
|
1. **Add state management for heats:**
|
||||||
|
```javascript
|
||||||
|
const [myHeats, setMyHeats] = useState([]);
|
||||||
|
const [userHeats, setUserHeats] = useState(new Map()); // userId → heats[]
|
||||||
|
const [showHeatsBanner, setShowHeatsBanner] = useState(false);
|
||||||
|
const [hideMyHeats, setHideMyHeats] = useState(false);
|
||||||
|
const [showHeatsModal, setShowHeatsModal] = useState(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Load heats on component mount:**
|
||||||
|
```javascript
|
||||||
|
useEffect(() => {
|
||||||
|
const loadHeats = async () => {
|
||||||
|
const [myHeatsData, allHeatsData] = await Promise.all([
|
||||||
|
heatsAPI.getMyHeats(slug),
|
||||||
|
heatsAPI.getAllHeats(slug),
|
||||||
|
]);
|
||||||
|
setMyHeats(myHeatsData);
|
||||||
|
setShowHeatsBanner(myHeatsData.length === 0);
|
||||||
|
|
||||||
|
// Map userHeats
|
||||||
|
const heatsMap = new Map();
|
||||||
|
allHeatsData.forEach(userHeat => {
|
||||||
|
heatsMap.set(userHeat.userId, userHeat.heats);
|
||||||
|
});
|
||||||
|
setUserHeats(heatsMap);
|
||||||
|
};
|
||||||
|
loadHeats();
|
||||||
|
}, [slug]);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add HeatsBanner before chat:**
|
||||||
|
```jsx
|
||||||
|
{showHeatsBanner && (
|
||||||
|
<HeatsBanner
|
||||||
|
slug={slug}
|
||||||
|
onSave={() => {
|
||||||
|
setShowHeatsBanner(false);
|
||||||
|
// Reload heats
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Add "Edit Heats" button in header (next to "Leave Event"):**
|
||||||
|
```jsx
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHeatsModal(true)}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<Edit size={16} />
|
||||||
|
Edit Heats
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create modal for editing heats (reuse HeatsBanner logic)**
|
||||||
|
|
||||||
|
6. **Add heat badges to sidebar under username:**
|
||||||
|
```jsx
|
||||||
|
{activeUsers.map(activeUser => {
|
||||||
|
const userHeatsForThisUser = userHeats.get(activeUser.userId) || [];
|
||||||
|
const hasHeats = userHeatsForThisUser.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={activeUser.userId}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src={activeUser.avatar} />
|
||||||
|
<span>{activeUser.username}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Heat badges */}
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{userHeatsForThisUser.slice(0, 3).map(heat => (
|
||||||
|
<span key={heat.id} className="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded">
|
||||||
|
{heat.competitionType.abbreviation} {heat.division.abbreviation} {heat.heatNumber}
|
||||||
|
{heat.role && ` ${heat.role[0]}`}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{userHeatsForThisUser.length > 3 && (
|
||||||
|
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-600 rounded">
|
||||||
|
+{userHeatsForThisUser.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* UserPlus button - disabled if no heats */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleMatchWith(activeUser.userId)}
|
||||||
|
disabled={!hasHeats}
|
||||||
|
className="p-1 text-primary-600 hover:bg-primary-50 rounded disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<UserPlus className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Add filter checkbox above active users:**
|
||||||
|
```jsx
|
||||||
|
<label className="flex items-center gap-2 text-sm text-gray-700 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={hideMyHeats}
|
||||||
|
onChange={(e) => setHideMyHeats(e.target.checked)}
|
||||||
|
className="rounded border-gray-300"
|
||||||
|
/>
|
||||||
|
Hide users from my heats
|
||||||
|
</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
8. **Filter logic:**
|
||||||
|
```javascript
|
||||||
|
const filteredUsers = hideMyHeats
|
||||||
|
? activeUsers.filter(activeUser => {
|
||||||
|
const theirHeats = userHeats.get(activeUser.userId) || [];
|
||||||
|
return !theirHeats.some(theirHeat =>
|
||||||
|
myHeats.some(myHeat =>
|
||||||
|
myHeat.divisionId === theirHeat.divisionId &&
|
||||||
|
myHeat.competitionTypeId === theirHeat.competitionTypeId &&
|
||||||
|
myHeat.heatNumber === theirHeat.heatNumber
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: activeUsers;
|
||||||
|
```
|
||||||
|
|
||||||
|
9. **Socket.IO heats_updated listener:**
|
||||||
|
```javascript
|
||||||
|
useEffect(() => {
|
||||||
|
const socket = getSocket();
|
||||||
|
if (!socket) return;
|
||||||
|
|
||||||
|
socket.on('heats_updated', ({ userId, username, heats }) => {
|
||||||
|
setUserHeats(prev => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
newMap.set(userId, heats);
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If it's current user, update myHeats
|
||||||
|
if (userId === user.id) {
|
||||||
|
setMyHeats(heats);
|
||||||
|
setShowHeatsBanner(heats.length === 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off('heats_updated');
|
||||||
|
};
|
||||||
|
}, [user.id]);
|
||||||
|
```
|
||||||
|
|
||||||
### Step 5: Styling & UX (0.5-1h) ⏳
|
### Step 5: Styling & UX (0.5-1h) ⏳
|
||||||
- [ ] Heat badges design (color-coded by division?)
|
- [ ] Heat badges design (color-coded by division?)
|
||||||
- [ ] Banner responsive design (mobile + desktop)
|
- [ ] Banner responsive design (mobile + desktop)
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useState, useRef, useEffect } from 'react';
|
|||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
import Layout from '../components/layout/Layout';
|
import Layout from '../components/layout/Layout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { Send, UserPlus, Loader2, LogOut, AlertTriangle, QrCode } from 'lucide-react';
|
import { Send, UserPlus, Loader2, LogOut, AlertTriangle, QrCode, Edit2, Filter, X } from 'lucide-react';
|
||||||
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
||||||
import { eventsAPI } from '../services/api';
|
import { eventsAPI, heatsAPI } from '../services/api';
|
||||||
|
import HeatsBanner from '../components/heats/HeatsBanner';
|
||||||
|
|
||||||
const EventChatPage = () => {
|
const EventChatPage = () => {
|
||||||
const { slug } = useParams();
|
const { slug } = useParams();
|
||||||
@@ -24,6 +25,13 @@ const EventChatPage = () => {
|
|||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
const messagesContainerRef = useRef(null);
|
const messagesContainerRef = useRef(null);
|
||||||
|
|
||||||
|
// Heats state
|
||||||
|
const [myHeats, setMyHeats] = useState([]);
|
||||||
|
const [userHeats, setUserHeats] = useState(new Map());
|
||||||
|
const [showHeatsBanner, setShowHeatsBanner] = useState(false);
|
||||||
|
const [hideMyHeats, setHideMyHeats] = useState(false);
|
||||||
|
const [showHeatsModal, setShowHeatsModal] = useState(false);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
@@ -57,6 +65,32 @@ const EventChatPage = () => {
|
|||||||
fetchEvent();
|
fetchEvent();
|
||||||
}, [slug]);
|
}, [slug]);
|
||||||
|
|
||||||
|
// Load heats data
|
||||||
|
useEffect(() => {
|
||||||
|
if (!event || !isParticipant) return;
|
||||||
|
|
||||||
|
const loadHeats = async () => {
|
||||||
|
try {
|
||||||
|
// Load my heats
|
||||||
|
const myHeatsData = await heatsAPI.getMyHeats(slug);
|
||||||
|
setMyHeats(myHeatsData);
|
||||||
|
setShowHeatsBanner(myHeatsData.length === 0);
|
||||||
|
|
||||||
|
// Load all users' heats
|
||||||
|
const allHeatsData = await heatsAPI.getAllHeats(slug);
|
||||||
|
const heatsMap = new Map();
|
||||||
|
allHeatsData.forEach((userHeat) => {
|
||||||
|
heatsMap.set(userHeat.userId, userHeat.heats);
|
||||||
|
});
|
||||||
|
setUserHeats(heatsMap);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load heats:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadHeats();
|
||||||
|
}, [event, isParticipant, slug]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
@@ -115,6 +149,28 @@ const EventChatPage = () => {
|
|||||||
console.log(`${userData.username} left the room`);
|
console.log(`${userData.username} left the room`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Heats updated notification
|
||||||
|
socket.on('heats_updated', (data) => {
|
||||||
|
const { userId, heats } = data;
|
||||||
|
|
||||||
|
// Update userHeats map
|
||||||
|
setUserHeats((prev) => {
|
||||||
|
const newMap = new Map(prev);
|
||||||
|
if (heats && heats.length > 0) {
|
||||||
|
newMap.set(userId, heats);
|
||||||
|
} else {
|
||||||
|
newMap.delete(userId);
|
||||||
|
}
|
||||||
|
return newMap;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If it's the current user, update myHeats
|
||||||
|
if (userId === user.id) {
|
||||||
|
setMyHeats(heats || []);
|
||||||
|
setShowHeatsBanner(heats.length === 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
return () => {
|
return () => {
|
||||||
socket.emit('leave_event_room');
|
socket.emit('leave_event_room');
|
||||||
@@ -125,6 +181,7 @@ const EventChatPage = () => {
|
|||||||
socket.off('active_users');
|
socket.off('active_users');
|
||||||
socket.off('user_joined');
|
socket.off('user_joined');
|
||||||
socket.off('user_left');
|
socket.off('user_left');
|
||||||
|
socket.off('heats_updated');
|
||||||
};
|
};
|
||||||
}, [event, slug, user.id]);
|
}, [event, slug, user.id]);
|
||||||
|
|
||||||
@@ -187,6 +244,41 @@ const EventChatPage = () => {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleHeatsSave = () => {
|
||||||
|
setShowHeatsBanner(false);
|
||||||
|
setShowHeatsModal(false);
|
||||||
|
// Heats will be updated via Socket.IO heats_updated event
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatHeat = (heat) => {
|
||||||
|
const parts = [
|
||||||
|
heat.competitionType?.abbreviation || '',
|
||||||
|
heat.division?.abbreviation || '',
|
||||||
|
heat.heatNumber,
|
||||||
|
];
|
||||||
|
if (heat.role) {
|
||||||
|
parts.push(heat.role.charAt(0)); // L or F
|
||||||
|
}
|
||||||
|
return parts.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldHideUser = (userId) => {
|
||||||
|
if (!hideMyHeats || myHeats.length === 0) return false;
|
||||||
|
|
||||||
|
const targetUserHeats = userHeats.get(userId);
|
||||||
|
if (!targetUserHeats || targetUserHeats.length === 0) return false;
|
||||||
|
|
||||||
|
// Hide if ANY of their heats match ANY of my heats (same division + competition_type + heat_number)
|
||||||
|
return targetUserHeats.some((targetHeat) =>
|
||||||
|
myHeats.some(
|
||||||
|
(myHeat) =>
|
||||||
|
myHeat.divisionId === targetHeat.divisionId &&
|
||||||
|
myHeat.competitionTypeId === targetHeat.competitionTypeId &&
|
||||||
|
myHeat.heatNumber === targetHeat.heatNumber
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLeaveEvent = async () => {
|
const handleLeaveEvent = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLeaving(true);
|
setIsLeaving(true);
|
||||||
@@ -304,6 +396,8 @@ const EventChatPage = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-primary-600 text-white p-4">
|
<div className="bg-primary-600 text-white p-4">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
<h2 className="text-2xl font-bold">{event.name}</h2>
|
<h2 className="text-2xl font-bold">{event.name}</h2>
|
||||||
<p className="text-primary-100 text-sm">{event.location}</p>
|
<p className="text-primary-100 text-sm">{event.location}</p>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
@@ -312,43 +406,110 @@ const EventChatPage = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{myHeats.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHeatsModal(true)}
|
||||||
|
className="flex items-center gap-2 px-3 py-2 bg-primary-700 hover:bg-primary-800 rounded-md transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
Edit Heats
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Heats Banner */}
|
||||||
|
{showHeatsBanner && (
|
||||||
|
<HeatsBanner
|
||||||
|
slug={slug}
|
||||||
|
onSave={handleHeatsSave}
|
||||||
|
onDismiss={() => setShowHeatsBanner(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex h-[calc(100vh-280px)]">
|
<div className="flex h-[calc(100vh-280px)]">
|
||||||
{/* Active Users Sidebar */}
|
{/* Active Users Sidebar */}
|
||||||
<div className="w-64 border-r bg-gray-50 p-4 overflow-y-auto">
|
<div className="w-64 border-r bg-gray-50 p-4 overflow-y-auto">
|
||||||
<h3 className="font-semibold text-gray-900 mb-4">
|
<div className="mb-4">
|
||||||
Active users ({activeUsers.length})
|
<h3 className="font-semibold text-gray-900 mb-3">
|
||||||
|
Active users ({activeUsers.filter(u => !shouldHideUser(u.userId)).length})
|
||||||
</h3>
|
</h3>
|
||||||
{activeUsers.length === 0 && (
|
|
||||||
|
{/* Filter Checkbox */}
|
||||||
|
{myHeats.length > 0 && (
|
||||||
|
<label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer hover:text-gray-900">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={hideMyHeats}
|
||||||
|
onChange={(e) => setHideMyHeats(e.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
/>
|
||||||
|
<Filter className="w-3 h-3" />
|
||||||
|
<span>Hide users from my heats</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeUsers.filter(u => !shouldHideUser(u.userId)).length === 0 && (
|
||||||
<p className="text-sm text-gray-500">No other users online</p>
|
<p className="text-sm text-gray-500">No other users online</p>
|
||||||
)}
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{activeUsers.map((activeUser) => (
|
{activeUsers
|
||||||
|
.filter((activeUser) => !shouldHideUser(activeUser.userId))
|
||||||
|
.map((activeUser) => {
|
||||||
|
const thisUserHeats = userHeats.get(activeUser.userId) || [];
|
||||||
|
const hasHeats = thisUserHeats.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={activeUser.userId}
|
key={activeUser.userId}
|
||||||
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
|
className="flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||||
<img
|
<img
|
||||||
src={activeUser.avatar}
|
src={activeUser.avatar}
|
||||||
alt={activeUser.username}
|
alt={activeUser.username}
|
||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-full flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-medium text-gray-900">
|
<p className="text-sm font-medium text-gray-900 truncate">
|
||||||
{activeUser.username}
|
{activeUser.username}
|
||||||
</p>
|
</p>
|
||||||
|
{/* Heat Badges */}
|
||||||
|
{hasHeats && (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{thisUserHeats.slice(0, 3).map((heat, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-xs px-1.5 py-0.5 bg-amber-100 text-amber-800 rounded font-mono"
|
||||||
|
>
|
||||||
|
{formatHeat(heat)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{thisUserHeats.length > 3 && (
|
||||||
|
<span className="text-xs px-1.5 py-0.5 bg-gray-200 text-gray-700 rounded font-mono">
|
||||||
|
+{thisUserHeats.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMatchWith(activeUser.userId)}
|
onClick={() => handleMatchWith(activeUser.userId)}
|
||||||
className="p-1 text-primary-600 hover:bg-primary-50 rounded"
|
disabled={!hasHeats}
|
||||||
title="Connect"
|
className={`p-1 rounded flex-shrink-0 ${
|
||||||
|
hasHeats
|
||||||
|
? 'text-primary-600 hover:bg-primary-50'
|
||||||
|
: 'text-gray-300 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
title={hasHeats ? 'Connect' : 'User has not declared heats'}
|
||||||
>
|
>
|
||||||
<UserPlus className="w-4 h-4" />
|
<UserPlus className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -490,6 +651,30 @@ const EventChatPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Edit Heats Modal */}
|
||||||
|
{showHeatsModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Edit Your Competition Heats</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHeatsModal(false)}
|
||||||
|
className="text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<HeatsBanner
|
||||||
|
slug={slug}
|
||||||
|
onSave={handleHeatsSave}
|
||||||
|
onDismiss={() => setShowHeatsModal(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user