Files
pdga-rating/public/js/players.js
T
Samuel Enocsson 20bbdbbfcf Extract inline CSS/JS, add EJS templates with shared layout
- Extract CSS into public/css/{shared,players,courses}.css
- Extract JS into public/js/{chart,tooltips,progress,players,courses}.js
- Consolidate 5 duplicated tooltip blocks into setupTooltip() helper
- Add EJS view engine with layout partial and nav partial
- Convert HTML pages to EJS templates (index.ejs, courses.ejs)
- Add /courses route with redirect from /courses.html
- Remove old monolithic HTML files (1478 + 612 lines)
2026-02-18 22:32:03 +01:00

596 lines
24 KiB
JavaScript

let cachedDebugInfo = {};
let pendingPlayerData = null;
function displayRatings(ratings) {
const tableDiv = document.getElementById('ratings-table');
if (ratings.length === 0) {
tableDiv.innerHTML = '<p>No ratings found.</p>';
return;
}
let tableHTML = `
<table>
<thead>
<tr>
<th class="mobile-hide">Rank</th>
<th>Player Name</th>
<th class="mobile-hide">PDGA #</th>
<th>Rating</th>
<th class="mobile-hide">Change</th>
<th class="mobile-hide">Predicted</th>
</tr>
</thead>
<tbody>
`;
ratings.forEach((player, index) => {
const difference = player.predictedRating && player.rating ?
player.predictedRating - player.rating : 0;
const diffText = difference > 0 ? `+${difference}` : difference.toString();
const diffClass = difference > 0 ? 'positive' : difference < 0 ? 'negative' : 'neutral';
const ratingChangeText = player.ratingChange ?
(player.ratingChange > 0 ? `+${player.ratingChange}` : player.ratingChange.toString()) : 'N/A';
const ratingChangeClass = player.ratingChange > 0 ? 'positive' :
player.ratingChange < 0 ? 'negative' : 'neutral';
tableHTML += `
<tr id="row-${player.pdgaNumber}" class="expandable-row" onclick="togglePlayerHistory(${player.pdgaNumber})">
<td class="mobile-hide">${index + 1}</td>
<td class="player-name">
<a href="https://www.pdga.com/player/${player.pdgaNumber}" target="_blank" onclick="event.stopPropagation()">${player.name}</a>
<div class="mobile-only pdga-number" style="font-size: 11px; color: #999; margin-top: 2px;">PDGA #${player.pdgaNumber}</div>
</td>
<td class="pdga-number mobile-hide">#${player.pdgaNumber}</td>
<td class="rating">
<div class="refresh-section">
<span class="rating-value" data-rating="${player.rating || ''}" data-stddev="${player.stdDev || ''}" data-pdga="${player.pdgaNumber}" style="cursor: help;">${player.rating || '<span style="color: #999; font-style: italic;">Click refresh</span>'}</span>
<i class="fas fa-sync-alt refresh-icon" onclick="refreshPlayer(${player.pdgaNumber})" title="Refresh player data"></i>
</div>
<div class="mobile-only rating-change ${ratingChangeClass}" style="font-size: 11px; margin-top: 2px;">${ratingChangeText}</div>
<div class="std-dev-tooltip" id="tooltip-rating-${player.pdgaNumber}"></div>
</td>
<td class="rating-change ${ratingChangeClass} mobile-hide">${ratingChangeText}</td>
<td class="predicted-rating mobile-hide" id="predicted-${player.pdgaNumber}">
<div class="refresh-section">
<span class="predicted-value" data-stddev="${player.stdDev || ''}" data-pdga="${player.pdgaNumber}" style="cursor: help;">${player.predictedRating || 'N/A'}</span>
<i class="fas fa-question-circle debug-icon" onclick="showDebugInfo(${player.pdgaNumber})" title="Show calculation details" style="margin-left: 5px; color: #6c757d; cursor: pointer; opacity: 0.6;"></i>
<i class="fas fa-sync-alt refresh-icon" onclick="refreshRoundHistory(${player.pdgaNumber})" title="Refresh prediction data"></i>
</div>
<div class="std-dev-tooltip" id="tooltip-stddev-${player.pdgaNumber}"></div>
</td>
</tr>
<tr id="history-${player.pdgaNumber}" class="expanded-content">
<td colspan="6" class="expanded-cell">
<div class="chart-title">
<div class="refresh-section">
Rating History for ${player.name}
<i class="fas fa-sync-alt refresh-icon" onclick="refreshRatingHistory(${player.pdgaNumber})" title="Refresh rating history"></i>
</div>
</div>
<div class="chart-container" id="chart-${player.pdgaNumber}" style="position: relative;">
<div class="loading-chart">Click to load rating history...</div>
</div>
<div class="chart-tooltip" id="tooltip-${player.pdgaNumber}"></div>
</td>
</tr>
`;
});
tableHTML += `
</tbody>
</table>
`;
tableDiv.innerHTML = tableHTML;
document.querySelectorAll('.predicted-value').forEach(span => {
const pdgaNumber = span.dataset.pdga;
const stdDev = span.dataset.stddev;
const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
if (stdDev && tooltip) {
setupTooltip(span, tooltip, () => `Standard Deviation: \u00b1${stdDev}`);
}
});
document.querySelectorAll('.rating-value').forEach(span => {
const pdgaNumber = span.dataset.pdga;
const rating = parseInt(span.dataset.rating);
const stdDev = parseInt(span.dataset.stddev);
const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && tooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
setupTooltip(span, tooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
});
}
async function togglePlayerHistory(pdgaNumber) {
const historyRow = document.getElementById(`history-${pdgaNumber}`);
const chartContainer = document.getElementById(`chart-${pdgaNumber}`);
if (historyRow.style.display === 'table-row') {
historyRow.style.display = 'none';
return;
}
historyRow.style.display = 'table-row';
if (chartContainer.dataset.loaded === 'true') {
return;
}
chartContainer.innerHTML = '<div class="loading-chart">Loading rating history...</div>';
try {
const response = await fetch(`/api/rating-history/${pdgaNumber}`);
const data = await response.json();
if (data.history && data.history.length > 0) {
createRatingChart(chartContainer, data.history);
chartContainer.dataset.loaded = 'true';
} else {
chartContainer.innerHTML = '<div class="loading-chart">No rating history available</div>';
}
} catch (error) {
console.error('Error loading rating history:', error);
chartContainer.innerHTML = '<div class="loading-chart">Error loading rating history</div>';
}
}
async function clearCache() {
try {
const response = await fetch('/api/clear-cache', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
alert(data.message);
if (confirm('Reload page to fetch fresh data?')) {
location.reload();
}
} else {
alert('Failed to clear cache');
}
} catch (error) {
console.error('Error clearing cache:', error);
alert('Error clearing cache');
}
}
async function refreshPlayer(pdgaNumber) {
const icon = document.querySelector(`#row-${pdgaNumber} .rating .refresh-icon`);
icon.classList.add('spinning');
try {
const response = await fetch(`/api/refresh-player/${pdgaNumber}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
const row = document.getElementById(`row-${pdgaNumber}`);
const ratingCell = row.querySelector('.rating');
const ratingChangeCell = row.querySelector('.rating-change');
const nameLink = row.querySelector('.player-name a');
nameLink.textContent = data.player.name;
const ratingChangeText = data.player.ratingChange ?
(data.player.ratingChange > 0 ? `+${data.player.ratingChange}` : data.player.ratingChange.toString()) : 'N/A';
const ratingChangeClass = data.player.ratingChange > 0 ? 'positive' :
data.player.ratingChange < 0 ? 'negative' : 'neutral';
const ratingValue = ratingCell.querySelector('.rating-value');
if (ratingValue) {
ratingValue.textContent = data.player.rating || 'N/A';
ratingValue.dataset.rating = data.player.rating || '';
const stdDev = parseInt(ratingValue.dataset.stddev);
const rating = parseInt(data.player.rating);
const tooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && tooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
replaceWithTooltip(ratingValue, tooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
}
if (ratingChangeCell) ratingChangeCell.textContent = ratingChangeText;
if (ratingChangeCell) ratingChangeCell.className = `rating-change ${ratingChangeClass} mobile-hide`;
const mobileChange = ratingCell.querySelector('.mobile-only.rating-change');
if (mobileChange) {
mobileChange.textContent = ratingChangeText;
mobileChange.className = `mobile-only rating-change ${ratingChangeClass}`;
}
}
} catch (error) {
console.error('Error refreshing player:', error);
alert('Failed to refresh player data');
} finally {
icon.classList.remove('spinning');
}
}
async function refreshRoundHistory(pdgaNumber) {
const icon = document.querySelector(`#predicted-${pdgaNumber} .refresh-icon`);
icon.classList.add('spinning');
try {
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, {
method: 'POST'
});
const data = await response.json();
if (!response.ok) {
throw new Error(JSON.stringify(data));
}
if (data.success) {
if (data.debugLog) {
cachedDebugInfo[pdgaNumber] = data.debugLog;
}
const predictedCell = document.getElementById(`predicted-${pdgaNumber}`);
if (predictedCell) {
const predictedValue = predictedCell.querySelector('.predicted-value');
if (predictedValue) {
predictedValue.textContent = data.predictedRating || 'N/A';
predictedValue.dataset.stddev = data.stdDev || '';
const tooltip = document.getElementById(`tooltip-stddev-${pdgaNumber}`);
if (data.stdDev && tooltip) {
replaceWithTooltip(predictedValue, tooltip, () => `Standard Deviation: \u00b1${data.stdDev}`);
}
}
}
const row = document.getElementById(`row-${pdgaNumber}`);
const ratingCell = row.querySelector('.rating');
const ratingValue = ratingCell.querySelector('.rating-value');
if (ratingValue && data.stdDev) {
ratingValue.dataset.stddev = data.stdDev;
const rating = parseInt(ratingValue.dataset.rating);
const stdDev = parseInt(data.stdDev);
const ratingTooltip = document.getElementById(`tooltip-rating-${pdgaNumber}`);
if (rating && stdDev && ratingTooltip) {
const minRating = rating - stdDev;
const maxRating = rating + stdDev;
replaceWithTooltip(ratingValue, ratingTooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${stdDev})`);
}
}
const diffCell = document.getElementById(`diff-${pdgaNumber}`);
if (diffCell) {
const currentRatingElement = document.querySelector(`#row-${pdgaNumber} .rating .refresh-section`);
if (currentRatingElement && currentRatingElement.firstChild) {
const currentRatingText = currentRatingElement.firstChild.textContent;
const currentRating = parseInt(currentRatingText);
if (data.predictedRating && currentRating && !isNaN(currentRating)) {
const difference = data.predictedRating - currentRating;
const diffText = difference > 0 ? `+${difference}` : difference.toString();
const diffClass = difference > 0 ? 'positive' : difference < 0 ? 'negative' : 'neutral';
diffCell.className = `difference ${diffClass}`;
diffCell.textContent = diffText;
} else {
diffCell.innerHTML = '<span style="color: #999; font-style: italic;">Use refresh</span>';
}
}
}
}
} catch (error) {
console.error('Error refreshing round history:', error);
let errorMessage = 'Failed to refresh prediction data';
let errorDetails = '';
try {
const errorData = JSON.parse(error.message);
errorMessage = errorData.error || errorMessage;
if (errorData.message) {
errorDetails = errorData.message;
} else {
errorDetails = errorData.details || '';
if (errorData.suggestion) {
errorDetails += '\n\nSuggestion: ' + errorData.suggestion;
}
if (errorData.errorType) {
errorDetails += '\n\nError Type: ' + errorData.errorType;
}
if (errorData.timestamp) {
errorDetails += '\n\nTime: ' + new Date(errorData.timestamp).toLocaleString();
}
}
} catch (parseError) {
errorDetails = error.message;
}
const fullMessage = errorDetails ? errorMessage + '\n\n' + errorDetails : errorMessage;
alert(fullMessage);
} finally {
icon.classList.remove('spinning');
}
}
async function refreshRatingHistory(pdgaNumber) {
const icon = document.querySelector(`#history-${pdgaNumber} .chart-title .refresh-icon`);
icon.classList.add('spinning');
try {
const response = await fetch(`/api/refresh-rating-history/${pdgaNumber}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
const chartContainer = document.getElementById(`chart-${pdgaNumber}`);
chartContainer.dataset.loaded = 'false';
if (data.history && data.history.length > 0) {
createRatingChart(chartContainer, data.history);
chartContainer.dataset.loaded = 'true';
} else {
chartContainer.innerHTML = '<div class="loading-chart">No rating history available</div>';
}
}
} catch (error) {
console.error('Error refreshing rating history:', error);
alert('Failed to refresh rating history');
} finally {
icon.classList.remove('spinning');
}
}
async function refreshAllPlayers() {
const icons = document.querySelectorAll('th .refresh-icon');
const ratingIcon = icons[0];
ratingIcon.classList.add('spinning');
try {
const ratings = await getAllPlayersFromDB();
displayRatings(ratings);
} catch (error) {
console.error('Error refreshing all players:', error);
alert('Failed to refresh player data');
} finally {
ratingIcon.classList.remove('spinning');
}
}
async function refreshAllPredictions() {
const icons = document.querySelectorAll('th .refresh-icon');
const predictedIcon = icons[1];
predictedIcon.classList.add('spinning');
try {
alert('Bulk prediction refresh not implemented yet. Use individual refresh icons.');
} catch (error) {
console.error('Error refreshing all predictions:', error);
alert('Failed to refresh predictions');
} finally {
predictedIcon.classList.remove('spinning');
}
}
async function showDebugInfo(pdgaNumber) {
const modal = document.getElementById('debug-modal');
const header = document.getElementById('debug-header');
const log = document.getElementById('debug-log');
const playerNameElement = document.querySelector(`#row-${pdgaNumber} .player-name a`);
const playerName = playerNameElement ? playerNameElement.textContent : `PDGA #${pdgaNumber}`;
header.textContent = `Prediction Calculation Details - ${playerName}`;
log.textContent = 'Loading calculation details...';
modal.style.display = 'flex';
try {
if (cachedDebugInfo[pdgaNumber]) {
log.textContent = cachedDebugInfo[pdgaNumber].join('\n');
return;
}
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, {
method: 'POST'
});
const data = await response.json();
if (data.success && data.debugLog) {
cachedDebugInfo[pdgaNumber] = data.debugLog;
log.textContent = data.debugLog.join('\n');
} else {
log.textContent = 'No debug information available. Try refreshing the prediction first.';
}
} catch (error) {
console.error('Error fetching debug info:', error);
log.textContent = 'Error loading debug information. Please try again.';
}
}
function closeDebugModal(event) {
const modal = document.getElementById('debug-modal');
modal.style.display = 'none';
}
async function searchAndAddPlayer() {
const input = document.getElementById('pdga-number-input');
const pdgaNumber = input.value.trim();
if (!pdgaNumber) {
alert('Please enter a PDGA number');
return;
}
const button = document.querySelector('.btn-add');
const originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<i class="fas fa-sync-alt spinning"></i> Searching...';
try {
const response = await fetch(`/api/search-player/${pdgaNumber}`);
const data = await response.json();
if (!response.ok) {
showErrorModal(data.error || 'Player not found');
return;
}
if (data.alreadyExists) {
showInfoModal(`${data.player.name} is already being tracked!`);
return;
}
pendingPlayerData = data.player;
showConfirmationModal(data.player);
} catch (error) {
console.error('Error searching for player:', error);
showErrorModal('Failed to search for player. Please try again.');
} finally {
button.disabled = false;
button.innerHTML = originalHTML;
}
}
function showConfirmationModal(player) {
const modal = document.getElementById('add-player-modal');
const header = document.getElementById('add-player-modal-header');
const body = document.getElementById('add-player-modal-body');
const footer = document.getElementById('add-player-modal-footer');
header.textContent = 'Confirm Player';
body.innerHTML = `
<p>Is this the correct player you want to add?</p>
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 15px;">
<strong style="font-size: 18px; color: #007bff;">${player.name}</strong><br>
<span style="color: #6c757d;">PDGA #${player.pdgaNumber}</span><br>
${player.rating ? `<span style="color: #28a745; font-weight: bold;">Current Rating: ${player.rating}</span>` : '<span style="color: #999;">No rating available</span>'}
</div>
`;
footer.innerHTML = `
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Cancel</button>
<button class="btn btn-confirm" onclick="confirmAddPlayer()">Add Player</button>
`;
modal.style.display = 'flex';
}
function showErrorModal(message) {
const modal = document.getElementById('add-player-modal');
const header = document.getElementById('add-player-modal-header');
const body = document.getElementById('add-player-modal-body');
const footer = document.getElementById('add-player-modal-footer');
header.textContent = 'Player Not Found';
body.innerHTML = `
<p style="color: #dc3545;">
<i class="fas fa-exclamation-circle"></i> ${message}
</p>
<p style="margin-top: 10px; color: #6c757d; font-size: 14px;">
Please check the PDGA number and try again.
</p>
`;
footer.innerHTML = `
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
`;
modal.style.display = 'flex';
}
function showInfoModal(message) {
const modal = document.getElementById('add-player-modal');
const header = document.getElementById('add-player-modal-header');
const body = document.getElementById('add-player-modal-body');
const footer = document.getElementById('add-player-modal-footer');
header.textContent = 'Information';
body.innerHTML = `
<p style="color: #007bff;">
<i class="fas fa-info-circle"></i> ${message}
</p>
`;
footer.innerHTML = `
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
`;
modal.style.display = 'flex';
}
async function confirmAddPlayer() {
if (!pendingPlayerData) {
closeAddPlayerModal();
return;
}
const modal = document.getElementById('add-player-modal');
const body = document.getElementById('add-player-modal-body');
const footer = document.getElementById('add-player-modal-footer');
body.innerHTML = '<p style="text-align: center;"><i class="fas fa-sync-alt spinning"></i> Adding player...</p>';
footer.innerHTML = '';
try {
const response = await fetch('/api/add-player', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ pdgaNumber: pendingPlayerData.pdgaNumber })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to add player');
}
body.innerHTML = `
<p style="color: #28a745; text-align: center;">
<i class="fas fa-check-circle" style="font-size: 48px; margin-bottom: 15px;"></i><br>
<strong>${data.player.name}</strong> has been added successfully!
</p>
`;
footer.innerHTML = `
<button class="btn btn-confirm" onclick="closeAddPlayerModal(); location.reload();">OK</button>
`;
document.getElementById('pdga-number-input').value = '';
pendingPlayerData = null;
} catch (error) {
console.error('Error adding player:', error);
body.innerHTML = `
<p style="color: #dc3545;">
<i class="fas fa-exclamation-circle"></i> ${error.message}
</p>
`;
footer.innerHTML = `
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
`;
}
}
function closeAddPlayerModal(event) {
const modal = document.getElementById('add-player-modal');
modal.style.display = 'none';
pendingPlayerData = null;
}