Add game confirmation step before starting

New step 3 shows a summary of all selections (format, carryover,
course, holes, players with HCP and tee) before submitting.
Also includes handicap in player query and tee tracking in Alpine state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rolf
2026-03-22 10:54:31 +01:00
parent 4ae515bbab
commit b03e515f75
2 changed files with 91 additions and 7 deletions
+2 -2
View File
@@ -158,7 +158,7 @@ async def games_list(request: Request, user: dict = Depends(get_current_user),
async def new_game_page(request: Request, user: dict = Depends(get_current_user), async def new_game_page(request: Request, user: dict = Depends(get_current_user),
db: aiosqlite.Connection = Depends(get_db)): db: aiosqlite.Connection = Depends(get_db)):
async with db.execute( async with db.execute(
"SELECT id, name, avatar FROM users WHERE is_onboarded = 1 ORDER BY name" "SELECT id, name, avatar, handicap FROM users WHERE is_onboarded = 1 ORDER BY name"
) as c: ) as c:
all_users = [dict(r) for r in await c.fetchall()] all_users = [dict(r) for r in await c.fetchall()]
async with db.execute("SELECT id, name, holes, location FROM courses ORDER BY name") as c: async with db.execute("SELECT id, name, holes, location FROM courses ORDER BY name") as c:
@@ -216,7 +216,7 @@ async def create_game(request: Request, holes_count: int = Form(...),
if error: if error:
async with db.execute( async with db.execute(
"SELECT id, name, avatar FROM users WHERE is_onboarded = 1 ORDER BY name" "SELECT id, name, avatar, handicap FROM users WHERE is_onboarded = 1 ORDER BY name"
) as c: ) as c:
all_users = [dict(r) for r in await c.fetchall()] all_users = [dict(r) for r in await c.fetchall()]
async with db.execute( async with db.execute(
+89 -5
View File
@@ -5,11 +5,16 @@
<script> <script>
const TEES_BY_COURSE = {{ tees_by_course | tojson }}; const TEES_BY_COURSE = {{ tees_by_course | tojson }};
const COURSES_DATA = {{ courses | tojson }}; const COURSES_DATA = {{ courses | tojson }};
const ALL_PLAYERS = {{ all_users | tojson }};
</script> </script>
<style> <style>
.picker-item { padding:0.65rem 0.75rem; cursor:pointer; border-bottom:1px solid var(--border); } .picker-item { padding:0.65rem 0.75rem; cursor:pointer; border-bottom:1px solid var(--border); }
.picker-item:last-child { border-bottom:none; } .picker-item:last-child { border-bottom:none; }
.picker-item.selected { background:var(--surface2); } .picker-item.selected { background:var(--surface2); }
.confirm-row { display:flex; justify-content:space-between; align-items:baseline; padding:0.45rem 0; border-bottom:1px solid var(--border); font-size:0.95rem; }
.confirm-row:last-child { border-bottom:none; }
.confirm-label { color:var(--text-muted); flex-shrink:0; margin-right:1rem; }
.confirm-value { font-weight:500; text-align:right; }
</style> </style>
{% endblock %} {% endblock %}
@@ -22,7 +27,10 @@
courseOpen: false, courseOpen: false,
courseSearch: '', courseSearch: '',
format: 'bingo_bango_bongo', format: 'bingo_bango_bongo',
carryoverEnabled: true,
holesCount: '',
selectedPlayers: ['{{ user.id }}'], selectedPlayers: ['{{ user.id }}'],
selectedTees: {},
playerSearch: '', playerSearch: '',
filteredCourses() { filteredCourses() {
const q = this.courseSearch.toLowerCase(); const q = this.courseSearch.toLowerCase();
@@ -44,7 +52,18 @@
else if (id !== '{{ user.id }}') this.selectedPlayers.splice(idx, 1); else if (id !== '{{ user.id }}') this.selectedPlayers.splice(idx, 1);
}, },
hasTees() { return (TEES_BY_COURSE[this.courseId] || []).length > 0; }, hasTees() { return (TEES_BY_COURSE[this.courseId] || []).length > 0; },
tees() { return TEES_BY_COURSE[this.courseId] || []; } tees() { return TEES_BY_COURSE[this.courseId] || []; },
getPlayer(uid) { return ALL_PLAYERS.find(p => p.id === uid) || {}; },
getTee(uid) {
const id = this.selectedTees[uid];
return id ? (TEES_BY_COURSE[this.courseId] || []).find(t => t.id === id) : null;
},
goToConfirm() { this.step = 3; },
backFromConfirm() { this.step = this.hasTees() ? 2 : 1; },
formatLabel() {
if (this.format === 'skins') return 'Skins';
return 'Bingo Bango Bongo';
}
}"> }">
<div class="page-header"> <div class="page-header">
@@ -75,7 +94,8 @@
</div> </div>
<div x-show="format === 'skins'" style="margin-top:0.75rem;"> <div x-show="format === 'skins'" style="margin-top:0.75rem;">
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;"> <label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
<input type="checkbox" name="carryover" value="1" checked style="width:auto;"> <input type="checkbox" name="carryover" value="1" checked style="width:auto;"
@change="carryoverEnabled = $event.target.checked">
<span style="font-weight:400; color:var(--text-muted);">Carryover (ties carry to next hole)</span> <span style="font-weight:400; color:var(--text-muted);">Carryover (ties carry to next hole)</span>
</label> </label>
</div> </div>
@@ -132,11 +152,13 @@
<label>Holes</label> <label>Holes</label>
<div style="display:flex; gap:0.75rem;"> <div style="display:flex; gap:0.75rem;">
<label class="radio-card"> <label class="radio-card">
<input type="radio" name="holes_count" value="9" required> <input type="radio" name="holes_count" value="9" required
@change="holesCount = $event.target.value">
<span>9 holes</span> <span>9 holes</span>
</label> </label>
<label class="radio-card"> <label class="radio-card">
<input type="radio" name="holes_count" value="18"> <input type="radio" name="holes_count" value="18"
@change="holesCount = $event.target.value">
<span>18 holes</span> <span>18 holes</span>
</label> </label>
</div> </div>
@@ -174,7 +196,7 @@
</div> </div>
<button type="button" x-show="hasTees()" class="btn btn-primary" @click="step = 2">Next →</button> <button type="button" x-show="hasTees()" class="btn btn-primary" @click="step = 2">Next →</button>
<button type="submit" x-show="!hasTees()" class="btn btn-primary">Start game ⛳</button> <button type="button" x-show="!hasTees()" class="btn btn-primary" @click="goToConfirm()">Next →</button>
</div> </div>
{# ── Step 2: Tee selection ── #} {# ── Step 2: Tee selection ── #}
@@ -192,6 +214,7 @@
{{ u.name }} {{ u.name }}
</label> </label>
<select name="tee_{{ u.id }}" <select name="tee_{{ u.id }}"
@change="selectedTees['{{ u.id }}'] = $event.target.value"
style="width:100%; padding:0.65rem 0.75rem; border-radius:0.5rem; border:1px solid var(--border); background:var(--surface); color:var(--text); font-size:1rem;"> style="width:100%; padding:0.65rem 0.75rem; border-radius:0.5rem; border:1px solid var(--border); background:var(--surface); color:var(--text); font-size:1rem;">
<option value="" disabled selected>Select tee…</option> <option value="" disabled selected>Select tee…</option>
<template x-for="t in tees()" :key="t.id"> <template x-for="t in tees()" :key="t.id">
@@ -205,8 +228,69 @@
<div style="display:flex; gap:0.75rem;"> <div style="display:flex; gap:0.75rem;">
<button type="button" @click="step = 1" <button type="button" @click="step = 1"
style="flex:1; padding:0.85rem; border-radius:0.65rem; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:1rem; cursor:pointer;">← Back</button> style="flex:1; padding:0.85rem; border-radius:0.65rem; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:1rem; cursor:pointer;">← Back</button>
<button type="button" class="btn btn-primary" style="flex:2;" @click="goToConfirm()">Next →</button>
</div>
</div>
{# ── Step 3: Confirmation ── #}
<div x-show="step === 3" style="display:none;">
<div class="card">
<h2 style="font-size:0.95rem; color:var(--text-muted); margin-bottom:0.75rem;">Game details</h2>
<div class="confirm-row">
<span class="confirm-label">Format</span>
<span class="confirm-value" x-text="formatLabel()"></span>
</div>
<div class="confirm-row" x-show="format === 'skins'">
<span class="confirm-label">Carryover</span>
<span class="confirm-value" x-text="carryoverEnabled ? 'Yes' : 'No'"></span>
</div>
<div class="confirm-row">
<span class="confirm-label">Course</span>
<span class="confirm-value" x-text="courseLabel || '—'"></span>
</div>
<div class="confirm-row">
<span class="confirm-label">Holes</span>
<span class="confirm-value" x-text="holesCount ? holesCount + ' holes' : '—'"></span>
</div>
</div>
<div class="card" style="margin-top:1rem;">
<h2 style="font-size:0.95rem; color:var(--text-muted); margin-bottom:0.75rem;">Players</h2>
<template x-for="uid in selectedPlayers" :key="uid">
<div style="display:flex; align-items:center; gap:0.75rem; padding:0.6rem 0; border-bottom:1px solid var(--border);">
{# Avatar — rendered per player via Alpine #}
<template x-if="getPlayer(uid).avatar && getPlayer(uid).avatar.startsWith('/')">
<img :src="getPlayer(uid).avatar"
style="width:2rem;height:2rem;border-radius:50%;object-fit:cover;flex-shrink:0;">
</template>
<template x-if="!(getPlayer(uid).avatar && getPlayer(uid).avatar.startsWith('/'))">
<span style="font-size:1.4rem;flex-shrink:0;" x-text="getPlayer(uid).avatar || '👤'"></span>
</template>
<div style="flex:1; min-width:0;">
<div style="font-weight:500;" x-text="getPlayer(uid).name"></div>
<div style="font-size:0.8rem; color:var(--text-muted);">
<span x-text="'HCP ' + (getPlayer(uid).handicap != null ? getPlayer(uid).handicap : '—')"></span>
<template x-if="getTee(uid)">
<span x-text="' · ' + getTee(uid).name + ' (Slope\u00a0' + getTee(uid).slope + ' · Rating\u00a0' + getTee(uid).rating + ')'"></span>
</template>
</div>
</div>
</div>
</template>
</div>
<div style="display:flex; gap:0.75rem; margin-top:1rem;">
<button type="button" @click="backFromConfirm()"
style="flex:1; padding:0.85rem; border-radius:0.65rem; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:1rem; cursor:pointer;">← Back</button>
<button type="submit" class="btn btn-primary" style="flex:2;">Start game ⛳</button> <button type="submit" class="btn btn-primary" style="flex:2;">Start game ⛳</button>
</div> </div>
</div> </div>
</form> </form>