2025-05-10 18:00:02 +02:00

203 lines
7.9 KiB
JavaScript

let data = {};
function fetchData() {
fetch('/api/data').then(r => r.json()).then(d => {
data = d;
renderTable();
});
}
function saveData() {
fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
let matchViewMode = 'fromLast'; // 'fromLast', 'all', 'future'
function renderTable() {
const container = document.getElementById('attendance-table');
container.innerHTML = '';
const table = document.createElement('table');
// Header row
const thead = document.createElement('thead');
const headRow = document.createElement('tr');
headRow.appendChild(document.createElement('th')).innerText = 'Date';
data.players.forEach((name, i) => {
const th = document.createElement('th');
th.innerText = name;
th.classList.add('name-col');
headRow.appendChild(th);
});
// Guest column
const guestTh = document.createElement('th');
guestTh.innerText = data.guest || 'Guest';
guestTh.classList.add('name-col');
headRow.appendChild(guestTh);
// Guest Name column (per date)
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 = {};
// Custom order: last played date (<= today) on top, next date (> today) second, others after
const today = new Date();
// Parse dates as DD/MM/YY
function parseDate(str) {
const [d, m, y] = str.split('/').map(Number);
// Assume 20xx for years < 100
return new Date(2000 + y, m - 1, d);
}
const pastDates = data.dates.filter(date => parseDate(date) <= today);
const futureDates = data.dates.filter(date => parseDate(date) > today);
// Find most recent past date (last match)
const lastMatch = pastDates.length > 0 ? pastDates.reduce((a, b) => parseDate(a) > parseDate(b) ? a : b) : null;
// Find next match (soonest future date)
const nextMatch = futureDates.length > 0 ? futureDates.reduce((a, b) => parseDate(a) < parseDate(b) ? a : b) : null;
// Filter dates based on view mode
let filteredDates = [];
if (matchViewMode === 'all') {
filteredDates = data.dates;
} else if (matchViewMode === 'future') {
filteredDates = data.dates.filter(date => parseDate(date) > today);
} else {
// Default: show from last played match and all after
const pastDates = data.dates.filter(date => parseDate(date) <= today);
const lastMatch = pastDates.length > 0 ? pastDates.reduce((a, b) => parseDate(a) > parseDate(b) ? a : b) : null;
let found = false;
filteredDates = data.dates.filter(date => {
if (date === lastMatch) found = true;
return found;
});
}
// Find closest date to today for 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.innerText = date;
tr.appendChild(dateTd);
// Player attendance
[...data.players, data.guest].forEach((player, colIdx) => {
const td = document.createElement('td');
td.className = 'clickable name-col';
const key = `${date}|${colIdx}`;
if (data.attendance[key] === true) {
td.innerText = 'Yes';
td.classList.add('yes');
td.classList.remove('no');
} else if (data.attendance[key] === 'no') {
td.innerText = 'No';
td.classList.add('no');
td.classList.remove('yes');
} else {
td.innerText = '';
td.classList.remove('yes', 'no');
}
td.onclick = () => {
if (!data.attendance[key]) {
data.attendance[key] = true;
} else if (data.attendance[key] === true) {
data.attendance[key] = 'no';
} else if (data.attendance[key] === 'no') {
delete data.attendance[key];
}
saveData();
renderTable();
};
tr.appendChild(td);
});
// Guest Name column (input per date)
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;
// Allow clearing the guest name (empty string is valid)
if (value === "") {
delete data.guestNames[date];
saveData();
return;
}
// Only allow plain text, disallow HTML/script tags, max 50 chars
if (/</.test(value) || />/.test(value) || /["'`\\]/.test(value)) {
alert("Guest name cannot contain code or special characters like <, >, \", \\, or backticks.");
guestNameInput.value = data.guestNames[date] || '';
return;
}
// Only allow a-z, A-Z, 0-9, spaces, hyphens, periods
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) {
alert('Guest name cannot be longer than 50 characters.');
guestNameInput.value = value.slice(0, 50);
value = value.slice(0, 50);
}
data.guestNames[date] = value;
saveData();
};
guestNameTd.appendChild(guestNameInput);
tr.appendChild(guestNameTd);
tbody.appendChild(tr);
});
table.appendChild(tbody);
container.appendChild(table);
// Scroll to the most current match row after rendering
setTimeout(() => {
const row = document.getElementById('current-match-row');
if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 0);
}
document.getElementById('add-date').onclick = function() {
const date = prompt('Enter date (DD/MM/YY):');
// Check format: DD/MM/YY
const dateRegex = /^\d{2}\/\d{2}\/\d{2}$/;
if (!dateRegex.test(date)) {
alert('Date must be in DD/MM/YY format.');
return;
}
if (date && !data.dates.includes(date)) {
data.dates.push(date);
saveData();
renderTable();
}
};
window.onload = () => {
fetchData();
// Menu bar event listeners
document.addEventListener('DOMContentLoaded', () => {
const showAll = document.getElementById('show-all-matches');
const showFuture = document.getElementById('show-future-matches');
if (showAll) showAll.onclick = () => { matchViewMode = 'all'; renderTable(); };
if (showFuture) showFuture.onclick = () => { matchViewMode = 'future'; renderTable(); };
// Clicking the menu resets to default
const showMenuBtn = document.getElementById('show-menu-btn');
if (showMenuBtn) showMenuBtn.onclick = () => { matchViewMode = 'fromLast'; renderTable(); };
});
};