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

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

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