Add Skins format, GolfCourseAPI import, and UX improvements
- Add Skins game format with optional carryover, mirroring BBB scoring pattern
- Add GolfCourseAPI integration for course search and import (httpx)
- Unwrap {"course": {}} API response envelope on import
- Add searchable course combobox and player filter to new game form
- Redirect to summary page after finishing a game
- Fix auth to allow registered users without a pending invitation
- Fix hardcoded BBB format in create_game; store carryover flag in DB
- Add game summary/scorecard view for completed games
- Add live games social stream to dashboard
- Add course name + subtitle to recent game cards
- Reorder nav: Home → Games → Admin → Profile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,13 @@
|
||||
{% block extra_head %}
|
||||
<script>
|
||||
const TEES_BY_COURSE = {{ tees_by_course | tojson }};
|
||||
const COURSES_DATA = {{ courses | tojson }};
|
||||
</script>
|
||||
<style>
|
||||
.picker-item { padding:0.65rem 0.75rem; cursor:pointer; border-bottom:1px solid var(--border); }
|
||||
.picker-item:last-child { border-bottom:none; }
|
||||
.picker-item.selected { background:var(--surface2); }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -12,7 +18,26 @@
|
||||
x-data="{
|
||||
step: 1,
|
||||
courseId: '',
|
||||
courseLabel: '',
|
||||
courseOpen: false,
|
||||
courseSearch: '',
|
||||
format: 'bingo_bango_bongo',
|
||||
selectedPlayers: ['{{ user.id }}'],
|
||||
playerSearch: '',
|
||||
filteredCourses() {
|
||||
const q = this.courseSearch.toLowerCase();
|
||||
if (!q) return COURSES_DATA;
|
||||
return COURSES_DATA.filter(c =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
(c.location || '').toLowerCase().includes(q)
|
||||
);
|
||||
},
|
||||
selectCourse(c) {
|
||||
this.courseId = c.id;
|
||||
this.courseLabel = c.name + (c.location ? ' · ' + c.location : '') + ' (' + c.holes + ' holes)';
|
||||
this.courseOpen = false;
|
||||
this.courseSearch = '';
|
||||
},
|
||||
togglePlayer(id) {
|
||||
const idx = this.selectedPlayers.indexOf(id);
|
||||
if (idx === -1) this.selectedPlayers.push(id);
|
||||
@@ -38,23 +63,65 @@
|
||||
<label>Format</label>
|
||||
<div style="display:flex; gap:0.75rem; flex-wrap:wrap;">
|
||||
<label class="radio-card" style="flex:1;">
|
||||
<input type="radio" name="format" value="bingo_bango_bongo" checked required>
|
||||
<input type="radio" name="format" value="bingo_bango_bongo" checked required
|
||||
@change="format = $event.target.value">
|
||||
<span>Bingo Bango Bongo</span>
|
||||
</label>
|
||||
<label class="radio-card" style="flex:1;">
|
||||
<input type="radio" name="format" value="skins"
|
||||
@change="format = $event.target.value">
|
||||
<span>Skins</span>
|
||||
</label>
|
||||
</div>
|
||||
<div x-show="format === 'skins'" style="margin-top:0.75rem;">
|
||||
<label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
<input type="checkbox" name="carryover" value="1" checked style="width:auto;">
|
||||
<span style="font-weight:400; color:var(--text-muted);">Carryover (ties carry to next hole)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="course_id">Course</label>
|
||||
{# ── Course picker ── #}
|
||||
<div class="form-group" @click.outside="courseOpen = false; courseSearch = ''">
|
||||
<label>Course</label>
|
||||
{% if courses %}
|
||||
<select id="course_id" name="course_id" required
|
||||
@change="courseId = $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;">
|
||||
<option value="" disabled selected>Select a course…</option>
|
||||
{% for c in courses %}
|
||||
<option value="{{ c.id }}">{{ c.name }}{% if c.location %} · {{ c.location }}{% endif %} ({{ c.holes }} holes)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="hidden" name="course_id" :value="courseId">
|
||||
|
||||
<button type="button"
|
||||
@click="courseOpen = !courseOpen; if (courseOpen) $nextTick(() => $refs.courseSearch && $refs.courseSearch.focus())"
|
||||
style="width:100%; display:flex; align-items:center; gap:0.5rem; padding:0.65rem 0.75rem; border-radius:0.5rem; border:1px solid var(--border); background:var(--surface); color:var(--text); font-size:1rem; cursor:pointer; text-align:left;">
|
||||
<span style="flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;"
|
||||
:style="courseId ? '' : 'color:var(--text-muted)'"
|
||||
x-text="courseLabel || 'Select a course…'"></span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="flex-shrink:0; transition:transform 0.15s;"
|
||||
:style="courseOpen ? 'transform:rotate(180deg)' : ''">
|
||||
<path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div x-show="courseOpen"
|
||||
style="border:1px solid var(--border); border-radius:0.5rem; background:var(--surface); margin-top:0.25rem; overflow:hidden; box-shadow:0 4px 16px rgba(0,0,0,0.15);">
|
||||
<div style="padding:0.5rem; border-bottom:1px solid var(--border);">
|
||||
<input type="text" x-model="courseSearch" x-ref="courseSearch"
|
||||
placeholder="Search by name or location…"
|
||||
@keydown.escape="courseOpen = false; courseSearch = ''"
|
||||
style="width:100%; padding:0.5rem 0.65rem; border-radius:0.4rem; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:0.95rem; box-sizing:border-box;">
|
||||
</div>
|
||||
<div style="max-height:14rem; overflow-y:auto; -webkit-overflow-scrolling:touch;">
|
||||
<template x-for="c in filteredCourses()" :key="c.id">
|
||||
<div class="picker-item" @click="selectCourse(c)" :class="courseId === c.id ? 'selected' : ''">
|
||||
<div x-text="c.name" style="font-weight:500;"></div>
|
||||
<div x-text="(c.location ? c.location + ' · ' : '') + c.holes + ' holes'"
|
||||
style="font-size:0.8rem; color:var(--text-muted);"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="filteredCourses().length === 0"
|
||||
style="padding:0.75rem; text-align:center; color:var(--text-muted); font-size:0.9rem;">
|
||||
No courses found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<p style="color:var(--text-muted); font-size:0.9rem;">No courses available. Ask the admin to add courses first.</p>
|
||||
<input type="hidden" name="course_id" value="">
|
||||
@@ -75,11 +142,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Player picker ── #}
|
||||
<div class="form-group">
|
||||
<label>Players</label>
|
||||
<input type="text" x-model="playerSearch" placeholder="Filter players…"
|
||||
style="width:100%; padding:0.5rem 0.65rem; border-radius:0.4rem; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:0.95rem; margin-bottom:0.5rem; box-sizing:border-box;">
|
||||
<div class="player-list">
|
||||
{% for u in all_users %}
|
||||
<label class="player-option">
|
||||
<label class="player-option"
|
||||
data-name="{{ u.name | lower }}"
|
||||
x-show="{% if u.id == user.id %}true{% else %}!playerSearch || $el.dataset.name.includes(playerSearch.toLowerCase()){% endif %}">
|
||||
<input type="checkbox" name="players" value="{{ u.id }}"
|
||||
{% if u.id == user.id %}checked disabled{% endif %}
|
||||
@change="togglePlayer('{{ u.id }}')"
|
||||
@@ -98,6 +170,7 @@
|
||||
</div>
|
||||
<input type="hidden" name="players" value="{{ user.id }}">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<button type="button" x-show="hasTees()" class="btn btn-primary" @click="step = 2">Next →</button>
|
||||
|
||||
Reference in New Issue
Block a user