Admin panels
This commit is contained in:
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user