Yuyuqt commited on
Commit
1fe511c
·
1 Parent(s): 6aad87c

add: wishlist and favourite features

Browse files
BlazorFrontend/Components/Layout/MainLayout.razor CHANGED
@@ -49,6 +49,10 @@
49
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
50
  Rewards
51
  </NavLink>
 
 
 
 
52
  }
53
  </Authorized>
54
  </AuthorizeView>
 
49
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
50
  Rewards
51
  </NavLink>
52
+ <NavLink href="Wishlist" class="nav-link" ActiveClass="active">
53
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /></svg>
54
+ Wishlist
55
+ </NavLink>
56
  }
57
  </Authorized>
58
  </AuthorizeView>
BlazorFrontend/Components/Pages/Books.razor CHANGED
@@ -3,6 +3,7 @@
3
  @using BlazorFrontend.Models.Dtos
4
  @using Microsoft.AspNetCore.Authorization
5
  @inject LibraryApiClient ApiClient
 
6
  @inject IJSRuntime JS
7
  @inject NavigationManager Navigation
8
 
@@ -128,6 +129,27 @@
128
  </div>
129
  <div class="mt-auto pt-4 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
130
  <button @onclick="() => ShowDetails(book.Id)" class="flex-1 btn-secondary py-1 text-[11px]">Details</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  <AuthorizeView Roles="Librarian">
132
  <button @onclick="() => ShowEdit(book.Id)" class="p-1.5 rounded-md border border-mystic-200 hover:bg-mystic-50 text-mystic-600">
133
  <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>
@@ -191,14 +213,32 @@ else if (_viewMode == ViewMode.Details && _selectedBook != null)
191
 
192
  <div class="flex items-center gap-4 pt-4">
193
  <AuthorizeView Roles="Member">
194
- @if (_selectedBook.AvailableCopies > 0)
195
- {
196
- <button @onclick="() => BorrowAsync(_selectedBook.Id)" class="btn-primary py-3 px-8 text-base shadow-lg shadow-mystic-200">Borrow This Book</button>
197
- }
198
- else
199
- {
200
- <button disabled class="btn-primary py-3 px-8 text-base opacity-50 cursor-not-allowed">Out of Stock</button>
201
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  </AuthorizeView>
203
 
204
  <AuthorizeView Roles="Librarian">
@@ -394,6 +434,57 @@ else if (_viewMode == ViewMode.Create || _viewMode == ViewMode.Edit)
394
  _formModel.CategoryIds.Remove(id);
395
  }
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  private async Task HandleSubmit()
398
  {
399
  _isSubmitting = true;
 
3
  @using BlazorFrontend.Models.Dtos
4
  @using Microsoft.AspNetCore.Authorization
5
  @inject LibraryApiClient ApiClient
6
+ @inject WishlistService WishlistService
7
  @inject IJSRuntime JS
8
  @inject NavigationManager Navigation
9
 
 
129
  </div>
130
  <div class="mt-auto pt-4 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
131
  <button @onclick="() => ShowDetails(book.Id)" class="flex-1 btn-secondary py-1 text-[11px]">Details</button>
132
+ <AuthorizeView Roles="Member">
133
+ <div class="flex gap-1">
134
+ <button @onclick="() => ToggleFavourite(book)"
135
+ class='p-1.5 rounded-md border transition-all @(IsFavourite(book.Id) ? "border-rose-200 bg-rose-50 text-rose-500" : "border-mystic-200 text-mystic-400 hover:bg-rose-50 hover:text-rose-500")'
136
+ title='@(IsFavourite(book.Id) ? "Remove from Favourites" : "Add to Favourites")'>
137
+ <svg class="w-3.5 h-3.5" fill='@(IsFavourite(book.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
138
+ <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>
139
+ </svg>
140
+ </button>
141
+ @if (book.AvailableCopies == 0)
142
+ {
143
+ <button @onclick="() => ToggleWishlist(book)"
144
+ class='p-1.5 rounded-md border transition-all @(IsInWishlist(book.Id) ? "border-amber-200 bg-amber-50 text-amber-500" : "border-mystic-200 text-mystic-400 hover:bg-amber-50 hover:text-amber-500")'
145
+ title='@(IsInWishlist(book.Id) ? "Remove from Wishlist" : "Add to Wishlist")'>
146
+ <svg class="w-3.5 h-3.5" fill='@(IsInWishlist(book.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
147
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
148
+ </svg>
149
+ </button>
150
+ }
151
+ </div>
152
+ </AuthorizeView>
153
  <AuthorizeView Roles="Librarian">
154
  <button @onclick="() => ShowEdit(book.Id)" class="p-1.5 rounded-md border border-mystic-200 hover:bg-mystic-50 text-mystic-600">
155
  <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>
 
213
 
214
  <div class="flex items-center gap-4 pt-4">
215
  <AuthorizeView Roles="Member">
216
+ <div class="flex items-center gap-3">
217
+ @if (_selectedBook.AvailableCopies > 0)
218
+ {
219
+ <button @onclick="() => BorrowAsync(_selectedBook.Id)" class="btn-primary py-3 px-8 text-base shadow-lg shadow-mystic-200">Borrow This Book</button>
220
+ }
221
+ else
222
+ {
223
+ <div class="flex flex-col gap-2">
224
+ <button disabled class="btn-primary py-3 px-8 text-base opacity-50 cursor-not-allowed">Out of Stock</button>
225
+ <button @onclick="() => ToggleWishlist(_selectedBook)"
226
+ class='text-xs font-bold flex items-center gap-1.5 transition-colors @(IsInWishlist(_selectedBook.Id) ? "text-amber-600 underline" : "text-mystic-400 hover:text-amber-600 hover:underline")'>
227
+ <svg class="w-4 h-4" fill='@(IsInWishlist(_selectedBook.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
228
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
229
+ </svg>
230
+ @(IsInWishlist(_selectedBook.Id) ? "In Wishlist (Notifying)" : "Add to Wishlist for Notification")
231
+ </button>
232
+ </div>
233
+ }
234
+ <button @onclick="() => ToggleFavourite(_selectedBook)"
235
+ class='p-3 rounded-xl border transition-all shadow-sm active:scale-95 @(IsFavourite(_selectedBook.Id) ? "border-rose-200 bg-rose-50 text-rose-500" : "border-mystic-200 text-mystic-400 hover:bg-rose-50 hover:text-rose-500")'
236
+ title='@(IsFavourite(_selectedBook.Id) ? "Remove from Favourites" : "Add to Favourites")'>
237
+ <svg class="w-6 h-6" fill='@(IsFavourite(_selectedBook.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
238
+ <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>
239
+ </svg>
240
+ </button>
241
+ </div>
242
  </AuthorizeView>
243
 
244
  <AuthorizeView Roles="Librarian">
 
434
  _formModel.CategoryIds.Remove(id);
435
  }
436
 
437
+ private List<BookDto> _wishlist = new();
438
+ private List<BookDto> _favourites = new();
439
+
440
+ protected override async Task OnAfterRenderAsync(bool firstRender)
441
+ {
442
+ if (firstRender)
443
+ {
444
+ await LoadCollectionsAsync();
445
+ StateHasChanged();
446
+ }
447
+ }
448
+
449
+ private async Task LoadCollectionsAsync()
450
+ {
451
+ _wishlist = await WishlistService.GetWishlistAsync();
452
+ _favourites = await WishlistService.GetFavouritesAsync();
453
+ }
454
+
455
+ private bool IsFavourite(int id) => _favourites.Any(b => b.Id == id);
456
+ private bool IsInWishlist(int id) => _wishlist.Any(b => b.Id == id);
457
+
458
+ private async Task ToggleFavourite(BookDto book)
459
+ {
460
+ if (IsFavourite(book.Id))
461
+ {
462
+ await WishlistService.RemoveFromFavouriteAsync(book.Id);
463
+ await JS.InvokeVoidAsync("showToast", $"Removed \"{book.Title}\" from favourites", "success");
464
+ }
465
+ else
466
+ {
467
+ await WishlistService.AddToFavouritesAsync(book);
468
+ await JS.InvokeVoidAsync("showToast", $"Added \"{book.Title}\" to favourites!", "success");
469
+ }
470
+ await LoadCollectionsAsync();
471
+ }
472
+
473
+ private async Task ToggleWishlist(BookDto book)
474
+ {
475
+ if (IsInWishlist(book.Id))
476
+ {
477
+ await WishlistService.RemoveFromWishlistAsync(book.Id);
478
+ await JS.InvokeVoidAsync("showToast", $"Removed \"{book.Title}\" from wishlist", "success");
479
+ }
480
+ else
481
+ {
482
+ await WishlistService.AddToWishlistAsync(book);
483
+ await JS.InvokeVoidAsync("showToast", $"Added \"{book.Title}\" to wishlist!", "success");
484
+ }
485
+ await LoadCollectionsAsync();
486
+ }
487
+
488
  private async Task HandleSubmit()
489
  {
490
  _isSubmitting = true;
BlazorFrontend/Components/Pages/Wishlist.razor ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/Wishlist"
2
+ @using BlazorFrontend.Services
3
+ @using BlazorFrontend.Models.Dtos
4
+ @using Microsoft.AspNetCore.Authorization
5
+ @attribute [Authorize(Roles = "Member")]
6
+ @inject WishlistService WishlistService
7
+ @inject IJSRuntime JS
8
+ @inject NavigationManager Navigation
9
+
10
+ <PageTitle>My Wishlist & Favourites</PageTitle>
11
+
12
+ <div class="mb-8 flex flex-col md:flex-row md:items-center justify-between gap-6 animate-in fade-in slide-in-from-top-4 duration-500">
13
+ <div>
14
+ <h1 class="text-3xl font-extrabold tracking-tight text-mystic-900">My Collections</h1>
15
+ <p class="text-mystic-600 mt-1">Track books you want to borrow and your all-time favourites.</p>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="mb-6 border-b border-mystic-200">
20
+ <nav class="-mb-px flex gap-6">
21
+ <button @onclick='() => _activeTab = "wishlist"'
22
+ class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "wishlist" ? "border-amber-500 text-amber-600" : "border-transparent text-mystic-400 hover:text-amber-500")'>
23
+ <span class="inline-flex items-center gap-1.5">
24
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
25
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
26
+ </svg>
27
+ Wishlist
28
+ @if (_wishlist.Any())
29
+ {
30
+ <span class="ml-1 px-1.5 py-0.5 rounded-full bg-amber-500 text-white text-[10px]">@_wishlist.Count</span>
31
+ }
32
+ </span>
33
+ </button>
34
+ <button @onclick='() => _activeTab = "favourites"'
35
+ class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "favourites" ? "border-rose-500 text-rose-600" : "border-transparent text-mystic-400 hover:text-rose-500")'>
36
+ <span class="inline-flex items-center gap-1.5">
37
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
38
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
39
+ </svg>
40
+ Favourites
41
+ @if (_favourites.Any())
42
+ {
43
+ <span class="ml-1 px-1.5 py-0.5 rounded-full bg-rose-500 text-white text-[10px]">@_favourites.Count</span>
44
+ }
45
+ </span>
46
+ </button>
47
+ </nav>
48
+ </div>
49
+
50
+ @if (_isLoading)
51
+ {
52
+ <div class="py-20 flex justify-center">
53
+ <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
54
+ </div>
55
+ }
56
+ else
57
+ {
58
+ <div class="animate-in fade-in duration-300">
59
+ @if (_activeTab == "wishlist")
60
+ {
61
+ @if (!_wishlist.Any())
62
+ {
63
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
64
+ <p class="text-mystic-400 font-medium">Your wishlist is empty. Add books that are currently out of stock!</p>
65
+ <a href="Books" class="inline-block mt-4 text-sm font-bold text-mystic-900 hover:underline underline-offset-4">Browse Catalog →</a>
66
+ </div>
67
+ }
68
+ else
69
+ {
70
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
71
+ @foreach (var book in _wishlist)
72
+ {
73
+ <div class="card p-4 flex gap-4 group hover:shadow-md transition-shadow relative">
74
+ <div class="w-20 h-28 bg-mystic-100 rounded-lg flex-shrink-0 flex items-center justify-center text-mystic-300 overflow-hidden border border-mystic-100">
75
+ @if (!string.IsNullOrWhiteSpace(book.CoverUrl))
76
+ {
77
+ <img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover" />
78
+ }
79
+ else
80
+ {
81
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" 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>
82
+ }
83
+ </div>
84
+ <div class="flex-grow min-w-0">
85
+ <h3 class="text-sm font-bold text-mystic-900 truncate">@book.Title</h3>
86
+ <p class="text-xs text-mystic-500 mt-0.5 truncate">@book.Author</p>
87
+ <div class="mt-2">
88
+ <span class="inline-flex items-center rounded-full @(book.AvailableCopies > 0 ? "bg-emerald-50 text-emerald-700" : "bg-rose-50 text-rose-700") px-2 py-0.5 text-[10px] font-bold">
89
+ @(book.AvailableCopies > 0 ? "Now Available" : "Out of Stock")
90
+ </span>
91
+ </div>
92
+ <div class="mt-4 flex items-center gap-2">
93
+ <button @onclick='() => Navigation.NavigateTo($"/Books")' class="text-[10px] font-bold text-mystic-600 hover:text-mystic-900 uppercase tracking-tight">View Info</button>
94
+ <span class="text-mystic-200">|</span>
95
+ <button @onclick="() => RemoveFromWishlist(book.Id)" class="text-[10px] font-bold text-rose-600 hover:text-rose-700 uppercase tracking-tight">Remove</button>
96
+ </div>
97
+ </div>
98
+ @if (book.AvailableCopies > 0)
99
+ {
100
+ <div class="absolute -top-2 -right-2 bg-emerald-500 text-white p-1 rounded-full shadow-sm animate-bounce">
101
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
102
+ </div>
103
+ }
104
+ </div>
105
+ }
106
+ </div>
107
+ <div class="mt-8 p-4 bg-mystic-900 rounded-2xl text-white flex items-center gap-4">
108
+ <div class="p-2 bg-mystic-800 rounded-lg">
109
+ <svg class="w-6 h-6 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
110
+ </div>
111
+ <p class="text-xs font-medium">We'll notify you via Firebase when out-of-stock items become available!</p>
112
+ </div>
113
+ }
114
+ }
115
+ else
116
+ {
117
+ @if (!_favourites.Any())
118
+ {
119
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
120
+ <p class="text-mystic-400 font-medium">You haven't added any favourites yet.</p>
121
+ <a href="Books" class="inline-block mt-4 text-sm font-bold text-mystic-900 hover:underline underline-offset-4">Browse Catalog →</a>
122
+ </div>
123
+ }
124
+ else
125
+ {
126
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
127
+ @foreach (var book in _favourites)
128
+ {
129
+ <div class="card p-4 flex gap-4 group hover:shadow-md transition-shadow">
130
+ <div class="w-20 h-28 bg-mystic-100 rounded-lg flex-shrink-0 flex items-center justify-center text-mystic-300 overflow-hidden border border-mystic-100">
131
+ @if (!string.IsNullOrWhiteSpace(book.CoverUrl))
132
+ {
133
+ <img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover" />
134
+ }
135
+ else
136
+ {
137
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" 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>
138
+ }
139
+ </div>
140
+ <div class="flex-grow min-w-0">
141
+ <h3 class="text-sm font-bold text-mystic-900 truncate">@book.Title</h3>
142
+ <p class="text-xs text-mystic-500 mt-0.5 truncate">@book.Author</p>
143
+ <div class="mt-4 flex items-center gap-2">
144
+ <button @onclick='() => Navigation.NavigateTo($"/Books")' class="text-[10px] font-bold text-mystic-600 hover:text-mystic-900 uppercase tracking-tight">View Info</button>
145
+ <span class="text-mystic-200">|</span>
146
+ <button @onclick="() => RemoveFromFavourite(book.Id)" class="text-[10px] font-bold text-rose-600 hover:text-rose-700 uppercase tracking-tight">Remove</button>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ }
151
+ </div>
152
+ }
153
+ }
154
+ </div>
155
+ }
156
+
157
+ @code {
158
+ private string _activeTab = "wishlist";
159
+ private bool _isLoading = true;
160
+ private List<BookDto> _wishlist = new();
161
+ private List<BookDto> _favourites = new();
162
+
163
+ protected override async Task OnAfterRenderAsync(bool firstRender)
164
+ {
165
+ if (firstRender)
166
+ {
167
+ await LoadDataAsync();
168
+ _isLoading = false;
169
+ StateHasChanged();
170
+ }
171
+ }
172
+
173
+ private async Task LoadDataAsync()
174
+ {
175
+ _wishlist = await WishlistService.GetWishlistAsync();
176
+ _favourites = await WishlistService.GetFavouritesAsync();
177
+ }
178
+
179
+ private async Task RemoveFromWishlist(int id)
180
+ {
181
+ await WishlistService.RemoveFromWishlistAsync(id);
182
+ await LoadDataAsync();
183
+ await JS.InvokeVoidAsync("showToast", "Removed from wishlist", "success");
184
+ }
185
+
186
+ private async Task RemoveFromFavourite(int id)
187
+ {
188
+ await WishlistService.RemoveFromFavouriteAsync(id);
189
+ await LoadDataAsync();
190
+ await JS.InvokeVoidAsync("showToast", "Removed from favourites", "success");
191
+ }
192
+ }
BlazorFrontend/Program.cs CHANGED
@@ -28,6 +28,7 @@ builder.Services.AddScoped(sp =>
28
 
29
 
30
  builder.Services.AddScoped<LibraryApiClient>();
 
31
 
32
  var app = builder.Build();
33
 
 
28
 
29
 
30
  builder.Services.AddScoped<LibraryApiClient>();
31
+ builder.Services.AddScoped<WishlistService>();
32
 
33
  var app = builder.Build();
34
 
BlazorFrontend/Services/WishlistService.cs ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using BlazorFrontend.Models.Dtos;
2
+ using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
3
+ using System.Text.Json;
4
+
5
+ namespace BlazorFrontend.Services;
6
+
7
+ public class WishlistService
8
+ {
9
+ private readonly ProtectedLocalStorage _localStorage;
10
+ private const string WishlistKey = "user_wishlist";
11
+ private const string FavouriteKey = "user_favourites";
12
+
13
+ public WishlistService(ProtectedLocalStorage localStorage)
14
+ {
15
+ _localStorage = localStorage;
16
+ }
17
+
18
+ public async Task<List<BookDto>> GetWishlistAsync()
19
+ {
20
+ try
21
+ {
22
+ var result = await _localStorage.GetAsync<string>(WishlistKey);
23
+ if (result.Success && !string.IsNullOrWhiteSpace(result.Value))
24
+ {
25
+ return JsonSerializer.Deserialize<List<BookDto>>(result.Value) ?? new List<BookDto>();
26
+ }
27
+ }
28
+ catch { }
29
+ return new List<BookDto>();
30
+ }
31
+
32
+ public async Task AddToWishlistAsync(BookDto book)
33
+ {
34
+ var list = await GetWishlistAsync();
35
+ if (!list.Any(b => b.Id == book.Id))
36
+ {
37
+ list.Add(book);
38
+ await _localStorage.SetAsync(WishlistKey, JsonSerializer.Serialize(list));
39
+ }
40
+ }
41
+
42
+ public async Task RemoveFromWishlistAsync(int bookId)
43
+ {
44
+ var list = await GetWishlistAsync();
45
+ var item = list.FirstOrDefault(b => b.Id == bookId);
46
+ if (item != null)
47
+ {
48
+ list.Remove(item);
49
+ await _localStorage.SetAsync(WishlistKey, JsonSerializer.Serialize(list));
50
+ }
51
+ }
52
+
53
+ public async Task<List<BookDto>> GetFavouritesAsync()
54
+ {
55
+ try
56
+ {
57
+ var result = await _localStorage.GetAsync<string>(FavouriteKey);
58
+ if (result.Success && !string.IsNullOrWhiteSpace(result.Value))
59
+ {
60
+ return JsonSerializer.Deserialize<List<BookDto>>(result.Value) ?? new List<BookDto>();
61
+ }
62
+ }
63
+ catch { }
64
+ return new List<BookDto>();
65
+ }
66
+
67
+ public async Task AddToFavouritesAsync(BookDto book)
68
+ {
69
+ var list = await GetFavouritesAsync();
70
+ if (!list.Any(b => b.Id == book.Id))
71
+ {
72
+ list.Add(book);
73
+ await _localStorage.SetAsync(FavouriteKey, JsonSerializer.Serialize(list));
74
+ }
75
+ }
76
+
77
+ public async Task RemoveFromFavouriteAsync(int bookId)
78
+ {
79
+ var list = await GetFavouritesAsync();
80
+ var item = list.FirstOrDefault(b => b.Id == bookId);
81
+ if (item != null)
82
+ {
83
+ list.Remove(item);
84
+ await _localStorage.SetAsync(FavouriteKey, JsonSerializer.Serialize(list));
85
+ }
86
+ }
87
+ }