Latest
This commit is contained in:
@@ -39,9 +39,40 @@
|
|||||||
.pdp-skeleton__block--options {
|
.pdp-skeleton__block--options {
|
||||||
min-height: 160px;
|
min-height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === PDP (Telerik bridge) === */
|
||||||
|
.t-pdp .product__name { line-height: 1.1; margin-top: .25rem; }
|
||||||
|
.t-pdp .product__price-new { color: #e53935; font-weight: 700; font-size: 1.75rem; }
|
||||||
|
|
||||||
|
/* Space option groups */
|
||||||
|
.t-pdp .product__option { margin-bottom: 1.25rem; }
|
||||||
|
.t-pdp .product__option-label { text-transform: uppercase; font-size: .8rem; color: #6c757d; letter-spacing: .02em; margin-bottom: .5rem; }
|
||||||
|
|
||||||
|
/* RadioGroup: make Kendo radios look like the template's radio-select */
|
||||||
|
.t-pdp .t-pdp-radio.k-radio-group { display: grid; gap: .35rem; }
|
||||||
|
.t-pdp .t-pdp-radio .k-radio-item { display: flex; align-items: center; gap: .5rem; padding: .125rem 0; }
|
||||||
|
.t-pdp .t-pdp-radio .k-radio-label { font-size: 1rem; }
|
||||||
|
.t-pdp .t-pdp-radio input.k-radio:checked + .k-radio-label { font-weight: 600; }
|
||||||
|
|
||||||
|
/* NumericTextBox sizing/spacing */
|
||||||
|
.t-pdp .t-pdp-number .k-numerictextbox,
|
||||||
|
.t-pdp .t-pdp-qty .k-numerictextbox { display: inline-flex; }
|
||||||
|
.t-pdp .product__actions-item { display: inline-flex; align-items: center; }
|
||||||
|
|
||||||
|
/* TabStrip headers: resemble template tabs underline */
|
||||||
|
.t-pdp .t-pdp-tabstrip .k-tabstrip-items { border-bottom: 1px solid #e9ecef; }
|
||||||
|
.t-pdp .t-pdp-tabstrip .k-item .k-link { padding: .75rem 1rem; color: #6c757d; }
|
||||||
|
.t-pdp .t-pdp-tabstrip .k-item.k-active .k-link { color: #e53935; border-bottom: 2px solid #e53935; }
|
||||||
|
|
||||||
|
/* Spec table tweaks */
|
||||||
|
.t-pdp .product__tab-specification table { margin-top: .5rem; }
|
||||||
|
|
||||||
|
/* Gallery image sizing */
|
||||||
|
.t-pdp .product__gallery img { max-width: 100%; height: auto; display: block; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container my-4">
|
<div class="block">
|
||||||
|
<div class="container container--max--xl t-pdp">
|
||||||
@if (_isLoading)
|
@if (_isLoading)
|
||||||
{
|
{
|
||||||
if (HasTelerikSkeleton)
|
if (HasTelerikSkeleton)
|
||||||
@@ -75,119 +106,130 @@
|
|||||||
|
|
||||||
<div class="product product--layout--standard">
|
<div class="product product--layout--standard">
|
||||||
<div class="product__content">
|
<div class="product__content">
|
||||||
<div class="product__gallery">
|
<div class="row g-4 g-lg-5 align-items-start">
|
||||||
<div class="product-gallery">
|
<div class="col-12 col-lg-6">
|
||||||
<div class="product-gallery__featured">
|
<div class="product__gallery">
|
||||||
<img src="/images/placeholder-600x600.png" alt="@product.Name" class="img-fluid" />
|
<div class="product-gallery">
|
||||||
|
<div class="product-gallery__featured">
|
||||||
|
<img src="/images/placeholder-600x600.png" alt="@product.Name" class="img-fluid" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-12 col-lg-6">
|
||||||
<div class="product__info">
|
<div class="product__info">
|
||||||
<div class="product__meta mb-2">
|
<!-- keep the existing meta, name, price, description, options, and actions exactly as below -->
|
||||||
<ul class="product__meta-list list-unstyled mb-2">
|
<div class="product__meta mb-2">
|
||||||
@if (!string.IsNullOrWhiteSpace(product.Sku))
|
<ul class="product__meta-list list-unstyled mb-2">
|
||||||
{
|
@if (!string.IsNullOrWhiteSpace(product.Sku))
|
||||||
<li><span class="text-muted">SKU:</span> @product.Sku</li>
|
|
||||||
}
|
|
||||||
@if (product.Categories.Any())
|
|
||||||
{
|
|
||||||
<li>
|
|
||||||
<span class="text-muted">Category:</span>
|
|
||||||
@for (var index = 0; index < product.Categories.Count; index++)
|
|
||||||
{
|
{
|
||||||
var category = product.Categories[index];
|
<li><span class="text-muted">SKU:</span> @product.Sku</li>
|
||||||
<span>@category.Name</span>
|
|
||||||
if (index < product.Categories.Count - 1)
|
|
||||||
{
|
|
||||||
<span>, </span>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</li>
|
@if (product.Categories.Any())
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 class="product__name">@product.Name</h1>
|
|
||||||
|
|
||||||
@if (product.BasePrice.HasValue)
|
|
||||||
{
|
|
||||||
<div class="product__price mt-3">
|
|
||||||
<span class="product__price-new">@FormatCurrency(product.BasePrice.Value)</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(product.Description))
|
|
||||||
{
|
|
||||||
<div class="product__description mt-3">@((MarkupString)product.Description)</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (product.Options.Count > 0)
|
|
||||||
{
|
|
||||||
<div class="product__options mt-4">
|
|
||||||
@foreach (var option in product.Options)
|
|
||||||
{
|
|
||||||
var optionIsChoice = option.DataType == (int)GetProductDetail.OptionDataType.Choice;
|
|
||||||
|
|
||||||
<div class="product__option mb-4">
|
|
||||||
<label class="product__option-label d-block fw-semibold">
|
|
||||||
@option.Name
|
|
||||||
@if (option.IsVariantAxis)
|
|
||||||
{
|
|
||||||
<span class="badge bg-secondary ms-2">Axis</span>
|
|
||||||
}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
@if (optionIsChoice)
|
|
||||||
{
|
{
|
||||||
<TelerikRadioGroup TValue="string" TItem="ChoiceItem"
|
<li>
|
||||||
Class="product__option-group"
|
<span class="text-muted">Category:</span>
|
||||||
Data="@BuildChoiceItems(option)"
|
@for (var index = 0; index < product.Categories.Count; index++)
|
||||||
TextField="@nameof(ChoiceItem.Text)"
|
|
||||||
ValueField="@nameof(ChoiceItem.Value)"
|
|
||||||
Value="@GetChoiceSelection(option.Id)"
|
|
||||||
ValueChanged="(string? value) => SetChoiceSelection(option.Id, value)" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="product__number-option d-flex align-items-center">
|
|
||||||
<TelerikNumericTextBox Value="@GetNumberSelection(option.Id)"
|
|
||||||
ValueChanged="(decimal? value) => SetNumberSelection(option.Id, value)"
|
|
||||||
Min="@option.Min"
|
|
||||||
Max="@option.Max"
|
|
||||||
Step="@ResolveStep(option)"
|
|
||||||
Format="n2"
|
|
||||||
Width="200px" />
|
|
||||||
@if (!string.IsNullOrWhiteSpace(option.Unit))
|
|
||||||
{
|
{
|
||||||
<small class="text-muted ms-2">Unit: @option.Unit</small>
|
var category = product.Categories[index];
|
||||||
|
<span>@category.Name</span>
|
||||||
|
if (index < product.Categories.Count - 1)
|
||||||
|
{
|
||||||
|
<span>, </span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="product__name">@product.Name</h1>
|
||||||
|
|
||||||
|
@if (product.BasePrice.HasValue)
|
||||||
|
{
|
||||||
|
<div class="product__price mt-3">
|
||||||
|
<span class="product__price-new">@FormatCurrency(product.BasePrice.Value)</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(product.Description))
|
||||||
|
{
|
||||||
|
<div class="product__description mt-3">@((MarkupString)product.Description)</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (product.Options.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="product__options mt-4">
|
||||||
|
@foreach (var option in product.Options)
|
||||||
|
{
|
||||||
|
var optionIsChoice = option.DataType == (int)GetProductDetail.OptionDataType.Choice;
|
||||||
|
|
||||||
|
<div class="product__option mb-4">
|
||||||
|
<label class="product__option-label d-block fw-semibold">
|
||||||
|
@option.Name
|
||||||
|
@if (option.IsVariantAxis)
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary ms-2">Axis</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
@if (optionIsChoice)
|
||||||
|
{
|
||||||
|
<TelerikRadioGroup TValue="string" TItem="ChoiceItem"
|
||||||
|
Class="t-pdp-radio product__option-group"
|
||||||
|
Data="@BuildChoiceItems(option)"
|
||||||
|
TextField="@nameof(ChoiceItem.Text)"
|
||||||
|
ValueField="@nameof(ChoiceItem.Value)"
|
||||||
|
Value="@GetChoiceSelection(option.Id)"
|
||||||
|
ValueChanged="(string? value) => SetChoiceSelection(option.Id, value)" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="product__number-option d-flex align-items-center">
|
||||||
|
<TelerikNumericTextBox Value="@GetNumberSelection(option.Id)"
|
||||||
|
ValueChanged="(decimal? value) => SetNumberSelection(option.Id, value)"
|
||||||
|
Min="@option.Min"
|
||||||
|
Max="@option.Max"
|
||||||
|
Step="@ResolveStep(option)"
|
||||||
|
Format="n2"
|
||||||
|
Size="@Telerik.Blazor.ThemeConstants.NumericTextBox.Size.Large"
|
||||||
|
Class="t-pdp-number"
|
||||||
|
Width="200px" />
|
||||||
|
@if (!string.IsNullOrWhiteSpace(option.Unit))
|
||||||
|
{
|
||||||
|
<small class="text-muted ms-2">Unit: @option.Unit</small>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (option.Tiers.Count > 0)
|
||||||
|
{
|
||||||
|
<small class="text-muted d-block mt-1">Tiered pricing data available.</small>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (option.Tiers.Count > 0)
|
|
||||||
{
|
|
||||||
<small class="text-muted d-block mt-1">Tiered pricing data available.</small>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="product__actions mt-4 d-flex align-items-center">
|
<div class="product__actions mt-4 d-flex align-items-center">
|
||||||
<div class="product__actions-item me-3 d-flex align-items-center">
|
<div class="product__actions-item me-3 d-flex align-items-center">
|
||||||
<label class="form-label me-2 mb-0">Quantity</label>
|
<label class="form-label me-2 mb-0">Quantity</label>
|
||||||
<TelerikNumericTextBox Value="@_quantity"
|
<TelerikNumericTextBox Value="@_quantity"
|
||||||
ValueChanged="(int value) => _quantity = Math.Max(1, value)"
|
ValueChanged="(int value) => _quantity = Math.Max(1, value)"
|
||||||
Min="1"
|
Min="1"
|
||||||
Step="1"
|
Step="1"
|
||||||
Format="n0"
|
Format="n0"
|
||||||
Width="150px" />
|
Size="@Telerik.Blazor.ThemeConstants.NumericTextBox.Size.Large"
|
||||||
</div>
|
Class="t-pdp-qty"
|
||||||
<div class="product__actions-item">
|
Width="150px" />
|
||||||
<TelerikButton Class="btn btn-primary btn-lg product__add-to-cart"
|
</div>
|
||||||
ThemeColor="ThemeColor.Primary"
|
<div class="product__actions-item">
|
||||||
Size="ButtonSize.Large">
|
<TelerikButton Class="btn btn-primary btn-lg product__add-to-cart"
|
||||||
Add to cart
|
ThemeColor="ThemeColor.Primary"
|
||||||
</TelerikButton>
|
Size="ButtonSize.Large">
|
||||||
|
Add to cart
|
||||||
|
</TelerikButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,7 +237,7 @@
|
|||||||
|
|
||||||
<div class="card product__tabs product-tabs product-tabs--layout--full mt-5">
|
<div class="card product__tabs product-tabs product-tabs--layout--full mt-5">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<TelerikTabStrip TabPosition="TabPosition.Top" Class="product-tabs__strip">
|
<TelerikTabStrip TabPosition="TabPosition.Top" Class="t-pdp-tabstrip product-tabs__strip">
|
||||||
<TabStripTab Title="Description">
|
<TabStripTab Title="Description">
|
||||||
<div class="product__tab-description">
|
<div class="product__tab-description">
|
||||||
@if (!string.IsNullOrWhiteSpace(product.Description))
|
@if (!string.IsNullOrWhiteSpace(product.Description))
|
||||||
@@ -240,7 +282,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div> <!-- /.container -->
|
||||||
|
</div> <!-- /.block -->
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private const string DocsAttributeKey = "catalog.product.docs";
|
private const string DocsAttributeKey = "catalog.product.docs";
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ public static class Products
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DetailService(IModuleClient moduleClient)
|
public sealed class DetailService(IModuleClient moduleClient) : IProductDisplayService
|
||||||
{
|
{
|
||||||
public async Task<Result<ProductDisplayModel>> Get(string slug, CancellationToken cancellationToken)
|
public async Task<Result<ProductDisplayModel>> Get(string slug, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public class Module : IModule
|
|||||||
builder.Services.AddScoped<Products.ListingService>();
|
builder.Services.AddScoped<Products.ListingService>();
|
||||||
builder.Services.AddScoped<Products.DetailService>();
|
builder.Services.AddScoped<Products.DetailService>();
|
||||||
builder.Services.AddScoped<IProductListingService>(sp => sp.GetRequiredService<Products.ListingService>());
|
builder.Services.AddScoped<IProductListingService>(sp => sp.GetRequiredService<Products.ListingService>());
|
||||||
|
builder.Services.AddScoped<IProductDisplayService>(sp => sp.GetRequiredService<Products.DetailService>());
|
||||||
builder.Services.AddScoped<IHomePageService>(sp => sp.GetRequiredService<Home.Service>());
|
builder.Services.AddScoped<IHomePageService>(sp => sp.GetRequiredService<Home.Service>());
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
|
|||||||
Reference in New Issue
Block a user