Merge pull request 'Add searchbar to cosplayer detail page for filtering albums' (#100) from feat/cosplayer-detail-search into develop

Reviewed-on: #100
Reviewed-by: Samuele Lorefice <aironenerowork@gmail.com>
This commit was merged in pull request #100.
This commit is contained in:
2026-07-12 18:23:51 +00:00
3 changed files with 129 additions and 43 deletions

View File

@@ -27,10 +27,10 @@ public class PersonController(
/// Gets a person by ID with their associated albums, filtered by access level and paginated.
/// </summary>
/// <param name="id">The person ID.</param>
/// <param name="albumPaging">Pagination parameters for albums.</param>
/// <param name="albumPaging">Pagination, search, and sort parameters for albums.</param>
/// <returns>The person details with albums, or 404 if not found.</returns>
[HttpGet("{id}")]
public ActionResult<PersonDetailedDto> Get([FromRoute] Guid id, [FromQuery] PagedParametersDto albumPaging) {
public ActionResult<PersonDetailedDto> Get([FromRoute] Guid id, [FromQuery] PagedSearchParametersDto albumPaging) {
var uid = authService.GetUserData(User)?.Id;
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
@@ -56,6 +56,33 @@ public class PersonController(
}
if (person.Albums != null) {
if (!string.IsNullOrEmpty(albumPaging.Search))
{
person.Albums = person.Albums
.Where(a => a.Title.Contains(albumPaging.Search, StringComparison.OrdinalIgnoreCase))
.ToList();
}
if (!string.IsNullOrEmpty(albumPaging.SortBy))
{
person.Albums = albumPaging.SortBy.ToLowerInvariant() switch
{
"name" => albumPaging.SortAsc
? [..person.Albums.OrderBy(a => a.Title)]
: [..person.Albums.OrderByDescending(a => a.Title)],
"created" => albumPaging.SortAsc
? [..person.Albums.OrderBy(a => a.CreatedAt)]
: [..person.Albums.OrderByDescending(a => a.CreatedAt)],
"updated" => albumPaging.SortAsc
? [..person.Albums.OrderBy(a => a.UpdatedAt)]
: [..person.Albums.OrderByDescending(a => a.UpdatedAt)],
"assets" => albumPaging.SortAsc
? [..person.Albums.OrderBy(a => a.Assets?.Count(asset => asset.DeletedAt == null) ?? 0)]
: [..person.Albums.OrderByDescending(a => a.Assets?.Count(asset => asset.DeletedAt == null) ?? 0)],
_ => person.Albums
};
}
person.Albums = person.Albums
.Skip(albumPaging.Page * albumPaging.PageSize)
.Take(albumPaging.PageSize)

View File

@@ -5,6 +5,7 @@
@using Microsoft.Extensions.Options
@using Microsoft.JSInterop
@using MilkStream.Client.Components.Layout
@using MilkStream.Client.Components.Shared
@using MilkStream.Client.Services
@using System.Globalization
@implements IAsyncDisposable
@@ -69,8 +70,18 @@
</div>
@* Albums header *@
<div class="d-flex align-items-center justify-content-between mt-4 mb-3 px-2">
<div class="d-flex align-items-center mt-4 mb-2 px-2">
<h4 class="m-0"><i class="bi bi-collection"></i> Albums</h4>
</div>
<SortFilterBar SearchQuery="@albumSearchQuery"
SearchQueryChanged="@((string? v) => { albumSearchQuery = v; _ = ReloadAlbums(); })"
SortBy="@albumSortBy"
SortByChanged="@((string? v) => { albumSortBy = v; _ = ReloadAlbums(); })"
SortAsc="@albumSortAsc"
SortAscChanged="@((bool v) => { albumSortAsc = v; _ = ReloadAlbums(); })"
SortOptions="@albumSortOptions">
<ActionButtons>
<div class="d-flex gap-2 align-items-start">
@if (allAlbums.Count > 0) {
<div class="btn-group btn-group-sm">
@@ -107,7 +118,8 @@
</button>
}
</div>
</div>
</ActionButtons>
</SortFilterBar>
@if (allAlbums.Count > 0) {
<div class="@("album-grid " + viewMode)">
@@ -192,6 +204,19 @@
HashSet<Guid> selectedAlbumIds = [];
List<string> bannerCoverUrls = [];
// Search & sort
string? albumSearchQuery;
string? albumSortBy;
bool albumSortAsc = true;
Dictionary<string, string> albumSortOptions = new()
{
["Name"] = "name",
["Created"] = "created",
["Updated"] = "updated",
["Asset count"] = "assets"
};
// Infinite scroll
List<AlbumPreviewDto> allAlbums = [];
int currentAlbumPage;
@@ -236,7 +261,7 @@
currentAlbumPage = 0;
hasMoreAlbums = true;
person = await personService.GetByIdAsync(Id);
person = await personService.GetByIdAsync(Id, 0, AlbumPageSize);
if (person?.Albums != null) {
allAlbums = person.Albums;
hasMoreAlbums = person.Albums.Count >= AlbumPageSize;
@@ -253,13 +278,28 @@
isLoading = false;
}
async Task ReloadAlbums() {
currentAlbumPage = 0;
hasMoreAlbums = true;
person = await personService.GetByIdAsync(Id, 0, AlbumPageSize, albumSearchQuery, albumSortBy, albumSortAsc);
allAlbums.Clear();
if (person?.Albums != null) {
allAlbums = person.Albums;
hasMoreAlbums = person.Albums.Count >= AlbumPageSize;
currentAlbumPage = 1;
}
StateHasChanged();
}
[JSInvokable]
public async Task LoadMoreAlbums() {
if (isLoadingMore || !hasMoreAlbums) return;
isLoadingMore = true;
StateHasChanged();
var fresh = await personService.GetByIdAsync(Id, currentAlbumPage, AlbumPageSize);
var fresh = await personService.GetByIdAsync(Id, currentAlbumPage, AlbumPageSize, albumSearchQuery, albumSortBy, albumSortAsc);
if (fresh?.Albums?.Count > 0) {
allAlbums.AddRange(fresh.Albums);
currentAlbumPage++;

View File

@@ -43,11 +43,30 @@ public sealed class PersonService(
/// <param name="id">The person ID.</param>
/// <param name="albumPage">Optional zero-based page number for albums.</param>
/// <param name="albumPageSize">Optional number of albums per page.</param>
/// <param name="search">Optional search term to filter album titles.</param>
/// <param name="sortBy">Optional field to sort albums by (e.g. "name", "created", "updated", "assets").</param>
/// <param name="sortAsc">Whether to sort ascending. Default is <c>true</c>.</param>
/// <returns>The person details, or null if not found.</returns>
public async Task<PersonDetailedDto?> GetByIdAsync(Guid id, int? albumPage = null, int? albumPageSize = null) {
public async Task<PersonDetailedDto?> GetByIdAsync(Guid id, int? albumPage = null, int? albumPageSize = null, string? search = null, string? sortBy = null, bool sortAsc = true) {
var url = $"/api/person/{id}";
if (albumPage.HasValue && albumPageSize.HasValue)
var hasParams = false;
if (albumPage.HasValue && albumPageSize.HasValue) {
url += $"?page={albumPage}&pageSize={albumPageSize}";
hasParams = true;
}
if (!string.IsNullOrEmpty(search)) {
url += $"{(hasParams ? "&" : "?")}search={Uri.EscapeDataString(search)}";
hasParams = true;
}
if (!string.IsNullOrEmpty(sortBy)) {
url += $"{(hasParams ? "&" : "?")}sortBy={Uri.EscapeDataString(sortBy)}";
hasParams = true;
}
url += $"{(hasParams ? "&" : "?")}sortAsc={sortAsc.ToString().ToLowerInvariant()}";
var response = await Client.GetAsync(url);