feat(cms): replace home redirect with catalogue dashboard of track/album/genre cards

This commit is contained in:
daniel-c-harvey
2026-06-10 21:35:59 -04:00
parent f8186fb7c7
commit 77dee5eac5
3 changed files with 227 additions and 2 deletions
+114 -2
View File
@@ -1,13 +1,125 @@
@page "/"
@using DeepDrftManager.Services
@attribute [Authorize]
@layout Layout.CmsLayout
@inject NavigationManager Nav
@inject ICmsTrackService CmsTrackService
@inject ILogger<Index> Logger
<PageTitle>DeepDrft CMS</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudText Typo="Typo.h3" Class="mb-6">Catalogue</MudText>
<MudGrid Spacing="4">
<MudItem xs="12" sm="4">
@SummaryCard("Tracks", Icons.Material.Filled.LibraryMusic, Color.Primary, _tracksLoading, _trackCount)
</MudItem>
<MudItem xs="12" sm="4">
@SummaryCard("Albums", Icons.Material.Filled.Album, Color.Secondary, _albumsLoading, _albumCount)
</MudItem>
<MudItem xs="12" sm="4">
@SummaryCard("Genres", Icons.Material.Filled.Category, Color.Tertiary, _genresLoading, _genreCount)
</MudItem>
</MudGrid>
</MudContainer>
@code {
protected override void OnInitialized()
private bool _tracksLoading = true;
private bool _albumsLoading = true;
private bool _genresLoading = true;
private int? _trackCount;
private int? _albumCount;
private int? _genreCount;
protected override async Task OnInitializedAsync()
{
Nav.NavigateTo("/tracks");
// Three independent reads run concurrently. Each loader calls StateHasChanged in its
// finally block so its card updates as soon as its own fetch returns.
await Task.WhenAll(LoadTrackCount(), LoadAlbumCount(), LoadGenreCount());
}
private async Task LoadTrackCount()
{
try
{
var result = await CmsTrackService.GetTrackCountAsync();
_trackCount = result.Success ? result.Value : null;
if (!result.Success)
{
Logger.LogWarning("Dashboard track count failed: {Error}",
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
}
}
finally
{
_tracksLoading = false;
StateHasChanged();
}
}
private async Task LoadAlbumCount()
{
try
{
var result = await CmsTrackService.GetAlbumSummariesAsync();
_albumCount = result.Success && result.Value is not null ? result.Value.Count : null;
if (!result.Success)
{
Logger.LogWarning("Dashboard album summaries failed: {Error}",
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
}
}
finally
{
_albumsLoading = false;
StateHasChanged();
}
}
private async Task LoadGenreCount()
{
try
{
var result = await CmsTrackService.GetGenreSummariesAsync();
_genreCount = result.Success && result.Value is not null ? result.Value.Count : null;
if (!result.Success)
{
Logger.LogWarning("Dashboard genre summaries failed: {Error}",
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
}
}
finally
{
_genresLoading = false;
StateHasChanged();
}
}
private RenderFragment SummaryCard(string label, string icon, Color color, bool loading, int? count) => __builder =>
{
<MudCard Elevation="8" Style="height: 100%;">
<MudCardContent>
<MudStack AlignItems="AlignItems.Center" Spacing="2" Class="py-4">
<MudIcon Icon="@icon" Color="@color" Size="Size.Large" />
@if (loading)
{
<MudProgressCircular Color="@color" Indeterminate="true" Size="Size.Small" />
}
else
{
<MudText Typo="Typo.h3" Color="@color">@(count?.ToString() ?? "—")</MudText>
}
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary text-uppercase">@label</MudText>
</MudStack>
</MudCardContent>
<MudCardActions Class="justify-center pb-4">
<MudButton Variant="Variant.Text" Color="@color" EndIcon="@Icons.Material.Filled.ArrowForward"
OnClick="@(() => Nav.NavigateTo("/tracks"))">
View
</MudButton>
</MudCardActions>
</MudCard>
};
}
+102
View File
@@ -440,4 +440,106 @@ public class CmsTrackService : ICmsTrackService
return Result.CreateFailResult("Failed to generate waveform profile.");
}
}
public async Task<ResultContainer<List<AlbumSummaryDto>>> GetAlbumSummariesAsync(CancellationToken ct = default)
{
var client = _httpClientFactory.CreateClient(ContentCmsClientName);
HttpResponseMessage response;
try
{
response = await client.GetAsync("api/track/albums", ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Content API call failed for album summaries");
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Content API is unreachable.");
}
using (response)
{
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Content API album summaries failed: {Status}", (int)response.StatusCode);
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Failed to load albums.");
}
List<AlbumSummaryDto>? albums;
try
{
albums = await response.Content.ReadFromJsonAsync<List<AlbumSummaryDto>>(ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to deserialize album summaries from Content API response");
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Content API returned an unexpected response.");
}
if (albums is null)
{
_logger.LogError("Content API returned a null album summaries list");
return ResultContainer<List<AlbumSummaryDto>>.CreateFailResult("Content API returned an empty response.");
}
return ResultContainer<List<AlbumSummaryDto>>.CreatePassResult(albums);
}
}
public async Task<ResultContainer<List<GenreSummaryDto>>> GetGenreSummariesAsync(CancellationToken ct = default)
{
var client = _httpClientFactory.CreateClient(ContentCmsClientName);
HttpResponseMessage response;
try
{
response = await client.GetAsync("api/track/genres", ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Content API call failed for genre summaries");
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Content API is unreachable.");
}
using (response)
{
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Content API genre summaries failed: {Status}", (int)response.StatusCode);
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Failed to load genres.");
}
List<GenreSummaryDto>? genres;
try
{
genres = await response.Content.ReadFromJsonAsync<List<GenreSummaryDto>>(ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to deserialize genre summaries from Content API response");
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Content API returned an unexpected response.");
}
if (genres is null)
{
_logger.LogError("Content API returned a null genre summaries list");
return ResultContainer<List<GenreSummaryDto>>.CreateFailResult("Content API returned an empty response.");
}
return ResultContainer<List<GenreSummaryDto>>.CreatePassResult(genres);
}
}
public async Task<ResultContainer<int>> GetTrackCountAsync(CancellationToken ct = default)
{
// Re-use the paged endpoint: a single-item page carries the full TotalCount, so no
// dedicated count endpoint is needed.
var paged = await GetPagedAsync(page: 1, pageSize: 1, sortColumn: null, sortDescending: false, ct);
if (!paged.Success || paged.Value is null)
{
var error = paged.Messages.FirstOrDefault()?.Message ?? "Failed to load track count.";
return ResultContainer<int>.CreateFailResult(error);
}
return ResultContainer<int>.CreatePassResult(paged.Value.TotalCount);
}
}
@@ -82,4 +82,15 @@ public interface ICmsTrackService
/// <c>POST api/track/{entryKey}/waveform</c>. Maps a 404 to a "Track audio not found." failure.
/// </summary>
Task<Result> GenerateWaveformProfileAsync(string entryKey, CancellationToken ct = default);
/// <summary>Returns all distinct albums with track counts from GET api/track/albums.</summary>
Task<ResultContainer<List<AlbumSummaryDto>>> GetAlbumSummariesAsync(CancellationToken ct = default);
/// <summary>Returns all distinct genres with track counts from GET api/track/genres.</summary>
Task<ResultContainer<List<GenreSummaryDto>>> GetGenreSummariesAsync(CancellationToken ct = default);
/// <summary>
/// Returns the total track count by calling GET api/track/page with pageSize=1 and reading TotalCount.
/// </summary>
Task<ResultContainer<int>> GetTrackCountAsync(CancellationToken ct = default);
}