Add full SkinsPins application

- FastAPI + SQLite + Alpine.js + HTMX PWA for Bingo Bango Bongo golf scoring
- Passwordless magic link auth with email (aiosmtplib) and admin invite system
- Real-time score entry via WebSocket with drag-and-drop player ordering
- Course management with per-hole par and stroke index
- Scorecard with golf score markers (birdie, eagle, bogey, etc.)
- Two-step new game form with tee selection per player
- Dashboard with stats, live games social stream, and recent game history
- Game summary view (read-only scorecard with leaderboard)
- User profile pages with win stats
- PWA install support with generated PNG icons
- Admin panel for users, courses, games, and invitations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Rolf
2026-03-20 10:16:46 +01:00
parent 4b31b98f89
commit 3a1da1482c
51 changed files with 4964 additions and 1 deletions
+66
View File
@@ -0,0 +1,66 @@
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
import aiosqlite
from app.database import get_db
from app.dependencies import get_current_user
from app.templates_config import templates
router = APIRouter()
@router.get("/users/{user_id}", response_class=HTMLResponse)
async def user_profile(user_id: str, request: Request,
user: dict = Depends(get_current_user),
db: aiosqlite.Connection = Depends(get_db)):
async with db.execute(
"SELECT id, name, handicap, avatar FROM users WHERE id = ? AND is_onboarded = 1",
(user_id,),
) as c:
profile = await c.fetchone()
if not profile:
from fastapi.responses import RedirectResponse
return RedirectResponse("/", status_code=302)
uid = user_id
async with db.execute(
"""SELECT COUNT(*) FROM games
WHERE status = 'completed'
AND id IN (SELECT game_id FROM game_players WHERE user_id = ?)""",
(uid,),
) as c:
games_played = (await c.fetchone())[0]
async with db.execute(
"""SELECT gp.game_id,
SUM(CASE WHEN hs.user_id = ? THEN hs.bingo + hs.bango + hs.bongo ELSE 0 END) AS my_total
FROM game_players gp
JOIN games g ON g.id = gp.game_id
JOIN hole_scores hs ON hs.game_id = gp.game_id
WHERE gp.user_id = ? AND g.status = 'completed'
GROUP BY gp.game_id""",
(uid, uid),
) as c:
game_rows = await c.fetchall()
victories = 0
for row in game_rows:
async with db.execute(
"""SELECT MAX(player_total) FROM (
SELECT user_id, SUM(bingo + bango + bongo) AS player_total
FROM hole_scores WHERE game_id = ? GROUP BY user_id
)""",
(row["game_id"],),
) as c2:
max_total = (await c2.fetchone())[0] or 0
if row["my_total"] >= max_total and row["my_total"] > 0:
victories += 1
win_pct = round((victories / games_played) * 100) if games_played > 0 else None
return templates.TemplateResponse(
"users/profile.html",
{"request": request, "user": user, "profile": dict(profile),
"games_played": games_played, "victories": victories, "win_pct": win_pct},
)