Yuyuqt commited on
Commit
861f0db
·
1 Parent(s): 36144b0

add: book categories and filter

Browse files
Backend/Features/Books/BookController.cs CHANGED
@@ -16,9 +16,9 @@ namespace Backend.Features.Books
16
 
17
  [HttpGet]
18
  [Authorize]
19
- public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
20
  {
21
- var books = await _bookService.GetAllBooksAsync();
22
  return Ok(books);
23
  }
24
 
 
16
 
17
  [HttpGet]
18
  [Authorize]
19
+ public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks([FromQuery] int? categoryId = null)
20
  {
21
+ var books = await _bookService.GetAllBooksAsync(categoryId);
22
  return Ok(books);
23
  }
24
 
Backend/Features/Books/BookService.cs CHANGED
@@ -5,7 +5,7 @@ namespace Backend.Features.Books
5
  {
6
  public interface IBookService
7
  {
8
- Task<IEnumerable<BookDto>> GetAllBooksAsync();
9
  Task<BookDto?> GetBookByIdAsync(int id);
10
  Task<BookDto> CreateBookAsync(BookCreateRequest request);
11
  Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request);
@@ -21,17 +21,26 @@ namespace Backend.Features.Books
21
  _context = context;
22
  }
23
 
24
- public async Task<IEnumerable<BookDto>> GetAllBooksAsync()
25
  {
26
- return await _context.Books
27
- .Where(b => b.IsActive)
28
- .Select(b => MapToDto(b))
29
- .ToListAsync();
 
 
 
 
 
 
 
30
  }
31
 
32
  public async Task<BookDto?> GetBookByIdAsync(int id)
33
  {
34
- var book = await _context.Books.FindAsync(id);
 
 
35
  if (book == null || !book.IsActive) return null;
36
  return MapToDto(book);
37
  }
@@ -56,6 +65,16 @@ namespace Backend.Features.Books
56
  CreatedAt = DateTime.UtcNow
57
  };
58
 
 
 
 
 
 
 
 
 
 
 
59
  _context.Books.Add(book);
60
  await _context.SaveChangesAsync();
61
 
@@ -64,7 +83,9 @@ namespace Backend.Features.Books
64
 
65
  public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
66
  {
67
- var book = await _context.Books.FindAsync(id);
 
 
68
  if (book == null || !book.IsActive) return null;
69
 
70
  book.Title = request.Title;
@@ -81,6 +102,17 @@ namespace Backend.Features.Books
81
 
82
  book.UpdatedAt = DateTime.UtcNow;
83
 
 
 
 
 
 
 
 
 
 
 
 
84
  await _context.SaveChangesAsync();
85
  return MapToDto(book);
86
  }
@@ -111,7 +143,8 @@ namespace Backend.Features.Books
111
  TotalCopies = book.TotalCopies,
112
  AvailableCopies = book.AvailableCopies,
113
  CreatedAt = book.CreatedAt,
114
- UpdatedAt = book.UpdatedAt
 
115
  };
116
  }
117
  }
@@ -129,6 +162,13 @@ namespace Backend.Features.Books
129
  public int AvailableCopies { get; set; }
130
  public DateTime CreatedAt { get; set; }
131
  public DateTime? UpdatedAt { get; set; }
 
 
 
 
 
 
 
132
  }
133
 
134
  public class BookCreateRequest
@@ -138,6 +178,7 @@ namespace Backend.Features.Books
138
  public string Author { get; set; } = string.Empty;
139
  public string? Description { get; set; }
140
  public int TotalCopies { get; set; }
 
141
  }
142
 
143
  public class BookUpdateRequest
@@ -146,5 +187,6 @@ namespace Backend.Features.Books
146
  public string Author { get; set; } = string.Empty;
147
  public string? Description { get; set; }
148
  public int TotalCopies { get; set; }
 
149
  }
150
  }
 
5
  {
6
  public interface IBookService
7
  {
8
+ Task<IEnumerable<BookDto>> GetAllBooksAsync(int? categoryId = null);
9
  Task<BookDto?> GetBookByIdAsync(int id);
10
  Task<BookDto> CreateBookAsync(BookCreateRequest request);
11
  Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request);
 
21
  _context = context;
22
  }
23
 
24
+ public async Task<IEnumerable<BookDto>> GetAllBooksAsync(int? categoryId = null)
25
  {
26
+ var query = _context.Books
27
+ .Include(b => b.Categories)
28
+ .Where(b => b.IsActive);
29
+
30
+ if (categoryId.HasValue)
31
+ {
32
+ query = query.Where(b => b.Categories.Any(c => c.Id == categoryId.Value));
33
+ }
34
+
35
+ var books = await query.ToListAsync();
36
+ return books.Select(b => MapToDto(b));
37
  }
38
 
39
  public async Task<BookDto?> GetBookByIdAsync(int id)
40
  {
41
+ var book = await _context.Books
42
+ .Include(b => b.Categories)
43
+ .FirstOrDefaultAsync(b => b.Id == id);
44
  if (book == null || !book.IsActive) return null;
45
  return MapToDto(book);
46
  }
 
65
  CreatedAt = DateTime.UtcNow
66
  };
67
 
68
+ // Attach selected categories
69
+ if (request.CategoryIds != null && request.CategoryIds.Any())
70
+ {
71
+ var categories = await _context.Categories
72
+ .Where(c => request.CategoryIds.Contains(c.Id))
73
+ .ToListAsync();
74
+ foreach (var cat in categories)
75
+ book.Categories.Add(cat);
76
+ }
77
+
78
  _context.Books.Add(book);
79
  await _context.SaveChangesAsync();
80
 
 
83
 
84
  public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
85
  {
86
+ var book = await _context.Books
87
+ .Include(b => b.Categories)
88
+ .FirstOrDefaultAsync(b => b.Id == id);
89
  if (book == null || !book.IsActive) return null;
90
 
91
  book.Title = request.Title;
 
102
 
103
  book.UpdatedAt = DateTime.UtcNow;
104
 
105
+ // Sync categories: replace existing with new selection
106
+ book.Categories.Clear();
107
+ if (request.CategoryIds != null && request.CategoryIds.Any())
108
+ {
109
+ var categories = await _context.Categories
110
+ .Where(c => request.CategoryIds.Contains(c.Id))
111
+ .ToListAsync();
112
+ foreach (var cat in categories)
113
+ book.Categories.Add(cat);
114
+ }
115
+
116
  await _context.SaveChangesAsync();
117
  return MapToDto(book);
118
  }
 
143
  TotalCopies = book.TotalCopies,
144
  AvailableCopies = book.AvailableCopies,
145
  CreatedAt = book.CreatedAt,
146
+ UpdatedAt = book.UpdatedAt,
147
+ Categories = book.Categories.Select(c => new BookCategoryDto { Id = c.Id, Name = c.Name }).ToList()
148
  };
149
  }
150
  }
 
162
  public int AvailableCopies { get; set; }
163
  public DateTime CreatedAt { get; set; }
164
  public DateTime? UpdatedAt { get; set; }
165
+ public List<BookCategoryDto> Categories { get; set; } = new();
166
+ }
167
+
168
+ public class BookCategoryDto
169
+ {
170
+ public int Id { get; set; }
171
+ public string Name { get; set; } = string.Empty;
172
  }
173
 
174
  public class BookCreateRequest
 
178
  public string Author { get; set; } = string.Empty;
179
  public string? Description { get; set; }
180
  public int TotalCopies { get; set; }
181
+ public List<int>? CategoryIds { get; set; }
182
  }
183
 
184
  public class BookUpdateRequest
 
187
  public string Author { get; set; } = string.Empty;
188
  public string? Description { get; set; }
189
  public int TotalCopies { get; set; }
190
+ public List<int>? CategoryIds { get; set; }
191
  }
192
  }
Backend/Features/Categories/CategoryController.cs CHANGED
@@ -22,6 +22,13 @@ namespace Backend.Features.Categories
22
  return Ok(categories);
23
  }
24
 
 
 
 
 
 
 
 
25
  [HttpPost]
26
  //[Authorize(Roles = "Librarian")]
27
  public async Task<ActionResult<CategoryDto>> CreateCategory([FromBody] CategoryCreateRequest request)
 
22
  return Ok(categories);
23
  }
24
 
25
+ [HttpGet("with-books")]
26
+ public async Task<ActionResult<IEnumerable<CategoryDto>>> GetCategoriesWithBooks()
27
+ {
28
+ var categories = await _categoryService.GetCategoriesWithBooksAsync();
29
+ return Ok(categories);
30
+ }
31
+
32
  [HttpPost]
33
  //[Authorize(Roles = "Librarian")]
34
  public async Task<ActionResult<CategoryDto>> CreateCategory([FromBody] CategoryCreateRequest request)
Backend/Features/Categories/CategoryService.cs CHANGED
@@ -6,6 +6,7 @@ namespace Backend.Features.Categories
6
  public interface ICategoryService
7
  {
8
  Task<IEnumerable<CategoryDto>> GetAllCategoriesAsync();
 
9
  Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request);
10
  Task<bool> DeleteCategoryAsync(int id);
11
  }
@@ -26,6 +27,15 @@ namespace Backend.Features.Categories
26
  .ToListAsync();
27
  }
28
 
 
 
 
 
 
 
 
 
 
29
  public async Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request)
30
  {
31
  if (await _context.Categories.AnyAsync(c => c.Name == request.Name))
 
6
  public interface ICategoryService
7
  {
8
  Task<IEnumerable<CategoryDto>> GetAllCategoriesAsync();
9
+ Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync();
10
  Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request);
11
  Task<bool> DeleteCategoryAsync(int id);
12
  }
 
27
  .ToListAsync();
28
  }
29
 
30
+ public async Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync()
31
+ {
32
+ // Only return categories that have at least one active book in BookCategories
33
+ return await _context.Categories
34
+ .Where(c => c.Books.Any(b => b.IsActive))
35
+ .Select(c => new CategoryDto { Id = c.Id, Name = c.Name })
36
+ .ToListAsync();
37
+ }
38
+
39
  public async Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request)
40
  {
41
  if (await _context.Categories.AnyAsync(c => c.Name == request.Name))
Frontend/Controllers/BooksController.cs CHANGED
@@ -13,10 +13,10 @@ namespace Frontend.Controllers
13
  _apiClient = apiClient;
14
  }
15
 
16
- public async Task<IActionResult> Index(int page = 1)
17
  {
18
  const int pageSize = 8;
19
- var allBooks = await _apiClient.GetBooksAsync() ?? Enumerable.Empty<BookDto>();
20
  var totalCount = allBooks.Count();
21
  var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
22
  page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
@@ -30,6 +30,9 @@ namespace Frontend.Controllers
30
  PageSize = pageSize
31
  };
32
 
 
 
 
33
  return View(pagedResult);
34
  }
35
 
@@ -80,7 +83,8 @@ namespace Frontend.Controllers
80
  Title = book.Title,
81
  Author = book.Author,
82
  Description = book.Description,
83
- TotalCopies = book.TotalCopies
 
84
  });
85
  }
86
 
 
13
  _apiClient = apiClient;
14
  }
15
 
16
+ public async Task<IActionResult> Index(int page = 1, int? categoryId = null)
17
  {
18
  const int pageSize = 8;
19
+ var allBooks = await _apiClient.GetBooksAsync(categoryId) ?? Enumerable.Empty<BookDto>();
20
  var totalCount = allBooks.Count();
21
  var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
22
  page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
 
30
  PageSize = pageSize
31
  };
32
 
33
+ ViewBag.Categories = await _apiClient.GetCategoriesWithBooksAsync();
34
+ ViewBag.SelectedCategoryId = categoryId;
35
+
36
  return View(pagedResult);
37
  }
38
 
 
83
  Title = book.Title,
84
  Author = book.Author,
85
  Description = book.Description,
86
+ TotalCopies = book.TotalCopies,
87
+ CategoryIds = book.Categories.Select(c => c.Id).ToList()
88
  });
89
  }
90
 
Frontend/Models/Dtos/LibraryDtos.cs CHANGED
@@ -41,6 +41,13 @@ namespace Frontend.Models.Dtos
41
  public int AvailableCopies { get; set; }
42
  public DateTime CreatedAt { get; set; }
43
  public DateTime? UpdatedAt { get; set; }
 
 
 
 
 
 
 
44
  }
45
 
46
  public class BookCreateRequest
@@ -50,6 +57,7 @@ namespace Frontend.Models.Dtos
50
  public string Author { get; set; } = string.Empty;
51
  public string? Description { get; set; }
52
  public int TotalCopies { get; set; }
 
53
  }
54
 
55
  public class BookUpdateRequest
@@ -58,6 +66,7 @@ namespace Frontend.Models.Dtos
58
  public string Author { get; set; } = string.Empty;
59
  public string? Description { get; set; }
60
  public int TotalCopies { get; set; }
 
61
  }
62
 
63
  // Borrowing DTOs
 
41
  public int AvailableCopies { get; set; }
42
  public DateTime CreatedAt { get; set; }
43
  public DateTime? UpdatedAt { get; set; }
44
+ public List<BookCategoryDto> Categories { get; set; } = new();
45
+ }
46
+
47
+ public class BookCategoryDto
48
+ {
49
+ public int Id { get; set; }
50
+ public string Name { get; set; } = string.Empty;
51
  }
52
 
53
  public class BookCreateRequest
 
57
  public string Author { get; set; } = string.Empty;
58
  public string? Description { get; set; }
59
  public int TotalCopies { get; set; }
60
+ public List<int>? CategoryIds { get; set; }
61
  }
62
 
63
  public class BookUpdateRequest
 
66
  public string Author { get; set; } = string.Empty;
67
  public string? Description { get; set; }
68
  public int TotalCopies { get; set; }
69
+ public List<int>? CategoryIds { get; set; }
70
  }
71
 
72
  // Borrowing DTOs
Frontend/Services/LibraryApiClient.cs CHANGED
@@ -54,9 +54,10 @@ namespace Frontend.Services
54
  #endregion
55
 
56
  #region Books
57
- public async Task<IEnumerable<BookDto>> GetBooksAsync()
58
  {
59
- return await _httpClient.GetFromJsonAsync<IEnumerable<BookDto>>("api/books") ?? Enumerable.Empty<BookDto>();
 
60
  }
61
 
62
  public async Task<BookDto?> GetBookAsync(int id)
@@ -113,6 +114,11 @@ namespace Frontend.Services
113
  return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
114
  }
115
 
 
 
 
 
 
116
  public async Task<CategoryDto?> CreateCategoryAsync(CategoryCreateRequest request)
117
  {
118
  var response = await _httpClient.PostAsJsonAsync("api/categories", request);
 
54
  #endregion
55
 
56
  #region Books
57
+ public async Task<IEnumerable<BookDto>> GetBooksAsync(int? categoryId = null)
58
  {
59
+ var url = categoryId.HasValue ? $"api/books?categoryId={categoryId}" : "api/books";
60
+ return await _httpClient.GetFromJsonAsync<IEnumerable<BookDto>>(url) ?? Enumerable.Empty<BookDto>();
61
  }
62
 
63
  public async Task<BookDto?> GetBookAsync(int id)
 
114
  return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
115
  }
116
 
117
+ public async Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync()
118
+ {
119
+ return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories/with-books") ?? Enumerable.Empty<CategoryDto>();
120
+ }
121
+
122
  public async Task<CategoryDto?> CreateCategoryAsync(CategoryCreateRequest request)
123
  {
124
  var response = await _httpClient.PostAsJsonAsync("api/categories", request);
Frontend/Views/Books/Create.cshtml CHANGED
@@ -67,6 +67,31 @@
67
  <span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
68
  </div>
69
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  </div>
71
 
72
  <div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
 
67
  <span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
68
  </div>
69
  </div>
70
+
71
+ <!-- Categories -->
72
+ <div class="sm:col-span-2">
73
+ <label class="block text-sm font-medium text-slate-700 mb-2">Categories</label>
74
+ @{
75
+ var allCategories = ViewBag.Categories as IEnumerable<Frontend.Models.Dtos.CategoryDto>;
76
+ }
77
+ @if (allCategories != null && allCategories.Any())
78
+ {
79
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
80
+ @foreach (var cat in allCategories)
81
+ {
82
+ <label class="flex items-center gap-2 px-3 py-2 rounded-md border border-slate-200 bg-slate-50 hover:bg-slate-100 cursor-pointer text-sm text-slate-700 transition-colors">
83
+ <input type="checkbox" name="CategoryIds" value="@cat.Id"
84
+ class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900" />
85
+ @cat.Name
86
+ </label>
87
+ }
88
+ </div>
89
+ }
90
+ else
91
+ {
92
+ <p class="text-sm text-slate-400 italic">No categories available. Add some first.</p>
93
+ }
94
+ </div>
95
  </div>
96
 
97
  <div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
Frontend/Views/Books/Edit.cshtml CHANGED
@@ -58,6 +58,33 @@
58
  <span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
59
  </div>
60
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
 
63
  <div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
 
58
  <span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
59
  </div>
60
  </div>
61
+
62
+ <!-- Categories -->
63
+ <div class="sm:col-span-2">
64
+ <label class="block text-sm font-medium text-slate-700 mb-2">Categories</label>
65
+ @{
66
+ var allCategories = ViewBag.Categories as IEnumerable<Frontend.Models.Dtos.CategoryDto>;
67
+ var selectedIds = Model.CategoryIds ?? new List<int>();
68
+ }
69
+ @if (allCategories != null && allCategories.Any())
70
+ {
71
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
72
+ @foreach (var cat in allCategories)
73
+ {
74
+ <label class="flex items-center gap-2 px-3 py-2 rounded-md border border-slate-200 bg-slate-50 hover:bg-slate-100 cursor-pointer text-sm text-slate-700 transition-colors">
75
+ <input type="checkbox" name="CategoryIds" value="@cat.Id"
76
+ class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900"
77
+ @(selectedIds.Contains(cat.Id) ? "checked" : "") />
78
+ @cat.Name
79
+ </label>
80
+ }
81
+ </div>
82
+ }
83
+ else
84
+ {
85
+ <p class="text-sm text-slate-400 italic">No categories available. Add some first.</p>
86
+ }
87
+ </div>
88
  </div>
89
 
90
  <div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
Frontend/Views/Books/Index.cshtml CHANGED
@@ -22,7 +22,7 @@
22
  }
23
  </div>
24
 
25
- <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-10">
26
  <div class="md:col-span-3">
27
  <div class="relative">
28
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
@@ -35,6 +35,30 @@
35
  </div>
36
  </div>
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  <!-- Book Grid -->
39
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 gap-8">
40
  @foreach (var book in Model.Items)
@@ -62,7 +86,19 @@
62
  <p class="text-xs text-slate-500 mt-0.5 truncate">@book.Author</p>
63
  </div>
64
  </div>
65
- <div class="mt-4 flex items-center justify-between">
 
 
 
 
 
 
 
 
 
 
 
 
66
  <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>
67
  <span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
68
  @book.AvailableCopies / @book.TotalCopies
@@ -96,7 +132,7 @@
96
  @* Previous *@
97
  @if (Model.HasPreviousPage)
98
  {
99
- <a asp-action="Index" asp-route-page="@(Model.CurrentPage - 1)"
100
  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">
101
  <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
@@ -123,7 +159,7 @@
123
  }
124
  else if (p == 1 || p == Model.TotalPages || (p >= Model.CurrentPage - 2 && p <= Model.CurrentPage + 2))
125
  {
126
- <a asp-action="Index" asp-route-page="@p"
127
  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>
128
  }
129
  else if (p == Model.CurrentPage - 3 || p == Model.CurrentPage + 3)
@@ -135,7 +171,7 @@
135
  @* Next *@
136
  @if (Model.HasNextPage)
137
  {
138
- <a asp-action="Index" asp-route-page="@(Model.CurrentPage + 1)"
139
  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">
140
  Next
141
  <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
 
22
  }
23
  </div>
24
 
25
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
26
  <div class="md:col-span-3">
27
  <div class="relative">
28
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
 
35
  </div>
36
  </div>
37
 
38
+ @* Category Filter Pills *@
39
+ @{
40
+ var categories = ViewBag.Categories as IEnumerable<Frontend.Models.Dtos.CategoryDto> ?? Enumerable.Empty<Frontend.Models.Dtos.CategoryDto>();
41
+ int? selectedCategoryId = ViewBag.SelectedCategoryId;
42
+ }
43
+ @if (categories.Any())
44
+ {
45
+ <div class="flex flex-wrap items-center gap-2 mb-8">
46
+ <span class="text-xs font-semibold text-slate-400 uppercase tracking-wider mr-1">Filter:</span>
47
+ <a asp-action="Index" asp-route-page="1"
48
+ 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")">
49
+ All
50
+ </a>
51
+ @foreach (var cat in categories)
52
+ {
53
+ <a asp-action="Index" asp-route-page="1" asp-route-categoryId="@cat.Id"
54
+ 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")">
55
+ @cat.Name
56
+ </a>
57
+ }
58
+ </div>
59
+ }
60
+
61
+
62
  <!-- Book Grid -->
63
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 gap-8">
64
  @foreach (var book in Model.Items)
 
86
  <p class="text-xs text-slate-500 mt-0.5 truncate">@book.Author</p>
87
  </div>
88
  </div>
89
+ @if (book.Categories.Any())
90
+ {
91
+ <div class="mt-2 flex flex-wrap gap-1">
92
+ @foreach (var cat in book.Categories)
93
+ {
94
+ <a asp-action="Index" asp-route-page="1" asp-route-categoryId="@cat.Id"
95
+ 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">
96
+ @cat.Name
97
+ </a>
98
+ }
99
+ </div>
100
+ }
101
+ <div class="mt-3 flex items-center justify-between">
102
  <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>
103
  <span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
104
  @book.AvailableCopies / @book.TotalCopies
 
132
  @* Previous *@
133
  @if (Model.HasPreviousPage)
134
  {
135
+ <a asp-action="Index" asp-route-page="@(Model.CurrentPage - 1)" asp-route-categoryId="@selectedCategoryId"
136
  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">
137
  <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
138
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
 
159
  }
160
  else if (p == 1 || p == Model.TotalPages || (p >= Model.CurrentPage - 2 && p <= Model.CurrentPage + 2))
161
  {
162
+ <a asp-action="Index" asp-route-page="@p" asp-route-categoryId="@selectedCategoryId"
163
  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>
164
  }
165
  else if (p == Model.CurrentPage - 3 || p == Model.CurrentPage + 3)
 
171
  @* Next *@
172
  @if (Model.HasNextPage)
173
  {
174
+ <a asp-action="Index" asp-route-page="@(Model.CurrentPage + 1)" asp-route-categoryId="@selectedCategoryId"
175
  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">
176
  Next
177
  <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">