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;
}
}

View File

@@ -0,0 +1,6 @@
@page "/categories"
@inherits CategoriesComponent
<PageTitle>Categories</PageTitle>

View 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; } = [];
}

View 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++;
}
}

View 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 -->

View 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;
}
}

View 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;
}

View 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>

View 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);
}
}
}

View 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);
}
}