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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user