Files
2026-07-19 09:54:35 +02:00

241 lines
12 KiB
Plaintext

@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
<div class="relative w-full max-w-7xl mx-auto px-4 sm:px-6 my-8 md:my-12">
<!-- Decorative Label -->
<div class="flex items-center gap-2 mb-4">
<div class="flex h-6 w-6 items-center justify-center rounded-md bg-slate-100 text-slate-600">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<span class="font-display text-xs font-bold tracking-wider text-slate-500 uppercase">
Bestsellers &amp; Monthly Trending
</span>
<span class="h-[1.5px] flex-1 bg-slate-100" />
</div>
@if (FeaturedBooks != null && FeaturedBooks.Count > 0)
{
var book = FeaturedBooks[ActiveIndex];
var isEbookOwned = Purchases.Any(p => p.BookId == book.Id && p.Format == "ebook");
var isAudioowned = Purchases.Any(p => p.BookId == book.Id && p.Format == "audiobook");
var ebookInCart = Cart.Any(i => i.Book.Id == book.Id && i.Format == "ebook");
var audioInCart = Cart.Any(i => i.Book.Id == book.Id && i.Format == "audiobook");
<!-- Main Carousel Card -->
<div class="relative overflow-hidden rounded-3xl border border-slate-800/80 bg-slate-900/95 backdrop-blur-md shadow-lg shadow-slate-950/25 min-h-[480px] md:min-h-[380px] flex items-center">
<!-- Dynamic Background Glow -->
<div class="absolute inset-0 bg-gradient-to-br @book.CoverColor opacity-25 blur-2xl scale-125 transition-all duration-1000 -z-10" />
<div class="w-full px-6 py-8 md:p-12 flex flex-col md:flex-row items-center gap-8 md:gap-12 relative z-10">
<!-- Ebook Cover Wrapper -->
<div class="relative cursor-pointer group flex-shrink-0" @onclick="() => SelectBook(book)">
<!-- Floating Shadow Backdrop -->
<div class="absolute -inset-1 rounded-2xl bg-white opacity-10 group-hover:opacity-20 blur-lg transition-opacity" />
<!-- Book Card Graphic Cover -->
<div class="relative h-64 w-44 md:h-76 md:w-52 rounded-xl overflow-hidden shadow-2xl border border-white/10 transition-transform duration-500 group-hover:scale-105">
@if (!string.IsNullOrEmpty(book.CoverUrl))
{
<img src="@book.CoverUrl" alt="@book.Title" referrerpolicy="no-referrer" class="h-full w-full object-cover" />
}
else
{
<div class="h-full w-full bg-gradient-to-br @book.CoverColor p-5 flex flex-col justify-between text-white">
<span class="text-[10px] font-mono font-bold tracking-widest uppercase text-white/40">@book.Category</span>
<span class="text-lg font-serif font-extrabold line-clamp-3 leading-tight">@book.Title</span>
<span class="text-xs font-sans text-white/70 block">By @book.Author</span>
</div>
}
<!-- Subtle spine shadow representing real bound book -->
<div class="absolute top-0 bottom-0 left-0 w-2.5 bg-gradient-to-r from-black/40 via-black/10 to-transparent" />
<!-- Category Overlay tag -->
<div class="absolute top-3 right-3 bg-slate-950/80 backdrop-blur-md border border-white/10 text-white font-mono text-[9px] uppercase tracking-wider px-2 py-0.5 rounded-full">
@book.Category
</div>
</div>
<!-- Bestseller Banner Ribbon -->
@if (book.Bestseller)
{
<div class="absolute -top-3 -left-3 bg-amber-400 text-slate-950 text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-lg shadow-lg flex items-center gap-1">
<svg class="h-3 w-3 fill-slate-950 text-slate-950" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
</svg>
Bestseller
</div>
}
</div>
<!-- Book Description details -->
<div class="flex-1 text-center md:text-left">
<div class="flex items-center justify-center md:justify-start gap-2 mb-2">
<!-- Rating Stars -->
<div class="flex items-center text-amber-400">
@for (int i = 0; i < 5; i++)
{
<svg class="h-3.5 w-3.5 @(i < Math.Floor(book.Rating) ? "text-amber-400 fill-amber-400" : "text-slate-600")" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z" />
</svg>
}
</div>
<span class="text-xs text-slate-400">(@book.ReviewsCount reviews)</span>
</div>
<h1 class="font-serif text-3xl md:text-4xl lg:text-5xl font-bold tracking-tight text-white mb-3 leading-tight">
@book.Title
</h1>
<p class="text-sm text-slate-300 mb-4">
By <span class="font-semibold text-white">@book.Author</span> • Published @book.PublishedYear
</p>
<p class="text-xs md:text-sm text-slate-200/95 leading-relaxed max-w-xl mb-6 line-clamp-3 md:line-clamp-4 font-light">
@book.Description
</p>
<!-- Actions Section -->
<div class="flex flex-wrap items-center justify-center md:justify-start gap-3">
@if (isEbookOwned)
{
<button @onclick="() => ReadEbook(book)" class="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-white hover:bg-slate-100 text-slate-900 font-bold text-xs shadow-md transition-all cursor-pointer">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
Read Ebook Now
</button>
}
else
{
<button @onclick='() => AddToCart(book, "ebook")' disabled="@ebookInCart" class="flex items-center gap-1.5 px-4 py-2.5 rounded-xl border border-white/20 bg-white/10 hover:bg-white/20 text-white font-medium text-xs transition-all cursor-pointer disabled:opacity-50">
Buy Ebook (R @book.Price.ToString("F2"))
</button>
}
@if (isAudioowned)
{
<button @onclick="() => StreamAudiobook(book)" class="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-bold text-xs shadow-md transition-all cursor-pointer">
<svg class="h-4 w-4 fill-slate-950" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
Listen Audiobook
</button>
}
else
{
<button @onclick='() => AddToCart(book, "audiobook")' disabled="@audioInCart" class="flex items-center gap-1.5 px-4 py-2.5 rounded-xl border border-white/20 bg-white/10 hover:bg-white/20 text-white font-medium text-xs transition-all cursor-pointer disabled:opacity-50">
Buy Audio (R @((book.Price * 1.25).ToString("F2")))
</button>
}
<button @onclick="() => SelectBook(book)" class="px-4 py-2.5 rounded-xl bg-slate-800/60 hover:bg-slate-800 text-slate-200 border border-slate-700/50 font-medium text-xs transition-colors cursor-pointer">
View Details
</button>
</div>
</div>
</div>
<!-- Carousel Navigation Arrows -->
<button @onclick="HandlePrev" class="absolute left-3 md:left-4 p-2 rounded-full border border-slate-700 bg-slate-900/60 hover:bg-slate-900 text-white hover:scale-105 transition-all z-20 backdrop-blur-sm cursor-pointer">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button @onclick="HandleNext" class="absolute right-3 md:right-4 p-2 rounded-full border border-slate-700 bg-slate-900/60 hover:bg-slate-900 text-white hover:scale-105 transition-all z-20 backdrop-blur-sm cursor-pointer">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
<!-- Bottom indicators dots -->
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-1.5 z-20">
@for (int idx = 0; idx < FeaturedBooks.Count; idx++)
{
var i = idx;
<button @onclick="() => SetActive(i)" class="h-1.5 rounded-full transition-all duration-300 cursor-pointer @(i == ActiveIndex ? "w-5 bg-white" : "w-1.5 bg-white/20")" />
}
</div>
</div>
}
</div>
@code {
[Parameter] public List<BookModel> FeaturedBooks { get; set; } = new();
[Parameter] public List<PurchaseRecord> Purchases { get; set; } = new();
[Parameter] public List<CartItem> Cart { get; set; } = new();
[Parameter] public EventCallback<BookModel> OnSelectBook { get; set; }
[Parameter] public EventCallback<BookModel> OnReadEbook { get; set; }
[Parameter] public EventCallback<BookModel> OnStreamAudiobook { get; set; }
[Parameter] public EventCallback<(BookModel Book, string Format)> OnAddToCart { get; set; }
private int ActiveIndex { get; set; } = 0;
private System.Threading.Timer? AutoRotationTimer { get; set; }
protected override void OnInitialized()
{
AutoRotationTimer = new System.Threading.Timer(_ =>
{
InvokeAsync(() =>
{
if (FeaturedBooks.Count > 0)
{
ActiveIndex = (ActiveIndex + 1) % FeaturedBooks.Count;
StateHasChanged();
}
});
}, null, 8000, 8000);
}
private void HandleNext()
{
ActiveIndex = (ActiveIndex + 1) % FeaturedBooks.Count;
}
private void HandlePrev()
{
ActiveIndex = (ActiveIndex - 1 + FeaturedBooks.Count) % FeaturedBooks.Count;
}
private void SetActive(int index)
{
ActiveIndex = index;
}
private void SelectBook(BookModel book)
{
OnSelectBook.InvokeAsync(book);
}
private void ReadEbook(BookModel book)
{
OnReadEbook.InvokeAsync(book);
}
private void StreamAudiobook(BookModel book)
{
OnStreamAudiobook.InvokeAsync(book);
}
private void AddToCart(BookModel book, string format)
{
OnAddToCart.InvokeAsync((book, format));
}
public void Dispose()
{
AutoRotationTimer?.Dispose();
}
}