Yuyuqt
add: small fixes, UI designing
ec76476
Raw
History Blame Contribute Delete
39.8 kB
@page "/books"
@using LibraryManagement.Shared.Models
@using BlazorWebAssembly.Services
@using Microsoft.AspNetCore.Authorization
@inject LibraryApiClient ApiClient
@inject WishlistService WishlistService
@inject IJSRuntime JS
@inject NavigationManager Navigation
<div class="max-w-7xl mx-auto py-10 px-6 animate-fade">
@if (_viewMode == ViewMode.List)
{
<!-- Header -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-12 gap-6">
<div>
<h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Book Collection</h1>
<p class="text-muted text-lg">Discover your next favorite story among our curated selection. <span class="text-xs">(@FilteredBooks.Count() total)</span></p>
</div>
<div class="flex items-center gap-4 w-full md:w-auto">
<div class="relative w-full md:w-96">
<input type="text" @bind="_searchQuery" @bind:event="oninput"
placeholder="Search by title, author, or ISBN..."
class="w-full bg-card border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all shadow-sm text-main" />
</div>
<AuthorizeView Roles="Librarian">
<button @onclick="ShowCreate" class="bg-primary text-white px-6 py-3 rounded-xl font-bold hover:bg-primary-hover whitespace-nowrap">
Add Book
</button>
</AuthorizeView>
</div>
</div>
<!-- Quick Filters -->
<div class="flex flex-wrap items-center gap-3 mb-6">
<button @onclick='() => SetQuickFilter("All")'
class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "All" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
All Books
</button>
<button @onclick='() => SetQuickFilter("New Arrivals")'
class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "New Arrivals" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
New Arrivals
</button>
<button @onclick='() => SetQuickFilter("Trending")'
class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "Trending" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
Trending
</button>
<button @onclick='() => SetQuickFilter("Recommended")'
class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "Recommended" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
Recommended for You
</button>
</div>
<div class="mb-10 animate-fade">
<button @onclick="ToggleCategoryDropdown" class="group inline-flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.3em] text-muted hover:text-primary transition-colors">
<div class="w-6 h-6 rounded-lg bg-card border border-border flex items-center justify-center group-hover:bg-primary/5 transition-colors">
<svg class="w-3 transition-transform duration-300 @(_isCategoryDropdownOpen ? "rotate-90" : "")" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M9 5l7 7-7 7"></path></svg>
</div>
<span>Browse by Category</span>
@if (_selectedCategoryId != null)
{
<span class="ml-2 px-2 py-0.5 bg-primary text-white text-[9px] rounded-full">@SelectedCategoryName</span>
}
</button>
@if (_isCategoryDropdownOpen)
{
<div class="flex flex-wrap items-center gap-2 mt-6 animate-slide-down">
<button @onclick="() => FilterByCategory(null)"
class='inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-bold border transition-all @(_selectedCategoryId == null ? "bg-primary text-white border-primary shadow-sm" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
All
</button>
@foreach (var cat in _categories)
{
<button @onclick="() => FilterByCategory(cat.Id)"
class='inline-flex items-center px-4 py-1.5 rounded-full text-[11px] font-bold border transition-all @(_selectedCategoryId == cat.Id ? "bg-primary text-white border-primary shadow-sm" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
@cat.Name
</button>
}
</div>
}
</div>
<!-- Loading State -->
@if (_isLoading)
{
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
@for (int i = 0; i < 8; i++)
{
<div class="animate-pulse bg-card rounded-xl border border-border h-96"></div>
}
</div>
}
else if (!FilteredBooks.Any())
{
<div class="text-center py-20 bg-card rounded-2xl border border-dashed border-border">
<h3 class="text-xl font-bold text-main mb-2">No results found</h3>
<p class="text-muted">Try adjusting your search or filters.</p>
</div>
}
else
{
<!-- Books Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
@foreach (var book in PaginatedBooks)
{
<div class="group bg-card rounded-xl border border-border overflow-hidden hover:shadow-xl transition-all duration-300 flex flex-col h-full">
<!-- Cover -->
<div class="aspect-[2/3] bg-body relative overflow-hidden cursor-pointer" @onclick="() => ShowDetails(book.Id)">
@if (!string.IsNullOrEmpty(book.CoverUrl))
{
<img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
}
else
{
<div class="w-full h-full flex items-center justify-center text-muted opacity-20">
<span class="font-bold uppercase tracking-widest text-xs">No Cover</span>
</div>
}
@if (book.AvailableCopies <= 0)
{
<div class="absolute inset-0 bg-black/60 backdrop-blur-[2px] flex items-center justify-center">
<span class="bg-white text-black px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest">Out of Stock</span>
</div>
}
</div>
<!-- Content -->
<div class="p-5 flex-grow flex flex-col">
<h3 class="font-bold text-main line-clamp-1 mb-1 group-hover:text-primary transition-colors cursor-pointer" @onclick="() => ShowDetails(book.Id)">@book.Title</h3>
<p class="text-xs text-muted mb-3 font-medium">@book.Author</p>
<div class="flex items-center gap-2 mb-4 flex-wrap">
@foreach (var cat in book.Categories.Take(2))
{
<span class="text-[9px] font-bold uppercase tracking-wider text-primary bg-primary/10 px-2 py-0.5 rounded-full">@cat.Name</span>
}
</div>
<div class="mt-auto flex flex-col gap-4">
<div class="flex items-center justify-between">
<span class="text-[10px] font-bold text-muted bg-body px-2 py-1 rounded border border-border">@book.Status</span>
<span class="text-xs font-bold @(book.AvailableCopies > 0 ? "text-green-500" : "text-red-400")">
@book.AvailableCopies / @book.TotalCopies
</span>
</div>
<div class="flex items-center gap-2">
<button @onclick="() => ShowDetails(book.Id)" class="flex-1 bg-body text-main border border-border py-1.5 rounded-lg text-[11px] font-bold hover:bg-card">Details</button>
<AuthorizeView Roles="Member">
<div class="flex gap-1">
<button @onclick="() => ToggleFavourite(book)"
class='p-1.5 rounded-lg border transition-all @(IsFavourite(book.Id) ? "border-red-200 bg-red-500/10 text-red-500" : "border-border text-muted hover:bg-red-500/10 hover:text-red-500")'
title='@(IsFavourite(book.Id) ? "Remove from Favourites" : "Add to Favourites")'>
<svg class="w-4 h-4" fill='@(IsFavourite(book.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001z"></path>
</svg>
</button>
@if (book.AvailableCopies == 0)
{
<button @onclick="() => ToggleWishlist(book)"
class='p-1.5 rounded-lg border transition-all @(IsInWishlist(book.Id) ? "border-amber-200 bg-amber-500/10 text-amber-500" : "border-border text-muted hover:bg-amber-500/10 hover:text-amber-500")'
title='@(IsInWishlist(book.Id) ? "Remove from Wishlist" : "Add to Wishlist")'>
<svg class="w-4 h-4" fill='@(IsInWishlist(book.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
</svg>
</button>
}
</div>
</AuthorizeView>
<AuthorizeView Roles="Librarian">
<button @onclick="() => ShowEdit(book.Id)" class="p-1.5 rounded-lg border border-border hover:bg-body text-muted">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</button>
</AuthorizeView>
</div>
</div>
</div>
</div>
}
</div>
<!-- Pagination -->
@if (TotalPages > 1)
{
<div class="mt-12 flex items-center justify-center gap-2">
<button disabled="@(_currentPage == 1)" @onclick="() => _currentPage--"
class="p-2 rounded-lg border border-border text-muted hover:bg-body disabled:opacity-30 disabled:cursor-not-allowed transition-all">
<svg class="w-5 h-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"></path></svg>
</button>
@for (int i = 1; i <= TotalPages; i++)
{
var pageNumber = i;
<button @onclick="() => _currentPage = pageNumber"
class='w-10 h-10 rounded-lg border text-sm font-bold transition-all @(_currentPage == pageNumber ? "bg-main border-main text-body shadow-md" : "bg-card border-border text-muted hover:border-primary hover:text-primary")'>
@pageNumber
</button>
}
<button disabled="@(_currentPage == TotalPages)" @onclick="() => _currentPage++"
class="p-2 rounded-lg border border-border text-muted hover:bg-body disabled:opacity-30 disabled:cursor-not-allowed transition-all">
<svg class="w-5 h-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"></path></svg>
</button>
</div>
}
}
}
else if (_viewMode == ViewMode.Details && _selectedBook != null)
{
<div class="mb-8 animate-fade">
<button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
Back to Catalog
</button>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
<div class="lg:col-span-1">
<div class="aspect-[2/3] w-full rounded-3xl bg-body border border-border shadow-sm flex items-center justify-center text-muted overflow-hidden">
@if (!string.IsNullOrWhiteSpace(_selectedBook.CoverUrl))
{
<img src="@_selectedBook.CoverUrl" alt="@_selectedBook.Title" class="w-full h-full object-cover" />
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
}
</div>
</div>
<div class="lg:col-span-2 space-y-8">
<div>
<span class="inline-flex items-center rounded-full bg-body border border-border px-3 py-1 text-xs font-semibold text-muted mb-4 tracking-wide uppercase">@_selectedBook.Status</span>
<h1 class="text-4xl font-extrabold text-main tracking-tight">@_selectedBook.Title</h1>
<p class="text-xl text-muted mt-2 font-medium">by @_selectedBook.Author</p>
</div>
<div class="prose max-w-none">
<h3 class="text-lg font-bold text-main border-b border-border pb-2 mb-4 uppercase tracking-wider text-sm">Description</h3>
<p class="text-muted leading-relaxed">
@(string.IsNullOrEmpty(_selectedBook.Description) ? "No description available for this book." : _selectedBook.Description)
</p>
</div>
<div class="grid grid-cols-2 gap-8 border-y border-border py-6">
<div>
<h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-1">ISBN</h4>
<p class="text-sm font-semibold text-main">@_selectedBook.Isbn</p>
</div>
<div>
<h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-1">Availability</h4>
<p class="text-sm font-semibold @(_selectedBook.AvailableCopies > 0 ? "text-green-500" : "text-red-500")">
@(_selectedBook.AvailableCopies > 0 ? $"{_selectedBook.AvailableCopies} / {_selectedBook.TotalCopies} units in stock" : "Currently occupied")
</p>
</div>
</div>
<div class="flex flex-wrap items-center gap-4 pt-4">
<AuthorizeView Roles="Member">
<div class="flex items-center gap-3">
@if (_selectedBook.AvailableCopies > 0)
{
<button @onclick="() => BorrowAsync(_selectedBook.Id)" class="bg-primary text-white py-3 px-8 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Borrow This Book</button>
}
else
{
<div class="flex flex-col gap-2">
<button disabled class="bg-body border border-border text-muted py-3 px-8 rounded-xl font-bold opacity-50 cursor-not-allowed">Out of Stock</button>
<button @onclick="() => ToggleWishlist(_selectedBook)"
class='text-xs font-bold flex items-center gap-1.5 transition-colors @(IsInWishlist(_selectedBook.Id) ? "text-amber-500 underline" : "text-muted hover:text-amber-500 hover:underline")'>
<svg class="w-4 h-4" fill='@(IsInWishlist(_selectedBook.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
</svg>
@(IsInWishlist(_selectedBook.Id) ? "In Wishlist (Notifying)" : "Add to Wishlist for Notification")
</button>
</div>
}
<button @onclick="() => ToggleFavourite(_selectedBook)"
class='p-3 rounded-xl border transition-all shadow-sm active:scale-95 @(IsFavourite(_selectedBook.Id) ? "border-red-200 bg-red-500/10 text-red-500" : "border-border text-muted hover:bg-red-500/10 hover:text-red-500")'
title='@(IsFavourite(_selectedBook.Id) ? "Remove from Favourites" : "Add to Favourites")'>
<svg class="w-6 h-6" fill='@(IsFavourite(_selectedBook.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001z"></path>
</svg>
</button>
</div>
</AuthorizeView>
<AuthorizeView Roles="Librarian">
<button @onclick="() => ShowEdit(_selectedBook.Id)" class="bg-body border border-border text-main py-3 px-8 rounded-xl font-bold hover:bg-card">Edit Details</button>
<button @onclick="() => DeleteAsync(_selectedBook.Id)" class="p-3 text-red-500 hover:text-red-600 hover:bg-red-500/10 rounded-xl transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</AuthorizeView>
<AuthorizeView>
<NotAuthorized>
<a href="/login" class="bg-primary text-white py-3 px-8 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20">Sign In to Borrow</a>
</NotAuthorized>
</AuthorizeView>
</div>
</div>
</div>
</div>
}
else if (_viewMode == ViewMode.Create || _viewMode == ViewMode.Edit)
{
<div class="mb-8 animate-fade">
<button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
Back to Catalog
</button>
<div class="max-w-3xl mx-auto">
<div class="mb-8">
<h1 class="text-3xl font-bold tracking-tight text-main">@(_viewMode == ViewMode.Create ? "Add New Book" : "Edit Book")</h1>
<p class="mt-2 text-muted">Fill in the details below to @(_viewMode == ViewMode.Create ? "add a new book to" : "update the") library catalog.</p>
</div>
<div class="bg-card border border-border p-6 sm:p-8 rounded-3xl shadow-sm">
<EditForm Model="@_formModel" OnValidSubmit="HandleSubmit" class="space-y-6">
<DataAnnotationsValidator />
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-main mb-1">Book Title <span class="text-red-500">*</span></label>
<InputText @bind-Value="_formModel.Title" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="e.g. The Great Gatsby" required />
</div>
<div>
<label class="block text-sm font-medium text-main mb-1">Author <span class="text-red-500">*</span></label>
<InputText @bind-Value="_formModel.Author" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="e.g. F. Scott Fitzgerald" required />
</div>
<div>
<label class="block text-sm font-medium text-main mb-1">ISBN <span class="text-red-500">*</span></label>
<InputText @bind-Value="_formModel.Isbn" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="e.g. 978-0743273565" required />
</div>
<div>
<label class="block text-sm font-medium text-main mb-1">Total Copies <span class="text-red-500">*</span></label>
<InputNumber @bind-Value="_formModel.TotalCopies" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="1" required />
</div>
<div>
<label class="block text-sm font-medium text-main mb-1">Cover Image URL</label>
<InputText @bind-Value="_formModel.CoverUrl" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="https://example.com/cover.jpg" />
</div>
<div class="hidden sm:block"></div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-main mb-1">Description</label>
<InputTextArea @bind-Value="_formModel.Description" rows="4" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="Brief summary of the book..."></InputTextArea>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-main mb-2">Categories</label>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
@foreach (var cat in _categories)
{
<label class="flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-body hover:bg-card cursor-pointer text-sm text-main transition-colors">
<input type="checkbox" checked="@(_formModel.CategoryIds.Contains(cat.Id))"
@onchange='(e) => ToggleCategory(cat.Id, (bool)e.Value!)'
class="h-4 w-4 rounded border-border text-primary focus:ring-primary" />
@cat.Name
</label>
}
</div>
</div>
</div>
<div class="pt-6 border-t border-border flex justify-end gap-3 mt-8">
<button type="button" @onclick="ShowList" class="bg-body border border-border text-main px-6 py-2.5 rounded-xl font-bold hover:bg-card">Cancel</button>
<button type="submit" class="bg-primary text-white px-6 py-2.5 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20" disabled="@_isSubmitting">
@(_isSubmitting ? "Saving..." : (_viewMode == ViewMode.Create ? "Add Book" : "Update Book"))
</button>
</div>
</EditForm>
</div>
</div>
</div>
}
</div>
@code {
private enum ViewMode { List, Details, Create, Edit }
private ViewMode _viewMode = ViewMode.List;
private bool _isLoading = true;
private bool _isSubmitting = false;
private string _searchQuery = "";
private int? _selectedCategoryId;
private string _activeQuickFilter = "All";
// Pagination
private int _currentPage = 1;
private int _pageSize = 12;
private IEnumerable<BookDto> _books = Enumerable.Empty<BookDto>();
private IEnumerable<CategoryDto> _categories = Enumerable.Empty<CategoryDto>();
private IEnumerable<BorrowingDto> _myBorrowings = Enumerable.Empty<BorrowingDto>();
private IEnumerable<BorrowingDto> _allBorrowings = Enumerable.Empty<BorrowingDto>();
private BookDto? _selectedBook;
// Form Model
private BookFormModel _formModel = new();
private bool _isCategoryDropdownOpen = false;
private string SelectedCategoryName => _categories.FirstOrDefault(c => c.Id == _selectedCategoryId)?.Name ?? "";
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private void ToggleCategoryDropdown() => _isCategoryDropdownOpen = !_isCategoryDropdownOpen;
private async Task LoadDataAsync()
{
_isLoading = true;
try
{
_books = await ApiClient.GetBooksAsync();
_categories = await ApiClient.GetCategoriesAsync();
// Fetch borrowings for Trending and Recommended
_myBorrowings = await ApiClient.GetMyBorrowingsAsync();
try {
_allBorrowings = await ApiClient.GetAllBorrowingsAsync();
} catch { }
_currentPage = 1;
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
_isLoading = false;
}
}
private IEnumerable<BookDto> FilteredBooks
{
get
{
var books = _books;
// Apply Search
if (!string.IsNullOrWhiteSpace(_searchQuery))
{
books = books.Where(b => b.Title.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) ||
b.Author.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) ||
b.Isbn.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase));
}
// Apply Category Filter
if (_selectedCategoryId != null)
{
books = books.Where(b => b.Categories.Any(c => c.Id == _selectedCategoryId));
}
// Apply Quick Filter
switch (_activeQuickFilter)
{
case "New Arrivals":
books = books.Where(b => b.CreatedAt >= DateTime.UtcNow.AddDays(-7));
break;
case "Trending":
var trendingIds = _allBorrowings
.Where(br => br.BorrowDate >= DateTime.UtcNow.AddDays(-7))
.GroupBy(br => br.BookId)
.OrderByDescending(g => g.Count())
.Select(g => g.Key)
.Take(12)
.ToList();
if (trendingIds.Any())
books = books.Where(b => trendingIds.Contains(b.Id));
else
books = Enumerable.Empty<BookDto>(); // Or show nothing if no data
break;
case "Recommended":
var myCategoryIds = _myBorrowings.SelectMany(br => _books.FirstOrDefault(b => b.Id == br.BookId)?.Categories.Select(c => c.Id) ?? Enumerable.Empty<int>())
.Concat(_favourites.SelectMany(b => b.Categories.Select(c => c.Id)))
.Concat(_wishlist.SelectMany(b => b.Categories.Select(c => c.Id)))
.Distinct()
.ToList();
if (myCategoryIds.Any())
books = books.Where(b => b.Categories.Any(c => myCategoryIds.Contains(c.Id))).Take(8);
break;
}
return books;
}
}
private IEnumerable<BookDto> PaginatedBooks => FilteredBooks
.Skip((_currentPage - 1) * _pageSize)
.Take(_pageSize);
private int TotalPages => (int)Math.Ceiling(FilteredBooks.Count() / (double)_pageSize);
private void SetQuickFilter(string filter)
{
_activeQuickFilter = filter;
_selectedCategoryId = null; // Clear category when switching quick filters
_currentPage = 1;
}
private void FilterByCategory(int? id)
{
_selectedCategoryId = id;
_currentPage = 1;
}
private void ShowList()
{
_viewMode = ViewMode.List;
_selectedBook = null;
}
private async Task ShowDetails(int id)
{
_isLoading = true;
_selectedBook = await ApiClient.GetBookAsync(id);
_viewMode = ViewMode.Details;
_isLoading = false;
}
private void ShowCreate()
{
_formModel = new BookFormModel();
_viewMode = ViewMode.Create;
}
private async Task ShowEdit(int id)
{
_isLoading = true;
var book = await ApiClient.GetBookAsync(id);
if (book != null)
{
_selectedBook = book;
_formModel = new BookFormModel
{
Id = book.Id,
Title = book.Title,
Author = book.Author,
Isbn = book.Isbn,
Description = book.Description,
TotalCopies = book.TotalCopies,
CoverUrl = book.CoverUrl,
CategoryIds = book.Categories.Select(c => c.Id).ToList()
};
_viewMode = ViewMode.Edit;
}
_isLoading = false;
}
private void ToggleCategory(int id, bool isChecked)
{
if (isChecked && !_formModel.CategoryIds.Contains(id))
_formModel.CategoryIds.Add(id);
else if (!isChecked)
_formModel.CategoryIds.Remove(id);
}
private List<BookDto> _wishlist = new();
private List<BookDto> _favourites = new();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await LoadCollectionsAsync();
StateHasChanged();
}
}
private async Task LoadCollectionsAsync()
{
_wishlist = await WishlistService.GetWishlistAsync();
_favourites = await WishlistService.GetFavouritesAsync();
}
private bool IsFavourite(int id) => _favourites.Any(b => b.Id == id);
private bool IsInWishlist(int id) => _wishlist.Any(b => b.Id == id);
private async Task ToggleFavourite(BookDto book)
{
if (IsFavourite(book.Id))
{
await WishlistService.RemoveFromFavouriteAsync(book.Id);
await JS.InvokeVoidAsync("alert", $"Removed \"{book.Title}\" from favourites");
}
else
{
await WishlistService.AddToFavouritesAsync(book);
await JS.InvokeVoidAsync("alert", $"Added \"{book.Title}\" to favourites!");
}
await LoadCollectionsAsync();
}
private async Task ToggleWishlist(BookDto book)
{
if (IsInWishlist(book.Id))
{
await WishlistService.RemoveFromWishlistAsync(book.Id);
await JS.InvokeVoidAsync("alert", $"Removed \"{book.Title}\" from wishlist");
}
else
{
await WishlistService.AddToWishlistAsync(book);
await JS.InvokeVoidAsync("alert", $"Added \"{book.Title}\" to wishlist!");
}
await LoadCollectionsAsync();
}
private async Task HandleSubmit()
{
_isSubmitting = true;
try
{
if (_viewMode == ViewMode.Create)
{
var request = new BookCreateRequest
{
Title = _formModel.Title,
Author = _formModel.Author,
Isbn = _formModel.Isbn,
Description = _formModel.Description,
TotalCopies = _formModel.TotalCopies,
CoverUrl = _formModel.CoverUrl,
CategoryIds = _formModel.CategoryIds
};
var result = await ApiClient.CreateBookAsync(request);
if (result != null)
{
await JS.InvokeVoidAsync("alert", "Book added successfully!");
await LoadDataAsync();
ShowList();
}
}
else
{
var request = new BookUpdateRequest
{
Title = _formModel.Title,
Author = _formModel.Author,
Description = _formModel.Description,
TotalCopies = _formModel.TotalCopies,
CoverUrl = _formModel.CoverUrl,
CategoryIds = _formModel.CategoryIds
};
var result = await ApiClient.UpdateBookAsync(_formModel.Id, request);
if (result != null)
{
await JS.InvokeVoidAsync("alert", "Book updated successfully!");
await LoadDataAsync();
ShowList();
}
}
}
catch (Exception ex)
{
await JS.InvokeVoidAsync("alert", $"Error: {ex.Message}");
}
finally
{
_isSubmitting = false;
}
}
private async Task DeleteAsync(int id)
{
bool confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to remove this book from the catalog?");
if (!confirmed) return;
var success = await ApiClient.DeleteBookAsync(id);
if (success)
{
await JS.InvokeVoidAsync("alert", "Book deleted successfully");
await LoadDataAsync();
ShowList();
}
}
private async Task BorrowAsync(int id)
{
var result = await ApiClient.BorrowBookAsync(new BorrowRequest { BookId = id });
if (result != null)
{
await JS.InvokeVoidAsync("alert", "Book borrowed successfully! Check 'My Loans' for details.");
await LoadDataAsync();
await ShowDetails(id);
}
else
{
await JS.InvokeVoidAsync("alert", "Failed to borrow book. Check your loan limit.");
}
}
private class BookFormModel
{
public int Id { get; set; }
public string Title { get; set; } = "";
public string Author { get; set; } = "";
public string Isbn { get; set; } = "";
public string? Description { get; set; }
public int TotalCopies { get; set; } = 1;
public string? CoverUrl { get; set; }
public List<int> CategoryIds { get; set; } = new();
}
}