Added admin dashboard, refactored user dashboard. Removed old reviews route.
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 1m53s

This commit is contained in:
2025-11-20 20:40:49 +00:00
parent cd04fa5212
commit f15d9a9cfd
16 changed files with 534 additions and 75 deletions

View File

@@ -6,7 +6,9 @@
</button>
<div class="collapse navbar-collapse" id="navMain">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{{ path('review_index') }}">Your Reviews</a></li>
{% if app.user %}
<li class="nav-item"><a class="nav-link" href="{{ path('album_new') }}">Create Album</a></li>
{% endif %}
</ul>
<div class="d-flex align-items-center gap-3">
{% if app.user %}
@@ -20,6 +22,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% if is_granted('ROLE_ADMIN') %}
<li><a class="dropdown-item" href="{{ path('admin_dashboard') }}">Site dashboard</a></li>
<li><a class="dropdown-item" href="{{ path('admin_settings') }}">Site settings</a></li>
<li><hr class="dropdown-divider"></li>
{% endif %}

View File

@@ -1,17 +1,96 @@
{% extends 'base.html.twig' %}
{% block title %}Dashboard{% endblock %}
{% block body %}
<h1 class="h4 mb-3">Your profile</h1>
<h1 class="h4 mb-3">Your dashboard</h1>
{% for msg in app.flashes('success') %}<div class="alert alert-success">{{ msg }}</div>{% endfor %}
{{ form_start(form, {attr: {class: 'vstack gap-3', novalidate: 'novalidate'}}) }}
<div>{{ form_label(form.email) }}{{ form_widget(form.email, {attr: {class: 'form-control'}}) }}{{ form_errors(form.email) }}</div>
<div>{{ form_label(form.displayName) }}{{ form_widget(form.displayName, {attr: {class: 'form-control'}}) }}{{ form_errors(form.displayName) }}</div>
<div>{{ form_label(form.currentPassword) }}{{ form_widget(form.currentPassword, {attr: {class: 'form-control'}}) }}{{ form_errors(form.currentPassword) }}</div>
<div>{{ form_label(form.newPassword.first) }}{{ form_widget(form.newPassword.first, {attr: {class: 'form-control'}}) }}{{ form_errors(form.newPassword.first) }}</div>
<div>{{ form_label(form.newPassword.second) }}{{ form_widget(form.newPassword.second, {attr: {class: 'form-control'}}) }}{{ form_errors(form.newPassword.second) }}</div>
<button class="btn btn-success" type="submit">Save changes</button>
{{ form_end(form) }}
<div class="row g-3 mb-4">
<div class="col-sm-4">
<div class="card h-100">
<div class="card-body">
<div class="text-secondary">Review Count</div>
<div class="display-6">{{ reviewCount }}</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card h-100">
<div class="card-body">
<div class="text-secondary">Album Count</div>
<div class="display-6">{{ albumCount }}</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card h-100">
<div class="card-body">
<div class="text-secondary">User Type</div>
<div class="display-6">{{ userType }}</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<h2 class="h6">Profile</h2>
<div class="row g-3">
<div class="col-sm-6">
<label class="form-label">Email</label>
<input class="form-control" value="{{ email }}" readonly />
</div>
<div class="col-sm-6">
<label class="form-label">Display name</label>
<input class="form-control" value="{{ displayName }}" readonly />
</div>
</div>
<div class="mt-3">
<a class="btn btn-outline-secondary" href="{{ path('account_password') }}">Change password</a>
</div>
</div>
</div>
<div class="row g-3 mt-3">
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h2 class="h6 mb-3">Your reviews</h2>
<div class="vstack gap-2">
{% for r in userReviews %}
<div>
<div><a href="{{ path('review_show', {id: r.id}) }}" class="text-decoration-none">{{ r.title }}</a> <span class="text-secondary">(Rating {{ r.rating }}/10)</span></div>
<div class="text-secondary small">{{ r.album.name }}{{ r.createdAt|date('Y-m-d H:i') }}</div>
</div>
{% else %}
<div class="text-secondary">You haven't written any reviews yet.</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h2 class="h6 mb-3">Your albums</h2>
<div class="vstack gap-2">
{% for a in userAlbums %}
<div class="d-flex justify-content-between">
<div>
<div><a href="{{ path('album_show', {id: a.localId}) }}" class="text-decoration-none">{{ a.name }}</a></div>
<div class="text-secondary small">{{ a.artists|join(', ') }}{% if a.releaseDate %}{{ a.releaseDate }}{% endif %}</div>
</div>
<div class="ms-2">
<a class="btn btn-sm btn-outline-secondary" href="{{ path('album_edit', {id: a.localId}) }}">Edit</a>
</div>
</div>
{% else %}
<div class="text-secondary">You haven't created any albums yet.</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block title %}Change Password{% endblock %}
{% block body %}
<h1 class="h4 mb-3">Change password</h1>
{{ form_start(form, {attr: {class: 'vstack gap-3', novalidate: 'novalidate'}}) }}
<div>{{ form_label(form.currentPassword) }}{{ form_widget(form.currentPassword, {attr: {class: 'form-control'}}) }}{{ form_errors(form.currentPassword) }}</div>
<div>{{ form_label(form.newPassword.first) }}{{ form_widget(form.newPassword.first, {attr: {class: 'form-control'}}) }}{{ form_errors(form.newPassword.first) }}</div>
<div>{{ form_label(form.newPassword.second) }}{{ form_widget(form.newPassword.second, {attr: {class: 'form-control'}}) }}{{ form_errors(form.newPassword.second) }}</div>
<button class="btn btn-success" type="submit">Update password</button>
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,81 @@
{% extends 'base.html.twig' %}
{% block title %}Site Dashboard{% endblock %}
{% block body %}
<h1 class="h4 mb-3">Site dashboard</h1>
<div class="row g-3 mb-4">
<div class="col-sm-4">
<div class="card h-100">
<div class="card-body">
<div class="text-secondary">Total Reviews</div>
<div class="display-6">{{ totalReviews }}</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card h-100">
<div class="card-body">
<div class="text-secondary">Total Albums</div>
<div class="display-6">{{ totalAlbums }}</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="card h-100">
<div class="card-body">
<div class="text-secondary">Total Users</div>
<div class="display-6">{{ totalUsers }}</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h2 class="h6 mb-3">Latest reviews (50)</h2>
<div class="vstack gap-2">
{% for r in recentReviews %}
<div>
<div><a class="text-decoration-none" href="{{ path('review_show', {id: r.id}) }}">{{ r.title }}</a> <span class="text-secondary">(Rating {{ r.rating }}/10)</span></div>
<div class="text-secondary small">{{ r.album.name }} • by {{ r.author.displayName ?? r.author.userIdentifier }}{{ r.createdAt|date('Y-m-d H:i') }}</div>
</div>
{% else %}
<div class="text-secondary">No reviews.</div>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h2 class="h6 mb-3">Latest albums (50)</h2>
<div class="vstack gap-2">
{% for a in recentAlbums %}
{% set publicId = a.source == 'user' ? a.localId : a.spotifyId %}
<div class="d-flex justify-content-between">
<div>
<div>
{% if publicId %}
<a class="text-decoration-none" href="{{ path('album_show', {id: publicId}) }}">{{ a.name }}</a>
{% else %}
<span>{{ a.name }}</span>
{% endif %}
{% if a.source == 'user' %}<span class="badge text-bg-primary">User</span>{% else %}<span class="badge text-bg-success">Spotify</span>{% endif %}
</div>
<div class="text-secondary small">{{ a.artists|join(', ') }}{% if a.releaseDate %}{{ a.releaseDate }}{% endif %}{{ a.createdAt|date('Y-m-d H:i') }}</div>
</div>
</div>
{% else %}
<div class="text-secondary">No albums.</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends 'base.html.twig' %}
{% block title %}Edit Album{% endblock %}
{% block body %}
<h1 class="h4 mb-3">Edit album</h1>
{{ form_start(form, {attr: {class: 'vstack gap-3', novalidate: 'novalidate'}}) }}
<div>{{ form_label(form.name) }}{{ form_widget(form.name, {attr: {class: 'form-control'}}) }}{{ form_errors(form.name) }}</div>
<div>{{ form_label(form.artistsCsv) }}{{ form_widget(form.artistsCsv, {attr: {class: 'form-control'}}) }}</div>
<div>{{ form_label(form.releaseDate) }}{{ form_widget(form.releaseDate, {attr: {class: 'form-control'}}) }}</div>
<div>{{ form_label(form.totalTracks) }}{{ form_widget(form.totalTracks, {attr: {class: 'form-control'}}) }}</div>
<div>{{ form_label(form.coverUrl) }}{{ form_widget(form.coverUrl, {attr: {class: 'form-control'}}) }}</div>
<div>{{ form_label(form.externalUrl) }}{{ form_widget(form.externalUrl, {attr: {class: 'form-control'}}) }}</div>
<button class="btn btn-success" type="submit">Save changes</button>
<a class="btn btn-link" href="{{ path('album_show', {id: albumId}) }}">Cancel</a>
{{ form_end(form) }}
{% endblock %}

View File

@@ -14,6 +14,13 @@
</div>
<div class="collapse col-12" id="advancedSearch">
<div class="row g-2 mt-1">
<div class="col-sm-3 order-sm-4">
<select class="form-select" name="source">
<option value="all" {{ (source is defined and source == 'all') or source is not defined ? 'selected' : '' }}>All sources</option>
<option value="spotify" {{ (source is defined and source == 'spotify') ? 'selected' : '' }}>Spotify</option>
<option value="user" {{ (source is defined and source == 'user') ? 'selected' : '' }}>User-created</option>
</select>
</div>
<div class="col-sm-4">
<input class="form-control" type="text" name="album" value="{{ album }}" placeholder="Album title" />
</div>
@@ -52,12 +59,14 @@
{% set s = stats[album.id] ?? { 'avg': 0, 'count': 0 } %}
<p class="card-text"><small class="text-secondary">User score: {{ s.avg }}/10 ({{ s.count }})</small></p>
<div class="mt-auto">
<a class="btn btn-outline-success btn-sm" href="{{ album.external_urls.spotify }}" target="_blank" rel="noopener">Open in Spotify</a>
{% if album.external_urls.spotify is defined and album.external_urls.spotify %}
<a class="btn btn-outline-success btn-sm" href="{{ album.external_urls.spotify }}" target="_blank" rel="noopener">Open in Spotify</a>
{% endif %}
<a class="btn btn-success btn-sm" href="{{ path('album_show', {id: album.id}) }}">Reviews</a>
{% if album.source is defined and album.source == 'user' %}
<span class="badge text-bg-primary ms-2">User album</span>
{% endif %}
{% if app.user %}
{% if app.user and (album.source is not defined or album.source != 'user') %}
{% if savedIds is defined and (album.id in savedIds) %}
<span class="badge text-bg-secondary ms-2">Saved</span>
{% else %}

View File

@@ -13,17 +13,22 @@
<div class="text-secondary mb-2">{{ album.artists|map(a => a.name)|join(', ') }}</div>
<p class="text-secondary mb-2">Released {{ album.release_date }}{{ album.total_tracks }} tracks</p>
<p class="mb-2"><strong>User score:</strong> {{ avg }}/10 ({{ count }})</p>
<a class="btn btn-outline-success btn-sm" href="{{ album.external_urls.spotify }}" target="_blank" rel="noopener">Open in Spotify</a>
{% if album.external_urls.spotify %}
<a class="btn btn-outline-success btn-sm" href="{{ album.external_urls.spotify }}" target="_blank" rel="noopener">Open in Spotify</a>
{% endif %}
{% if album.source is defined and album.source == 'user' %}
<span class="badge text-bg-primary ms-2">User album</span>
{% endif %}
{% if app.user and (isSaved is defined) and (not isSaved) %}
{% if app.user and (album.source is not defined or album.source != 'user') and (isSaved is defined) and (not isSaved) %}
<form class="d-inline ms-2" method="post" action="{{ path('album_save', {id: albumId}) }}">
<input type="hidden" name="_token" value="{{ csrf_token('save-album-' ~ albumId) }}">
<button class="btn btn-primary btn-sm" type="submit">Save album</button>
</form>
{% endif %}
{% if is_granted('ROLE_ADMIN') %}
{% if allowedEdit %}
<a class="btn btn-outline-secondary btn-sm ms-2" href="{{ path('album_edit', {id: albumId}) }}">Edit</a>
{% endif %}
{% if allowedDelete %}
<form class="d-inline ms-2" method="post" action="{{ path('album_delete', {id: albumId}) }}" onsubmit="return confirm('Delete this album from the database?');">
<input type="hidden" name="_token" value="{{ csrf_token('delete-album-' ~ albumId) }}">
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button>

View File

@@ -1,29 +0,0 @@
{% extends 'base.html.twig' %}
{% block title %}Album Reviews{% endblock %}
{% block body %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="h4 mb-0">Album reviews</h1>
{% if app.user %}
<a class="btn btn-success" href="{{ path('review_new') }}">New review</a>
{% endif %}
</div>
<div class="row g-3">
{% for r in reviews %}
<div class="col-12">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title mb-1">{{ r.title }} <span class="text-secondary">(Rating {{ r.rating }}/10)</span></h5>
<div class="text-secondary mb-2">{{ r.album.name }}{{ r.album.artists|join(', ') }}</div>
<p class="card-text">{{ r.content|u.truncate(220, '…', false) }}</p>
<a class="btn btn-link p-0" href="{{ path('review_show', {id: r.id}) }}">Read more</a>
</div>
</div>
</div>
{% else %}
<p>No reviews yet.</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,7 +1,7 @@
{% extends 'base.html.twig' %}
{% block title %}{{ review.title }}{% endblock %}
{% block body %}
<p><a href="{{ path('review_index') }}">← Back</a></p>
<p><a href="{{ path('account_dashboard') }}">← Back to dashboard</a></p>
<h1 class="h4">{{ review.title }} <span class="text-secondary">(Rating {{ review.rating }}/10)</span></h1>
<p class="text-secondary">
{{ review.album.name }}{{ review.album.artists|join(', ') }}