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

298 lines
11 KiB
JavaScript

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) {
const content = document.getElementById(accordionId);
const icon = document.getElementById(`${accordionId}-icon`);
if (content.classList.contains('expanded')) {
content.classList.remove('expanded');
icon.classList.remove('expanded');
} else {
content.classList.add('expanded');
icon.classList.add('expanded');
}
}
async function toggleCourseLayouts(courseId) {
const layoutsRow = document.getElementById(`layouts-${courseId}`);
const layoutsContainer = document.getElementById(`layouts-container-${courseId}`);
if (layoutsRow.style.display === 'table-row') {
layoutsRow.style.display = 'none';
return;
}
layoutsRow.style.display = 'table-row';
if (layoutsContainer.dataset.loaded === 'true') {
return;
}
layoutsContainer.innerHTML = '<div class="no-layouts">Loading layouts...</div>';
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">&#9660;</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() {
const btn = document.getElementById('scrape-courses-btn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-sync-alt spinning"></i> Scraping...';
try {
const response = await fetch('/api/scrape-courses', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
alert(data.message);
await loadCourses();
searchCourses();
} else {
alert('Failed to scrape courses');
}
} catch (error) {
console.error('Error scraping courses:', error);
alert('Error scraping courses');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-sync-alt"></i> Scrape Courses';
}
}
async function scrapeLayouts(courseId, courseName) {
const icon = document.querySelector(`#row-${courseId} .refresh-icon`);
icon.classList.add('spinning');
try {
const response = await fetch(`/api/scrape-layouts/${courseId}`, {
method: 'POST'
});
const data = await response.json();
if (response.status === 409) {
alert(data.message || 'Scrape already in progress for this course. Please wait.');
} else if (data.success) {
const layoutsContainer = document.getElementById(`layouts-container-${courseId}`);
layoutsContainer.dataset.loaded = 'false';
const layoutsRow = document.getElementById(`layouts-${courseId}`);
if (layoutsRow.style.display === 'table-row') {
toggleCourseLayouts(courseId);
setTimeout(() => toggleCourseLayouts(courseId), 100);
}
alert(data.message);
} else {
alert('Failed to scrape layouts');
}
} catch (error) {
console.error('Error scraping layouts:', error);
alert('Error scraping layouts');
} finally {
icon.classList.remove('spinning');
}
}