Init
This commit is contained in:
60
Prefab.Web.Client/Components/Catalog/CategoryCard.razor
Normal file
60
Prefab.Web.Client/Components/Catalog/CategoryCard.razor
Normal file
@@ -0,0 +1,60 @@
|
||||
<Card Variant="CardVariant.Category"
|
||||
CssClass="@CssClass"
|
||||
LinkHref="@LinkHref"
|
||||
LinkTarget="@LinkTarget"
|
||||
LinkRel="@LinkRel"
|
||||
ImagePlaceholderText="@ImagePlaceholderText"
|
||||
OnLinkClick="HandleLinkClickAsync">
|
||||
<Image>
|
||||
@if (!string.IsNullOrWhiteSpace(Category.ImageUrl))
|
||||
{
|
||||
<img src="@Category.ImageUrl"
|
||||
alt="@Category.Title"
|
||||
loading="lazy" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="category-card__image-placeholder">
|
||||
@ImagePlaceholderText
|
||||
</div>
|
||||
}
|
||||
</Image>
|
||||
|
||||
<Heading>
|
||||
@Category.Title
|
||||
</Heading>
|
||||
|
||||
<Description>
|
||||
@if (HasSecondaryText)
|
||||
{
|
||||
@Category.SecondaryText
|
||||
}
|
||||
</Description>
|
||||
</Card>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public CategoryCardModel Category { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string CssClass { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string ImagePlaceholderText { get; set; } = "Image coming soon";
|
||||
|
||||
[Parameter]
|
||||
public string? LinkTarget { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? LinkRel { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CategoryCardModel> OnCategorySelected { get; set; }
|
||||
|
||||
private string? LinkHref => string.IsNullOrWhiteSpace(Category.Url) ? null : Category.Url;
|
||||
|
||||
private bool HasSecondaryText => !string.IsNullOrWhiteSpace(Category.SecondaryText);
|
||||
|
||||
private Task HandleLinkClickAsync(MouseEventArgs args) =>
|
||||
OnCategorySelected.HasDelegate ? OnCategorySelected.InvokeAsync(Category) : Task.CompletedTask;
|
||||
}
|
||||
14
Prefab.Web.Client/Components/Catalog/CategoryCard.razor.css
Normal file
14
Prefab.Web.Client/Components/Catalog/CategoryCard.razor.css
Normal file
@@ -0,0 +1,14 @@
|
||||
.category-card__image-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: #f5f5f5;
|
||||
color: #6c757d;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
163
Prefab.Web.Client/Components/Catalog/ProductCard.razor
Normal file
163
Prefab.Web.Client/Components/Catalog/ProductCard.razor
Normal file
@@ -0,0 +1,163 @@
|
||||
@using System.Globalization
|
||||
@using System.Linq
|
||||
@using Prefab.Web.Client.Models.Shared
|
||||
|
||||
<Card Variant="CardVariant.Product"
|
||||
Layout="@Layout"
|
||||
CssClass="@CssClass"
|
||||
ImagePlaceholderText="@ImagePlaceholderText"
|
||||
AdditionalAttributes="RootAttributes"
|
||||
ShowBadges="@ShowBadgesSection"
|
||||
ShowMeta="@ShowMetaSection"
|
||||
ShowRating="@ShouldRenderRating"
|
||||
ShowDescription="@ShowDescriptionSection"
|
||||
ShowFooter="false"
|
||||
Prices="@PricesTemplate">
|
||||
<Badges>
|
||||
@if (Product.IsOnSale)
|
||||
{
|
||||
<div class="product-card__badge product-card__badge--style--sale">Sale</div>
|
||||
}
|
||||
@foreach (var badge in Product.Badges.Where(static badge => !string.IsNullOrWhiteSpace(badge)))
|
||||
{
|
||||
<div class="product-card__badge">@badge</div>
|
||||
}
|
||||
</Badges>
|
||||
<BadgesContent>
|
||||
@BadgesContent?.Invoke(Product)
|
||||
</BadgesContent>
|
||||
<Image>
|
||||
<a href="@Product.Url">
|
||||
<img src="@ImageSource"
|
||||
alt="@ImageAltText"
|
||||
loading="lazy" />
|
||||
</a>
|
||||
</Image>
|
||||
<Meta>
|
||||
@if (!string.IsNullOrWhiteSpace(Product.CategoryName))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(Product.CategoryUrl))
|
||||
{
|
||||
<a href="@Product.CategoryUrl">@Product.CategoryName</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Product.CategoryName
|
||||
}
|
||||
}
|
||||
</Meta>
|
||||
<Heading>
|
||||
<a href="@Product.Url">
|
||||
@Product.Title
|
||||
</a>
|
||||
</Heading>
|
||||
<Rating>
|
||||
@if (Product.ReviewCount > 0)
|
||||
{
|
||||
<div class="product-card__rating-title">@GetRatingTitle()</div>
|
||||
}
|
||||
<div class="product-card__rating-stars">
|
||||
<div class="rating" aria-label="@RatingAriaLabel">
|
||||
<div class="rating__body">
|
||||
@foreach (var star in Enumerable.Range(1, TotalStars))
|
||||
{
|
||||
var isActive = star <= Math.Clamp(Product.Rating, 0, TotalStars);
|
||||
<svg class="rating__star @(isActive ? "rating__star--active" : string.Empty)"
|
||||
width="13px"
|
||||
height="12px"
|
||||
aria-hidden="true">
|
||||
<g class="rating__fill">
|
||||
<use xlink:href="images/sprite.svg#star-normal"></use>
|
||||
</g>
|
||||
<g class="rating__stroke">
|
||||
<use xlink:href="images/sprite.svg#star-normal-stroke"></use>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Rating>
|
||||
<Description>
|
||||
@DescriptionContent?.Invoke(Product)
|
||||
</Description>
|
||||
</Card>
|
||||
|
||||
@code {
|
||||
private static readonly CultureInfo PriceCulture = CultureInfo.CurrentCulture;
|
||||
private const string PlaceholderImageDataUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA0MDAgNDAwJz48cmVjdCB3aWR0aD0nNDAwJyBoZWlnaHQ9JzQwMCcgZmlsbD0nI2YzZjNmMycvPjx0ZXh0IHg9JzUwJScgeT0nNTAlJyBkb21pbmFudC1iYXNlbGluZT0nbWlkZGxlJyB0ZXh0LWFuY2hvcj0nbWlkZGxlJyBmb250LWZhbWlseT0nT3BlbiBTYW5zLCBBcmlhbCwgc2Fucy1zZXJpZicgZm9udC1zaXplPScyOCcgZmlsbD0nIzliOWI5Yic+SW1hZ2UgY29taW5nIHNvb248L3RleHQ+PC9zdmc+";
|
||||
|
||||
[Parameter, EditorRequired]
|
||||
public ProductCardModel Product { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string Layout { get; set; } = "grid";
|
||||
|
||||
[Parameter]
|
||||
public string CssClass { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string ImagePlaceholderText { get; set; } = "Image coming soon";
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment<ProductCardModel>? BadgesContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment<ProductCardModel>? DescriptionContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool ShowPrice { get; set; }
|
||||
|
||||
private bool HasBadges => Product.IsOnSale || Product.Badges.Any(static badge => !string.IsNullOrWhiteSpace(badge));
|
||||
|
||||
private bool ShouldRenderRating => Product.Rating > 0 || Product.ReviewCount > 0;
|
||||
|
||||
private bool ShowBadgesSection => HasBadges || BadgesContent is not null;
|
||||
|
||||
private bool ShowMetaSection => !string.IsNullOrWhiteSpace(Product.CategoryName);
|
||||
|
||||
private bool ShowDescriptionSection => DescriptionContent is not null;
|
||||
|
||||
private RenderFragment? PricesTemplate => !ShowPrice || !Product.HasPrice
|
||||
? null
|
||||
: @<text>
|
||||
<div class="product-card__price">
|
||||
<span class="product-card__price-new">@FormatMoney(Product.FromPrice!)</span>
|
||||
@if (Product.OldPrice.HasValue)
|
||||
{
|
||||
<span class="product-card__price-old">@FormatDecimal(Product.OldPrice.Value)</span>
|
||||
}
|
||||
</div>
|
||||
</text>;
|
||||
|
||||
private string RatingAriaLabel => Product.Rating > 0
|
||||
? $"{Math.Clamp(Product.Rating, 0, TotalStars)} out of {TotalStars}"
|
||||
: "No rating yet";
|
||||
|
||||
private IReadOnlyDictionary<string, object>? RootAttributes =>
|
||||
string.IsNullOrWhiteSpace(Product.Sku)
|
||||
? null
|
||||
: new Dictionary<string, object>
|
||||
{
|
||||
["data-sku"] = Product.Sku
|
||||
};
|
||||
|
||||
private string FormatMoney(MoneyModel money) => money.Amount.ToString("C", PriceCulture);
|
||||
|
||||
private string FormatDecimal(decimal value) => value.ToString("C", PriceCulture);
|
||||
|
||||
private string GetRatingTitle() => $"Reviews ({Math.Max(Product.ReviewCount, 0)})";
|
||||
|
||||
private string ImageSource => string.IsNullOrWhiteSpace(Product.PrimaryImageUrl)
|
||||
? PlaceholderImageDataUrl
|
||||
: Product.PrimaryImageUrl!;
|
||||
|
||||
private string ImageAltText => string.IsNullOrWhiteSpace(Product.PrimaryImageUrl)
|
||||
? ImagePlaceholderText
|
||||
: Product.Title;
|
||||
|
||||
private const int TotalStars = 5;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user