Initial commit - ECommerce.RCL
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
|
||||
|
||||
<div class="flex items-end overflow-hidden bg-cover rounded-lg h-96"
|
||||
style="background-image:url('@(Bundle.Photos?.Any() == true ? Bundle.Photos[0] : "https://via.placeholder.com/400x300?text=No+Image")')">
|
||||
<div class="w-full px-8 py-4 overflow-hidden rounded-b-lg backdrop-blur-sm bg-white/60 dark:bg-gray-800/60">
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-800 capitalize dark:text-white">@Bundle.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h2>
|
||||
<p class="mt-2 text-lg tracking-wider text-blue-500 uppercase dark:text-blue-400 ">@Bundle.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)</p>
|
||||
</div>
|
||||
</div>
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public ProductVM Bundle { get; set; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@inject NavigationManager nav
|
||||
@using ECommerceModule.Services
|
||||
@inject ShopStateService ShopStateService
|
||||
|
||||
<button @onclick="NavTo"
|
||||
class="relative grid h-[35rem] max-w-lg flex-col items-end justify-center overflow-hidden rounded-lg bg-white">
|
||||
<div style="background-image: url('@Category.Photos[0]');"
|
||||
class="absolute inset-0 m-0 h-full w-full overflow-hidden rounded-none bg-transparent bg-cover bg-center">
|
||||
<div class="to-bg-black-10 absolute inset-0 h-full w-full bg-gradient-to-t from-black/80 via-black/50"></div>
|
||||
</div>
|
||||
<div class="relative text-center p-6 px-6 py-14 md:px-12">
|
||||
<h2 class="mb-6 text-3xl font-medium text-white">
|
||||
@Category.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public CategoryVM Category { get; set; }
|
||||
|
||||
async Task NavTo()
|
||||
{
|
||||
await ShopStateService.SetCategoryId(Category.Id);
|
||||
if (Category.IsBundle)
|
||||
nav.NavigateTo("/bundles");
|
||||
else
|
||||
nav.NavigateTo("/products");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@*
|
||||
|
||||
|
||||
<div class="flex items-end overflow-hidden bg-cover rounded-lg h-96"
|
||||
style="background-image:url('@Category.PhotoPath')">
|
||||
<div class="w-full px-8 py-4 overflow-hidden rounded-b-lg backdrop-blur-sm bg-white/60 dark:bg-gray-800/60">
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-800 capitalize dark:text-white">@Category.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h2>
|
||||
<p class="mt-2 text-lg tracking-wider text-blue-500 uppercase dark:text-blue-400 ">@Category.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)</p>
|
||||
</div>
|
||||
</div>
|
||||
@code{
|
||||
|
||||
[Parameter]
|
||||
public Category Category {get; set;}
|
||||
|
||||
|
||||
href="@(Category.IsBundle ? $"bundles?category={Category.Id}&pagesize=12¤tpage=1" :
|
||||
$"products?category={Category.Id}&pagesize=12¤tpage=1")"
|
||||
|
||||
} *@
|
||||
@@ -0,0 +1,41 @@
|
||||
<div class="w-80 h-20">
|
||||
<div class="flex flex-col md:flex-row flex-wrap md:flex-nowrap bg-white shadow-md rounded-2xl p-2">
|
||||
<img src="@Category.Photos[0]" alt="Just a flower" class=" w-20 object-cover mx-auto h-20 rounded-xl">
|
||||
<div class="flex flex-col justify-center w-full px-2 py-1">
|
||||
<div class="flex justify-between items-center ">
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-xs md:text-sm font-medium">@Category.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h2>
|
||||
</div>
|
||||
@* <svg xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-0 w-0 md:w-5 md:h-5 @favoriteClass cursor-pointer" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
|
||||
</svg> *@
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row mx-auto md:mx-0 gap-2 md text-sm text-gray-400">
|
||||
<div class="flex items-center mr-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-yellow-500 mr-1" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path
|
||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z">
|
||||
</path>
|
||||
</svg>
|
||||
<p class="font-normal text-sm md:text-md">@Loc["4.5"]</p>
|
||||
</div>
|
||||
@* <div class="flex items-center mx-auto md:mx-0 font-medium text-gray-900 ">
|
||||
$@ProductModel.Price
|
||||
</div> *@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public CategoryVM Category { get; set; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
@* @inject IStringLocalizer<App> Loc *@
|
||||
|
||||
|
||||
<div class="flex items-end overflow-hidden bg-cover rounded-lg h-96"
|
||||
style="background-image:url('@Category.Photos[0]')">
|
||||
<div class="w-full px-8 py-4 overflow-hidden rounded-b-lg backdrop-blur-sm bg-white/60 dark:bg-gray-800/60">
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-800 capitalize dark:text-white">@Category.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h2>
|
||||
<p class="mt-2 text-lg tracking-wider text-blue-500 uppercase dark:text-blue-400 ">@Loc["Ui_kit"]</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public CategoryVM Category { get; set; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@using ECommerceModule.Services
|
||||
@inject ShopStateService ShopStateService
|
||||
|
||||
<div class="">
|
||||
<div class="flex flex-col md:flex-row flex-wrap md:flex-nowrap bg-white shadow-md rounded-2xl p-2">
|
||||
<img
|
||||
src="@(ProductModel.Photos?.Any() == true ? ProductModel.Photos[0] : "https://via.placeholder.com/400x300?text=No+Image")"
|
||||
alt="@ProductModel.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)" class="w-20 object-cover mx-auto h-20 rounded-xl">
|
||||
<div class="flex flex-col justify-center w-full px-2 py-1">
|
||||
<div class="flex justify-between items-center ">
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-xs md:text-sm font-medium">@ProductModel.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h2>
|
||||
</div>
|
||||
<svg @onclick="ToggleFavorite" xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-0 w-0 md:w-5 md:h-5 @favoriteClass cursor-pointer" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row mx-auto md:mx-0 gap-2 md text-sm text-gray-400">
|
||||
<div class="flex items-center mr-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-yellow-500 mr-1" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path
|
||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z">
|
||||
</path>
|
||||
</svg>
|
||||
<p class="font-normal text-sm md:text-md">@Loc["4.5"]</p>
|
||||
</div>
|
||||
@* <div class="flex items-center mx-auto md:mx-0 font-medium text-gray-900 ">
|
||||
$@ProductModel.Price
|
||||
</div> *@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
|
||||
[Parameter]
|
||||
public ProductVM ProductModel { get; set; }
|
||||
string favoriteClass = "text-white";
|
||||
|
||||
protected async override Task OnInitializedAsync()
|
||||
{
|
||||
await ShopStateService.InitializeAsync();
|
||||
UpdateFavoriteState();
|
||||
ShopStateService.OnChange += StateChanged;
|
||||
}
|
||||
|
||||
void StateChanged()
|
||||
{
|
||||
UpdateFavoriteState();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
void UpdateFavoriteState()
|
||||
{
|
||||
if (ShopStateService.IsFavorite(ProductModel.Id.ToString()))
|
||||
favoriteClass = "text-pink-500 fill-current";
|
||||
else
|
||||
favoriteClass = "text-white";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ShopStateService.OnChange -= StateChanged;
|
||||
}
|
||||
|
||||
async Task ToggleFavorite()
|
||||
{
|
||||
await ShopStateService.ToggleFavorite(ProductModel.Id.ToString());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<div
|
||||
class="group flex flex-col h-full bg-white border border-gray-200 shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-700 dark:shadow-neutral-700/70">
|
||||
<div class="h-52 flex flex-col justify-center items-center bg-amber-500 rounded-t-xl">
|
||||
<svg class="size-28" width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="56" height="56" rx="10" fill="white" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M23.7326 11.968C21.9637 11.9693 20.5321 13.4049 20.5334 15.1738C20.5321 16.9427 21.965 18.3782 23.7339 18.3795H26.9345V15.1751C26.9358 13.4062 25.5029 11.9706 23.7326 11.968C23.7339 11.968 23.7339 11.968 23.7326 11.968M23.7326 20.5184H15.2005C13.4316 20.5197 11.9987 21.9553 12 23.7242C11.9974 25.4931 13.4303 26.9286 15.1992 26.9312H23.7326C25.5016 26.9299 26.9345 25.4944 26.9332 23.7255C26.9345 21.9553 25.5016 20.5197 23.7326 20.5184V20.5184Z"
|
||||
fill="#36C5F0" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M44.0001 23.7242C44.0014 21.9553 42.5684 20.5197 40.7995 20.5184C39.0306 20.5197 37.5977 21.9553 37.599 23.7242V26.9312H40.7995C42.5684 26.9299 44.0014 25.4944 44.0001 23.7242ZM35.4666 23.7242V15.1738C35.4679 13.4062 34.0363 11.9706 32.2674 11.968C30.4985 11.9693 29.0656 13.4049 29.0669 15.1738V23.7242C29.0643 25.4931 30.4972 26.9286 32.2661 26.9312C34.035 26.9299 35.4679 25.4944 35.4666 23.7242Z"
|
||||
fill="#2EB67D" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M32.2661 44.0322C34.035 44.0309 35.4679 42.5953 35.4666 40.8264C35.4679 39.0575 34.035 37.622 32.2661 37.6207H29.0656V40.8264C29.0642 42.594 30.4972 44.0295 32.2661 44.0322ZM32.2661 35.4804H40.7995C42.5684 35.4791 44.0013 34.0436 44 32.2747C44.0026 30.5058 42.5697 29.0702 40.8008 29.0676H32.2674C30.4985 29.0689 29.0656 30.5045 29.0669 32.2734C29.0656 34.0436 30.4972 35.4791 32.2661 35.4804V35.4804Z"
|
||||
fill="#ECB22E" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M12 32.2746C11.9987 34.0435 13.4316 35.479 15.2005 35.4804C16.9694 35.479 18.4024 34.0435 18.401 32.2746V29.0688H15.2005C13.4316 29.0702 11.9987 30.5057 12 32.2746ZM20.5334 32.2746V40.825C20.5308 42.5939 21.9637 44.0295 23.7326 44.0321C25.5016 44.0308 26.9345 42.5952 26.9332 40.8263V32.2772C26.9358 30.5083 25.5029 29.0728 23.7339 29.0702C21.9637 29.0702 20.5321 30.5057 20.5334 32.2746C20.5334 32.2759 20.5334 32.2746 20.5334 32.2746Z"
|
||||
fill="#E01E5A" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="p-4 md:p-6">
|
||||
<span class="block mb-1 text-xs font-semibold uppercase text-amber-500">@Loc["Slack_API"]</span>
|
||||
<h3 class="text-xl font-semibold text-gray-800 dark:text-neutral-300 dark:hover:text-white">@Loc["Slack"]</h3>
|
||||
<p class="mt-3 text-gray-500 dark:text-neutral-500">@Loc["Email_collaboration_and"]</p>
|
||||
</div>
|
||||
<div
|
||||
class="mt-auto flex border-t border-gray-200 divide-x divide-gray-200 dark:border-neutral-700 dark:divide-neutral-700">
|
||||
<a class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-es-xl bg-white text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800 dark:focus:bg-neutral-800"
|
||||
href="#">@Loc["View_sample"]</a>
|
||||
<a class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-ee-xl bg-white text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-800 dark:focus:bg-neutral-800"
|
||||
href="#">@Loc["View_API"]</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public ProductVM Product { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
@if (Product == null) return;
|
||||
<div
|
||||
class="group relative flex flex-col h-full bg-white border @(Product.IsBundle ? "border-primary/40 shadow-xl shadow-primary/5 scale-[1.02]" : "border-gray-200") shadow-sm rounded-xl dark:bg-neutral-900 dark:border-neutral-700 dark:shadow-neutral-700/70 transition-all duration-300 hover:shadow-2xl">
|
||||
|
||||
<div class="relative flex items-end overflow-hidden bg-cover rounded-t-xl h-52"
|
||||
style="background-image:url('@(Product.Photos?.Any() == true ? Product.Photos[0] : "https://via.placeholder.com/400x300?text=No+Image")')">
|
||||
|
||||
@if (Product.IsBundle)
|
||||
{
|
||||
<div
|
||||
class="absolute top-3 right-3 px-3 py-1.5 rounded-xl backdrop-blur-md bg-primary/20 border border-primary/30 shadow-lg animate-pulse">
|
||||
<span class="text-[10px] font-black tracking-widest text-primary uppercase flex items-center gap-1.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
Bundle
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute bottom-3 left-3 px-2 py-1 rounded-lg backdrop-blur-md bg-black/40 border border-white/10">
|
||||
<span class="text-[9px] font-bold text-white/90 uppercase tracking-tighter">Value Pack</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="p-4 md:p-6">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-[10px] font-black uppercase text-primary/60 tracking-wider">
|
||||
@Product.CategoryName
|
||||
</span>
|
||||
@if (Product.IsBundle && Product.ChildProducts?.Count > 0)
|
||||
{
|
||||
<div class="badge badge-ghost badge-sm font-bold opacity-50">@(Product.ChildProducts.Count) Items</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-neutral-300 group-hover:text-primary transition-colors">
|
||||
@Product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)
|
||||
</h3>
|
||||
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-neutral-500 line-clamp-2 leading-relaxed">
|
||||
@Product.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)
|
||||
</p>
|
||||
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<span class="text-2xl font-black text-gray-900 dark:text-white">@Product.Price?.ToString("C")</span>
|
||||
@if (Product.Discount > 0)
|
||||
{
|
||||
<span class="text-xs font-bold text-error line-through opacity-50">@((Product.Price * (1 + Product.Discount /
|
||||
100.0))?.ToString("C"))</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-auto flex border-t border-gray-200 divide-x divide-gray-200 dark:border-neutral-700 dark:divide-neutral-700 bg-gray-50/50 dark:bg-neutral-800/50 rounded-b-xl">
|
||||
<a class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-bold bg-transparent text-gray-800 hover:bg-white dark:text-white dark:hover:bg-neutral-800 transition-all rounded-es-xl"
|
||||
href="/product/@Product.Id">
|
||||
@Loc["View"]
|
||||
</a>
|
||||
<a class="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-bold bg-transparent text-gray-800 hover:bg-white dark:text-white dark:hover:bg-neutral-800 transition-all rounded-ee-xl"
|
||||
href="#">
|
||||
@Loc["Contact_Us"]
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public ProductVM? Product { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
|
||||
<div
|
||||
class="group relative flex flex-col h-full bg-base-100 border border-base-300 shadow-lg rounded-2xl overflow-hidden hover:shadow-2xl transition-all duration-500 transform hover:-translate-y-1">
|
||||
|
||||
@* Image Section with Hover Zoom *@
|
||||
<div class="relative h-64 overflow-hidden bg-base-200">
|
||||
@if (Product.Photos != null && Product.Photos.Any())
|
||||
{
|
||||
<img src="@Product.Photos[0]"
|
||||
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
alt="@Product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex items-center justify-center h-full bg-base-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 opacity-20" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
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>
|
||||
}
|
||||
|
||||
@* Badges Layer (Glassmorphism) *@
|
||||
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
||||
@if (Product.IsBundle)
|
||||
{
|
||||
<span class="badge badge-accent font-bold py-3 glass px-3 shadow-sm border-none uppercase text-[10px]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
@Loc["Bundle"]
|
||||
</span>
|
||||
}
|
||||
@if (Product.Discount > 0)
|
||||
{
|
||||
<span class="badge badge-error font-bold py-3 glass px-3 shadow-sm border-none uppercase text-[10px]">
|
||||
@Loc["Sale"]
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Content Section *@
|
||||
<div class="p-5 flex flex-col flex-grow">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-primary opacity-70">
|
||||
@Product.CategoryName
|
||||
</span>
|
||||
<span class="text-[10px] font-medium text-base-content opacity-40">
|
||||
#@Product.CodeName
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl font-bold text-base-content mb-2 line-clamp-1 group-hover:text-primary transition-colors">
|
||||
@Product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-base-content opacity-60 line-clamp-2 mb-4">
|
||||
@Product.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)
|
||||
</p>
|
||||
|
||||
@* Price Section *@
|
||||
<div class="mt-auto pt-4 flex items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
@if (Product.Discount > 0)
|
||||
{
|
||||
<span class="text-xs line-through opacity-40 font-medium">$@Product.Price</span>
|
||||
<span class="text-2xl font-black text-primary">$@GetDiscountedPrice()</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-2xl font-black text-base-content">$@Product.Price</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<a href="/product/@Product.Id"
|
||||
class="btn btn-primary btn-circle btn-sm shadow-md transition-all duration-300 transform group-hover:scale-125">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
|
||||
|
||||
@code {
|
||||
[Parameter] public ProductVM Product { get; set; } = new();
|
||||
|
||||
private double GetDiscountedPrice()
|
||||
{
|
||||
if (Product.Price == null) return 0;
|
||||
var discount = Product.Discount ?? 0;
|
||||
return Math.Round(Product.Price.Value * (1 - discount / 100.0), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
@page "/shop"
|
||||
@inject ProductAPIConsumer productService
|
||||
@inject CategoryAPIConsumer categoryService
|
||||
@inject NavigationManager nav
|
||||
@using Common
|
||||
|
||||
|
||||
<div class="flex flex-col gap-8 pb-20">
|
||||
<!-- Hero Section -->
|
||||
<div class="relative w-full h-80 rounded-2xl overflow-hidden shadow-2xl">
|
||||
<div class="absolute inset-0 bg-cover bg-center"
|
||||
style="background-image: url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp);">
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-black/50 backdrop-blur-[2px]"></div>
|
||||
<div class="relative z-10 h-full flex flex-col justify-center items-center text-center text-white p-6">
|
||||
<h1 class="text-4xl md:text-6xl font-extrabold mb-4 tracking-tight">Discover Premium Quality</h1>
|
||||
<p class="text-lg md:text-xl text-gray-200 max-w-2xl">Explore our curated collection of top-tier products
|
||||
and exclusive bundles designed to elevate your lifestyle.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section -->
|
||||
<EditForm Model="productFilter" OnValidSubmit="() => OnSearchProduct(1)"
|
||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 border border-gray-100 dark:border-gray-700">
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6 items-end">
|
||||
<!-- Search -->
|
||||
<div class="lg:col-span-1">
|
||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Search</label>
|
||||
<div class="relative">
|
||||
<InputText @bind-Value="productFilter.Name" placeholder="Search products..."
|
||||
class="w-full pl-4 pr-10 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
|
||||
<button type="submit"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-gray-400 hover:text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div class="lg:col-span-1">
|
||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Category</label>
|
||||
<CategoryTreeSelect @bind-Value="productFilter.CategoryId" ShowBundles="false"
|
||||
Placeholder="All Categories" />
|
||||
</div>
|
||||
|
||||
<!-- Type -->
|
||||
<div class="lg:col-span-1">
|
||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Type</label>
|
||||
<InputSelect @bind-Value="productFilter.IsBundle"
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all dark:bg-gray-700 dark:border-gray-600 dark:text-white">
|
||||
<option value="">All Types</option>
|
||||
<option value="true">Bundles Only</option>
|
||||
<option value="false">Products Only</option>
|
||||
</InputSelect>
|
||||
</div>
|
||||
|
||||
<!-- Price -->
|
||||
<div class="lg:col-span-1">
|
||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Max Price</label>
|
||||
<InputNumber @bind-Value="productFilter.Price" placeholder="Max Price"
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
|
||||
</div>
|
||||
|
||||
<!-- Discount -->
|
||||
<div class="lg:col-span-1">
|
||||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Min Discount %</label>
|
||||
<InputNumber @bind-Value="productFilter.Discount" placeholder="Min Discount"
|
||||
class="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all dark:bg-gray-700 dark:border-gray-600 dark:text-white" />
|
||||
</div>
|
||||
|
||||
<!-- Action -->
|
||||
<div class="lg:col-span-1">
|
||||
<button type="submit"
|
||||
class="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg shadow-md transition-colors duration-200">
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full h-full gap-5 p-5 md:p-12 lg:p-20">
|
||||
@foreach (var product in products)
|
||||
{
|
||||
<ProductComp4 Product="product" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<PaginationComp OnSearch="OnSearchProduct" PaginationModel="pagination"></PaginationComp>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
public Pagination pagination { get; set; } = new() { CurrentPage = 1, PageSize = 15 };
|
||||
|
||||
public ProductFilter productFilter { get; set; } = new ProductFilter();
|
||||
|
||||
List<ProductVM> products = new List<ProductVM>();
|
||||
|
||||
|
||||
[Parameter, SupplyParameterFromQuery]
|
||||
public string? CategoryId { get; set; }
|
||||
|
||||
[Parameter, SupplyParameterFromQuery]
|
||||
public bool? IsBundle { get; set; }
|
||||
|
||||
protected async override Task OnParametersSetAsync()
|
||||
{
|
||||
bool changed = false;
|
||||
if (!string.IsNullOrEmpty(CategoryId) && Guid.TryParse(CategoryId, out var categoryGuid))
|
||||
{
|
||||
productFilter.CategoryId = categoryGuid;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (IsBundle.HasValue)
|
||||
{
|
||||
productFilter.IsBundle = IsBundle.Value;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
await OnSearchProduct();
|
||||
}
|
||||
}
|
||||
|
||||
protected async override Task OnInitializedAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(CategoryId))
|
||||
{
|
||||
await OnSearchProduct();
|
||||
}
|
||||
}
|
||||
|
||||
async Task OnSearchProduct(int pageNumber = 1)
|
||||
{
|
||||
pagination.CurrentPage = pageNumber;
|
||||
|
||||
var result = await productService.FetchProduct(productFilter, pagination);
|
||||
products = result.Item1;
|
||||
pagination.ItemsCount = result.Item2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
@page "/bundle/{productid}"
|
||||
@inject ProductAPIConsumer ProductService
|
||||
@inject NavigationManager nav
|
||||
|
||||
|
||||
<div class="text-gray-700 dark:text-gray-200 body-font overflow-hidden bg-white dark:bg-gray-900">
|
||||
<div class="container px-5 py-12 mx-auto">
|
||||
<div class="lg:w-4/5 mx-auto flex flex-wrap">
|
||||
<div class="lg:w-1/2 w-full lg:pr-10 lg:py-6 mb-6 lg:mb-0">
|
||||
<h2 class="text-sm title-font text-gray-500 tracking-widest uppercase">@Loc["Bundle_Deal"]</h2>
|
||||
<h1 class="text-gray-900 dark:text-white text-3xl title-font font-medium mb-1">@(Product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h1>
|
||||
@if (!string.IsNullOrEmpty(Product.CodeName))
|
||||
{
|
||||
<h2 class="text-xs text-gray-500 tracking-widest font-medium mb-4">MODEL: @Product.CodeName</h2>
|
||||
}
|
||||
|
||||
<p class="leading-relaxed mb-6">@(Product.Translations?.FirstOrDefault()?.Info?.Description ?? string.Empty)</p>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">@Loc["Included_in_Bundle"]:
|
||||
</h3>
|
||||
<ul class="space-y-2">
|
||||
@if (Product.ChildProducts != null)
|
||||
{
|
||||
@foreach (var item in Product.ChildProducts)
|
||||
{
|
||||
<li class="flex items-center text-gray-600 dark:text-gray-400">
|
||||
<svg class="w-4 h-4 mr-2 text-green-500" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
@(item.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div class="flex flex-col">
|
||||
@if (Product.Discount > 0 && Product.Price.HasValue)
|
||||
{
|
||||
<span class="title-font font-medium text-2xl text-red-600 dark:text-red-400">
|
||||
$@((Product.Price.Value - (Product.Price.Value * (Product.Discount.Value /
|
||||
100))).ToString("N2"))
|
||||
</span>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="text-sm text-gray-500 line-through">
|
||||
$@Product.Price.Value.ToString("N2")
|
||||
</span>
|
||||
<span class="text-xs font-bold text-white bg-red-500 px-2 py-1 rounded">
|
||||
-@Product.Discount.Value.ToString("0")%
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="title-font font-medium text-2xl text-gray-900 dark:text-white">
|
||||
$@(Product.Price.HasValue? Product.Price.Value.ToString("N2") : "N/A")
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
class="flex ml-auto text-white bg-indigo-500 border-0 py-2 px-6 focus:outline-none hover:bg-indigo-600 rounded">@Loc["Buy_Bundle"]</button>
|
||||
<button
|
||||
class="rounded-full w-10 h-10 bg-gray-200 dark:bg-gray-700 p-0 border-0 inline-flex items-center justify-center text-gray-500 ml-4">
|
||||
<svg fill="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img alt="@(Product.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)" class="lg:w-1/2 w-full lg:h-auto h-64 object-cover object-center rounded shadow-lg"
|
||||
src="@(Product.Photos != null && Product.Photos.Any() ? Product.Photos.First() : "https://dummyimage.com/400x400")">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public string ProductId { get; set; }
|
||||
|
||||
ProductVM Product = new();
|
||||
protected async override Task OnInitializedAsync()
|
||||
{
|
||||
Product = await ProductService.GetProduct(ProductId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
@page "/shop/categories"
|
||||
@inject CategoryAPIConsumer categoryService
|
||||
|
||||
|
||||
<div class="hero h-1/3"
|
||||
style="background-image: url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp);">
|
||||
<div class="hero-overlay bg-opacity-60"></div>
|
||||
<div class="hero-content text-white text-center pt-20">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-5xl font-bold ">@Loc["Our_Categories"]</h1>
|
||||
<p class="mb-5">@Loc["Provident_cupiditate_voluptatem"]</p>
|
||||
<button class="btn btn-primary">@Loc["Get_Started"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="max-w-7xl mx-auto p-10">
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="flex flex-col items-center justify-center py-20 gap-4">
|
||||
<span class="loading loading-infinity loading-lg text-primary"></span>
|
||||
<span class="text-sm font-bold opacity-50">Loading Categories...</span>
|
||||
</div>
|
||||
}
|
||||
else if (categories == null || !categories.Any())
|
||||
{
|
||||
<div class="flex flex-col items-center justify-center py-20 opacity-40 italic">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mb-4" 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>
|
||||
<span>No categories found.</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="overflow-x-auto pb-4 custom-scrollbar">
|
||||
<div class="inline-block min-w-max space-y-4">
|
||||
@foreach (var category in categories)
|
||||
{
|
||||
<CategoryTreeItem Category="category" IsAdmin="false" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
List<CategoryVM> categories = new List<CategoryVM>();
|
||||
bool _loading = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
categories = await categoryService.FetchCategoryTree();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
@page "/shop/category/{id}"
|
||||
|
||||
@inject ProductAPIConsumer productService
|
||||
@inject CategoryAPIConsumer categoryService
|
||||
@inject NavigationManager nav
|
||||
|
||||
<div class="hero h-1/3"
|
||||
style="background-image: url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp);">
|
||||
<div class="hero-overlay bg-opacity-60"></div>
|
||||
<div class="hero-content text-neutral-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-5xl font-bold">@Loc["Hello_there"]</h1>
|
||||
<p class="mb-5">@Loc["Provident_cupiditate_voluptatem"]</p>
|
||||
<button class="btn btn-primary">@Loc["Get_Started"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h2>Child Categories for @(category.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</h2>
|
||||
|
||||
<div class="p-6 md:p-14 lg:p-20 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">
|
||||
@foreach (var cat in categories)
|
||||
{
|
||||
<CategoryComp Category="cat"></CategoryComp>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public string Id { get; set; }
|
||||
|
||||
CategoryVM category = new();
|
||||
List<CategoryVM> categories = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Id))
|
||||
{
|
||||
category = await categoryService.GetCategory(Id);
|
||||
// Assuming GetCategory response doesn't include ChildCategories populated depth?
|
||||
// If ChildCategories exists on VM, use it.
|
||||
// checking CategoryVM: yes it has ChildCategories.
|
||||
if (category.ChildCategories != null)
|
||||
{
|
||||
categories = category.ChildCategories.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
@* @page "/productss"
|
||||
@inject ProductAPIConsumer productService
|
||||
@inject CategoryAPIConsumer categoryService
|
||||
|
||||
|
||||
<div class="flex flex-col gap-5 ">
|
||||
<div class="hero h-1/3"
|
||||
style="background-image: url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp);">
|
||||
<div class="hero-overlay bg-opacity-60"></div>
|
||||
<div class="hero-content text-white text-center py-20">
|
||||
<div class="max-w-md">
|
||||
<h1 class="mb-5 text-5xl font-bold ">@Loc["Hello_there"]</h1>
|
||||
<p class="mb-5">@Loc["Provident_cupiditate_voluptatem"]</p>
|
||||
<button class="btn btn-primary">@Loc["Get_Started"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<EditForm Model="productFilter" OnValidSubmit="() => OnSearchProduct(1)"
|
||||
class="max-w-4xl mx-auto bg-white rounded-lg shadow-lg p-6">
|
||||
<!-- Search Bar -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<InputText @bind-Value="productFilter.Name" type="text" placeholder="Search..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500">@Loc["Search"]</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="mt-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<!-- Category Filter -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">@Loc["Category"]</label>
|
||||
<InputSelect @bind-Value="productFilter.CategoryId"
|
||||
class="mt-1 block w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">@Loc["All_Categories"]</option>
|
||||
|
||||
@foreach (var item in categories.Where(x => x.IsBundle == false).ToList())
|
||||
{
|
||||
<option value="@item.Id">@(item.Translations?.FirstOrDefault()?.Info?.Name ?? string.Empty)</option>
|
||||
}
|
||||
|
||||
</InputSelect>
|
||||
</div>
|
||||
|
||||
<!-- Sort By Filter -->
|
||||
<!-- Rating Filter -->
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full h-full gap-5 p-5 md:p-12 lg:p-20">
|
||||
@foreach (var product in products)
|
||||
{
|
||||
<ProductComp4 Product="product" />
|
||||
}
|
||||
</div>
|
||||
|
||||
<PaginationComp OnSearch="OnSearchProduct" PaginationModel="pagination"></PaginationComp>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
|
||||
public Pagination pagination { get; set; } = new() { CurrentPage = 1, PageSize = 15 };
|
||||
|
||||
public ProductFilter productFilter { get; set; } = new ProductFilter();
|
||||
|
||||
|
||||
List<ProductVM> products = new List<ProductVM>();
|
||||
|
||||
List<CategoryVM> categories = new List<CategoryVM>();
|
||||
|
||||
|
||||
SortModel sortModel = new SortModel();
|
||||
|
||||
|
||||
|
||||
|
||||
protected async override Task OnInitializedAsync()
|
||||
{
|
||||
var categoryResult = await categoryService.FetchCategory(new CategoryFilter(), new Pagination
|
||||
{
|
||||
PageSize = 100,
|
||||
CurrentPage = 1
|
||||
});
|
||||
categories = categoryResult.Item1;
|
||||
|
||||
await OnSearchProduct();
|
||||
}
|
||||
|
||||
async Task OnSearchProduct(int pageNumber = 1)
|
||||
{
|
||||
pagination.CurrentPage = pageNumber;
|
||||
var result = await productService.FetchProduct(productFilter, pagination);
|
||||
products = result.Item1;
|
||||
pagination.ItemsCount = result.Item2;
|
||||
}
|
||||
|
||||
} *@
|
||||
Reference in New Issue
Block a user