Files
SkinsPins/app/templates/games/new_game.html
T
Rolf c51c872c33 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>
2026-03-22 09:18:04 +01:00

215 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}New Game Skins &amp; Pins{% endblock %}
{% 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 %}
<div class="page"
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);
else if (id !== '{{ user.id }}') this.selectedPlayers.splice(idx, 1);
},
hasTees() { return (TEES_BY_COURSE[this.courseId] || []).length > 0; },
tees() { return TEES_BY_COURSE[this.courseId] || []; }
}">
<div class="page-header">
<h1>New Game</h1>
</div>
{% if error %}<div class="alert alert-error">{{ error }}</div>{% endif %}
<form method="post" action="/games/new">
{# ── Step 1: Format, course, holes, players ── #}
<div x-show="step === 1">
<div class="card">
<div class="form-group">
<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
@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>
{# ── Course picker ── #}
<div class="form-group" @click.outside="courseOpen = false; courseSearch = ''">
<label>Course</label>
{% if courses %}
<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="">
{% endif %}
</div>
<div class="form-group">
<label>Holes</label>
<div style="display:flex; gap:0.75rem;">
<label class="radio-card">
<input type="radio" name="holes_count" value="9" required>
<span>9 holes</span>
</label>
<label class="radio-card">
<input type="radio" name="holes_count" value="18">
<span>18 holes</span>
</label>
</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"
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 }}')"
:checked="selectedPlayers.includes('{{ u.id }}')">
<span class="player-avatar">
{% if u.avatar and u.avatar.startswith('/') %}
<img src="{{ u.avatar }}" style="width:1.3rem;height:1.3rem;border-radius:50%;object-fit:cover;vertical-align:middle;">
{% else %}
{{ u.avatar or '👤' }}
{% endif %}
</span>
<span class="player-name">{{ u.name }}</span>
{% if u.id == user.id %}<span style="color:var(--text-muted); font-size:0.75rem;">you</span>{% endif %}
</label>
{% endfor %}
</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>
<button type="submit" x-show="!hasTees()" class="btn btn-primary">Start game ⛳</button>
</div>
{# ── Step 2: Tee selection ── #}
<div x-show="step === 2" style="display:none;">
<div class="card">
<h2 style="font-size:0.95rem; color:var(--text-muted); margin-bottom:1rem;">Select tees</h2>
{% for u in all_users %}
<div class="form-group" x-show="selectedPlayers.includes('{{ u.id }}')">
<label>
{% if u.avatar and u.avatar.startswith('/') %}
<img src="{{ u.avatar }}" style="width:1.1rem;height:1.1rem;border-radius:50%;object-fit:cover;vertical-align:middle;margin-right:0.3rem;">
{% else %}
{{ u.avatar or '👤' }}
{% endif %}
{{ u.name }}
</label>
<select name="tee_{{ u.id }}"
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>
<template x-for="t in tees()" :key="t.id">
<option :value="t.id" x-text="t.name + ' (Slope ' + t.slope + ' · Rating ' + t.rating + ')'"></option>
</template>
</select>
</div>
{% endfor %}
</div>
<div style="display:flex; gap:0.75rem;">
<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>
<button type="submit" class="btn btn-primary" style="flex:2;">Start game ⛳</button>
</div>
</div>
</form>
</div>
{% endblock %}