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:
@@ -0,0 +1,108 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin – Skins & Pins{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<h1>⚙️ Admin</h1>
|
||||
</div>
|
||||
|
||||
<div class="admin-subnav">
|
||||
<a href="/admin" class="admin-subnav-item active">Users</a>
|
||||
<a href="/admin/courses" class="admin-subnav-item">Courses</a>
|
||||
<a href="/admin/games" class="admin-subnav-item">Games</a>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('error') %}
|
||||
<div class="alert alert-error">{{ request.query_params.get('error') }}</div>
|
||||
{% endif %}
|
||||
{% if request.query_params.get('success') %}
|
||||
<div class="alert alert-success">{{ request.query_params.get('success') }}</div>
|
||||
{% endif %}
|
||||
{% if error %}<div class="alert alert-error">{{ error }}</div>{% endif %}
|
||||
{% if success %}<div class="alert alert-success">{{ success }}</div>{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<h2 style="font-size: 1rem; margin-bottom: 1rem; color: var(--text-muted);">Invite a player</h2>
|
||||
<form method="post" action="/admin/invite">
|
||||
<div class="form-group">
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" placeholder="player@example.com"
|
||||
autocomplete="off" autocapitalize="none" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Send invitation</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="font-size: 1rem; margin-bottom: 1rem; color: var(--text-muted);">Pending invitations</h2>
|
||||
{% if invitations %}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>Email</th><th>Invited</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for inv in invitations %}
|
||||
<tr>
|
||||
<td>{{ inv.email }}</td>
|
||||
<td style="color: var(--text-muted);">{{ inv.created_at[:10] }}</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<form method="post" action="/admin/invitations/{{ inv.id }}/resend" style="display:inline;">
|
||||
<button type="submit" class="btn-table-action">Resend</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/invitations/{{ inv.id }}/delete" style="display:inline;"
|
||||
onsubmit="return confirm('Delete invitation for {{ inv.email }}?')">
|
||||
<button type="submit" class="btn-table-action danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem;">No pending invitations.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
|
||||
<h2 style="font-size: 1rem; color: var(--text-muted); margin:0;">Registered users</h2>
|
||||
<a href="/admin/users/new" class="btn btn-primary" style="width:auto; padding:0.4rem 0.85rem; font-size:0.85rem;">+ Add user</a>
|
||||
</div>
|
||||
{% if users %}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>User</th><th>Hcp</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if u.avatar and u.avatar.startswith('/') %}
|
||||
<img src="{{ u.avatar }}" style="width:1.4rem;height:1.4rem;border-radius:50%;object-fit:cover;vertical-align:middle;margin-right:0.3rem;">
|
||||
{% else %}
|
||||
{{ u.avatar or '👤' }}
|
||||
{% endif %}
|
||||
{% if u.name %}<a href="/users/{{ u.id }}" style="color:inherit; text-decoration:none;">{{ u.name }}</a>{% else %}(not set up){% endif %}
|
||||
<div style="font-size:0.75rem; color:var(--text-muted);">{{ u.email }}</div>
|
||||
</td>
|
||||
<td style="color:var(--text-muted);">{{ u.handicap if u.handicap is not none else '—' }}</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<a href="/admin/users/{{ u.id }}/edit" class="btn-table-action">Edit</a>
|
||||
{% if u.id != user.id %}
|
||||
<form method="post" action="/admin/users/{{ u.id }}/delete" style="display:inline;"
|
||||
onsubmit="return confirm('Delete {{ u.name or u.email }}?')">
|
||||
<button type="submit" class="btn-table-action danger">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="color: var(--text-muted); font-size: 0.9rem;">No users yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user