Stable run with tailwind
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
@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 & 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/40 bg-slate-900 shadow-2xl 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user