From 37d2a7c548809930a37cc466083f31a8a2a77248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Gierwia=C5=82o?= Date: Fri, 14 Nov 2025 17:34:15 +0100 Subject: [PATCH] feat: add heats frontend API and HeatsBanner component - Add divisionsAPI, competitionTypesAPI, heatsAPI to frontend services - Create HeatsBanner component for declaring competition heats - Support dynamic heat entry (add/remove heats) - Validate required fields (competition type, division, heat number) - Optional role selection (Leader/Follower) - Real-time API integration with backend --- frontend/src/components/heats/HeatsBanner.jsx | 234 ++++++++++++++++++ frontend/src/services/api.js | 44 ++++ 2 files changed, 278 insertions(+) create mode 100644 frontend/src/components/heats/HeatsBanner.jsx diff --git a/frontend/src/components/heats/HeatsBanner.jsx b/frontend/src/components/heats/HeatsBanner.jsx new file mode 100644 index 0000000..0ebd56c --- /dev/null +++ b/frontend/src/components/heats/HeatsBanner.jsx @@ -0,0 +1,234 @@ +import { useState, useEffect } from 'react'; +import { X, Plus, Trash2, Loader2 } from 'lucide-react'; +import { divisionsAPI, competitionTypesAPI, heatsAPI } from '../../services/api'; + +export default function HeatsBanner({ slug, onSave, onDismiss }) { + const [divisions, setDivisions] = useState([]); + const [competitionTypes, setCompetitionTypes] = useState([]); + const [heats, setHeats] = useState([{ divisionId: '', competitionTypeId: '', heatNumber: '', role: '' }]); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(''); + + useEffect(() => { + loadOptions(); + }, []); + + const loadOptions = async () => { + try { + setLoading(true); + const [divisionsData, competitionTypesData] = await Promise.all([ + divisionsAPI.getAll(), + competitionTypesAPI.getAll(), + ]); + setDivisions(divisionsData); + setCompetitionTypes(competitionTypesData); + } catch (err) { + console.error('Failed to load options:', err); + setError('Failed to load divisions and competition types'); + } finally { + setLoading(false); + } + }; + + const addHeat = () => { + setHeats([...heats, { divisionId: '', competitionTypeId: '', heatNumber: '', role: '' }]); + }; + + const removeHeat = (index) => { + setHeats(heats.filter((_, i) => i !== index)); + }; + + const updateHeat = (index, field, value) => { + const newHeats = [...heats]; + newHeats[index][field] = value; + setHeats(newHeats); + }; + + const handleSave = async () => { + // Validation + for (const heat of heats) { + if (!heat.divisionId || !heat.competitionTypeId || !heat.heatNumber) { + setError('Please fill in all required fields (Division, Competition Type, Heat Number)'); + return; + } + } + + try { + setSaving(true); + setError(''); + + const heatsToSave = heats.map(heat => ({ + divisionId: parseInt(heat.divisionId), + competitionTypeId: parseInt(heat.competitionTypeId), + heatNumber: parseInt(heat.heatNumber), + role: heat.role || null, + })); + + await heatsAPI.saveHeats(slug, heatsToSave); + + if (onSave) { + onSave(); + } + } catch (err) { + console.error('Failed to save heats:', err); + setError(err.message || 'Failed to save heats. Please try again.'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
+ + Loading... +
+
+ ); + } + + return ( +
+
+
+
+

+ 🏆 + Declare Your Competition Heats +

+

+ To participate in matchmaking, please specify which heats you're dancing in +

+
+ {onDismiss && ( + + )} +
+ + {error && ( +
+ {error} +
+ )} + +
+ {heats.map((heat, index) => ( +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ ))} +
+ +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index ff245f9..84c7d52 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -220,4 +220,48 @@ export const eventsAPI = { }, }; +// Divisions API (Phase 1.6) +export const divisionsAPI = { + async getAll() { + const data = await fetchAPI('/divisions'); + return data.data; + }, +}; + +// Competition Types API (Phase 1.6) +export const competitionTypesAPI = { + async getAll() { + const data = await fetchAPI('/competition-types'); + return data.data; + }, +}; + +// Heats API (Phase 1.6) +export const heatsAPI = { + async saveHeats(slug, heats) { + const data = await fetchAPI(`/events/${slug}/heats`, { + method: 'POST', + body: JSON.stringify({ heats }), + }); + return data; + }, + + async getMyHeats(slug) { + const data = await fetchAPI(`/events/${slug}/heats/me`); + return data.data; + }, + + async getAllHeats(slug) { + const data = await fetchAPI(`/events/${slug}/heats/all`); + return data.data; + }, + + async deleteHeat(slug, heatId) { + const data = await fetchAPI(`/events/${slug}/heats/${heatId}`, { + method: 'DELETE', + }); + return data; + }, +}; + export { ApiError };