This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
@page "/catalog/products"
@inherits ResultComponentBase<ProductListingModel>
<PageTitle>@PageTitle</PageTitle>
@if (IsLoading)
{
<div class="block">
<div class="container container--max--xl">
<div class="row g-custom-30">
<div class="col">
<div class="products-view">
<TelerikSkeleton Width="100%" Height="240px" />
</div>
</div>
</div>
</div>
</div>
}
else if (Page.Products.Count == 0)
{
<div class="page">
<div class="page__header">
<div class="container container--max--xl">
<div class="row g-custom-30">
<div class="col">
<ol class="page__header-breadcrumbs breadcrumb">
<li class="breadcrumb-item"><NavLink href="/">Home</NavLink></li>
<li class="breadcrumb-item"><NavLink href="/catalog">Catalog</NavLink></li>
<li class="breadcrumb-item active" aria-current="page">@Page.Category.Name</li>
</ol>
<h1 class="page__header-title decor-header decor-header--align--center">@Page.Category.Name</h1>
@if (!string.IsNullOrWhiteSpace(Page.Category.Description))
{
<p class="text-muted text-center">@Page.Category.Description</p>
}
</div>
</div>
</div>
</div>
<div class="page__body">
<div class="block">
<div class="container container--max--xl">
<div class="row g-custom-30">
<div class="col">
<div class="alert alert-info mb-0">No products are available in this category yet.</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
else
{
<div class="page">
<div class="page__header">
<div class="container container--max--xl">
<div class="row g-custom-30">
<div class="col">
<ol class="page__header-breadcrumbs breadcrumb">
<li class="breadcrumb-item"><NavLink href="/">Home</NavLink></li>
<li class="breadcrumb-item"><NavLink href="/catalog">Catalog</NavLink></li>
<li class="breadcrumb-item active" aria-current="page">@Page.Category.Name</li>
</ol>
<h1 class="page__header-title decor-header decor-header--align--center">@Page.Category.Name</h1>
@if (!string.IsNullOrWhiteSpace(Page.Category.Description))
{
<p class="text-muted text-center">@Page.Category.Description</p>
}
</div>
</div>
</div>
</div>
<div class="page__body">
<div class="block">
<div class="container container--max--xl">
<div class="row g-custom-30">
<div class="col-12 col-lg-3 order-1 order-lg-0">
<div class="block">
<aside class="sidebar">
<div class="widget widget--card">
<div class="widget__title">Filters</div>
<div class="widget__body text-muted">
Filter options are coming soon.
</div>
</div>
</aside>
</div>
</div>
<div class="col-12 col-lg-9">
<div class="products-view">
<div class="products-view__options view-options">
<div class="view-options__layout">
<button type="button" class="view-options__layout-button view-options__layout-button--active" disabled aria-disabled="true">
<svg width="14px" height="14px">
<use xlink:href="images/sprite.svg#grid-14"></use>
</svg>
</button>
<button type="button" class="view-options__layout-button" disabled aria-disabled="true">
<svg width="14px" height="14px">
<use xlink:href="images/sprite.svg#list-14"></use>
</svg>
</button>
</div>
<div class="view-options__legend">@LegendText</div>
<div class="view-options__divider"></div>
<div class="view-options__control">
<label class="view-options__control-label" for="view-options-sort">Sort By:</label>
<div class="view-options__control-content">
<select class="form-select form-select-sm" id="view-options-sort" disabled>
<option selected>Default</option>
<option>Name (A-Z)</option>
<option>Name (Z-A)</option>
</select>
</div>
</div>
<div class="view-options__control">
<label class="view-options__control-label" for="view-options-show">Show:</label>
<div class="view-options__control-content">
<select class="form-select form-select-sm" id="view-options-show" disabled>
<option selected>12</option>
<option>24</option>
<option>48</option>
</select>
</div>
</div>
</div>
<CardGrid View="grid">
@foreach (var product in Page.Products)
{
<CardGridItem>
<ProductCard Product="product" Layout="grid" />
</CardGridItem>
}
</CardGrid>
@if (TotalPages > 1)
{
<div class="products-view__pagination">
<nav aria-label="Product pagination">
<ul class="pagination justify-content-center">
<li class="page-item @(HasPreviousPage ? string.Empty : "disabled")">
<span class="page-link" aria-label="Previous" aria-disabled="@(!HasPreviousPage)">
<svg width="6px" height="9px">
<use xlink:href="images/sprite.svg#arrow-left-6x9"></use>
</svg>
<span class="visually-hidden">Previous</span>
</span>
</li>
@foreach (var pageNumber in VisiblePageNumbers)
{
var isActive = pageNumber == CurrentPage;
<li class="page-item @(isActive ? "active" : string.Empty)">
<span class="page-link">@pageNumber</span>
</li>
}
<li class="page-item @(HasNextPage ? string.Empty : "disabled")">
<span class="page-link" aria-label="Next" aria-disabled="@(!HasNextPage)">
<svg width="6px" height="9px">
<use xlink:href="images/sprite.svg#arrow-right-6x9"></use>
</svg>
<span class="visually-hidden">Next</span>
</span>
</li>
</ul>
</nav>
</div>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}

View File

@@ -0,0 +1,107 @@
using Microsoft.AspNetCore.Components;
using Prefab.Web.Client.Models.Catalog;
using Prefab.Web.Client.Services;
namespace Prefab.Web.Client.Pages.Catalog;
public partial class Products
{
private string? _currentSlug;
[Inject]
private IProductListingService ProductListingService { get; set; } = null!;
[SupplyParameterFromQuery(Name = "category-slug")]
public string? CategorySlug { get; set; }
protected bool IsLoading { get; private set; } = true;
protected ProductListingModel Page { get; private set; } = new();
protected string PageTitle => string.IsNullOrWhiteSpace(Page.Category.Name)
? "Products"
: $"{Page.Category.Name} Products";
protected string LegendText
{
get
{
var total = EffectiveTotal;
if (total == 0 || Page.Products.Count == 0)
{
return "Showing 0 products";
}
var pageSize = EffectivePageSize;
var startIndex = ((CurrentPage - 1) * pageSize) + 1;
var endIndex = Math.Min(total, startIndex + Page.Products.Count - 1);
return $"Showing {startIndex}-{endIndex} of {total} products";
}
}
protected int CurrentPage => Page.Page > 0 ? Page.Page : 1;
protected int TotalPages => EffectiveTotal == 0 ? 0 : (int)Math.Ceiling(EffectiveTotal / (double)EffectivePageSize);
protected bool HasPreviousPage => CurrentPage > 1;
protected bool HasNextPage => TotalPages > CurrentPage;
protected IReadOnlyList<int> VisiblePageNumbers => BuildVisiblePages();
private int EffectiveTotal => Page.Total > 0 ? Page.Total : Page.Products.Count;
private int EffectivePageSize => Page.PageSize > 0 ? Page.PageSize : Math.Max(Page.Products.Count, 1);
protected override async Task OnParametersSetAsync()
{
var normalizedSlug = string.IsNullOrWhiteSpace(CategorySlug)
? null
: CategorySlug.Trim().ToLowerInvariant();
if (string.Equals(_currentSlug, normalizedSlug, StringComparison.Ordinal))
{
return;
}
_currentSlug = normalizedSlug;
if (string.IsNullOrWhiteSpace(normalizedSlug))
{
Page = new ProductListingModel();
IsLoading = false;
return;
}
IsLoading = true;
var result = await Execute(ct => ProductListingService.GetCategoryProducts(normalizedSlug, ct), CancellationToken.None);
Page = result.IsSuccess && result.Value is { } value
? value
: new ProductListingModel();
IsLoading = false;
}
private IReadOnlyList<int> BuildVisiblePages()
{
if (TotalPages <= 1)
{
return [];
}
const int maxPagesToShow = 5;
var first = Math.Max(1, CurrentPage - 2);
var last = Math.Min(TotalPages, first + maxPagesToShow - 1);
first = Math.Max(1, last - maxPagesToShow + 1);
var pages = new List<int>(last - first + 1);
for (var page = first; page <= last; page++)
{
pages.Add(page);
}
return pages;
}
}