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

281 lines
18 KiB
Plaintext

@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
<div class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/60 backdrop-blur-sm">
<div class="bg-white rounded-3xl border border-slate-200 shadow-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto relative p-6 sm:p-8 animate-in zoom-in-95 duration-200">
<!-- Close Button -->
<button @onclick="OnClose" class="absolute top-4 right-4 p-2 rounded-full hover:bg-slate-100 text-slate-400 hover:text-slate-600 transition-colors 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="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 mt-4">
<!-- Left Cover Art & Quick Stats (4 cols) -->
<div class="md:col-span-4 flex flex-col items-center md:items-stretch">
<div class="relative h-72 w-52 md:h-84 md:w-full rounded-2xl overflow-hidden shadow-xl border border-slate-200 mx-auto bg-slate-50">
@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-6 flex flex-col justify-between text-white">
<div class="absolute top-0 right-0 h-24 w-24 bg-white/5 rounded-full blur-md pointer-events-none" />
<span class="text-xs font-mono font-bold tracking-widest block uppercase text-white/40">@Book.Category</span>
<span class="text-xl font-serif font-extrabold line-clamp-4 leading-tight text-white">@Book.Title</span>
<span class="text-xs font-sans text-white/70 truncate block font-semibold">By @Book.Author</span>
</div>
}
<div class="absolute top-0 bottom-0 left-0 w-3 bg-gradient-to-r from-black/20 to-transparent" />
</div>
<!-- Quick Stats Grid -->
<div class="grid grid-cols-2 gap-3 mt-6 w-full max-w-xs md:max-w-none">
<div class="p-3 rounded-xl border border-slate-100 bg-white shadow-sm flex flex-col items-center justify-center text-center">
<svg class="h-4 w-4 text-slate-600 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span class="text-[10px] text-slate-400 font-mono">PUBLISHED</span>
<span class="text-xs font-semibold text-slate-800 mt-0.5">@Book.PublishedYear</span>
</div>
<div class="p-3 rounded-xl border border-slate-100 bg-white shadow-sm flex flex-col items-center justify-center text-center">
<svg class="h-4 w-4 text-slate-600 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
<span class="text-[10px] text-slate-400 font-mono">PAGES</span>
<span class="text-xs font-semibold text-slate-800 mt-0.5">@(Book.PagesCount?.ToString() ?? "N/A")</span>
</div>
<div class="p-3 rounded-xl border border-slate-100 bg-white shadow-sm flex flex-col items-center justify-center text-center">
<svg class="h-4 w-4 text-slate-600 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="text-[10px] text-slate-400 font-mono">AUDIO TIME</span>
<span class="text-xs font-semibold text-slate-800 mt-0.5 truncate w-full">@(Book.AudioDuration ?? "N/A")</span>
</div>
<div class="p-3 rounded-xl border border-slate-100 bg-white shadow-sm flex flex-col items-center justify-center text-center">
<svg class="h-4 w-4 text-slate-600 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-[10px] text-slate-400 font-mono">NARRATOR</span>
<span class="text-[11px] font-semibold text-slate-800 mt-0.5 truncate w-full" title="@Book.Narrator">
@(Book.Narrator ?? "None")
</span>
</div>
</div>
</div>
<!-- Right Details Section (8 cols) -->
<div class="md:col-span-8 flex flex-col h-full">
<div class="mb-4">
<span class="text-[10px] font-mono tracking-wider font-semibold px-2.5 py-1 rounded bg-slate-100 text-slate-600 border border-slate-200">
@Book.Category.ToUpper()
</span>
</div>
<h2 class="font-serif text-2xl sm:text-3xl font-bold text-slate-800 leading-tight mb-2">
@Book.Title
</h2>
<p class="text-sm text-slate-500 mb-4">
By <span class="font-semibold text-slate-700">@Book.Author</span>
</p>
<div class="flex items-center gap-3 border-b border-slate-100 pb-4 mb-4">
<div class="flex items-center text-amber-400">
@for (int i = 0; i < 5; i++)
{
<svg class="h-4 w-4 @(i < Math.Floor(Book.Rating) ? "fill-amber-400 text-amber-400" : "text-slate-200")" 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-700 font-semibold">@Book.Rating.ToString("F1") Rating</span>
<span class="text-slate-300">•</span>
<span class="text-xs text-slate-500">(@Book.ReviewsCount customer reviews)</span>
</div>
<!-- Description/Overview -->
<div class="flex-1">
<h4 class="text-xs font-mono text-slate-400 tracking-wider uppercase mb-2 font-bold">Overview</h4>
<p class="text-sm text-slate-600 leading-relaxed mb-6 font-light">
@Book.Description
</p>
</div>
<!-- Interactive Rating Section -->
<div class="p-4 rounded-2xl border border-dashed border-slate-200 bg-slate-50/50 mb-6 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="text-center sm:text-left">
<h5 class="text-xs font-mono font-bold text-slate-600 uppercase tracking-wider">Have you read or listened to this?</h5>
<p class="text-[11px] text-slate-400 mt-0.5">
@if (UserRating.HasValue)
{
<span>You rated this title @UserRating.Value star@(UserRating.Value > 1 ? "s" : ""). Thank you!</span>
}
else
{
<span>Tap a star to rate this title instantly.</span>
}
</p>
</div>
<div class="flex items-center gap-1.5 bg-white px-3.5 py-1.5 rounded-full shadow-sm border border-slate-100">
@for (int starValue = 1; starValue <= 5; starValue++)
{
var currentStar = starValue;
bool isHovered = hoverRating.HasValue && currentStar <= hoverRating.Value;
bool isSelected = UserRating.HasValue && currentStar <= UserRating.Value;
bool isActive = hoverRating.HasValue ? isHovered : isSelected;
<button
type="button"
@onmouseenter="() => hoverRating = currentStar"
@onmouseleave="() => hoverRating = null"
@onclick="() => RateBook(currentStar)"
class="p-1 text-slate-200 hover:scale-125 hover:text-amber-400 active:scale-95 transition-all cursor-pointer focus:outline-none"
aria-label="Rate @currentStar stars"
>
<svg class="h-5 w-5 transition-colors @(isActive ? "fill-amber-400 text-amber-400" : "text-slate-300")" 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>
</button>
}
@if (UserRating.HasValue)
{
<span class="text-xs font-mono font-bold text-amber-600 bg-amber-50 px-2 py-0.5 rounded ml-1">
@UserRating.Value/5
</span>
}
</div>
</div>
<!-- Purchase / Format Rail -->
<div class="space-y-4">
<h4 class="text-xs font-mono text-slate-400 tracking-wider uppercase font-bold">Purchase &amp; Access Formats</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<!-- Ebook Card -->
<div class="p-4 rounded-2xl border border-slate-200 bg-white shadow-sm hover:border-slate-300 transition-all flex flex-col justify-between min-h-[140px]">
<div>
<span class="flex items-center gap-1 text-xs font-bold text-indigo-600 font-mono tracking-wide">
<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="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>
PROPRIETARY EBOOK
</span>
<p class="text-[11px] text-slate-400 mt-1">Read on any device in our premium reader. Offline support.</p>
</div>
<div class="mt-4 flex items-center justify-between gap-2">
@if (IsEbookOwned)
{
<div class="w-full space-y-2">
<div class="flex items-center justify-between">
<span class="text-[11px] text-emerald-600 flex items-center gap-1 font-semibold">
✓ Owned
</span>
@if (IsEbookCached)
{
<span class="text-[10px] text-indigo-600 flex items-center gap-0.5 font-mono font-semibold">
✓ Cached
</span>
}
</div>
<div class="flex gap-2">
<button @onclick="ReadEbook" class="flex-1 py-2 text-center text-xs font-bold rounded-xl bg-slate-900 hover:bg-slate-800 text-white shadow-sm transition-colors flex items-center justify-center gap-1 cursor-pointer">
Read Now
</button>
</div>
</div>
}
else
{
<span class="font-mono text-sm font-semibold text-slate-800">R @Book.Price.ToString("F2")</span>
<button @onclick='() => BuyBook("ebook")' disabled="@EbookInCart" class="px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer @(EbookInCart ? "bg-emerald-50 text-emerald-700 border border-emerald-200" : "bg-slate-900 hover:bg-slate-800 text-white shadow-sm")">
@(EbookInCart ? "In Cart" : "Buy Ebook")
</button>
}
</div>
</div>
<!-- Audiobook Card -->
<div class="p-4 rounded-2xl border border-slate-200 bg-white shadow-sm hover:border-slate-300 transition-all flex flex-col justify-between min-h-[140px]">
<div>
<span class="flex items-center gap-1 text-xs font-bold text-violet-600 font-mono tracking-wide">
<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="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
</svg>
STUDIO AUDIOBOOK
</span>
<p class="text-[11px] text-slate-400 mt-1">High fidelity streaming or offline cached audio player.</p>
</div>
<div class="mt-4 flex items-center justify-between gap-2">
@if (IsAudioOwned)
{
<button @onclick="PlayAudiobook" class="w-full py-2 text-center text-xs font-bold rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 shadow-sm transition-colors flex items-center justify-center gap-1.5 cursor-pointer">
Play Audiobook
</button>
}
else
{
<span class="font-mono text-sm font-semibold text-slate-800">R @((Book.Price * 1.25).ToString("F2"))</span>
<button @onclick='() => BuyBook("audiobook")' disabled="@AudioInCart" class="px-4 py-2 rounded-xl text-xs font-bold transition-all cursor-pointer @(AudioInCart ? "bg-emerald-50 text-emerald-700 border border-emerald-200" : "bg-slate-900 hover:bg-slate-800 text-white shadow-sm")">
@(AudioInCart ? "In Cart" : "Buy Audiobook")
</button>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@code {
[Parameter] public BookModel Book { get; set; } = new();
[Parameter] public bool IsEbookOwned { get; set; }
[Parameter] public bool IsEbookCached { get; set; }
[Parameter] public bool IsAudioOwned { get; set; }
[Parameter] public bool EbookInCart { get; set; }
[Parameter] public bool AudioInCart { get; set; }
[Parameter] public int? UserRating { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback<string> OnAddToCart { get; set; }
[Parameter] public EventCallback OnReadEbook { get; set; }
[Parameter] public EventCallback OnPlayAudiobook { get; set; }
[Parameter] public EventCallback<int> OnRateBook { get; set; }
private int? hoverRating = null;
private void BuyBook(string format)
{
OnAddToCart.InvokeAsync(format);
}
private void ReadEbook()
{
OnReadEbook.InvokeAsync();
}
private void PlayAudiobook()
{
OnPlayAudiobook.InvokeAsync();
}
private void RateBook(int rating)
{
OnRateBook.InvokeAsync(rating);
}
}