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
+14 -6
View File
@@ -43,14 +43,22 @@ async def _send(to: str, subject: str, body_text: str, body_html: str) -> None:
)
async def send_magic_link(to: str, link: str) -> None:
subject = f"Your {SITE_NAME} login link"
body_text = f"Click the link below to log in (expires in 15 minutes):\n\n{link}\n\nIf you didn't request this, you can ignore this email."
async def send_magic_link(to: str, link: str, code: str) -> None:
subject = f"Your {SITE_NAME} login code: {code}"
body_text = (
f"Your login code is: {code}\n\n"
f"Enter it on the app, or click the link below (both expire in 15 minutes):\n\n{link}\n\n"
f"If you didn't request this, you can ignore this email."
)
body_html = f"""
<div style="font-family:sans-serif;max-width:480px;margin:auto;">
<h2 style="color:#2d6a4f;">⛳ {SITE_NAME}</h2>
<p>Click the button below to log in. This link expires in 15 minutes.</p>
<p style="margin:2rem 0;">
<p>Enter this code in the app to log in:</p>
<div style="font-size:2.5rem;font-weight:700;letter-spacing:0.4rem;text-align:center;
background:#f0f7f4;border-radius:0.5rem;padding:1rem 1.5rem;margin:1.5rem 0;
color:#1b4332;font-family:monospace;">{code}</div>
<p style="color:#555;font-size:0.9rem;">Or click the button below — whichever is easier. Both expire in 15 minutes.</p>
<p style="margin:1.5rem 0;">
<a href="{link}"
style="background:#2d6a4f;color:#fff;padding:0.75rem 1.5rem;border-radius:0.5rem;text-decoration:none;font-weight:600;">
Log in to {SITE_NAME}
@@ -67,7 +75,7 @@ async def send_magic_link(to: str, link: str) -> None:
except Exception as e:
logger.error("Failed to send magic link email to %s: %s", to, e)
else:
logger.info("Magic link for %s: %s", to, link)
logger.info("Magic link for %s — code: %s link: %s", to, code, link)
async def send_invitation(to: str, invited_by_name: str, login_url: str) -> None: