refactor(frontend): extract EventDetailsPage into components

Split large EventDetailsPage (545 lines) into smaller, focused components:
- QRCodeSection: QR code display and copy link functionality
- ParticipantsSection: participants list with avatars
- MatchingConfigSection: deadline and matching controls
- ScheduleConfigSection: division slot configuration

EventDetailsPage now 148 lines (-73%)
This commit is contained in:
Radosław Gierwiało
2025-11-23 22:02:09 +01:00
parent 4467c570b0
commit bddcf5f4f9
5 changed files with 472 additions and 410 deletions

View File

@@ -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 (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<Video className="text-primary-600" />
Auto-Matching (Nagrywanie)
</h2>
<div className="grid md:grid-cols-2 gap-6">
{/* Registration Deadline */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Clock size={16} className="inline mr-1" />
Deadline rejestracji
</label>
<p className="text-xs text-gray-500 mb-2">
Matching uruchomi sie 30 min po tym terminie
</p>
<div className="flex gap-2">
<input
type="datetime-local"
value={deadlineInput}
onChange={(e) => setDeadlineInput(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
/>
<button
onClick={handleSaveDeadline}
disabled={savingDeadline}
className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50 flex items-center gap-2"
>
{savingDeadline ? (
<RefreshCw size={16} className="animate-spin" />
) : (
<Save size={16} />
)}
Zapisz
</button>
</div>
{event?.registrationDeadline && (
<p className="text-sm text-green-600 mt-2">
Aktualny deadline: {new Date(event.registrationDeadline).toLocaleString('pl-PL')}
</p>
)}
</div>
{/* Matching Status & Run */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Status matchingu
</label>
{event?.matchingRunAt ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mb-3">
<p className="text-green-800 text-sm">
Ostatnie uruchomienie: {new Date(event.matchingRunAt).toLocaleString('pl-PL')}
</p>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-3">
<p className="text-amber-800 text-sm">
Matching nie byl jeszcze uruchomiony
</p>
</div>
)}
<button
onClick={handleRunMatching}
disabled={runningMatching}
className="w-full px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
{runningMatching ? (
<>
<RefreshCw size={16} className="animate-spin" />
Trwa matching...
</>
) : (
<>
<Video size={16} />
Uruchom Matching
</>
)}
</button>
</div>
</div>
</div>
);
};
export default MatchingConfigSection;

View File

@@ -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 (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<Users className="text-primary-600" />
Participants ({totalCount})
</h2>
{participants.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Users size={48} className="mx-auto mb-2 text-gray-300" />
<p>No participants yet</p>
<p className="text-sm">Share the QR code to get started!</p>
</div>
) : (
<div className="space-y-3 max-h-[500px] overflow-y-auto">
{participants.map((participant) => (
<div
key={participant.userId}
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-primary-600 flex items-center justify-center text-white font-semibold">
{participant.avatar ? (
<img
src={participant.avatar}
alt={participant.username}
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<span>{participant.username.charAt(0).toUpperCase()}</span>
)}
</div>
{/* User Info */}
<div className="flex-1">
<p className="font-medium text-gray-900">
{participant.firstName && participant.lastName
? `${participant.firstName} ${participant.lastName}`
: participant.username}
</p>
<p className="text-sm text-gray-600">@{participant.username}</p>
</div>
{/* Joined Date */}
<div className="text-xs text-gray-500">
{new Date(participant.joinedAt).toLocaleDateString()}
</div>
</div>
))}
</div>
)}
</div>
);
};
export default ParticipantsSection;

View File

@@ -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 (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<QrCode className="text-primary-600" />
Event Check-in QR Code
</h2>
{/* QR Code Display */}
<div className="bg-white p-6 rounded-lg border-2 border-gray-200 mb-4 flex justify-center">
<QRCodeSVG
value={checkin.url}
size={256}
level="H"
includeMargin={true}
/>
</div>
{/* Check-in URL */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Check-in Link
</label>
<div className="flex gap-2">
<input
type="text"
value={checkin.url}
readOnly
className="flex-1 px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-sm"
/>
<button
onClick={copyToClipboard}
className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors flex items-center gap-2"
>
{copied ? <Check size={16} /> : <Copy size={16} />}
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
{/* Valid Dates */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm">
<p className="font-medium text-blue-900 mb-1">Valid Period</p>
<p className="text-blue-700">
{formatDate(checkin.validFrom)} - {formatDate(checkin.validUntil)}
</p>
{process.env.NODE_ENV === 'development' && (
<p className="text-blue-600 mt-2 text-xs">
Development mode: Date validation disabled
</p>
)}
</div>
</div>
);
};
export default QRCodeSection;

View File

@@ -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 (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<Layers className="text-primary-600" />
Konfiguracja harmonogramu
</h2>
<p className="text-sm text-gray-600 mb-4">
Dywizje w tym samym slocie czasowym koliduja ze soba (nie mozna nagrywac podczas gdy sie tancczy).
Dywizje bez przypisanego slotu sa traktowane jako osobne.
</p>
{/* Slots */}
<div className="space-y-4 mb-4">
{scheduleSlots
.sort((a, b) => a.order - b.order)
.map((slot) => (
<div key={slot.order} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium text-gray-900">
Slot {slot.order}
</h3>
<button
onClick={() => handleRemoveSlot(slot.order)}
className="p-1 text-red-500 hover:bg-red-50 rounded"
title="Usun slot"
>
<Trash2 size={16} />
</button>
</div>
<div className="flex flex-wrap gap-2">
{divisions.map((division) => {
const isInSlot = slot.divisionIds.includes(division.id);
const assignedDivIds = getAssignedDivisionIds();
const isInOtherSlot = assignedDivIds.has(division.id) && !isInSlot;
return (
<button
key={division.id}
onClick={() => 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}
</button>
);
})}
</div>
{slot.divisionIds.length === 0 && (
<p className="text-sm text-gray-400 mt-2 italic">
Kliknij dywizje aby dodac do slotu
</p>
)}
</div>
))}
</div>
{/* Add slot button */}
<div className="flex gap-3">
<button
onClick={handleAddSlot}
className="flex items-center gap-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-primary-400 hover:text-primary-600"
>
<Plus size={16} />
Dodaj slot
</button>
<button
onClick={handleSaveSchedule}
disabled={savingSchedule}
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{savingSchedule ? (
<RefreshCw size={16} className="animate-spin" />
) : (
<Save size={16} />
)}
Zapisz harmonogram
</button>
</div>
{/* Current config info */}
{event?.scheduleConfig?.slots?.length > 0 && (
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-sm text-blue-800">
Zapisany harmonogram: {event.scheduleConfig.slots.length} slot(ow)
</p>
</div>
)}
</div>
);
};
export default ScheduleConfigSection;

View File

@@ -1,58 +1,28 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import { QRCodeSVG } from 'qrcode.react'; import { Calendar, MapPin } from 'lucide-react';
import { Copy, Check, Users, Calendar, MapPin, QrCode, Video, Clock, Save, RefreshCw, Layers, Plus, Trash2 } from 'lucide-react';
import Layout from '../components/layout/Layout'; 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() { export default function EventDetailsPage() {
const { slug } = useParams(); const { slug } = useParams();
const [eventDetails, setEventDetails] = useState(null); const [eventDetails, setEventDetails] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); 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(() => { useEffect(() => {
fetchEventDetails(); fetchEventDetails();
loadDivisions();
}, [slug]); }, [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 () => { const fetchEventDetails = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await eventsAPI.getDetails(slug); const response = await eventsAPI.getDetails(slug);
setEventDetails(response.data); 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) { } catch (err) {
console.error('Error loading event details:', err); console.error('Error loading event details:', err);
setError(err.message || 'Failed to load event details'); 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) => { const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', { return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
@@ -221,290 +94,20 @@ export default function EventDetailsPage() {
</div> </div>
</div> </div>
{/* QR Code & Participants Grid */}
<div className="grid md:grid-cols-2 gap-6"> <div className="grid md:grid-cols-2 gap-6">
{/* QR Code Section */} <QRCodeSection checkin={checkin} formatDate={formatDate} />
<div className="bg-white rounded-lg shadow-md p-6"> <ParticipantsSection participants={participants} totalCount={stats.totalParticipants} />
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<QrCode className="text-primary-600" />
Event Check-in QR Code
</h2>
{/* QR Code Display */}
<div className="bg-white p-6 rounded-lg border-2 border-gray-200 mb-4 flex justify-center">
<QRCodeSVG
value={checkin.url}
size={256}
level="H"
includeMargin={true}
/>
</div>
{/* Check-in URL */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Check-in Link
</label>
<div className="flex gap-2">
<input
type="text"
value={checkin.url}
readOnly
className="flex-1 px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-sm"
/>
<button
onClick={copyToClipboard}
className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 transition-colors flex items-center gap-2"
>
{copied ? <Check size={16} /> : <Copy size={16} />}
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
{/* Valid Dates */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm">
<p className="font-medium text-blue-900 mb-1">Valid Period</p>
<p className="text-blue-700">
{formatDate(checkin.validFrom)} - {formatDate(checkin.validUntil)}
</p>
{process.env.NODE_ENV === 'development' && (
<p className="text-blue-600 mt-2 text-xs">
Development mode: Date validation disabled
</p>
)}
</div>
</div>
{/* Participants Section */}
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<Users className="text-primary-600" />
Participants ({stats.totalParticipants})
</h2>
{participants.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Users size={48} className="mx-auto mb-2 text-gray-300" />
<p>No participants yet</p>
<p className="text-sm">Share the QR code to get started!</p>
</div>
) : (
<div className="space-y-3 max-h-[500px] overflow-y-auto">
{participants.map((participant) => (
<div
key={participant.userId}
className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
{/* Avatar */}
<div className="w-10 h-10 rounded-full bg-primary-600 flex items-center justify-center text-white font-semibold">
{participant.avatar ? (
<img
src={participant.avatar}
alt={participant.username}
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<span>{participant.username.charAt(0).toUpperCase()}</span>
)}
</div>
{/* User Info */}
<div className="flex-1">
<p className="font-medium text-gray-900">
{participant.firstName && participant.lastName
? `${participant.firstName} ${participant.lastName}`
: participant.username}
</p>
<p className="text-sm text-gray-600">@{participant.username}</p>
</div>
{/* Joined Date */}
<div className="text-xs text-gray-500">
{new Date(participant.joinedAt).toLocaleDateString()}
</div>
</div>
))}
</div>
)}
</div>
</div> </div>
{/* Auto-Matching Configuration */} {/* Auto-Matching Configuration */}
<div className="mt-6 bg-white rounded-lg shadow-md p-6"> <div className="mt-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2"> <MatchingConfigSection slug={slug} event={event} onRefresh={fetchEventDetails} />
<Video className="text-primary-600" />
Auto-Matching (Nagrywanie)
</h2>
<div className="grid md:grid-cols-2 gap-6">
{/* Registration Deadline */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Clock size={16} className="inline mr-1" />
Deadline rejestracji
</label>
<p className="text-xs text-gray-500 mb-2">
Matching uruchomi sie 30 min po tym terminie
</p>
<div className="flex gap-2">
<input
type="datetime-local"
value={deadlineInput}
onChange={(e) => setDeadlineInput(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"
/>
<button
onClick={handleSaveDeadline}
disabled={savingDeadline}
className="px-4 py-2 bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50 flex items-center gap-2"
>
{savingDeadline ? (
<RefreshCw size={16} className="animate-spin" />
) : (
<Save size={16} />
)}
Zapisz
</button>
</div>
{event.registrationDeadline && (
<p className="text-sm text-green-600 mt-2">
Aktualny deadline: {new Date(event.registrationDeadline).toLocaleString('pl-PL')}
</p>
)}
</div>
{/* Matching Status & Run */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Status matchingu
</label>
{event.matchingRunAt ? (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mb-3">
<p className="text-green-800 text-sm">
Ostatnie uruchomienie: {new Date(event.matchingRunAt).toLocaleString('pl-PL')}
</p>
</div>
) : (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 mb-3">
<p className="text-amber-800 text-sm">
Matching nie byl jeszcze uruchomiony
</p>
</div>
)}
<button
onClick={handleRunMatching}
disabled={runningMatching}
className="w-full px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
{runningMatching ? (
<>
<RefreshCw size={16} className="animate-spin" />
Trwa matching...
</>
) : (
<>
<Video size={16} />
Uruchom Matching
</>
)}
</button>
</div>
</div>
</div> </div>
{/* Schedule Configuration */} {/* Schedule Configuration */}
<div className="mt-6 bg-white rounded-lg shadow-md p-6"> <div className="mt-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2"> <ScheduleConfigSection slug={slug} event={event} onRefresh={fetchEventDetails} />
<Layers className="text-primary-600" />
Konfiguracja harmonogramu
</h2>
<p className="text-sm text-gray-600 mb-4">
Dywizje w tym samym slocie czasowym koliduja ze soba (nie mozna nagrywac podczas gdy sie tancczy).
Dywizje bez przypisanego slotu sa traktowane jako osobne.
</p>
{/* Slots */}
<div className="space-y-4 mb-4">
{scheduleSlots
.sort((a, b) => a.order - b.order)
.map((slot) => (
<div key={slot.order} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-medium text-gray-900">
Slot {slot.order}
</h3>
<button
onClick={() => handleRemoveSlot(slot.order)}
className="p-1 text-red-500 hover:bg-red-50 rounded"
title="Usun slot"
>
<Trash2 size={16} />
</button>
</div>
<div className="flex flex-wrap gap-2">
{divisions.map((division) => {
const isInSlot = slot.divisionIds.includes(division.id);
const assignedDivIds = getAssignedDivisionIds();
const isInOtherSlot = assignedDivIds.has(division.id) && !isInSlot;
return (
<button
key={division.id}
onClick={() => 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}
</button>
);
})}
</div>
{slot.divisionIds.length === 0 && (
<p className="text-sm text-gray-400 mt-2 italic">
Kliknij dywizje aby dodac do slotu
</p>
)}
</div>
))}
</div>
{/* Add slot button */}
<div className="flex gap-3">
<button
onClick={handleAddSlot}
className="flex items-center gap-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-primary-400 hover:text-primary-600"
>
<Plus size={16} />
Dodaj slot
</button>
<button
onClick={handleSaveSchedule}
disabled={savingSchedule}
className="flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{savingSchedule ? (
<RefreshCw size={16} className="animate-spin" />
) : (
<Save size={16} />
)}
Zapisz harmonogram
</button>
</div>
{/* Current config info */}
{event.scheduleConfig?.slots?.length > 0 && (
<div className="mt-4 bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-sm text-blue-800">
Zapisany harmonogram: {event.scheduleConfig.slots.length} slot(ow)
</p>
</div>
)}
</div> </div>
{/* Action Buttons */} {/* Action Buttons */}