Test
This commit is contained in:
240
tournament-frontend/src/MatchesSchedule.js
Normal file
240
tournament-frontend/src/MatchesSchedule.js
Normal file
@@ -0,0 +1,240 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import './MatchesSchedule.css';
|
||||
import TeamLogo from './TeamLogo';
|
||||
|
||||
const poolColors = [
|
||||
'#f8fafc', // light blue
|
||||
'#fef9c3', // light yellow
|
||||
'#fce7f3', // light pink
|
||||
'#d1fae5', // light green
|
||||
'#fee2e2', // light red
|
||||
'#e0e7ff', // light 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;
|
||||
Reference in New Issue
Block a user