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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
241
Prefab.Web.Client/Components/Shared/Card.razor
Normal file
241
Prefab.Web.Client/Components/Shared/Card.razor
Normal file
@@ -0,0 +1,241 @@
|
||||
<div class="@RootClass" @attributes="AdditionalAttributes">
|
||||
@if (Variant == CardVariant.Product)
|
||||
{
|
||||
if (ShowBadges && (Badges is not null || BadgesContent is not null))
|
||||
{
|
||||
<div class="product-card__badges-list">
|
||||
@Badges
|
||||
@BadgesContent
|
||||
</div>
|
||||
}
|
||||
|
||||
if (ShowActions && Actions is not null)
|
||||
{
|
||||
<div class="product-card__actions">
|
||||
<div class="product-card__actions-list">
|
||||
@Actions
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="product-card__image">
|
||||
@if (Image is not null)
|
||||
{
|
||||
@Image
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(ImagePlaceholderText))
|
||||
{
|
||||
<span class="product-card__image-placeholder">@ImagePlaceholderText</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="product-card__info">
|
||||
@if (ShowMeta && Meta is not null)
|
||||
{
|
||||
<div class="product-card__category">
|
||||
@Meta
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowHeading && Heading is not null)
|
||||
{
|
||||
<div class="product-card__name">
|
||||
@Heading
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowRating && Rating is not null)
|
||||
{
|
||||
<div class="product-card__rating">
|
||||
@Rating
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowDescription && Description is not null)
|
||||
{
|
||||
<div class="product-card__description">
|
||||
@Description
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowPrices && Prices is not null)
|
||||
{
|
||||
<div class="product-card__prices-list">
|
||||
@Prices
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowFooter && Footer is not null)
|
||||
{
|
||||
<div class="product-card__buttons">
|
||||
<div class="product-card__buttons-list">
|
||||
@Footer
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else if (Variant == CardVariant.Category)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(LinkHref))
|
||||
{
|
||||
<a href="@LinkHref" target="@LinkTarget" rel="@LinkRel" @onclick="HandleLinkClickAsync">
|
||||
@CategoryContent
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@CategoryContent
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private const string ProductRootClass = "product-card";
|
||||
private const string ProductLayoutGridClass = "product-card--layout--grid";
|
||||
private const string ProductLayoutListClass = "product-card--layout--list";
|
||||
private const string CategoryRootClass = "card";
|
||||
private const string CategoryCardClass = "category-card";
|
||||
|
||||
[Parameter]
|
||||
public CardVariant Variant { get; set; } = CardVariant.Product;
|
||||
|
||||
[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 string? LinkHref { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? LinkTarget { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? LinkRel { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<MouseEventArgs> OnLinkClick { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Badges { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? BadgesContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Actions { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Image { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Meta { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Heading { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Rating { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Description { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Prices { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? Footer { get; set; }
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool ShowBadges { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowActions { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowMeta { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowHeading { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowRating { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowDescription { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowPrices { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public bool ShowFooter { get; set; } = true;
|
||||
|
||||
private string RootClass => Variant switch
|
||||
{
|
||||
CardVariant.Category => BuildClass(CategoryRootClass, CategoryCardClass, CssClass),
|
||||
_ => BuildClass(ProductRootClass, ResolveLayoutClass(), CssClass)
|
||||
};
|
||||
|
||||
private string ResolveLayoutClass() =>
|
||||
string.Equals(Layout, "list", StringComparison.OrdinalIgnoreCase)
|
||||
? ProductLayoutListClass
|
||||
: ProductLayoutGridClass;
|
||||
|
||||
private static string BuildClass(params string?[] values)
|
||||
{
|
||||
var parts = new List<string>(values.Length);
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
parts.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
private RenderFragment CategoryContent => @<text>
|
||||
@if (Image is not null)
|
||||
{
|
||||
<div class="category-card__image">
|
||||
@Image
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(ImagePlaceholderText))
|
||||
{
|
||||
<div class="category-card__image">@ImagePlaceholderText</div>
|
||||
}
|
||||
|
||||
@if (Heading is not null)
|
||||
{
|
||||
<div class="category-card__name">
|
||||
@Heading
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Description is not null)
|
||||
{
|
||||
<div class="category-card__products">
|
||||
@Description
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Footer is not null)
|
||||
{
|
||||
@Footer
|
||||
}
|
||||
</text>;
|
||||
|
||||
private Task HandleLinkClickAsync(MouseEventArgs args) =>
|
||||
OnLinkClick.HasDelegate ? OnLinkClick.InvokeAsync(args) : Task.CompletedTask;
|
||||
}
|
||||
33
Prefab.Web.Client/Components/Shared/CardGrid.razor
Normal file
33
Prefab.Web.Client/Components/Shared/CardGrid.razor
Normal file
@@ -0,0 +1,33 @@
|
||||
<div class="@ContainerClass">
|
||||
@ChildContent
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string View { get; set; } = "grid";
|
||||
|
||||
[Parameter]
|
||||
public string CssClass { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private string ContainerClass => string.Join(" ", BuildClasses());
|
||||
|
||||
private IEnumerable<string> BuildClasses()
|
||||
{
|
||||
yield return "products-view__list";
|
||||
yield return "products-list";
|
||||
yield return ResolveLayoutClass();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(CssClass))
|
||||
{
|
||||
yield return CssClass;
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveLayoutClass() =>
|
||||
string.Equals(View, "list", StringComparison.OrdinalIgnoreCase)
|
||||
? "products-list--layout--list"
|
||||
: "products-list--layout--grid-3";
|
||||
}
|
||||
11
Prefab.Web.Client/Components/Shared/CardGridItem.razor
Normal file
11
Prefab.Web.Client/Components/Shared/CardGridItem.razor
Normal file
@@ -0,0 +1,11 @@
|
||||
@if (ChildContent is not null)
|
||||
{
|
||||
<div class="products-list__item">
|
||||
@ChildContent
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
7
Prefab.Web.Client/Components/Shared/CardVariant.cs
Normal file
7
Prefab.Web.Client/Components/Shared/CardVariant.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Prefab.Web.Client.Components.Shared;
|
||||
|
||||
public enum CardVariant
|
||||
{
|
||||
Product,
|
||||
Category
|
||||
}
|
||||
Reference in New Issue
Block a user