Added SearchPageBase class and implemented it on the voice works page and circles page. Turned off AOT compilation.

This commit is contained in:
2025-11-27 18:28:57 -05:00
parent 6817fa7353
commit 403c436a34
10 changed files with 341 additions and 321 deletions

View File

@@ -2,42 +2,32 @@
@using JSMR.Application.Circles.Queries.Search
@using JSMR.Application.Common.Search
@using JSMR.UI.Blazor.Components
@using JSMR.UI.Blazor.Filters
@using JSMR.UI.Blazor.Services
@using JSMR.UI.Blazor.Shared
@inject VoiceWorksClient Client
@inject IJSRuntime JS
@inject HttpClient Http
@inherits SearchPageBase<CircleFilterState, CircleSearchItem>
<PageTitle>Circles</PageTitle>
<h1>Circles</h1>
<div class="search-filter-control-container">
<div class="search-filter-control-span-3">
<MudTextField @bind-Value="Keywords" Placeholder="Circle Name or Maker Id" Immediate="true" DebounceInterval="500" Variant="MudBlazor.Variant.Outlined" Margin="Margin.Dense" Adornment="@Adornment.End" AdornmentIcon="@Icons.Material.Outlined.Search" />
</div>
<div class="search-filter-control-span-1">
<MudSelect @bind-Value="SelectedCircleStatus" Placeholder="Circle Status" Dense="true" Variant="MudBlazor.Variant.Outlined" Margin="Margin.Dense">
<MudSelectItem Value="@CircleStatus.NotBlacklisted.ToString()">Not Blacklisted</MudSelectItem>
<MudSelectItem Value="@CircleStatus.Favorited.ToString()">Favorite</MudSelectItem>
<MudSelectItem Value="@CircleStatus.Blacklisted.ToString()">Blacklisted</MudSelectItem>
<MudSelectItem Value="@CircleStatus.Spam.ToString()">Spam</MudSelectItem>
<MudSelectItem Value="@String.Empty">All</MudSelectItem>
</MudSelect>
</div>
</div>
<CircleFilters Value="@State" ValueChanged="UpdateAsync"></CircleFilters>
@if (searchResults is null)
@if (Result is null)
{
<p>Loading…</p>
}
else if (searchResults.Items.Length == 0)
else if (Result.Items.Length == 0)
{
<p>No results.</p>
}
else
{
<div class="circle-item-container-2">
@foreach (var item in searchResults.Items)
@foreach (var item in Result.Items)
{
<div class="circle-item">
<JImage @key="item.CircleId" ContainerClass="j-circle-image-container-2" ImageClass="j-circle-image-2" Source="@ImageUrlProvider.GetImageURL(item.LatestProductId, item.LatestVoiceWorkHasImage ?? false, item.LatestVoiceWorkSalesDate, "main")" />
@@ -90,101 +80,11 @@ else
}
</div>
<MudDataGrid Items="@searchResults.Items" Style="table-layout: fixed">
<Columns>
<TemplateColumn HeaderStyle="width: 12em">
<CellTemplate>
<JImage ContainerClass="j-circle-image-container-mini" ImageClass="j-circle-image-mini" Source="@ImageUrlProvider.GetImageURL(context.Item.LatestProductId, context.Item.LatestVoiceWorkHasImage ?? false, context.Item.LatestVoiceWorkSalesDate, "main")" />
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Name">
<CellTemplate>
<div class="circle-name-container">
<div class="circle-name">@context.Item.Name</div>
</div>
</CellTemplate>
</TemplateColumn>
@* <TemplateColumn Title="Maker Id">
<CellTemplate>
<MudChip T="string" Label="true" Class="maker-id">@context.Item.MakerId</MudChip>
</CellTemplate>
</TemplateColumn> *@
@* <PropertyColumn Property="x => x.Name" Title="Name" />
<PropertyColumn Property="x => x.MakerId" Title="Maker Id" HeaderStyle="width: 12em" /> *@
<TemplateColumn Title="Status" HeaderStyle="width: 10em">
<CellTemplate>
@if (context.Item.Favorite)
{
<MudChip T="string" Label="true" Color="Color.Info" Style="width: 100%">Favorite</MudChip>
}
else if (context.Item.Blacklisted)
{
<MudChip T="string" Label="true" Color="Color.Warning" Style="color:black; width: 100%">Blacklisted</MudChip>
}
else if (context.Item.Spam)
{
<MudChip T="string" Label="true" Color="Color.Error" Style="width: 100%">Spam</MudChip>
}
else
{
<MudChip T="string" Label="true" Style="width: 100%">Normal</MudChip>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Rating" HeaderStyle="width: 8em">
<CellTemplate>
@if (context.Item.Releases > 0)
{
<div class="circle-star-container">
<div class="circle-star @GetStarRatingClass(context.Item)"></div>
</div>
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Downloads" Title="Downloads" HeaderStyle="width: 12em" />
<PropertyColumn Property="x => x.Releases" Title="Releases" HeaderStyle="width: 12em" />
<PropertyColumn Property="x => x.Pending" Title="Pending" HeaderStyle="width: 12em" />
@* <TemplateColumn Title="First Release Date" HeaderStyle="width: 14em">
<CellTemplate>
<MudText>@context.Item.FirstReleaseDate?.ToString("MMMM d, yyyy")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Latest Release Date" HeaderStyle="width: 14em">
<CellTemplate>
<MudText>@context.Item.LatestReleaseDate?.ToString("MMMM d, yyyy")</MudText>
</CellTemplate>
</TemplateColumn> *@
</Columns>
</MudDataGrid>
@* <div class="items-container circle-items-container">
@foreach (var item in searchResults.Items)
{
<MudPaper Outlined="true">
<JImage @key="item.CircleId" ContainerClass="j-circle-image-container" ImageClass="j-circle-image" Source="@ImageUrlProvider.GetImageURL(item.LatestProductId, item.LatestVoiceWorkHasImage ?? false, item.LatestVoiceWorkSalesDate, "main")" />
<MudText Typo="Typo.h6">@item.Name</MudText>
<div>@item.Releases</div>
<div>@item.Pending</div>
@if (item.Releases > 0)
{
<div class="circle-star-container">
<div class="circle-star @GetStarRatingClass(item)"></div>
</div>
}
<div>@item.Downloads</div>
<MudChip T="string" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Download" Color="Color.Info">@item.Downloads.ToString("n0")</MudChip>
<div>@item.FirstReleaseDate?.ToString("MMMM d, yyyy")</div>
<div>@item.LatestReleaseDate?.ToString("MMMM d, yyyy")</div>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(item.Favorite? Color.Secondary: Color.Dark)" />
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(item.Blacklisted? Color.Secondary: Color.Dark)" />
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.RestoreFromTrash" Color="@(item.Spam? Color.Secondary: Color.Dark)" />
</MudPaper>
}
</div> *@
<JPagination @bind-PageNumber="PageNumber" @bind-PageSize="PageSize" @bind-TotalItems="searchResults.TotalItems" />
<JPagination PageNumber="@State.PageNumber"
PageNumberChanged="@(pageNumber => UpdateAsync(State with { PageNumber = pageNumber }))"
PageSize="@State.PageSize"
PageSizeChanged="@(pageSize => UpdateAsync(State with { PageSize = pageSize, PageNumber = 1 }))"
@bind-TotalItems="Result.TotalItems" />
}
<style>
@@ -275,97 +175,20 @@ else
</style>
@code {
private string? keywords;
protected override CircleFilterState ParseStateFromUri(string absoluteUri)
=> CircleFilterState.FromQuery(new Uri(absoluteUri).Query);
public string? Keywords
protected override string BuildUri(CircleFilterState state)
{
get { return keywords; }
set
{
keywords = value;
_ = UpdateDataAsync(true);
}
var basePath = new Uri(Nav.Uri).GetLeftPart(UriPartial.Path);
return Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(basePath, state.ToQuery());
}
private string circleStatus = string.Empty;
protected override bool IsThisPage(string absoluteUri)
=> Nav.ToBaseRelativePath(absoluteUri).StartsWith("circles", StringComparison.OrdinalIgnoreCase);
public string SelectedCircleStatus
{
get { return circleStatus; }
set
{
circleStatus = value;
_ = UpdateDataAsync(true);
}
}
private int pageNumber = 1;
public int PageNumber
{
get { return pageNumber; }
set
{
pageNumber = value;
_ = UpdateDataAsync(false);
}
}
int pageSize = 100;
public int PageSize
{
get { return pageSize; }
set
{
pageSize = value;
_ = UpdateDataAsync(true);
}
}
SearchResult<CircleSearchItem>? searchResults;
protected override Task OnInitializedAsync()
{
_ = LoadCirclesAsync();
return Task.CompletedTask;
}
private async Task LoadCirclesAsync()
{
SearchCirclesRequest request = new(
Options: new()
{
PageNumber = PageNumber,
PageSize = pageSize,
Criteria = new()
{
Name = Keywords,
Status = string.IsNullOrWhiteSpace(SelectedCircleStatus) == false ? Enum.Parse<CircleStatus>(SelectedCircleStatus) : null
},
SortOptions =
[
new(CircleSortField.Name, Application.Common.Search.SortDirection.Ascending)
]
}
);
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
var result = await Client.SearchAsync(request);
searchResults = result?.Results ?? new();
await InvokeAsync(StateHasChanged);
}
private async Task UpdateDataAsync(bool resetPageNumber)
{
if (resetPageNumber)
pageNumber = 1;
await LoadCirclesAsync();
}
protected override Task<SearchResult<CircleSearchItem>> ExecuteSearchAsync(CircleFilterState state, CancellationToken ct)
=> Client.SearchAsync(state.ToSearchRequest(), ct).ContinueWith(t => t.Result?.Results ?? new SearchResult<CircleSearchItem>(), ct);
private string GetStarRatingClass(CircleSearchItem item)
{

View File

@@ -4,136 +4,41 @@
@using JSMR.UI.Blazor.Components
@using JSMR.UI.Blazor.Filters
@using JSMR.UI.Blazor.Services
@using JSMR.UI.Blazor.Shared
@using Microsoft.AspNetCore.WebUtilities
@inject VoiceWorksClient Client
@inject IJSRuntime JS
@inject NavigationManager NavigationManager
@implements IAsyncDisposable;
@inherits SearchPageBase<VoiceWorkFilterState, VoiceWorkSearchResult>
<PageTitle>Voice Works</PageTitle>
<h3>Voice Works</h3>
<VoiceWorkFilters Value="@FilterState" ValueChanged="OnFilterStateChanged" />
<JProductCollection Products="searchResults?.Items"></JProductCollection>
<VoiceWorkFilters Value="@State" ValueChanged="UpdateAsync" />
<JProductCollection Products="Result?.Items"></JProductCollection>
@if (searchResults is not null)
@if (Result is not null)
{
<JPagination PageNumber="@FilterState.PageNumber"
PageNumberChanged="@(pageNumber => OnFilterStateChanged(FilterState with { PageNumber = pageNumber }))"
PageSize="@FilterState.PageSize"
PageSizeChanged="@(pageSize => OnFilterStateChanged(FilterState with { PageSize = pageSize, PageNumber = 1 }))"
@bind-TotalItems="searchResults.TotalItems" />
<JPagination PageNumber="@State.PageNumber"
PageNumberChanged="@(pageNumber => UpdateAsync(State with { PageNumber = pageNumber }))"
PageSize="@State.PageSize"
PageSizeChanged="@(pageSize => UpdateAsync(State with { PageSize = pageSize, PageNumber = 1 }))"
@bind-TotalItems="Result.TotalItems" />
}
@code {
private bool _suppressNextLocation;
private bool _isAlive = true;
//private bool IsLoading;
private CancellationTokenSource _cts = new();
protected override VoiceWorkFilterState ParseStateFromUri(string absoluteUri)
=> VoiceWorkFilterState.FromQuery(new Uri(absoluteUri).Query);
VoiceWorkFilterState FilterState = new();
SearchResult<VoiceWorkSearchResult>? searchResults;
protected override async Task OnInitializedAsync()
protected override string BuildUri(VoiceWorkFilterState state)
{
NavigationManager.LocationChanged += OnLocationChanged;
FilterState = VoiceWorkFilterState.FromQuery(new Uri(NavigationManager.Uri).Query);
await RunSearchAsync();
var basePath = new Uri(Nav.Uri).GetLeftPart(UriPartial.Path);
return Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(basePath, state.ToQuery());
}
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
if (_suppressNextLocation)
{
_suppressNextLocation = false;
return;
}
protected override bool IsThisPage(string absoluteUri)
=> Nav.ToBaseRelativePath(absoluteUri).StartsWith("voiceworks", StringComparison.OrdinalIgnoreCase);
if (!_isAlive)
return;
if (!IsThisPage(e.Location))
return;
string query = NavigationManager.ToAbsoluteUri(e.Location).Query;
VoiceWorkFilterState next = VoiceWorkFilterState.FromQuery(query);
if (next != FilterState)
{
FilterState = next;
await InvokeAsync(StateHasChanged);
RunSearchAsync();
}
}
private bool IsThisPage(string absoluteUri)
{
string baseRelativePath = NavigationManager.ToBaseRelativePath(absoluteUri);
return baseRelativePath.StartsWith("voiceworks", StringComparison.OrdinalIgnoreCase);
}
async Task OnFilterStateChanged(VoiceWorkFilterState next)
{
if (next == FilterState)
return;
FilterState = next;
// Optional immediate paint if you want instant visual feedback on filters:
//await InvokeAsync(StateHasChanged);
UpdateUrl(next, false);
RunSearchAsync();
}
void UpdateUrl(VoiceWorkFilterState next, bool replace)
{
string basePath = new Uri(NavigationManager.Uri).GetLeftPart(UriPartial.Path);
string uri = QueryHelpers.AddQueryString(basePath, next.ToQuery());
_suppressNextLocation = true;
NavigationManager.NavigateTo(uri, replace: replace);
}
async Task RunSearchAsync()
{
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
try
{
//IsLoading = true; // show skeletons NOW
//StateHasChanged(); // paint immediately
_cts.Cancel();
_cts = new();
SearchVoiceWorksResponse? response = await Client.SearchAsync(FilterState.ToSearchRequest(), _cts.Token);
searchResults = response?.Results;
}
catch (OperationCanceledException)
{
}
finally
{
if (_isAlive)
{
//IsLoading = false; // hide skeletons
await InvokeAsync(StateHasChanged);
}
}
}
public async ValueTask DisposeAsync()
{
_isAlive = false;
NavigationManager.LocationChanged -= OnLocationChanged;
_cts.Cancel();
_cts.Dispose();
await Task.CompletedTask;
}
protected override Task<SearchResult<VoiceWorkSearchResult>> ExecuteSearchAsync(VoiceWorkFilterState state, CancellationToken ct)
=> Client.SearchAsync(state.ToSearchRequest(), ct).ContinueWith(t => t.Result?.Results ?? new SearchResult<VoiceWorkSearchResult>(), ct);
}