223 lines
12 KiB
Plaintext
223 lines
12 KiB
Plaintext
@namespace MidrandBooks.Components
|
|
@using Microsoft.AspNetCore.Components
|
|
@using MidrandBooks.Models
|
|
|
|
<div id="library-view-razor" class="w-full max-w-7xl mx-auto px-4 sm:px-6 my-10 space-y-8 min-h-[500px]">
|
|
|
|
<!-- Header Section -->
|
|
<div class="flex flex-col sm:flex-row sm:items-end justify-between gap-4 border-b border-slate-100 pb-4">
|
|
<div>
|
|
<h2 class="font-serif text-2xl md:text-3xl font-bold text-slate-800 flex items-center gap-2">
|
|
<svg class="h-7 w-7 text-slate-700" 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>
|
|
My Secure Library
|
|
</h2>
|
|
<p class="text-xs text-slate-500 mt-1">
|
|
Access your licensed digital purchases. Sync progress instantly across all browsers.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Format Filter Tabs -->
|
|
<div class="flex bg-slate-100 border border-slate-200/60 p-1 rounded-xl self-start sm:self-auto">
|
|
@foreach (var format in Formats)
|
|
{
|
|
<button @onclick="() => FilterByFormat(format)"
|
|
class="px-4 py-1.5 rounded-lg text-xs font-semibold uppercase tracking-wider transition-all cursor-pointer @(ActiveFormat == format ? "bg-white text-slate-900 shadow-sm border border-slate-200/50" : "text-slate-500 hover:text-slate-800")">
|
|
@format
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grid Layout -->
|
|
@if (FilteredLibraryItems().Any())
|
|
{
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
@foreach (var item in FilteredLibraryItems())
|
|
{
|
|
var book = item.Book;
|
|
var isEbook = item.Format == "ebook";
|
|
var isCached = OfflineBooks.Contains(book.Id);
|
|
var isDownloading = DownloadingBookId == book.Id;
|
|
|
|
<div class="group relative flex gap-5 rounded-2xl p-4 border border-slate-100 bg-white/70 backdrop-blur-md shadow-sm hover:shadow-md transition-all">
|
|
|
|
<!-- Book Cover -->
|
|
<div @onclick="() => HandleSelectBook(book)" class="relative h-36 w-24 rounded-xl overflow-hidden shadow-sm bg-slate-100 flex-shrink-0 cursor-pointer">
|
|
@if (!string.IsNullOrEmpty(book.CoverUrl))
|
|
{
|
|
<img src="@book.CoverUrl" alt="@book.Title" class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
|
}
|
|
else
|
|
{
|
|
<div class="h-full w-full bg-gradient-to-br @book.CoverColor p-3 flex flex-col justify-between text-white transition-transform duration-500 group-hover:scale-105">
|
|
<span class="text-[6px] font-mono font-bold tracking-widest block uppercase text-white/40">@book.Category</span>
|
|
<span class="text-xs font-serif font-extrabold line-clamp-3 leading-tight text-white">@book.Title</span>
|
|
<span class="text-[8px] font-sans text-white/70 truncate block">By @book.Author</span>
|
|
</div>
|
|
}
|
|
<div class="absolute top-0 bottom-0 left-0 w-2 bg-gradient-to-r from-black/20 to-transparent" />
|
|
</div>
|
|
|
|
<!-- Details Node -->
|
|
<div class="flex-1 min-w-0 flex flex-col justify-between">
|
|
<div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="inline-block text-[8px] font-mono font-extrabold uppercase rounded px-1.5 py-0.5 @(isEbook ? "bg-indigo-50 text-indigo-700" : "bg-violet-50 text-violet-700")">
|
|
@item.Format
|
|
</span>
|
|
|
|
<!-- Offline Cache Trigger Button -->
|
|
@if (isEbook)
|
|
{
|
|
@if (isCached)
|
|
{
|
|
<span class="inline-flex items-center gap-0.5 text-[9px] font-semibold text-emerald-600 font-mono" title="Synced Offline Cached copy secure">
|
|
✓ Cached
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<button @onclick="() => TriggerCacheBook(book.Id)" disabled="@isDownloading" class="text-[9px] font-bold text-violet-600 hover:text-violet-800 flex items-center gap-0.5 cursor-pointer disabled:opacity-50">
|
|
@if (isDownloading)
|
|
{
|
|
<span>Caching...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>↓ Sync Offline</span>
|
|
}
|
|
</button>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
<h3 @onclick="() => HandleSelectBook(book)" class="font-serif text-sm font-bold text-slate-800 leading-tight mt-1.5 hover:text-violet-600 cursor-pointer truncate">
|
|
@book.Title
|
|
</h3>
|
|
<span class="text-[10px] text-slate-400 block mt-0.5">By @book.Author</span>
|
|
|
|
<!-- Progress Track Slider bar -->
|
|
<div class="mt-3.5 space-y-1">
|
|
<div class="flex justify-between text-[8px] font-mono font-bold text-slate-400">
|
|
<span>READING PROGRESS</span>
|
|
<span>@(isEbook ? $"Page {item.CurrentPage + 1}" : "In Progress")</span>
|
|
</div>
|
|
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
|
|
<div class="bg-violet-600 h-full rounded-full transition-all" style="width: @(isEbook ? $"{item.Progress}%" : "15%")" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Access Action Buttons -->
|
|
<div class="mt-4 flex gap-2">
|
|
@if (isEbook)
|
|
{
|
|
<button @onclick="() => HandleReadBook(book)" class="flex-1 py-1.5 text-center text-[10px] font-bold rounded-xl bg-slate-900 hover:bg-slate-800 text-white shadow-sm transition-colors cursor-pointer flex items-center justify-center gap-1">
|
|
Open Ebook
|
|
</button>
|
|
}
|
|
else
|
|
{
|
|
<button @onclick="() => HandlePlayBook(book)" class="flex-1 py-1.5 text-center text-[10px] font-bold rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 shadow-sm transition-colors cursor-pointer flex items-center justify-center gap-1">
|
|
Listen Audio
|
|
</button>
|
|
}
|
|
<button @onclick="() => HandleSelectBook(book)" class="p-1.5 rounded-xl border border-slate-200 hover:bg-slate-50 text-slate-500 hover:text-slate-700 cursor-pointer" title="View details">
|
|
<svg class="h-4.5 w-4.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="p-12 text-center border border-dashed border-slate-200 bg-slate-50/50 rounded-3xl space-y-2">
|
|
<svg class="mx-auto h-8 w-8 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
|
<h4 class="font-serif text-sm font-bold text-slate-700">Your secure library is empty</h4>
|
|
<p class="text-[10px] text-slate-500 font-sans max-w-sm mx-auto leading-relaxed">
|
|
Unlock local publishing titles, bestsellers, and studio audiobooks in our browse store. Purchases are instantly activated here.
|
|
</p>
|
|
</div>
|
|
}
|
|
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public List<PurchaseRecord> Purchases { get; set; } = new();
|
|
[Parameter] public List<BookModel> Books { get; set; } = new();
|
|
[Parameter] public List<string> OfflineBooks { get; set; } = new();
|
|
[Parameter] public bool IsOfflineMode { get; set; }
|
|
|
|
[Parameter] public EventCallback<BookModel> OnSelectBook { get; set; }
|
|
[Parameter] public EventCallback<BookModel> OnReadBook { get; set; }
|
|
[Parameter] public EventCallback<BookModel> OnPlayBook { get; set; }
|
|
[Parameter] public EventCallback<string> OnCacheBookOffline { get; set; }
|
|
|
|
private string ActiveFormat { get; set; } = "all";
|
|
private string[] Formats = new string[] { "all", "ebook", "audiobook" };
|
|
private string? DownloadingBookId { get; set; }
|
|
|
|
private void FilterByFormat(string format)
|
|
{
|
|
ActiveFormat = format;
|
|
}
|
|
|
|
private IEnumerable<LibraryItem> FilteredLibraryItems()
|
|
{
|
|
var items = new List<LibraryItem>();
|
|
foreach (var purchase in Purchases)
|
|
{
|
|
var matchedBook = Books.FirstOrDefault(b => b.Id == purchase.BookId);
|
|
if (matchedBook != null)
|
|
{
|
|
items.Add(new LibraryItem
|
|
{
|
|
Book = matchedBook,
|
|
Format = purchase.Format,
|
|
CurrentPage = purchase.CurrentPage,
|
|
Progress = purchase.Progress
|
|
});
|
|
}
|
|
}
|
|
|
|
if (ActiveFormat == "all") return items;
|
|
return items.Where(i => i.Format.Equals(ActiveFormat, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private async Task TriggerCacheBook(string bookId)
|
|
{
|
|
if (DownloadingBookId != null) return;
|
|
DownloadingBookId = bookId;
|
|
|
|
await Task.Delay(1500); // Simulate network latency
|
|
await OnCacheBookOffline.InvokeAsync(bookId);
|
|
|
|
DownloadingBookId = null;
|
|
}
|
|
|
|
private void HandleSelectBook(BookModel book)
|
|
{
|
|
OnSelectBook.InvokeAsync(book);
|
|
}
|
|
|
|
private void HandleReadBook(BookModel book)
|
|
{
|
|
OnReadBook.InvokeAsync(book);
|
|
}
|
|
|
|
private void HandlePlayBook(BookModel book)
|
|
{
|
|
OnPlayBook.InvokeAsync(book);
|
|
}
|
|
}
|