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
+81
View File
@@ -0,0 +1,81 @@
{% extends "base.html" %}
{% block title %}Add User Skins &amp; Pins{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="https://unpkg.com/cropperjs@1.6.2/dist/cropper.min.css">
<script src="https://unpkg.com/cropperjs@1.6.2/dist/cropper.min.js"></script>
<script src="/static/avatar-crop.js"></script>
{% endblock %}
{% block content %}
<div class="page">
<div class="page-header">
<a href="/admin" style="color:var(--text-muted); font-size:0.9rem;">← Admin</a>
<h1 style="margin-top:0.25rem;">Add user</h1>
</div>
<div x-data="avatarUpload('/admin/users/new', '/admin')">
<form action="/admin/users/new" method="post" enctype="multipart/form-data"
@submit="submitForm($event)">
<div class="card">
<div class="form-group">
<label for="name">Name</label>
{% if errors and errors.name %}<div class="alert alert-error">{{ errors.name }}</div>{% endif %}
<input type="text" id="name" name="name" value="{{ form.name if form else '' }}" required>
</div>
<div class="form-group">
<label for="email">Email</label>
{% if errors and errors.email %}<div class="alert alert-error">{{ errors.email }}</div>{% endif %}
<input type="email" id="email" name="email" value="{{ form.email if form else '' }}" required>
</div>
<div class="form-group">
<label for="handicap">Handicap (054)</label>
{% if errors and errors.handicap %}<div class="alert alert-error">{{ errors.handicap }}</div>{% endif %}
<input type="number" id="handicap" name="handicap" min="0" max="54" step="0.1"
value="{{ form.handicap if form else '' }}" required>
</div>
<div class="form-group">
<label>Avatar</label>
{% if errors and errors.avatar %}<div class="alert alert-error">{{ errors.avatar }}</div>{% endif %}
<div class="avatar-tabs">
<button type="button" class="avatar-tab" :class="{ active: tab === 'emoji' }" @click="tab = 'emoji'">Emoji</button>
<button type="button" class="avatar-tab" :class="{ active: tab === 'upload' }" @click="tab = 'upload'">Photo</button>
</div>
<div x-show="tab === 'emoji'">
<div class="avatar-grid" style="margin-top: 0.75rem;">
{% for a in avatars %}
<div class="avatar-option">
<input type="radio" name="avatar_emoji" id="avatar_{{ loop.index }}" value="{{ a }}">
<label for="avatar_{{ loop.index }}">{{ a }}</label>
</div>
{% endfor %}
</div>
</div>
<div x-show="tab === 'upload'" style="margin-top: 0.75rem;">
<div x-show="previewUrl" style="margin-bottom: 0.75rem; display:flex; align-items:center; gap:1rem;">
<img :src="previewUrl" class="avatar-preview">
<span style="color:var(--text-muted); font-size:0.85rem;">Tap to change</span>
</div>
<label class="file-input-label">
<span x-text="previewUrl ? 'Choose different photo' : 'Choose photo'"></span>
<input type="file" name="avatar_file" accept="image/jpeg,image/png,image/webp,image/gif"
style="display:none" @change="openFile($event)">
</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Add user</button>
</form>
{% include "partials/crop_modal.html" %}
</div>
</div>
{% endblock %}