8601584099
- Playing handicap: computed at game creation (WHS formula), stored on game_players alongside per-hole strokes allocation; displayed on game summary with HC card and / markers on scorecard (only when SI defined); live game shows +N per player card when SI exists, HC total otherwise; new game confirmation previews computed HC per player - Login: 6-digit code sent alongside magic link in every auth email; check_email page has auto-advancing digit inputs with paste support; new POST /auth/verify-code route handles code verification - Game view: Score/Card toggle fixed with position:fixed to always show above nav bar on mobile; swipe on score view changes holes, swipe on scorecard switches tabs; dirty tracking prevents saving unmodified holes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
271 lines
12 KiB
HTML
271 lines
12 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ game.course_name or 'Game' }} – Skins & Pins{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="page" x-data="{ view: 'strokes' }">
|
||
<div class="page-header" style="display:flex; align-items:center; justify-content:space-between;">
|
||
<a href="javascript:history.back()" style="color:var(--text-muted); font-size:0.9rem;">← Back</a>
|
||
<div style="text-align:center;">
|
||
<div style="font-weight:600;">{{ game.course_name or 'Bingo Bango Bongo' }}</div>
|
||
<div style="font-size:0.8rem; color:var(--text-muted);">{{ game.holes_count }} holes · {{ game.created_at[:10] }}</div>
|
||
</div>
|
||
<div style="width:3rem;"></div>
|
||
</div>
|
||
|
||
{# Leaderboard #}
|
||
<div class="card" style="padding:0.75rem 1rem;">
|
||
{% for p in results %}
|
||
{% if game.format == 'skins' %}
|
||
{% set score_val = skins_won.get(p.id, 0) %}
|
||
{% set score_label = score_val | string + ' skin' + ('s' if score_val != 1 else '') %}
|
||
{% else %}
|
||
{% set t = totals.get(p.id, {}) %}
|
||
{% set score_val = t.total %}
|
||
{% set score_label = t.total | round(1) | string + ' pts' %}
|
||
{% endif %}
|
||
<div class="result-row {% if loop.first %}result-winner{% endif %}" style="padding:0.4rem 0;">
|
||
<span class="result-rank">{% if loop.first %}🏆{% else %}{{ loop.index }}{% endif %}</span>
|
||
<span class="result-avatar">
|
||
{% if p.avatar and p.avatar.startswith('/') %}
|
||
<img src="{{ p.avatar }}" style="width:1.4rem;height:1.4rem;border-radius:50%;object-fit:cover;vertical-align:middle;">
|
||
{% else %}
|
||
{{ p.avatar or '👤' }}
|
||
{% endif %}
|
||
</span>
|
||
<a href="/users/{{ p.id }}" style="flex:1; color:inherit; text-decoration:none;">{{ p.name }}</a>
|
||
<span class="result-pts">{{ score_label }}</span>
|
||
{% if game.format == 'bingo_bango_bongo' %}
|
||
<span style="font-size:0.75rem; color:var(--text-muted); margin-left:0.5rem;">
|
||
B{{ t.bingo | int }}/{{ t.bango | round(1) }}/{{ t.bongo | int }}
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% if game.format == 'skins' and unclaimed %}
|
||
<div style="font-size:0.8rem; color:var(--text-muted); margin-top:0.5rem; text-align:center;">
|
||
{{ unclaimed }} skin{{ 's' if unclaimed != 1 else '' }} unclaimed
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{# Playing handicaps #}
|
||
{% if playing_handicaps and playing_handicaps.values() | select | list %}
|
||
<div class="card" style="margin-top:0.75rem; padding:0.75rem 1rem;">
|
||
<div style="font-size:0.8rem; color:var(--text-muted); font-weight:500; margin-bottom:0.5rem; text-transform:uppercase; letter-spacing:0.05em;">Playing Handicaps</div>
|
||
{% for p in players %}
|
||
{% set ph = playing_handicaps.get(p.id) %}
|
||
{% if ph is not none %}
|
||
<div style="display:flex; align-items:center; gap:0.6rem; padding:0.35rem 0; {% if not loop.last %}border-bottom:1px solid var(--border);{% endif %} font-size:0.9rem;">
|
||
<span>
|
||
{% if p.avatar and p.avatar.startswith('/') %}
|
||
<img src="{{ p.avatar }}" style="width:1.3rem;height:1.3rem;border-radius:50%;object-fit:cover;vertical-align:middle;">
|
||
{% else %}{{ p.avatar or '👤' }}{% endif %}
|
||
</span>
|
||
<span style="flex:1; font-weight:500;">{{ p.name }}</span>
|
||
{% if p.handicap_snapshot is not none %}
|
||
<span style="color:var(--text-muted);">HI {{ p.handicap_snapshot }}</span>
|
||
{% endif %}
|
||
<span style="font-weight:600; color:var(--accent); min-width:3rem; text-align:right;">HC {{ ph }}</span>
|
||
{% if p.tee_name %}
|
||
<span style="color:var(--text-muted); font-size:0.8rem; min-width:4rem; text-align:right;">{{ p.tee_name }}</span>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{# Scorecard toggle #}
|
||
<div class="scorecard-toggle" style="margin-top:1rem;">
|
||
<button @click="view = 'strokes'" :class="{ active: view === 'strokes' }">Strokes</button>
|
||
{% if game.format == 'skins' %}
|
||
<button @click="view = 'skins'" :class="{ active: view === 'skins' }">Skins</button>
|
||
{% else %}
|
||
<button @click="view = 'points'" :class="{ active: view === 'points' }">Points</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{# Strokes scorecard #}
|
||
<table class="scorecard" x-show="view === 'strokes'">
|
||
<thead>
|
||
<tr>
|
||
<th>Hole</th>
|
||
{% if hole_pars %}<th style="color:var(--text-muted);">Par</th>{% endif %}
|
||
{% for p in players %}
|
||
<th>
|
||
{% if p.avatar and p.avatar.startswith('/') %}
|
||
<img src="{{ p.avatar }}" style="width:1.3rem;height:1.3rem;border-radius:50%;object-fit:cover;vertical-align:middle;">
|
||
{% else %}
|
||
{{ p.avatar or '👤' }}
|
||
{% endif %}<br>
|
||
<a href="/users/{{ p.id }}" style="font-size:0.7rem; color:inherit; text-decoration:none;">{{ p.name.split()[0] }}</a>
|
||
</th>
|
||
{% endfor %}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for h in holes %}
|
||
<tr>
|
||
<td class="hole-num">{{ h }}</td>
|
||
{% if hole_pars %}<td class="score-cell" style="color:var(--text-muted);">{{ hole_pars.get(h, '—') }}</td>{% endif %}
|
||
{% for p in players %}
|
||
{% set s = scores.get(h, {}).get(p.id, {}) %}
|
||
{% set strokes = s.strokes if s else 0 %}
|
||
{% set par = hole_pars.get(h, 0) %}
|
||
{% set sr = strokes_per_hole.get(p.id, {}).get(h, 0) %}
|
||
<td class="score-cell">
|
||
{% if strokes %}
|
||
{% if par %}
|
||
{% set diff = strokes - par %}
|
||
{% if strokes == 1 %}<span class="score-hio">{{ strokes }}</span>
|
||
{% elif diff <= -2 %}<span class="score-eagle">{{ strokes }}</span>
|
||
{% elif diff == -1 %}<span class="score-birdie">{{ strokes }}</span>
|
||
{% elif diff == 1 %}<span class="score-bogey">{{ strokes }}</span>
|
||
{% elif diff >= 2 %}<span class="score-double-bogey">{{ strokes }}</span>
|
||
{% else %}{{ strokes }}{% endif %}
|
||
{% else %}{{ strokes }}{% endif %}
|
||
{% else %}—{% endif %}
|
||
{% if has_stroke_index and sr > 0 %}<sup style="color:var(--accent); font-size:0.6em; line-height:0;">{{ '/' * sr }}</sup>{% endif %}
|
||
</td>
|
||
{% endfor %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="totals-row">
|
||
<td>Total</td>
|
||
{% if hole_pars %}<td style="color:var(--text-muted);">{{ hole_pars.values() | sum }}</td>{% endif %}
|
||
{% for p in players %}
|
||
{% set stroke_total = namespace(val=0) %}
|
||
{% for h in holes %}
|
||
{% set s = scores.get(h, {}).get(p.id, {}) %}
|
||
{% if s and s.strokes %}{% set stroke_total.val = stroke_total.val + s.strokes %}{% endif %}
|
||
{% endfor %}
|
||
<td>{{ stroke_total.val or '—' }}</td>
|
||
{% endfor %}
|
||
</tr>
|
||
{% set any_hc = playing_handicaps and playing_handicaps.values() | select | list %}
|
||
{% if any_hc %}
|
||
<tr>
|
||
<td style="color:var(--text-muted); font-size:0.8rem;">HC</td>
|
||
{% if hole_pars %}<td></td>{% endif %}
|
||
{% for p in players %}
|
||
{% set ph = playing_handicaps.get(p.id) %}
|
||
<td style="color:var(--text-muted); font-size:0.8rem;">{{ ph if ph is not none else '—' }}</td>
|
||
{% endfor %}
|
||
</tr>
|
||
{% endif %}
|
||
</tfoot>
|
||
</table>
|
||
|
||
{# Points scorecard #}
|
||
<table class="scorecard" x-show="view === 'points'">
|
||
<thead>
|
||
<tr>
|
||
<th>Hole</th>
|
||
{% for p in players %}
|
||
<th>
|
||
{% if p.avatar and p.avatar.startswith('/') %}
|
||
<img src="{{ p.avatar }}" style="width:1.3rem;height:1.3rem;border-radius:50%;object-fit:cover;vertical-align:middle;">
|
||
{% else %}
|
||
{{ p.avatar or '👤' }}
|
||
{% endif %}<br>
|
||
<a href="/users/{{ p.id }}" style="font-size:0.7rem; color:inherit; text-decoration:none;">{{ p.name.split()[0] }}</a>
|
||
</th>
|
||
{% endfor %}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for h in holes %}
|
||
<tr>
|
||
<td class="hole-num">{{ h }}</td>
|
||
{% for p in players %}
|
||
{% set s = scores.get(h, {}).get(p.id, {}) %}
|
||
{% set pts = (s.bingo + s.bango + s.bongo) if s else 0 %}
|
||
<td class="score-cell">{{ pts | round(1) if pts else '—' }}</td>
|
||
{% endfor %}
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="totals-row">
|
||
<td>Total</td>
|
||
{% for p in players %}
|
||
<td>{{ totals.get(p.id, {}).get('total', 0) | round(1) }}</td>
|
||
{% endfor %}
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
|
||
{# Skins scorecard #}
|
||
{% if game.format == 'skins' %}
|
||
<table class="scorecard" x-show="view === 'skins'">
|
||
<thead>
|
||
<tr>
|
||
<th>Hole</th>
|
||
{% if hole_pars %}<th style="color:var(--text-muted);">Par</th>{% endif %}
|
||
{% for p in players %}
|
||
<th>
|
||
{% if p.avatar and p.avatar.startswith('/') %}
|
||
<img src="{{ p.avatar }}" style="width:1.3rem;height:1.3rem;border-radius:50%;object-fit:cover;vertical-align:middle;">
|
||
{% else %}
|
||
{{ p.avatar or '👤' }}
|
||
{% endif %}<br>
|
||
<a href="/users/{{ p.id }}" style="font-size:0.7rem; color:inherit; text-decoration:none;">{{ p.name.split()[0] }}</a>
|
||
</th>
|
||
{% endfor %}
|
||
<th style="color:var(--text-muted);">Skin</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for h in holes %}
|
||
{% set hr = skins_hole_results.get(h) %}
|
||
<tr>
|
||
<td class="hole-num">{{ h }}</td>
|
||
{% if hole_pars %}<td class="score-cell" style="color:var(--text-muted);">{{ hole_pars.get(h, '—') }}</td>{% endif %}
|
||
{% for p in players %}
|
||
{% set s = scores.get(h, {}).get(p.id, {}) %}
|
||
{% set strokes = s.strokes if s else 0 %}
|
||
{% set par = hole_pars.get(h, 0) %}
|
||
{% set sr = strokes_per_hole.get(p.id, {}).get(h, 0) %}
|
||
<td class="score-cell">
|
||
{% if strokes %}
|
||
{% set is_winner = hr and hr.winner == p.id %}
|
||
{% if par %}
|
||
{% set diff = strokes - par %}
|
||
{% if strokes == 1 %}<span class="score-hio{% if is_winner %} skin-winner{% endif %}">{{ strokes }}</span>
|
||
{% elif diff <= -2 %}<span class="score-eagle{% if is_winner %} skin-winner{% endif %}">{{ strokes }}</span>
|
||
{% elif diff == -1 %}<span class="score-birdie{% if is_winner %} skin-winner{% endif %}">{{ strokes }}</span>
|
||
{% elif diff == 1 %}<span class="score-bogey{% if is_winner %} skin-winner{% endif %}">{{ strokes }}</span>
|
||
{% elif diff >= 2 %}<span class="score-double-bogey{% if is_winner %} skin-winner{% endif %}">{{ strokes }}</span>
|
||
{% else %}<span{% if is_winner %} class="skin-winner"{% endif %}>{{ strokes }}</span>{% endif %}
|
||
{% else %}<span{% if is_winner %} class="skin-winner"{% endif %}>{{ strokes }}</span>{% endif %}
|
||
{% else %}—{% endif %}
|
||
{% if has_stroke_index and sr > 0 %}<sup style="color:var(--accent); font-size:0.6em; line-height:0;">{{ '/' * sr }}</sup>{% endif %}
|
||
</td>
|
||
{% endfor %}
|
||
<td class="score-cell">
|
||
{% if hr %}
|
||
{% if hr.winner %}✓{% if hr.pot > 1 %} {{ hr.pot }}{% endif %}
|
||
{% else %}→ {{ hr.pot }}{% endif %}
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="totals-row">
|
||
<td>Skins</td>
|
||
{% if hole_pars %}<td></td>{% endif %}
|
||
{% for p in players %}
|
||
<td>{{ skins_won.get(p.id, 0) }}</td>
|
||
{% endfor %}
|
||
<td>{% if unclaimed %}({{ unclaimed }}){% endif %}</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
{% endif %}
|
||
|
||
</div>
|
||
{% endblock %}
|