- Season label always visible above table (e.g. "Season 2025/2026")
- Green highlighted banner when viewing next season ("⚡ Next Season — 2026/2027")
- Next Season button turns green and active when selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
238 lines
8.1 KiB
JavaScript
238 lines
8.1 KiB
JavaScript
let data = {};
|
|
let showAllMatches = false;
|
|
let viewingNextSeason = false;
|
|
let initialLoad = true;
|
|
|
|
function parseDate(str) {
|
|
const [d, m, y] = str.split('/').map(Number);
|
|
return new Date(2000 + y, m - 1, d);
|
|
}
|
|
|
|
function getSeasonYear(dateStr) {
|
|
const [d, m, y] = dateStr.split('/').map(Number);
|
|
return m >= 4 ? 2000 + y : 1999 + y;
|
|
}
|
|
|
|
function currentSeasonYear() {
|
|
const now = new Date();
|
|
const m = now.getMonth() + 1;
|
|
return m >= 4 ? now.getFullYear() : now.getFullYear() - 1;
|
|
}
|
|
|
|
function fetchData() {
|
|
fetch('/api/data').then(r => r.json()).then(d => {
|
|
data = d;
|
|
updateSeasonUI();
|
|
renderTable();
|
|
});
|
|
}
|
|
|
|
function updateSeasonUI() {
|
|
const thisSeason = currentSeasonYear();
|
|
const hasNextSeason = (data.dates || []).some(d => getSeasonYear(d) === thisSeason + 1);
|
|
|
|
const btn = document.getElementById('next-season-btn');
|
|
if (btn) {
|
|
btn.style.display = hasNextSeason ? '' : 'none';
|
|
btn.classList.toggle('active', viewingNextSeason);
|
|
btn.textContent = viewingNextSeason ? 'Current Season' : 'Next Season';
|
|
}
|
|
|
|
const banner = document.getElementById('season-banner');
|
|
if (banner) {
|
|
if (viewingNextSeason) {
|
|
banner.className = 'next';
|
|
banner.textContent = `\u26a1 Next Season \u2014 ${thisSeason + 1}/${thisSeason + 2}`;
|
|
} else {
|
|
banner.className = 'current';
|
|
banner.textContent = `Season ${thisSeason}/${thisSeason + 1}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
function saveData() {
|
|
fetch('/api/data', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
}
|
|
|
|
function renderTable() {
|
|
const container = document.getElementById('attendance-table');
|
|
const scrollTop = window.scrollY;
|
|
container.innerHTML = '';
|
|
const table = document.createElement('table');
|
|
|
|
// Header row
|
|
const thead = document.createElement('thead');
|
|
const headRow = document.createElement('tr');
|
|
const dateTh = document.createElement('th');
|
|
dateTh.innerText = 'Date';
|
|
dateTh.style.width = '120px';
|
|
dateTh.style.minWidth = '100px';
|
|
dateTh.style.maxWidth = '140px';
|
|
dateTh.style.textAlign = 'left';
|
|
headRow.appendChild(dateTh);
|
|
data.players.forEach(name => {
|
|
const th = document.createElement('th');
|
|
th.innerText = name;
|
|
th.classList.add('name-col');
|
|
headRow.appendChild(th);
|
|
});
|
|
const guestNameTh = document.createElement('th');
|
|
guestNameTh.innerText = 'Guest Name';
|
|
guestNameTh.classList.add('name-col');
|
|
headRow.appendChild(guestNameTh);
|
|
thead.appendChild(headRow);
|
|
table.appendChild(thead);
|
|
|
|
// Body rows
|
|
const tbody = document.createElement('tbody');
|
|
if (!data.guestNames) data.guestNames = {};
|
|
|
|
const today = new Date();
|
|
const thisSeason = currentSeasonYear();
|
|
const targetSeason = viewingNextSeason ? thisSeason + 1 : thisSeason;
|
|
|
|
let filteredDates = (data.dates || []).filter(d => getSeasonYear(d) === targetSeason);
|
|
if (!viewingNextSeason && !showAllMatches) {
|
|
filteredDates = filteredDates.filter(date => parseDate(date) > today);
|
|
}
|
|
|
|
// Find closest date for auto-scroll
|
|
let closestIdx = 0;
|
|
let minDiff = Infinity;
|
|
filteredDates.forEach((date, rowIdx) => {
|
|
const diff = Math.abs(parseDate(date) - today);
|
|
if (diff < minDiff) { minDiff = diff; closestIdx = rowIdx; }
|
|
});
|
|
|
|
filteredDates.forEach((date, rowIdx) => {
|
|
const tr = document.createElement('tr');
|
|
if (rowIdx === closestIdx) tr.id = 'current-match-row';
|
|
|
|
// Date cell
|
|
const dateTd = document.createElement('td');
|
|
dateTd.style.width = '120px';
|
|
dateTd.style.minWidth = '100px';
|
|
dateTd.style.maxWidth = '140px';
|
|
dateTd.style.textAlign = 'left';
|
|
if (date === '29/05/25' || date === '25/12/25' || date === '01/01/26') {
|
|
const strong = document.createElement('strong');
|
|
strong.innerText = date;
|
|
dateTd.appendChild(strong);
|
|
dateTd.title = 'Public Holiday';
|
|
} else {
|
|
dateTd.innerText = date;
|
|
}
|
|
tr.appendChild(dateTd);
|
|
|
|
// Player attendance cells
|
|
data.players.forEach((player, colIdx) => {
|
|
const td = document.createElement('td');
|
|
const key = `${date}|${colIdx}`;
|
|
|
|
const setCell = val => {
|
|
if (val === true) {
|
|
td.innerText = 'Yes'; td.className = 'clickable name-col yes';
|
|
} else if (val === 'no') {
|
|
td.innerText = 'No'; td.className = 'clickable name-col no';
|
|
} else if (val === 'maybe') {
|
|
td.innerText = 'Maybe'; td.className = 'clickable name-col maybe';
|
|
} else {
|
|
td.innerText = ''; td.className = 'clickable name-col';
|
|
}
|
|
};
|
|
setCell(data.attendance[key]);
|
|
|
|
td.onclick = () => {
|
|
const cur = data.attendance[key];
|
|
if (!cur) data.attendance[key] = true;
|
|
else if (cur === true) data.attendance[key] = 'no';
|
|
else if (cur === 'no') data.attendance[key] = 'maybe';
|
|
else delete data.attendance[key];
|
|
setCell(data.attendance[key]);
|
|
saveData();
|
|
};
|
|
tr.appendChild(td);
|
|
});
|
|
|
|
// Guest Name column
|
|
const guestNameTd = document.createElement('td');
|
|
guestNameTd.classList.add('name-col');
|
|
const guestNameInput = document.createElement('input');
|
|
guestNameInput.type = 'text';
|
|
guestNameInput.value = data.guestNames[date] || '';
|
|
guestNameInput.placeholder = 'Enter guest name';
|
|
guestNameInput.maxLength = 50;
|
|
guestNameInput.onchange = e => {
|
|
let value = e.target.value;
|
|
if (value === '') {
|
|
delete data.guestNames[date];
|
|
saveData();
|
|
return;
|
|
}
|
|
if (/</.test(value) || />/.test(value) || /["'`\\]/.test(value)) {
|
|
alert('Guest name cannot contain special characters like <, >, ", \\, or backticks.');
|
|
guestNameInput.value = data.guestNames[date] || '';
|
|
return;
|
|
}
|
|
if (!/^([a-zA-Z0-9 .-]+)$/.test(value)) {
|
|
alert('Guest name can only contain letters, numbers, spaces, hyphens, and periods.');
|
|
guestNameInput.value = data.guestNames[date] || '';
|
|
return;
|
|
}
|
|
if (value.length > 50) {
|
|
value = value.slice(0, 50);
|
|
guestNameInput.value = value;
|
|
}
|
|
data.guestNames[date] = value;
|
|
saveData();
|
|
};
|
|
guestNameTd.appendChild(guestNameInput);
|
|
tr.appendChild(guestNameTd);
|
|
tbody.appendChild(tr);
|
|
});
|
|
|
|
table.appendChild(tbody);
|
|
container.appendChild(table);
|
|
|
|
setTimeout(() => {
|
|
if (initialLoad) {
|
|
const row = document.getElementById('current-match-row');
|
|
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
initialLoad = false;
|
|
} else {
|
|
window.scrollTo({ top: scrollTop });
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
fetchData();
|
|
|
|
const toggleBtn = document.getElementById('toggle-matches-btn');
|
|
if (toggleBtn) {
|
|
toggleBtn.textContent = 'Show All Matches';
|
|
toggleBtn.onclick = () => {
|
|
showAllMatches = !showAllMatches;
|
|
toggleBtn.textContent = showAllMatches ? 'Show Future Matches' : 'Show All Matches';
|
|
renderTable();
|
|
};
|
|
}
|
|
|
|
const nextSeasonBtn = document.getElementById('next-season-btn');
|
|
if (nextSeasonBtn) {
|
|
nextSeasonBtn.onclick = () => {
|
|
viewingNextSeason = !viewingNextSeason;
|
|
// Reset "show all" toggle when switching seasons
|
|
showAllMatches = false;
|
|
if (toggleBtn) toggleBtn.textContent = 'Show All Matches';
|
|
initialLoad = true;
|
|
updateSeasonUI();
|
|
renderTable();
|
|
};
|
|
}
|
|
});
|