Files
OpenTournament/tournament-frontend/src/MatchesSchedule.js
2025-07-20 01:08:51 +02:00

240 lines
9.7 KiB
JavaScript

import React, { useEffect, useState } from 'react';
import './MatchesSchedule.css';
import TeamLogo from './TeamLogo';
const poolColors = [
'#bae6fd', // darker blue
'#fde68a', // darker yellow
'#f9a8d4', // darker pink
'#6ee7b7', // darker green
'#fca5a5', // darker red
'#a5b4fc', // darker purple
];
function groupMatchesByPoolAndRound(matches) {
// Only round robin matches
const rrMatches = matches.filter(m => m.stage?.type === 'ROUND_ROBIN');
let poolMap = {};
rrMatches.forEach(m => {
const pool = m.pool || 1;
if (!poolMap[pool]) poolMap[pool] = [];
poolMap[pool].push(m);
});
// For each pool, group matches by round (scheduledAt)
const poolRounds = {};
Object.entries(poolMap).forEach(([pool, matches]) => {
const rounds = {};
matches.forEach(match => {
const key = match.scheduledAt;
if (!rounds[key]) rounds[key] = [];
rounds[key].push(match);
});
poolRounds[pool] = Object.values(rounds).sort((a, b) => new Date(a[0].scheduledAt) - new Date(b[0].scheduledAt));
});
return poolRounds;
}
const MatchesSchedule = () => {
const [matches, setMatches] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [editingMatchId, setEditingMatchId] = useState(null);
const [score1, setScore1] = useState('');
const [score2, setScore2] = useState('');
const [submitMsg, setSubmitMsg] = useState('');
const [mobileTab, setMobileTab] = useState(null);
// Calculate poolRounds and poolNumbers BEFORE any useEffect/useState that uses them
const poolRounds = groupMatchesByPoolAndRound(matches);
const poolNumbers = Object.keys(poolRounds).sort((a, b) => Number(a) - Number(b));
useEffect(() => {
setLoading(true);
fetch('/api/matches')
.then(res => res.ok ? res.json() : Promise.reject(res))
.then(data => {
setMatches(data);
setLoading(false);
})
.catch(() => {
setError('Failed to load matches');
setLoading(false);
});
}, []);
// Set default mobile tab
React.useEffect(() => {
if (mobileTab === null && poolNumbers.length > 0) setMobileTab(poolNumbers[0]);
}, [poolNumbers, mobileTab]);
// Responsive: tabs for mobile, columns for desktop
const isMobile = window.innerWidth < 700;
const handleEdit = (match) => {
setEditingMatchId(match.id);
setScore1('');
setScore2('');
setSubmitMsg('');
};
const handleSubmit = async (match) => {
setSubmitMsg('');
if (score1 === '' || score2 === '' || score1 === score2) {
setSubmitMsg('Scores must be different and not empty.');
return;
}
try {
const res = await fetch(`/api/admin/matches/${match.id}/result`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ team1Score: Number(score1), team2Score: Number(score2) })
});
const data = await res.json();
if (res.ok) {
setSubmitMsg('Result saved!');
setEditingMatchId(null);
setLoading(true);
fetch('/api/matches')
.then(res => res.ok ? res.json() : Promise.reject(res))
.then(data => {
setMatches(data);
setLoading(false);
});
} else {
setSubmitMsg(data.error || 'Failed to save result');
}
} catch {
setSubmitMsg('Failed to save result');
}
};
if (loading) return <div style={{ padding: 32 }}>Loading matches...</div>;
if (error) return <div style={{ padding: 32, color: 'red' }}>{error}</div>;
return (
<div className="matches-schedule-container">
<h2>Matches Schedule (by Pool)</h2>
{isMobile ? (
<>
<div style={{ display: 'flex', gap: 12, marginBottom: 24 }}>
{poolNumbers.map((pool, idx) => (
<button
key={pool}
onClick={() => setMobileTab(pool)}
style={{
padding: '8px 20px',
borderRadius: 8,
border: 'none',
background: mobileTab === pool ? poolColors[idx % poolColors.length] : '#e5e7eb',
color: '#222',
fontWeight: mobileTab === pool ? 'bold' : 'normal',
cursor: 'pointer',
boxShadow: mobileTab === pool ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
outline: mobileTab === pool ? '2px solid #2563eb' : 'none',
transition: 'background 0.2s, box-shadow 0.2s',
}}
>
{`Pool ${pool}`}
</button>
))}
</div>
{poolNumbers.map((pool, idx) => (
mobileTab === pool && (
<div key={pool} style={{
minWidth: 320,
background: poolColors[idx % poolColors.length],
borderRadius: 8,
padding: 16,
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
marginBottom: 24
}}>
<h3 style={{ textAlign: 'center' }}>{`Pool ${pool}`}</h3>
{poolRounds[pool].map((round, roundIdx) => (
<div key={roundIdx} className="matches-grid">
<div style={{ fontWeight: 'bold', margin: '8px 0', textAlign: 'center' }}>{`Round ${roundIdx + 1}`}</div>
{round.map(match => (
<div className="match-card" key={match.id}>
<div className="match-players">
<div style={{ textAlign: 'center' }}>
<TeamLogo team={match.team1} size="small" />
</div>
<span className="vs">vs</span>
<div style={{ textAlign: 'center' }}>
<TeamLogo team={match.team2} size="small" />
</div>
</div>
<div className="match-time">{new Date(match.scheduledAt).toLocaleString()}</div>
{match.result ? (
<div className="match-result">Result: {match.result.team1Score} - {match.result.team2Score}</div>
) : (
editingMatchId === match.id ? (
<div className="match-edit-form">
<input type="number" value={score1} onChange={e => setScore1(e.target.value)} placeholder="Team 1 Score" />
<input type="number" value={score2} onChange={e => setScore2(e.target.value)} placeholder="Team 2 Score" />
<button onClick={() => handleSubmit(match)}>Save</button>
<button onClick={() => setEditingMatchId(null)}>Cancel</button>
<div className="match-edit-msg">{submitMsg}</div>
</div>
) : (
<button className="match-edit-btn" onClick={() => handleEdit(match)}>Enter Result</button>
)
)}
</div>
))}
</div>
))}
</div>
)
))}
</>
) : (
<div style={{ display: 'flex', gap: 32, alignItems: 'flex-start', overflowX: 'auto' }}>
{poolNumbers.map((pool, idx) => (
<div className="matches-round" key={pool} style={{ minWidth: 320, background: poolColors[idx % poolColors.length], borderRadius: 8, padding: 16, boxShadow: '0 2px 8px rgba(0,0,0,0.04)', marginBottom: 24 }}>
<div className="matches-round-title">{`Pool ${pool}`}</div>
{poolRounds[pool].map((round, roundIdx) => (
<div key={roundIdx} className="matches-grid">
<div style={{ fontWeight: 'bold', margin: '8px 0', textAlign: 'center' }}>{`Round ${roundIdx + 1}`}</div>
{round.map(match => (
<div className="match-card" key={match.id}>
<div className="match-players">
<div style={{ textAlign: 'center' }}>
<TeamLogo team={match.team1} size="small" />
</div>
<span className="vs">vs</span>
<div style={{ textAlign: 'center' }}>
<TeamLogo team={match.team2} size="small" />
</div>
</div>
<div className="match-time">{new Date(match.scheduledAt).toLocaleString()}</div>
{match.result ? (
<div className="match-result">Result: {match.result.team1Score} - {match.result.team2Score}</div>
) : (
editingMatchId === match.id ? (
<div className="match-edit-form">
<input type="number" value={score1} onChange={e => setScore1(e.target.value)} placeholder="Team 1 Score" />
<input type="number" value={score2} onChange={e => setScore2(e.target.value)} placeholder="Team 2 Score" />
<button onClick={() => handleSubmit(match)}>Save</button>
<button onClick={() => setEditingMatchId(null)}>Cancel</button>
<div className="match-edit-msg">{submitMsg}</div>
</div>
) : (
<button className="match-edit-btn" onClick={() => handleEdit(match)}>Enter Result</button>
)
)}
</div>
))}
</div>
))}
</div>
))}
</div>
)}
</div>
);
};
export default MatchesSchedule;