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)) { 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'); const dropdown = document.querySelector('.menu-bar .dropdown'); const dropdownContent = document.getElementById('show-menu-dropdown'); const showMenuBtn = document.getElementById('show-menu-btn'); let menuOpen = false; function openMenu() { dropdownContent.style.opacity = '1'; dropdownContent.style.visibility = 'visible'; dropdownContent.style.pointerEvents = 'auto'; showMenuBtn.setAttribute('aria-expanded', 'true'); menuOpen = true; } function closeMenu() { dropdownContent.style.opacity = ''; dropdownContent.style.visibility = ''; dropdownContent.style.pointerEvents = ''; showMenuBtn.setAttribute('aria-expanded', 'false'); menuOpen = false; } if (dropdown && dropdownContent && showMenuBtn) { dropdown.addEventListener('mouseenter', openMenu); dropdown.addEventListener('mouseleave', closeMenu); showMenuBtn.addEventListener('focus', openMenu); showMenuBtn.addEventListener('blur', closeMenu); } if (showAll) showAll.onclick = () => { matchViewMode = 'all'; renderTable(); closeMenu(); }; if (showFuture) showFuture.onclick = () => { matchViewMode = 'future'; renderTable(); closeMenu(); }; }); };