Add HTMX migration for server-rendered tables and lazy loading
- Add HTMX CDN to layout - Replace client-side table rendering (displayRatings, displayCourses) with server-rendered EJS partials via hx-get - Add server-side course search with debounced hx-trigger - Lazy-load player history and course layouts via htmx.ajax() - Render rating chart via htmx:afterSwap with data attributes - Add partial routes: ratings-table, course-table, player-history, course-layouts
This commit is contained in:
+10
-220
@@ -1,112 +1,3 @@
|
|||||||
let allCourses = [];
|
|
||||||
|
|
||||||
async function loadCourses() {
|
|
||||||
const loading = document.getElementById('loading');
|
|
||||||
const tableDiv = document.getElementById('courses-table');
|
|
||||||
|
|
||||||
loading.style.display = 'block';
|
|
||||||
tableDiv.innerHTML = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/courses');
|
|
||||||
allCourses = await response.json();
|
|
||||||
|
|
||||||
loading.style.display = 'none';
|
|
||||||
displayCourses(allCourses);
|
|
||||||
updateSearchInfo(allCourses.length, allCourses.length);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading courses:', error);
|
|
||||||
loading.style.display = 'none';
|
|
||||||
tableDiv.innerHTML = '<p>Error loading courses. Please try again.</p>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchCourses() {
|
|
||||||
const searchInput = document.getElementById('course-search');
|
|
||||||
const searchTerm = searchInput.value.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (!searchTerm) {
|
|
||||||
displayCourses(allCourses);
|
|
||||||
updateSearchInfo(allCourses.length, allCourses.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered = allCourses.filter(course => {
|
|
||||||
return course.name.toLowerCase().includes(searchTerm) ||
|
|
||||||
course.city.toLowerCase().includes(searchTerm);
|
|
||||||
});
|
|
||||||
|
|
||||||
displayCourses(filtered);
|
|
||||||
updateSearchInfo(filtered.length, allCourses.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSearchInfo(showing, total) {
|
|
||||||
const infoDiv = document.getElementById('search-results-info');
|
|
||||||
if (showing === total) {
|
|
||||||
infoDiv.textContent = `Showing all ${total} courses`;
|
|
||||||
} else {
|
|
||||||
infoDiv.textContent = `Showing ${showing} of ${total} courses`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayCourses(courses) {
|
|
||||||
const tableDiv = document.getElementById('courses-table');
|
|
||||||
|
|
||||||
if (courses.length === 0) {
|
|
||||||
tableDiv.innerHTML = '<p>No courses found. Click "Scrape Courses" to load Swedish courses from PDGA.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tableHTML = `
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Course Name</th>
|
|
||||||
<th class="mobile-hide">City</th>
|
|
||||||
<th class="mobile-hide">Last Updated</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
`;
|
|
||||||
|
|
||||||
courses.forEach(course => {
|
|
||||||
const lastUpdated = new Date(course.last_updated).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
|
|
||||||
tableHTML += `
|
|
||||||
<tr id="row-${course.id}" class="expandable-row" onclick="toggleCourseLayouts(${course.id})">
|
|
||||||
<td>
|
|
||||||
<a href="${course.link}" target="_blank" onclick="event.stopPropagation()">${course.name}</a>
|
|
||||||
<div class="mobile-only" style="font-size: 11px; color: #999; margin-top: 2px;">${course.city}</div>
|
|
||||||
</td>
|
|
||||||
<td class="mobile-hide">${course.city}</td>
|
|
||||||
<td class="mobile-hide">${lastUpdated}</td>
|
|
||||||
<td>
|
|
||||||
<i class="fas fa-sync-alt refresh-icon" onclick="scrapeLayouts(${course.id}, '${course.name.replace(/'/g, "\\'")}'); event.stopPropagation();" title="Scrape layouts for this course"></i>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr id="layouts-${course.id}" class="expanded-content">
|
|
||||||
<td colspan="4">
|
|
||||||
<div class="layouts-container" id="layouts-container-${course.id}">
|
|
||||||
<div class="no-layouts">Click to load layouts...</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
tableHTML += `
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`;
|
|
||||||
|
|
||||||
tableDiv.innerHTML = tableHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleAccordion(accordionId) {
|
function toggleAccordion(accordionId) {
|
||||||
const content = document.getElementById(accordionId);
|
const content = document.getElementById(accordionId);
|
||||||
const icon = document.getElementById(`${accordionId}-icon`);
|
const icon = document.getElementById(`${accordionId}-icon`);
|
||||||
@@ -120,7 +11,7 @@ function toggleAccordion(accordionId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleCourseLayouts(courseId) {
|
function toggleCourseLayouts(courseId) {
|
||||||
const layoutsRow = document.getElementById(`layouts-${courseId}`);
|
const layoutsRow = document.getElementById(`layouts-${courseId}`);
|
||||||
const layoutsContainer = document.getElementById(`layouts-container-${courseId}`);
|
const layoutsContainer = document.getElementById(`layouts-container-${courseId}`);
|
||||||
|
|
||||||
@@ -135,120 +26,22 @@ async function toggleCourseLayouts(courseId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutsContainer.innerHTML = '<div class="no-layouts">Loading layouts...</div>';
|
htmx.ajax('GET', `/partials/course-layouts/${courseId}`, {target: `#layouts-container-${courseId}`, swap: 'innerHTML'});
|
||||||
|
layoutsContainer.dataset.loaded = 'true';
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/layouts/${courseId}`);
|
|
||||||
const layouts = await response.json();
|
|
||||||
|
|
||||||
if (layouts.length > 0) {
|
|
||||||
const oneYearAgo = new Date();
|
|
||||||
oneYearAgo.setDate(oneYearAgo.getDate() - 365);
|
|
||||||
|
|
||||||
const activeLayouts = [];
|
|
||||||
const inactiveLayouts = [];
|
|
||||||
|
|
||||||
layouts.forEach(layout => {
|
|
||||||
if (layout.last_played) {
|
|
||||||
const lastPlayedDate = new Date(layout.last_played);
|
|
||||||
if (lastPlayedDate >= oneYearAgo) {
|
|
||||||
activeLayouts.push(layout);
|
|
||||||
} else {
|
|
||||||
inactiveLayouts.push(layout);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
inactiveLayouts.push(layout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let layoutsHTML = '<h4 style="margin-top: 0;">Layouts:</h4>';
|
|
||||||
|
|
||||||
if (activeLayouts.length > 0) {
|
|
||||||
activeLayouts.forEach(layout => {
|
|
||||||
const ratingDisplay = layout.mean_rating ?
|
|
||||||
`<span style="color: #28a745; font-weight: bold; margin-left: 10px;">Rating: ${layout.mean_rating}</span>` :
|
|
||||||
'';
|
|
||||||
const dateDisplay = layout.last_played ?
|
|
||||||
`<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">Last played: ${new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</span>` :
|
|
||||||
'';
|
|
||||||
layoutsHTML += `
|
|
||||||
<div class="layout-item">
|
|
||||||
<div>
|
|
||||||
<span class="layout-name">${layout.name}</span>
|
|
||||||
${dateDisplay}
|
|
||||||
</div>
|
|
||||||
<span class="layout-par">Par ${layout.par}${ratingDisplay}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inactiveLayouts.length > 0) {
|
|
||||||
const accordionId = `accordion-${courseId}`;
|
|
||||||
layoutsHTML += `
|
|
||||||
<div class="inactive-layouts-accordion">
|
|
||||||
<div class="accordion-header" onclick="toggleAccordion('${accordionId}')">
|
|
||||||
<span class="accordion-header-text">Inactive Layouts (${inactiveLayouts.length}) - Not played in last year</span>
|
|
||||||
<span class="accordion-icon" id="${accordionId}-icon">▼</span>
|
|
||||||
</div>
|
|
||||||
<div class="accordion-content" id="${accordionId}">
|
|
||||||
`;
|
|
||||||
|
|
||||||
inactiveLayouts.forEach(layout => {
|
|
||||||
const ratingDisplay = layout.mean_rating ?
|
|
||||||
`<span style="color: #28a745; font-weight: bold; margin-left: 10px;">Rating: ${layout.mean_rating}</span>` :
|
|
||||||
'';
|
|
||||||
const dateDisplay = layout.last_played ?
|
|
||||||
`<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">Last played: ${new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}</span>` :
|
|
||||||
`<span style="color: #dc3545; font-size: 12px; margin-left: 10px;">Never played</span>`;
|
|
||||||
layoutsHTML += `
|
|
||||||
<div class="layout-item inactive">
|
|
||||||
<div>
|
|
||||||
<span class="layout-name">${layout.name}</span>
|
|
||||||
${dateDisplay}
|
|
||||||
</div>
|
|
||||||
<span class="layout-par">Par ${layout.par}${ratingDisplay}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
layoutsHTML += `
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeLayouts.length === 0 && inactiveLayouts.length === 0) {
|
|
||||||
layoutsHTML = '<div class="no-layouts">No layouts found. Click the refresh icon to scrape layouts.</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutsContainer.innerHTML = layoutsHTML;
|
|
||||||
layoutsContainer.dataset.loaded = 'true';
|
|
||||||
} else {
|
|
||||||
layoutsContainer.innerHTML = '<div class="no-layouts">No layouts found. Click the refresh icon to scrape layouts.</div>';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading layouts:', error);
|
|
||||||
layoutsContainer.innerHTML = '<div class="no-layouts">Error loading layouts</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scrapeCourses() {
|
async function scrapeCourses() {
|
||||||
const btn = document.getElementById('scrape-courses-btn');
|
const btn = document.getElementById('scrape-courses-btn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.innerHTML = '<i class="fas fa-sync-alt spinning"></i> Scraping...';
|
btn.textContent = 'Scraping...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/scrape-courses', {
|
const response = await fetch('/api/scrape-courses', { method: 'POST' });
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert(data.message);
|
alert(data.message);
|
||||||
await loadCourses();
|
htmx.ajax('GET', '/partials/course-table', '#courses-table');
|
||||||
searchCourses();
|
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to scrape courses');
|
alert('Failed to scrape courses');
|
||||||
}
|
}
|
||||||
@@ -257,7 +50,7 @@ async function scrapeCourses() {
|
|||||||
alert('Error scraping courses');
|
alert('Error scraping courses');
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.innerHTML = '<i class="fas fa-sync-alt"></i> Scrape Courses';
|
btn.textContent = 'Scrape Courses';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,10 +59,7 @@ async function scrapeLayouts(courseId, courseName) {
|
|||||||
icon.classList.add('spinning');
|
icon.classList.add('spinning');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/scrape-layouts/${courseId}`, {
|
const response = await fetch(`/api/scrape-layouts/${courseId}`, { method: 'POST' });
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.status === 409) {
|
if (response.status === 409) {
|
||||||
@@ -280,8 +70,8 @@ async function scrapeLayouts(courseId, courseName) {
|
|||||||
|
|
||||||
const layoutsRow = document.getElementById(`layouts-${courseId}`);
|
const layoutsRow = document.getElementById(`layouts-${courseId}`);
|
||||||
if (layoutsRow.style.display === 'table-row') {
|
if (layoutsRow.style.display === 'table-row') {
|
||||||
toggleCourseLayouts(courseId);
|
htmx.ajax('GET', `/partials/course-layouts/${courseId}`, {target: `#layouts-container-${courseId}`, swap: 'innerHTML'});
|
||||||
setTimeout(() => toggleCourseLayouts(courseId), 100);
|
layoutsContainer.dataset.loaded = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(data.message);
|
alert(data.message);
|
||||||
|
|||||||
+172
-279
@@ -1,90 +1,28 @@
|
|||||||
let cachedDebugInfo = {};
|
let cachedDebugInfo = {};
|
||||||
let pendingPlayerData = null;
|
let pendingPlayerData = null;
|
||||||
|
|
||||||
function displayRatings(ratings) {
|
function setupTooltipsAfterSwap() {
|
||||||
const tableDiv = document.getElementById('ratings-table');
|
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||||
|
if (event.detail.target.id === 'ratings-table') {
|
||||||
if (ratings.length === 0) {
|
initRatingsTooltips();
|
||||||
tableDiv.innerHTML = '<p>No ratings found.</p>';
|
}
|
||||||
return;
|
// After player history partial loads, render the chart
|
||||||
}
|
const target = event.detail.target;
|
||||||
|
if (target.id && target.id.startsWith('history-content-')) {
|
||||||
let tableHTML = `
|
const container = target.querySelector('.chart-container');
|
||||||
<table>
|
if (container && container.dataset.history) {
|
||||||
<thead>
|
try {
|
||||||
<tr>
|
const history = JSON.parse(container.dataset.history);
|
||||||
<th class="mobile-hide">Rank</th>
|
createRatingChart(container, history);
|
||||||
<th>Player Name</th>
|
} catch (e) {
|
||||||
<th class="mobile-hide">PDGA #</th>
|
console.error('Error rendering chart:', e);
|
||||||
<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;
|
|
||||||
|
|
||||||
|
function initRatingsTooltips() {
|
||||||
document.querySelectorAll('.predicted-value').forEach(span => {
|
document.querySelectorAll('.predicted-value').forEach(span => {
|
||||||
const pdgaNumber = span.dataset.pdga;
|
const pdgaNumber = span.dataset.pdga;
|
||||||
const stdDev = span.dataset.stddev;
|
const stdDev = span.dataset.stddev;
|
||||||
@@ -109,47 +47,30 @@ function displayRatings(ratings) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function togglePlayerHistory(pdgaNumber) {
|
function togglePlayerHistory(pdgaNumber) {
|
||||||
const historyRow = document.getElementById(`history-${pdgaNumber}`);
|
const historyRow = document.getElementById(`history-${pdgaNumber}`);
|
||||||
const chartContainer = document.getElementById(`chart-${pdgaNumber}`);
|
const contentDiv = document.getElementById(`history-content-${pdgaNumber}`);
|
||||||
|
|
||||||
if (historyRow.style.display === 'table-row') {
|
if (historyRow.style.display === 'table-row') {
|
||||||
historyRow.style.display = 'none';
|
historyRow.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
historyRow.style.display = 'table-row';
|
historyRow.style.display = 'table-row';
|
||||||
|
|
||||||
if (chartContainer.dataset.loaded === 'true') {
|
if (contentDiv.dataset.loaded === 'true') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chartContainer.innerHTML = '<div class="loading-chart">Loading rating history...</div>';
|
htmx.ajax('GET', `/partials/player-history/${pdgaNumber}`, {target: `#history-content-${pdgaNumber}`, swap: 'innerHTML'});
|
||||||
|
contentDiv.dataset.loaded = 'true';
|
||||||
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() {
|
async function clearCache() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/clear-cache', {
|
const response = await fetch('/api/clear-cache', { method: 'POST' });
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
alert(data.message);
|
alert(data.message);
|
||||||
if (confirm('Reload page to fetch fresh data?')) {
|
if (confirm('Reload page to fetch fresh data?')) {
|
||||||
@@ -167,27 +88,24 @@ async function clearCache() {
|
|||||||
async function refreshPlayer(pdgaNumber) {
|
async function refreshPlayer(pdgaNumber) {
|
||||||
const icon = document.querySelector(`#row-${pdgaNumber} .rating .refresh-icon`);
|
const icon = document.querySelector(`#row-${pdgaNumber} .rating .refresh-icon`);
|
||||||
icon.classList.add('spinning');
|
icon.classList.add('spinning');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/refresh-player/${pdgaNumber}`, {
|
const response = await fetch(`/api/refresh-player/${pdgaNumber}`, { method: 'POST' });
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const row = document.getElementById(`row-${pdgaNumber}`);
|
const row = document.getElementById(`row-${pdgaNumber}`);
|
||||||
const ratingCell = row.querySelector('.rating');
|
const ratingCell = row.querySelector('.rating');
|
||||||
const ratingChangeCell = row.querySelector('.rating-change');
|
const ratingChangeCell = row.querySelector('.rating-change');
|
||||||
|
|
||||||
const nameLink = row.querySelector('.player-name a');
|
const nameLink = row.querySelector('.player-name a');
|
||||||
nameLink.textContent = data.player.name;
|
nameLink.textContent = data.player.name;
|
||||||
|
|
||||||
const ratingChangeText = data.player.ratingChange ?
|
const ratingChangeText = data.player.ratingChange ?
|
||||||
(data.player.ratingChange > 0 ? `+${data.player.ratingChange}` : data.player.ratingChange.toString()) : 'N/A';
|
(data.player.ratingChange > 0 ? `+${data.player.ratingChange}` : data.player.ratingChange.toString()) : 'N/A';
|
||||||
const ratingChangeClass = data.player.ratingChange > 0 ? 'positive' :
|
const ratingChangeClass = data.player.ratingChange > 0 ? 'positive' :
|
||||||
data.player.ratingChange < 0 ? 'negative' : 'neutral';
|
data.player.ratingChange < 0 ? 'negative' : 'neutral';
|
||||||
|
|
||||||
const ratingValue = ratingCell.querySelector('.rating-value');
|
const ratingValue = ratingCell.querySelector('.rating-value');
|
||||||
if (ratingValue) {
|
if (ratingValue) {
|
||||||
ratingValue.textContent = data.player.rating || 'N/A';
|
ratingValue.textContent = data.player.rating || 'N/A';
|
||||||
@@ -224,18 +142,15 @@ async function refreshPlayer(pdgaNumber) {
|
|||||||
async function refreshRoundHistory(pdgaNumber) {
|
async function refreshRoundHistory(pdgaNumber) {
|
||||||
const icon = document.querySelector(`#predicted-${pdgaNumber} .refresh-icon`);
|
const icon = document.querySelector(`#predicted-${pdgaNumber} .refresh-icon`);
|
||||||
icon.classList.add('spinning');
|
icon.classList.add('spinning');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, {
|
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' });
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(JSON.stringify(data));
|
throw new Error(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if (data.debugLog) {
|
if (data.debugLog) {
|
||||||
cachedDebugInfo[pdgaNumber] = data.debugLog;
|
cachedDebugInfo[pdgaNumber] = data.debugLog;
|
||||||
@@ -271,27 +186,6 @@ async function refreshRoundHistory(pdgaNumber) {
|
|||||||
replaceWithTooltip(ratingValue, ratingTooltip, () => `Rating Range: ${minRating} - ${maxRating} (\u00b1${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) {
|
} catch (error) {
|
||||||
console.error('Error refreshing round history:', error);
|
console.error('Error refreshing round history:', error);
|
||||||
@@ -331,24 +225,16 @@ async function refreshRoundHistory(pdgaNumber) {
|
|||||||
async function refreshRatingHistory(pdgaNumber) {
|
async function refreshRatingHistory(pdgaNumber) {
|
||||||
const icon = document.querySelector(`#history-${pdgaNumber} .chart-title .refresh-icon`);
|
const icon = document.querySelector(`#history-${pdgaNumber} .chart-title .refresh-icon`);
|
||||||
icon.classList.add('spinning');
|
icon.classList.add('spinning');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/refresh-rating-history/${pdgaNumber}`, {
|
const response = await fetch(`/api/refresh-rating-history/${pdgaNumber}`, { method: 'POST' });
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
const chartContainer = document.getElementById(`chart-${pdgaNumber}`);
|
const contentDiv = document.getElementById(`history-content-${pdgaNumber}`);
|
||||||
chartContainer.dataset.loaded = 'false';
|
contentDiv.dataset.loaded = 'false';
|
||||||
|
htmx.ajax('GET', `/partials/player-history/${pdgaNumber}`, {target: `#history-content-${pdgaNumber}`, swap: 'innerHTML'});
|
||||||
if (data.history && data.history.length > 0) {
|
contentDiv.dataset.loaded = 'true';
|
||||||
createRatingChart(chartContainer, data.history);
|
|
||||||
chartContainer.dataset.loaded = 'true';
|
|
||||||
} else {
|
|
||||||
chartContainer.innerHTML = '<div class="loading-chart">No rating history available</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error refreshing rating history:', error);
|
console.error('Error refreshing rating history:', error);
|
||||||
@@ -358,61 +244,27 @@ async function refreshRatingHistory(pdgaNumber) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
async function showDebugInfo(pdgaNumber) {
|
||||||
const modal = document.getElementById('debug-modal');
|
const modal = document.getElementById('debug-modal');
|
||||||
const header = document.getElementById('debug-header');
|
const header = document.getElementById('debug-header');
|
||||||
const log = document.getElementById('debug-log');
|
const log = document.getElementById('debug-log');
|
||||||
|
|
||||||
const playerNameElement = document.querySelector(`#row-${pdgaNumber} .player-name a`);
|
const playerNameElement = document.querySelector(`#row-${pdgaNumber} .player-name a`);
|
||||||
const playerName = playerNameElement ? playerNameElement.textContent : `PDGA #${pdgaNumber}`;
|
const playerName = playerNameElement ? playerNameElement.textContent : `PDGA #${pdgaNumber}`;
|
||||||
|
|
||||||
header.textContent = `Prediction Calculation Details - ${playerName}`;
|
header.textContent = `Prediction Calculation Details - ${playerName}`;
|
||||||
log.textContent = 'Loading calculation details...';
|
log.textContent = 'Loading calculation details...';
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (cachedDebugInfo[pdgaNumber]) {
|
if (cachedDebugInfo[pdgaNumber]) {
|
||||||
log.textContent = cachedDebugInfo[pdgaNumber].join('\n');
|
log.textContent = cachedDebugInfo[pdgaNumber].join('\n');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, {
|
const response = await fetch(`/api/refresh-round-history/${pdgaNumber}`, { method: 'POST' });
|
||||||
method: 'POST'
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success && data.debugLog) {
|
if (data.success && data.debugLog) {
|
||||||
cachedDebugInfo[pdgaNumber] = data.debugLog;
|
cachedDebugInfo[pdgaNumber] = data.debugLog;
|
||||||
log.textContent = data.debugLog.join('\n');
|
log.textContent = data.debugLog.join('\n');
|
||||||
@@ -426,8 +278,7 @@ async function showDebugInfo(pdgaNumber) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeDebugModal(event) {
|
function closeDebugModal(event) {
|
||||||
const modal = document.getElementById('debug-modal');
|
document.getElementById('debug-modal').style.display = 'none';
|
||||||
modal.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchAndAddPlayer() {
|
async function searchAndAddPlayer() {
|
||||||
@@ -440,9 +291,9 @@ async function searchAndAddPlayer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const button = document.querySelector('.btn-add');
|
const button = document.querySelector('.btn-add');
|
||||||
const originalHTML = button.innerHTML;
|
const originalText = button.textContent;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.innerHTML = '<i class="fas fa-sync-alt spinning"></i> Searching...';
|
button.textContent = 'Searching...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/search-player/${pdgaNumber}`);
|
const response = await fetch(`/api/search-player/${pdgaNumber}`);
|
||||||
@@ -466,70 +317,112 @@ async function searchAndAddPlayer() {
|
|||||||
showErrorModal('Failed to search for player. Please try again.');
|
showErrorModal('Failed to search for player. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
button.innerHTML = originalHTML;
|
button.textContent = originalText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConfirmationModal(player) {
|
function showConfirmationModal(player) {
|
||||||
const modal = document.getElementById('add-player-modal');
|
const modal = document.getElementById('add-player-modal');
|
||||||
const header = document.getElementById('add-player-modal-header');
|
document.getElementById('add-player-modal-header').textContent = 'Confirm Player';
|
||||||
const body = document.getElementById('add-player-modal-body');
|
|
||||||
const footer = document.getElementById('add-player-modal-footer');
|
|
||||||
|
|
||||||
header.textContent = 'Confirm Player';
|
const body = document.getElementById('add-player-modal-body');
|
||||||
body.innerHTML = `
|
body.textContent = '';
|
||||||
<p>Is this the correct player you want to add?</p>
|
|
||||||
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 15px;">
|
const question = document.createElement('p');
|
||||||
<strong style="font-size: 18px; color: #007bff;">${player.name}</strong><br>
|
question.textContent = 'Is this the correct player you want to add?';
|
||||||
<span style="color: #6c757d;">PDGA #${player.pdgaNumber}</span><br>
|
body.appendChild(question);
|
||||||
${player.rating ? `<span style="color: #28a745; font-weight: bold;">Current Rating: ${player.rating}</span>` : '<span style="color: #999;">No rating available</span>'}
|
|
||||||
</div>
|
const info = document.createElement('div');
|
||||||
`;
|
info.style.cssText = 'background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 15px;';
|
||||||
footer.innerHTML = `
|
|
||||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Cancel</button>
|
const name = document.createElement('strong');
|
||||||
<button class="btn btn-confirm" onclick="confirmAddPlayer()">Add Player</button>
|
name.style.cssText = 'font-size: 18px; color: #007bff;';
|
||||||
`;
|
name.textContent = player.name;
|
||||||
|
info.appendChild(name);
|
||||||
|
info.appendChild(document.createElement('br'));
|
||||||
|
|
||||||
|
const pdga = document.createElement('span');
|
||||||
|
pdga.style.color = '#6c757d';
|
||||||
|
pdga.textContent = `PDGA #${player.pdgaNumber}`;
|
||||||
|
info.appendChild(pdga);
|
||||||
|
info.appendChild(document.createElement('br'));
|
||||||
|
|
||||||
|
const rating = document.createElement('span');
|
||||||
|
if (player.rating) {
|
||||||
|
rating.style.cssText = 'color: #28a745; font-weight: bold;';
|
||||||
|
rating.textContent = `Current Rating: ${player.rating}`;
|
||||||
|
} else {
|
||||||
|
rating.style.color = '#999';
|
||||||
|
rating.textContent = 'No rating available';
|
||||||
|
}
|
||||||
|
info.appendChild(rating);
|
||||||
|
body.appendChild(info);
|
||||||
|
|
||||||
|
const footer = document.getElementById('add-player-modal-footer');
|
||||||
|
footer.textContent = '';
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.className = 'btn btn-cancel';
|
||||||
|
cancelBtn.textContent = 'Cancel';
|
||||||
|
cancelBtn.onclick = closeAddPlayerModal;
|
||||||
|
footer.appendChild(cancelBtn);
|
||||||
|
|
||||||
|
const confirmBtn = document.createElement('button');
|
||||||
|
confirmBtn.className = 'btn btn-confirm';
|
||||||
|
confirmBtn.textContent = 'Add Player';
|
||||||
|
confirmBtn.onclick = confirmAddPlayer;
|
||||||
|
footer.appendChild(confirmBtn);
|
||||||
|
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
function showErrorModal(message) {
|
function showErrorModal(message) {
|
||||||
const modal = document.getElementById('add-player-modal');
|
const modal = document.getElementById('add-player-modal');
|
||||||
const header = document.getElementById('add-player-modal-header');
|
document.getElementById('add-player-modal-header').textContent = 'Player Not Found';
|
||||||
const body = document.getElementById('add-player-modal-body');
|
|
||||||
const footer = document.getElementById('add-player-modal-footer');
|
|
||||||
|
|
||||||
header.textContent = 'Player Not Found';
|
const body = document.getElementById('add-player-modal-body');
|
||||||
body.innerHTML = `
|
body.textContent = '';
|
||||||
<p style="color: #dc3545;">
|
|
||||||
<i class="fas fa-exclamation-circle"></i> ${message}
|
const errorP = document.createElement('p');
|
||||||
</p>
|
errorP.style.color = '#dc3545';
|
||||||
<p style="margin-top: 10px; color: #6c757d; font-size: 14px;">
|
errorP.textContent = message;
|
||||||
Please check the PDGA number and try again.
|
body.appendChild(errorP);
|
||||||
</p>
|
|
||||||
`;
|
const helpP = document.createElement('p');
|
||||||
footer.innerHTML = `
|
helpP.style.cssText = 'margin-top: 10px; color: #6c757d; font-size: 14px;';
|
||||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
|
helpP.textContent = 'Please check the PDGA number and try again.';
|
||||||
`;
|
body.appendChild(helpP);
|
||||||
|
|
||||||
|
const footer = document.getElementById('add-player-modal-footer');
|
||||||
|
footer.textContent = '';
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'btn btn-cancel';
|
||||||
|
closeBtn.textContent = 'Close';
|
||||||
|
closeBtn.onclick = closeAddPlayerModal;
|
||||||
|
footer.appendChild(closeBtn);
|
||||||
|
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
function showInfoModal(message) {
|
function showInfoModal(message) {
|
||||||
const modal = document.getElementById('add-player-modal');
|
const modal = document.getElementById('add-player-modal');
|
||||||
const header = document.getElementById('add-player-modal-header');
|
document.getElementById('add-player-modal-header').textContent = 'Information';
|
||||||
const body = document.getElementById('add-player-modal-body');
|
|
||||||
const footer = document.getElementById('add-player-modal-footer');
|
|
||||||
|
|
||||||
header.textContent = 'Information';
|
const body = document.getElementById('add-player-modal-body');
|
||||||
body.innerHTML = `
|
body.textContent = '';
|
||||||
<p style="color: #007bff;">
|
|
||||||
<i class="fas fa-info-circle"></i> ${message}
|
const infoP = document.createElement('p');
|
||||||
</p>
|
infoP.style.color = '#007bff';
|
||||||
`;
|
infoP.textContent = message;
|
||||||
footer.innerHTML = `
|
body.appendChild(infoP);
|
||||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
|
|
||||||
`;
|
const footer = document.getElementById('add-player-modal-footer');
|
||||||
|
footer.textContent = '';
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.className = 'btn btn-cancel';
|
||||||
|
closeBtn.textContent = 'Close';
|
||||||
|
closeBtn.onclick = closeAddPlayerModal;
|
||||||
|
footer.appendChild(closeBtn);
|
||||||
|
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
@@ -540,19 +433,14 @@ async function confirmAddPlayer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = document.getElementById('add-player-modal');
|
|
||||||
const body = document.getElementById('add-player-modal-body');
|
const body = document.getElementById('add-player-modal-body');
|
||||||
const footer = document.getElementById('add-player-modal-footer');
|
body.textContent = 'Adding player...';
|
||||||
|
document.getElementById('add-player-modal-footer').textContent = '';
|
||||||
body.innerHTML = '<p style="text-align: center;"><i class="fas fa-sync-alt spinning"></i> Adding player...</p>';
|
|
||||||
footer.innerHTML = '';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/add-player', {
|
const response = await fetch('/api/add-player', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ pdgaNumber: pendingPlayerData.pdgaNumber })
|
body: JSON.stringify({ pdgaNumber: pendingPlayerData.pdgaNumber })
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -562,34 +450,39 @@ async function confirmAddPlayer() {
|
|||||||
throw new Error(data.error || 'Failed to add player');
|
throw new Error(data.error || 'Failed to add player');
|
||||||
}
|
}
|
||||||
|
|
||||||
body.innerHTML = `
|
body.textContent = '';
|
||||||
<p style="color: #28a745; text-align: center;">
|
const successP = document.createElement('p');
|
||||||
<i class="fas fa-check-circle" style="font-size: 48px; margin-bottom: 15px;"></i><br>
|
successP.style.cssText = 'color: #28a745; text-align: center;';
|
||||||
<strong>${data.player.name}</strong> has been added successfully!
|
successP.textContent = `${data.player.name} has been added successfully!`;
|
||||||
</p>
|
body.appendChild(successP);
|
||||||
`;
|
|
||||||
footer.innerHTML = `
|
const footer = document.getElementById('add-player-modal-footer');
|
||||||
<button class="btn btn-confirm" onclick="closeAddPlayerModal(); location.reload();">OK</button>
|
footer.textContent = '';
|
||||||
`;
|
const okBtn = document.createElement('button');
|
||||||
|
okBtn.className = 'btn btn-confirm';
|
||||||
|
okBtn.textContent = 'OK';
|
||||||
|
okBtn.onclick = function() { closeAddPlayerModal(); location.reload(); };
|
||||||
|
footer.appendChild(okBtn);
|
||||||
|
|
||||||
document.getElementById('pdga-number-input').value = '';
|
document.getElementById('pdga-number-input').value = '';
|
||||||
pendingPlayerData = null;
|
pendingPlayerData = null;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error adding player:', error);
|
console.error('Error adding player:', error);
|
||||||
body.innerHTML = `
|
body.textContent = error.message;
|
||||||
<p style="color: #dc3545;">
|
body.style.color = '#dc3545';
|
||||||
<i class="fas fa-exclamation-circle"></i> ${error.message}
|
|
||||||
</p>
|
const footer = document.getElementById('add-player-modal-footer');
|
||||||
`;
|
footer.textContent = '';
|
||||||
footer.innerHTML = `
|
const closeBtn = document.createElement('button');
|
||||||
<button class="btn btn-cancel" onclick="closeAddPlayerModal()">Close</button>
|
closeBtn.className = 'btn btn-cancel';
|
||||||
`;
|
closeBtn.textContent = 'Close';
|
||||||
|
closeBtn.onclick = closeAddPlayerModal;
|
||||||
|
footer.appendChild(closeBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAddPlayerModal(event) {
|
function closeAddPlayerModal(event) {
|
||||||
const modal = document.getElementById('add-player-modal');
|
document.getElementById('add-player-modal').style.display = 'none';
|
||||||
modal.style.display = 'none';
|
|
||||||
pendingPlayerData = null;
|
pendingPlayerData = null;
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-21
@@ -3,15 +3,15 @@ function fetchRatingsWithProgress() {
|
|||||||
const progressBar = document.getElementById('progress-bar');
|
const progressBar = document.getElementById('progress-bar');
|
||||||
const progressText = document.getElementById('progress-text');
|
const progressText = document.getElementById('progress-text');
|
||||||
const tableDiv = document.getElementById('ratings-table');
|
const tableDiv = document.getElementById('ratings-table');
|
||||||
|
|
||||||
progressSection.style.display = 'block';
|
progressSection.style.display = 'block';
|
||||||
tableDiv.innerHTML = '';
|
tableDiv.innerHTML = '';
|
||||||
|
|
||||||
const eventSource = new EventSource('/api/ratings/progress');
|
const eventSource = new EventSource('/api/ratings/progress');
|
||||||
|
|
||||||
eventSource.onmessage = function(event) {
|
eventSource.onmessage = function(event) {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
if (data.status === 'loading') {
|
if (data.status === 'loading') {
|
||||||
const percentage = Math.round((data.current / data.total) * 100);
|
const percentage = Math.round((data.current / data.total) * 100);
|
||||||
progressBar.style.width = `${percentage}%`;
|
progressBar.style.width = `${percentage}%`;
|
||||||
@@ -29,18 +29,14 @@ function fetchRatingsWithProgress() {
|
|||||||
progressText.textContent = `Error loading PDGA #${data.pdgaNumber} (${data.current}/${data.total})`;
|
progressText.textContent = `Error loading PDGA #${data.pdgaNumber} (${data.current}/${data.total})`;
|
||||||
} else if (data.status === 'complete') {
|
} else if (data.status === 'complete') {
|
||||||
progressSection.style.display = 'none';
|
progressSection.style.display = 'none';
|
||||||
displayRatings(data.ratings);
|
htmx.ajax('GET', '/partials/ratings-table', '#ratings-table');
|
||||||
eventSource.close();
|
|
||||||
} else if (data.status === 'error') {
|
|
||||||
progressSection.style.display = 'none';
|
|
||||||
tableDiv.innerHTML = '<p>Error loading ratings. Please try again.</p>';
|
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = function() {
|
eventSource.onerror = function() {
|
||||||
progressSection.style.display = 'none';
|
progressSection.style.display = 'none';
|
||||||
tableDiv.innerHTML = '<p>Connection error. Please refresh the page.</p>';
|
tableDiv.textContent = 'Connection error. Please refresh the page.';
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -50,26 +46,26 @@ function loadAllPlayers() {
|
|||||||
const originalText = button.textContent;
|
const originalText = button.textContent;
|
||||||
button.textContent = 'Loading...';
|
button.textContent = 'Loading...';
|
||||||
button.style.pointerEvents = 'none';
|
button.style.pointerEvents = 'none';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const progressSection = document.getElementById('progress-section');
|
const progressSection = document.getElementById('progress-section');
|
||||||
const progressBar = document.getElementById('progress-bar');
|
const progressBar = document.getElementById('progress-bar');
|
||||||
const progressText = document.getElementById('progress-text');
|
const progressText = document.getElementById('progress-text');
|
||||||
const tableDiv = document.getElementById('ratings-table');
|
const tableDiv = document.getElementById('ratings-table');
|
||||||
|
|
||||||
progressSection.style.display = 'block';
|
progressSection.style.display = 'block';
|
||||||
tableDiv.innerHTML = '';
|
tableDiv.textContent = '';
|
||||||
|
|
||||||
const eventSource = new EventSource('/api/load-all-players');
|
const eventSource = new EventSource('/api/load-all-players');
|
||||||
|
|
||||||
eventSource.onmessage = function(event) {
|
eventSource.onmessage = function(event) {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
if (data.status === 'loading' || data.status === 'completed' || data.status === 'error') {
|
if (data.status === 'loading' || data.status === 'completed' || data.status === 'error') {
|
||||||
const percentage = Math.round((data.current / data.total) * 100);
|
const percentage = Math.round((data.current / data.total) * 100);
|
||||||
progressBar.style.width = `${percentage}%`;
|
progressBar.style.width = `${percentage}%`;
|
||||||
progressBar.textContent = `${percentage}%`;
|
progressBar.textContent = `${percentage}%`;
|
||||||
|
|
||||||
if (data.status === 'loading') {
|
if (data.status === 'loading') {
|
||||||
progressText.textContent = `Loading player ${data.current}/${data.total}: PDGA #${data.pdgaNumber}`;
|
progressText.textContent = `Loading player ${data.current}/${data.total}: PDGA #${data.pdgaNumber}`;
|
||||||
} else if (data.status === 'completed') {
|
} else if (data.status === 'completed') {
|
||||||
@@ -79,16 +75,16 @@ function loadAllPlayers() {
|
|||||||
}
|
}
|
||||||
} else if (data.status === 'complete') {
|
} else if (data.status === 'complete') {
|
||||||
progressSection.style.display = 'none';
|
progressSection.style.display = 'none';
|
||||||
displayRatings(data.ratings);
|
htmx.ajax('GET', '/partials/ratings-table', '#ratings-table');
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
button.textContent = originalText;
|
button.textContent = originalText;
|
||||||
button.style.pointerEvents = 'auto';
|
button.style.pointerEvents = 'auto';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onerror = function() {
|
eventSource.onerror = function() {
|
||||||
progressSection.style.display = 'none';
|
progressSection.style.display = 'none';
|
||||||
tableDiv.innerHTML = '<p>Connection error. Please refresh the page.</p>';
|
tableDiv.textContent = 'Connection error. Please refresh the page.';
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
button.textContent = originalText;
|
button.textContent = originalText;
|
||||||
button.style.pointerEvents = 'auto';
|
button.style.pointerEvents = 'auto';
|
||||||
|
|||||||
@@ -8,6 +8,36 @@ const { layoutEventCache, scrapeCourseDirectory, scrapeCourseLayouts, scrapeEven
|
|||||||
// Request locking to prevent concurrent scrapes of the same resource
|
// Request locking to prevent concurrent scrapes of the same resource
|
||||||
const activeScrapes = new Map();
|
const activeScrapes = new Map();
|
||||||
|
|
||||||
|
router.get('/partials/course-table', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const allCourses = await getAllCoursesFromDB();
|
||||||
|
const query = req.query.q || '';
|
||||||
|
let courses = allCourses;
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const q = query.toLowerCase();
|
||||||
|
courses = allCourses.filter(c =>
|
||||||
|
c.name.toLowerCase().includes(q) || c.city.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('../partials/course-table', { courses, query, total: allCourses.length });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send('<p>Error loading courses. Please try again.</p>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/partials/course-layouts/:courseId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { courseId } = req.params;
|
||||||
|
const layouts = await getLayoutsForCourse(courseId);
|
||||||
|
res.render('../partials/course-layouts', { layouts, courseId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading course layouts:', error.message);
|
||||||
|
res.status(500).send('<div class="no-layouts">Error loading layouts</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/api/courses', async (req, res) => {
|
router.get('/api/courses', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const courses = await getAllCoursesFromDB();
|
const courses = await getAllCoursesFromDB();
|
||||||
|
|||||||
@@ -8,6 +8,43 @@ const { launchBrowser } = require('../scrapers/browser');
|
|||||||
const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB } = require('../services/player-service');
|
const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB } = require('../services/player-service');
|
||||||
const { calculatePredictedRating } = require('../services/rating-calculator');
|
const { calculatePredictedRating } = require('../services/rating-calculator');
|
||||||
|
|
||||||
|
router.get('/partials/ratings-table', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const ratings = await getAllRatingsFromDB();
|
||||||
|
res.render('../partials/ratings-table', { ratings });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send('<p>Error loading ratings. Please try again.</p>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/partials/player-history/:pdgaNumber', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { pdgaNumber } = req.params;
|
||||||
|
|
||||||
|
let history = await getRatingHistoryFromDB(pdgaNumber);
|
||||||
|
if (!history || history.length === 0) {
|
||||||
|
const html = await fetchRatingHistory(pdgaNumber);
|
||||||
|
history = parseRatingHistory(html);
|
||||||
|
try {
|
||||||
|
await saveRatingHistoryToDB(pdgaNumber, history);
|
||||||
|
} catch (dbErr) {
|
||||||
|
console.error('Failed to save rating history:', dbErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedHistory = (history || []).map(row => ({
|
||||||
|
date: row.date,
|
||||||
|
rating: row.rating,
|
||||||
|
displayDate: new Date(row.date).toLocaleDateString('en-US', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.render('../partials/player-history', { pdgaNumber, history: formattedHistory });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading player history:', error.message);
|
||||||
|
res.status(500).send('<div class="loading-chart">Error loading rating history</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/api/ratings', async (req, res) => {
|
router.get('/api/ratings', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const ratings = await getAllRatingsFromDB();
|
const ratings = await getAllRatingsFromDB();
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
id="course-search"
|
id="course-search"
|
||||||
|
name="q"
|
||||||
placeholder="Search courses by name or city..."
|
placeholder="Search courses by name or city..."
|
||||||
oninput="searchCourses()"
|
hx-get="/partials/course-table"
|
||||||
|
hx-trigger="input changed delay:300ms, search"
|
||||||
|
hx-target="#courses-table"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-results-info" class="search-results-info"></div>
|
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn" onclick="scrapeCourses()" id="scrape-courses-btn">
|
<button class="btn" onclick="scrapeCourses()" id="scrape-courses-btn">
|
||||||
@@ -17,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="loading" class="loading" style="display: none;">Loading courses...</div>
|
<div id="loading" class="loading" style="display: none;">Loading courses...</div>
|
||||||
<div id="courses-table"></div>
|
<div id="courses-table" hx-get="/partials/course-table" hx-trigger="load"></div>
|
||||||
`; %>
|
`; %>
|
||||||
|
|
||||||
<%- include('../partials/layout', {
|
<%- include('../partials/layout', {
|
||||||
@@ -26,6 +28,5 @@
|
|||||||
activePage: 'courses',
|
activePage: 'courses',
|
||||||
cssFiles: ['courses.css'],
|
cssFiles: ['courses.css'],
|
||||||
jsFiles: ['courses.js'],
|
jsFiles: ['courses.js'],
|
||||||
initScript: 'loadCourses();',
|
|
||||||
body: body
|
body: body
|
||||||
}) %>
|
}) %>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="progress-text" class="progress-text">Preparing to load ratings...</div>
|
<div id="progress-text" class="progress-text">Preparing to load ratings...</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ratings-table"></div>
|
<div id="ratings-table" hx-get="/partials/ratings-table" hx-trigger="load"></div>
|
||||||
`; %>
|
`; %>
|
||||||
|
|
||||||
<% var modals = `
|
<% var modals = `
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
activePage: 'players',
|
activePage: 'players',
|
||||||
cssFiles: ['players.css'],
|
cssFiles: ['players.css'],
|
||||||
jsFiles: ['tooltips.js', 'chart.js', 'progress.js', 'players.js'],
|
jsFiles: ['tooltips.js', 'chart.js', 'progress.js', 'players.js'],
|
||||||
initScript: 'fetchRatingsWithProgress();',
|
initScript: 'setupTooltipsAfterSwap();',
|
||||||
body: body,
|
body: body,
|
||||||
modals: modals
|
modals: modals
|
||||||
}) %>
|
}) %>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<% if (layouts.length === 0) { %>
|
||||||
|
<div class="no-layouts">No layouts found. Click the refresh icon to scrape layouts.</div>
|
||||||
|
<% } else {
|
||||||
|
var oneYearAgo = new Date();
|
||||||
|
oneYearAgo.setDate(oneYearAgo.getDate() - 365);
|
||||||
|
|
||||||
|
var activeLayouts = [];
|
||||||
|
var inactiveLayouts = [];
|
||||||
|
|
||||||
|
layouts.forEach(function(layout) {
|
||||||
|
if (layout.last_played) {
|
||||||
|
var lastPlayedDate = new Date(layout.last_played);
|
||||||
|
if (lastPlayedDate >= oneYearAgo) {
|
||||||
|
activeLayouts.push(layout);
|
||||||
|
} else {
|
||||||
|
inactiveLayouts.push(layout);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
inactiveLayouts.push(layout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
%>
|
||||||
|
<h4 style="margin-top: 0;">Layouts:</h4>
|
||||||
|
|
||||||
|
<% if (activeLayouts.length > 0) { %>
|
||||||
|
<% activeLayouts.forEach(function(layout) {
|
||||||
|
var ratingDisplay = layout.mean_rating ?
|
||||||
|
'<span style="color: #28a745; font-weight: bold; margin-left: 10px;">Rating: ' + layout.mean_rating + '</span>' : '';
|
||||||
|
var dateDisplay = layout.last_played ?
|
||||||
|
'<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">Last played: ' + new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + '</span>' : '';
|
||||||
|
%>
|
||||||
|
<div class="layout-item">
|
||||||
|
<div>
|
||||||
|
<span class="layout-name"><%= layout.name %></span>
|
||||||
|
<%- dateDisplay %>
|
||||||
|
</div>
|
||||||
|
<span class="layout-par">Par <%= layout.par %><%- ratingDisplay %></span>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (inactiveLayouts.length > 0) { %>
|
||||||
|
<div class="inactive-layouts-accordion">
|
||||||
|
<div class="accordion-header" onclick="toggleAccordion('accordion-<%= courseId %>')">
|
||||||
|
<span class="accordion-header-text">Inactive Layouts (<%= inactiveLayouts.length %>) - Not played in last year</span>
|
||||||
|
<span class="accordion-icon" id="accordion-<%= courseId %>-icon">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="accordion-content" id="accordion-<%= courseId %>">
|
||||||
|
<% inactiveLayouts.forEach(function(layout) {
|
||||||
|
var ratingDisplay = layout.mean_rating ?
|
||||||
|
'<span style="color: #28a745; font-weight: bold; margin-left: 10px;">Rating: ' + layout.mean_rating + '</span>' : '';
|
||||||
|
var dateDisplay = layout.last_played ?
|
||||||
|
'<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">Last played: ' + new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + '</span>' :
|
||||||
|
'<span style="color: #dc3545; font-size: 12px; margin-left: 10px;">Never played</span>';
|
||||||
|
%>
|
||||||
|
<div class="layout-item inactive">
|
||||||
|
<div>
|
||||||
|
<span class="layout-name"><%= layout.name %></span>
|
||||||
|
<%- dateDisplay %>
|
||||||
|
</div>
|
||||||
|
<span class="layout-par">Par <%= layout.par %><%- ratingDisplay %></span>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (activeLayouts.length === 0 && inactiveLayouts.length === 0) { %>
|
||||||
|
<div class="no-layouts">No layouts found. Click the refresh icon to scrape layouts.</div>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<div id="search-results-info" class="search-results-info">
|
||||||
|
<% if (typeof query !== 'undefined' && query) { %>
|
||||||
|
Showing <%= courses.length %> of <%= total %> courses
|
||||||
|
<% } else { %>
|
||||||
|
Showing all <%= courses.length %> courses
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if (courses.length === 0) { %>
|
||||||
|
<p>No courses found. Click "Scrape Courses" to load Swedish courses from PDGA.</p>
|
||||||
|
<% } else { %>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Course Name</th>
|
||||||
|
<th class="mobile-hide">City</th>
|
||||||
|
<th class="mobile-hide">Last Updated</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% courses.forEach(function(course) {
|
||||||
|
var lastUpdated = new Date(course.last_updated).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||||
|
%>
|
||||||
|
<tr id="row-<%= course.id %>" class="expandable-row" onclick="toggleCourseLayouts(<%= course.id %>)">
|
||||||
|
<td>
|
||||||
|
<a href="<%= course.link %>" target="_blank" onclick="event.stopPropagation()"><%= course.name %></a>
|
||||||
|
<div class="mobile-only" style="font-size: 11px; color: #999; margin-top: 2px;"><%= course.city %></div>
|
||||||
|
</td>
|
||||||
|
<td class="mobile-hide"><%= course.city %></td>
|
||||||
|
<td class="mobile-hide"><%= lastUpdated %></td>
|
||||||
|
<td>
|
||||||
|
<i class="fas fa-sync-alt refresh-icon" onclick="scrapeLayouts(<%= course.id %>, '<%= course.name.replace(/'/g, "\\'") %>'); event.stopPropagation();" title="Scrape layouts for this course"></i>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="layouts-<%= course.id %>" class="expanded-content">
|
||||||
|
<td colspan="4">
|
||||||
|
<div class="layouts-container" id="layouts-container-<%= course.id %>">
|
||||||
|
<div class="no-layouts">Click to load layouts...</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% }); %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><%= title %></title>
|
<title><%= title %></title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<link rel="stylesheet" href="/css/shared.css">
|
<link rel="stylesheet" href="/css/shared.css">
|
||||||
<% if (typeof cssFiles !== 'undefined') { %>
|
<% if (typeof cssFiles !== 'undefined') { %>
|
||||||
<% cssFiles.forEach(function(file) { %>
|
<% cssFiles.forEach(function(file) { %>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<% if (history && history.length > 0) { %>
|
||||||
|
<div class="chart-container" id="chart-<%= pdgaNumber %>"
|
||||||
|
data-history="<%= JSON.stringify(history) %>"
|
||||||
|
style="position: relative;">
|
||||||
|
<div class="loading-chart">Loading chart...</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-tooltip" id="tooltip-<%= pdgaNumber %>"></div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="chart-container">
|
||||||
|
<div class="loading-chart">No rating history available</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<% if (ratings.length === 0) { %>
|
||||||
|
<p>No ratings found.</p>
|
||||||
|
<% } else { %>
|
||||||
|
<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(function(player, index) {
|
||||||
|
var difference = player.predictedRating && player.rating ? player.predictedRating - player.rating : 0;
|
||||||
|
var diffText = difference > 0 ? '+' + difference : difference.toString();
|
||||||
|
var diffClass = difference > 0 ? 'positive' : difference < 0 ? 'negative' : 'neutral';
|
||||||
|
var ratingChangeText = player.ratingChange ? (player.ratingChange > 0 ? '+' + player.ratingChange : player.ratingChange.toString()) : 'N/A';
|
||||||
|
var ratingChangeClass = player.ratingChange > 0 ? 'positive' : player.ratingChange < 0 ? 'negative' : 'neutral';
|
||||||
|
%>
|
||||||
|
<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 id="history-content-<%= player.pdgaNumber %>">
|
||||||
|
<div class="loading-chart">Click to load rating history...</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% }); %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% } %>
|
||||||
Reference in New Issue
Block a user