20bbdbbfcf
- 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)
298 lines
11 KiB
JavaScript
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">▼</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');
|
|
}
|
|
}
|