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 (
+
+ );
+ }
+
+ 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 };