feat: add async tour system
Players can create tours with selected courses/layouts and a date range. Others join via a 6-character tour code, play the courses, and report their total strokes. Live leaderboard with points and +/- par display. Includes: database schema, model, service, routes, views, and styling.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<% if (!tour) { %>
|
||||
<% var body = `
|
||||
<div class="card-section">
|
||||
<h3>Tour Not Found</h3>
|
||||
<p>The tour code is invalid or the tour has been removed.</p>
|
||||
<a href="/tours" class="btn"><i class="fas fa-arrow-left"></i> Back to Tours</a>
|
||||
</div>
|
||||
`; %>
|
||||
<%- include('../partials/layout', {
|
||||
title: 'Tour Not Found',
|
||||
heading: 'Tour Not Found',
|
||||
activePage: 'tours',
|
||||
cssFiles: ['tours.css'],
|
||||
body: body
|
||||
}) %>
|
||||
<% } else { %>
|
||||
<%
|
||||
var statusBadge = isActive
|
||||
? '<span class="badge badge-active">Active</span>'
|
||||
: isFinished
|
||||
? '<span class="badge badge-finished">Finished</span>'
|
||||
: '<span class="badge badge-upcoming">Upcoming</span>';
|
||||
|
||||
var courseOptionsHtml = '';
|
||||
courses.forEach(function(c) {
|
||||
courseOptionsHtml += '<option value="' + c.tour_course_id + '">'
|
||||
+ c.course_name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
+ ' - '
|
||||
+ c.layout_name.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
+ ' (Par ' + c.par + ')'
|
||||
+ '</option>';
|
||||
});
|
||||
|
||||
var escapedCode = tour.code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
var escapedStartDate = tour.start_date.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
var escapedEndDate = tour.end_date.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
%>
|
||||
|
||||
<% var body = `
|
||||
<div class="tour-header">
|
||||
<div class="tour-info">
|
||||
<div class="tour-meta">
|
||||
${statusBadge}
|
||||
<span class="tour-dates"><i class="fas fa-calendar"></i> ${escapedStartDate} — ${escapedEndDate}</span>
|
||||
<span class="tour-code-label"><i class="fas fa-key"></i> ${escapedCode}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-section" id="join-section">
|
||||
<h3>Join This Tour</h3>
|
||||
<div class="card-section-form">
|
||||
<input type="text" class="input" id="pdga-number" placeholder="PDGA Number" style="width: 160px;" />
|
||||
<input type="text" class="input" id="player-name" placeholder="Your Name" style="width: 200px;" />
|
||||
<button class="btn" onclick="joinTour()">
|
||||
<i class="fas fa-user-plus"></i> Join
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-section" id="result-section" style="display: none;">
|
||||
<h3>Record Result</h3>
|
||||
<div class="card-section-form">
|
||||
<select class="input" id="result-course" style="width: 280px;">
|
||||
<option value="">Select course...</option>
|
||||
${courseOptionsHtml}
|
||||
</select>
|
||||
<input type="number" class="input" id="result-strokes" placeholder="Total strokes" style="width: 140px;" min="1" />
|
||||
<button class="btn" onclick="recordResult('${escapedCode}')">
|
||||
<i class="fas fa-save"></i> Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="leaderboard-container"
|
||||
hx-get="/partials/tour-leaderboard/${escapedCode}"
|
||||
hx-trigger="load">
|
||||
<div class="loading">Loading leaderboard...</div>
|
||||
</div>
|
||||
`; %>
|
||||
|
||||
<%- include('../partials/layout', {
|
||||
title: tour.name + ' - PDGA Tours',
|
||||
heading: tour.name,
|
||||
activePage: 'tours',
|
||||
cssFiles: ['tours.css'],
|
||||
jsFiles: ['tour.js'],
|
||||
initScript: 'initTour("' + escapedCode + '");',
|
||||
body: body
|
||||
}) %>
|
||||
<% } %>
|
||||
@@ -0,0 +1,67 @@
|
||||
<% var body = `
|
||||
<div class="card-section">
|
||||
<h3>Create a Tour</h3>
|
||||
<div class="tour-form">
|
||||
<div class="form-group">
|
||||
<label for="tour-name">Tour Name</label>
|
||||
<input type="text" class="input" id="tour-name" placeholder="e.g. Summer Tour 2026" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="tour-start">Start Date</label>
|
||||
<input type="date" class="input" id="tour-start" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tour-end">End Date</label>
|
||||
<input type="date" class="input" id="tour-end" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Courses</label>
|
||||
<div id="course-selector">
|
||||
<div class="course-entry">
|
||||
<select class="input course-select" data-index="0">
|
||||
<option value="">Loading courses...</option>
|
||||
</select>
|
||||
<select class="input layout-select" data-index="0" disabled>
|
||||
<option value="">Select course first</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" onclick="addCourseEntry()" id="add-course-btn">
|
||||
<i class="fas fa-plus"></i> Add Course
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn" onclick="createTour()" id="create-tour-btn">
|
||||
<i class="fas fa-flag-checkered"></i> Create Tour
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<h3>Join a Tour</h3>
|
||||
<div class="card-section-form">
|
||||
<input type="text" class="input" id="tour-code" placeholder="Tour code (e.g. X7K9M2)" maxlength="6" style="width: 180px;" />
|
||||
<button class="btn" onclick="goToTour()">
|
||||
<i class="fas fa-arrow-right"></i> Go
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tour-created" class="card-section" style="display: none;">
|
||||
<h3>Tour Created!</h3>
|
||||
<p class="tour-created-message">Share this link with players:</p>
|
||||
<div class="tour-code-display">
|
||||
<a id="tour-link" href="#" target="_blank"></a>
|
||||
</div>
|
||||
</div>
|
||||
`; %>
|
||||
|
||||
<%- include('../partials/layout', {
|
||||
title: 'Tours - PDGA Ratings',
|
||||
heading: 'Tours',
|
||||
activePage: 'tours',
|
||||
cssFiles: ['tours.css'],
|
||||
jsFiles: ['tours.js'],
|
||||
body: body
|
||||
}) %>
|
||||
Reference in New Issue
Block a user