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:
Rolf
2026-03-23 20:37:08 +01:00
parent c658c970ac
commit 8601584099
10 changed files with 335 additions and 123 deletions
+94 -4
View File
@@ -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>