Admin panels

This commit is contained in:
Frank van den Berg
2026-06-27 15:53:43 +02:00
parent 0d8335dd2c
commit ac57385a9d
17 changed files with 997 additions and 88 deletions
+2
View File
@@ -23,6 +23,7 @@
{% endif %}
</nav>
</header>
{% block main %}
<main>
{% for label, messages in app.flashes %}
{% for message in messages %}
@@ -33,6 +34,7 @@
{% endfor %}
{% block body %}{% endblock %}
</main>
{% endblock %}
<footer>
<small>&copy; {{ "now"|date("Y") }} {{ 'site.name'|trans }}</small>
</footer>
+78
View File
@@ -0,0 +1,78 @@
{% extends 'base.html.twig' %}
{% block main %}
<div style="display: flex; min-height: calc(100vh - 60px);">
{# ── Sidebar ──────────────────────────────────────────────────────── #}
<nav style="
width: 220px;
flex-shrink: 0;
background: #1e293b;
color: #cbd5e1;
display: flex;
flex-direction: column;
padding: 0;
">
<div style="padding: 1.25rem 1.5rem; border-bottom: 1px solid #334155;">
<span style="font-weight: 700; font-size: 1rem; color: #f1f5f9;">Game Admin</span>
</div>
<ul style="list-style: none; margin: 0; padding: 0.5rem 0; flex: 1;">
{% set current = app.request.attributes.get('_route') %}
{% set navItems = [
{ route: 'game_admin_dashboard', label: 'Dashboard', icon: '▦' },
{ route: 'game_admin_users', label: 'Users', icon: '👤' },
{ route: 'game_admin_games', label: 'Games', icon: '🎮' },
{ route: 'game_admin_sessions', label: 'Sessions', icon: '▶' },
{ route: 'game_admin_email_log', label: 'Email Log', icon: '✉' },
] %}
{% for item in navItems %}
{% set isActive = current starts with item.route %}
<li>
<a href="{{ path(item.route) }}" style="
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 1.5rem;
color: {{ isActive ? '#f1f5f9' : '#94a3b8' }};
background: {{ isActive ? '#334155' : 'transparent' }};
text-decoration: none;
font-size: 0.9rem;
border-left: 3px solid {{ isActive ? '#3b82f6' : 'transparent' }};
">
<span>{{ item.icon }}</span>
{{ item.label }}
</a>
</li>
{% endfor %}
</ul>
<div style="padding: 1rem 1.5rem; border-top: 1px solid #334155;">
<a href="{{ path('game_dashboard') }}" style="color: #64748b; font-size: 0.8rem; text-decoration: none;">
← Back to game
</a>
</div>
</nav>
{# ── Content ──────────────────────────────────────────────────────── #}
<main style="flex: 1; background: #f8fafc; padding: 2rem; overflow-x: auto;">
{% for label, messages in app.flashes %}
{% for message in messages %}
<div style="
padding: 0.75rem 1rem;
margin-bottom: 1rem;
border-radius: 6px;
background: {{ label == 'success' ? '#dcfce7' : (label == 'danger' ? '#fee2e2' : '#fef9c3') }};
color: {{ label == 'success' ? '#166534' : (label == 'danger' ? '#991b1b' : '#854d0e') }};
border: 1px solid {{ label == 'success' ? '#86efac' : (label == 'danger' ? '#fca5a5' : '#fde047') }};
">{{ message }}</div>
{% endfor %}
{% endfor %}
{% block admin_body %}{% endblock %}
</main>
</div>
{% endblock %}
@@ -0,0 +1,49 @@
{% extends 'game/admin/base.html.twig' %}
{% block title %}Email Log — Admin{% endblock %}
{% block admin_body %}
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h1 style="margin: 0; font-size: 1.5rem; color: #0f172a;">Email Log</h1>
<span style="color: #64748b; font-size: 0.9rem;">{{ logs|length }} entries</span>
</div>
<div style="background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.07); overflow: hidden;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #e2e8f0;">
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">ID</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">User</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Email</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Type</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Sent at</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 0.75rem 1rem; color: #94a3b8;">{{ log.id }}</td>
<td style="padding: 0.75rem 1rem; font-weight: 500; color: #0f172a;">{{ log.user.username }}</td>
<td style="padding: 0.75rem 1rem; color: #475569;">{{ log.user.email }}</td>
<td style="padding: 0.75rem 1rem;">
<span style="
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
background: #dbeafe;
color: #1e40af;
">{{ log.emailIdentifier }}</span>
</td>
<td style="padding: 0.75rem 1rem; color: #475569;">{{ log.sentAt|date('Y-m-d H:i:s') }}</td>
</tr>
{% else %}
<tr>
<td colspan="5" style="padding: 2rem; text-align: center; color: #94a3b8;">No emails logged yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
{% extends 'game/admin/base.html.twig' %}
{% block title %}Edit Game — Admin{% endblock %}
{% block admin_body %}
<div style="margin-bottom: 1.5rem;">
<a href="{{ path('game_admin_games') }}" style="color: #64748b; text-decoration: none; font-size: 0.9rem;">← Games</a>
</div>
<h1 style="margin: 0 0 1.5rem; font-size: 1.5rem; color: #0f172a;">Edit game: {{ game.name }}</h1>
<div style="background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.07); padding: 1.5rem; max-width: 500px;">
{{ form_start(form, {attr: {style: 'display: flex; flex-direction: column; gap: 1.25rem;'}}) }}
<div>
{{ form_label(form.name, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }}
{{ form_widget(form.name, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }}
{{ form_errors(form.name) }}
</div>
<div>
{{ form_label(form.numberOfPlayers, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }}
{{ form_widget(form.numberOfPlayers, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }}
{{ form_errors(form.numberOfPlayers) }}
</div>
<div>
{{ form_label(form.status, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }}
{{ form_widget(form.status, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }}
{{ form_errors(form.status) }}
</div>
<div>
{{ form_label(form.totalTime, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }}
{{ form_widget(form.totalTime, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }}
<small style="color: #64748b; font-size: 0.8rem;">In seconds — e.g. 3600 = 1 hour</small>
{{ form_errors(form.totalTime) }}
</div>
<div style="display: flex; gap: 0.75rem; margin-top: 0.5rem;">
<button type="submit" style="
padding: 0.6rem 1.25rem;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
">Save changes</button>
<a href="{{ path('game_admin_games') }}" style="
padding: 0.6rem 1.25rem;
background: #f1f5f9;
color: #475569;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
">Cancel</a>
</div>
{{ form_end(form) }}
</div>
{% endblock %}
@@ -0,0 +1,60 @@
{% extends 'game/admin/base.html.twig' %}
{% block title %}Games — Admin{% endblock %}
{% block admin_body %}
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h1 style="margin: 0; font-size: 1.5rem; color: #0f172a;">Games</h1>
<span style="color: #64748b; font-size: 0.9rem;">{{ games|length }} total</span>
</div>
<div style="background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.07); overflow: hidden;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #e2e8f0;">
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">ID</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Name</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Players</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Status</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Sessions</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Actions</th>
</tr>
</thead>
<tbody>
{% for game in games %}
{% set statusColor = {
'inDevelopment': '#fef3c7:#92400e',
'locked': '#fee2e2:#991b1b',
'open': '#dcfce7:#166534',
} %}
{% set colors = (statusColor[game.status.value] ?? '#f1f5f9:#475569')|split(':') %}
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 0.75rem 1rem; color: #94a3b8;">{{ game.id }}</td>
<td style="padding: 0.75rem 1rem; font-weight: 500; color: #0f172a;">{{ game.name }}</td>
<td style="padding: 0.75rem 1rem; color: #475569;">{{ game.numberOfPlayers }}</td>
<td style="padding: 0.75rem 1rem;">
<span style="
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
background: {{ colors[0] }};
color: {{ colors[1] }};
">{{ game.status.value }}</span>
</td>
<td style="padding: 0.75rem 1rem; color: #475569;">{{ game.sessions|length }}</td>
<td style="padding: 0.75rem 1rem;">
<a href="{{ path('game_admin_game_edit', {id: game.id}) }}" style="color: #3b82f6; text-decoration: none; font-size: 0.85rem;">Edit</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" style="padding: 2rem; text-align: center; color: #94a3b8;">No games found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+84 -76
View File
@@ -1,82 +1,90 @@
{% extends 'base.html.twig' %}
{% extends 'game/admin/base.html.twig' %}
{% block title %}Game Admin Dashboard{% endblock %}
{% block title %}Admin Dashboard{% endblock %}
{% block body %}
<h1>Game Admin Dashboard</h1>
{% block admin_body %}
<h1 style="margin: 0 0 1.5rem; font-size: 1.5rem; color: #0f172a;">Dashboard</h1>
<div style="display: flex; gap: 2rem;">
<div style="flex: 1;">
<h2>All Players ({{ players|length }})</h2>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: #f2f2f2;">
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Roles</th>
<th>Verified</th>
</tr>
</thead>
<tbody>
{% for player in players %}
<tr>
<td>{{ player.id }}</td>
<td>{{ player.username }}</td>
<td>{{ player.email }}</td>
<td>{{ player.roles|join(', ') }}</td>
<td>{{ player.isVerified ? 'Yes' : 'No' }}</td>
</tr>
{% else %}
<tr>
<td colspan="5">No players found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
{% set stats = [
{ label: 'Total Users', value: totalUsers, color: '#3b82f6' },
{ label: 'Players', value: totalPlayers, color: '#8b5cf6' },
{ label: 'Admins', value: totalAdmins, color: '#f59e0b' },
{ label: 'Games', value: totalGames, color: '#10b981' },
{ label: 'Active Sessions', value: activeSessions, color: '#ef4444' },
{ label: 'Total Sessions', value: totalSessions, color: '#64748b' },
] %}
<div style="flex: 2;">
<h2>All Sessions ({{ sessions|length }})</h2>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: #f2f2f2;">
<th>ID</th>
<th>Game</th>
<th>Status</th>
<th>Players Joined</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for session in sessions %}
<tr>
<td>{{ session.id }}</td>
<td>{{ session.game.name }}</td>
<td>{{ session.status.value }}</td>
<td>
<ul>
{% for p in session.players %}
<li>{{ p.user.username }} (Screen: {{ p.screen ?? 'N/A' }})</li>
{% else %}
<li>No players</li>
{% endfor %}
</ul>
({{ session.players|length }} / {{ session.game.numberOfPlayers }})
</td>
<td>{{ session.created|date('Y-m-d H:i') }}</td>
<td>
<a href="{{ path('game_admin_view_session', {session: session.id}) }}">View Game Logs</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="6">No sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% for stat in stats %}
<div style="
background: #fff;
border-radius: 8px;
padding: 1.25rem;
border-left: 4px solid {{ stat.color }};
box-shadow: 0 1px 3px rgba(0,0,0,.07);
">
<div style="font-size: 1.75rem; font-weight: 700; color: {{ stat.color }};">{{ stat.value }}</div>
<div style="font-size: 0.8rem; color: #64748b; margin-top: 0.25rem;">{{ stat.label }}</div>
</div>
{% endfor %}
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;">
<a href="{{ path('game_admin_users') }}" style="
background: #fff;
border-radius: 8px;
padding: 1.25rem;
text-decoration: none;
color: #1e293b;
box-shadow: 0 1px 3px rgba(0,0,0,.07);
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
">
<span style="font-size: 1.5rem;">👤</span> Manage Users
</a>
<a href="{{ path('game_admin_games') }}" style="
background: #fff;
border-radius: 8px;
padding: 1.25rem;
text-decoration: none;
color: #1e293b;
box-shadow: 0 1px 3px rgba(0,0,0,.07);
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
">
<span style="font-size: 1.5rem;">🎮</span> Manage Games
</a>
<a href="{{ path('game_admin_sessions') }}" style="
background: #fff;
border-radius: 8px;
padding: 1.25rem;
text-decoration: none;
color: #1e293b;
box-shadow: 0 1px 3px rgba(0,0,0,.07);
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
">
<span style="font-size: 1.5rem;">▶</span> View Sessions
</a>
<a href="{{ path('game_admin_email_log') }}" style="
background: #fff;
border-radius: 8px;
padding: 1.25rem;
text-decoration: none;
color: #1e293b;
box-shadow: 0 1px 3px rgba(0,0,0,.07);
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
">
<span style="font-size: 1.5rem;">✉</span> Email Log
</a>
</div>
{% endblock %}
@@ -0,0 +1,79 @@
{% extends 'game/admin/base.html.twig' %}
{% block title %}Sessions — Admin{% endblock %}
{% block admin_body %}
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h1 style="margin: 0; font-size: 1.5rem; color: #0f172a;">Sessions</h1>
<span style="color: #64748b; font-size: 0.9rem;">{{ sessions|length }} total</span>
</div>
<div style="background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.07); overflow: hidden;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #e2e8f0;">
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">ID</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Game</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Status</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Players</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Created</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Actions</th>
</tr>
</thead>
<tbody>
{% for session in sessions %}
{% set statusColors = {
'created': '#ede9fe:#5b21b6',
'ready': '#fef3c7:#92400e',
'playing': '#dbeafe:#1e40af',
'won': '#dcfce7:#166534',
'lost': '#fee2e2:#991b1b',
} %}
{% set colors = (statusColors[session.status.value] ?? '#f1f5f9:#475569')|split(':') %}
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 0.75rem 1rem; color: #94a3b8;">{{ session.id }}</td>
<td style="padding: 0.75rem 1rem; font-weight: 500; color: #0f172a;">{{ session.game.name }}</td>
<td style="padding: 0.75rem 1rem;">
<span style="
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
background: {{ colors[0] }};
color: {{ colors[1] }};
">{{ session.status.value }}</span>
</td>
<td style="padding: 0.75rem 1rem; color: #475569;">
{{ session.players|map(p => p.user.username)|join(', ') ?: '—' }}
<span style="color: #94a3b8;">({{ session.players|length }}/{{ session.game.numberOfPlayers }})</span>
</td>
<td style="padding: 0.75rem 1rem; color: #475569;">{{ session.created|date('Y-m-d H:i') }}</td>
<td style="padding: 0.75rem 1rem; white-space: nowrap;">
<a href="{{ path('game_admin_view_session', {session: session.id}) }}" style="color: #3b82f6; text-decoration: none; font-size: 0.85rem; margin-right: 0.75rem;">View logs</a>
{% if session.status.value not in ['won', 'lost'] %}
<form method="post" action="{{ path('game_admin_session_close', {id: session.id}) }}" style="display: inline;" onsubmit="return confirm('Force-close session #{{ session.id }}?')">
<input type="hidden" name="_token" value="{{ csrf_token('close_session_' ~ session.id) }}">
<button type="submit" style="
background: none;
border: none;
color: #ef4444;
cursor: pointer;
font-size: 0.85rem;
padding: 0;
">Close</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6" style="padding: 2rem; text-align: center; color: #94a3b8;">No sessions found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
@@ -0,0 +1,83 @@
{% extends 'game/admin/base.html.twig' %}
{% block title %}Session #{{ session.id }} logs — Admin{% endblock %}
{% block admin_body %}
<div style="margin-bottom: 1.5rem;">
<a href="{{ path('game_admin_sessions') }}" style="color: #64748b; text-decoration: none; font-size: 0.9rem;">← Sessions</a>
</div>
<h1 style="margin: 0 0 0.25rem; font-size: 1.5rem; color: #0f172a;">{{ session.game.name }} — Session #{{ session.id }}</h1>
<p style="margin: 0 0 1.5rem; color: #64748b; font-size: 0.9rem;">{{ session.status.value }} · {{ session.players|length }} player(s) · Created {{ session.created|date('Y-m-d H:i') }}</p>
{% if playersLogs is empty %}
<div style="background: #fff; border-radius: 8px; padding: 2rem; text-align: center; color: #94a3b8; box-shadow: 0 1px 3px rgba(0,0,0,.07);">
No players in this session.
</div>
{% else %}
{# Tab buttons #}
<div style="display: flex; gap: 0; margin-bottom: 0; border-bottom: 1px solid #e2e8f0;">
{% for playerLog in playersLogs %}
<button
onclick="openTab('player-{{ loop.index }}')"
id="tab-{{ loop.index }}"
style="
padding: 0.6rem 1.25rem;
border: 1px solid {{ loop.first ? '#3b82f6' : '#e2e8f0' }};
border-bottom: {{ loop.first ? '1px solid #fff' : '1px solid #e2e8f0' }};
background: {{ loop.first ? '#fff' : '#f8fafc' }};
color: {{ loop.first ? '#1e40af' : '#64748b' }};
font-size: 0.9rem;
font-weight: {{ loop.first ? '600' : '400' }};
cursor: pointer;
border-radius: 6px 6px 0 0;
margin-bottom: -1px;
"
>{{ playerLog.username }}</button>
{% endfor %}
</div>
{# Tab content #}
{% for playerLog in playersLogs %}
<div id="player-{{ loop.index }}" style="
display: {{ loop.first ? 'block' : 'none' }};
background: #fff;
border: 1px solid #e2e8f0;
border-top: none;
border-radius: 0 0 8px 8px;
padding: 1.25rem;
box-shadow: 0 1px 3px rgba(0,0,0,.07);
">
<pre style="
background: #1e293b;
color: #e2e8f0;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
font-size: 0.85rem;
line-height: 1.5;
margin: 0;
">{{ playerLog.logs ?: 'No logs found for this player.' }}</pre>
</div>
{% endfor %}
{% endif %}
<script>
function openTab(id) {
document.querySelectorAll('[id^="player-"]').forEach(el => el.style.display = 'none');
document.getElementById(id).style.display = 'block';
const idx = id.split('-')[1];
document.querySelectorAll('[id^="tab-"]').forEach((btn, i) => {
const active = String(i + 1) === idx;
btn.style.background = active ? '#fff' : '#f8fafc';
btn.style.color = active ? '#1e40af' : '#64748b';
btn.style.fontWeight = active ? '600' : '400';
btn.style.borderColor = active ? '#3b82f6' : '#e2e8f0';
btn.style.borderBottom = active ? '1px solid #fff' : '1px solid #e2e8f0';
});
}
</script>
{% endblock %}
+71
View File
@@ -0,0 +1,71 @@
{% extends 'game/admin/base.html.twig' %}
{% block title %}Edit User — Admin{% endblock %}
{% block admin_body %}
<div style="margin-bottom: 1.5rem;">
<a href="{{ path('game_admin_users') }}" style="color: #64748b; text-decoration: none; font-size: 0.9rem;">← Users</a>
</div>
<h1 style="margin: 0 0 1.5rem; font-size: 1.5rem; color: #0f172a;">Edit user: {{ user.username }}</h1>
<div style="background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.07); padding: 1.5rem; max-width: 500px;">
{{ form_start(form, {attr: {style: 'display: flex; flex-direction: column; gap: 1.25rem;'}}) }}
<div>
{{ form_label(form.email, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }}
{{ form_widget(form.email, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }}
{{ form_errors(form.email) }}
</div>
<div>
{{ form_label(form.username, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }}
{{ form_widget(form.username, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }}
{{ form_errors(form.username) }}
</div>
<div>
{{ form_label(form.plainPassword, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.35rem; color: #374151; font-size: 0.875rem;'}}) }}
{{ form_widget(form.plainPassword, {attr: {style: 'width: 100%; padding: 0.5rem 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: 0.9rem; box-sizing: border-box;'}}) }}
{{ form_errors(form.plainPassword) }}
</div>
<div>
{{ form_label(form.roles, null, {label_attr: {style: 'display: block; font-weight: 600; margin-bottom: 0.5rem; color: #374151; font-size: 0.875rem;'}}) }}
<div style="display: flex; flex-direction: column; gap: 0.4rem;">
{{ form_widget(form.roles) }}
</div>
{{ form_errors(form.roles) }}
</div>
<div style="display: flex; align-items: center; gap: 0.5rem;">
{{ form_widget(form.isVerified) }}
{{ form_label(form.isVerified, null, {label_attr: {style: 'font-weight: 600; color: #374151; font-size: 0.875rem; cursor: pointer;'}}) }}
{{ form_errors(form.isVerified) }}
</div>
<div style="display: flex; gap: 0.75rem; margin-top: 0.5rem;">
<button type="submit" style="
padding: 0.6rem 1.25rem;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
">Save changes</button>
<a href="{{ path('game_admin_users') }}" style="
padding: 0.6rem 1.25rem;
background: #f1f5f9;
color: #475569;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
">Cancel</a>
</div>
{{ form_end(form) }}
</div>
{% endblock %}
@@ -0,0 +1,83 @@
{% extends 'game/admin/base.html.twig' %}
{% block title %}Users — Admin{% endblock %}
{% block admin_body %}
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem;">
<h1 style="margin: 0; font-size: 1.5rem; color: #0f172a;">Users</h1>
<span style="color: #64748b; font-size: 0.9rem;">{{ users|length }} total</span>
</div>
<div style="background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.07); overflow: hidden;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
<thead>
<tr style="background: #f1f5f9; border-bottom: 1px solid #e2e8f0;">
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">ID</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Username</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Email</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Roles</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Verified</th>
<th style="padding: 0.75rem 1rem; text-align: left; color: #475569; font-weight: 600;">Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 0.75rem 1rem; color: #94a3b8;">{{ user.id }}</td>
<td style="padding: 0.75rem 1rem; font-weight: 500; color: #0f172a;">{{ user.username }}</td>
<td style="padding: 0.75rem 1rem; color: #475569;">{{ user.email }}</td>
<td style="padding: 0.75rem 1rem;">
{% for role in user.roles %}
{% if role != 'ROLE_USER' %}
<span style="
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
background: {{ role == 'ROLE_ADMIN' ? '#fef3c7' : '#ede9fe' }};
color: {{ role == 'ROLE_ADMIN' ? '#92400e' : '#5b21b6' }};
margin-right: 2px;
">{{ role }}</span>
{% endif %}
{% endfor %}
</td>
<td style="padding: 0.75rem 1rem;">
{% if user.verified %}
<span style="color: #16a34a; font-weight: 500;">✓ Yes</span>
{% else %}
<span style="color: #dc2626;">✗ No</span>
{% endif %}
</td>
<td style="padding: 0.75rem 1rem;">
<a href="{{ path('game_admin_user_edit', {id: user.id}) }}" style="
color: #3b82f6;
text-decoration: none;
font-size: 0.85rem;
margin-right: 0.75rem;
">Edit</a>
{% if user != app.user %}
<form method="post" action="{{ path('game_admin_user_delete', {id: user.id}) }}" style="display: inline;" onsubmit="return confirm('Delete user {{ user.username }}?')">
<input type="hidden" name="_token" value="{{ csrf_token('delete_user_' ~ user.id) }}">
<button type="submit" style="
background: none;
border: none;
color: #ef4444;
cursor: pointer;
font-size: 0.85rem;
padding: 0;
">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6" style="padding: 2rem; text-align: center; color: #94a3b8;">No users found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}