@page "/browse/product" @using System.Globalization @using System.Linq @using System.Net @using Microsoft.AspNetCore.Http @using Prefab.Shared.Catalog.Products @using Prefab.Web.Client.Components.Catalog @using Prefab.Web.Client.Models.Catalog @using Prefab.Web.Client.Models.Shared @using Prefab.Web.Client.Services @using Telerik.Blazor.Components @using Telerik.Blazor @inject IProductDisplayService ProductDisplayService @inject PersistentComponentState? State @implements IDisposable @(Model?.Name ?? "Product") | Prefab
@if (_isLoading) { if (HasTelerikSkeleton) {
} else {
} } else if (Problem is not null) { } else if (Model is not null) { var product = Model; var hasDocs = TryGetDocs(product, out var docsJson);
    @if (!string.IsNullOrWhiteSpace(product.Sku)) {
  • SKU: @product.Sku
  • } @if (product.Categories.Any()) {
  • Category: @for (var index = 0; index < product.Categories.Count; index++) { var category = product.Categories[index]; @category.Name if (index < product.Categories.Count - 1) { , } }
  • }

@product.Name

@if (product.BasePrice.HasValue) {
@FormatCurrency(product.BasePrice.Value)
} @if (!string.IsNullOrWhiteSpace(product.Description)) {
@((MarkupString)product.Description)
} @if (product.Options.Count > 0) {
@foreach (var option in product.Options) { var optionIsChoice = option.DataType == (int)GetProductDetail.OptionDataType.Choice;
@if (optionIsChoice) { } else {
@if (!string.IsNullOrWhiteSpace(option.Unit)) { Unit: @option.Unit }
@if (option.Tiers.Count > 0) { Tiered pricing data available. } }
}
}
Add to cart
@if (!string.IsNullOrWhiteSpace(product.Description)) {
@((MarkupString)product.Description)
} else {

No description available.

} @if (hasDocs && docsJson is not null) { }
@if (product.Specs.Any()) { @foreach (var spec in product.Specs) { }
@spec.Name @FormatSpecValue(spec)
} else {

No specifications were provided.

}
}
@code { private const string DocsAttributeKey = "catalog.product.docs"; private const string StateKey = "browse-product-state"; private static readonly bool HasTelerikSkeleton = Type.GetType("Telerik.Blazor.Components.TelerikSkeleton, Telerik.UI.for.Blazor") is not null; [SupplyParameterFromQuery(Name = "slug")] public string? Slug { get; set; } private Result? _result; private string? _currentSlug; private bool _isLoading = true; private PersistingComponentStateSubscription? _subscription; private readonly Dictionary _choiceSelections = new(); private readonly Dictionary _numberSelections = new(); private int _quantity = 1; private ProductDisplayModel? Model => _result?.Value; private ResultProblem? Problem => _result?.Problem; protected override async Task OnInitializedAsync() { if (State is not null) { _subscription = State.RegisterOnPersisting(PersistStateAsync); if (State.TryTakeFromJson(StateKey, out var restored) && restored is not null) { _currentSlug = restored.Slug; _result = restored.Result; _isLoading = false; if (_result?.Value is { } restoredModel) { PrepareSelections(restoredModel); } } } await base.OnInitializedAsync(); } protected override async Task OnParametersSetAsync() { if (string.IsNullOrWhiteSpace(Slug)) { _currentSlug = null; _result = Result.Failure(new ResultProblem { Title = "Product not specified.", Detail = "A product slug is required to load this page.", StatusCode = (int)HttpStatusCode.BadRequest }); _isLoading = false; return; } var normalizedSlug = Slug.Trim(); if (string.Equals(_currentSlug, normalizedSlug, StringComparison.OrdinalIgnoreCase)) { _isLoading = false; return; } _isLoading = true; var result = await ProductDisplayService.Get(normalizedSlug); _result = result; _currentSlug = normalizedSlug; if (result.IsSuccess && result.Value is not null) { PrepareSelections(result.Value); } else { ClearSelections(); } _isLoading = false; } private Task PersistStateAsync() { if (_currentSlug is not null && _result is not null && State is not null) { State.PersistAsJson(StateKey, new PageState(_currentSlug, _result)); } return Task.CompletedTask; } private static string FormatSpecValue(ProductDisplayModel.Spec spec) { var raw = spec.Value; if (!string.IsNullOrWhiteSpace(raw)) { return raw; } var numericValue = spec.NumericValue; if (numericValue.HasValue) { var formatted = numericValue.Value.ToString("0.##", CultureInfo.InvariantCulture); var unit = spec.UnitCode; return string.IsNullOrWhiteSpace(unit) ? formatted : $"{formatted} {unit}"; } return string.Empty; } private static string FormatCurrency(decimal amount) => amount.ToString("C", CultureInfo.CurrentCulture); private string? GetChoiceSelection(Guid optionId) => _choiceSelections.TryGetValue(optionId, out var value) ? value : null; private void SetChoiceSelection(Guid optionId, string? value) => _choiceSelections[optionId] = value; private decimal? GetNumberSelection(Guid optionId) => _numberSelections.TryGetValue(optionId, out var value) ? value : null; private void SetNumberSelection(Guid optionId, decimal? value) => _numberSelections[optionId] = value; private static IEnumerable BuildChoiceItems(ProductDisplayModel.OptionDefinition option) => option.Values.Select(value => new ChoiceItem( string.IsNullOrWhiteSpace(value.Code) ? value.Id.ToString("N") : value.Code, value.Label)); private static decimal? ResolveStep(ProductDisplayModel.OptionDefinition option) => option.Step ?? 1m; private void PrepareSelections(ProductDisplayModel product) { _choiceSelections.Clear(); _numberSelections.Clear(); _quantity = 1; foreach (var option in product.Options) { if (option.DataType == (int)GetProductDetail.OptionDataType.Choice) { var defaultValue = option.Values.FirstOrDefault(); _choiceSelections[option.Id] = defaultValue?.Code ?? defaultValue?.Id.ToString("N"); } else { decimal? initial = option.Min ?? option.Max ?? 0m; _numberSelections[option.Id] = initial; } } } private void ClearSelections() { _choiceSelections.Clear(); _numberSelections.Clear(); _quantity = 1; } private static bool TryGetDocs(ProductDisplayModel product, out string? docsJson) { docsJson = null; return product.GenericAttributes is not null && product.GenericAttributes.TryGetValue(DocsAttributeKey, out docsJson) && !string.IsNullOrWhiteSpace(docsJson); } public void Dispose() { _subscription?.Dispose(); } private sealed record PageState(string Slug, Result Result); private sealed record ChoiceItem(string Value, string Text); }