feat: add golf prototype with hole navigation and stats

Co-authored-by: aider (openrouter/thinkingmachines/inkling-small:free) <aider@aider.chat>
This commit is contained in:
Jiří Stachandaider committed 2026-10-09 21:57:56 +02:00
1 parent 6be02b9a23
commit e164b1958c
3 files changed
+448

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
let currentUser = null;
let currentRound = null;
let currentData = null;
let currentIndex = 0;
async function signup() {
const name = document.getElementById('name').value;
const hcp = parseInt(document.getElementById('hcp').value) || 0;
const res = await fetch('/signup', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, handicap: hcp})
});
currentUser = await res.json();
document.getElementById('signup-section').style.display = 'none';
document.getElementById('start-section').style.display = 'block';
loadCourse();
}
async function loadCourse() {
const res = await fetch('/course');
const course = await res.json();
document.getElementById('course-info').textContent = `${course.name} — ${course.holes.length} holes`;
}
async function startRound() {
const res = await fetch('/course');
const course = await res.json();
const res2 = await fetch('/round', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user_id: currentUser.id, course_id: course.id})
});
currentRound = await res2.json();
document.getElementById('start-section').style.display = 'none';
document.getElementById('round-section').style.display = 'block';
document.getElementById('stats-section').style.display = 'block';
loadRound();
}
async function loadRound() {
const res = await fetch(`/round/${currentRound.id}`);
currentData = await res.json();
const holes = currentData.holes.sort((a,b) => a.hole_number - b.hole_number);
if (holes.length === 0) return;
if (currentIndex >= holes.length) currentIndex = 0;
renderHole(holes[currentIndex]);
renderNav(holes);
loadStats();
}
function renderHole(hole) {
document.getElementById('hole-score-id').value = hole.hole_score_id;
document.getElementById('hole-label').textContent = `Hole ${hole.hole_number}`;
document.getElementById('strokes').value = hole.strokes || 0;
document.getElementById('fairway').checked = hole.fairway_hit;
document.getElementById('skipped').checked = hole.skipped;
document.getElementById('completed').checked = hole.completed;
document.getElementById('gir-display').textContent = hole.gir ? 'Yes' : 'No';
const putts = hole.putts || [];
document.getElementById('putts-input').value = putts.map(p => p.location).join(',');
const pens = (hole.penalties || []).map(p => p.type).join(',');
document.getElementById('penalties-input').value = pens;
const buns = (hole.bunkers || []).map(b => b.location).join(',');
document.getElementById('bunkers-input').value = buns;
}
function renderNav(holes) {
document.getElementById('prev-btn').disabled = currentIndex <= 0;
document.getElementById('next-btn').disabled = currentIndex >= holes.length - 1;
}
function prevHole() {
if (currentIndex > 0) {
currentIndex--;
const holes = currentData.holes.sort((a,b) => a.hole_number - b.hole_number);
renderHole(holes[currentIndex]);
renderNav(holes);
}
}
function nextHole() {
const holes = currentData.holes.sort((a,b) => a.hole_number - b.hole_number);
if (currentIndex < holes.length - 1) {
currentIndex++;
renderHole(holes[currentIndex]);
renderNav(holes);
}
}
async function saveHole() {
const id = document.getElementById('hole-score-id').value;
const strokes = parseInt(document.getElementById('strokes').value) || 0;
const fairway = document.getElementById('fairway').checked;
const skipped = document.getElementById('skipped').checked;
const completed = document.getElementById('completed').checked;
const puttsStr = document.getElementById('putts-input').value;
const putts = puttsStr.split(',').filter(s => s.trim()).map(s => ({location: s.trim()}));
const pensStr = document.getElementById('penalties-input').value;
const penalties = pensStr.split(',').filter(s => s.trim()).map(s => ({type: s.trim()}));
const bunsStr = document.getElementById('bunkers-input').value;
const bunkers = bunsStr.split(',').filter(s => s.trim()).map(s => ({location: s.trim()}));
const res = await fetch(`/hole/${id}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
strokes, skipped, completed, fairway_hit: fairway,
putts, penalties, bunkers
})
});
const result = await res.json();
document.getElementById('gir-display').textContent = result.gir ? 'Yes' : 'No';
loadRound();
}
async function loadStats() {
const res = await fetch(`/stats/${currentRound.id}`);
const stats = await res.json();
document.getElementById('stats-display').innerHTML = `
<p><strong>Total Strokes:</strong> ${stats.total_strokes}</p>
<p><strong>Total Putts:</strong> ${stats.total_putts}</p>
<p><strong>Fairways Hit:</strong> ${stats.fairways_hit}</p>
<p><strong>GIR:</strong> ${stats.gir}</p>
<p><strong>Holes Played:</strong> ${stats.holes_played}</p>
`;
}
+60
View File
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Golf Prototype</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; color: #222; }
section { margin-bottom: 1.5rem; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; background: #fafafa; }
h1 { font-size: 1.5rem; }
label { display: inline-block; margin: 0.25rem 0; }
input, select { padding: 0.25rem; }
button { padding: 0.4rem 0.8rem; margin-right: 0.5rem; cursor: pointer; }
#stats-display p { margin: 0.2rem 0; }
</style>
</head>
<body>
<h1>Golf Score Card — Prototype</h1>
<section id="signup-section">
<h2>Sign Up</h2>
<label>Name: <input id="name" type="text" placeholder="Your name" /></label><br>
<label>Handicap: <input id="hcp" type="number" value="0" /></label><br>
<button onclick="signup()">Sign Up</button>
</section>
<section id="start-section" style="display:none;">
<h2>Course</h2>
<p id="course-info"></p>
<button onclick="startRound()">Start Round</button>
</section>
<section id="round-section" style="display:none;">
<h2>Round</h2>
<div>
<button id="prev-btn" onclick="prevHole()">Prev</button>
<strong id="hole-label">Hole 1</strong>
<button id="next-btn" onclick="nextHole()">Next</button>
</div>
<form id="hole-form" onsubmit="event.preventDefault(); saveHole();">
<input type="hidden" id="hole-score-id" />
<label>Strokes: <input id="strokes" type="number" value="0" min="0" /></label><br>
<label>Fairway Hit: <input id="fairway" type="checkbox" /></label><br>
<label>Skipped: <input id="skipped" type="checkbox" /></label><br>
<label>Completed: <input id="completed" type="checkbox" /></label><br>
<label>Putts (comma list): <input id="putts-input" type="text" placeholder="green,green" /></label><br>
<label>Penalties (comma list): <input id="penalties-input" type="text" placeholder="water" /></label><br>
<label>Bunkers (comma list): <input id="bunkers-input" type="text" placeholder="greenside" /></label><br>
<button type="submit">Save Hole</button>
</form>
<p>GIR: <strong id="gir-display">No</strong></p>
</section>
<section id="stats-section" style="display:none;">
<h2>Basic Stats</h2>
<div id="stats-display"></div>
</section>
<script src="app.js"></script>
</body>
</html>