Add full SkinsPins application
- FastAPI + SQLite + Alpine.js + HTMX PWA for Bingo Bango Bongo golf scoring - Passwordless magic link auth with email (aiosmtplib) and admin invite system - Real-time score entry via WebSocket with drag-and-drop player ordering - Course management with per-hole par and stroke index - Scorecard with golf score markers (birdie, eagle, bogey, etc.) - Two-step new game form with tee selection per player - Dashboard with stats, live games social stream, and recent game history - Game summary view (read-only scorecard with leaderboard) - User profile pages with win stats - PWA install support with generated PNG icons - Admin panel for users, courses, games, and invitations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}New Game – Skins & Pins{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script>
|
||||
const TEES_BY_COURSE = {{ tees_by_course | tojson }};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page"
|
||||
x-data="{
|
||||
step: 1,
|
||||
courseId: '',
|
||||
selectedPlayers: ['{{ user.id }}'],
|
||||
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>
|
||||
<span>Bingo Bango Bongo</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="course_id">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>
|
||||
{% 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>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Players</label>
|
||||
<div class="player-list">
|
||||
{% for u in all_users %}
|
||||
<label class="player-option">
|
||||
<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 %}
|
||||
Reference in New Issue
Block a user