diff --git a/public/js/courses.js b/public/js/courses.js
index bede122..ef557eb 100644
--- a/public/js/courses.js
+++ b/public/js/courses.js
@@ -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 = '
Error loading courses. Please try again.
';
- }
-}
-
-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 = 'No courses found. Click "Scrape Courses" to load Swedish courses from PDGA.
';
- return;
- }
-
- let tableHTML = `
-
-
-
- | Course Name |
- City |
- Last Updated |
- Actions |
-
-
-
- `;
-
- courses.forEach(course => {
- const lastUpdated = new Date(course.last_updated).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric'
- });
-
- tableHTML += `
-
- |
- ${course.name}
- ${course.city}
- |
- ${course.city} |
- ${lastUpdated} |
-
-
- |
-
-
-
-
- Click to load layouts...
-
- |
-
- `;
- });
-
- tableHTML += `
-
-
- `;
-
- tableDiv.innerHTML = tableHTML;
-}
-
function toggleAccordion(accordionId) {
const content = document.getElementById(accordionId);
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 layoutsContainer = document.getElementById(`layouts-container-${courseId}`);
@@ -135,120 +26,22 @@ async function toggleCourseLayouts(courseId) {
return;
}
- layoutsContainer.innerHTML = 'Loading layouts...
';
-
- 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 = 'Layouts:
';
-
- if (activeLayouts.length > 0) {
- activeLayouts.forEach(layout => {
- const ratingDisplay = layout.mean_rating ?
- `Rating: ${layout.mean_rating}` :
- '';
- const dateDisplay = layout.last_played ?
- `Last played: ${new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}` :
- '';
- layoutsHTML += `
-
-
- ${layout.name}
- ${dateDisplay}
-
-
Par ${layout.par}${ratingDisplay}
-
- `;
- });
- }
-
- if (inactiveLayouts.length > 0) {
- const accordionId = `accordion-${courseId}`;
- layoutsHTML += `
-
-
-
- `;
-
- inactiveLayouts.forEach(layout => {
- const ratingDisplay = layout.mean_rating ?
- `
Rating: ${layout.mean_rating}` :
- '';
- const dateDisplay = layout.last_played ?
- `
Last played: ${new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}` :
- `
Never played`;
- layoutsHTML += `
-
-
- ${layout.name}
- ${dateDisplay}
-
-
Par ${layout.par}${ratingDisplay}
-
- `;
- });
-
- layoutsHTML += `
-
-
- `;
- }
-
- if (activeLayouts.length === 0 && inactiveLayouts.length === 0) {
- layoutsHTML = 'No layouts found. Click the refresh icon to scrape layouts.
';
- }
-
- layoutsContainer.innerHTML = layoutsHTML;
- layoutsContainer.dataset.loaded = 'true';
- } else {
- layoutsContainer.innerHTML = 'No layouts found. Click the refresh icon to scrape layouts.
';
- }
- } catch (error) {
- console.error('Error loading layouts:', error);
- layoutsContainer.innerHTML = 'Error loading layouts
';
- }
+ htmx.ajax('GET', `/partials/course-layouts/${courseId}`, {target: `#layouts-container-${courseId}`, swap: 'innerHTML'});
+ layoutsContainer.dataset.loaded = 'true';
}
async function scrapeCourses() {
const btn = document.getElementById('scrape-courses-btn');
btn.disabled = true;
- btn.innerHTML = ' Scraping...';
+ btn.textContent = 'Scraping...';
try {
- const response = await fetch('/api/scrape-courses', {
- method: 'POST'
- });
-
+ const response = await fetch('/api/scrape-courses', { method: 'POST' });
const data = await response.json();
if (data.success) {
alert(data.message);
- await loadCourses();
- searchCourses();
+ htmx.ajax('GET', '/partials/course-table', '#courses-table');
} else {
alert('Failed to scrape courses');
}
@@ -257,7 +50,7 @@ async function scrapeCourses() {
alert('Error scraping courses');
} finally {
btn.disabled = false;
- btn.innerHTML = ' Scrape Courses';
+ btn.textContent = 'Scrape Courses';
}
}
@@ -266,10 +59,7 @@ async function scrapeLayouts(courseId, courseName) {
icon.classList.add('spinning');
try {
- const response = await fetch(`/api/scrape-layouts/${courseId}`, {
- method: 'POST'
- });
-
+ const response = await fetch(`/api/scrape-layouts/${courseId}`, { method: 'POST' });
const data = await response.json();
if (response.status === 409) {
@@ -280,8 +70,8 @@ async function scrapeLayouts(courseId, courseName) {
const layoutsRow = document.getElementById(`layouts-${courseId}`);
if (layoutsRow.style.display === 'table-row') {
- toggleCourseLayouts(courseId);
- setTimeout(() => toggleCourseLayouts(courseId), 100);
+ htmx.ajax('GET', `/partials/course-layouts/${courseId}`, {target: `#layouts-container-${courseId}`, swap: 'innerHTML'});
+ layoutsContainer.dataset.loaded = 'true';
}
alert(data.message);
diff --git a/public/js/players.js b/public/js/players.js
index 96c265e..d0c80e6 100644
--- a/public/js/players.js
+++ b/public/js/players.js
@@ -1,90 +1,28 @@
let cachedDebugInfo = {};
let pendingPlayerData = null;
-function displayRatings(ratings) {
- const tableDiv = document.getElementById('ratings-table');
-
- if (ratings.length === 0) {
- tableDiv.innerHTML = 'No ratings found.
';
- return;
- }
-
- let tableHTML = `
-
-
-
- | Rank |
- Player Name |
- PDGA # |
- Rating |
- Change |
- Predicted |
-
-
-
- `;
-
- 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 += `
-
- | ${index + 1} |
-
- ${player.name}
- PDGA #${player.pdgaNumber}
- |
- #${player.pdgaNumber} |
-
-
- ${player.rating || 'Click refresh'}
-
-
- ${ratingChangeText}
-
- |
- ${ratingChangeText} |
-
-
- ${player.predictedRating || 'N/A'}
-
-
-
-
- |
-
-
-
-
-
- Rating History for ${player.name}
-
-
-
-
- Click to load rating history...
-
-
- |
-
- `;
+function setupTooltipsAfterSwap() {
+ document.body.addEventListener('htmx:afterSwap', function(event) {
+ if (event.detail.target.id === 'ratings-table') {
+ initRatingsTooltips();
+ }
+ // After player history partial loads, render the chart
+ const target = event.detail.target;
+ if (target.id && target.id.startsWith('history-content-')) {
+ const container = target.querySelector('.chart-container');
+ if (container && container.dataset.history) {
+ try {
+ const history = JSON.parse(container.dataset.history);
+ createRatingChart(container, history);
+ } catch (e) {
+ console.error('Error rendering chart:', e);
+ }
+ }
+ }
});
-
- tableHTML += `
-
-
- `;
-
- tableDiv.innerHTML = tableHTML;
+}
+function initRatingsTooltips() {
document.querySelectorAll('.predicted-value').forEach(span => {
const pdgaNumber = span.dataset.pdga;
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 chartContainer = document.getElementById(`chart-${pdgaNumber}`);
-
+ const contentDiv = document.getElementById(`history-content-${pdgaNumber}`);
+
if (historyRow.style.display === 'table-row') {
historyRow.style.display = 'none';
return;
}
-
+
historyRow.style.display = 'table-row';
-
- if (chartContainer.dataset.loaded === 'true') {
+
+ if (contentDiv.dataset.loaded === 'true') {
return;
}
-
- chartContainer.innerHTML = 'Loading rating history...
';
-
- 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 = 'No rating history available
';
- }
- } catch (error) {
- console.error('Error loading rating history:', error);
- chartContainer.innerHTML = 'Error loading rating history
';
- }
+
+ htmx.ajax('GET', `/partials/player-history/${pdgaNumber}`, {target: `#history-content-${pdgaNumber}`, swap: 'innerHTML'});
+ contentDiv.dataset.loaded = 'true';
}
async function clearCache() {
try {
- const response = await fetch('/api/clear-cache', {
- method: 'POST'
- });
-
+ 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?')) {
@@ -167,27 +88,24 @@ async function clearCache() {
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 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 ?
+
+ 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' :
+ 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';
@@ -224,18 +142,15 @@ async function refreshPlayer(pdgaNumber) {
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 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;
@@ -271,27 +186,6 @@ async function refreshRoundHistory(pdgaNumber) {
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 = 'Use refresh';
- }
- }
- }
-
}
} catch (error) {
console.error('Error refreshing round history:', error);
@@ -331,24 +225,16 @@ async function refreshRoundHistory(pdgaNumber) {
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 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 = 'No rating history available
';
- }
+ const contentDiv = document.getElementById(`history-content-${pdgaNumber}`);
+ contentDiv.dataset.loaded = 'false';
+ htmx.ajax('GET', `/partials/player-history/${pdgaNumber}`, {target: `#history-content-${pdgaNumber}`, swap: 'innerHTML'});
+ contentDiv.dataset.loaded = 'true';
}
} catch (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) {
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 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');
@@ -426,8 +278,7 @@ async function showDebugInfo(pdgaNumber) {
}
function closeDebugModal(event) {
- const modal = document.getElementById('debug-modal');
- modal.style.display = 'none';
+ document.getElementById('debug-modal').style.display = 'none';
}
async function searchAndAddPlayer() {
@@ -440,9 +291,9 @@ async function searchAndAddPlayer() {
}
const button = document.querySelector('.btn-add');
- const originalHTML = button.innerHTML;
+ const originalText = button.textContent;
button.disabled = true;
- button.innerHTML = ' Searching...';
+ button.textContent = 'Searching...';
try {
const response = await fetch(`/api/search-player/${pdgaNumber}`);
@@ -466,70 +317,112 @@ async function searchAndAddPlayer() {
showErrorModal('Failed to search for player. Please try again.');
} finally {
button.disabled = false;
- button.innerHTML = originalHTML;
+ button.textContent = originalText;
}
}
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');
+ document.getElementById('add-player-modal-header').textContent = 'Confirm Player';
- header.textContent = 'Confirm Player';
- body.innerHTML = `
- Is this the correct player you want to add?
-
- ${player.name}
- PDGA #${player.pdgaNumber}
- ${player.rating ? `Current Rating: ${player.rating}` : 'No rating available'}
-
- `;
- footer.innerHTML = `
-
-
- `;
+ const body = document.getElementById('add-player-modal-body');
+ body.textContent = '';
+
+ const question = document.createElement('p');
+ question.textContent = 'Is this the correct player you want to add?';
+ body.appendChild(question);
+
+ const info = document.createElement('div');
+ info.style.cssText = 'background-color: #f8f9fa; padding: 15px; border-radius: 4px; margin-top: 15px;';
+
+ const name = document.createElement('strong');
+ 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';
}
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');
+ document.getElementById('add-player-modal-header').textContent = 'Player Not Found';
- header.textContent = 'Player Not Found';
- body.innerHTML = `
-
- ${message}
-
-
- Please check the PDGA number and try again.
-
- `;
- footer.innerHTML = `
-
- `;
+ const body = document.getElementById('add-player-modal-body');
+ body.textContent = '';
+
+ const errorP = document.createElement('p');
+ errorP.style.color = '#dc3545';
+ errorP.textContent = message;
+ body.appendChild(errorP);
+
+ const helpP = document.createElement('p');
+ helpP.style.cssText = 'margin-top: 10px; color: #6c757d; font-size: 14px;';
+ 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';
}
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');
+ document.getElementById('add-player-modal-header').textContent = 'Information';
- header.textContent = 'Information';
- body.innerHTML = `
-
- ${message}
-
- `;
- footer.innerHTML = `
-
- `;
+ const body = document.getElementById('add-player-modal-body');
+ body.textContent = '';
+
+ const infoP = document.createElement('p');
+ infoP.style.color = '#007bff';
+ infoP.textContent = message;
+ body.appendChild(infoP);
+
+ 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';
}
@@ -540,19 +433,14 @@ async function confirmAddPlayer() {
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 = ' Adding player...
';
- footer.innerHTML = '';
+ body.textContent = 'Adding player...';
+ document.getElementById('add-player-modal-footer').textContent = '';
try {
const response = await fetch('/api/add-player', {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
+ headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pdgaNumber: pendingPlayerData.pdgaNumber })
});
@@ -562,34 +450,39 @@ async function confirmAddPlayer() {
throw new Error(data.error || 'Failed to add player');
}
- body.innerHTML = `
-
-
- ${data.player.name} has been added successfully!
-
- `;
- footer.innerHTML = `
-
- `;
+ body.textContent = '';
+ const successP = document.createElement('p');
+ successP.style.cssText = 'color: #28a745; text-align: center;';
+ successP.textContent = `${data.player.name} has been added successfully!`;
+ body.appendChild(successP);
+
+ const footer = document.getElementById('add-player-modal-footer');
+ 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 = '';
pendingPlayerData = null;
} catch (error) {
console.error('Error adding player:', error);
- body.innerHTML = `
-
- ${error.message}
-
- `;
- footer.innerHTML = `
-
- `;
+ body.textContent = error.message;
+ body.style.color = '#dc3545';
+
+ 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);
}
}
function closeAddPlayerModal(event) {
- const modal = document.getElementById('add-player-modal');
- modal.style.display = 'none';
+ document.getElementById('add-player-modal').style.display = 'none';
pendingPlayerData = null;
}
diff --git a/public/js/progress.js b/public/js/progress.js
index 0db252e..4f9b18a 100644
--- a/public/js/progress.js
+++ b/public/js/progress.js
@@ -3,15 +3,15 @@ function fetchRatingsWithProgress() {
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const tableDiv = document.getElementById('ratings-table');
-
+
progressSection.style.display = 'block';
tableDiv.innerHTML = '';
-
+
const eventSource = new EventSource('/api/ratings/progress');
-
+
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
-
+
if (data.status === 'loading') {
const percentage = Math.round((data.current / data.total) * 100);
progressBar.style.width = `${percentage}%`;
@@ -29,18 +29,14 @@ function fetchRatingsWithProgress() {
progressText.textContent = `Error loading PDGA #${data.pdgaNumber} (${data.current}/${data.total})`;
} else if (data.status === 'complete') {
progressSection.style.display = 'none';
- displayRatings(data.ratings);
- eventSource.close();
- } else if (data.status === 'error') {
- progressSection.style.display = 'none';
- tableDiv.innerHTML = 'Error loading ratings. Please try again.
';
+ htmx.ajax('GET', '/partials/ratings-table', '#ratings-table');
eventSource.close();
}
};
-
+
eventSource.onerror = function() {
progressSection.style.display = 'none';
- tableDiv.innerHTML = 'Connection error. Please refresh the page.
';
+ tableDiv.textContent = 'Connection error. Please refresh the page.';
eventSource.close();
};
}
@@ -50,26 +46,26 @@ function loadAllPlayers() {
const originalText = button.textContent;
button.textContent = 'Loading...';
button.style.pointerEvents = 'none';
-
+
try {
const progressSection = document.getElementById('progress-section');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const tableDiv = document.getElementById('ratings-table');
-
+
progressSection.style.display = 'block';
- tableDiv.innerHTML = '';
-
+ tableDiv.textContent = '';
+
const eventSource = new EventSource('/api/load-all-players');
-
+
eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
-
+
if (data.status === 'loading' || data.status === 'completed' || data.status === 'error') {
const percentage = Math.round((data.current / data.total) * 100);
progressBar.style.width = `${percentage}%`;
progressBar.textContent = `${percentage}%`;
-
+
if (data.status === 'loading') {
progressText.textContent = `Loading player ${data.current}/${data.total}: PDGA #${data.pdgaNumber}`;
} else if (data.status === 'completed') {
@@ -79,16 +75,16 @@ function loadAllPlayers() {
}
} else if (data.status === 'complete') {
progressSection.style.display = 'none';
- displayRatings(data.ratings);
+ htmx.ajax('GET', '/partials/ratings-table', '#ratings-table');
eventSource.close();
button.textContent = originalText;
button.style.pointerEvents = 'auto';
}
};
-
+
eventSource.onerror = function() {
progressSection.style.display = 'none';
- tableDiv.innerHTML = 'Connection error. Please refresh the page.
';
+ tableDiv.textContent = 'Connection error. Please refresh the page.';
eventSource.close();
button.textContent = originalText;
button.style.pointerEvents = 'auto';
diff --git a/src/routes/courses.js b/src/routes/courses.js
index 69dbd01..734f601 100644
--- a/src/routes/courses.js
+++ b/src/routes/courses.js
@@ -8,6 +8,36 @@ const { layoutEventCache, scrapeCourseDirectory, scrapeCourseLayouts, scrapeEven
// Request locking to prevent concurrent scrapes of the same resource
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('Error loading courses. Please try again.
');
+ }
+});
+
+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('Error loading layouts
');
+ }
+});
+
router.get('/api/courses', async (req, res) => {
try {
const courses = await getAllCoursesFromDB();
diff --git a/src/routes/players.js b/src/routes/players.js
index 209cf85..5601c30 100644
--- a/src/routes/players.js
+++ b/src/routes/players.js
@@ -8,6 +8,43 @@ const { launchBrowser } = require('../scrapers/browser');
const { getPlayerDataFromDB, scrapePDGARating, getAllRatingsFromDB, refreshAllPlayersInDB, getPredictedRatingFromDB } = require('../services/player-service');
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('Error loading ratings. Please try again.
');
+ }
+});
+
+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('Error loading rating history
');
+ }
+});
+
router.get('/api/ratings', async (req, res) => {
try {
const ratings = await getAllRatingsFromDB();
diff --git a/views/pages/courses.ejs b/views/pages/courses.ejs
index c8d72ed..91acf63 100644
--- a/views/pages/courses.ejs
+++ b/views/pages/courses.ejs
@@ -4,11 +4,13 @@
type="text"
class="search-input"
id="course-search"
+ name="q"
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"
/>
-
Loading courses...
-
+
`; %>
<%- include('../partials/layout', {
@@ -26,6 +28,5 @@
activePage: 'courses',
cssFiles: ['courses.css'],
jsFiles: ['courses.js'],
- initScript: 'loadCourses();',
body: body
-}) %>
\ No newline at end of file
+}) %>
diff --git a/views/pages/index.ejs b/views/pages/index.ejs
index 171391d..8b4a9f9 100644
--- a/views/pages/index.ejs
+++ b/views/pages/index.ejs
@@ -26,7 +26,7 @@
Preparing to load ratings...
-
+
`; %>
<% var modals = `
@@ -59,7 +59,7 @@
activePage: 'players',
cssFiles: ['players.css'],
jsFiles: ['tooltips.js', 'chart.js', 'progress.js', 'players.js'],
- initScript: 'fetchRatingsWithProgress();',
+ initScript: 'setupTooltipsAfterSwap();',
body: body,
modals: modals
}) %>
\ No newline at end of file
diff --git a/views/partials/course-layouts.ejs b/views/partials/course-layouts.ejs
new file mode 100644
index 0000000..90861a0
--- /dev/null
+++ b/views/partials/course-layouts.ejs
@@ -0,0 +1,71 @@
+<% if (layouts.length === 0) { %>
+ No layouts found. Click the refresh icon to scrape layouts.
+<% } 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);
+ }
+ });
+%>
+ Layouts:
+
+ <% if (activeLayouts.length > 0) { %>
+ <% activeLayouts.forEach(function(layout) {
+ var ratingDisplay = layout.mean_rating ?
+ 'Rating: ' + layout.mean_rating + '' : '';
+ var dateDisplay = layout.last_played ?
+ 'Last played: ' + new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + '' : '';
+ %>
+
+
+ <%= layout.name %>
+ <%- dateDisplay %>
+
+
Par <%= layout.par %><%- ratingDisplay %>
+
+ <% }); %>
+ <% } %>
+
+ <% if (inactiveLayouts.length > 0) { %>
+
+
+
+ <% inactiveLayouts.forEach(function(layout) {
+ var ratingDisplay = layout.mean_rating ?
+ '
Rating: ' + layout.mean_rating + '' : '';
+ var dateDisplay = layout.last_played ?
+ '
Last played: ' + new Date(layout.last_played).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + '' :
+ '
Never played';
+ %>
+
+
+ <%= layout.name %>
+ <%- dateDisplay %>
+
+
Par <%= layout.par %><%- ratingDisplay %>
+
+ <% }); %>
+
+
+ <% } %>
+
+ <% if (activeLayouts.length === 0 && inactiveLayouts.length === 0) { %>
+ No layouts found. Click the refresh icon to scrape layouts.
+ <% } %>
+<% } %>
diff --git a/views/partials/course-table.ejs b/views/partials/course-table.ejs
new file mode 100644
index 0000000..93a26d3
--- /dev/null
+++ b/views/partials/course-table.ejs
@@ -0,0 +1,46 @@
+
+ <% if (typeof query !== 'undefined' && query) { %>
+ Showing <%= courses.length %> of <%= total %> courses
+ <% } else { %>
+ Showing all <%= courses.length %> courses
+ <% } %>
+
+
+<% if (courses.length === 0) { %>
+ No courses found. Click "Scrape Courses" to load Swedish courses from PDGA.
+<% } else { %>
+
+
+
+ | Course Name |
+ City |
+ Last Updated |
+ Actions |
+
+
+
+ <% courses.forEach(function(course) {
+ var lastUpdated = new Date(course.last_updated).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
+ %>
+
+ |
+ <%= course.name %>
+ <%= course.city %>
+ |
+ <%= course.city %> |
+ <%= lastUpdated %> |
+
+ '); event.stopPropagation();" title="Scrape layouts for this course">
+ |
+
+
+
+
+ Click to load layouts...
+
+ |
+
+ <% }); %>
+
+
+<% } %>
diff --git a/views/partials/layout.ejs b/views/partials/layout.ejs
index 0d11232..c3edb14 100644
--- a/views/partials/layout.ejs
+++ b/views/partials/layout.ejs
@@ -5,6 +5,7 @@
<%= title %>
+
<% if (typeof cssFiles !== 'undefined') { %>
<% cssFiles.forEach(function(file) { %>
diff --git a/views/partials/player-history.ejs b/views/partials/player-history.ejs
new file mode 100644
index 0000000..9bb0edf
--- /dev/null
+++ b/views/partials/player-history.ejs
@@ -0,0 +1,12 @@
+<% if (history && history.length > 0) { %>
+
+
+<% } else { %>
+
+
No rating history available
+
+<% } %>
diff --git a/views/partials/ratings-table.ejs b/views/partials/ratings-table.ejs
new file mode 100644
index 0000000..fb8cb82
--- /dev/null
+++ b/views/partials/ratings-table.ejs
@@ -0,0 +1,64 @@
+<% if (ratings.length === 0) { %>
+ No ratings found.
+<% } else { %>
+
+
+
+ | Rank |
+ Player Name |
+ PDGA # |
+ Rating |
+ Change |
+ Predicted |
+
+
+
+ <% 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';
+ %>
+
+ | <%= index + 1 %> |
+
+ <%= player.name %>
+ PDGA #<%= player.pdgaNumber %>
+ |
+ #<%= player.pdgaNumber %> |
+
+
+ <%- player.rating || 'Click refresh' %>
+
+
+ <%= ratingChangeText %>
+
+ |
+ <%= ratingChangeText %> |
+
+
+ <%= player.predictedRating || 'N/A' %>
+
+
+
+
+ |
+
+
+
+
+
+ Rating History for <%= player.name %>
+
+
+
+
+ Click to load rating history...
+
+ |
+
+ <% }); %>
+
+
+<% } %>