Updated UI. Fixed circle search performance.
All checks were successful
ci / build-test (push) Successful in 1m43s
ci / publish-image (push) Has been skipped

This commit is contained in:
2025-11-10 18:55:46 -05:00
parent 840bec72d2
commit 9cd9230cec
43 changed files with 1105 additions and 164 deletions

View File

@@ -1,143 +1,206 @@
using JSMR.Application.Circles.Queries.Search;
using JSMR.Application.Common.Search;
using JSMR.Domain.Entities;
using JSMR.Infrastructure.Common.Queries;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
namespace JSMR.Infrastructure.Data.Repositories.Circles;
public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleSearchItem, CircleSearchCriteria, CircleSortField, CircleSearchItem>, ICircleSearchProvider
public class CircleQuery
{
protected override IQueryable<CircleSearchItem> GetBaseQuery()
public required Circle Circle { get; init; }
}
public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleSearchItem, CircleSearchCriteria, CircleSortField, CircleQuery>, ICircleSearchProvider
{
protected override IQueryable<CircleQuery> GetBaseQuery()
{
// Project from Circles so we can use correlated subqueries per CircleId.
var q =
from c in context.Circles.AsNoTracking()
select new CircleSearchItem
select new CircleQuery
{
CircleId = c.CircleId,
Name = c.Name,
MakerId = c.MakerId,
Favorite = c.Favorite,
Blacklisted = c.Blacklisted,
Spam = c.Spam,
// Aggregates
Downloads = context.VoiceWorks
.Where(v => v.CircleId == c.CircleId)
.Select(v => (int?)v.Downloads) // make nullable for Sum over empty set
.Sum() ?? 0,
Releases = context.VoiceWorks
.Count(v => v.CircleId == c.CircleId && v.SalesDate != null),
Pending = context.VoiceWorks
.Count(v => v.CircleId == c.CircleId && v.ExpectedDate != null),
FirstReleaseDate = context.VoiceWorks
.Where(v => v.CircleId == c.CircleId)
.Select(v => v.SalesDate)
.Min(),
LatestReleaseDate = context.VoiceWorks
.Where(v => v.CircleId == c.CircleId)
.Select(v => v.SalesDate)
.Max(),
// "Latest" by ProductId length, then value
LatestProductId = context.VoiceWorks
.Where(v => v.CircleId == c.CircleId)
.OrderByDescending(v => v.ProductId.Length)
.ThenByDescending(v => v.ProductId)
.Select(v => v.ProductId)
.FirstOrDefault(),
// If you want these two in base query too:
LatestVoiceWorkHasImage = context.VoiceWorks
.Where(v => v.CircleId == c.CircleId)
.OrderByDescending(v => v.ProductId.Length)
.ThenByDescending(v => v.ProductId)
.Select(v => (bool?)v.HasImage)
.FirstOrDefault(),
LatestVoiceWorkSalesDate = context.VoiceWorks
.Where(v => v.CircleId == c.CircleId)
.OrderByDescending(v => v.ProductId.Length)
.ThenByDescending(v => v.ProductId)
.Select(v => v.SalesDate)
.FirstOrDefault()
Circle = c
};
return q;
}
protected override IQueryable<CircleSearchItem> ApplyFilters(IQueryable<CircleSearchItem> query, CircleSearchCriteria criteria)
//protected override IQueryable<CircleSearchItem> GetBaseQuery()
//{
// // Project from Circles so we can use correlated subqueries per CircleId.
// var q =
// from c in context.Circles.AsNoTracking()
// select new CircleSearchItem
// {
// CircleId = c.CircleId,
// Name = c.Name,
// MakerId = c.MakerId,
// Favorite = c.Favorite,
// Blacklisted = c.Blacklisted,
// Spam = c.Spam,
// // Aggregates
// Downloads = context.VoiceWorks
// .Where(v => v.CircleId == c.CircleId)
// .Select(v => (int?)v.Downloads) // make nullable for Sum over empty set
// .Sum() ?? 0,
// Releases = context.VoiceWorks
// .Count(v => v.CircleId == c.CircleId && v.SalesDate != null),
// Pending = context.VoiceWorks
// .Count(v => v.CircleId == c.CircleId && v.ExpectedDate != null),
// FirstReleaseDate = context.VoiceWorks
// .Where(v => v.CircleId == c.CircleId)
// .Select(v => v.SalesDate)
// .Min(),
// LatestReleaseDate = context.VoiceWorks
// .Where(v => v.CircleId == c.CircleId)
// .Select(v => v.SalesDate)
// .Max(),
// // "Latest" by ProductId length, then value
// LatestProductId = context.VoiceWorks
// .Where(v => v.CircleId == c.CircleId)
// .OrderByDescending(v => v.ProductId.Length)
// .ThenByDescending(v => v.ProductId)
// .Select(v => v.ProductId)
// .FirstOrDefault(),
// // If you want these two in base query too:
// LatestVoiceWorkHasImage = context.VoiceWorks
// .Where(v => v.CircleId == c.CircleId)
// .OrderByDescending(v => v.ProductId.Length)
// .ThenByDescending(v => v.ProductId)
// .Select(v => (bool?)v.HasImage)
// .FirstOrDefault(),
// LatestVoiceWorkSalesDate = context.VoiceWorks
// .Where(v => v.CircleId == c.CircleId)
// .OrderByDescending(v => v.ProductId.Length)
// .ThenByDescending(v => v.ProductId)
// .Select(v => v.SalesDate)
// .FirstOrDefault()
// };
// return q;
//}
protected override IQueryable<CircleQuery> ApplyFilters(IQueryable<CircleQuery> query, CircleSearchCriteria criteria)
{
if (!string.IsNullOrWhiteSpace(criteria.Name))
{
var term = $"%{criteria.Name.Trim()}%";
query = query.Where(x =>
EF.Functions.Like(x.Name, term) ||
EF.Functions.Like(x.MakerId, term));
EF.Functions.Like(x.Circle.Name, term) ||
EF.Functions.Like(x.Circle.MakerId, term));
}
switch (criteria.Status)
{
case CircleStatus.NotBlacklisted:
query = query.Where(x => !x.Blacklisted);
query = query.Where(x => !x.Circle.Blacklisted);
break;
case CircleStatus.Favorited:
query = query.Where(x => x.Favorite);
query = query.Where(x => x.Circle.Favorite);
break;
case CircleStatus.Blacklisted:
query = query.Where(x => x.Blacklisted);
query = query.Where(x => x.Circle.Blacklisted);
break;
case CircleStatus.Spam:
query = query.Where(x => x.Spam);
query = query.Where(x => x.Circle.Spam);
break;
}
return query;
}
protected override Expression<Func<CircleSearchItem, object>> GetSortExpression(CircleSortField field) => field switch
protected override Expression<Func<CircleQuery, object>> GetSortExpression(CircleSortField field) => field switch
{
CircleSortField.Favorite => x => !x.Favorite,
CircleSortField.Blacklisted => x => !x.Blacklisted,
CircleSortField.Spam => x => !x.Spam,
_ => x => x.Name
CircleSortField.Favorite => x => !x.Circle.Favorite,
CircleSortField.Blacklisted => x => !x.Circle.Blacklisted,
CircleSortField.Spam => x => !x.Circle.Spam,
_ => x => x.Circle.Name
};
protected override IEnumerable<(Expression<Func<CircleSearchItem, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
protected override IEnumerable<(Expression<Func<CircleQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
{
yield return (x => x.Name, SortDirection.Ascending);
yield return (x => x.MakerId, SortDirection.Ascending);
yield return (x => x.Circle.Name, SortDirection.Ascending);
yield return (x => x.Circle.MakerId, SortDirection.Ascending);
}
protected override IQueryable<CircleSearchItem> GetSelectQuery(IOrderedQueryable<CircleSearchItem> query)
protected override IQueryable<CircleSearchItem> GetSelectQuery(IOrderedQueryable<CircleQuery> query)
{
// Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate
var selected =
from item in query
join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws
from latest in vws.DefaultIfEmpty()
//join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws
//from latest in vws.DefaultIfEmpty()
select new CircleSearchItem
{
CircleId = item.CircleId,
Name = item.Name,
MakerId = item.MakerId,
Favorite = item.Favorite,
Blacklisted = item.Blacklisted,
Spam = item.Spam,
Downloads = item.Downloads,
Releases = item.Releases,
Pending = item.Pending,
FirstReleaseDate = item.FirstReleaseDate,
LatestReleaseDate = item.LatestReleaseDate,
LatestProductId = item.LatestProductId,
LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null,
LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null
CircleId = item.Circle.CircleId,
Name = item.Circle.Name,
MakerId = item.Circle.MakerId,
Favorite = item.Circle.Favorite,
Blacklisted = item.Circle.Blacklisted,
Spam = item.Circle.Spam,
// Aggregates
Downloads = context.VoiceWorks
.Where(v => v.CircleId == item.Circle.CircleId)
.Select(v => (int?)v.Downloads) // make nullable for Sum over empty set
.Sum() ?? 0,
Releases = context.VoiceWorks
.Count(v => v.CircleId == item.Circle.CircleId && v.SalesDate != null),
Pending = context.VoiceWorks
.Count(v => v.CircleId == item.Circle.CircleId && v.ExpectedDate != null),
FirstReleaseDate = context.VoiceWorks
.Where(v => v.CircleId == item.Circle.CircleId)
.Select(v => v.SalesDate)
.Min(),
LatestReleaseDate = context.VoiceWorks
.Where(v => v.CircleId == item.Circle.CircleId)
.Select(v => v.SalesDate)
.Max(),
// "Latest" by ProductId length, then value
LatestProductId = context.VoiceWorks
.Where(v => v.CircleId == item.Circle.CircleId)
.OrderByDescending(v => v.ProductId.Length)
.ThenByDescending(v => v.ProductId)
.Select(v => v.ProductId)
.FirstOrDefault(),
// If you want these two in base query too:
LatestVoiceWorkHasImage = context.VoiceWorks
.Where(v => v.CircleId == item.Circle.CircleId)
.OrderByDescending(v => v.ProductId.Length)
.ThenByDescending(v => v.ProductId)
.Select(v => (bool?)v.HasImage)
.FirstOrDefault(),
LatestVoiceWorkSalesDate = context.VoiceWorks
.Where(v => v.CircleId == item.Circle.CircleId)
.OrderByDescending(v => v.ProductId.Length)
.ThenByDescending(v => v.ProductId)
.Select(v => v.SalesDate)
.FirstOrDefault(),
//Downloads = item.Downloads,
//Releases = item.Releases,
//Pending = item.Pending,
//FirstReleaseDate = item.FirstReleaseDate,
//LatestReleaseDate = item.LatestReleaseDate,
//LatestProductId = item.LatestProductId,
//LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null,
//LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null
};
return selected;

View File

@@ -0,0 +1,84 @@
<div class="@ContainerClassees">
<div class="j-image-overlay"></div>
<img class="@ImageClasses" loading="@LoadingAttribute" src="@Source" @onload="OnImageLoaded">
</div>
@code {
[Parameter]
public required string Source { get; set; }
[Parameter]
public string FallbackSource { get; set; } = "images/home/no_img_main.gif";
[Parameter]
public bool LazyLoading { get; set; } = true;
[Parameter]
public string? ContainerClass { get; set; }
[Parameter]
public string? ImageClass { get; set; }
private bool _isLoaded;
private string? _lastSource;
private string ContainerClassees => GetContainerClasses();
private string ImageClasses => GetImageClasses();
private string? LoadingAttribute => LazyLoading ? "lazy" : null;
protected override void OnParametersSet()
{
if (!string.Equals(_lastSource, Source, StringComparison.Ordinal))
{
_lastSource = Source;
_isLoaded = false;
}
}
private string GetContainerClasses()
{
List<string> classNames = ["j-image-container"];
if (!string.IsNullOrEmpty(ContainerClass))
{
List<string> customClassNames = ContainerClass
.Split(" ")
.Select(className => className.Trim())
.Where(className => !string.IsNullOrWhiteSpace(className))
.ToList();
classNames.AddRange(customClassNames);
}
return string.Join(" ", classNames);
}
private string GetImageClasses()
{
List<string> classNames = ["j-image"];
if (!_isLoaded)
{
classNames.Add("j-lazy-load");
}
if (!string.IsNullOrEmpty(ImageClass))
{
List<string> customClassNames = ImageClass
.Split(" ")
.Select(className => className.Trim())
.Where(className => !string.IsNullOrWhiteSpace(className))
.ToList();
classNames.AddRange(customClassNames);
}
return string.Join(" ", classNames);
}
private void OnImageLoaded()
{
_isLoaded = true;
}
}

View File

@@ -0,0 +1,37 @@
<div class="pagination">
<div>
<label>@IndexInfo</label>
</div>
<MudPagination ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)TotalItems / (decimal)PageSize))" Selected="@PageNumber" SelectedChanged="OnSelectedChanged" />
</div>
@code {
[Parameter]
public int PageNumber { get; set; }
[Parameter]
public EventCallback<int> PageNumberChanged { get; set; }
[Parameter]
public int PageSize { get; set; }
[Parameter]
public EventCallback<int> PageSizeChanged { get; set; }
[Parameter]
public int TotalItems { get; set; }
[Parameter]
public EventCallback<int> TotalItemsChanged { get; set; }
public string IndexInfo => TotalItems == 0 ? "No items" : $"{StartIndex.ToString("n0")} - {EndIndex.ToString("n0")} of {TotalItems.ToString("n0")} items";
public int StartIndex => (PageNumber - 1) * PageSize + 1;
public int EndIndex => PageNumber * PageSize < TotalItems ? PageNumber * PageSize : TotalItems;
private async Task OnSelectedChanged(int newPage)
{
PageNumber = newPage;
await PageNumberChanged.InvokeAsync(newPage);
}
}

View File

@@ -1,57 +1,265 @@
@page "/circles"
@using JSMR.Application.Circles.Queries.Search
@using JSMR.Application.Common.Search
@using JSMR.UI.Blazor.Components
@using JSMR.UI.Blazor.Services
@inject VoiceWorksClient Client
@inject IJSRuntime JS
@inject HttpClient Http
<PageTitle>Circles</PageTitle>
<h1>Circles</h1>
<p>This component demonstrates fetching data from the server.</p>
<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="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="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>
@* @if (forecasts == null)
@if (searchResults is null)
{
<p><em>Loading...</em></p>
<p>Loading</p>
}
else if (searchResults.Items.Length == 0)
{
<p>No results.</p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th aria-label="Temperature in Celsius">Temp. (C)</th>
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
} *@
<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>
<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" />
@* <MudPagination ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)searchResults.TotalItems / (decimal)100))" @bind-Selected="@PageNumber" Class="j-pager" /> *@
}
<style>
.mud-table-root {
table-layout: fixed;
}
.j-pager {
position: fixed;
bottom: 0;
left: 0;
right: 0;
justify-content: center;
z-index: 2;
background: var(--mud-palette-background);
padding: .5em 1em;
}
</style>
@code {
// private WeatherForecast[]? forecasts;
private string? keywords;
// protected override async Task OnInitializedAsync()
// {
// forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
// }
public string? Keywords
{
get { return keywords; }
set
{
keywords = value;
_ = UpdateDataAsync(true);
}
}
// public class WeatherForecast
// {
// public DateOnly Date { get; set; }
private string circleStatus = string.Empty;
// public int TemperatureC { get; set; }
public string SelectedCircleStatus
{
get { return circleStatus; }
set
{
circleStatus = value;
_ = UpdateDataAsync(true);
}
}
// public string? Summary { get; set; }
private int pageNumber = 1;
// public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
// }
public int PageNumber
{
get { return pageNumber; }
set
{
pageNumber = value;
_ = UpdateDataAsync(false);
}
}
int pageSize = 100;
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();
}
private string GetStarRatingClass(CircleSearchItem item)
{
double averageDownloads = item.Downloads / item.Releases;
if (averageDownloads < 100)
{
return "circle-star-poor";
}
if (averageDownloads < 250)
{
return "circle-star-below-average";
}
if (averageDownloads < 500)
{
return "circle-star-average";
}
if (averageDownloads < 1000)
{
return "circle-star-above-average";
}
if (averageDownloads < 2000)
{
return "circle-star-popular";
}
if (averageDownloads < 4000)
{
return "circle-star-super-popular";
}
if (averageDownloads < 8000)
{
return "circle-star-ultra-popular";
}
return "circle-star-god-tier";
}
}

View File

@@ -1,18 +1,122 @@
@page "/creators"
@inject VoiceWorksClient Client
@inject IJSRuntime JS
@using JSMR.Application.Common.Search
@using JSMR.Application.Creators.Queries.Search
@using JSMR.Application.Creators.Queries.Search.Contracts
@using JSMR.UI.Blazor.Services
<PageTitle>Creators</PageTitle>
<h1>Creators</h1>
<p role="status">Current count: @currentCount</p>
<MudTextField @bind-Value="Keywords" Immediate="true" DebounceInterval="500" Label="Filter" Variant="Variant.Text" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" />
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@if (searchResults is null)
{
<p>Loading…</p>
}
else if (searchResults.Items.Length == 0)
{
<p>No results.</p>
}
else
{
<MudDataGrid Items="@searchResults.Items" Style="table-layout: fixed">
<Columns>
<PropertyColumn Property="x => x.Name" Title="Name" />
<PropertyColumn Property="x => x.VoiceWorkCount" Title="Voice Works" HeaderStyle="width: 14em" />
<TemplateColumn Title="Favorite" HeaderStyle="width: 8em">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(context.Item.Favorite? Color.Secondary: Color.Dark)" />
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Blacklisted" HeaderStyle="width: 8em">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(context.Item.Blacklisted? Color.Secondary: Color.Dark)" />
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
<MudPagination ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)searchResults.TotalItems / (decimal)100))" @bind-Selected="@PageNumber" Class="j-pager" />
}
<style>
.mud-table-root {
table-layout: fixed;
}
.j-pager {
position: fixed;
bottom: 0;
left: 0;
right: 0;
justify-content: center;
z-index: 2;
background: var(--mud-palette-background);
padding: .5em 1em;
}
</style>
@code {
private int currentCount = 0;
private string? keywords;
private void IncrementCount()
public string? Keywords
{
currentCount++;
get { return keywords; }
set
{
keywords = value;
_ = LoadTagsAsync();
}
}
}
private int pageNumber = 1;
public int PageNumber
{
get { return pageNumber; }
set
{
pageNumber = value;
_ = LoadTagsAsync();
}
}
int pageSize = 100;
SearchResult<CreatorSearchItem>? searchResults;
protected override Task OnInitializedAsync()
{
_ = LoadTagsAsync();
return Task.CompletedTask;
}
private async Task LoadTagsAsync()
{
SearchCreatorsRequest request = new(
Options: new()
{
PageNumber = PageNumber,
PageSize = pageSize,
Criteria = new()
{
Name = Keywords
},
SortOptions =
[
new(CreatorSortField.Name, Application.Common.Search.SortDirection.Ascending)
]
}
);
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
var result = await Client.SearchAsync(request);
searchResults = result?.Results ?? new();
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -1,6 +1,7 @@
@page "/"
@inject VoiceWorksClient Client
@using JSMR.Application.VoiceWorks.Queries.Search
@using JSMR.UI.Blazor.Services
<PageTitle>Home</PageTitle>

View File

@@ -1,13 +1,17 @@
@page "/tags"
@inject VoiceWorksClient Client
@inject IJSRuntime JS
@using JSMR.Application.Common.Search
@using JSMR.Application.Tags.Queries.Search
@using JSMR.Application.Tags.Queries.Search.Contracts
@using JSMR.UI.Blazor.Services
<PageTitle>Tags</PageTitle>
<h1>Tags</h1>
<MudTextField @bind-Value="Keywords" Immediate="true" DebounceInterval="500" Label="Filter" Variant="Variant.Text" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" />
@if (searchResults is null)
{
<p>Loading…</p>
@@ -18,13 +22,21 @@ else if (searchResults.Items.Length == 0)
}
else
{
<MudDataGrid Items="@searchResults.Items">
<MudDataGrid Items="@searchResults.Items" Style="table-layout: fixed">
<Columns>
<PropertyColumn Property="x => x.Name" Title="Name" />
<PropertyColumn Property="x => x.EnglishName" Title="English Name" />
<PropertyColumn Property="x => x.VoiceWorkCount" Title="Voice Works" />
<PropertyColumn Property="x => x.Favorite" Title="Favorite" />
<PropertyColumn Property="x => x.Blacklisted" Title="Blacklisted" />
<PropertyColumn Property="x => x.VoiceWorkCount" Title="Voice Works" HeaderStyle="width: 14em" />
<TemplateColumn Title="Favorite" HeaderStyle="width: 8em">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(context.Item.Favorite ? Color.Secondary : Color.Dark)" @onclick="@(e => IncrementCount(context.Item))" />
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Blacklisted" HeaderStyle="width: 8em">
<CellTemplate>
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(context.Item.Blacklisted? Color.Secondary: Color.Dark)" />
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
@@ -32,6 +44,10 @@ else
}
<style>
.mud-table-root {
table-layout: fixed;
}
.j-pager {
position: fixed;
bottom: 0;
@@ -45,6 +61,18 @@ else
</style>
@code {
private string? keywords;
public string? Keywords
{
get { return keywords; }
set
{
keywords = value;
_ = LoadTagsAsync();
}
}
private int pageNumber = 1;
public int PageNumber
@@ -76,7 +104,7 @@ else
PageSize = pageSize,
Criteria = new()
{
Name = Keywords
},
SortOptions =
[
@@ -85,6 +113,7 @@ else
}
);
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
var result = await Client.SearchAsync(request);
searchResults = result?.Results ?? new();
@@ -92,4 +121,8 @@ else
await InvokeAsync(StateHasChanged);
}
private void IncrementCount(TagSearchItem item)
{
//item.Favorite = !item.Favorite;
}
}

View File

@@ -1,5 +1,6 @@
@page "/voiceworks"
@using JSMR.Application.VoiceWorks.Queries.Search
@using JSMR.UI.Blazor.Services
@inject VoiceWorksClient Client
<PageTitle>Voice Works</PageTitle>

View File

@@ -1,11 +1,8 @@
using JSMR.Application.Tags.Queries.Search;
using JSMR.Application.VoiceWorks.Queries.Search;
using JSMR.UI.Blazor;
using JSMR.UI.Blazor.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Mvc;
using MudBlazor.Services;
using System.Net.Http.Json;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@@ -21,28 +18,4 @@ builder.Services.AddMudServices();
builder.Services.AddScoped<VoiceWorksClient>();
await builder.Build().RunAsync();
public class VoiceWorksClient(HttpClient http)
{
public async Task<SearchVoiceWorksResponse?> SearchAsync(SearchVoiceWorksRequest request, CancellationToken ct = default)
{
using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct);
return await resp.Content.ReadFromJsonAsync<SearchVoiceWorksResponse>(cancellationToken: ct);
}
public async Task<SearchTagsResponse?> SearchAsync(SearchTagsRequest request, CancellationToken ct = default)
{
using var resp = await http.PostAsJsonAsync("/api/tags/search", request, ct);
return await resp.Content.ReadFromJsonAsync<SearchTagsResponse>(cancellationToken: ct);
}
}
public record VoiceWorkDto(string ProductId, string ProductName);
// Tiny helper result type
public readonly record struct Result<T>(bool Ok, T? Value, string? Error)
{
public static Result<T> Success(T v) => new(true, v, null);
public static Result<T> Failure(string e) => new(false, default, e);
}
await builder.Build().RunAsync();

View File

@@ -0,0 +1,78 @@
namespace JSMR.UI.Blazor.Services;
public static class ImageUrlProvider
{
public static string GetImageURL(string? productId, bool hasImage, DateTime? salesDate, string size)
{
string folder = "modpub";
string imageSize = "main";
string imageWorkType = productId != null ? productId.StartsWith("RJ") ? "doujin" : "professional" : "doujin";
switch (size)
{
case "small":
imageSize = "sam";
folder = "modpub";
break;
case "300x300":
imageSize = hasImage ? "main_300x300" : "main";
folder = "resize";
break;
case "240x":
//imageSize = hasImage ? imageWorkType == "doujin" ? "main_240x240" : "sam_170x" : "main";
imageSize = hasImage ? imageWorkType == "doujin" ? "main_240x240" : "main_240x240" : "main";
folder = "resize";
break;
case "main":
default:
imageSize = "main";
folder = "modpub";
break;
}
if (hasImage == false || productId == null)
{
string noImageUrlTemplate = "/images/web/home/no_img_[imageSize].gif";
string noImageUrl = noImageUrlTemplate.Replace("[imageSize]", imageSize);
return noImageUrl;
}
var imageUrlTemplate = "//img.dlsite.jp/[folder]/images2/[imageType1]/[imageWorkType]/[fullRoundedProductId]/[productId][imageType2]_img_[imageSize].jpg";
var productIdWithoutPrefixString = productId.Substring(2);
int productIdWithoutPrefix = Convert.ToInt32(productId.Substring(2));
string productIdPrefix = productId.Substring(0, 2);
double something = (double)((productIdWithoutPrefix / 1000) * 1000);
int roundedProductId = (int)Math.Round(Math.Ceiling((double)productIdWithoutPrefix / 1000) * 1000);
//string actualRoundedProductId = ("000000" + roundedProductId.ToString()).Substring(roundedProductId.ToString().Length);
//string fullRoundedProductId = productIdPrefix + actualRoundedProductId;
var productIdWithPrefixStringLength = productIdWithoutPrefixString.Length;
var zeroPadLength = productIdWithPrefixStringLength - roundedProductId.ToString().Length;
var fullRoundedProductId = productIdPrefix.PadRight(productIdPrefix.Length + zeroPadLength, '0') + roundedProductId;
bool hasSalesDate = salesDate.HasValue;
string imageType1 = hasSalesDate ? "work" : "ana";
string imageType2 = hasSalesDate ? "" : "_ana";
string productLinkPage = salesDate.HasValue ? "work" : "announce";
string imageUrl = imageUrlTemplate
.Replace("[folder]", folder)
.Replace("[imageType1]", imageType1)
.Replace("[imageWorkType]", imageWorkType)
.Replace("[fullRoundedProductId]", fullRoundedProductId)
.Replace("[productId]", productId)
.Replace("[imageType2]", imageType2)
.Replace("[imageSize]", imageSize);
return imageUrl;
}
}

View File

@@ -0,0 +1,34 @@
using JSMR.Application.Circles.Queries.Search;
using JSMR.Application.Creators.Queries.Search;
using JSMR.Application.Tags.Queries.Search;
using JSMR.Application.VoiceWorks.Queries.Search;
using System.Net.Http.Json;
namespace JSMR.UI.Blazor.Services;
public class VoiceWorksClient(HttpClient http)
{
public async Task<SearchVoiceWorksResponse?> SearchAsync(SearchVoiceWorksRequest request, CancellationToken ct = default)
{
using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct);
return await resp.Content.ReadFromJsonAsync<SearchVoiceWorksResponse>(cancellationToken: ct);
}
public async Task<SearchCirclesResponse?> SearchAsync(SearchCirclesRequest request, CancellationToken ct = default)
{
using var resp = await http.PostAsJsonAsync("/api/circles/search", request, ct);
return await resp.Content.ReadFromJsonAsync<SearchCirclesResponse>(cancellationToken: ct);
}
public async Task<SearchCreatorsResponse?> SearchAsync(SearchCreatorsRequest request, CancellationToken ct = default)
{
using var resp = await http.PostAsJsonAsync("/api/creators/search", request, ct);
return await resp.Content.ReadFromJsonAsync<SearchCreatorsResponse>(cancellationToken: ct);
}
public async Task<SearchTagsResponse?> SearchAsync(SearchTagsRequest request, CancellationToken ct = default)
{
using var resp = await http.PostAsJsonAsync("/api/tags/search", request, ct);
return await resp.Content.ReadFromJsonAsync<SearchTagsResponse>(cancellationToken: ct);
}
}

View File

@@ -1,5 +1,5 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
/*font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;*/
}
h1:focus {
@@ -111,4 +111,197 @@ code {
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
/* Custom */
/* Search Filters */
.search-filter-control-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-column-gap: 16px;
grid-row-gap: 12px;
background-color: #0f2031;
padding: 12px;
border: 1px solid #304562;
margin-bottom: 24px
}
.search-filter-control-container > .search-filter-control {
display: flex;
align-items: center;
}
.search-filter-control-container > .search-filter-control-span-2 {
display: flex;
align-items: center;
grid-column: span 2;
}
.search-filter-control-container > .search-filter-control-span-3 {
display: flex;
align-items: center;
grid-column: span 3;
}
.search-filter-control-container > .search-filter-control-span-4 {
display: flex;
align-items: center;
grid-column: span 4;
}
.items-container {
display: grid;
grid-column-gap: 1.2em;
grid-row-gap: 1.2em;
}
.circle-items-container {
grid-template-columns: repeat(auto-fill,minmax(300px,1fr));
}
.j-image-container {
position: relative;
}
.j-image-overlay {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
background-color: #000;
opacity: 0.1;
}
.j-image.j-lazy-load {
opacity: 0;
}
.j-image[loading="lazy"] {
transition: .5s linear;
}
/* Pagination */
.pagination {
display: flex;
width: 100%;
align-items: center;
margin-top: 20px;
margin-bottom: 20px;
border-radius: 0px;
bottom: 0;
background: var(--background-color);
padding: .5rem 0;
margin: 0;
position: sticky;
display: grid;
grid-template-columns: 1fr 2fr 1fr;
z-index: 1;
}
/* Circle */
.j-circle-image-container {
height: 300px;
}
.j-circle-image-container-mini {
height: 100px;
}
.j-circle-image {
display: block;
min-width: 200px;
width: 226px;
height: 170px;
object-fit: cover;
background-color: black;
border: 1px solid #949494;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.j-circle-image-mini {
display: block;
object-fit: cover;
background-color: black;
border: 1px solid #949494;
box-sizing: border-box;
width: 100%;
height: 100%;
}
/* Cirlce Stars */
.circle-star-container {
mask-image: url("../svg/star-fill.svg");
mask-size: auto;
align-self: center;
mask-repeat: repeat-x;
background: rgb(180,200, 214);
height: 16px;
width: 80px;
}
.circle-star {
mask-image: url("../svg/star-fill.svg");
mask-size: auto;
align-self: center;
mask-repeat: repeat-x;
height: 16px;
background: linear-gradient(rgb(252,224,43), rgb(249,179,2));
}
.circle-star-poor {
width: 16px;
width: 8px;
background-color: darkred;
}
.circle-star-below-average {
width: 16px;
background-color: orange;
}
.circle-star-average {
width: 32px;
}
.circle-star-above-average {
width: 48px;
}
.circle-star-popular {
width: 64px;
}
.circle-star-super-popular {
width: 80px;
}
.circle-star-ultra-popular {
width: 80px;
background: linear-gradient(to bottom,#4dbdd4 0,#3398ac 100%);
}
.circle-star-god-tier {
width: 80px;
background: linear-gradient(to bottom,#8963d3 0,#6640b3 100%);
}
.star_rating.mini::before {
height: 16px;
width: 80px;
}
.star_rating.mini::before {
background: url(assets/images/web/common/icon_star_rating_02.png) no-repeat 0 0;
background-position-x: 0px;
background-position-y: 0px;
background-size: auto;
background-size: auto 32px;
}
.star_rating.mini.star_45::before {
background-position: 0 -16px;
}

View File

@@ -0,0 +1,22 @@
.mud-input {
background-color: var(--input-background-color);
font-family: var(--font-family);
}
.mud-input-control {
border-color: var(--input-border-color);
}
.mud-input > input.mud-input-root,
div.mud-input-slot.mud-input-root {
line-height: 1.5rem;
}
.mud-input > input.mud-input-root-outlined.mud-input-root-margin-dense,
div.mud-input-slot.mud-input-root-outlined.mud-input-root-margin-dense {
padding: .25rem .5rem;
}
.mud-typography {
font-family: var(--font-family);
}

View File

@@ -0,0 +1,9 @@
body {
background-color: var(--background-color);
color: var(--primary-text-color);
font-family: var(--font-family);
}
label {
font-weight: 600;
}

View File

@@ -0,0 +1,24 @@
:root {
--font-family: 'Poppins';
--background-color: rgb(16, 36, 50);
--input-background-color: rgb(0,20,34);
--input-border-color: #304562;
--primary-text-color: rgb(180,200, 214);
--product-container-background-color: rgb(39,59,73);
--product-container-box-shadow: 1px 1px 4px rgba(0,0,0,.5);
--product-title-text-color: rgb(200,220,234);
--image-overlay-opacity: 0.9;
--tag-background-color: #415664;
--tag-text-color: rgb(210,220,230);
--tag-border: none;
--tag-border-radius: 0.8em;
--tag-padding: 0.3em 1em;
--tag-margin: 1em 0.5em 0 0;
--tag-hover-background-color: #596f7e;
--tag-hover-text-color: rgb(240,250,254);
--tag-hover-border: none;
--product-footer-background-color: rgb(0,20,34);
--product-footer-text-color: rgb(220,230,234);
--expected-date-text-color: #ffe073;
--planned-date-text-color: #73bdff;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -16,7 +16,13 @@
<!-- Important: Increment the version parameter whenever you update MudBlazor to prevent caching issues -->
<script src="_content/MudBlazor/MudBlazor.min.js?v=1"></script>
<script src="js/site.js"></script>
<link rel="stylesheet" href="css/jsmr-mud-blazor.css" />
<link rel="stylesheet" href="css/app.css" />
<link rel="stylesheet" href="css/theme-base.css" />
<link rel="stylesheet" href="css/theme-frozen.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="JSMR.UI.Blazor.styles.css" rel="stylesheet" />
<link href="manifest.webmanifest" rel="manifest" />

View File

@@ -0,0 +1,5 @@
window.pageHelpers = {
scrollToTop: () => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
}
};

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30 20"><defs><path id="a" d="M0-1L.588.809-.952-.309H.952L-.588.809z" fill="#FF0"/></defs><path fill="#EE1C25" d="M0 0h30v20H0z"/><use xlink:href="#a" transform="matrix(3 0 0 3 5 5)"/><use xlink:href="#a" transform="rotate(23.036 .093 25.536)"/><use xlink:href="#a" transform="rotate(45.87 1.273 16.18)"/><use xlink:href="#a" transform="rotate(69.945 .996 12.078)"/><use xlink:href="#a" transform="rotate(20.66 -19.689 31.932)"/></svg>

After

Width:  |  Height:  |  Size: 531 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-jp" viewBox="0 0 640 480">
<defs>
<clipPath id="a">
<path fill-opacity=".7" d="M-88 32h640v480H-88z"/>
</clipPath>
</defs>
<g fill-rule="evenodd" stroke-width="1pt" clip-path="url(#a)" transform="translate(88 -32)">
<path fill="#fff" d="M-128 32h720v480h-720z"/>
<circle cx="523.1" cy="344.1" r="194.9" fill="#bc002d" transform="translate(-168.4 8.6) scale(.76554)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-36 -24 72 48"><path fill="#fff" d="M-36-24h72v48h-72z"/><g transform="rotate(-56.31)"><g id="b"><path id="a" d="M-6-25H6m-12 3H6m-12 3H6" stroke="#000" stroke-width="2"/><use xlink:href="#a" y="44"/></g><path stroke="#fff" d="M0 17v10"/><circle fill="#cd2e3a" r="12"/><path fill="#0047a0" d="M0-12A6 6 0 000 0a6 6 0 010 12 12 12 0 010-24z"/></g><g transform="rotate(-123.69)"><use xlink:href="#b"/><path stroke="#fff" d="M0-23.5v3M0 17v3.5m0 3v3"/></g></svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 7410 3900"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>

After

Width:  |  Height:  |  Size: 741 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-headphones" viewBox="0 0 16 16">
<path d="M8 3a5 5 0 0 0-5 5v1h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V8a6 6 0 1 1 12 0v5a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1V8a5 5 0 0 0-5-5z"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-heart-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-heart" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 2.748l-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143c.06.055.119.112.176.171a3.12 3.12 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15z"/>
</svg>

After

Width:  |  Height:  |  Size: 504 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-radioactive" viewBox="0 0 16 16">
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Z"/>
<path d="M9.653 5.496A2.986 2.986 0 0 0 8 5c-.61 0-1.179.183-1.653.496L4.694 2.992A5.972 5.972 0 0 1 8 2c1.222 0 2.358.365 3.306.992L9.653 5.496Zm1.342 2.324a2.986 2.986 0 0 1-.884 2.312 3.01 3.01 0 0 1-.769.552l1.342 2.683c.57-.286 1.09-.66 1.538-1.103a5.986 5.986 0 0 0 1.767-4.624l-2.994.18Zm-5.679 5.548 1.342-2.684A3 3 0 0 1 5.005 7.82l-2.994-.18a6 6 0 0 0 3.306 5.728ZM10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-shield-fill-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 .5c-.662 0-1.77.249-2.813.525a61.11 61.11 0 0 0-2.772.815 1.454 1.454 0 0 0-1.003 1.184c-.573 4.197.756 7.307 2.368 9.365a11.192 11.192 0 0 0 2.417 2.3c.371.256.715.451 1.007.586.27.124.558.225.796.225s.527-.101.796-.225c.292-.135.636-.33 1.007-.586a11.191 11.191 0 0 0 2.418-2.3c1.611-2.058 2.94-5.168 2.367-9.365a1.454 1.454 0 0 0-1.003-1.184 61.09 61.09 0 0 0-2.772-.815C9.77.749 8.663.5 8 .5zM6.854 6.146a.5.5 0 1 0-.708.708L7.293 8 6.146 9.146a.5.5 0 1 0 .708.708L8 8.707l1.146 1.147a.5.5 0 0 0 .708-.708L8.707 8l1.147-1.146a.5.5 0 0 0-.708-.708L8 7.293 6.854 6.146z"/>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View File

@@ -0,0 +1,4 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-shield-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M5.443 1.991a60.17 60.17 0 0 0-2.725.802.454.454 0 0 0-.315.366C1.87 7.056 3.1 9.9 4.567 11.773c.736.94 1.533 1.636 2.197 2.093.333.228.626.394.857.5.116.053.21.089.282.11A.73.73 0 0 0 8 14.5c.007-.001.038-.005.097-.023.072-.022.166-.058.282-.111.23-.106.525-.272.857-.5a10.197 10.197 0 0 0 2.197-2.093C12.9 9.9 14.13 7.056 13.597 3.159a.454.454 0 0 0-.315-.366c-.626-.2-1.682-.526-2.725-.802C9.491 1.71 8.51 1.5 8 1.5c-.51 0-1.49.21-2.557.491zm-.256-.966C6.23.749 7.337.5 8 .5c.662 0 1.77.249 2.813.525a61.09 61.09 0 0 1 2.772.815c.528.168.926.623 1.003 1.184.573 4.197-.756 7.307-2.367 9.365a11.191 11.191 0 0 1-2.418 2.3 6.942 6.942 0 0 1-1.007.586c-.27.124-.558.225-.796.225s-.526-.101-.796-.225a6.908 6.908 0 0 1-1.007-.586 11.192 11.192 0 0 1-2.417-2.3C2.167 10.331.839 7.221 1.412 3.024A1.454 1.454 0 0 1 2.415 1.84a61.11 61.11 0 0 1 2.772-.815z"/>
<path fill-rule="evenodd" d="M6.146 6.146a.5.5 0 0 1 .708 0L8 7.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 8l1.147 1.146a.5.5 0 0 1-.708.708L8 8.707 6.854 9.854a.5.5 0 0 1-.708-.708L7.293 8 6.146 6.854a.5.5 0 0 1 0-.708z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-star-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.283.95l-3.523 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
</svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-star-half" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M5.354 5.119L7.538.792A.516.516 0 0 1 8 .5c.183 0 .366.097.465.292l2.184 4.327 4.898.696A.537.537 0 0 1 16 6.32a.55.55 0 0 1-.17.445l-3.523 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256a.519.519 0 0 1-.146.05c-.341.06-.668-.254-.6-.642l.83-4.73L.173 6.765a.55.55 0 0 1-.171-.403.59.59 0 0 1 .084-.302.513.513 0 0 1 .37-.245l4.898-.696zM8 12.027c.08 0 .16.018.232.056l3.686 1.894-.694-3.957a.564.564 0 0 1 .163-.505l2.906-2.77-4.052-.576a.525.525 0 0 1-.393-.288L8.002 2.223 8 2.226v9.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 672 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-star" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.523-3.356c.329-.314.158-.888-.283-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767l-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288l1.847-3.658 1.846 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.564.564 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 664 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-4" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-11z"/>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-2" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4 5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4 8a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-badge-cc" viewBox="0 0 16 16">
<path d="M3.708 7.755c0-1.111.488-1.753 1.319-1.753.681 0 1.138.47 1.186 1.107H7.36V7c-.052-1.186-1.024-2-2.342-2C3.414 5 2.5 6.05 2.5 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114H6.213c-.048.615-.496 1.05-1.186 1.05-.84 0-1.319-.62-1.319-1.727v-.743zm6.14 0c0-1.111.488-1.753 1.318-1.753.682 0 1.139.47 1.187 1.107H13.5V7c-.053-1.186-1.024-2-2.342-2C9.554 5 8.64 6.05 8.64 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114h-1.147c-.048.615-.497 1.05-1.187 1.05-.839 0-1.318-.62-1.318-1.727v-.743z"/>
<path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2z"/>
</svg>

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -0,0 +1,3 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1H2.5zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zM8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5zm3 .5a.5.5 0 0 0-1 0v7a.5.5 0 0 0 1 0v-7z"/>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -0,0 +1,4 @@
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>

After

Width:  |  Height:  |  Size: 575 B