Add playing handicap, 6-digit login code, and game UX improvements
- 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>
This commit is contained in:
@@ -108,7 +108,9 @@
|
||||
</div>
|
||||
<div class="form-group" style="margin:0; width:7rem;">
|
||||
<label style="font-size:0.8rem;">Rating</label>
|
||||
<input type="number" step="0.1" name="rating" value="{{ t.rating }}" min="60" max="80" required style="padding:0.4rem 0.6rem;">
|
||||
<input type="number" step="0.1" name="rating" value="{{ t.rating }}"
|
||||
min="{{ '29' if course.holes == 9 else '60' }}" max="{{ '40' if course.holes == 9 else '80' }}"
|
||||
required style="padding:0.4rem 0.6rem;">
|
||||
</div>
|
||||
<div style="display:flex; gap:0.4rem; padding-bottom:0.1rem;">
|
||||
<button type="submit" class="btn btn-primary" style="padding:0.4rem 0.75rem; font-size:0.85rem;">Save</button>
|
||||
@@ -138,7 +140,10 @@
|
||||
</div>
|
||||
<div class="form-group" style="margin:0; width:7rem;">
|
||||
<label style="font-size:0.8rem;">Rating</label>
|
||||
<input type="number" step="0.1" name="rating" placeholder="72.0" min="60" max="80" required style="padding:0.4rem 0.6rem;">
|
||||
<input type="number" step="0.1" name="rating"
|
||||
placeholder="{{ '35.0' if course.holes == 9 else '72.0' }}"
|
||||
min="{{ '29' if course.holes == 9 else '60' }}" max="{{ '40' if course.holes == 9 else '80' }}"
|
||||
required style="padding:0.4rem 0.6rem;">
|
||||
</div>
|
||||
<div style="padding-bottom:0.1rem;">
|
||||
<button type="submit" class="btn btn-primary" style="padding:0.4rem 0.75rem; font-size:0.85rem;">Add tee</button>
|
||||
|
||||
@@ -12,17 +12,107 @@
|
||||
<div class="login-container">
|
||||
<div class="login-logo">📬</div>
|
||||
<h1 class="login-title">Check your email</h1>
|
||||
<p class="login-subtitle">We sent a login link to <strong>{{ email }}</strong></p>
|
||||
<p class="login-subtitle">We sent a code and a login link to <strong>{{ email }}</strong></p>
|
||||
|
||||
<div class="login-card card">
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem; text-align: center;">
|
||||
The link expires in 15 minutes.<br>You can close this tab.
|
||||
{% if error %}
|
||||
<p style="color:var(--danger); font-size:0.9rem; text-align:center; margin-bottom:1rem;">{{ error }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/auth/verify-code" autocomplete="off"
|
||||
style="display:flex; flex-direction:column; gap:1rem;">
|
||||
<input type="hidden" name="email" value="{{ email }}">
|
||||
|
||||
<label style="font-size:0.85rem; color:var(--text-muted); text-align:center;">
|
||||
Enter your 6-digit code
|
||||
</label>
|
||||
|
||||
<div style="display:flex; gap:0.5rem; justify-content:center;" id="code-inputs">
|
||||
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]"
|
||||
class="code-digit" tabindex="1" autocomplete="one-time-code">
|
||||
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]"
|
||||
class="code-digit" tabindex="2">
|
||||
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]"
|
||||
class="code-digit" tabindex="3">
|
||||
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]"
|
||||
class="code-digit" tabindex="4">
|
||||
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]"
|
||||
class="code-digit" tabindex="5">
|
||||
<input type="text" inputmode="numeric" maxlength="1" pattern="[0-9]"
|
||||
class="code-digit" tabindex="6">
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="code" id="code-value">
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="code-submit" disabled>Verify code</button>
|
||||
</form>
|
||||
|
||||
<p style="color:var(--text-muted); font-size:0.85rem; text-align:center; margin-top:1rem;">
|
||||
The code and link expire in 15 minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href="/login" style="color: var(--text-muted); font-size: 0.85rem; margin-top: 1.5rem;">
|
||||
<a href="/login" style="color:var(--text-muted); font-size:0.85rem; margin-top:1.5rem;">
|
||||
← Use a different email
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.code-digit {
|
||||
width: 3rem;
|
||||
height: 3.5rem;
|
||||
text-align: center;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
border: 2px solid var(--surface2);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
caret-color: transparent;
|
||||
}
|
||||
.code-digit:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const digits = [...document.querySelectorAll('.code-digit')];
|
||||
const hidden = document.getElementById('code-value');
|
||||
const submit = document.getElementById('code-submit');
|
||||
|
||||
function sync() {
|
||||
const val = digits.map(d => d.value).join('');
|
||||
hidden.value = val;
|
||||
submit.disabled = val.length < 6;
|
||||
}
|
||||
|
||||
digits.forEach((el, i) => {
|
||||
el.addEventListener('input', () => {
|
||||
el.value = el.value.replace(/\D/g, '').slice(-1);
|
||||
sync();
|
||||
if (el.value && i < digits.length - 1) digits[i + 1].focus();
|
||||
});
|
||||
|
||||
el.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' && !el.value && i > 0) {
|
||||
digits[i - 1].focus();
|
||||
digits[i - 1].value = '';
|
||||
sync();
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener('paste', (e) => {
|
||||
e.preventDefault();
|
||||
const pasted = (e.clipboardData || window.clipboardData).getData('text').replace(/\D/g, '');
|
||||
digits.forEach((d, j) => d.value = pasted[j] || '');
|
||||
sync();
|
||||
const next = Math.min(pasted.length, digits.length - 1);
|
||||
digits[next].focus();
|
||||
});
|
||||
});
|
||||
|
||||
digits[0].focus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div id="game-app"
|
||||
x-data="gameApp()"
|
||||
x-init="init()"
|
||||
style="height: calc(100vh - 64px); display:flex; flex-direction:column; overflow:hidden;">
|
||||
style="position:fixed; top:0; left:0; right:0; bottom:calc(var(--nav-height) + env(safe-area-inset-bottom)); display:flex; flex-direction:column; overflow:hidden;">
|
||||
|
||||
{# ── Header ── #}
|
||||
<div class="game-header">
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="hole-label" x-text="view === 'score' ? 'Hole ' + currentHole + ' / {{ game.holes_count }}' : (cardView === 'strokes' ? 'Strokes' : (GAME_FORMAT === 'skins' ? 'Skins' : 'Points'))"></div>
|
||||
<div style="font-size:0.72rem; color:var(--text-muted); margin-top:0.1rem;">
|
||||
<template x-if="view === 'score'">
|
||||
<span x-text="(HOLE_PARS[currentHole] ? 'Par ' + HOLE_PARS[currentHole] : '') + (HOLE_PARS[currentHole] && HOLE_SIS[currentHole] ? ' · ' : '') + (HOLE_SIS[currentHole] ? 'SI ' + HOLE_SIS[currentHole] : '')"></span>
|
||||
<span x-text="(HOLE_PARS[currentHole] ? 'Par ' + HOLE_PARS[currentHole] : '') + (HOLE_SIS[currentHole] ? ' · SI ' + HOLE_SIS[currentHole] : '')"></span>
|
||||
</template>
|
||||
<template x-if="view === 'card'">
|
||||
<span>{{ game.course_name or '' }}</span>
|
||||
@@ -26,6 +26,7 @@
|
||||
</div>
|
||||
<button @click="view === 'score' ? nextHole() : cardView = (GAME_FORMAT === 'skins' ? 'skins' : 'points')" class="icon-btn"
|
||||
:disabled="view === 'score' ? currentHole >= {{ game.holes_count }} : cardView !== 'strokes'">›</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +64,14 @@
|
||||
@touchmove.stop.prevent="touchDragMove($event)"
|
||||
@touchend.stop="touchDragEnd()">⠿</span>
|
||||
<span x-text="p.name"></span>
|
||||
<template x-if="HAS_STROKE_INDEX && (STROKES_PER_HOLE[p.id] || {})[currentHole] > 0">
|
||||
<span style="font-size:0.7rem; color:var(--accent); margin-left:0.35rem; font-weight:600;"
|
||||
x-text="'+' + (STROKES_PER_HOLE[p.id] || {})[currentHole]"></span>
|
||||
</template>
|
||||
<template x-if="!HAS_STROKE_INDEX && p.playing_handicap != null">
|
||||
<span style="font-size:0.7rem; color:var(--text-muted); margin-left:0.35rem;"
|
||||
x-text="'HC ' + p.playing_handicap"></span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="psc-controls">
|
||||
<span class="stroke-controls">
|
||||
@@ -110,17 +119,6 @@
|
||||
{# Scorecard panel #}
|
||||
<div class="swipe-panel" :class="{ active: view === 'card' }">
|
||||
<div class="panel-scroll">
|
||||
{# Scorecard sub-toggle #}
|
||||
<div class="scorecard-toggle">
|
||||
<button @click="cardView = 'strokes'" :class="{ active: cardView === 'strokes' }">Strokes</button>
|
||||
<template x-if="GAME_FORMAT === 'bingo_bango_bongo'">
|
||||
<button @click="cardView = 'points'" :class="{ active: cardView === 'points' }">Points</button>
|
||||
</template>
|
||||
<template x-if="GAME_FORMAT === 'skins'">
|
||||
<button @click="cardView = 'skins'" :class="{ active: cardView === 'skins' }">Skins</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{# Strokes scorecard #}
|
||||
<table class="scorecard" x-show="cardView === 'strokes'">
|
||||
<thead>
|
||||
@@ -135,8 +133,6 @@
|
||||
{{ 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>
|
||||
{% set ph = playing_handicaps.get(p.id) %}
|
||||
{% if ph is not none %}<br><span style="font-size:0.65rem; color:var(--text-muted);">HCP {{ ph }}</span>{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
@@ -147,9 +143,11 @@
|
||||
<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 sr = strokes_per_hole.get(p.id, {}).get(h, 0) %}
|
||||
<td class="score-cell">
|
||||
<span x-text="holeStrokesStr({{ h }}, '{{ p.id }}')"
|
||||
:class="holeScoreClass({{ h }}, '{{ p.id }}')"></span>
|
||||
{% 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>
|
||||
@@ -179,8 +177,6 @@
|
||||
{{ 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>
|
||||
{% set ph = playing_handicaps.get(p.id) %}
|
||||
{% if ph is not none %}<br><span style="font-size:0.65rem; color:var(--text-muted);">HCP {{ ph }}</span>{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
@@ -232,9 +228,11 @@
|
||||
<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 sr = strokes_per_hole.get(p.id, {}).get(h, 0) %}
|
||||
<td class="score-cell">
|
||||
<span x-text="holeStrokesStr({{ h }}, '{{ p.id }}')"
|
||||
:class="[holeScoreClass({{ h }}, '{{ p.id }}'), isSkinWinner({{ h }}, '{{ p.id }}') ? 'skin-winner' : '']"></span>
|
||||
{% 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" x-text="skinHoleLabel({{ h }})"></td>
|
||||
@@ -282,6 +280,8 @@ const GAME_FORMAT = "{{ game.format }}";
|
||||
const SKINS_CARRYOVER = {{ carryover | tojson }};
|
||||
const HOLE_PARS = {{ hole_pars | tojson }};
|
||||
const HOLE_SIS = {{ hole_stroke_indices | tojson }};
|
||||
const STROKES_PER_HOLE = {{ strokes_per_hole | tojson }};
|
||||
const HAS_STROKE_INDEX = {{ has_stroke_index | tojson }};
|
||||
const PLAYERS_DATA = {{ players | tojson }};
|
||||
|
||||
// Initial scores from server
|
||||
@@ -297,6 +297,7 @@ function gameApp() {
|
||||
scores: {}, // { hole: { uid: {strokes,bingo,bango,bongo} } }
|
||||
totals: {},
|
||||
skipped: new Set(),
|
||||
dirty: new Set(),
|
||||
ws: null,
|
||||
touchStartX: 0,
|
||||
dragSrc: null,
|
||||
@@ -350,12 +351,8 @@ function gameApp() {
|
||||
adjustStrokes(hole, uid, delta) {
|
||||
this._ensurePlayer(hole, uid);
|
||||
const cur = this.scores[hole][uid].strokes;
|
||||
if (!cur) {
|
||||
// First press: start at par for this hole
|
||||
this.scores[hole][uid].strokes = HOLE_PARS[hole] || 4;
|
||||
} else {
|
||||
this.scores[hole][uid].strokes = Math.max(1, cur + delta);
|
||||
}
|
||||
this.scores[hole][uid].strokes = cur ? Math.max(1, cur + delta) : (HOLE_PARS[hole] || 4);
|
||||
this.dirty.add(hole);
|
||||
},
|
||||
|
||||
setBingo(hole, uid) {
|
||||
@@ -363,6 +360,7 @@ function gameApp() {
|
||||
const current = this.scores[hole][uid].bingo;
|
||||
for (const p in this.scores[hole]) this.scores[hole][p].bingo = 0;
|
||||
this.scores[hole][uid].bingo = current ? 0 : 1;
|
||||
this.dirty.add(hole);
|
||||
},
|
||||
|
||||
toggleBango(hole, uid) {
|
||||
@@ -372,6 +370,7 @@ function gameApp() {
|
||||
const winners = Object.entries(this.scores[hole]).filter(([, s]) => s.bango > 0).map(([u]) => u);
|
||||
const share = winners.length ? Math.round(10000 / winners.length) / 10000 : 0;
|
||||
for (const p in this.scores[hole]) this.scores[hole][p].bango = winners.includes(p) ? share : 0;
|
||||
this.dirty.add(hole);
|
||||
},
|
||||
|
||||
setBongo(hole, uid) {
|
||||
@@ -379,9 +378,12 @@ function gameApp() {
|
||||
const current = this.scores[hole][uid].bongo;
|
||||
for (const p in this.scores[hole]) this.scores[hole][p].bongo = 0;
|
||||
this.scores[hole][uid].bongo = current ? 0 : 1;
|
||||
this.dirty.add(hole);
|
||||
},
|
||||
|
||||
saveHole(hole) {
|
||||
if (!this.dirty.has(hole)) return;
|
||||
this.dirty.delete(hole);
|
||||
const holeScores = this.scores[hole] ?? {};
|
||||
const params = new URLSearchParams();
|
||||
params.set('hole', hole);
|
||||
@@ -525,10 +527,12 @@ function gameApp() {
|
||||
if (!this.touchStartX) return;
|
||||
const dx = e.changedTouches[0].screenX - this.touchStartX;
|
||||
this.touchStartX = 0;
|
||||
if (Math.abs(dx) > 60) {
|
||||
const next = dx < 0 ? 'card' : 'score';
|
||||
if (next === 'card' && this.view === 'score') this.saveHole(this.currentHole);
|
||||
this.view = next;
|
||||
if (Math.abs(dx) < 60) return;
|
||||
if (this.view === 'score') {
|
||||
if (dx < 0) this.nextHole(); else this.prevHole();
|
||||
} else {
|
||||
const secondary = GAME_FORMAT === 'skins' ? 'skins' : 'points';
|
||||
this.cardView = dx < 0 ? secondary : 'strokes';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -58,6 +58,15 @@
|
||||
const id = this.selectedTees[uid];
|
||||
return id ? (TEES_BY_COURSE[this.courseId] || []).find(t => t.id === id) : null;
|
||||
},
|
||||
getCourseHandicap(uid) {
|
||||
const player = this.getPlayer(uid);
|
||||
const tee = this.getTee(uid);
|
||||
const course = COURSES_DATA.find(c => c.id === this.courseId);
|
||||
if (!course || !tee || player.handicap == null) return null;
|
||||
const coursePar = (parseInt(this.holesCount) === 9 && course.holes === 18) ? course.par_9 : course.par;
|
||||
if (!coursePar) return null;
|
||||
return Math.round(player.handicap * (tee.slope / 113) + (tee.rating - coursePar));
|
||||
},
|
||||
goToConfirm() { this.step = 3; },
|
||||
backFromConfirm() { this.step = this.hasTees() ? 2 : 1; },
|
||||
formatLabel() {
|
||||
@@ -195,8 +204,10 @@
|
||||
|
||||
</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="goToConfirm()">Next →</button>
|
||||
<div style="position:sticky; bottom:calc(var(--nav-height) + env(safe-area-inset-bottom)); background:var(--bg); padding:0.75rem 0; margin-top:0.5rem;">
|
||||
<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="goToConfirm()">Next →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Step 2: Tee selection ── #}
|
||||
@@ -274,10 +285,13 @@
|
||||
<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>
|
||||
<span x-text="'HI\u00a0' + (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>
|
||||
<template x-if="getCourseHandicap(uid) != null">
|
||||
<span style="color:var(--accent); font-weight:600;" x-text="' · HC\u00a0' + getCourseHandicap(uid)"></span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -49,32 +49,28 @@
|
||||
</div>
|
||||
|
||||
{# Playing handicaps #}
|
||||
{% if 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 %}
|
||||
{% 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 %}
|
||||
{% set ph = playing_handicaps.get(p.id) %}
|
||||
{% if ph is not none %}
|
||||
<span style="font-weight:600; color:var(--accent); min-width:3rem; text-align:right;">HC {{ ph }}</span>
|
||||
{% else %}
|
||||
<span style="color:var(--text-muted); min-width:3rem; text-align:right;">HC —</span>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
@@ -121,26 +117,15 @@
|
||||
{% 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 sr > 0 %}<sup style="color:var(--accent); font-size:0.6em; line-height:0;">{{ '·' * sr }}</sup>{% endif %}
|
||||
{% 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>
|
||||
@@ -159,12 +144,14 @@
|
||||
<td>{{ stroke_total.val or '—' }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% if playing_handicaps %}
|
||||
{% 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 %}
|
||||
<td style="color:var(--text-muted); font-size:0.8rem;">{{ playing_handicaps.get(p.id, '—') if playing_handicaps.get(p.id) is not none else '—' }}</td>
|
||||
{% 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 %}
|
||||
@@ -254,7 +241,7 @@
|
||||
{% 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 sr > 0 %}<sup style="color:var(--accent); font-size:0.6em; line-height:0;">{{ '·' * sr }}</sup>{% 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">
|
||||
|
||||
Reference in New Issue
Block a user