232 lines
13 KiB
Plaintext
232 lines
13 KiB
Plaintext
@page "/product/{Id:guid}"
|
|
@inject ProductAPIConsumer ProductService
|
|
|
|
<div class="container mx-auto px-4 py-8 md:py-16">
|
|
@if (_loading)
|
|
{
|
|
<div class="flex flex-col items-center justify-center min-h-[50vh] gap-4">
|
|
<div class="loading loading-spinner loading-lg text-primary"></div>
|
|
<p class="text-gray-500 animate-pulse">@Loc["Loading_Product"]...</p>
|
|
</div>
|
|
}
|
|
else if (_product == null)
|
|
{
|
|
<div class="flex flex-col items-center justify-center min-h-[50vh] gap-6 text-center">
|
|
<div class="bg-red-50 p-6 rounded-full">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-red-500" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M9.172 9.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<h2 class="text-2xl font-bold text-gray-800">@Loc["Product_Not_Found"]</h2>
|
|
<a href="/shop" class="btn btn-primary">@Loc["Back_to_Shop"]</a>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
|
|
<!-- Left: Image Viewer -->
|
|
<div class="w-full">
|
|
<Generic.ImageViewer.ImageViewer Width="5" Height="4" ThumbSize="100"
|
|
MediaUrls="@(_product.Photos ?? Array.Empty<string>())" Class="sticky top-24" />
|
|
</div>
|
|
|
|
<!-- Right: Product Info -->
|
|
<div class="flex flex-col gap-8">
|
|
<div>
|
|
@if (!string.IsNullOrEmpty(_product.CategoryName))
|
|
{
|
|
<span
|
|
class="inline-block px-3 py-1 bg-primary/10 text-primary text-xs font-bold uppercase tracking-widest rounded-full mb-4">
|
|
@_product.CategoryName
|
|
</span>
|
|
}
|
|
<h1 class="text-4xl md:text-5xl font-black text-gray-900 leading-tight mb-4">
|
|
@(_product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h1>
|
|
|
|
<div class="flex items-center gap-4">
|
|
<span class="text-3xl font-bold text-primary">
|
|
@(_product.Price * (1 - _product.Discount / 100)) $
|
|
</span>
|
|
@if (_product.Discount > 0)
|
|
{
|
|
<span class="text-xl text-gray-400 line-through">@_product.Price $</span>
|
|
<span class="bg-green-500 text-white text-xs font-bold px-2 py-1 rounded">
|
|
-@_product.Discount%
|
|
</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tabs tabs-bordered mb-4">
|
|
<button class="tab tab-lg @(_activeTab == "description" ? "tab-active font-bold text-primary" : "")"
|
|
@onclick='() => _activeTab = "description"'>
|
|
@Loc["Description"]
|
|
</button>
|
|
<button class="tab tab-lg @(_activeTab == "technical" ? "tab-active font-bold text-primary" : "")"
|
|
@onclick='() => _activeTab = "technical"'>
|
|
@Loc["Technical_Details"]
|
|
</button>
|
|
</div>
|
|
|
|
<div class="min-h-[200px]">
|
|
@if (_activeTab == "description")
|
|
{
|
|
<div class="prose prose-lg text-gray-600 max-w-none animate-in fade-in duration-300">
|
|
<p>@(_product.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)</p>
|
|
</div>
|
|
}
|
|
else if (_activeTab == "technical")
|
|
{
|
|
<!-- Technical Details -->
|
|
@if (_product.Translations?.FirstOrDefault()?.Info?.TechnicalDetails != null &&
|
|
_product.Translations?.FirstOrDefault()?.Info?.TechnicalDetails?.Any() == true)
|
|
{
|
|
<div class="bg-gray-50 rounded-2xl p-6 md:p-8 animate-in fade-in duration-300">
|
|
<h3 class="text-xl font-bold mb-6 flex items-center gap-2">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
@Loc["Technical_Specifications"]
|
|
</h3>
|
|
<dl class="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4">
|
|
@foreach (var detail in _product.Translations?.FirstOrDefault()?.Info?.TechnicalDetails ?? new())
|
|
{
|
|
<div class="flex flex-col border-b border-gray-200 pb-2">
|
|
<dt class="text-sm font-semibold text-gray-500">@detail.Key</dt>
|
|
<dd class="text-base text-gray-900 font-medium">@detail.Value</dd>
|
|
</div>
|
|
}
|
|
</dl>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<p class="text-gray-500 italic">@Loc["No_Technical_Details_Available"]</p>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
<div class="pt-6 flex flex-wrap gap-4">
|
|
<button class="btn btn-primary btn-lg flex-1 md:flex-none md:min-w-[200px] shadow-xl shadow-primary/20">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
|
</svg>
|
|
@Loc["Add_to_Cart"]
|
|
</button>
|
|
<button class="btn btn-outline btn-lg">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
@Loc["Favorite"]
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (_product.IsBundle && _product.ChildProducts?.Any() == true)
|
|
{
|
|
<div class="mt-20 pt-16 border-t border-gray-100 animate-in slide-in-from-bottom-10 duration-700">
|
|
<div class="flex flex-col gap-10">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-3xl font-black text-gray-900 tracking-tight flex items-center gap-3">
|
|
<div class="p-2 bg-primary/10 rounded-xl text-primary">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
</div>
|
|
@Loc["Included_in_this_Bundle"]
|
|
</h2>
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="text-sm font-bold text-gray-400 uppercase tracking-widest">@Loc["Consisting_of"]:</span>
|
|
<div class="badge badge-md badge-primary font-bold opacity-80 shadow-lg shadow-primary/10">
|
|
@(_product.ChildProducts.Count) @Loc["Items"]</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">
|
|
@foreach (var child in _product.ChildProducts)
|
|
{
|
|
<div
|
|
class="group flex flex-col bg-white border border-gray-100 rounded-3xl overflow-hidden shadow-sm hover:shadow-2xl hover:border-primary/20 transition-all duration-500 hover:-translate-y-2">
|
|
<div class="h-48 overflow-hidden bg-gray-50 relative">
|
|
@if (child.Photos?.Length > 0)
|
|
{
|
|
<img src="@child.Photos[0]"
|
|
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
|
|
alt="@(child.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)" />
|
|
}
|
|
else
|
|
{
|
|
<div class="w-full h-full flex items-center justify-center text-gray-200">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
|
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="p-6 flex flex-col gap-4 flex-1">
|
|
<div class="flex-1">
|
|
<h4
|
|
class="font-bold text-lg text-gray-900 group-hover:text-primary transition-colors truncate">
|
|
@(child.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)
|
|
</h4>
|
|
<p class="text-sm text-gray-500 line-clamp-2 leading-relaxed mt-2">
|
|
@(child.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)</p>
|
|
</div>
|
|
|
|
<a href="/product/@child.Id"
|
|
class="btn btn-primary btn-sm rounded-xl w-full gap-2 shadow-lg shadow-primary/10">
|
|
@Loc["View_Details"]
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public Guid Id { get; set; }
|
|
|
|
private ProductVM? _product;
|
|
private bool _loading = true;
|
|
private string _activeTab = "description";
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
_loading = true;
|
|
try
|
|
{
|
|
_product = await ProductService.GetProduct(Id.ToString());
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error fetching product: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
} |