Added SearchPageBase class and implemented it on the voice works page and circles page. Turned off AOT compilation.
This commit is contained in:
41
JSMR.UI.Blazor/Components/CircleFilters.razor
Normal file
41
JSMR.UI.Blazor/Components/CircleFilters.razor
Normal file
@@ -0,0 +1,41 @@
|
||||
@using JSMR.Application.Circles.Queries.Search
|
||||
@using JSMR.UI.Blazor.Filters
|
||||
@using JSMR.UI.Blazor.Services
|
||||
|
||||
<div class="search-filter-control-container">
|
||||
<div class="search-filter-control-span-3">
|
||||
<BitTextField Placeholder="Circle Name or Maker Id"
|
||||
Immediate="true"
|
||||
DebounceTime="500"
|
||||
Value="@Value.Keywords"
|
||||
ValueChanged="@(value => Update(Value with { Keywords = value }))" />
|
||||
</div>
|
||||
<div class="search-filter-control-span-1">
|
||||
<BitDropdown Items="CircleStatuses"
|
||||
Placeholder="Select..."
|
||||
TItem="BitDropdownItem<CircleStatus?>"
|
||||
TValue="CircleStatus?"
|
||||
Value="@Value.Status"
|
||||
ValueChanged="@(value => Update(Value with { Status = value }))" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public CircleFilterState Value { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CircleFilterState> ValueChanged { get; set; }
|
||||
|
||||
List<BitDropdownItem<CircleStatus?>> CircleStatuses =
|
||||
[
|
||||
new() { Text = "Not Blacklisted", Value = CircleStatus.NotBlacklisted },
|
||||
new() { Text = "Favorites", Value = CircleStatus.Favorited },
|
||||
new() { Text = "Blacklisted", Value = CircleStatus.Blacklisted },
|
||||
new() { Text = "Spam", Value = CircleStatus.Spam },
|
||||
new() { Text = "All", Value = null }
|
||||
];
|
||||
|
||||
private Task Update(CircleFilterState next)
|
||||
=> ValueChanged.InvokeAsync(next with { PageNumber = 1 });
|
||||
}
|
||||
64
JSMR.UI.Blazor/Filters/CircleFilterState.cs
Normal file
64
JSMR.UI.Blazor/Filters/CircleFilterState.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Globalization;
|
||||
|
||||
namespace JSMR.UI.Blazor.Filters;
|
||||
|
||||
public sealed record CircleFilterState : IFilterState<SearchCirclesRequest>
|
||||
{
|
||||
public string? Keywords { get; init; }
|
||||
public CircleStatus? Status { get; init; }
|
||||
public int PageNumber { get; init; } = 1;
|
||||
public int PageSize { get; init; } = 100;
|
||||
|
||||
public QueryParameters ToQuery()
|
||||
{
|
||||
QueryParameters query = [];
|
||||
|
||||
query.Set("keywords", Keywords);
|
||||
|
||||
if (Status is not null)
|
||||
query.Set("circle", Status.ToString());
|
||||
|
||||
if (PageNumber != 1)
|
||||
query.Set("pageNumber", PageNumber.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
if (PageSize != 100)
|
||||
query.Set("pageSize", PageSize.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public static CircleFilterState FromQuery(string query)
|
||||
{
|
||||
QueryParameters queryParameters = new(QueryHelpers.ParseQuery(query));
|
||||
|
||||
return new CircleFilterState
|
||||
{
|
||||
Keywords = queryParameters.GetValue("keywords"),
|
||||
Status = queryParameters.GetEnum<CircleStatus>("circle"),
|
||||
PageNumber = Math.Max(1, queryParameters.GetInteger("pageNumber", 1)),
|
||||
PageSize = queryParameters.GetInteger("pageSize", defaultValue: 100)
|
||||
};
|
||||
}
|
||||
|
||||
public SearchCirclesRequest ToSearchRequest()
|
||||
{
|
||||
return new(
|
||||
Options: new()
|
||||
{
|
||||
PageNumber = PageNumber,
|
||||
PageSize = PageSize,
|
||||
Criteria = new()
|
||||
{
|
||||
Name = Keywords,
|
||||
Status = Status
|
||||
},
|
||||
SortOptions =
|
||||
[
|
||||
new(CircleSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
||||
]
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
7
JSMR.UI.Blazor/Filters/IFilterState.cs
Normal file
7
JSMR.UI.Blazor/Filters/IFilterState.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace JSMR.UI.Blazor.Filters;
|
||||
|
||||
public interface IFilterState<TSearchRequest>
|
||||
{
|
||||
QueryParameters ToQuery();
|
||||
TSearchRequest ToSearchRequest();
|
||||
}
|
||||
51
JSMR.UI.Blazor/Filters/QueryParameters.cs
Normal file
51
JSMR.UI.Blazor/Filters/QueryParameters.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace JSMR.UI.Blazor.Filters;
|
||||
|
||||
public class QueryParameters : Dictionary<string, string?>
|
||||
{
|
||||
public QueryParameters()
|
||||
{
|
||||
|
||||
}
|
||||
public QueryParameters(IEnumerable<KeyValuePair<string, StringValues>> items)
|
||||
{
|
||||
foreach (var kv in items)
|
||||
{
|
||||
this[kv.Key] = kv.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public static QueryParameters FromQueryString(string query)
|
||||
=> new(QueryHelpers.ParseQuery(query));
|
||||
|
||||
public void Set(string key, string? value, bool includeWhenEmpty = false)
|
||||
{
|
||||
if (includeWhenEmpty || !string.IsNullOrWhiteSpace(value))
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
public bool Has(string key) => ContainsKey(key);
|
||||
|
||||
public string? GetValue(string key) => TryGetValue(key, out var value) ? value!.ToString() : null;
|
||||
public string[] GetValues(string key) => GetValue(key)?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? [];
|
||||
|
||||
public int GetInteger(string key, int defaultValue)
|
||||
=> TryGetValue(key, out var value) && int.TryParse(value, out int intValue) ? intValue : defaultValue;
|
||||
|
||||
public int[] GetIntegers(string key) => [.. GetValues(key).Select(int.Parse)];
|
||||
|
||||
public T? GetEnum<T>(string key) where T : struct, Enum
|
||||
=> TryGetValue(key, out var value) && Enum.TryParse<T>(value!, true, out var enumValue) ? enumValue : null;
|
||||
|
||||
public TEnum[] GetEnums<TEnum>(string key) where TEnum : struct, Enum
|
||||
=> GetValues(key).Select(value =>
|
||||
Enum.TryParse<TEnum>(value, true, out var enumValue)
|
||||
? enumValue
|
||||
: (TEnum?)null).Where(e => e is not null).Select(e => e!.Value).ToArray()
|
||||
?? [];
|
||||
|
||||
public DateOnly? GetDate(string key, string? format = "yyyy-MM-dd")
|
||||
=> DateOnly.TryParseExact(GetValue(key), format, out var date) ? date : null;
|
||||
}
|
||||
@@ -8,6 +8,13 @@ using System.Globalization;
|
||||
|
||||
namespace JSMR.UI.Blazor.Filters;
|
||||
|
||||
public abstract record FilterStateBase<TSearchRequest> : IFilterState<TSearchRequest>
|
||||
{
|
||||
public abstract QueryParameters ToQuery();
|
||||
public abstract TSearchRequest ToSearchRequest();
|
||||
//public static abstract IFilterState<TSearchRequest> FromQuery(string query);
|
||||
}
|
||||
|
||||
public sealed record VoiceWorkFilterState
|
||||
{
|
||||
// Core
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
|
||||
<RunAOTCompilation>true</RunAOTCompilation>
|
||||
<RunAOTCompilation>false</RunAOTCompilation>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
<MudDrawer @bind-Open="@_open" ClipMode="_clipMode" Breakpoint="@_breakpoint" Elevation="1" Variant="@DrawerVariant.Mini">
|
||||
<MudNavMenu>
|
||||
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home" href="/">Home</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Tag" href="tags">Tags</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Circle" href="circles">Circles</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Person" href="creators">Creators</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.SatelliteAlt" href="scanner">Scanner</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.StackedLineChart" href="analytics">Analytics</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Headphones" href="voiceworks">Voice Works</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Tag" href="tags">Tags</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Circle" href="circles">Circles</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Person" href="creators">Creators</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.SatelliteAlt" href="scanner">Scanner</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.StackedLineChart" href="analytics">Analytics</MudNavLink>
|
||||
<MudNavLink Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Headphones" href="voiceworks">Voice Works</MudNavLink>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
<MudMainContent Class="pt-18 px-8">
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
122
JSMR.UI.Blazor/Shared/SearchPageBase.cs
Normal file
122
JSMR.UI.Blazor/Shared/SearchPageBase.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using JSMR.Application.Common.Search;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace JSMR.UI.Blazor.Shared;
|
||||
|
||||
public abstract class SearchPageBase<TState, TItem> : ComponentBase, IAsyncDisposable
|
||||
where TState : notnull
|
||||
{
|
||||
[Inject]
|
||||
protected NavigationManager Nav { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IJSRuntime JS { get; set; } = default!;
|
||||
|
||||
protected TState State { get; private set; } = default!;
|
||||
protected SearchResult<TItem>? Result { get; private set; }
|
||||
protected bool IsLoading { get; private set; }
|
||||
|
||||
private bool _suppressNextLocation;
|
||||
private bool _isAlive = true;
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
Nav.LocationChanged += OnLocationChanged;
|
||||
State = ParseStateFromUri(Nav.Uri);
|
||||
|
||||
await RunSearchAsync();
|
||||
}
|
||||
|
||||
protected async Task UpdateAsync(TState next)
|
||||
{
|
||||
if (Equals(next, State))
|
||||
return;
|
||||
|
||||
State = next;
|
||||
NavigateToState(next);
|
||||
|
||||
_ = RunSearchAsync();
|
||||
}
|
||||
|
||||
private void NavigateToState(TState next)
|
||||
{
|
||||
string uri = BuildUri(next);
|
||||
_suppressNextLocation = true;
|
||||
Nav.NavigateTo(uri);
|
||||
}
|
||||
|
||||
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
if (_suppressNextLocation)
|
||||
{
|
||||
_suppressNextLocation = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isAlive)
|
||||
return;
|
||||
|
||||
if (!IsThisPage(e.Location))
|
||||
return;
|
||||
|
||||
TState next = ParseStateFromUri(e.Location);
|
||||
|
||||
if (Equals(next, State))
|
||||
return;
|
||||
|
||||
State = next;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
_ = RunSearchAsync();
|
||||
}
|
||||
|
||||
private async Task RunSearchAsync()
|
||||
{
|
||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
||||
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
//StateHasChanged();
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
Result = await ExecuteSearchAsync(State, _cancellationTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_isAlive)
|
||||
{
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract TState ParseStateFromUri(string absoluteUri);
|
||||
protected abstract string BuildUri(TState state);
|
||||
protected abstract bool IsThisPage(string absoluteUri);
|
||||
protected abstract Task<SearchResult<TItem>> ExecuteSearchAsync(TState state, CancellationToken cancellationToken);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_isAlive = false;
|
||||
|
||||
Nav.LocationChanged -= OnLocationChanged;
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user