Init
This commit is contained in:
180
Prefab.Web.Client/Pages/Catalog/Products.razor
Normal file
180
Prefab.Web.Client/Pages/Catalog/Products.razor
Normal 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>
|
||||
}
|
||||
107
Prefab.Web.Client/Pages/Catalog/Products.razor.cs
Normal file
107
Prefab.Web.Client/Pages/Catalog/Products.razor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
6
Prefab.Web.Client/Pages/Categories.razor
Normal file
6
Prefab.Web.Client/Pages/Categories.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
@page "/categories"
|
||||
|
||||
@inherits CategoriesComponent
|
||||
|
||||
<PageTitle>Categories</PageTitle>
|
||||
|
||||
32
Prefab.Web.Client/Pages/Categories.razor.cs
Normal file
32
Prefab.Web.Client/Pages/Categories.razor.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Prefab.Web.Client.Models;
|
||||
using Prefab.Web.Client.Services;
|
||||
|
||||
namespace Prefab.Web.Client.Pages;
|
||||
|
||||
public class CategoriesComponent : ResultComponentBase<CategoriesPageModel>
|
||||
{
|
||||
[Inject]
|
||||
protected ICategoriesPageService PageService { get; set; } = null!;
|
||||
|
||||
protected CategoriesPageModel Page { get; set; } = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var result = await Execute(PageService.GetPage, CancellationToken.None);
|
||||
|
||||
if (result.IsSuccess && result.Value is { } value)
|
||||
{
|
||||
Page = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Page = new CategoriesPageModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CategoriesPageModel
|
||||
{
|
||||
public IEnumerable<CategoryListItemModel> Categories { get; set; } = [];
|
||||
}
|
||||
18
Prefab.Web.Client/Pages/Counter.razor
Normal file
18
Prefab.Web.Client/Pages/Counter.razor
Normal file
@@ -0,0 +1,18 @@
|
||||
@page "/counter"
|
||||
|
||||
<PageTitle>Counter</PageTitle>
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
260
Prefab.Web.Client/Pages/Home.razor
Normal file
260
Prefab.Web.Client/Pages/Home.razor
Normal file
@@ -0,0 +1,260 @@
|
||||
@page "/"
|
||||
|
||||
@inherits HomeComponent
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<TelerikMediaQuery Media="(max-width: 576px)" OnChange="@OnExtraSmallChange"></TelerikMediaQuery>
|
||||
<TelerikMediaQuery Media="(min-width: 577px) and (max-width: 768px)" OnChange="@OnSmallChange"></TelerikMediaQuery>
|
||||
<TelerikMediaQuery Media="(min-width: 769px) and (max-width: 992px)" OnChange="@OnMediumChange"></TelerikMediaQuery>
|
||||
<TelerikMediaQuery Media="(min-width: 993px) and (max-width: 1200px)" OnChange="@OnLargeChange"></TelerikMediaQuery>
|
||||
<TelerikMediaQuery Media="(min-width: 1201px)" OnChange="@OnExtraLargeChange"></TelerikMediaQuery>
|
||||
|
||||
<!-- block-hero-slider -->
|
||||
<div class="block block-slider block-slider--featured">
|
||||
<div class="container container--max--xl">
|
||||
<div class="slider slider--with-dots">
|
||||
<TelerikCarousel Width="100%"
|
||||
Height="500px"
|
||||
Arrows="true"
|
||||
Pageable="true"
|
||||
LoopPages="true"
|
||||
AutomaticPageChange="true"
|
||||
AutomaticPageChangeInterval="3000"
|
||||
Data="@Slides">
|
||||
<Template>
|
||||
<div class="slide">
|
||||
@if (!string.IsNullOrWhiteSpace(context.GoToUrl))
|
||||
{
|
||||
<a href="@context.GoToUrl" class="slide-link">
|
||||
<picture>
|
||||
<source media="(min-width: 768px)"
|
||||
srcset='@($"images/slides/slide{context.Number}.jpg 1x, images/slides/slide{context.Number}@2x.jpg 2x")'>
|
||||
<source media="(max-width: 767px)"
|
||||
srcset="images/slides/slide@(context.Number)-portrait.jpg 1x,
|
||||
images/slides/slide@(context.Number)-portrait@2x.jpg 2x">
|
||||
<img src="images/slides/slide@(context.Number).jpg"
|
||||
alt="@(!string.IsNullOrWhiteSpace(context.AltText) ? context.AltText : $"Slide {context.Number}")"
|
||||
style="width: 100%; height: 100%; object-fit: cover;" />
|
||||
</picture>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<picture>
|
||||
<source media="(min-width: 768px)"
|
||||
srcset='@($"images/slides/slide{context.Number}.jpg 1x, images/slides/slide{context.Number}@2x.jpg 2x")'>
|
||||
<source media="(max-width: 767px)"
|
||||
srcset="images/slides/slide@(context.Number)-portrait.jpg 1x,
|
||||
images/slides/slide@(context.Number)-portrait@2x.jpg 2x">
|
||||
<img src="images/slides/slide@(context.Number).jpg"
|
||||
alt="@(!string.IsNullOrWhiteSpace(context.AltText) ? context.AltText : $"Slide {context.Number}")"
|
||||
style="width: 100%; height: 100%; object-fit: cover;" />
|
||||
</picture>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(context.Text))
|
||||
{
|
||||
<div class=@($"slide-text {context.TextPosition} {context.TextWidth}")>
|
||||
@if (!string.IsNullOrWhiteSpace(context.Text))
|
||||
{
|
||||
<h2>@context.Text</h2>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Template>
|
||||
</TelerikCarousel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- block-hero-slider / end -->
|
||||
|
||||
<!-- block-featured-products -->
|
||||
@if (IsFeaturedProductsLoading)
|
||||
{
|
||||
<div class="block block-products-carousel featured-products-block featured-products-block--loading">
|
||||
<div class="container container--max--xl">
|
||||
<div class="block__title">
|
||||
<h2 class="decor-header decor-header--align--center">Featured Products</h2>
|
||||
</div>
|
||||
<div class="block-products-carousel__slider slider slider--with-arrows">
|
||||
<div class="product-group" style="display: flex; flex-wrap: wrap; width: 100%;">
|
||||
@for (var i = 0; i < NumberOfFeaturedProductCardsPerGroup; i++)
|
||||
{
|
||||
<div class="product-wrapper" style="@CalculateWidthOfFeaturedProductCard()">
|
||||
<TelerikSkeleton Width="100%" Height="260px" ShapeType="@Telerik.Blazor.SkeletonShapeType.Rectangle" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (FeaturedProductsHaveItemsToShow)
|
||||
{
|
||||
<div class="block block-products-carousel featured-products-block">
|
||||
<div class="container container--max--xl">
|
||||
<div class="block__title">
|
||||
<h2 class="decor-header decor-header--align--center">Featured Products</h2>
|
||||
</div>
|
||||
<div class="block-products-carousel__slider slider slider--with-arrows">
|
||||
<TelerikCarousel Width="100%"
|
||||
Height="350px"
|
||||
Arrows="true"
|
||||
Pageable="false"
|
||||
LoopPages="true"
|
||||
AutomaticPageChange="false"
|
||||
Data="@GetProductCardGroups()"
|
||||
Class="owl-carousel product-carousel-custom">
|
||||
<Template>
|
||||
<div class="product-group" style="display: flex; flex-wrap: wrap; width: 100%;">
|
||||
@foreach (var product in context)
|
||||
{
|
||||
<div class="product-wrapper" style="@CalculateWidthOfFeaturedProductCard()">
|
||||
<ProductCard Product="product"
|
||||
CssClass="catalog-card catalog-card--layout--grid" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Template>
|
||||
</TelerikCarousel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="block block-products-carousel featured-products-block featured-products-block--empty">
|
||||
<div class="container container--max--xl">
|
||||
<div class="block__title">
|
||||
<h2 class="decor-header decor-header--align--center">Featured Products</h2>
|
||||
</div>
|
||||
<div class="alert alert-info mb-0">
|
||||
Featured products will appear here once items are available.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- block-featured-products / end -->
|
||||
|
||||
<!-- block-collections -->
|
||||
<div class="block block-collections">
|
||||
<div class="container container--max--xl">
|
||||
<div class="block__title">
|
||||
<h2 class="decor-header decor-header--align--center">Latest Collections</h2>
|
||||
</div>
|
||||
<div class="row g-custom-30">
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<div class="block-collections__item block-collections__item--start">
|
||||
<div class="block-collections__info block-collections__info--top-start">
|
||||
<div class="block-collections__name">Smart Building Solutions</div>
|
||||
<div class="block-collections__description">
|
||||
Complete intelligent building automation systems featuring advanced lighting controls,
|
||||
occupancy sensors, and energy management solutions for modern commercial spaces.
|
||||
</div>
|
||||
<div class="block-collections__button">
|
||||
<a href="/catalog/smart-building" class="btn btn-primary">Explore Collection</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-collections__image">
|
||||
<a href="/catalog/smart-building">
|
||||
<picture>
|
||||
<source media="(min-width: 992px)" srcset="images/collections/collection1-lg.jpg 1x,
|
||||
images/collections/collection1-lg@2x.jpg 2x">
|
||||
<source media="(max-width: 991px)" srcset="images/collections/collection1.jpg 1x,
|
||||
images/collections/collection1@2x.jpg 2x">
|
||||
<img src="images/collections/collection1-lg.jpg" alt="Smart Building Solutions Collection">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-lg-7 pt-5 pt-md-0">
|
||||
<div class="block-collections__item block-collections__item--end">
|
||||
<div class="block-collections__image">
|
||||
<a href="/catalog/prefabricated-electrical-assemblies/pre-assembled-panels">
|
||||
<picture>
|
||||
<source media="(min-width: 992px)" srcset="images/collections/collection2-lg.jpg 1x,
|
||||
images/collections/collection2-lg@2x.jpg 2x">
|
||||
<source media="(max-width: 991px)" srcset="images/collections/collection2.jpg 1x,
|
||||
images/collections/collection2@2x.jpg 2x">
|
||||
<img src="images/collections/collection2-lg.jpg" alt="Pre-Assembled Panel Collection">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
<div class="block-collections__info block-collections__info--bottom-end">
|
||||
<div class="block-collections__name">Pre-Assembled Panel Systems</div>
|
||||
<div class="block-collections__description">
|
||||
Ready-to-install electrical panels and distribution assemblies, factory-tested and certified.
|
||||
Reduce installation time by up to 70% with our professionally pre-wired solutions designed
|
||||
for commercial and industrial applications.
|
||||
</div>
|
||||
<div class="block-collections__button">
|
||||
<a href="/catalog/prefabricated-electrical-assemblies/pre-assembled-panels" class="btn btn-primary">View Panels</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- block-collections / end -->
|
||||
|
||||
<!-- block-shop-categories -->
|
||||
@if (AreCategoriesLoading)
|
||||
{
|
||||
<div class="block block-shop-categories home-categories-block home-categories-block--loading">
|
||||
<div class="container container--max--xl">
|
||||
<div class="block__title">
|
||||
<h2 class="decor-header decor-header--align--center">Latest Categories</h2>
|
||||
</div>
|
||||
<div class="categories-list">
|
||||
@for (var i = 0; i < NumberOfCategorySkeletons; i++)
|
||||
{
|
||||
<div class="card category-card">
|
||||
<div class="category-card__image">
|
||||
<TelerikSkeleton Width="100%" Height="180px" ShapeType="@Telerik.Blazor.SkeletonShapeType.Rectangle" />
|
||||
</div>
|
||||
<div class="category-card__name">
|
||||
<TelerikSkeleton Width="70%" Height="20px" ShapeType="@Telerik.Blazor.SkeletonShapeType.Text" />
|
||||
</div>
|
||||
<div class="category-card__products">
|
||||
<TelerikSkeleton Width="50%" Height="16px" ShapeType="@Telerik.Blazor.SkeletonShapeType.Text" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else if (CategoriesHaveItemsToShow)
|
||||
{
|
||||
<div class="block block-shop-categories home-categories-block">
|
||||
<div class="container container--max--xl">
|
||||
<div class="block__title">
|
||||
<h2 class="decor-header decor-header--align--center">Latest Categories</h2>
|
||||
</div>
|
||||
<div class="categories-list">
|
||||
@foreach (var category in LatestCategories)
|
||||
{
|
||||
<CategoryCard Category="category" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="block block-shop-categories home-categories-block home-categories-block--empty">
|
||||
<div class="container container--max--xl">
|
||||
<div class="block__title">
|
||||
<h2 class="decor-header decor-header--align--center">Latest Categories</h2>
|
||||
</div>
|
||||
<div class="alert alert-info mb-0">
|
||||
Categories will appear here once items are available.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- block-shop-categories / end -->
|
||||
197
Prefab.Web.Client/Pages/Home.razor.cs
Normal file
197
Prefab.Web.Client/Pages/Home.razor.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Prefab.Web.Client.Models.Catalog;
|
||||
using Prefab.Web.Client.Models.Home;
|
||||
using Prefab.Web.Client.Services;
|
||||
using Prefab.Web.Client.ViewModels.Catalog;
|
||||
|
||||
namespace Prefab.Web.Client.Pages;
|
||||
|
||||
public class HomeComponent : ResultComponentBase<HomePageModel>
|
||||
{
|
||||
private const int CategorySkeletonSlots = 4;
|
||||
|
||||
private readonly List<ProductCardModel> _featuredProducts = [];
|
||||
private readonly List<CategoryCardModel> _latestCategories = [];
|
||||
|
||||
[Inject]
|
||||
protected IHomePageService HomePageService { get; set; } = null!;
|
||||
|
||||
public IEnumerable<Slide> Slides => new List<Slide>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Number = 1,
|
||||
Text = "Take on more work, without adding more people.",
|
||||
TextWidth = "full",
|
||||
TextPosition = "top-right"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Number = 4,
|
||||
Text = "Real-time visibility to your assemblies.",
|
||||
TextWidth = "half",
|
||||
TextPosition = "center-right"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Number = 2,
|
||||
Text = "Explore, configure, order, install.",
|
||||
TextWidth = "full",
|
||||
TextPosition = "top-left"
|
||||
}
|
||||
};
|
||||
|
||||
protected IReadOnlyList<ProductCardModel> FeaturedProducts => _featuredProducts;
|
||||
|
||||
protected bool IsFeaturedProductsLoading { get; private set; } = true;
|
||||
|
||||
protected bool FeaturedProductsHaveItemsToShow => _featuredProducts.Count > 0;
|
||||
|
||||
protected int NumberOfFeaturedProductCardsPerGroup { get; private set; } = 4;
|
||||
|
||||
protected IReadOnlyList<CategoryCardModel> LatestCategories => _latestCategories;
|
||||
|
||||
protected bool AreCategoriesLoading { get; private set; } = true;
|
||||
|
||||
protected bool CategoriesHaveItemsToShow => _latestCategories.Count > 0;
|
||||
|
||||
protected int NumberOfCategorySkeletons => CategorySkeletonSlots;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadHomeContentAsync();
|
||||
}
|
||||
|
||||
protected IReadOnlyList<IReadOnlyList<ProductCardModel>> GetProductCardGroups()
|
||||
{
|
||||
if (!FeaturedProductsHaveItemsToShow)
|
||||
{
|
||||
return Array.Empty<IReadOnlyList<ProductCardModel>>();
|
||||
}
|
||||
|
||||
var groupSize = Math.Max(1, NumberOfFeaturedProductCardsPerGroup);
|
||||
var groups = new List<IReadOnlyList<ProductCardModel>>();
|
||||
|
||||
for (var index = 0; index < _featuredProducts.Count; index += groupSize)
|
||||
{
|
||||
var segment = _featuredProducts
|
||||
.Skip(index)
|
||||
.Take(groupSize)
|
||||
.ToList();
|
||||
|
||||
if (segment.Count > 0)
|
||||
{
|
||||
groups.Add(segment);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
protected string CalculateWidthOfFeaturedProductCard()
|
||||
{
|
||||
var groupSize = Math.Max(1, NumberOfFeaturedProductCardsPerGroup);
|
||||
var width = 100d / groupSize;
|
||||
return $"width: {width:0.##}%; padding: 0 10px; box-sizing: border-box;";
|
||||
}
|
||||
|
||||
#region Media Query Handlers
|
||||
|
||||
protected void OnExtraSmallChange(bool matches)
|
||||
{
|
||||
if (!matches)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NumberOfFeaturedProductCardsPerGroup = 1;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected void OnSmallChange(bool matches)
|
||||
{
|
||||
if (!matches)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NumberOfFeaturedProductCardsPerGroup = 2;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected void OnMediumChange(bool matches)
|
||||
{
|
||||
if (!matches)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NumberOfFeaturedProductCardsPerGroup = 2;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected void OnLargeChange(bool matches)
|
||||
{
|
||||
if (!matches)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NumberOfFeaturedProductCardsPerGroup = 3;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected void OnExtraLargeChange(bool matches)
|
||||
{
|
||||
if (!matches)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NumberOfFeaturedProductCardsPerGroup = 4;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task LoadHomeContentAsync()
|
||||
{
|
||||
IsFeaturedProductsLoading = true;
|
||||
AreCategoriesLoading = true;
|
||||
_featuredProducts.Clear();
|
||||
_latestCategories.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Execute(HomePageService.Get, CancellationToken.None);
|
||||
|
||||
if (result.IsSuccess && result.Value is { } payload)
|
||||
{
|
||||
if (payload.FeaturedProducts.Count > 0)
|
||||
{
|
||||
_featuredProducts.AddRange(payload.FeaturedProducts);
|
||||
}
|
||||
|
||||
if (payload.LatestCategories.Count > 0)
|
||||
{
|
||||
_latestCategories.AddRange(payload.LatestCategories);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsFeaturedProductsLoading = false;
|
||||
AreCategoriesLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public class Slide
|
||||
{
|
||||
public int Number { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public string TextWidth { get; set; } = string.Empty;
|
||||
public string TextPosition { get; set; } = string.Empty;
|
||||
public string AltText { get; set; } = string.Empty;
|
||||
public string GoToUrl { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
246
Prefab.Web.Client/Pages/Home.razor.css
Normal file
246
Prefab.Web.Client/Pages/Home.razor.css
Normal file
@@ -0,0 +1,246 @@
|
||||
.slide {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slide-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.slide-text {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 5px rgba(0,0,0,0.7);
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.slide-text.full {
|
||||
width: 90%;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.slide-text.half {
|
||||
width: 45%;
|
||||
max-width: 45%;
|
||||
}
|
||||
|
||||
.slide-text.third {
|
||||
width: 30%;
|
||||
max-width: 30%;
|
||||
}
|
||||
|
||||
.slide-text.bottom-left {
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
.slide-text.bottom-right {
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.slide-text.bottom-center {
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slide-text.top-left {
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
.slide-text.top-right {
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.slide-text.top-center {
|
||||
top: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slide-text.center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.slide-text.center-left {
|
||||
top: 50%;
|
||||
left: 24px;
|
||||
transform: translateY(-50%);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.slide-text.center-right {
|
||||
top: 50%;
|
||||
right: 24px;
|
||||
transform: translateY(-50%);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.slide-text {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.slide-text.full,
|
||||
.slide-text.half,
|
||||
.slide-text.third {
|
||||
width: 70%;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.slide-text h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.slide-text p {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.slide-text {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.slide-text.full,
|
||||
.slide-text.half,
|
||||
.slide-text.third {
|
||||
width: 85%;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.slide-text h2 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.slide-text p {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.slide-text.top-left,
|
||||
.slide-text.bottom-left {
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
.slide-text.center-left {
|
||||
left: 18px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.slide-text.top-right,
|
||||
.slide-text.bottom-right {
|
||||
right: 18px;
|
||||
}
|
||||
|
||||
.slide-text.center-right {
|
||||
right: 18px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.slide-text.top-center,
|
||||
.slide-text.bottom-center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.slide-text.center {
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.slide-text {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.slide-text.full,
|
||||
.slide-text.half,
|
||||
.slide-text.third {
|
||||
width: calc(100% - 32px);
|
||||
max-width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.slide-text.top-left,
|
||||
.slide-text.bottom-left,
|
||||
.slide-text.center-left {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
.slide-text.top-right,
|
||||
.slide-text.bottom-right,
|
||||
.slide-text.center-right {
|
||||
right: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.slide-text.top-center,
|
||||
.slide-text.bottom-center {
|
||||
width: calc(100% - 48px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.slide-text.center {
|
||||
width: calc(100% - 48px);
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.slide-text h2 {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.slide-text p {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom styling for product carousel to remove border and background */
|
||||
::deep .product-carousel-custom.k-scrollview {
|
||||
border: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
::deep .product-carousel-custom .k-scrollview-wrap {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
::deep .product-carousel-custom .k-scrollview-prev,
|
||||
::deep .product-carousel-custom .k-scrollview-next {
|
||||
background-color: transparent !important;
|
||||
color: #ffd599 !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
::deep .product-carousel-custom .k-scrollview-prev:hover,
|
||||
::deep .product-carousel-custom .k-scrollview-next:hover,
|
||||
::deep .product-carousel-custom .k-scrollview-prev:focus,
|
||||
::deep .product-carousel-custom .k-scrollview-next:focus {
|
||||
color: #576166 !important;
|
||||
}
|
||||
|
||||
::deep .product-carousel-custom .k-scrollview-prev .k-icon,
|
||||
::deep .product-carousel-custom .k-scrollview-next .k-icon {
|
||||
color: inherit !important;
|
||||
}
|
||||
12
Prefab.Web.Client/Pages/NotFound.razor
Normal file
12
Prefab.Web.Client/Pages/NotFound.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
@rendermode InteractiveAuto
|
||||
|
||||
<PageTitle>Page Not Found</PageTitle>
|
||||
|
||||
<div class="block mt-4">
|
||||
<div class="container container--max--xl">
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
</div>
|
||||
</div>
|
||||
43
Prefab.Web.Client/Pages/ResultComponentBase.cs
Normal file
43
Prefab.Web.Client/Pages/ResultComponentBase.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Prefab.Web.Client.Models.Shared;
|
||||
using Prefab.Web.Client.Services;
|
||||
|
||||
namespace Prefab.Web.Client.Pages;
|
||||
|
||||
public abstract class ResultComponentBase<T> : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
protected INotificationService NotificationService { get; set; } = null!;
|
||||
|
||||
protected async Task<Result<T>> Execute(
|
||||
Func<CancellationToken, Task<Result<T>>> operation,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await operation(cancellationToken);
|
||||
|
||||
if (!result.IsSuccess && result.Problem is { } problem)
|
||||
{
|
||||
var message = string.IsNullOrWhiteSpace(problem.Detail)
|
||||
? problem.Title ?? "An unexpected error occurred."
|
||||
: problem.Detail;
|
||||
|
||||
NotificationService.ShowError(message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
var problem = ResultProblem.Unexpected("Failed to complete the requested operation.", exception);
|
||||
var fallback = problem.Detail ?? problem.Title ?? "Failed to complete the requested operation.";
|
||||
NotificationService.ShowError(fallback);
|
||||
return Result<T>.Failure(problem);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Prefab.Web.Client/Pages/Weather.razor
Normal file
63
Prefab.Web.Client/Pages/Weather.razor
Normal file
@@ -0,0 +1,63 @@
|
||||
@page "/weather"
|
||||
|
||||
<PageTitle>Weather</PageTitle>
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
||||
<th aria-label="Temperature in Fahrenheit">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>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Simulate asynchronous loading to demonstrate a loading indicator
|
||||
await Task.Delay(500);
|
||||
|
||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = startDate.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int TemperatureC { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user