diff --git a/public/css/tours.css b/public/css/tours.css index 171eb7c..47c66c7 100644 --- a/public/css/tours.css +++ b/public/css/tours.css @@ -44,6 +44,16 @@ flex: 1; } +.manual-layout { + display: flex; + gap: 6px; + align-items: center; +} + +.manual-layout input { + flex: 1; +} + .btn-secondary { background: var(--surface-2); color: var(--text-secondary); @@ -178,10 +188,15 @@ flex-direction: column; } - .course-entry select { + .course-entry select, + .manual-layout { width: 100%; } + .manual-layout { + flex-direction: row; + } + .tour-meta { gap: 8px; } diff --git a/public/js/tours.js b/public/js/tours.js index 9f8d384..93f209d 100644 --- a/public/js/tours.js +++ b/public/js/tours.js @@ -35,12 +35,17 @@ function updateCourseDropdowns() { function onCourseChange(courseSelect) { var index = courseSelect.dataset.index; - var layoutSelect = document.querySelector('.layout-select[data-index="' + index + '"]'); + var layoutSelect = courseSelect.parentElement.querySelector('.layout-select'); + var manualFields = courseSelect.parentElement.querySelector('.manual-layout'); var courseId = parseInt(courseSelect.value); layoutSelect.textContent = ''; layoutSelect.disabled = true; + if (manualFields) { + manualFields.style.display = 'none'; + } + var defaultOpt = document.createElement('option'); defaultOpt.value = ''; defaultOpt.textContent = 'Select layout...'; @@ -51,36 +56,95 @@ function onCourseChange(courseSelect) { var course = coursesData.find(function(c) { return c.id === courseId; }); if (!course) return; - course.layouts.forEach(function(layout) { - var opt = document.createElement('option'); - opt.value = layout.id; - opt.textContent = layout.name + ' (Par ' + layout.par + ')'; - layoutSelect.appendChild(opt); - }); + if (course.layouts.length > 0) { + course.layouts.forEach(function(layout) { + var opt = document.createElement('option'); + opt.value = layout.id; + opt.textContent = layout.name + ' (Par ' + layout.par + ')'; + layoutSelect.appendChild(opt); + }); + + // Add "custom" option at the end + var customOpt = document.createElement('option'); + customOpt.value = 'custom'; + customOpt.textContent = '+ Add custom layout...'; + layoutSelect.appendChild(customOpt); + } else { + // No layouts — show "custom" as the only real option + var customOpt = document.createElement('option'); + customOpt.value = 'custom'; + customOpt.textContent = '+ Add custom layout...'; + layoutSelect.appendChild(customOpt); + } + layoutSelect.disabled = false; } +function onLayoutChange(layoutSelect) { + var manualFields = layoutSelect.parentElement.querySelector('.manual-layout'); + if (layoutSelect.value === 'custom') { + if (manualFields) manualFields.style.display = ''; + } else { + if (manualFields) manualFields.style.display = 'none'; + } +} + +function createManualLayoutFields() { + var div = document.createElement('div'); + div.className = 'manual-layout'; + div.style.display = 'none'; + + var nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.className = 'input layout-name-input'; + nameInput.placeholder = 'Layout name'; + + var parInput = document.createElement('input'); + parInput.type = 'number'; + parInput.className = 'input layout-par-input'; + parInput.placeholder = 'Par'; + parInput.min = '1'; + parInput.style.width = '80px'; + + div.appendChild(nameInput); + div.appendChild(parInput); + return div; +} + var courseIndex = 1; -function addCourseEntry() { - var container = document.getElementById('course-selector'); +function createCourseEntry(index) { var entry = document.createElement('div'); entry.className = 'course-entry'; var courseSelect = document.createElement('select'); courseSelect.className = 'input course-select'; - courseSelect.dataset.index = courseIndex; + courseSelect.dataset.index = index; populateSelectWithCourses(courseSelect); courseSelect.addEventListener('change', function() { onCourseChange(courseSelect); }); var layoutSelect = document.createElement('select'); layoutSelect.className = 'input layout-select'; - layoutSelect.dataset.index = courseIndex; + layoutSelect.dataset.index = index; layoutSelect.disabled = true; var defaultOpt = document.createElement('option'); defaultOpt.value = ''; defaultOpt.textContent = 'Select course first'; layoutSelect.appendChild(defaultOpt); + layoutSelect.addEventListener('change', function() { onLayoutChange(layoutSelect); }); + + var manualFields = createManualLayoutFields(); + + entry.appendChild(courseSelect); + entry.appendChild(layoutSelect); + entry.appendChild(manualFields); + + return entry; +} + +function addCourseEntry() { + var container = document.getElementById('course-selector'); + var entry = createCourseEntry(courseIndex); var removeBtn = document.createElement('button'); removeBtn.className = 'btn-remove'; @@ -90,8 +154,6 @@ function addCourseEntry() { removeBtn.appendChild(icon); removeBtn.addEventListener('click', function() { entry.remove(); }); - entry.appendChild(courseSelect); - entry.appendChild(layoutSelect); entry.appendChild(removeBtn); container.appendChild(entry); @@ -109,14 +171,30 @@ async function createTour() { } var courses = []; + var valid = true; document.querySelectorAll('.course-entry').forEach(function(entry) { var courseId = entry.querySelector('.course-select').value; - var layoutId = entry.querySelector('.layout-select').value; - if (courseId && layoutId) { - courses.push({ courseId: parseInt(courseId), layoutId: parseInt(layoutId) }); + var layoutSelect = entry.querySelector('.layout-select'); + var layoutValue = layoutSelect.value; + + if (!courseId) return; + + if (layoutValue === 'custom') { + var layoutName = entry.querySelector('.layout-name-input').value.trim(); + var par = entry.querySelector('.layout-par-input').value; + if (!layoutName || !par) { + alert('Please fill in layout name and par for custom layouts'); + valid = false; + return; + } + courses.push({ courseId: parseInt(courseId), layoutName: layoutName, par: parseInt(par) }); + } else if (layoutValue) { + courses.push({ courseId: parseInt(courseId), layoutId: parseInt(layoutValue) }); } }); + if (!valid) return; + if (courses.length === 0) { alert('Please select at least one course with a layout'); return; @@ -160,10 +238,18 @@ function goToTour() { document.addEventListener('DOMContentLoaded', function() { loadCourses(); - // Delegate change events for course selects - document.getElementById('course-selector').addEventListener('change', function(e) { + // Replace the initial static course entry with a dynamic one + var container = document.getElementById('course-selector'); + container.textContent = ''; + container.appendChild(createCourseEntry(0)); + + // Delegate change events + container.addEventListener('change', function(e) { if (e.target.classList.contains('course-select')) { onCourseChange(e.target); } + if (e.target.classList.contains('layout-select')) { + onLayoutChange(e.target); + } }); -}); \ No newline at end of file +}); diff --git a/src/models/course.js b/src/models/course.js index e035e59..97ca377 100644 --- a/src/models/course.js +++ b/src/models/course.js @@ -69,10 +69,33 @@ function updateLayoutRating(courseId, layoutName, par, meanRating, ratingCount, }); } +function getOrCreateLayout(courseId, name, par) { + return new Promise((resolve, reject) => { + db.get( + 'SELECT id FROM layouts WHERE course_id = ? AND name = ? AND par = ?', + [courseId, name, par], + (err, row) => { + if (err) return reject(err); + if (row) return resolve(row.id); + + db.run( + 'INSERT INTO layouts (course_id, name, par) VALUES (?, ?, ?)', + [courseId, name, par], + function(err) { + if (err) reject(err); + else resolve(this.lastID); + } + ); + } + ); + }); +} + module.exports = { saveCourseToDB, getAllCoursesFromDB, saveLayoutToDB, getLayoutsForCourse, + getOrCreateLayout, updateLayoutRating }; diff --git a/src/routes/tours.js b/src/routes/tours.js index 606a85d..827d65b 100644 --- a/src/routes/tours.js +++ b/src/routes/tours.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { getAllCoursesFromDB, getLayoutsForCourse } = require('../models/course'); +const { getAllCoursesFromDB, getLayoutsForCourse, getOrCreateLayout } = require('../models/course'); const { createTour, getTourByCode, addCourseToTour, getTourCourses, getTourCourseById, joinTour, getPlayerInTour, recordResult @@ -25,7 +25,18 @@ router.post('/api/tours', async (req, res) => { const tourId = await createTour(name, code, startDate, endDate); for (const course of courses) { - await addCourseToTour(tourId, course.courseId, course.layoutId); + let layoutId = course.layoutId; + + // Create new layout if name and par provided instead of layoutId + if (!layoutId && course.layoutName && course.par) { + layoutId = await getOrCreateLayout(course.courseId, course.layoutName, parseInt(course.par)); + } + + if (!layoutId) { + return res.status(400).json({ error: 'Each course needs a layout (existing or new with name and par)' }); + } + + await addCourseToTour(tourId, course.courseId, layoutId); } logger.info(`Tour created: "${name}" (${code}) with ${courses.length} courses`); @@ -121,20 +132,18 @@ router.get('/partials/tour-leaderboard/:code', async (req, res) => { } }); -// Get courses and layouts for tour creation form +// Get all courses with their layouts (if any) for tour creation form router.get('/api/tours/courses-with-layouts', async (req, res) => { try { const courses = await getAllCoursesFromDB(); - const coursesWithLayouts = []; + const result = []; for (const course of courses) { const layouts = await getLayoutsForCourse(course.id); - if (layouts.length > 0) { - coursesWithLayouts.push({ ...course, layouts }); - } + result.push({ ...course, layouts }); } - res.json(coursesWithLayouts); + res.json(result); } catch (error) { logger.error('Error fetching courses with layouts:', error.message); res.status(500).json({ error: 'Failed to fetch courses' });