refactor: remove tour feature and Tjing import
Release Please / release-please (push) Waiting to run
Release Please / docker (push) Blocked by required conditions

Tour functionality has moved to its own project (HyzrTour).
Removes all tour-related code, Tjing integration, and associated
views/styles/scripts. Keeps the saveCourseToDB ON CONFLICT fix.
This commit is contained in:
Samuel Enocsson
2026-03-20 15:05:14 +01:00
parent b4206d9865
commit 6e05d3014d
21 changed files with 1 additions and 3056 deletions
-33
View File
@@ -132,36 +132,3 @@
border-style: dashed;
}
/* ── Tjing Import ────────────────────────────── */
.tjing-result {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 14px;
margin: 6px 0;
background: var(--surface-2);
border-radius: var(--radius-sm);
border: 1px solid var(--border);
}
.tjing-result-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.tjing-result-name {
font-weight: 600;
font-size: 14px;
color: var(--text-primary);
}
.tjing-result-address {
font-size: 13px;
color: var(--text-secondary);
}
#tjing-results {
margin-top: 12px;
}
-211
View File
@@ -1,211 +0,0 @@
/* Tour Form */
.tour-form {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 600px;
margin: 0 auto;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-group label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-row {
display: flex;
gap: 12px;
}
.form-row .form-group {
flex: 1;
}
/* Course Selector */
.course-entry {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
}
.course-entry select {
flex: 1;
min-width: 0;
}
.manual-layout {
display: flex;
gap: 6px;
align-items: center;
flex-basis: 100%;
}
.manual-layout .layout-name-input {
flex: 1;
}
.manual-layout .layout-par-input {
width: 80px;
flex: none;
}
.btn-secondary {
background: var(--surface-2);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: var(--surface-3);
color: var(--text-primary);
}
.btn-remove {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 6px;
border-radius: var(--radius-sm);
font-size: 14px;
transition: color var(--transition), background var(--transition);
}
.btn-remove:hover {
color: var(--red);
background: var(--red-subtle);
}
/* Tour Header */
.tour-header {
margin-bottom: 24px;
}
.tour-meta {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.tour-dates,
.tour-code-label {
font-size: 14px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 6px;
}
/* Badges */
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.badge-active {
background: var(--green-subtle);
color: var(--green);
}
.badge-finished {
background: var(--surface-3);
color: var(--text-secondary);
}
.badge-upcoming {
background: var(--accent-subtle);
color: var(--accent);
}
/* Tour Code Display */
.tour-created-message {
text-align: center;
color: var(--text-secondary);
margin: 0 0 12px 0;
}
.tour-code-display {
text-align: center;
padding: 16px;
background: var(--surface-2);
border-radius: var(--radius-md);
border: 1px solid var(--border);
}
.tour-code-display a {
font-family: var(--font-mono);
font-size: 16px;
font-weight: 500;
}
/* Leaderboard */
.strokes {
font-weight: 600;
font-family: var(--font-mono);
}
.under-par {
color: var(--green);
font-size: 12px;
font-weight: 600;
}
.over-par {
color: var(--red);
font-size: 12px;
font-weight: 600;
}
.even-par {
color: var(--text-muted);
font-size: 12px;
font-weight: 600;
}
/* Responsive */
@media (max-width: 768px) {
.form-row {
flex-direction: column;
}
.course-entry {
flex-direction: column;
}
.course-entry select,
.manual-layout {
width: 100%;
}
.manual-layout {
flex-direction: row;
}
.tour-meta {
gap: 8px;
}
}
-82
View File
@@ -54,88 +54,6 @@ async function scrapeCourses() {
}
}
async function searchTjing() {
var input = document.getElementById('tjing-search');
var query = input.value.trim();
if (query.length < 2) return;
var btn = document.getElementById('tjing-search-btn');
btn.disabled = true;
try {
var response = await fetch('/api/tjing/search?q=' + encodeURIComponent(query));
var courses = await response.json();
var container = document.getElementById('tjing-results');
container.textContent = '';
if (courses.length === 0) {
var p = document.createElement('p');
p.className = 'no-layouts';
p.textContent = 'No courses found on Tjing';
container.appendChild(p);
return;
}
courses.forEach(function(course) {
var item = document.createElement('div');
item.className = 'tjing-result';
var info = document.createElement('div');
info.className = 'tjing-result-info';
var name = document.createElement('span');
name.className = 'tjing-result-name';
name.textContent = course.name;
var addr = document.createElement('span');
addr.className = 'tjing-result-address';
addr.textContent = course.address || '';
info.appendChild(name);
info.appendChild(addr);
var importBtn = document.createElement('button');
importBtn.className = 'btn';
importBtn.textContent = 'Import';
importBtn.addEventListener('click', function() { importFromTjing(course.id, importBtn); });
item.appendChild(info);
item.appendChild(importBtn);
container.appendChild(item);
});
} catch (error) {
console.error('Error searching Tjing:', error);
alert('Failed to search Tjing');
} finally {
btn.disabled = false;
}
}
async function importFromTjing(tjingId, btn) {
btn.disabled = true;
btn.textContent = 'Importing...';
try {
var response = await fetch('/api/tjing/import/' + tjingId, { method: 'POST' });
var data = await response.json();
if (data.success) {
btn.textContent = 'Imported!';
btn.style.background = 'var(--green)';
htmx.ajax('GET', '/partials/course-table', '#courses-table');
} else {
alert(data.error || 'Failed to import');
btn.disabled = false;
btn.textContent = 'Import';
}
} catch (error) {
console.error('Error importing from Tjing:', error);
alert('Failed to import course');
btn.disabled = false;
btn.textContent = 'Import';
}
}
async function scrapeLayouts(courseId, courseName) {
const icon = document.querySelector(`#row-${courseId} .refresh-icon`);
icon.classList.add('spinning');
-91
View File
@@ -1,91 +0,0 @@
var currentPdgaNumber = null;
function initTour(code) {
// Check if player already joined (stored in localStorage)
var stored = localStorage.getItem('tour_' + code);
if (stored) {
var data = JSON.parse(stored);
currentPdgaNumber = data.pdgaNumber;
showResultSection();
}
}
function showResultSection() {
var joinSection = document.getElementById('join-section');
var resultSection = document.getElementById('result-section');
if (joinSection) joinSection.style.display = 'none';
if (resultSection) resultSection.style.display = '';
}
async function joinTour() {
var pdgaNumber = document.getElementById('pdga-number').value.trim();
var playerName = document.getElementById('player-name').value.trim();
if (!pdgaNumber || !playerName) {
alert('Please enter your PDGA number and name');
return;
}
var code = window.location.pathname.split('/').pop();
try {
var res = await fetch('/api/tours/' + code + '/join', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pdgaNumber: pdgaNumber, playerName: playerName })
});
var data = await res.json();
if (data.success) {
currentPdgaNumber = pdgaNumber;
localStorage.setItem('tour_' + code, JSON.stringify({ pdgaNumber: pdgaNumber, playerName: playerName }));
showResultSection();
// Refresh leaderboard
htmx.ajax('GET', '/partials/tour-leaderboard/' + code, { target: '#leaderboard-container' });
} else {
alert(data.error || 'Failed to join tour');
}
} catch (err) {
console.error('Error joining tour:', err);
alert('Failed to join tour');
}
}
async function recordResult(code) {
var tourCourseId = document.getElementById('result-course').value;
var totalStrokes = document.getElementById('result-strokes').value;
if (!tourCourseId || !totalStrokes) {
alert('Please select a course and enter your strokes');
return;
}
if (!currentPdgaNumber) {
alert('Please join the tour first');
return;
}
try {
var res = await fetch('/api/tours/' + code + '/results', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pdgaNumber: currentPdgaNumber,
tourCourseId: parseInt(tourCourseId),
totalStrokes: parseInt(totalStrokes)
})
});
var data = await res.json();
if (data.success) {
document.getElementById('result-strokes').value = '';
// Refresh leaderboard
htmx.ajax('GET', '/partials/tour-leaderboard/' + code, { target: '#leaderboard-container' });
} else {
alert(data.error || 'Failed to record result');
}
} catch (err) {
console.error('Error recording result:', err);
alert('Failed to record result');
}
}
-255
View File
@@ -1,255 +0,0 @@
var coursesData = [];
async function loadCourses() {
try {
var res = await fetch('/api/tours/courses-with-layouts');
coursesData = await res.json();
updateCourseDropdowns();
} catch (err) {
console.error('Failed to load courses:', err);
}
}
function populateSelectWithCourses(select) {
select.textContent = '';
var defaultOpt = document.createElement('option');
defaultOpt.value = '';
defaultOpt.textContent = 'Select a course...';
select.appendChild(defaultOpt);
coursesData.forEach(function(course) {
var opt = document.createElement('option');
opt.value = course.id;
opt.textContent = course.name + ' (' + course.city + ')';
select.appendChild(opt);
});
}
function updateCourseDropdowns() {
document.querySelectorAll('.course-select').forEach(function(select) {
var currentValue = select.value;
populateSelectWithCourses(select);
select.value = currentValue;
});
}
function onCourseChange(courseSelect) {
var index = courseSelect.dataset.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...';
layoutSelect.appendChild(defaultOpt);
if (!courseId) return;
var course = coursesData.find(function(c) { return c.id === courseId; });
if (!course) return;
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 createCourseEntry(index) {
var entry = document.createElement('div');
entry.className = 'course-entry';
var courseSelect = document.createElement('select');
courseSelect.className = 'input course-select';
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 = 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';
removeBtn.type = 'button';
var icon = document.createElement('i');
icon.className = 'fas fa-times';
removeBtn.appendChild(icon);
removeBtn.addEventListener('click', function() { entry.remove(); });
entry.appendChild(removeBtn);
container.appendChild(entry);
courseIndex++;
}
async function createTour() {
var name = document.getElementById('tour-name').value.trim();
var startDate = document.getElementById('tour-start').value;
var endDate = document.getElementById('tour-end').value;
if (!name || !startDate || !endDate) {
alert('Please fill in name and dates');
return;
}
var courses = [];
var valid = true;
document.querySelectorAll('.course-entry').forEach(function(entry) {
var courseId = entry.querySelector('.course-select').value;
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;
}
var btn = document.getElementById('create-tour-btn');
btn.disabled = true;
try {
var res = await fetch('/api/tours', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, startDate: startDate, endDate: endDate, courses: courses })
});
var data = await res.json();
if (data.success) {
var link = document.getElementById('tour-link');
link.href = '/tours/' + data.code;
link.textContent = window.location.origin + '/tours/' + data.code;
document.getElementById('tour-created').style.display = '';
} else {
alert(data.error || 'Failed to create tour');
}
} catch (err) {
console.error('Error creating tour:', err);
alert('Failed to create tour');
} finally {
btn.disabled = false;
}
}
function goToTour() {
var code = document.getElementById('tour-code').value.trim().toUpperCase();
if (code) {
window.location.href = '/tours/' + code;
}
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadCourses();
// 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);
}
});
});