diff --git a/frontend/src/components/events/MatchingConfigSection.jsx b/frontend/src/components/events/MatchingConfigSection.jsx
new file mode 100644
index 0000000..91b2e6e
--- /dev/null
+++ b/frontend/src/components/events/MatchingConfigSection.jsx
@@ -0,0 +1,133 @@
+import { useState } from 'react';
+import { Video, Clock, Save, RefreshCw } from 'lucide-react';
+import { matchingAPI } from '../../services/api';
+
+/**
+ * Auto-matching configuration section
+ * Allows setting registration deadline and running matching algorithm
+ */
+const MatchingConfigSection = ({ slug, event, onRefresh }) => {
+ const [deadlineInput, setDeadlineInput] = useState(() => {
+ if (event?.registrationDeadline) {
+ const deadline = new Date(event.registrationDeadline);
+ return deadline.toISOString().slice(0, 16);
+ }
+ return '';
+ });
+ const [savingDeadline, setSavingDeadline] = useState(false);
+ const [runningMatching, setRunningMatching] = useState(false);
+
+ const handleSaveDeadline = async () => {
+ try {
+ setSavingDeadline(true);
+ const deadline = deadlineInput ? new Date(deadlineInput).toISOString() : null;
+ await matchingAPI.setRegistrationDeadline(slug, deadline);
+ onRefresh?.();
+ } catch (err) {
+ console.error('Failed to save deadline:', err);
+ alert('Nie udalo sie zapisac deadline');
+ } finally {
+ setSavingDeadline(false);
+ }
+ };
+
+ const handleRunMatching = async () => {
+ try {
+ setRunningMatching(true);
+ const result = await matchingAPI.runMatching(slug);
+ alert(`Matching zakonczony! Dopasowano: ${result.matched}, Nie znaleziono: ${result.notFound}`);
+ onRefresh?.();
+ } catch (err) {
+ console.error('Failed to run matching:', err);
+ alert('Nie udalo sie uruchomic matchingu');
+ } finally {
+ setRunningMatching(false);
+ }
+ };
+
+ return (
+
+
+
+ Auto-Matching (Nagrywanie)
+
+
+
+ {/* Registration Deadline */}
+
+
+
+ Deadline rejestracji
+
+
+ Matching uruchomi sie 30 min po tym terminie
+
+
+ setDeadlineInput(e.target.value)}
+ className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
+ />
+
+ {savingDeadline ? (
+
+ ) : (
+
+ )}
+ Zapisz
+
+
+ {event?.registrationDeadline && (
+
+ Aktualny deadline: {new Date(event.registrationDeadline).toLocaleString('pl-PL')}
+
+ )}
+
+
+ {/* Matching Status & Run */}
+
+
+ Status matchingu
+
+ {event?.matchingRunAt ? (
+
+
+ Ostatnie uruchomienie: {new Date(event.matchingRunAt).toLocaleString('pl-PL')}
+
+
+ ) : (
+
+
+ Matching nie byl jeszcze uruchomiony
+
+
+ )}
+
+ {runningMatching ? (
+ <>
+
+ Trwa matching...
+ >
+ ) : (
+ <>
+
+ Uruchom Matching
+ >
+ )}
+
+
+
+
+ );
+};
+
+export default MatchingConfigSection;
diff --git a/frontend/src/components/events/ParticipantsSection.jsx b/frontend/src/components/events/ParticipantsSection.jsx
new file mode 100644
index 0000000..64953ce
--- /dev/null
+++ b/frontend/src/components/events/ParticipantsSection.jsx
@@ -0,0 +1,63 @@
+import { Users } from 'lucide-react';
+
+/**
+ * Participants section for event details page
+ * Displays list of event participants with avatars
+ */
+const ParticipantsSection = ({ participants, totalCount }) => {
+ return (
+
+
+
+ Participants ({totalCount})
+
+
+ {participants.length === 0 ? (
+
+
+
No participants yet
+
Share the QR code to get started!
+
+ ) : (
+
+ {participants.map((participant) => (
+
+ {/* Avatar */}
+
+ {participant.avatar ? (
+
+ ) : (
+
{participant.username.charAt(0).toUpperCase()}
+ )}
+
+
+ {/* User Info */}
+
+
+ {participant.firstName && participant.lastName
+ ? `${participant.firstName} ${participant.lastName}`
+ : participant.username}
+
+
@{participant.username}
+
+
+ {/* Joined Date */}
+
+ {new Date(participant.joinedAt).toLocaleDateString()}
+
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default ParticipantsSection;
diff --git a/frontend/src/components/events/QRCodeSection.jsx b/frontend/src/components/events/QRCodeSection.jsx
new file mode 100644
index 0000000..44cecc6
--- /dev/null
+++ b/frontend/src/components/events/QRCodeSection.jsx
@@ -0,0 +1,77 @@
+import { useState } from 'react';
+import { QRCodeSVG } from 'qrcode.react';
+import { Copy, Check, QrCode } from 'lucide-react';
+
+/**
+ * QR Code section for event check-in
+ * Displays QR code, copy link button, and validity period
+ */
+const QRCodeSection = ({ checkin, formatDate }) => {
+ const [copied, setCopied] = useState(false);
+
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(checkin.url);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ } catch (err) {
+ console.error('Failed to copy:', err);
+ }
+ };
+
+ return (
+
+
+
+ Event Check-in QR Code
+
+
+ {/* QR Code Display */}
+
+
+
+
+ {/* Check-in URL */}
+
+
+ Check-in Link
+
+
+
+
+ {copied ? : }
+ {copied ? 'Copied!' : 'Copy'}
+
+
+
+
+ {/* Valid Dates */}
+
+
Valid Period
+
+ {formatDate(checkin.validFrom)} - {formatDate(checkin.validUntil)}
+
+ {process.env.NODE_ENV === 'development' && (
+
+ Development mode: Date validation disabled
+
+ )}
+
+
+ );
+};
+
+export default QRCodeSection;
diff --git a/frontend/src/components/events/ScheduleConfigSection.jsx b/frontend/src/components/events/ScheduleConfigSection.jsx
new file mode 100644
index 0000000..c661793
--- /dev/null
+++ b/frontend/src/components/events/ScheduleConfigSection.jsx
@@ -0,0 +1,186 @@
+import { useState, useEffect } from 'react';
+import { Layers, Plus, Trash2, Save, RefreshCw } from 'lucide-react';
+import { matchingAPI, divisionsAPI } from '../../services/api';
+
+/**
+ * Schedule configuration section for division collision groups
+ * Allows organizing divisions into time slots for matching algorithm
+ */
+const ScheduleConfigSection = ({ slug, event, onRefresh }) => {
+ const [divisions, setDivisions] = useState([]);
+ const [scheduleSlots, setScheduleSlots] = useState([]);
+ const [savingSchedule, setSavingSchedule] = useState(false);
+
+ // Load divisions on mount
+ useEffect(() => {
+ loadDivisions();
+ }, []);
+
+ // Initialize schedule slots when event changes
+ useEffect(() => {
+ if (event?.scheduleConfig?.slots) {
+ setScheduleSlots(event.scheduleConfig.slots);
+ } else {
+ setScheduleSlots([]);
+ }
+ }, [event]);
+
+ const loadDivisions = async () => {
+ try {
+ const data = await divisionsAPI.getAll();
+ setDivisions(data);
+ } catch (err) {
+ console.error('Failed to load divisions:', err);
+ }
+ };
+
+ const handleAddSlot = () => {
+ const maxOrder = scheduleSlots.length > 0
+ ? Math.max(...scheduleSlots.map(s => s.order))
+ : 0;
+ setScheduleSlots([...scheduleSlots, { order: maxOrder + 1, divisionIds: [] }]);
+ };
+
+ const handleRemoveSlot = (order) => {
+ setScheduleSlots(scheduleSlots.filter(s => s.order !== order));
+ };
+
+ const handleToggleDivision = (slotOrder, divisionId) => {
+ setScheduleSlots(scheduleSlots.map(slot => {
+ if (slot.order !== slotOrder) {
+ // Remove division from other slots if it's being added to this one
+ return {
+ ...slot,
+ divisionIds: slot.divisionIds.filter(id => id !== divisionId)
+ };
+ }
+ // Toggle division in this slot
+ const hasDiv = slot.divisionIds.includes(divisionId);
+ return {
+ ...slot,
+ divisionIds: hasDiv
+ ? slot.divisionIds.filter(id => id !== divisionId)
+ : [...slot.divisionIds, divisionId]
+ };
+ }));
+ };
+
+ const handleSaveSchedule = async () => {
+ try {
+ setSavingSchedule(true);
+ const scheduleConfig = scheduleSlots.length > 0
+ ? { slots: scheduleSlots.filter(s => s.divisionIds.length > 0) }
+ : null;
+ await matchingAPI.setScheduleConfig(slug, scheduleConfig);
+ onRefresh?.();
+ } catch (err) {
+ console.error('Failed to save schedule:', err);
+ alert('Nie udalo sie zapisac harmonogramu');
+ } finally {
+ setSavingSchedule(false);
+ }
+ };
+
+ // Get all assigned division IDs
+ const getAssignedDivisionIds = () => {
+ return new Set(scheduleSlots.flatMap(s => s.divisionIds));
+ };
+
+ return (
+
+
+
+ Konfiguracja harmonogramu
+
+
+ Dywizje w tym samym slocie czasowym koliduja ze soba (nie mozna nagrywac podczas gdy sie tancczy).
+ Dywizje bez przypisanego slotu sa traktowane jako osobne.
+
+
+ {/* Slots */}
+
+ {scheduleSlots
+ .sort((a, b) => a.order - b.order)
+ .map((slot) => (
+
+
+
+ Slot {slot.order}
+
+ handleRemoveSlot(slot.order)}
+ className="p-1 text-red-500 hover:bg-red-50 rounded"
+ title="Usun slot"
+ >
+
+
+
+
+ {divisions.map((division) => {
+ const isInSlot = slot.divisionIds.includes(division.id);
+ const assignedDivIds = getAssignedDivisionIds();
+ const isInOtherSlot = assignedDivIds.has(division.id) && !isInSlot;
+
+ return (
+ handleToggleDivision(slot.order, division.id)}
+ className={`px-3 py-1.5 text-sm rounded-full border transition-colors ${
+ isInSlot
+ ? 'bg-primary-600 text-white border-primary-600'
+ : isInOtherSlot
+ ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-pointer'
+ : 'bg-white text-gray-700 border-gray-300 hover:border-primary-400'
+ }`}
+ title={isInOtherSlot ? `Przypisane do innego slotu` : division.name}
+ >
+ {division.abbreviation}
+
+ );
+ })}
+
+ {slot.divisionIds.length === 0 && (
+
+ Kliknij dywizje aby dodac do slotu
+
+ )}
+
+ ))}
+
+
+ {/* Add slot button */}
+
+
+
+ Dodaj slot
+
+
+ {savingSchedule ? (
+
+ ) : (
+
+ )}
+ Zapisz harmonogram
+
+
+
+ {/* Current config info */}
+ {event?.scheduleConfig?.slots?.length > 0 && (
+
+
+ Zapisany harmonogram: {event.scheduleConfig.slots.length} slot(ow)
+
+
+ )}
+
+ );
+};
+
+export default ScheduleConfigSection;
diff --git a/frontend/src/pages/EventDetailsPage.jsx b/frontend/src/pages/EventDetailsPage.jsx
index 06a61f4..8e6e165 100644
--- a/frontend/src/pages/EventDetailsPage.jsx
+++ b/frontend/src/pages/EventDetailsPage.jsx
@@ -1,58 +1,28 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
-import { QRCodeSVG } from 'qrcode.react';
-import { Copy, Check, Users, Calendar, MapPin, QrCode, Video, Clock, Save, RefreshCw, Layers, Plus, Trash2 } from 'lucide-react';
+import { Calendar, MapPin } from 'lucide-react';
import Layout from '../components/layout/Layout';
-import { eventsAPI, matchingAPI, divisionsAPI } from '../services/api';
+import { eventsAPI } from '../services/api';
+import QRCodeSection from '../components/events/QRCodeSection';
+import ParticipantsSection from '../components/events/ParticipantsSection';
+import MatchingConfigSection from '../components/events/MatchingConfigSection';
+import ScheduleConfigSection from '../components/events/ScheduleConfigSection';
export default function EventDetailsPage() {
const { slug } = useParams();
const [eventDetails, setEventDetails] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
- const [copied, setCopied] = useState(false);
-
- // Registration deadline state
- const [deadlineInput, setDeadlineInput] = useState('');
- const [savingDeadline, setSavingDeadline] = useState(false);
- const [runningMatching, setRunningMatching] = useState(false);
-
- // Schedule config state
- const [divisions, setDivisions] = useState([]);
- const [scheduleSlots, setScheduleSlots] = useState([]);
- const [savingSchedule, setSavingSchedule] = useState(false);
useEffect(() => {
fetchEventDetails();
- loadDivisions();
}, [slug]);
- // Initialize schedule slots when eventDetails loads
- useEffect(() => {
- if (eventDetails?.event?.scheduleConfig?.slots) {
- setScheduleSlots(eventDetails.event.scheduleConfig.slots);
- }
- }, [eventDetails]);
-
- const loadDivisions = async () => {
- try {
- const data = await divisionsAPI.getAll();
- setDivisions(data);
- } catch (err) {
- console.error('Failed to load divisions:', err);
- }
- };
-
const fetchEventDetails = async () => {
try {
setLoading(true);
const response = await eventsAPI.getDetails(slug);
setEventDetails(response.data);
- // Initialize deadline input
- if (response.data?.event?.registrationDeadline) {
- const deadline = new Date(response.data.event.registrationDeadline);
- setDeadlineInput(deadline.toISOString().slice(0, 16)); // Format for datetime-local
- }
} catch (err) {
console.error('Error loading event details:', err);
setError(err.message || 'Failed to load event details');
@@ -61,103 +31,6 @@ export default function EventDetailsPage() {
}
};
- const handleSaveDeadline = async () => {
- try {
- setSavingDeadline(true);
- const deadline = deadlineInput ? new Date(deadlineInput).toISOString() : null;
- await matchingAPI.setRegistrationDeadline(slug, deadline);
- await fetchEventDetails(); // Refresh data
- } catch (err) {
- console.error('Failed to save deadline:', err);
- alert('Nie udalo sie zapisac deadline');
- } finally {
- setSavingDeadline(false);
- }
- };
-
- const handleRunMatching = async () => {
- try {
- setRunningMatching(true);
- const result = await matchingAPI.runMatching(slug);
- alert(`Matching zakonczony! Dopasowano: ${result.matched}, Nie znaleziono: ${result.notFound}`);
- await fetchEventDetails(); // Refresh data
- } catch (err) {
- console.error('Failed to run matching:', err);
- alert('Nie udalo sie uruchomic matchingu');
- } finally {
- setRunningMatching(false);
- }
- };
-
- // Schedule config handlers
- const handleAddSlot = () => {
- const maxOrder = scheduleSlots.length > 0
- ? Math.max(...scheduleSlots.map(s => s.order))
- : 0;
- setScheduleSlots([...scheduleSlots, { order: maxOrder + 1, divisionIds: [] }]);
- };
-
- const handleRemoveSlot = (order) => {
- setScheduleSlots(scheduleSlots.filter(s => s.order !== order));
- };
-
- const handleToggleDivision = (slotOrder, divisionId) => {
- setScheduleSlots(scheduleSlots.map(slot => {
- if (slot.order !== slotOrder) {
- // Remove division from other slots if it's being added to this one
- return {
- ...slot,
- divisionIds: slot.divisionIds.filter(id => id !== divisionId)
- };
- }
- // Toggle division in this slot
- const hasDiv = slot.divisionIds.includes(divisionId);
- return {
- ...slot,
- divisionIds: hasDiv
- ? slot.divisionIds.filter(id => id !== divisionId)
- : [...slot.divisionIds, divisionId]
- };
- }));
- };
-
- const handleSaveSchedule = async () => {
- try {
- setSavingSchedule(true);
- const scheduleConfig = scheduleSlots.length > 0
- ? { slots: scheduleSlots.filter(s => s.divisionIds.length > 0) }
- : null;
- await matchingAPI.setScheduleConfig(slug, scheduleConfig);
- await fetchEventDetails();
- } catch (err) {
- console.error('Failed to save schedule:', err);
- alert('Nie udalo sie zapisac harmonogramu');
- } finally {
- setSavingSchedule(false);
- }
- };
-
- // Get division name by ID
- const getDivisionName = (divisionId) => {
- const div = divisions.find(d => d.id === divisionId);
- return div ? div.abbreviation : `#${divisionId}`;
- };
-
- // Get all assigned division IDs
- const getAssignedDivisionIds = () => {
- return new Set(scheduleSlots.flatMap(s => s.divisionIds));
- };
-
- const copyToClipboard = async () => {
- try {
- await navigator.clipboard.writeText(eventDetails.checkin.url);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- } catch (err) {
- console.error('Failed to copy:', err);
- }
- };
-
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
@@ -221,290 +94,20 @@ export default function EventDetailsPage() {
+ {/* QR Code & Participants Grid */}
- {/* QR Code Section */}
-
-
-
- Event Check-in QR Code
-
-
- {/* QR Code Display */}
-
-
-
-
- {/* Check-in URL */}
-
-
- Check-in Link
-
-
-
-
- {copied ? : }
- {copied ? 'Copied!' : 'Copy'}
-
-
-
-
- {/* Valid Dates */}
-
-
Valid Period
-
- {formatDate(checkin.validFrom)} - {formatDate(checkin.validUntil)}
-
- {process.env.NODE_ENV === 'development' && (
-
- ⚠️ Development mode: Date validation disabled
-
- )}
-
-
-
- {/* Participants Section */}
-
-
-
- Participants ({stats.totalParticipants})
-
-
- {participants.length === 0 ? (
-
-
-
No participants yet
-
Share the QR code to get started!
-
- ) : (
-
- {participants.map((participant) => (
-
- {/* Avatar */}
-
- {participant.avatar ? (
-
- ) : (
-
{participant.username.charAt(0).toUpperCase()}
- )}
-
-
- {/* User Info */}
-
-
- {participant.firstName && participant.lastName
- ? `${participant.firstName} ${participant.lastName}`
- : participant.username}
-
-
@{participant.username}
-
-
- {/* Joined Date */}
-
- {new Date(participant.joinedAt).toLocaleDateString()}
-
-
- ))}
-
- )}
-
+
+
{/* Auto-Matching Configuration */}
-
-
-
- Auto-Matching (Nagrywanie)
-
-
-
- {/* Registration Deadline */}
-
-
-
- Deadline rejestracji
-
-
- Matching uruchomi sie 30 min po tym terminie
-
-
- setDeadlineInput(e.target.value)}
- className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
- />
-
- {savingDeadline ? (
-
- ) : (
-
- )}
- Zapisz
-
-
- {event.registrationDeadline && (
-
- Aktualny deadline: {new Date(event.registrationDeadline).toLocaleString('pl-PL')}
-
- )}
-
-
- {/* Matching Status & Run */}
-
-
- Status matchingu
-
- {event.matchingRunAt ? (
-
-
- Ostatnie uruchomienie: {new Date(event.matchingRunAt).toLocaleString('pl-PL')}
-
-
- ) : (
-
-
- Matching nie byl jeszcze uruchomiony
-
-
- )}
-
- {runningMatching ? (
- <>
-
- Trwa matching...
- >
- ) : (
- <>
-
- Uruchom Matching
- >
- )}
-
-
-
+
+
{/* Schedule Configuration */}
-
-
-
- Konfiguracja harmonogramu
-
-
- Dywizje w tym samym slocie czasowym koliduja ze soba (nie mozna nagrywac podczas gdy sie tancczy).
- Dywizje bez przypisanego slotu sa traktowane jako osobne.
-
-
- {/* Slots */}
-
- {scheduleSlots
- .sort((a, b) => a.order - b.order)
- .map((slot) => (
-
-
-
- Slot {slot.order}
-
- handleRemoveSlot(slot.order)}
- className="p-1 text-red-500 hover:bg-red-50 rounded"
- title="Usun slot"
- >
-
-
-
-
- {divisions.map((division) => {
- const isInSlot = slot.divisionIds.includes(division.id);
- const assignedDivIds = getAssignedDivisionIds();
- const isInOtherSlot = assignedDivIds.has(division.id) && !isInSlot;
-
- return (
- handleToggleDivision(slot.order, division.id)}
- className={`px-3 py-1.5 text-sm rounded-full border transition-colors ${
- isInSlot
- ? 'bg-primary-600 text-white border-primary-600'
- : isInOtherSlot
- ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-pointer'
- : 'bg-white text-gray-700 border-gray-300 hover:border-primary-400'
- }`}
- title={isInOtherSlot ? `Przypisane do innego slotu` : division.name}
- >
- {division.abbreviation}
-
- );
- })}
-
- {slot.divisionIds.length === 0 && (
-
- Kliknij dywizje aby dodac do slotu
-
- )}
-
- ))}
-
-
- {/* Add slot button */}
-
-
-
- Dodaj slot
-
-
- {savingSchedule ? (
-
- ) : (
-
- )}
- Zapisz harmonogram
-
-
-
- {/* Current config info */}
- {event.scheduleConfig?.slots?.length > 0 && (
-
-
- Zapisany harmonogram: {event.scheduleConfig.slots.length} slot(ow)
-
-
- )}
+
+
{/* Action Buttons */}