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
|
||||
|
||||
### Step 4: Frontend Components (2-3h) ⏳
|
||||
- [ ] Create HeatsBanner component (sticky between header and chat):
|
||||
- Show only if user has no heats declared
|
||||
- Form with dynamic heat entries (add/remove)
|
||||
- Fields per entry: Competition Type (select), Division (select), Heat Number (1-9), Role (optional: Leader/Follower)
|
||||
- "Save Heats" button → POST /api/events/:slug/heats
|
||||
- On save success: hide banner, show success message
|
||||
- [ ] Add "Edit Heats" button in EventChatPage header (next to "Leave Event")
|
||||
- [x] Create HeatsBanner component (sticky between header and chat) - ✅ DONE
|
||||
- [x] Show only if user has no heats declared
|
||||
- [x] Form with dynamic heat entries (add/remove)
|
||||
- [x] Fields per entry: Competition Type (select), Division (select), Heat Number (1-9), Role (optional: Leader/Follower)
|
||||
- [x] "Save Heats" button → POST /api/events/:slug/heats
|
||||
- [x] On save success: hide banner, show success message
|
||||
- [ ] Add "Edit Heats" button in EventChatPage header (next to "Leave Event") - ⏳ TODO
|
||||
- Opens modal with same form as banner
|
||||
- Pre-fill with existing heats
|
||||
- "Update Heats" button
|
||||
- [ ] Update EventChatPage sidebar (Active Users):
|
||||
- [ ] Update EventChatPage sidebar (Active Users) - ⏳ TODO
|
||||
- Display heat badges under username
|
||||
- Format: "J&J NOV 1 L", "STR ADV 3" (no role if NULL)
|
||||
- Max 3 visible badges, "+" indicator if more
|
||||
- Add checkbox: "Hide users from my heats"
|
||||
- Logic: Hide users with ANY matching (division + competition_type + heat_number)
|
||||
- Disable UserPlus icon if user has no heats declared
|
||||
- [ ] Create frontend API methods in services/api.js:
|
||||
- divisionsAPI.getAll()
|
||||
- competitionTypesAPI.getAll()
|
||||
- heatsAPI.saveHeats(slug, heats[])
|
||||
- heatsAPI.getMyHeats(slug)
|
||||
- heatsAPI.getAllHeats(slug)
|
||||
- heatsAPI.deleteHeat(slug, heatId)
|
||||
- [ ] Socket.IO integration:
|
||||
- [x] Create frontend API methods in services/api.js - ✅ DONE
|
||||
- [x] divisionsAPI.getAll()
|
||||
- [x] competitionTypesAPI.getAll()
|
||||
- [x] heatsAPI.saveHeats(slug, heats[])
|
||||
- [x] heatsAPI.getMyHeats(slug)
|
||||
- [x] heatsAPI.getAllHeats(slug)
|
||||
- [x] heatsAPI.deleteHeat(slug, heatId)
|
||||
- [ ] Socket.IO integration - ⏳ TODO
|
||||
- Listen to `heats_updated` event
|
||||
- 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) ⏳
|
||||
- [ ] Heat badges design (color-coded by division?)
|
||||
- [ ] Banner responsive design (mobile + desktop)
|
||||
|
||||
Reference in New Issue
Block a user