Spaces:
Sleeping
Sleeping
Yuyuqt
add: books, borrowings, home, login, membership, register, reward, userinsights, wishlist, themes
6f32ae8 | @model Frontend.Models.PagedResult<LibraryManagement.Shared.Models.BookDto> | |
| @{ | |
| ViewData["Title"] = "Books Catalog"; | |
| } | |
| <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8"> | |
| <div> | |
| <h1 class="text-3xl font-bold tracking-tight text-slate-900">Books Catalog</h1> | |
| <p class="mt-2 text-slate-500"> | |
| Explore and discover your next great read. | |
| <span class="ml-2 text-xs text-slate-400">(@Model.TotalCount total)</span> | |
| </p> | |
| </div> | |
| @if (User.IsInRole("Librarian")) | |
| { | |
| <div class="flex items-center gap-3"> | |
| <a asp-action="Create" class="btn-primary"> | |
| <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg> | |
| Add New Book | |
| </a> | |
| </div> | |
| } | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> | |
| <div class="md:col-span-3"> | |
| <div class="relative"> | |
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> | |
| <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" /> | |
| </svg> | |
| </div> | |
| <input type="text" class="block w-full pl-10 pr-3 py-2 border border-slate-200 rounded-lg bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent sm:text-sm shadow-sm" placeholder="Search by title, author or ISBN..."> | |
| </div> | |
| </div> | |
| </div> | |
| @* Category Filter Pills *@ | |
| @{ | |
| var categories = ViewBag.Categories as IEnumerable<LibraryManagement.Shared.Models.CategoryDto> ?? Enumerable.Empty<LibraryManagement.Shared.Models.CategoryDto>(); | |
| int? selectedCategoryId = ViewBag.SelectedCategoryId; | |
| } | |
| @if (categories.Any()) | |
| { | |
| <div class="flex flex-wrap items-center gap-2 mb-8"> | |
| <span class="text-xs font-semibold text-slate-400 uppercase tracking-wider mr-1">Filter:</span> | |
| <a asp-action="Index" asp-route-page="1" | |
| class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(selectedCategoryId == null ? "bg-slate-900 text-white border-slate-900" : "bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:text-slate-900")"> | |
| All | |
| </a> | |
| @foreach (var cat in categories) | |
| { | |
| <a asp-action="Index" asp-route-page="1" asp-route-categoryId="@cat.Id" | |
| class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(selectedCategoryId == cat.Id ? "bg-slate-900 text-white border-slate-900" : "bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:text-slate-900")"> | |
| @cat.Name | |
| </a> | |
| } | |
| </div> | |
| } | |
| <!-- Book Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 gap-8"> | |
| @foreach (var book in Model.Items) | |
| { | |
| <div class="group card hover:shadow-lg transition-all duration-300"> | |
| <div class="relative aspect-[2/3] w-full bg-slate-100 flex items-center justify-center text-slate-300 overflow-hidden text-center p-4"> | |
| <div class="flex flex-col items-center gap-4"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" 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> | |
| <span class="text-[10px] font-bold text-slate-400">@book.Isbn</span> | |
| </div> | |
| @if (book.AvailableCopies <= 0) | |
| { | |
| <div class="absolute inset-0 bg-slate-900/60 backdrop-blur-[2px] flex items-center justify-center"> | |
| <span class="bg-white text-slate-900 text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded">Out of Stock</span> | |
| </div> | |
| } | |
| </div> | |
| <div class="p-4"> | |
| <div class="flex items-start justify-between gap-2"> | |
| <div class="min-w-0"> | |
| <h3 class="text-sm font-bold text-slate-900 truncate group-hover:text-amber-600 transition-colors"> | |
| <a asp-action="Details" asp-route-id="@book.Id">@book.Title</a> | |
| </h3> | |
| <p class="text-xs text-slate-500 mt-0.5 truncate">@book.Author</p> | |
| </div> | |
| </div> | |
| @if (book.Categories.Any()) | |
| { | |
| <div class="mt-2 flex flex-wrap gap-1"> | |
| @foreach (var cat in book.Categories) | |
| { | |
| <a asp-action="Index" asp-route-page="1" asp-route-categoryId="@cat.Id" | |
| class="inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2 py-0.5 text-[9px] font-semibold text-amber-700 hover:bg-amber-100 transition-colors"> | |
| @cat.Name | |
| </a> | |
| } | |
| </div> | |
| } | |
| <div class="mt-3 flex items-center justify-between"> | |
| <span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">@book.Status</span> | |
| <span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")"> | |
| @book.AvailableCopies / @book.TotalCopies | |
| </span> | |
| </div> | |
| <div class="mt-4 pt-4 border-t border-slate-50 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <a asp-action="Details" asp-route-id="@book.Id" class="flex-1 btn-secondary py-1 text-[11px]">Details</a> | |
| @if (User.IsInRole("Librarian")) | |
| { | |
| <a asp-action="Edit" asp-route-id="@book.Id" class="p-1.5 rounded-md border border-slate-200 hover:bg-slate-50 text-slate-600"> | |
| <svg class="w-3.5 h-3.5" 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> | |
| </a> | |
| } | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| @* Pagination *@ | |
| @if (Model.TotalPages > 1) | |
| { | |
| <div class="mt-10 flex items-center justify-between"> | |
| <p class="text-sm text-slate-500"> | |
| Showing <span class="font-medium text-slate-700">@((Model.CurrentPage - 1) * Model.PageSize + 1)</span> | |
| – | |
| <span class="font-medium text-slate-700">@(Math.Min(Model.CurrentPage * Model.PageSize, Model.TotalCount))</span> | |
| of <span class="font-medium text-slate-700">@Model.TotalCount</span> books | |
| </p> | |
| <nav class="flex items-center gap-1" aria-label="Pagination"> | |
| @* Previous *@ | |
| @if (Model.HasPreviousPage) | |
| { | |
| <a asp-action="Index" asp-route-page="@(Model.CurrentPage - 1)" asp-route-categoryId="@selectedCategoryId" | |
| class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors"> | |
| <svg class="w-4 h-4 mr-1" 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> | |
| Prev | |
| </a> | |
| } | |
| else | |
| { | |
| <span class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-100 text-sm font-medium text-slate-300 cursor-not-allowed select-none"> | |
| <svg class="w-4 h-4 mr-1" 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> | |
| Prev | |
| </span> | |
| } | |
| @* Page numbers *@ | |
| @for (int p = 1; p <= Model.TotalPages; p++) | |
| { | |
| if (p == Model.CurrentPage) | |
| { | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-md bg-slate-900 text-white text-sm font-semibold">@p</span> | |
| } | |
| else if (p == 1 || p == Model.TotalPages || (p >= Model.CurrentPage - 2 && p <= Model.CurrentPage + 2)) | |
| { | |
| <a asp-action="Index" asp-route-page="@p" asp-route-categoryId="@selectedCategoryId" | |
| class="inline-flex items-center justify-center w-8 h-8 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">@p</a> | |
| } | |
| else if (p == Model.CurrentPage - 3 || p == Model.CurrentPage + 3) | |
| { | |
| <span class="inline-flex items-center justify-center w-8 h-8 text-sm text-slate-400">…</span> | |
| } | |
| } | |
| @* Next *@ | |
| @if (Model.HasNextPage) | |
| { | |
| <a asp-action="Index" asp-route-page="@(Model.CurrentPage + 1)" asp-route-categoryId="@selectedCategoryId" | |
| class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors"> | |
| Next | |
| <svg class="w-4 h-4 ml-1" 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> | |
| </a> | |
| } | |
| else | |
| { | |
| <span class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-100 text-sm font-medium text-slate-300 cursor-not-allowed select-none"> | |
| Next | |
| <svg class="w-4 h-4 ml-1" 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> | |
| </span> | |
| } | |
| </nav> | |
| </div> | |
| } | |