Yuyuqt commited on
Commit
6f32ae8
·
1 Parent(s): 4433399

add: books, borrowings, home, login, membership, register, reward, userinsights, wishlist, themes

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Backend/Features/Loyalty/LoyaltyController.cs +32 -2
  2. Backend/Features/Loyalty/LoyaltyService.cs +82 -147
  3. Backend/Program.cs +11 -0
  4. BlazorFrontend/Services/LibraryApiClient.cs +11 -11
  5. BlazorWebAssembly/App.razor +30 -12
  6. BlazorWebAssembly/BlazorWebAssembly.csproj +2 -0
  7. BlazorWebAssembly/Layout/MainLayout.razor +96 -12
  8. BlazorWebAssembly/Layout/NavMenu.razor +66 -34
  9. BlazorWebAssembly/Layout/ThemeSwitcher.razor +29 -0
  10. BlazorWebAssembly/Pages/Books.razor +143 -0
  11. BlazorWebAssembly/Pages/Borrowings.razor +122 -0
  12. BlazorWebAssembly/Pages/Counter.razor +0 -18
  13. BlazorWebAssembly/Pages/Home.razor +146 -4
  14. BlazorWebAssembly/Pages/Login.razor +97 -0
  15. BlazorWebAssembly/Pages/Membership.razor +130 -0
  16. BlazorWebAssembly/Pages/Register.razor +110 -0
  17. BlazorWebAssembly/Pages/Rewards.razor +100 -0
  18. BlazorWebAssembly/Pages/UserInsights.razor +100 -0
  19. BlazorWebAssembly/Pages/Weather.razor +0 -57
  20. BlazorWebAssembly/Pages/Wishlist.razor +79 -0
  21. BlazorWebAssembly/Program.cs +25 -1
  22. BlazorWebAssembly/Providers/JwtAuthenticationStateProvider.cs +87 -0
  23. BlazorWebAssembly/Services/AuthService.cs +69 -0
  24. BlazorWebAssembly/Services/LibraryApiClient.cs +131 -0
  25. BlazorWebAssembly/Services/ThemeService.cs +40 -0
  26. BlazorWebAssembly/_Imports.razor +5 -1
  27. BlazorWebAssembly/wwwroot/css/app.css +65 -91
  28. BlazorWebAssembly/wwwroot/index.html +68 -7
  29. Frontend/Controllers/AuthController.cs +2 -1
  30. Frontend/Controllers/BooksController.cs +2 -1
  31. Frontend/Controllers/BorrowingsController.cs +2 -1
  32. Frontend/Controllers/HomeController.cs +2 -1
  33. Frontend/Controllers/MembersController.cs +4 -3
  34. Frontend/Controllers/MembershipController.cs +2 -1
  35. Frontend/Controllers/RewardsController.cs +2 -1
  36. Frontend/Controllers/SubscriptionsController.cs +2 -1
  37. Frontend/Frontend.csproj +4 -0
  38. Frontend/Models/Dtos/LibraryDtos.cs +0 -285
  39. Frontend/Models/Dtos/LoyaltyRedemptionDto.cs +0 -32
  40. Frontend/Models/MemberDetailsViewModel.cs +2 -1
  41. Frontend/Models/MembershipViewModel.cs +2 -1
  42. Frontend/Models/RewardsViewModel.cs +2 -1
  43. Frontend/Models/SubscriptionsViewModel.cs +2 -1
  44. Frontend/Services/LibraryApiClient.cs +19 -24
  45. Frontend/Views/Auth/Login.cshtml +2 -1
  46. Frontend/Views/Auth/Register.cshtml +2 -1
  47. Frontend/Views/Books/Create.cshtml +3 -2
  48. Frontend/Views/Books/Details.cshtml +2 -1
  49. Frontend/Views/Books/Edit.cshtml +3 -2
  50. Frontend/Views/Books/Index.cshtml +3 -2
Backend/Features/Loyalty/LoyaltyController.cs CHANGED
@@ -24,6 +24,13 @@ namespace Backend.Features.Loyalty
24
  _context = context;
25
  }
26
 
 
 
 
 
 
 
 
27
  // ── Member endpoints ──────────────────────────────────────────────────
28
 
29
  [HttpGet("my-account")]
@@ -32,8 +39,31 @@ namespace Backend.Features.Loyalty
32
  var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
33
  if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
34
 
 
 
 
 
 
35
  var account = await _loyaltyService.GetUserAccountAsync(userIdStr);
36
- if (account == null) return NotFound("Loyalty account not found.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  return Ok(account);
38
  }
39
 
@@ -55,7 +85,7 @@ namespace Backend.Features.Loyalty
55
  if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
56
 
57
  var redemptions = await _loyaltyService.GetUserRedemptionsAsync(userIdStr);
58
- var pending = redemptions.Where(r => r.Status.Equals("Pending", StringComparison.OrdinalIgnoreCase));
59
  return Ok(pending);
60
  }
61
 
 
24
  _context = context;
25
  }
26
 
27
+ [HttpGet("rewards")]
28
+ public async Task<IActionResult> GetRewards()
29
+ {
30
+ var rewards = await _loyaltyService.GetActiveRewardsAsync();
31
+ return Ok(rewards);
32
+ }
33
+
34
  // ── Member endpoints ──────────────────────────────────────────────────
35
 
36
  [HttpGet("my-account")]
 
39
  var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
40
  if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
41
 
42
+ // 2. Get user from DB to get Email/Mobile for potential registration
43
+ var user = await _context.Users.FindAsync(Guid.Parse(userIdStr));
44
+ if (user == null) return NotFound("User not found.");
45
+
46
+ // 3. Get Loyalty Account
47
  var account = await _loyaltyService.GetUserAccountAsync(userIdStr);
48
+
49
+ if (account == null)
50
+ {
51
+ // Auto-register if account not found
52
+ var registered = await _loyaltyService.RegisterUserAsync(userIdStr, user.Email, user.PhoneNumber ?? "0000000000");
53
+ if (registered)
54
+ {
55
+ // Process a small signup bonus if it's their first time being linked
56
+ await _loyaltyService.ProcessEventAsync(userIdStr, "SIGNUP", 20, $"LINK-{user.Id}", "Account Auto-Linked Bonus", user.Email, user.PhoneNumber);
57
+
58
+ // Try lookup again
59
+ account = await _loyaltyService.GetUserAccountAsync(userIdStr);
60
+ }
61
+ }
62
+
63
+ if (account == null)
64
+ {
65
+ return Ok(null); // Will show "Account Not Linked" in UI
66
+ }
67
  return Ok(account);
68
  }
69
 
 
85
  if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
86
 
87
  var redemptions = await _loyaltyService.GetUserRedemptionsAsync(userIdStr);
88
+ var pending = redemptions.Where(r => string.Equals(r.Status, "Pending", StringComparison.OrdinalIgnoreCase));
89
  return Ok(pending);
90
  }
91
 
Backend/Features/Loyalty/LoyaltyService.cs CHANGED
@@ -14,15 +14,15 @@ namespace Backend.Features.Loyalty
14
  public interface ILoyaltyService
15
  {
16
  Task<bool> RegisterUserAsync(string externalUserId, string email, string mobile);
17
- Task<bool> ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string referenceId, string description, string email, string mobile);
18
- Task<AccountLookupResponse?> GetUserAccountAsync(string externalUserId);
19
  Task<(bool Success, string Message)> ClaimRewardAsync(string externalUserId, string rewardId, string notes);
20
  Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync();
21
  Task<IEnumerable<LoyaltyRedemptionDto>> GetUserRedemptionsAsync(string externalUserId);
22
  Task<IEnumerable<LoyaltyRedemptionDto>> GetRedemptionsHistoryAsync();
23
  Task<bool> UpdateRedemptionStatusAsync(string redemptionId, string status);
24
  Task<IEnumerable<PointHistoryEntryDto>> GetPointsHistoryAsync(string accountId);
25
- Task<IEnumerable<PointHistoryEntryDto>> GetAllMembersPointsHistoryAsync();
26
  }
27
 
28
  public class LoyaltyService : ILoyaltyService
@@ -45,23 +45,19 @@ namespace Backend.Features.Loyalty
45
  {
46
  try
47
  {
 
 
 
48
  var payload = new
49
  {
50
- systemId = SystemId,
51
  externalUserId = externalUserId,
52
  email = email,
53
  mobile = mobile,
54
- tier = "Member" // Default
55
  };
56
 
57
  var response = await _httpClient.PostAsJsonAsync("/api/v1/accounts", payload);
58
- if (!response.IsSuccessStatusCode)
59
- {
60
- var error = await response.Content.ReadAsStringAsync();
61
- _logger.LogWarning($"Failed to register user in loyalty system: {error}");
62
- return false;
63
- }
64
- return true;
65
  }
66
  catch (Exception ex)
67
  {
@@ -70,12 +66,16 @@ namespace Backend.Features.Loyalty
70
  }
71
  }
72
 
73
- public async Task<bool> ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string referenceId, string description, string email, string mobile)
74
  {
75
  try
76
  {
 
 
 
77
  var payload = new
78
  {
 
79
  externalUserId = externalUserId,
80
  eventKey = eventKey,
81
  eventValue = eventValue,
@@ -85,14 +85,8 @@ namespace Backend.Features.Loyalty
85
  mobile = mobile
86
  };
87
 
88
- var response = await _httpClient.PostAsJsonAsync("/api/v1/events/process", payload);
89
- if (!response.IsSuccessStatusCode)
90
- {
91
- var error = await response.Content.ReadAsStringAsync();
92
- _logger.LogWarning($"Failed to process loyalty event {eventKey} for user {externalUserId}: {error}");
93
- return false;
94
- }
95
- return true;
96
  }
97
  catch (Exception ex)
98
  {
@@ -101,26 +95,32 @@ namespace Backend.Features.Loyalty
101
  }
102
  }
103
 
104
- public async Task<AccountLookupResponse?> GetUserAccountAsync(string externalUserId)
105
  {
106
  try
107
  {
108
- var response = await _httpClient.GetAsync($"/api/v1/accounts/lookup/{SystemId}/{externalUserId}");
 
 
 
 
109
  if (response.IsSuccessStatusCode)
110
  {
111
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
112
- var account = await response.Content.ReadFromJsonAsync<AccountLookupResponse>(options);
 
113
  if (account != null && string.IsNullOrWhiteSpace(account.Id) && !string.IsNullOrWhiteSpace(account.AccountId))
114
  {
115
  account.Id = account.AccountId;
116
  }
117
  return account;
118
  }
 
119
  return null;
120
  }
121
  catch (Exception ex)
122
  {
123
- _logger.LogError(ex, $"Error fetching loyalty account for user {externalUserId}.");
124
  return null;
125
  }
126
  }
@@ -131,36 +131,22 @@ namespace Backend.Features.Loyalty
131
  {
132
  var payload = new
133
  {
 
134
  externalUserId = externalUserId,
135
  rewardId = rewardId,
136
  notes = notes
137
  };
138
 
139
- var response = await _httpClient.PostAsJsonAsync("/api/v1/redemption/claim", payload);
140
- if (response.IsSuccessStatusCode)
141
- {
142
- return (true, "Reward claimed successfully!");
143
- }
144
 
145
  var error = await response.Content.ReadAsStringAsync();
146
- _logger.LogWarning($"Failed to claim reward {rewardId} for user {externalUserId}: {error}");
147
-
148
- try
149
- {
150
- var errorDoc = JsonDocument.Parse(error);
151
- if (errorDoc.RootElement.TryGetProperty("message", out var msgElement))
152
- {
153
- return (false, msgElement.GetString() ?? "Failed to claim reward.");
154
- }
155
- }
156
- catch { }
157
-
158
- return (false, string.IsNullOrEmpty(error) ? "Failed to claim reward." : error);
159
  }
160
  catch (Exception ex)
161
  {
162
  _logger.LogError(ex, "Error while claiming reward.");
163
- return (false, "An error occurred while processing your request.");
164
  }
165
  }
166
 
@@ -168,58 +154,56 @@ namespace Backend.Features.Loyalty
168
  {
169
  try
170
  {
171
- var response = await _httpClient.GetAsync($"/api/v1/redemption/history/{SystemId}/{externalUserId}");
 
 
 
172
  if (response.IsSuccessStatusCode)
173
  {
174
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
175
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
176
  }
177
  return Enumerable.Empty<LoyaltyRedemptionDto>();
178
  }
179
- catch (Exception ex)
180
- {
181
- _logger.LogError(ex, $"Error fetching redemptions for user {externalUserId}.");
182
- return Enumerable.Empty<LoyaltyRedemptionDto>();
183
- }
184
  }
185
 
186
  public async Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync()
187
  {
188
  try
189
  {
190
- var response = await _httpClient.GetAsync("/api/v1/admin/redemptions/pending");
 
 
 
191
  if (response.IsSuccessStatusCode)
192
  {
193
- var rawJson = await response.Content.ReadAsStringAsync();
194
- _logger.LogInformation($"Raw Pending Redemptions JSON: {rawJson}");
195
-
196
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
197
- return JsonSerializer.Deserialize<IEnumerable<LoyaltyRedemptionDto>>(rawJson, options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
198
  }
199
  return Enumerable.Empty<LoyaltyRedemptionDto>();
200
  }
201
- catch (Exception ex)
202
- {
203
- _logger.LogError(ex, "Error fetching pending redemptions.");
204
- return Enumerable.Empty<LoyaltyRedemptionDto>();
205
- }
206
  }
207
 
208
  public async Task<IEnumerable<LoyaltyRedemptionDto>> GetRedemptionsHistoryAsync()
209
  {
210
  try
211
  {
212
- var response = await _httpClient.GetAsync("/api/v1/admin/redemptions/history");
 
 
 
213
  if (response.IsSuccessStatusCode)
214
  {
215
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
216
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
217
  }
218
  return Enumerable.Empty<LoyaltyRedemptionDto>();
219
  }
220
  catch (Exception ex)
221
  {
222
- _logger.LogError(ex, "Error fetching redemptions history.");
223
  return Enumerable.Empty<LoyaltyRedemptionDto>();
224
  }
225
  }
@@ -228,107 +212,58 @@ namespace Backend.Features.Loyalty
228
  {
229
  try
230
  {
231
- var response = await _httpClient.PutAsJsonAsync($"/api/v1/admin/redemptions/{redemptionId}/status", new { status });
 
 
 
232
  return response.IsSuccessStatusCode;
233
  }
234
- catch (Exception ex)
235
- {
236
- _logger.LogError(ex, $"Error updating redemption status to {status} for {redemptionId}.");
237
- return false;
238
- }
239
  }
240
 
241
  public async Task<IEnumerable<PointHistoryEntryDto>> GetPointsHistoryAsync(string accountId)
242
  {
243
  try
244
  {
245
- var response = await _httpClient.GetAsync($"/api/v1/accounts/{accountId}/history");
 
 
 
246
  if (response.IsSuccessStatusCode)
247
  {
248
- var rawJson = await response.Content.ReadAsStringAsync();
249
- _logger.LogInformation("Raw Points History JSON for account {AccountId}: {Json}", accountId, rawJson);
250
-
251
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
252
- return JsonSerializer.Deserialize<IEnumerable<PointHistoryEntryDto>>(rawJson, options) ?? Enumerable.Empty<PointHistoryEntryDto>();
253
  }
254
-
255
- var error = await response.Content.ReadAsStringAsync();
256
- _logger.LogWarning("Points history request failed for account {AccountId}. Status={Status}. Body={Body}", accountId, response.StatusCode, error);
257
  return Enumerable.Empty<PointHistoryEntryDto>();
258
  }
259
  catch (Exception ex)
260
  {
261
- _logger.LogError(ex, $"Error fetching points history for account {accountId}.");
262
  return Enumerable.Empty<PointHistoryEntryDto>();
263
  }
264
  }
265
 
266
- public async Task<IEnumerable<PointHistoryEntryDto>> GetAllMembersPointsHistoryAsync()
267
  {
268
- return Enumerable.Empty<PointHistoryEntryDto>();
269
- }
270
- }
271
-
272
- public class PointHistoryEntryDto
273
- {
274
- [JsonPropertyName("id")]
275
- public int Id { get; set; }
276
-
277
- [JsonPropertyName("accountId")]
278
- public string AccountId { get; set; } = string.Empty;
279
-
280
- [JsonPropertyName("externalUserId")]
281
- public string? ExternalUserId { get; set; }
282
-
283
- [JsonPropertyName("pointDelta")]
284
- public double PointDelta { get; set; }
285
-
286
- [JsonPropertyName("eventKey")]
287
- public string EventKey { get; set; } = string.Empty;
288
-
289
- [JsonPropertyName("description")]
290
- public string Description { get; set; } = string.Empty;
291
-
292
- [JsonPropertyName("referenceId")]
293
- public string? ReferenceId { get; set; }
294
-
295
- [JsonPropertyName("createdAt")]
296
- public DateTime CreatedAt { get; set; }
297
-
298
- // Optional redemption-related fields (present for REDEEM events)
299
- [JsonPropertyName("rewardId")]
300
- public string? RewardId { get; set; }
301
-
302
- [JsonPropertyName("rewardName")]
303
- public string? RewardName { get; set; }
304
-
305
- [JsonPropertyName("redemptionStatus")]
306
- public string? RedemptionStatus { get; set; }
307
-
308
- [JsonPropertyName("redeemedAt")]
309
- public DateTime? RedeemedAt { get; set; }
310
- }
311
-
312
- public class AccountLookupResponse
313
- {
314
- [JsonPropertyName("id")]
315
- public string Id { get; set; } = string.Empty;
316
-
317
- // Some loyalty API versions return `accountId` instead of `id`.
318
- [JsonPropertyName("accountId")]
319
- public string? AccountId { get; set; }
320
-
321
- [JsonPropertyName("externalUserId")]
322
- public string ExternalUserId { get; set; } = string.Empty;
323
-
324
- [JsonPropertyName("currentBalance")]
325
- public double CurrentBalance { get; set; }
326
-
327
- [JsonPropertyName("tier")]
328
- public string Tier { get; set; } = string.Empty;
329
 
330
- [JsonPropertyName("lifetimePoints")]
331
- public double LifetimePoints { get; set; }
 
 
 
 
 
 
 
 
 
 
 
 
 
332
  }
333
  }
334
-
 
14
  public interface ILoyaltyService
15
  {
16
  Task<bool> RegisterUserAsync(string externalUserId, string email, string mobile);
17
+ Task<bool> ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string? referenceId = null, string? description = null, string? email = null, string? mobile = null);
18
+ Task<LoyaltyAccountDto?> GetUserAccountAsync(string externalUserId);
19
  Task<(bool Success, string Message)> ClaimRewardAsync(string externalUserId, string rewardId, string notes);
20
  Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync();
21
  Task<IEnumerable<LoyaltyRedemptionDto>> GetUserRedemptionsAsync(string externalUserId);
22
  Task<IEnumerable<LoyaltyRedemptionDto>> GetRedemptionsHistoryAsync();
23
  Task<bool> UpdateRedemptionStatusAsync(string redemptionId, string status);
24
  Task<IEnumerable<PointHistoryEntryDto>> GetPointsHistoryAsync(string accountId);
25
+ Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync();
26
  }
27
 
28
  public class LoyaltyService : ILoyaltyService
 
45
  {
46
  try
47
  {
48
+ _httpClient.DefaultRequestHeaders.Clear();
49
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
50
+
51
  var payload = new
52
  {
 
53
  externalUserId = externalUserId,
54
  email = email,
55
  mobile = mobile,
56
+ systemId = SystemId
57
  };
58
 
59
  var response = await _httpClient.PostAsJsonAsync("/api/v1/accounts", payload);
60
+ return response.IsSuccessStatusCode;
 
 
 
 
 
 
61
  }
62
  catch (Exception ex)
63
  {
 
66
  }
67
  }
68
 
69
+ public async Task<bool> ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string? referenceId = null, string? description = null, string? email = null, string? mobile = null)
70
  {
71
  try
72
  {
73
+ _httpClient.DefaultRequestHeaders.Clear();
74
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
75
+
76
  var payload = new
77
  {
78
+ systemId = SystemId,
79
  externalUserId = externalUserId,
80
  eventKey = eventKey,
81
  eventValue = eventValue,
 
85
  mobile = mobile
86
  };
87
 
88
+ var response = await _httpClient.PostAsJsonAsync($"/api/v1/events/process?systemId={SystemId}", payload);
89
+ return response.IsSuccessStatusCode;
 
 
 
 
 
 
90
  }
91
  catch (Exception ex)
92
  {
 
95
  }
96
  }
97
 
98
+ public async Task<LoyaltyAccountDto?> GetUserAccountAsync(string externalUserId)
99
  {
100
  try
101
  {
102
+ _httpClient.DefaultRequestHeaders.Clear();
103
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
104
+
105
+ var response = await _httpClient.GetAsync($"/api/v1/accounts/lookup/{externalUserId}?systemId={SystemId}");
106
+
107
  if (response.IsSuccessStatusCode)
108
  {
109
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
110
+ var account = await response.Content.ReadFromJsonAsync<LoyaltyAccountDto>(options);
111
+
112
  if (account != null && string.IsNullOrWhiteSpace(account.Id) && !string.IsNullOrWhiteSpace(account.AccountId))
113
  {
114
  account.Id = account.AccountId;
115
  }
116
  return account;
117
  }
118
+
119
  return null;
120
  }
121
  catch (Exception ex)
122
  {
123
+ _logger.LogError(ex, $"Error looking up loyalty account for {externalUserId}.");
124
  return null;
125
  }
126
  }
 
131
  {
132
  var payload = new
133
  {
134
+ systemId = SystemId,
135
  externalUserId = externalUserId,
136
  rewardId = rewardId,
137
  notes = notes
138
  };
139
 
140
+ var response = await _httpClient.PostAsJsonAsync($"/api/v1/redemption/claim?systemId={SystemId}", payload);
141
+ if (response.IsSuccessStatusCode) return (true, "Reward claimed successfully!");
 
 
 
142
 
143
  var error = await response.Content.ReadAsStringAsync();
144
+ return (false, error);
 
 
 
 
 
 
 
 
 
 
 
 
145
  }
146
  catch (Exception ex)
147
  {
148
  _logger.LogError(ex, "Error while claiming reward.");
149
+ return (false, "An error occurred.");
150
  }
151
  }
152
 
 
154
  {
155
  try
156
  {
157
+ _httpClient.DefaultRequestHeaders.Clear();
158
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
159
+
160
+ var response = await _httpClient.GetAsync($"/api/v1/redemption/history/{externalUserId}?systemId={SystemId}");
161
  if (response.IsSuccessStatusCode)
162
  {
163
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
164
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
165
  }
166
  return Enumerable.Empty<LoyaltyRedemptionDto>();
167
  }
168
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
 
 
 
 
169
  }
170
 
171
  public async Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync()
172
  {
173
  try
174
  {
175
+ _httpClient.DefaultRequestHeaders.Clear();
176
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
177
+
178
+ var response = await _httpClient.GetAsync($"/api/v1/admin/redemptions/pending?systemId={SystemId}");
179
  if (response.IsSuccessStatusCode)
180
  {
181
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
182
+ return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
 
 
 
183
  }
184
  return Enumerable.Empty<LoyaltyRedemptionDto>();
185
  }
186
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
 
 
 
 
187
  }
188
 
189
  public async Task<IEnumerable<LoyaltyRedemptionDto>> GetRedemptionsHistoryAsync()
190
  {
191
  try
192
  {
193
+ _httpClient.DefaultRequestHeaders.Clear();
194
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
195
+
196
+ var response = await _httpClient.GetAsync($"/api/v1/admin/redemptions/history?systemId={SystemId}");
197
  if (response.IsSuccessStatusCode)
198
  {
199
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
200
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
201
  }
202
  return Enumerable.Empty<LoyaltyRedemptionDto>();
203
  }
204
  catch (Exception ex)
205
  {
206
+ _logger.LogError(ex, "Error fetching redemption history.");
207
  return Enumerable.Empty<LoyaltyRedemptionDto>();
208
  }
209
  }
 
212
  {
213
  try
214
  {
215
+ _httpClient.DefaultRequestHeaders.Clear();
216
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
217
+
218
+ var response = await _httpClient.PutAsJsonAsync($"/api/v1/admin/redemptions/{redemptionId}/status?systemId={SystemId}", new { systemId = SystemId, status });
219
  return response.IsSuccessStatusCode;
220
  }
221
+ catch { return false; }
 
 
 
 
222
  }
223
 
224
  public async Task<IEnumerable<PointHistoryEntryDto>> GetPointsHistoryAsync(string accountId)
225
  {
226
  try
227
  {
228
+ _httpClient.DefaultRequestHeaders.Clear();
229
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
230
+
231
+ var response = await _httpClient.GetAsync($"/api/v1/accounts/{accountId}/history?systemId={SystemId}");
232
  if (response.IsSuccessStatusCode)
233
  {
234
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
235
+ return await response.Content.ReadFromJsonAsync<IEnumerable<PointHistoryEntryDto>>(options) ?? Enumerable.Empty<PointHistoryEntryDto>();
 
 
 
236
  }
 
 
 
237
  return Enumerable.Empty<PointHistoryEntryDto>();
238
  }
239
  catch (Exception ex)
240
  {
241
+ _logger.LogError(ex, $"Error fetching points history for {accountId}.");
242
  return Enumerable.Empty<PointHistoryEntryDto>();
243
  }
244
  }
245
 
246
+ public async Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync()
247
  {
248
+ try
249
+ {
250
+ _httpClient.DefaultRequestHeaders.Clear();
251
+ _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ var response = await _httpClient.GetAsync($"/api/v1/rewards/active?systemId={SystemId}");
254
+
255
+ if (response.IsSuccessStatusCode)
256
+ {
257
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
258
+ return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRewardDto>>(options) ?? Enumerable.Empty<LoyaltyRewardDto>();
259
+ }
260
+ return Enumerable.Empty<LoyaltyRewardDto>();
261
+ }
262
+ catch (Exception ex)
263
+ {
264
+ _logger.LogError(ex, "Error fetching active rewards.");
265
+ return Enumerable.Empty<LoyaltyRewardDto>();
266
+ }
267
+ }
268
  }
269
  }
 
Backend/Program.cs CHANGED
@@ -25,6 +25,14 @@ var builder = WebApplication.CreateBuilder(args);
25
  // Credential = GoogleCredential.FromFile("LibraryFirebase.json")
26
  //});
27
 
 
 
 
 
 
 
 
 
28
 
29
  builder.Services.AddControllers();
30
  builder.Services.AddAuthorization();
@@ -104,6 +112,9 @@ var app = builder.Build();
104
 
105
  app.UseSwagger();
106
  app.UseSwaggerUI();
 
 
 
107
  app.UseAuthentication();
108
  app.UseAuthorization();
109
 
 
25
  // Credential = GoogleCredential.FromFile("LibraryFirebase.json")
26
  //});
27
 
28
+ builder.Services.AddCors(options =>
29
+ {
30
+ options.AddPolicy("AllowWasm",
31
+ policy => policy.WithOrigins("https://localhost:7058", "http://localhost:5158")
32
+ .AllowAnyMethod()
33
+ .AllowAnyHeader());
34
+ });
35
+
36
 
37
  builder.Services.AddControllers();
38
  builder.Services.AddAuthorization();
 
112
 
113
  app.UseSwagger();
114
  app.UseSwaggerUI();
115
+
116
+ app.UseCors("AllowWasm");
117
+
118
  app.UseAuthentication();
119
  app.UseAuthorization();
120
 
BlazorFrontend/Services/LibraryApiClient.cs CHANGED
@@ -1,3 +1,4 @@
 
1
  using System.Net.Http.Json;
2
  using System.Text.Json;
3
  using LibraryManagement.Shared.Models;
@@ -182,7 +183,7 @@ namespace BlazorFrontend.Services
182
  {
183
  // JsonPropertyName attributes in LoyaltyAccountDto will handle the mapping
184
  // and correctly populate CurrentBalance from the backend's currentBalance
185
- return await response.Content.ReadFromJsonAsync<LoyaltyAccountDto>();
186
  }
187
  else
188
  {
@@ -221,11 +222,8 @@ namespace BlazorFrontend.Services
221
  {
222
  try
223
  {
224
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
225
- var response = await _httpClient.GetAsync($"api/v1/accounts/{accountId}/history");
226
- if (response.IsSuccessStatusCode)
227
- return await response.Content.ReadFromJsonAsync<IEnumerable<PointHistoryEntryDto>>(options) ?? Enumerable.Empty<PointHistoryEntryDto>();
228
- return Enumerable.Empty<PointHistoryEntryDto>();
229
  }
230
  catch { return Enumerable.Empty<PointHistoryEntryDto>(); }
231
  }
@@ -233,7 +231,7 @@ namespace BlazorFrontend.Services
233
  public async Task<IEnumerable<UserPointsHistoryDto>> GetAllMembersPointsHistoryAsync()
234
  {
235
  try {
236
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
237
  var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history");
238
  var raw = await response.Content.ReadAsStringAsync();
239
  if (!response.IsSuccessStatusCode)
@@ -293,9 +291,8 @@ namespace BlazorFrontend.Services
293
  {
294
  try
295
  {
296
-
297
- var client = new HttpClient();
298
- return await client.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS") ?? Enumerable.Empty<LoyaltyRewardDto>();
299
  }
300
  catch { return Enumerable.Empty<LoyaltyRewardDto>(); }
301
  }
@@ -342,7 +339,7 @@ namespace BlazorFrontend.Services
342
  var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending");
343
  if (response.IsSuccessStatusCode)
344
  {
345
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
346
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
347
  }
348
  return Enumerable.Empty<LoyaltyRedemptionDto>();
@@ -413,3 +410,6 @@ namespace BlazorFrontend.Services
413
  #endregion
414
  }
415
  }
 
 
 
 
1
+ using System.Text.Json.Serialization;
2
  using System.Net.Http.Json;
3
  using System.Text.Json;
4
  using LibraryManagement.Shared.Models;
 
183
  {
184
  // JsonPropertyName attributes in LoyaltyAccountDto will handle the mapping
185
  // and correctly populate CurrentBalance from the backend's currentBalance
186
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; return await response.Content.ReadFromJsonAsync<LoyaltyAccountDto>(options);
187
  }
188
  else
189
  {
 
222
  {
223
  try
224
  {
225
+ // Call the Backend's proxy endpoint instead of trying to hit the external API directly
226
+ return await _httpClient.GetFromJsonAsync<IEnumerable<PointHistoryEntryDto>>("api/Loyalty/my-points-history") ?? Enumerable.Empty<PointHistoryEntryDto>();
 
 
 
227
  }
228
  catch { return Enumerable.Empty<PointHistoryEntryDto>(); }
229
  }
 
231
  public async Task<IEnumerable<UserPointsHistoryDto>> GetAllMembersPointsHistoryAsync()
232
  {
233
  try {
234
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
235
  var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history");
236
  var raw = await response.Content.ReadAsStringAsync();
237
  if (!response.IsSuccessStatusCode)
 
291
  {
292
  try
293
  {
294
+ // Call our Backend proxy which handles the external API and SystemId
295
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("api/Loyalty/rewards") ?? Enumerable.Empty<LoyaltyRewardDto>();
 
296
  }
297
  catch { return Enumerable.Empty<LoyaltyRewardDto>(); }
298
  }
 
339
  var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending");
340
  if (response.IsSuccessStatusCode)
341
  {
342
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
343
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
344
  }
345
  return Enumerable.Empty<LoyaltyRedemptionDto>();
 
410
  #endregion
411
  }
412
  }
413
+
414
+
415
+
BlazorWebAssembly/App.razor CHANGED
@@ -1,12 +1,30 @@
1
- <Router AppAssembly="@typeof(App).Assembly">
2
- <Found Context="routeData">
3
- <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
4
- <FocusOnNavigate RouteData="@routeData" Selector="h1" />
5
- </Found>
6
- <NotFound>
7
- <PageTitle>Not found</PageTitle>
8
- <LayoutView Layout="@typeof(MainLayout)">
9
- <p role="alert">Sorry, there's nothing at this address.</p>
10
- </LayoutView>
11
- </NotFound>
12
- </Router>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <CascadingAuthenticationState>
2
+ <Router AppAssembly="@typeof(App).Assembly">
3
+ <Found Context="routeData">
4
+ <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
5
+ <NotAuthorized>
6
+ <div class="p-5 text-center">
7
+ <h2 class="text-danger">Access Denied</h2>
8
+ <p>You don't have permission to view this page. Please log in.</p>
9
+ <a href="/login" class="btn btn-primary mt-3">Go to Login</a>
10
+ </div>
11
+ </NotAuthorized>
12
+ <Authorizing>
13
+ <div class="p-5 text-center">
14
+ <div class="spinner-border text-primary" role="status">
15
+ <span class="visually-hidden">Loading...</span>
16
+ </div>
17
+ <p class="mt-2">Verifying credentials...</p>
18
+ </div>
19
+ </Authorizing>
20
+ </AuthorizeRouteView>
21
+ <FocusOnNavigate RouteData="@routeData" Selector="h1" />
22
+ </Found>
23
+ <NotFound>
24
+ <PageTitle>Not found</PageTitle>
25
+ <LayoutView Layout="@typeof(MainLayout)">
26
+ <p role="alert">Sorry, there's nothing at this address.</p>
27
+ </LayoutView>
28
+ </NotFound>
29
+ </Router>
30
+ </CascadingAuthenticationState>
BlazorWebAssembly/BlazorWebAssembly.csproj CHANGED
@@ -7,6 +7,8 @@
7
  </PropertyGroup>
8
 
9
  <ItemGroup>
 
 
10
  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.26" />
11
  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.26" PrivateAssets="all" />
12
  </ItemGroup>
 
7
  </PropertyGroup>
8
 
9
  <ItemGroup>
10
+ <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
11
+ <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.0" />
12
  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.26" />
13
  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.26" PrivateAssets="all" />
14
  </ItemGroup>
BlazorWebAssembly/Layout/MainLayout.razor CHANGED
@@ -1,16 +1,100 @@
1
- @inherits LayoutComponentBase
2
- <div class="page">
3
- <div class="sidebar">
4
- <NavMenu />
5
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- <main>
8
- <div class="top-row px-4">
9
- <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- <article class="content px-4">
13
- @Body
14
- </article>
15
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  </div>
 
 
 
 
 
 
 
 
1
+ @inherits LayoutComponentBase
2
+ @using BlazorWebAssembly.Services
3
+ @using Microsoft.AspNetCore.Components.Authorization
4
+ @inject ThemeService ThemeService
5
+ @inject AuthenticationStateProvider AuthStateProvider
6
+
7
+ <div class="flex h-screen overflow-hidden bg-body text-main animate-fade font-sans">
8
+ <!-- Sidebar (Hidden on mobile) -->
9
+ <aside class="hidden lg:flex lg:w-64 lg:flex-col lg:fixed lg:inset-y-0 z-50 border-r border-border bg-card">
10
+ <div class="flex flex-col flex-grow pt-8 pb-4 overflow-y-auto">
11
+ <!-- Brand -->
12
+ <div class="flex items-center flex-shrink-0 px-6 gap-3 mb-10">
13
+ <div class="bg-primary rounded-xl p-2 text-white shadow-lg shadow-primary/20">
14
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/><path d="M4 12V4l4 4"/><path d="m12 6 4 4"/></svg>
15
+ </div>
16
+ <span class="text-xl font-black tracking-tighter uppercase">LibraryLuxe</span>
17
+ </div>
18
 
19
+ <!-- Navigation -->
20
+ <div class="flex-grow px-3">
21
+ <NavMenu />
22
+ </div>
23
+
24
+ <!-- Theme & User Mini Profile -->
25
+ <div class="px-6 py-6 border-t border-border">
26
+ <div class="mb-6">
27
+ <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-4 text-center">Switch Theme</p>
28
+ <ThemeSwitcher />
29
+ </div>
30
+
31
+ <AuthorizeView>
32
+ <Authorized>
33
+ <div class="flex items-center gap-3 p-3 bg-body rounded-2xl border border-border">
34
+ <div class="h-10 w-10 rounded-xl bg-primary flex items-center justify-center text-white font-bold shadow-sm">
35
+ @context.User.Identity?.Name?[..1].ToUpper()
36
+ </div>
37
+ <div class="flex flex-col min-w-0">
38
+ <span class="text-xs font-bold truncate">@context.User.Identity?.Name</span>
39
+ <span class="text-[10px] text-muted font-medium uppercase tracking-wider">Member</span>
40
+ </div>
41
+ </div>
42
+ </Authorized>
43
+ <NotAuthorized>
44
+ <div class="flex items-center gap-3 p-3 bg-body rounded-2xl border border-border">
45
+ <div class="h-10 w-10 rounded-xl bg-muted/20 flex items-center justify-center text-muted font-bold">
46
+ G
47
+ </div>
48
+ <div class="flex flex-col">
49
+ <span class="text-xs font-bold text-muted">Guest User</span>
50
+ <a href="login" class="text-[10px] text-primary font-black uppercase tracking-wider hover:underline">Sign In</a>
51
+ </div>
52
+ </div>
53
+ </NotAuthorized>
54
+ </AuthorizeView>
55
+ </div>
56
  </div>
57
+ </aside>
58
+
59
+ <!-- Main Content Wrapper -->
60
+ <div class="lg:pl-64 flex flex-col flex-1 w-0">
61
+ <!-- Topbar -->
62
+ <header class="h-20 bg-card/80 backdrop-blur-md border-b border-border sticky top-0 z-40 flex items-center justify-between px-8">
63
+ <div class="flex-grow max-w-lg">
64
+ <div class="relative">
65
+ <svg class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
66
+ <input type="text" placeholder="Quick search..." class="w-full bg-body border border-border rounded-xl pl-12 pr-4 py-2.5 text-sm focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all shadow-sm" />
67
+ </div>
68
+ </div>
69
 
70
+ <div class="flex items-center gap-6">
71
+ <AuthorizeView>
72
+ <Authorized>
73
+ <button class="relative text-muted hover:text-primary transition-colors">
74
+ <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="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
75
+ <span class="absolute top-0 right-0 w-2 h-2 bg-primary rounded-full border-2 border-card"></span>
76
+ </button>
77
+ <a href="/logout" class="text-xs font-black uppercase tracking-widest text-muted hover:text-red-500 transition-colors">Logout</a>
78
+ </Authorized>
79
+ <NotAuthorized>
80
+ <a href="/login" class="bg-primary text-white px-6 py-2 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Sign In</a>
81
+ </NotAuthorized>
82
+ </AuthorizeView>
83
+ </div>
84
+ </header>
85
+
86
+ <!-- Main Content Section -->
87
+ <main class="flex-1 overflow-y-auto p-8 scrollbar-hide">
88
+ <div class="max-w-6xl mx-auto">
89
+ @Body
90
+ </div>
91
+ </main>
92
+ </div>
93
  </div>
94
+
95
+ @code {
96
+ protected override async Task OnInitializedAsync()
97
+ {
98
+ await ThemeService.InitializeAsync();
99
+ }
100
+ }
BlazorWebAssembly/Layout/NavMenu.razor CHANGED
@@ -1,39 +1,71 @@
1
- <div class="top-row ps-3 navbar navbar-dark">
2
- <div class="container-fluid">
3
- <a class="navbar-brand" href="">BlazorWebAssembly</a>
4
- <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
5
- <span class="navbar-toggler-icon"></span>
6
- </button>
7
- </div>
8
- </div>
 
 
9
 
10
- <div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
11
- <nav class="flex-column">
12
- <div class="nav-item px-3">
13
- <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
14
- <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
15
- </NavLink>
16
- </div>
17
- <div class="nav-item px-3">
18
- <NavLink class="nav-link" href="counter">
19
- <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
20
- </NavLink>
21
- </div>
22
- <div class="nav-item px-3">
23
- <NavLink class="nav-link" href="weather">
24
- <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
25
- </NavLink>
26
- </div>
27
- </nav>
28
- </div>
29
 
30
- @code {
31
- private bool collapseNavMenu = true;
 
 
 
 
 
 
 
 
 
32
 
33
- private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- private void ToggleNavMenu()
36
- {
37
- collapseNavMenu = !collapseNavMenu;
38
  }
39
- }
 
1
+ <nav class="space-y-1">
2
+ <NavLink class="nav-link-v" href="" Match="NavLinkMatch.All">
3
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
4
+ Dashboard
5
+ </NavLink>
6
+
7
+ <NavLink class="nav-link-v" href="books">
8
+ <svg class="w-5 h-5 mr-3" 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 18.246 18.477 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
9
+ Books Catalog
10
+ </NavLink>
11
 
12
+ <AuthorizeView Roles="Member">
13
+ <NavLink class="nav-link-v" href="borrowings">
14
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path></svg>
15
+ My Loans
16
+ </NavLink>
17
+ <NavLink class="nav-link-v" href="rewards">
18
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"></path></svg>
19
+ Rewards
20
+ </NavLink>
21
+ <NavLink class="nav-link-v" href="membership">
22
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"></path></svg>
23
+ Plans
24
+ </NavLink>
25
+ <NavLink class="nav-link-v" href="wishlist">
26
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>
27
+ Wishlist
28
+ </NavLink>
29
+ </AuthorizeView>
 
30
 
31
+ <AuthorizeView Roles="Librarian">
32
+ <NavLink class="nav-link-v" href="user-insights">
33
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
34
+ Insights
35
+ </NavLink>
36
+ <NavLink class="nav-link-v" href="members">
37
+ <svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
38
+ Members
39
+ </NavLink>
40
+ </AuthorizeView>
41
+ </nav>
42
 
43
+ <style>
44
+ .nav-link-v {
45
+ display: flex;
46
+ align-items: center;
47
+ padding: 0.75rem 1.25rem;
48
+ border-radius: 12px;
49
+ font-size: 0.875rem;
50
+ font-weight: 600;
51
+ color: var(--text-muted);
52
+ text-decoration: none;
53
+ transition: all 0.2s;
54
+ margin-bottom: 0.25rem;
55
+ }
56
+
57
+ .nav-link-v:hover {
58
+ color: var(--primary);
59
+ background: var(--bg-body);
60
+ }
61
+
62
+ .nav-link-v.active {
63
+ color: var(--primary);
64
+ background: var(--bg-body);
65
+ box-shadow: inset 0 0 0 1px var(--border);
66
+ }
67
 
68
+ .nav-link-v.active svg {
69
+ color: var(--primary);
 
70
  }
71
+ </style>
BlazorWebAssembly/Layout/ThemeSwitcher.razor ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @using BlazorWebAssembly.Services
2
+ @inject ThemeService ThemeService
3
+
4
+ <div class="flex items-center bg-bg-body/50 border border-border p-1 rounded-2xl">
5
+ <button @onclick='() => SetTheme("light")'
6
+ class='w-9 h-9 rounded-xl flex items-center justify-center transition-all @(CurrentTheme == "light" ? "bg-card text-primary shadow-sm shadow-primary/10" : "text-muted hover:text-primary")'
7
+ title="Light Mode">
8
+ <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="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
9
+ </button>
10
+ <button @onclick='() => SetTheme("dark")'
11
+ class='w-9 h-9 rounded-xl flex items-center justify-center transition-all @(CurrentTheme == "dark" ? "bg-card text-primary shadow-sm shadow-primary/10" : "text-muted hover:text-primary")'
12
+ title="Dark Mode">
13
+ <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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
14
+ </button>
15
+ <button @onclick='() => SetTheme("emerald")'
16
+ class='w-9 h-9 rounded-xl flex items-center justify-center transition-all @(CurrentTheme == "emerald" ? "bg-card text-primary shadow-sm shadow-primary/10" : "text-muted hover:text-primary")'
17
+ title="Emerald Mode">
18
+ <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="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"></path></svg>
19
+ </button>
20
+ </div>
21
+
22
+ @code {
23
+ private string CurrentTheme => ThemeService.CurrentTheme;
24
+
25
+ private async Task SetTheme(string theme)
26
+ {
27
+ await ThemeService.SetThemeAsync(theme);
28
+ }
29
+ }
BlazorWebAssembly/Pages/Books.razor ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/books"
2
+ @using LibraryManagement.Shared.Models
3
+ @using BlazorWebAssembly.Services
4
+ @inject LibraryApiClient ApiClient
5
+ @inject NavigationManager Navigation
6
+
7
+ <div class="max-w-7xl mx-auto py-10 px-6">
8
+ <!-- Header -->
9
+ <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-12 gap-6">
10
+ <div>
11
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Book Collection</h1>
12
+ <p class="text-muted text-lg">Discover your next favorite story among our curated selection.</p>
13
+ </div>
14
+ <div class="relative w-full md:w-96">
15
+ <input type="text" @bind="_searchQuery" @bind:event="oninput"
16
+ placeholder="Search by title, author..."
17
+ 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" />
18
+ </div>
19
+ </div>
20
+
21
+ <!-- Category Filter -->
22
+ <div class="flex flex-wrap gap-2 mb-10">
23
+ <button @onclick="() => FilterByCategory(null)"
24
+ class='px-4 py-2 rounded-full text-sm font-semibold transition-all @(_selectedCategoryId == null ? "bg-primary text-white" : "bg-card text-muted border border-border hover:border-primary hover:text-primary")'>
25
+ All Genres
26
+ </button>
27
+ @foreach (var cat in _categories)
28
+ {
29
+ <button @onclick="() => FilterByCategory(cat.Id)"
30
+ class='px-4 py-2 rounded-full text-sm font-semibold transition-all @(_selectedCategoryId == cat.Id ? "bg-primary text-white" : "bg-card text-muted border border-border hover:border-primary hover:text-primary")'>
31
+ @cat.Name
32
+ </button>
33
+ }
34
+ </div>
35
+
36
+ <!-- Loading State -->
37
+ @if (_isLoading)
38
+ {
39
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
40
+ @for (int i = 0; i < 8; i++)
41
+ {
42
+ <div class="animate-pulse bg-card rounded-xl border border-border h-96"></div>
43
+ }
44
+ </div>
45
+ }
46
+ else if (!FilteredBooks.Any())
47
+ {
48
+ <div class="text-center py-20 bg-card rounded-2xl border border-dashed border-border">
49
+ <h3 class="text-xl font-bold text-main mb-2">No results found</h3>
50
+ <p class="text-muted">Try adjusting your search or filters.</p>
51
+ </div>
52
+ }
53
+ else
54
+ {
55
+ <!-- Books Grid -->
56
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
57
+ @foreach (var book in FilteredBooks)
58
+ {
59
+ <div class="group bg-card rounded-xl border border-border overflow-hidden hover:shadow-xl transition-all duration-300 flex flex-col h-full cursor-pointer"
60
+ @onclick="() => ShowDetails(book.Id)">
61
+ <!-- Cover -->
62
+ <div class="aspect-[2/3] bg-body relative overflow-hidden">
63
+ @if (!string.IsNullOrEmpty(book.CoverUrl))
64
+ {
65
+ <img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
66
+ }
67
+ else
68
+ {
69
+ <div class="w-full h-full flex items-center justify-center text-muted opacity-20">
70
+ <span class="font-bold uppercase tracking-widest text-xs">No Cover</span>
71
+ </div>
72
+ }
73
+
74
+ @if (book.AvailableCopies <= 0)
75
+ {
76
+ <div class="absolute inset-0 bg-black/40 backdrop-blur-[2px] flex items-center justify-center">
77
+ <span class="bg-white text-black px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest">Unavailable</span>
78
+ </div>
79
+ }
80
+ </div>
81
+
82
+ <!-- Content -->
83
+ <div class="p-5 flex-grow">
84
+ <div class="flex items-center gap-2 mb-2">
85
+ @foreach (var cat in book.Categories.Take(2))
86
+ {
87
+ <span class="text-[10px] font-bold uppercase tracking-wider text-primary opacity-80">@cat.Name</span>
88
+ }
89
+ </div>
90
+ <h3 class="font-bold text-main line-clamp-1 mb-1 group-hover:text-primary transition-colors">@book.Title</h3>
91
+ <p class="text-xs text-muted mb-4 font-medium">@book.Author</p>
92
+
93
+ <div class="mt-auto flex items-center justify-between">
94
+ <span class="text-[10px] font-bold text-muted bg-body px-2 py-1 rounded border border-border">@book.Status</span>
95
+ <span class="text-xs font-bold @(book.AvailableCopies > 0 ? "text-green-500" : "text-red-400")">
96
+ @book.AvailableCopies / @book.TotalCopies
97
+ </span>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ }
102
+ </div>
103
+ }
104
+ </div>
105
+
106
+ @code {
107
+ private bool _isLoading = true;
108
+ private string _searchQuery = "";
109
+ private int? _selectedCategoryId;
110
+ private List<BookDto> _books = new();
111
+ private List<CategoryDto> _categories = new();
112
+
113
+ protected override async Task OnInitializedAsync()
114
+ {
115
+ await LoadData();
116
+ }
117
+
118
+ private async Task LoadData()
119
+ {
120
+ _isLoading = true;
121
+ try
122
+ {
123
+ _books = (await ApiClient.GetBooksAsync()).ToList();
124
+ _categories = (await ApiClient.GetCategoriesAsync()).ToList();
125
+ }
126
+ catch { }
127
+ finally { _isLoading = false; }
128
+ }
129
+
130
+ private IEnumerable<BookDto> FilteredBooks => _books
131
+ .Where(b => (string.IsNullOrEmpty(_searchQuery) || b.Title.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) || b.Author.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase))
132
+ && (!_selectedCategoryId.HasValue || b.Categories.Any(c => c.Id == _selectedCategoryId)));
133
+
134
+ private void FilterByCategory(int? id)
135
+ {
136
+ _selectedCategoryId = id;
137
+ }
138
+
139
+ private void ShowDetails(int id)
140
+ {
141
+ // Navigation.NavigateTo($"books/{id}");
142
+ }
143
+ }
BlazorWebAssembly/Pages/Borrowings.razor ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/borrowings"
2
+ @using LibraryManagement.Shared.Models
3
+ @using BlazorWebAssembly.Services
4
+ @inject LibraryApiClient ApiClient
5
+ @inject NavigationManager Navigation
6
+
7
+ <div class="max-w-6xl mx-auto py-12 px-6">
8
+ <!-- Header -->
9
+ <div class="flex justify-between items-end mb-16">
10
+ <div>
11
+ <h1 class="text-5xl font-black tracking-tight text-main mb-4">My Loans</h1>
12
+ <p class="text-lg text-muted max-w-xl">
13
+ Keep track of your reading journey and manage your active borrowings.
14
+ </p>
15
+ </div>
16
+ <button @onclick='() => Navigation.NavigateTo("books")' class="hidden md:block bg-body text-main border border-border px-8 py-3 rounded-xl font-bold hover:bg-card transition-all">
17
+ Browse More
18
+ </button>
19
+ </div>
20
+
21
+ @if (_isLoading)
22
+ {
23
+ <div class="space-y-6">
24
+ @for (int i = 0; i < 3; i++)
25
+ {
26
+ <div class="animate-pulse bg-card rounded-2xl border border-border h-32 w-full"></div>
27
+ }
28
+ </div>
29
+ }
30
+ else if (!_borrowings.Any())
31
+ {
32
+ <div class="text-center py-24 bg-card rounded-[2.5rem] border border-dashed border-border shadow-sm">
33
+ <h3 class="text-2xl font-bold text-main mb-3">No active loans</h3>
34
+ <p class="text-muted mb-8">You haven't borrowed any books yet. Explore our collection to get started.</p>
35
+ <button @onclick='() => Navigation.NavigateTo("books")' class="bg-primary text-white px-10 py-4 rounded-xl font-black hover:bg-primary-hover transition-all shadow-lg shadow-primary/20">
36
+ Go to Catalog
37
+ </button>
38
+ </div>
39
+ }
40
+ else
41
+ {
42
+ <div class="space-y-6">
43
+ @foreach (var loan in _borrowings)
44
+ {
45
+ <div class="bg-card rounded-2xl border border-border p-6 flex flex-col md:flex-row items-center gap-8 hover:border-primary/50 transition-all group">
46
+ <!-- Book Cover / Placeholder -->
47
+ <div class="w-20 h-28 bg-body rounded-lg overflow-hidden flex-shrink-0 border border-border flex items-center justify-center">
48
+ <svg class="w-8 h-8 text-muted opacity-20" 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 18.246 18.477 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
49
+ </div>
50
+
51
+ <!-- Details -->
52
+ <div class="flex-grow text-center md:text-left">
53
+ <h3 class="text-xl font-bold text-main mb-1 group-hover:text-primary transition-colors">@loan.BookTitle</h3>
54
+ <p class="text-sm text-muted font-medium mb-4 md:mb-0">ID: @loan.BookId</p>
55
+ </div>
56
+
57
+ <!-- Dates -->
58
+ <div class="grid grid-cols-2 gap-8 px-8 border-x border-border hidden lg:grid">
59
+ <div>
60
+ <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-1">Borrowed</p>
61
+ <p class="text-sm font-bold text-main">@loan.BorrowDate.ToString("MMM dd, yyyy")</p>
62
+ </div>
63
+ <div>
64
+ <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-1">Due Date</p>
65
+ <p class="text-sm font-bold @(loan.DueDate < DateTime.UtcNow ? "text-red-500" : "text-main")">
66
+ @loan.DueDate.ToString("MMM dd, yyyy")
67
+ </p>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Status & Action -->
72
+ <div class="flex flex-col md:items-end gap-3 min-w-[150px]">
73
+ <span class='px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest text-center @GetStatusColor(loan.Status)'>
74
+ @loan.Status
75
+ </span>
76
+ @if (loan.Status == "Borrowed")
77
+ {
78
+ <button @onclick="() => ReturnRequestAsync(loan.Id)" class="text-xs font-bold text-primary hover:underline">
79
+ Request Return
80
+ </button>
81
+ }
82
+ </div>
83
+ </div>
84
+ }
85
+ </div>
86
+ }
87
+ </div>
88
+
89
+ @code {
90
+ private bool _isLoading = true;
91
+ private List<BorrowingDto> _borrowings = new();
92
+
93
+ protected override async Task OnInitializedAsync()
94
+ {
95
+ await LoadData();
96
+ }
97
+
98
+ private async Task LoadData()
99
+ {
100
+ _isLoading = true;
101
+ try
102
+ {
103
+ _borrowings = (await ApiClient.GetMyBorrowingsAsync()).ToList();
104
+ }
105
+ catch { }
106
+ finally { _isLoading = false; }
107
+ }
108
+
109
+ private async Task ReturnRequestAsync(Guid id)
110
+ {
111
+ var success = await ApiClient.RequestReturnAsync(id);
112
+ if (success) await LoadData();
113
+ }
114
+
115
+ private string GetStatusColor(string status) => status switch
116
+ {
117
+ "Borrowed" => "bg-blue-500/10 text-blue-500",
118
+ "Returned" => "bg-green-500/10 text-green-500",
119
+ "PendingReturn" => "bg-orange-500/10 text-orange-500",
120
+ _ => "bg-body text-muted"
121
+ };
122
+ }
BlazorWebAssembly/Pages/Counter.razor DELETED
@@ -1,18 +0,0 @@
1
- @page "/counter"
2
-
3
- <PageTitle>Counter</PageTitle>
4
-
5
- <h1>Counter</h1>
6
-
7
- <p role="status">Current count: @currentCount</p>
8
-
9
- <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
10
-
11
- @code {
12
- private int currentCount = 0;
13
-
14
- private void IncrementCount()
15
- {
16
- currentCount++;
17
- }
18
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BlazorWebAssembly/Pages/Home.razor CHANGED
@@ -1,7 +1,149 @@
1
- @page "/"
 
 
 
 
 
 
2
 
3
- <PageTitle>Home</PageTitle>
 
 
 
 
 
 
 
 
 
4
 
5
- <h1>Hello, world!</h1>
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- Welcome to your new app.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/"
2
+ @using LibraryManagement.Shared.Models
3
+ @using BlazorWebAssembly.Services
4
+ @using Microsoft.AspNetCore.Components.Authorization
5
+ @inject LibraryApiClient ApiClient
6
+ @inject NavigationManager Navigation
7
+ @inject AuthenticationStateProvider AuthStateProvider
8
 
9
+ <AuthorizeView>
10
+ <Authorized>
11
+ <!-- Authenticated Dashboard View -->
12
+ <div class="space-y-12 animate-fade">
13
+ <section>
14
+ <h1 class="text-4xl font-black tracking-tight mb-2 text-main">
15
+ Welcome back, <span class="text-primary">@context.User.Identity?.Name</span>
16
+ </h1>
17
+ <p class="text-lg text-muted">Here is what's happening with your library account today.</p>
18
+ </section>
19
 
20
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
21
+ <!-- Loyalty Points -->
22
+ <div class="bg-card p-8 rounded-3xl border border-border shadow-sm hover:shadow-md transition-all">
23
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-4">Loyalty Balance</p>
24
+ <div class="flex items-baseline gap-2 mb-4">
25
+ <h3 class="text-5xl font-black text-main">@(_loyaltyAccount?.CurrentBalance ?? 0)</h3>
26
+ <span class="text-xs font-bold text-primary">POINTS</span>
27
+ </div>
28
+ <div class="w-full bg-body h-1.5 rounded-full overflow-hidden">
29
+ <div class="bg-primary h-full" style="width: 45%"></div>
30
+ </div>
31
+ </div>
32
 
33
+ <!-- Active Loans -->
34
+ <div class="bg-card p-8 rounded-3xl border border-border shadow-sm hover:shadow-md transition-all">
35
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-4">Current Loans</p>
36
+ <h3 class="text-5xl font-black text-main mb-4">2</h3>
37
+ <div class="flex gap-2">
38
+ <span class="px-3 py-1 bg-green-500/10 text-green-600 text-[10px] font-black uppercase rounded-lg">Healthy</span>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Subscription -->
43
+ <div class="bg-card p-8 rounded-3xl border border-border shadow-sm hover:shadow-md transition-all">
44
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-4">Membership</p>
45
+ <h3 class="text-2xl font-black text-main mb-1 uppercase">Premium Gold</h3>
46
+ <p class="text-xs text-muted mb-6">Active Status</p>
47
+ <button @onclick='() => Navigation.NavigateTo("membership")' class="text-xs font-black text-primary uppercase tracking-widest hover:underline">Manage Plan</button>
48
+ </div>
49
+ </div>
50
+
51
+ <section>
52
+ <h2 class="text-xl font-bold mb-6 text-main">Recommended for You</h2>
53
+ <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-6">
54
+ @for (int i = 0; i < 5; i++)
55
+ {
56
+ <div class="bg-card aspect-[2/3] rounded-2xl border border-border overflow-hidden animate-pulse"></div>
57
+ }
58
+ </div>
59
+ </section>
60
+ </div>
61
+ </Authorized>
62
+
63
+ <NotAuthorized>
64
+ <!-- Guest Landing Page -->
65
+ <div class="space-y-24 animate-fade py-12">
66
+ <!-- Hero Section -->
67
+ <section class="text-center">
68
+ <h1 class="text-7xl font-black tracking-tighter text-main mb-8 leading-tight">
69
+ Where Every Page <br /> <span class="text-primary italic">Finds a Purpose.</span>
70
+ </h1>
71
+ <p class="text-xl text-muted max-w-3xl mx-auto leading-relaxed mb-12">
72
+ Join over 5,000 readers in a digital-first library experience. Access a global collection of literature, earn rewards for reading, and connect with fellow book lovers.
73
+ </p>
74
+ <div class="flex items-center justify-center gap-6">
75
+ <button @onclick='() => Navigation.NavigateTo("register")' class="bg-primary text-white px-10 py-5 rounded-2xl font-black text-lg hover:bg-primary-hover shadow-xl shadow-primary/20 transition-all transform hover:scale-105">
76
+ Start Reading Now
77
+ </button>
78
+ <button @onclick='() => Navigation.NavigateTo("books")' class="bg-card border border-border text-main px-10 py-5 rounded-2xl font-black text-lg hover:bg-body transition-all">
79
+ Explore Catalog
80
+ </button>
81
+ </div>
82
+ </section>
83
+
84
+ <!-- Stats Bar -->
85
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-12 border-y border-border py-16">
86
+ <div class="text-center">
87
+ <h3 class="text-5xl font-black text-main mb-2">12,000+</h3>
88
+ <p class="text-muted font-bold uppercase tracking-widest text-xs">Verified Books</p>
89
+ </div>
90
+ <div class="text-center border-x border-border">
91
+ <h3 class="text-5xl font-black text-main mb-2">5.2k</h3>
92
+ <p class="text-muted font-bold uppercase tracking-widest text-xs">Active Readers</p>
93
+ </div>
94
+ <div class="text-center">
95
+ <h3 class="text-5xl font-black text-main mb-2">150+</h3>
96
+ <p class="text-muted font-bold uppercase tracking-widest text-xs">Daily Borrowings</p>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- Features -->
101
+ <section class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
102
+ <div class="space-y-8">
103
+ <h2 class="text-4xl font-black text-main tracking-tight">Experience Library <br /> Management redefined.</h2>
104
+ <div class="space-y-6">
105
+ <div class="flex gap-4">
106
+ <div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center text-primary flex-shrink-0">
107
+ <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.5" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"></path></svg>
108
+ </div>
109
+ <div>
110
+ <h4 class="font-bold text-main">Loyalty Reward System</h4>
111
+ <p class="text-sm text-muted">Earn points for every book you return on time and exchange them for membership perks.</p>
112
+ </div>
113
+ </div>
114
+ <div class="flex gap-4">
115
+ <div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center text-primary flex-shrink-0">
116
+ <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.5" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
117
+ </div>
118
+ <div>
119
+ <h4 class="font-bold text-main">Instant Notifications</h4>
120
+ <p class="text-sm text-muted">Get real-time updates when your favorite books are back in stock or when your loan is due.</p>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ <div class="bg-card aspect-video rounded-[3rem] border border-border shadow-2xl flex items-center justify-center relative overflow-hidden">
126
+ <div class="absolute inset-0 bg-primary opacity-5"></div>
127
+ <span class="text-muted font-black uppercase tracking-[0.3em] text-xs">Preview of Interface</span>
128
+ </div>
129
+ </section>
130
+ </div>
131
+ </NotAuthorized>
132
+ </AuthorizeView>
133
+
134
+ @code {
135
+ private LoyaltyAccountDto? _loyaltyAccount;
136
+
137
+ protected override async Task OnInitializedAsync()
138
+ {
139
+ try
140
+ {
141
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
142
+ if (authState.User.Identity?.IsAuthenticated == true)
143
+ {
144
+ _loyaltyAccount = await ApiClient.GetMyLoyaltyAccountAsync();
145
+ }
146
+ }
147
+ catch { }
148
+ }
149
+ }
BlazorWebAssembly/Pages/Login.razor ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/login"
2
+ @using BlazorWebAssembly.Services
3
+ @using LibraryManagement.Shared.Models
4
+ @inject AuthService AuthService
5
+ @inject NavigationManager Navigation
6
+
7
+ <div class="min-h-[80vh] flex items-center justify-center py-12 px-6">
8
+ <div class="w-full max-w-md">
9
+ <!-- Brand / Header -->
10
+ <div class="text-center mb-12">
11
+ <div class="inline-flex items-center justify-center w-16 h-16 bg-primary/10 rounded-2xl mb-6">
12
+ <svg class="w-8 h-8 text-primary" 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 18.246 18.477 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
13
+ </div>
14
+ <h1 class="text-3xl font-black text-main mb-2">Welcome Back</h1>
15
+ <p class="text-muted">Enter your credentials to access your library account.</p>
16
+ </div>
17
+
18
+ <!-- Login Form -->
19
+ <div class="bg-card p-10 rounded-[2rem] border border-border shadow-2xl shadow-primary/5">
20
+ <EditForm Model="@_loginModel" OnValidSubmit="HandleLogin" class="space-y-6">
21
+ <DataAnnotationsValidator />
22
+
23
+ <div>
24
+ <label class="block text-xs font-black text-muted uppercase tracking-[0.2em] mb-2">Email Address</label>
25
+ <InputText @bind-Value="_loginModel.Email"
26
+ class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
27
+ placeholder="name@example.com" />
28
+ </div>
29
+
30
+ <div>
31
+ <label class="block text-xs font-black text-muted uppercase tracking-[0.2em] mb-2">Password</label>
32
+ <InputText @bind-Value="_loginModel.Password" type="password"
33
+ class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
34
+ placeholder="••••••••" />
35
+ </div>
36
+
37
+ @if (!string.IsNullOrEmpty(_errorMessage))
38
+ {
39
+ <div class="p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-500 text-sm font-bold text-center">
40
+ @_errorMessage
41
+ </div>
42
+ }
43
+
44
+ <button type="submit" disabled="@_isLoading"
45
+ class='w-full py-4 rounded-xl font-black text-white bg-primary hover:bg-primary-hover transition-all transform hover:scale-[1.02] active:scale-95 shadow-lg shadow-primary/20 flex items-center justify-center gap-2'>
46
+ @if (_isLoading)
47
+ {
48
+ <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
49
+ <span>Authenticating...</span>
50
+ }
51
+ else
52
+ {
53
+ <span>Sign In</span>
54
+ }
55
+ </button>
56
+ </EditForm>
57
+ </div>
58
+
59
+ <!-- Footer -->
60
+ <p class="text-center mt-10 text-muted font-medium">
61
+ Don't have an account?
62
+ <button @onclick='() => Navigation.NavigateTo("register")' class="text-primary font-bold hover:underline">Create Account</button>
63
+ </p>
64
+ </div>
65
+ </div>
66
+
67
+ @code {
68
+ private LoginRequest _loginModel = new();
69
+ private bool _isLoading = false;
70
+ private string? _errorMessage;
71
+
72
+ private async Task HandleLogin()
73
+ {
74
+ _isLoading = true;
75
+ _errorMessage = null;
76
+ try
77
+ {
78
+ var success = await AuthService.LoginAsync(_loginModel);
79
+ if (success)
80
+ {
81
+ Navigation.NavigateTo("/");
82
+ }
83
+ else
84
+ {
85
+ _errorMessage = "Invalid email or password. Please try again.";
86
+ }
87
+ }
88
+ catch (Exception ex)
89
+ {
90
+ _errorMessage = "A connection error occurred. Please check your network.";
91
+ }
92
+ finally
93
+ {
94
+ _isLoading = false;
95
+ }
96
+ }
97
+ }
BlazorWebAssembly/Pages/Membership.razor ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/membership"
2
+ @using LibraryManagement.Shared.Models
3
+ @using BlazorWebAssembly.Services
4
+ @inject LibraryApiClient ApiClient
5
+ @inject NavigationManager Navigation
6
+
7
+ <div class="max-w-6xl mx-auto py-16 px-6">
8
+ <!-- Header -->
9
+ <div class="text-center mb-20">
10
+ <h1 class="text-5xl font-black tracking-tight text-main mb-6">Choose Your Journey</h1>
11
+ <p class="text-xl text-muted max-w-2xl mx-auto leading-relaxed">
12
+ From casual readers to devoted bookworms, we have a plan that perfectly matches your literary appetite.
13
+ </p>
14
+ </div>
15
+
16
+ @if (_isLoading)
17
+ {
18
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
19
+ @for (int i = 0; i < 3; i++)
20
+ {
21
+ <div class="animate-pulse bg-card rounded-3xl border border-border h-[500px]"></div>
22
+ }
23
+ </div>
24
+ }
25
+ else
26
+ {
27
+ <!-- Current Subscription Banner -->
28
+ @if (_currentSub != null)
29
+ {
30
+ <div class="bg-primary/5 border border-primary/20 rounded-3xl p-8 mb-16 flex flex-col md:flex-row items-center justify-between gap-6">
31
+ <div class="flex items-center gap-6">
32
+ <div class="w-16 h-16 bg-primary rounded-2xl flex items-center justify-center text-white text-3xl font-bold">
33
+ P
34
+ </div>
35
+ <div>
36
+ <h3 class="text-2xl font-bold text-main">@_currentSub.MembershipType Plan</h3>
37
+ <p class="text-muted">Active until @_currentSub.ExpiryDate.ToString("MMMM dd, yyyy")</p>
38
+ </div>
39
+ </div>
40
+ <div class="flex items-center gap-4">
41
+ <span class="px-4 py-1.5 bg-green-500/10 text-green-600 rounded-full text-xs font-bold uppercase tracking-widest">Active</span>
42
+ </div>
43
+ </div>
44
+ }
45
+
46
+ <!-- Membership Plans -->
47
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
48
+ @foreach (var plan in _memberships)
49
+ {
50
+ <div class='relative bg-card rounded-[2rem] border p-10 flex flex-col transition-all duration-500 hover:-translate-y-2 @(IsCurrentPlan(plan.Id) ? "border-primary shadow-xl shadow-primary/5 scale-105 z-10" : "border-border shadow-sm")'>
51
+ @if (IsCurrentPlan(plan.Id))
52
+ {
53
+ <div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-primary text-white px-6 py-1 rounded-full text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-primary/30">
54
+ Current Plan
55
+ </div>
56
+ }
57
+
58
+ <div class="mb-10">
59
+ <h3 class="text-sm font-black text-muted uppercase tracking-[0.2em] mb-4">@plan.Type</h3>
60
+ <div class="flex items-baseline gap-1">
61
+ <span class="text-4xl font-black text-main">$@plan.Price</span>
62
+ <span class="text-muted font-medium">/@plan.DurationMonths mo</span>
63
+ </div>
64
+ </div>
65
+
66
+ <div class="space-y-6 mb-12 flex-grow">
67
+ <div class="flex items-center gap-4">
68
+ <div class="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center">
69
+ <svg class="w-3 h-3 text-primary" 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>
70
+ </div>
71
+ <span class="text-main font-medium">Borrow up to <span class="font-bold">@plan.MaxBooks</span> books</span>
72
+ </div>
73
+ <div class="flex items-center gap-4">
74
+ <div class="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center">
75
+ <svg class="w-3 h-3 text-primary" 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>
76
+ </div>
77
+ <span class="text-main font-medium">Keep for <span class="font-bold">@plan.BorrowingDays</span> days</span>
78
+ </div>
79
+ <div class="flex items-center gap-4">
80
+ <div class="w-5 h-5 rounded-full bg-primary/10 flex items-center justify-center">
81
+ <svg class="w-3 h-3 text-primary" 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>
82
+ </div>
83
+ <span class="text-main font-medium">Digital access included</span>
84
+ </div>
85
+ </div>
86
+
87
+ <button @onclick="() => SubscribeAsync(plan)"
88
+ disabled="@IsCurrentPlan(plan.Id)"
89
+ class='w-full py-5 rounded-2xl font-black transition-all @(IsCurrentPlan(plan.Id) ? "bg-body text-muted border border-border" : "bg-primary text-white hover:bg-primary-hover hover:shadow-xl shadow-primary/20")'>
90
+ @(IsCurrentPlan(plan.Id) ? "Your Plan" : "Upgrade Now")
91
+ </button>
92
+ </div>
93
+ }
94
+ </div>
95
+ }
96
+ </div>
97
+
98
+ @code {
99
+ private bool _isLoading = true;
100
+ private List<MembershipDto> _memberships = new();
101
+ private SubscriptionDto? _currentSub;
102
+
103
+ protected override async Task OnInitializedAsync()
104
+ {
105
+ await LoadData();
106
+ }
107
+
108
+ private async Task LoadData()
109
+ {
110
+ _isLoading = true;
111
+ try
112
+ {
113
+ _memberships = (await ApiClient.GetMembershipsAsync()).ToList();
114
+ _currentSub = await ApiClient.GetMySubscriptionAsync();
115
+ }
116
+ catch { }
117
+ finally { _isLoading = false; }
118
+ }
119
+
120
+ private bool IsCurrentPlan(int id) => _currentSub?.MembershipId == id;
121
+
122
+ private async Task SubscribeAsync(MembershipDto plan)
123
+ {
124
+ var result = await ApiClient.SubscribeAsync(new SubscribeRequest { MembershipId = plan.Id });
125
+ if (result != null)
126
+ {
127
+ await LoadData();
128
+ }
129
+ }
130
+ }
BlazorWebAssembly/Pages/Register.razor ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/register"
2
+ @using BlazorWebAssembly.Services
3
+ @using LibraryManagement.Shared.Models
4
+ @inject AuthService AuthService
5
+ @inject NavigationManager Navigation
6
+
7
+ <div class="min-h-[90vh] flex items-center justify-center py-12 px-6">
8
+ <div class="w-full max-w-lg">
9
+ <!-- Brand / Header -->
10
+ <div class="text-center mb-12">
11
+ <h1 class="text-4xl font-black text-main mb-3 tracking-tight">Create Account</h1>
12
+ <p class="text-muted text-lg">Join our community and start your reading journey today.</p>
13
+ </div>
14
+
15
+ <!-- Register Form -->
16
+ <div class="bg-card p-10 rounded-[2.5rem] border border-border shadow-2xl shadow-primary/5">
17
+ <EditForm Model="@_registerModel" OnValidSubmit="HandleRegister" class="space-y-6">
18
+ <DataAnnotationsValidator />
19
+
20
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
21
+ <div class="md:col-span-2">
22
+ <label class="block text-xs font-black text-muted uppercase tracking-[0.2em] mb-2">Full Name</label>
23
+ <InputText @bind-Value="_registerModel.FullName"
24
+ class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
25
+ placeholder="John Doe" />
26
+ </div>
27
+
28
+ <div>
29
+ <label class="block text-xs font-black text-muted uppercase tracking-[0.2em] mb-2">Email Address</label>
30
+ <InputText @bind-Value="_registerModel.Email"
31
+ class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
32
+ placeholder="john@example.com" />
33
+ </div>
34
+
35
+ <div>
36
+ <label class="block text-xs font-black text-muted uppercase tracking-[0.2em] mb-2">Phone Number</label>
37
+ <InputText @bind-Value="_registerModel.PhoneNumber"
38
+ class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
39
+ placeholder="+1 234 567 890" />
40
+ </div>
41
+
42
+ <div class="md:col-span-2">
43
+ <label class="block text-xs font-black text-muted uppercase tracking-[0.2em] mb-2">Password</label>
44
+ <InputText @bind-Value="_registerModel.Password" type="password"
45
+ class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none transition-all"
46
+ placeholder="Minimum 6 characters" />
47
+ </div>
48
+ </div>
49
+
50
+ @if (!string.IsNullOrEmpty(_errorMessage))
51
+ {
52
+ <div class="p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-500 text-sm font-bold text-center">
53
+ @_errorMessage
54
+ </div>
55
+ }
56
+
57
+ <button type="submit" disabled="@_isLoading"
58
+ class='w-full py-5 mt-4 rounded-xl font-black text-white bg-primary hover:bg-primary-hover transition-all transform hover:scale-[1.01] active:scale-95 shadow-xl shadow-primary/20 flex items-center justify-center gap-3'>
59
+ @if (_isLoading)
60
+ {
61
+ <div class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
62
+ <span>Creating Account...</span>
63
+ }
64
+ else
65
+ {
66
+ <span>Get Started</span>
67
+ }
68
+ </button>
69
+ </EditForm>
70
+ </div>
71
+
72
+ <!-- Footer -->
73
+ <p class="text-center mt-10 text-muted font-medium">
74
+ Already have an account?
75
+ <button @onclick='() => Navigation.NavigateTo("login")' class="text-primary font-bold hover:underline">Sign In</button>
76
+ </p>
77
+ </div>
78
+ </div>
79
+
80
+ @code {
81
+ private RegisterRequest _registerModel = new();
82
+ private bool _isLoading = false;
83
+ private string? _errorMessage;
84
+
85
+ private async Task HandleRegister()
86
+ {
87
+ _isLoading = true;
88
+ _errorMessage = null;
89
+ try
90
+ {
91
+ var success = await AuthService.RegisterAsync(_registerModel);
92
+ if (success)
93
+ {
94
+ Navigation.NavigateTo("/");
95
+ }
96
+ else
97
+ {
98
+ _errorMessage = "Registration failed. This email may already be in use.";
99
+ }
100
+ }
101
+ catch (Exception ex)
102
+ {
103
+ _errorMessage = "An unexpected error occurred. Please try again later.";
104
+ }
105
+ finally
106
+ {
107
+ _isLoading = false;
108
+ }
109
+ }
110
+ }
BlazorWebAssembly/Pages/Rewards.razor ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/rewards"
2
+ @using LibraryManagement.Shared.Models
3
+ @using BlazorWebAssembly.Services
4
+ @inject LibraryApiClient ApiClient
5
+ @inject NavigationManager Navigation
6
+
7
+ <div class="max-w-6xl mx-auto py-12 px-6">
8
+ <!-- Header -->
9
+ <div class="text-center mb-16">
10
+ <h1 class="text-5xl font-black tracking-tight text-main mb-4">Loyalty Rewards</h1>
11
+ <p class="text-lg text-muted max-w-2xl mx-auto">
12
+ Your points are your power. Exchange them for exclusive memberships and reading perks.
13
+ </p>
14
+ </div>
15
+
16
+ @if (_isLoading)
17
+ {
18
+ <div class="flex justify-center py-20">
19
+ <div class="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
20
+ </div>
21
+ }
22
+ else
23
+ {
24
+ <!-- Account Status Card -->
25
+ <div class="bg-card rounded-2xl border border-border p-10 mb-16 flex flex-col md:flex-row items-center justify-between gap-8 shadow-sm">
26
+ <div>
27
+ <p class="text-sm font-bold text-muted uppercase tracking-widest mb-1">Your Balance</p>
28
+ <div class="flex items-baseline gap-2">
29
+ <h2 class="text-6xl font-black text-main">@(_account?.CurrentBalance ?? 0)</h2>
30
+ <span class="text-xl font-bold text-primary">POINTS</span>
31
+ </div>
32
+ </div>
33
+ <div class="h-16 w-px bg-border hidden md:block"></div>
34
+ <div class="text-center md:text-left">
35
+ <p class="text-sm font-medium text-muted mb-2">Member Tier</p>
36
+ <p class="text-xl font-bold text-main">@(_account?.Tier ?? "Member")</p>
37
+ </div>
38
+ <button class="bg-primary text-white px-10 py-4 rounded-xl font-black hover:bg-primary-hover transition-all transform hover:scale-105 shadow-lg shadow-primary/20">
39
+ How it works
40
+ </button>
41
+ </div>
42
+
43
+ <!-- Rewards Grid -->
44
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
45
+ @foreach (var reward in _rewards)
46
+ {
47
+ <div class="bg-card rounded-2xl border border-border p-8 flex flex-col hover:border-primary transition-all group">
48
+ <div class="flex justify-between items-start mb-6">
49
+ <div class="w-14 h-14 bg-body rounded-xl flex items-center justify-center border border-border group-hover:bg-primary/5 transition-colors">
50
+ <svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"></path></svg>
51
+ </div>
52
+ <span class="bg-primary/10 text-primary px-4 py-1 rounded-full text-sm font-black">@reward.PointCost pts</span>
53
+ </div>
54
+
55
+ <h3 class="text-2xl font-bold text-main mb-2">@reward.Name</h3>
56
+ <p class="text-muted leading-relaxed mb-8 flex-grow">@reward.Description</p>
57
+
58
+ <button @onclick="() => ClaimAsync(reward)"
59
+ disabled="@((_account?.CurrentBalance ?? 0) < reward.PointCost)"
60
+ class='w-full py-4 rounded-xl font-bold transition-all @((_account?.CurrentBalance ?? 0) >= reward.PointCost ? "bg-main text-bg-body hover:bg-primary" : "bg-body text-muted cursor-not-allowed border border-border")'>
61
+ @((_account?.CurrentBalance ?? 0) >= reward.PointCost ? "Claim Reward" : "Insufficient Points")
62
+ </button>
63
+ </div>
64
+ }
65
+ </div>
66
+ }
67
+ </div>
68
+
69
+ @code {
70
+ private bool _isLoading = true;
71
+ private LoyaltyAccountDto? _account;
72
+ private List<LoyaltyRewardDto> _rewards = new();
73
+
74
+ protected override async Task OnInitializedAsync()
75
+ {
76
+ await LoadData();
77
+ }
78
+
79
+ private async Task LoadData()
80
+ {
81
+ _isLoading = true;
82
+ try
83
+ {
84
+ _account = await ApiClient.GetMyLoyaltyAccountAsync();
85
+ _rewards = (await ApiClient.GetActiveRewardsAsync()).ToList();
86
+ }
87
+ catch { }
88
+ finally { _isLoading = false; }
89
+ }
90
+
91
+ private async Task ClaimAsync(LoyaltyRewardDto reward)
92
+ {
93
+ var (success, msg) = await ApiClient.ClaimRewardAsync(reward.Id);
94
+ if (success)
95
+ {
96
+ await LoadData();
97
+ // Show success message
98
+ }
99
+ }
100
+ }
BlazorWebAssembly/Pages/UserInsights.razor ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/user-insights"
2
+ @using LibraryManagement.Shared.Models
3
+ @using BlazorWebAssembly.Services
4
+ @inject LibraryApiClient ApiClient
5
+ @inject NavigationManager Navigation
6
+ @using Microsoft.AspNetCore.Authorization
7
+ @attribute [Authorize(Roles = "Librarian")]
8
+
9
+ <div class="max-w-7xl mx-auto py-12 px-6">
10
+ <!-- Header -->
11
+ <div class="flex flex-col md:flex-row justify-between items-start md:items-end mb-16 gap-6">
12
+ <div>
13
+ <h1 class="text-5xl font-black tracking-tight text-main mb-4">User Insights</h1>
14
+ <p class="text-lg text-muted max-w-xl">
15
+ Analytics and data-driven visibility into library membership and reading patterns.
16
+ </p>
17
+ </div>
18
+ <div class="flex gap-4">
19
+ <div class="bg-card border border-border px-6 py-3 rounded-2xl flex flex-col">
20
+ <span class="text-[10px] font-black text-muted uppercase tracking-[0.2em]">Total Members</span>
21
+ <span class="text-2xl font-black text-main">1,284</span>
22
+ </div>
23
+ <div class="bg-card border border-border px-6 py-3 rounded-2xl flex flex-col text-primary border-primary/20">
24
+ <span class="text-[10px] font-black text-primary uppercase tracking-[0.2em]">Active Now</span>
25
+ <span class="text-2xl font-black text-primary">42</span>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Metrics Grid -->
31
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-16">
32
+ <!-- Top Readers -->
33
+ <div class="bg-card rounded-[2rem] border border-border p-10 shadow-sm">
34
+ <h3 class="text-xl font-bold text-main mb-8 border-b border-border pb-4 uppercase tracking-widest text-xs">Top Readers</h3>
35
+ <div class="space-y-8">
36
+ @for (int i = 1; i <= 5; i++)
37
+ {
38
+ <div class="flex items-center gap-4">
39
+ <span class="w-8 h-8 rounded-lg bg-body flex items-center justify-center font-black text-muted text-xs">@i</span>
40
+ <div class="flex-grow">
41
+ <p class="font-bold text-main text-sm">Member User @i</p>
42
+ <p class="text-[10px] text-muted font-medium uppercase tracking-widest">@(20 - i) Books Borrowed</p>
43
+ </div>
44
+ <div class="w-12 bg-body h-1 rounded-full overflow-hidden">
45
+ <div class="bg-primary h-full" style="width: @(100 - (i * 15))%"></div>
46
+ </div>
47
+ </div>
48
+ }
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Loyalty Leaders -->
53
+ <div class="bg-card rounded-[2rem] border border-border p-10 shadow-sm">
54
+ <h3 class="text-xl font-bold text-main mb-8 border-b border-border pb-4 uppercase tracking-widest text-xs">Point Leaders</h3>
55
+ <div class="space-y-8">
56
+ @for (int i = 1; i <= 5; i++)
57
+ {
58
+ <div class="flex items-center gap-4">
59
+ <div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary font-black text-xs">
60
+ L
61
+ </div>
62
+ <div class="flex-grow">
63
+ <p class="font-bold text-main text-sm">User Alpha @i</p>
64
+ <p class="text-[10px] text-muted font-medium uppercase tracking-widest">@(5000 - (i * 500)) Points</p>
65
+ </div>
66
+ <span class="text-xs font-black text-primary">TIER @(6-i)</span>
67
+ </div>
68
+ }
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Membership Distribution -->
73
+ <div class="bg-card rounded-[2rem] border border-border p-10 shadow-sm">
74
+ <h3 class="text-xl font-bold text-main mb-8 border-b border-border pb-4 uppercase tracking-widest text-xs">Growth Patterns</h3>
75
+ <div class="space-y-6">
76
+ <div class="flex justify-between items-end gap-2 h-40">
77
+ <div class="bg-primary/20 w-full rounded-t-xl" style="height: 40%"></div>
78
+ <div class="bg-primary/40 w-full rounded-t-xl" style="height: 60%"></div>
79
+ <div class="bg-primary/60 w-full rounded-t-xl" style="height: 30%"></div>
80
+ <div class="bg-primary/80 w-full rounded-t-xl" style="height: 90%"></div>
81
+ <div class="bg-primary w-full rounded-t-xl" style="height: 100%"></div>
82
+ </div>
83
+ <div class="flex justify-between text-[10px] font-black text-muted uppercase tracking-[0.2em] px-2">
84
+ <span>Mon</span>
85
+ <span>Tue</span>
86
+ <span>Wed</span>
87
+ <span>Thu</span>
88
+ <span>Fri</span>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ @code {
96
+ protected override async Task OnInitializedAsync()
97
+ {
98
+ // Data loading logic here
99
+ }
100
+ }
BlazorWebAssembly/Pages/Weather.razor DELETED
@@ -1,57 +0,0 @@
1
- @page "/weather"
2
- @inject HttpClient Http
3
-
4
- <PageTitle>Weather</PageTitle>
5
-
6
- <h1>Weather</h1>
7
-
8
- <p>This component demonstrates fetching data from the server.</p>
9
-
10
- @if (forecasts == null)
11
- {
12
- <p><em>Loading...</em></p>
13
- }
14
- else
15
- {
16
- <table class="table">
17
- <thead>
18
- <tr>
19
- <th>Date</th>
20
- <th>Temp. (C)</th>
21
- <th>Temp. (F)</th>
22
- <th>Summary</th>
23
- </tr>
24
- </thead>
25
- <tbody>
26
- @foreach (var forecast in forecasts)
27
- {
28
- <tr>
29
- <td>@forecast.Date.ToShortDateString()</td>
30
- <td>@forecast.TemperatureC</td>
31
- <td>@forecast.TemperatureF</td>
32
- <td>@forecast.Summary</td>
33
- </tr>
34
- }
35
- </tbody>
36
- </table>
37
- }
38
-
39
- @code {
40
- private WeatherForecast[]? forecasts;
41
-
42
- protected override async Task OnInitializedAsync()
43
- {
44
- forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
45
- }
46
-
47
- public class WeatherForecast
48
- {
49
- public DateOnly Date { get; set; }
50
-
51
- public int TemperatureC { get; set; }
52
-
53
- public string? Summary { get; set; }
54
-
55
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
56
- }
57
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
BlazorWebAssembly/Pages/Wishlist.razor ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/wishlist"
2
+ @using LibraryManagement.Shared.Models
3
+ @using BlazorWebAssembly.Services
4
+ @inject LibraryApiClient ApiClient
5
+ @inject NavigationManager Navigation
6
+
7
+ <div class="max-w-6xl mx-auto py-12 px-6">
8
+ <!-- Header -->
9
+ <div class="text-center mb-16">
10
+ <h1 class="text-5xl font-black tracking-tight text-main mb-4">My Wishlist</h1>
11
+ <p class="text-lg text-muted max-w-xl mx-auto">
12
+ A curated collection of books you're waiting for. We'll notify you as soon as they're back on the shelf.
13
+ </p>
14
+ </div>
15
+
16
+ @if (_isLoading)
17
+ {
18
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
19
+ @for (int i = 0; i < 3; i++)
20
+ {
21
+ <div class="animate-pulse bg-card rounded-2xl border border-border h-48"></div>
22
+ }
23
+ </div>
24
+ }
25
+ else if (!_wishlistItems.Any())
26
+ {
27
+ <div class="text-center py-24 bg-card rounded-[2.5rem] border border-dashed border-border">
28
+ <h3 class="text-2xl font-bold text-main mb-4">Your wishlist is empty</h3>
29
+ <p class="text-muted mb-8">Found a book that's currently borrowed? Add it to your wishlist!</p>
30
+ <button @onclick='() => Navigation.NavigateTo("books")' class="bg-primary text-white px-10 py-4 rounded-xl font-black hover:bg-primary-hover transition-all shadow-lg shadow-primary/20">
31
+ Browse Books
32
+ </button>
33
+ </div>
34
+ }
35
+ else
36
+ {
37
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
38
+ @foreach (var book in _wishlistItems)
39
+ {
40
+ <div class="bg-card rounded-2xl border border-border p-6 flex items-center gap-6 group hover:border-primary transition-all">
41
+ <div class="w-20 h-28 bg-body rounded-lg overflow-hidden flex-shrink-0 border border-border flex items-center justify-center">
42
+ @if (!string.IsNullOrEmpty(book.CoverUrl))
43
+ {
44
+ <img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover" />
45
+ }
46
+ else
47
+ {
48
+ <svg class="w-8 h-8 text-muted opacity-20" 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 18.246 18.477 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
49
+ }
50
+ </div>
51
+ <div class="flex-grow">
52
+ <h3 class="font-bold text-main line-clamp-1 group-hover:text-primary transition-colors">@book.Title</h3>
53
+ <p class="text-xs text-muted mb-4 font-medium">@book.Author</p>
54
+
55
+ <div class="flex items-center justify-between mt-auto">
56
+ <span class='px-2 py-0.5 rounded text-[9px] font-black uppercase tracking-widest @(book.AvailableCopies > 0 ? "bg-green-500/10 text-green-600" : "bg-orange-500/10 text-orange-600")'>
57
+ @(book.AvailableCopies > 0 ? "Available" : "Waiting")
58
+ </span>
59
+ <button class="text-xs font-bold text-red-400 hover:text-red-500">Remove</button>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ }
64
+ </div>
65
+ }
66
+ </div>
67
+
68
+ @code {
69
+ private bool _isLoading = false;
70
+ private List<BookDto> _wishlistItems = new();
71
+
72
+ protected override async Task OnInitializedAsync()
73
+ {
74
+ // Mocking for now, would use a real service
75
+ _isLoading = true;
76
+ await Task.Delay(500);
77
+ _isLoading = false;
78
+ }
79
+ }
BlazorWebAssembly/Program.cs CHANGED
@@ -1,4 +1,8 @@
1
  using BlazorWebAssembly;
 
 
 
 
2
  using Microsoft.AspNetCore.Components.Web;
3
  using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
4
 
@@ -6,6 +10,26 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args);
6
  builder.RootComponents.Add<App>("#app");
7
  builder.RootComponents.Add<HeadOutlet>("head::after");
8
 
9
- builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  await builder.Build().RunAsync();
 
1
  using BlazorWebAssembly;
2
+ using BlazorWebAssembly.Providers;
3
+ using BlazorWebAssembly.Services;
4
+ using Blazored.LocalStorage;
5
+ using Microsoft.AspNetCore.Components.Authorization;
6
  using Microsoft.AspNetCore.Components.Web;
7
  using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
8
 
 
10
  builder.RootComponents.Add<App>("#app");
11
  builder.RootComponents.Add<HeadOutlet>("head::after");
12
 
13
+ // Add HttpClient with Backend address
14
+ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://localhost:7028/") });
15
+
16
+ // Add Blazored LocalStorage
17
+ builder.Services.AddBlazoredLocalStorage();
18
+
19
+ // Add Authorization Core
20
+ builder.Services.AddAuthorizationCore();
21
+
22
+ // Register Custom AuthStateProvider
23
+ builder.Services.AddScoped<JwtAuthenticationStateProvider>();
24
+ builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<JwtAuthenticationStateProvider>());
25
+
26
+ // Register AuthService
27
+ builder.Services.AddScoped<AuthService>();
28
+
29
+ // Register ThemeService
30
+ builder.Services.AddScoped<ThemeService>();
31
+
32
+ // Register LibraryApiClient
33
+ builder.Services.AddScoped<LibraryApiClient>();
34
 
35
  await builder.Build().RunAsync();
BlazorWebAssembly/Providers/JwtAuthenticationStateProvider.cs ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Net.Http.Headers;
2
+ using System.Security.Claims;
3
+ using System.Text.Json;
4
+ using Blazored.LocalStorage;
5
+ using Microsoft.AspNetCore.Components.Authorization;
6
+
7
+ namespace BlazorWebAssembly.Providers
8
+ {
9
+ public class JwtAuthenticationStateProvider : AuthenticationStateProvider
10
+ {
11
+ private readonly ILocalStorageService _localStorage;
12
+ private readonly HttpClient _httpClient;
13
+ private readonly AuthenticationState _anonymous;
14
+
15
+ public JwtAuthenticationStateProvider(ILocalStorageService localStorage, HttpClient httpClient)
16
+ {
17
+ _localStorage = localStorage;
18
+ _httpClient = httpClient;
19
+ _anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
20
+ }
21
+
22
+ public override async Task<AuthenticationState> GetAuthenticationStateAsync()
23
+ {
24
+ var token = await _localStorage.GetItemAsync<string>("authToken");
25
+
26
+ if (string.IsNullOrWhiteSpace(token))
27
+ return _anonymous;
28
+
29
+ _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
30
+
31
+ return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt")));
32
+ }
33
+
34
+ public void NotifyUserAuthentication(string token)
35
+ {
36
+ var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
37
+ var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
38
+ NotifyAuthenticationStateChanged(authState);
39
+ }
40
+
41
+ public void NotifyUserLogout()
42
+ {
43
+ var authState = Task.FromResult(_anonymous);
44
+ NotifyAuthenticationStateChanged(authState);
45
+ }
46
+
47
+ private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
48
+ {
49
+ var claims = new List<Claim>();
50
+ var payload = jwt.Split('.')[1];
51
+
52
+ var jsonBytes = ParseBase64WithoutPadding(payload);
53
+
54
+ var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
55
+
56
+ if (keyValuePairs != null)
57
+ {
58
+ foreach (var kvp in keyValuePairs)
59
+ {
60
+ if (kvp.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
61
+ {
62
+ foreach (var item in element.EnumerateArray())
63
+ {
64
+ claims.Add(new Claim(kvp.Key, item.ToString()));
65
+ }
66
+ }
67
+ else
68
+ {
69
+ claims.Add(new Claim(kvp.Key, kvp.Value.ToString() ?? ""));
70
+ }
71
+ }
72
+ }
73
+
74
+ return claims;
75
+ }
76
+
77
+ private byte[] ParseBase64WithoutPadding(string base64)
78
+ {
79
+ switch (base64.Length % 4)
80
+ {
81
+ case 2: base64 += "=="; break;
82
+ case 3: base64 += "="; break;
83
+ }
84
+ return Convert.FromBase64String(base64);
85
+ }
86
+ }
87
+ }
BlazorWebAssembly/Services/AuthService.cs ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Net.Http.Json;
2
+ using Blazored.LocalStorage;
3
+ using LibraryManagement.Shared.Models;
4
+ using BlazorWebAssembly.Providers;
5
+ using Microsoft.AspNetCore.Components.Authorization;
6
+
7
+ namespace BlazorWebAssembly.Services
8
+ {
9
+ public class AuthService
10
+ {
11
+ private readonly HttpClient _httpClient;
12
+ private readonly ILocalStorageService _localStorage;
13
+ private readonly AuthenticationStateProvider _authStateProvider;
14
+
15
+ public AuthService(HttpClient httpClient, ILocalStorageService localStorage, AuthenticationStateProvider authStateProvider)
16
+ {
17
+ _httpClient = httpClient;
18
+ _localStorage = localStorage;
19
+ _authStateProvider = authStateProvider;
20
+ }
21
+
22
+ public async Task<bool> LoginAsync(LoginRequest request)
23
+ {
24
+ var response = await _httpClient.PostAsJsonAsync("api/auth/login", request);
25
+
26
+ if (!response.IsSuccessStatusCode)
27
+ return false;
28
+
29
+ var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>();
30
+
31
+ if (authResponse != null)
32
+ {
33
+ await _localStorage.SetItemAsync("authToken", authResponse.Token);
34
+ ((JwtAuthenticationStateProvider)_authStateProvider).NotifyUserAuthentication(authResponse.Token);
35
+ _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", authResponse.Token);
36
+ return true;
37
+ }
38
+
39
+ return false;
40
+ }
41
+
42
+ public async Task<bool> RegisterAsync(RegisterRequest request)
43
+ {
44
+ var response = await _httpClient.PostAsJsonAsync("api/auth/register", request);
45
+
46
+ if (!response.IsSuccessStatusCode)
47
+ return false;
48
+
49
+ var authResponse = await response.Content.ReadFromJsonAsync<AuthResponse>();
50
+ if (authResponse != null)
51
+ {
52
+ // Auto login after registration
53
+ await _localStorage.SetItemAsync("authToken", authResponse.Token);
54
+ ((JwtAuthenticationStateProvider)_authStateProvider).NotifyUserAuthentication(authResponse.Token);
55
+ _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", authResponse.Token);
56
+ return true;
57
+ }
58
+
59
+ return false;
60
+ }
61
+
62
+ public async Task Logout()
63
+ {
64
+ await _localStorage.RemoveItemAsync("authToken");
65
+ ((JwtAuthenticationStateProvider)_authStateProvider).NotifyUserLogout();
66
+ _httpClient.DefaultRequestHeaders.Authorization = null;
67
+ }
68
+ }
69
+ }
BlazorWebAssembly/Services/LibraryApiClient.cs ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System.Net.Http.Json;
2
+ using System.Text.Json;
3
+ using System.Text.Json.Serialization;
4
+ using LibraryManagement.Shared.Models;
5
+
6
+ namespace BlazorWebAssembly.Services
7
+ {
8
+ public class LibraryApiClient
9
+ {
10
+ private readonly HttpClient _httpClient;
11
+
12
+ public LibraryApiClient(HttpClient httpClient)
13
+ {
14
+ _httpClient = httpClient;
15
+ }
16
+
17
+ #region Books
18
+ public async Task<IEnumerable<BookDto>> GetBooksAsync(int? categoryId = null)
19
+ {
20
+ var url = categoryId.HasValue ? $"api/books?categoryId={categoryId}" : "api/books";
21
+ return await _httpClient.GetFromJsonAsync<IEnumerable<BookDto>>(url) ?? Enumerable.Empty<BookDto>();
22
+ }
23
+
24
+ public async Task<BookDto?> GetBookAsync(int id)
25
+ {
26
+ return await _httpClient.GetFromJsonAsync<BookDto>($"api/books/{id}");
27
+ }
28
+
29
+ public async Task<BookDto?> CreateBookAsync(BookCreateRequest request)
30
+ {
31
+ var response = await _httpClient.PostAsJsonAsync("api/books", request);
32
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BookDto>() : null;
33
+ }
34
+
35
+ public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
36
+ {
37
+ var response = await _httpClient.PutAsJsonAsync($"api/books/{id}", request);
38
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BookDto>() : null;
39
+ }
40
+
41
+ public async Task<bool> DeleteBookAsync(int id)
42
+ {
43
+ var response = await _httpClient.DeleteAsync($"api/books/{id}");
44
+ return response.IsSuccessStatusCode;
45
+ }
46
+ #endregion
47
+
48
+ #region Borrowings
49
+ public async Task<BorrowingDto?> BorrowBookAsync(BorrowRequest request)
50
+ {
51
+ var response = await _httpClient.PostAsJsonAsync("api/borrowings/borrow", request);
52
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BorrowingDto>() : null;
53
+ }
54
+
55
+ public async Task<BorrowingDto?> ReturnBookAsync(Guid id)
56
+ {
57
+ var response = await _httpClient.PostAsync($"api/borrowings/return/{id}", null);
58
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BorrowingDto>() : null;
59
+ }
60
+
61
+ public async Task<IEnumerable<BorrowingDto>> GetMyBorrowingsAsync()
62
+ {
63
+ return await _httpClient.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings/me") ?? Enumerable.Empty<BorrowingDto>();
64
+ }
65
+
66
+ public async Task<IEnumerable<BorrowingDto>> GetAllBorrowingsAsync()
67
+ {
68
+ return await _httpClient.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings") ?? Enumerable.Empty<BorrowingDto>();
69
+ }
70
+
71
+ public async Task<bool> RequestReturnAsync(Guid borrowingId)
72
+ {
73
+ var response = await _httpClient.PostAsync($"api/borrowings/return-request/{borrowingId}", null);
74
+ return response.IsSuccessStatusCode;
75
+ }
76
+ #endregion
77
+
78
+ #region Categories
79
+ public async Task<IEnumerable<CategoryDto>> GetCategoriesAsync()
80
+ {
81
+ return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
82
+ }
83
+ #endregion
84
+
85
+ #region Subscriptions
86
+ public async Task<IEnumerable<MembershipDto>> GetMembershipsAsync()
87
+ {
88
+ return await _httpClient.GetFromJsonAsync<IEnumerable<MembershipDto>>("api/memberships") ?? Enumerable.Empty<MembershipDto>();
89
+ }
90
+
91
+ public async Task<SubscriptionDto?> GetMySubscriptionAsync()
92
+ {
93
+ try { return await _httpClient.GetFromJsonAsync<SubscriptionDto>("api/subscriptions/me"); }
94
+ catch { return null; }
95
+ }
96
+
97
+ public async Task<SubscriptionDto?> SubscribeAsync(SubscribeRequest request)
98
+ {
99
+ var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe", request);
100
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
101
+ }
102
+ #endregion
103
+
104
+ #region Loyalty
105
+ public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
106
+ {
107
+ try
108
+ {
109
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
110
+ return await _httpClient.GetFromJsonAsync<LoyaltyAccountDto>("api/Loyalty/my-account", options);
111
+ }
112
+ catch { return null; }
113
+ }
114
+
115
+ public async Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync()
116
+ {
117
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("api/Loyalty/rewards") ?? Enumerable.Empty<LoyaltyRewardDto>();
118
+ }
119
+
120
+ public async Task<(bool Success, string Message)> ClaimRewardAsync(string rewardId, string? notes = null)
121
+ {
122
+ var request = new ClaimRewardRequestDto { RewardId = rewardId, Notes = notes };
123
+ var response = await _httpClient.PostAsJsonAsync("api/Loyalty/claim", request);
124
+
125
+ if (response.IsSuccessStatusCode) return (true, "Claimed successfully!");
126
+ var error = await response.Content.ReadAsStringAsync();
127
+ return (false, error);
128
+ }
129
+ #endregion
130
+ }
131
+ }
BlazorWebAssembly/Services/ThemeService.cs ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using Microsoft.JSInterop;
2
+
3
+ namespace BlazorWebAssembly.Services
4
+ {
5
+ public class ThemeService
6
+ {
7
+ private readonly IJSRuntime _jsRuntime;
8
+ private string _currentTheme = "light";
9
+
10
+ public event Action<string>? OnThemeChanged;
11
+
12
+ public ThemeService(IJSRuntime jsRuntime)
13
+ {
14
+ _jsRuntime = jsRuntime;
15
+ }
16
+
17
+ public string CurrentTheme => _currentTheme;
18
+
19
+ public async Task InitializeAsync()
20
+ {
21
+ var savedTheme = await _jsRuntime.InvokeAsync<string>("localStorage.getItem", "app-theme");
22
+ if (!string.IsNullOrEmpty(savedTheme))
23
+ {
24
+ await SetThemeAsync(savedTheme);
25
+ }
26
+ else
27
+ {
28
+ await SetThemeAsync("light");
29
+ }
30
+ }
31
+
32
+ public async Task SetThemeAsync(string theme)
33
+ {
34
+ _currentTheme = theme;
35
+ await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "app-theme", theme);
36
+ await _jsRuntime.InvokeVoidAsync("document.documentElement.setAttribute", "data-theme", theme);
37
+ OnThemeChanged?.Invoke(theme);
38
+ }
39
+ }
40
+ }
BlazorWebAssembly/_Imports.razor CHANGED
@@ -1,4 +1,4 @@
1
- @using System.Net.Http
2
  @using System.Net.Http.Json
3
  @using Microsoft.AspNetCore.Components.Forms
4
  @using Microsoft.AspNetCore.Components.Routing
@@ -8,3 +8,7 @@
8
  @using Microsoft.JSInterop
9
  @using BlazorWebAssembly
10
  @using BlazorWebAssembly.Layout
 
 
 
 
 
1
+ @using System.Net.Http
2
  @using System.Net.Http.Json
3
  @using Microsoft.AspNetCore.Components.Forms
4
  @using Microsoft.AspNetCore.Components.Routing
 
8
  @using Microsoft.JSInterop
9
  @using BlazorWebAssembly
10
  @using BlazorWebAssembly.Layout
11
+ @using Microsoft.AspNetCore.Components.Authorization
12
+ @using BlazorWebAssembly.Providers
13
+ @using BlazorWebAssembly.Services
14
+ @using LibraryManagement.Shared.Models
BlazorWebAssembly/wwwroot/css/app.css CHANGED
@@ -1,103 +1,77 @@
1
- html, body {
2
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
3
- }
4
-
5
- h1:focus {
6
- outline: none;
7
- }
8
-
9
- a, .btn-link {
10
- color: #0071c1;
11
- }
12
-
13
- .btn-primary {
14
- color: #fff;
15
- background-color: #1b6ec2;
16
- border-color: #1861ac;
17
- }
18
-
19
- .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
20
- box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
21
- }
22
-
23
- .content {
24
- padding-top: 1.1rem;
25
  }
26
 
27
- .valid.modified:not([type=checkbox]) {
28
- outline: 1px solid #26b050;
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
 
31
- .invalid {
32
- outline: 1px solid red;
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
 
35
- .validation-message {
36
- color: red;
 
 
 
 
 
 
 
 
 
 
 
 
37
  }
38
 
39
- #blazor-error-ui {
40
- background: lightyellow;
41
- bottom: 0;
42
- box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
43
- display: none;
44
- left: 0;
45
- padding: 0.6rem 1.25rem 0.7rem 1.25rem;
46
- position: fixed;
47
- width: 100%;
48
- z-index: 1000;
49
- }
50
-
51
- #blazor-error-ui .dismiss {
52
- cursor: pointer;
53
- position: absolute;
54
- right: 0.75rem;
55
- top: 0.5rem;
56
- }
57
-
58
- .blazor-error-boundary {
59
- background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
60
- padding: 1rem 1rem 1rem 3.7rem;
61
- color: white;
62
- }
63
-
64
- .blazor-error-boundary::after {
65
- content: "An error has occurred."
66
- }
67
-
68
- .loading-progress {
69
- position: relative;
70
- display: block;
71
- width: 8rem;
72
- height: 8rem;
73
- margin: 20vh auto 1rem auto;
74
- }
75
-
76
- .loading-progress circle {
77
- fill: none;
78
- stroke: #e0e0e0;
79
- stroke-width: 0.6rem;
80
- transform-origin: 50% 50%;
81
- transform: rotate(-90deg);
82
- }
83
-
84
- .loading-progress circle:last-child {
85
- stroke: #1b6ec2;
86
- stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
87
- transition: stroke-dasharray 0.05s ease-in-out;
88
- }
89
-
90
- .loading-progress-text {
91
- position: absolute;
92
- text-align: center;
93
- font-weight: bold;
94
- inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
95
  }
96
 
97
- .loading-progress-text:after {
98
- content: var(--blazor-load-percentage-text, "Loading");
99
- }
 
 
100
 
101
- code {
102
- color: #c02d76;
 
 
103
  }
 
 
1
+ /* Design System Tokens */
2
+ :root {
3
+ --transition-speed: 0.3s;
4
+ --border-radius: 16px;
5
+ --font-main: 'Outfit', 'Inter', system-ui, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
 
8
+ /* --- LIGHT THEME (Olive + Gold) --- */
9
+ [data-theme='light'] {
10
+ --bg-body: #f7f5ef;
11
+ --bg-card: #ffffff;
12
+ --bg-nav: #f7f5ef;
13
+ --text-main: #3a3d2e;
14
+ --text-muted: #6b705c;
15
+ --primary: #a68a64;
16
+ --primary-hover: #8e7555;
17
+ --accent: #6b705c;
18
+ --border: #d6c7a1;
19
+ --shadow: 0 4px 6px -1px rgb(58 61 46 / 0.1);
20
+ --glass-border: rgba(214, 199, 161, 0.5);
21
+ --text-on-primary: #ffffff;
22
  }
23
 
24
+ /* --- DARK THEME (Mute Purple Stone) --- */
25
+ [data-theme='dark'] {
26
+ --bg-body: #2d2838;
27
+ --bg-card: #373245;
28
+ --bg-nav: #1e1b26;
29
+ --text-main: #f8f7fc;
30
+ --text-muted: #b8a9d9;
31
+ --primary: #b8a9d9;
32
+ --primary-hover: #e9e4f5;
33
+ --accent: #7c6a9b;
34
+ --border: #4a4458;
35
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
36
+ --glass-border: rgba(184, 169, 217, 0.1);
37
+ --text-on-primary: #2d2838;
38
  }
39
 
40
+ /* --- EMERALD THEME (Slate + Mint) --- */
41
+ [data-theme='emerald'] {
42
+ --bg-body: #f5f7f8;
43
+ --bg-card: #ffffff;
44
+ --bg-nav: #2b3a42;
45
+ --text-main: #2b3a42;
46
+ --text-muted: #5c8374;
47
+ --primary: #8fb9a8;
48
+ --primary-hover: #7aa594;
49
+ --accent: #5c8374;
50
+ --border: #dce3e5;
51
+ --shadow: 0 4px 6px -1px rgb(43 58 66 / 0.1);
52
+ --glass-border: rgba(143, 185, 168, 0.2);
53
+ --text-on-primary: #2b3a42;
54
  }
55
 
56
+ /* Global resets */
57
+ html, body {
58
+ margin: 0;
59
+ padding: 0;
60
+ font-family: var(--font-main);
61
+ background-color: var(--bg-body);
62
+ color: var(--text-main);
63
+ transition: background-color var(--transition-speed), color var(--transition-speed);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
 
66
+ /* Scrollbar styling */
67
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
68
+ ::-webkit-scrollbar-track { background: transparent; }
69
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
70
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
71
 
72
+ /* Animations */
73
+ @keyframes fadeIn {
74
+ from { opacity: 0; transform: translateY(8px); }
75
+ to { opacity: 1; transform: translateY(0); }
76
  }
77
+ .animate-fade { animation: fadeIn 0.4s ease-out forwards; }
BlazorWebAssembly/wwwroot/index.html CHANGED
@@ -4,21 +4,56 @@
4
  <head>
5
  <meta charset="utf-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>BlazorWebAssembly</title>
8
  <base href="/" />
9
- <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
10
  <link rel="stylesheet" href="css/app.css" />
11
  <link rel="icon" type="image/png" href="favicon.png" />
12
  <link href="BlazorWebAssembly.styles.css" rel="stylesheet" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  </head>
14
 
15
  <body>
16
  <div id="app">
17
- <svg class="loading-progress">
18
- <circle r="40%" cx="50%" cy="50%" />
19
- <circle r="40%" cx="50%" cy="50%" />
20
- </svg>
21
- <div class="loading-progress-text"></div>
22
  </div>
23
 
24
  <div id="blazor-error-ui">
@@ -26,7 +61,33 @@
26
  <a href="" class="reload">Reload</a>
27
  <a class="dismiss">🗙</a>
28
  </div>
 
29
  <script src="_framework/blazor.webassembly.js"></script>
30
  </body>
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  </html>
 
4
  <head>
5
  <meta charset="utf-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>LibraryLuxe | Management System</title>
8
  <base href="/" />
 
9
  <link rel="stylesheet" href="css/app.css" />
10
  <link rel="icon" type="image/png" href="favicon.png" />
11
  <link href="BlazorWebAssembly.styles.css" rel="stylesheet" />
12
+
13
+ <!-- Premium Typography -->
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
17
+
18
+ <!-- Tailwind CSS CDN -->
19
+ <script src="https://cdn.tailwindcss.com"></script>
20
+ <script>
21
+ tailwind.config = {
22
+ theme: {
23
+ extend: {
24
+ colors: {
25
+ primary: 'var(--primary)',
26
+ hover: 'var(--primary-hover)',
27
+ main: 'var(--text-main)',
28
+ muted: 'var(--text-muted)',
29
+ body: 'var(--bg-body)',
30
+ card: 'var(--bg-card)',
31
+ nav: 'var(--bg-nav)',
32
+ border: 'var(--border)',
33
+ },
34
+ borderRadius: {
35
+ 'xl': 'var(--border-radius)',
36
+ }
37
+ }
38
+ }
39
+ }
40
+ </script>
41
+
42
+ <!-- Anti-Flicker Theme Script -->
43
+ <script>
44
+ (function() {
45
+ const savedTheme = localStorage.getItem('app-theme') || 'light';
46
+ document.documentElement.setAttribute('data-theme', savedTheme);
47
+ })();
48
+ </script>
49
  </head>
50
 
51
  <body>
52
  <div id="app">
53
+ <div class="loading-container">
54
+ <div class="loader"></div>
55
+ <p>Entering LibraryLuxe...</p>
56
+ </div>
 
57
  </div>
58
 
59
  <div id="blazor-error-ui">
 
61
  <a href="" class="reload">Reload</a>
62
  <a class="dismiss">🗙</a>
63
  </div>
64
+
65
  <script src="_framework/blazor.webassembly.js"></script>
66
  </body>
67
 
68
+ <style>
69
+ .loading-container {
70
+ display: flex;
71
+ flex-direction: column;
72
+ align-items: center;
73
+ justify-content: center;
74
+ height: 100vh;
75
+ font-family: 'Outfit', sans-serif;
76
+ color: #3b82f6;
77
+ }
78
+ .loader {
79
+ border: 4px solid #f3f3f3;
80
+ border-top: 4px solid #3b82f6;
81
+ border-radius: 50%;
82
+ width: 40px;
83
+ height: 40px;
84
+ animation: spin 1s linear infinite;
85
+ margin-bottom: 1rem;
86
+ }
87
+ @keyframes spin {
88
+ 0% { transform: rotate(0deg); }
89
+ 100% { transform: rotate(360deg); }
90
+ }
91
+ </style>
92
+
93
  </html>
Frontend/Controllers/AuthController.cs CHANGED
@@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc;
2
  using Microsoft.AspNetCore.Authentication;
3
  using Microsoft.AspNetCore.Authentication.Cookies;
4
  using System.Security.Claims;
5
- using Frontend.Models.Dtos;
6
  using Frontend.Services;
7
 
8
  namespace Frontend.Controllers
@@ -108,3 +108,4 @@ namespace Frontend.Controllers
108
  }
109
  }
110
  }
 
 
2
  using Microsoft.AspNetCore.Authentication;
3
  using Microsoft.AspNetCore.Authentication.Cookies;
4
  using System.Security.Claims;
5
+ using LibraryManagement.Shared.Models;
6
  using Frontend.Services;
7
 
8
  namespace Frontend.Controllers
 
108
  }
109
  }
110
  }
111
+
Frontend/Controllers/BooksController.cs CHANGED
@@ -1,6 +1,6 @@
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
- using Frontend.Models.Dtos;
4
 
5
  namespace Frontend.Controllers
6
  {
@@ -125,3 +125,4 @@ namespace Frontend.Controllers
125
  }
126
  }
127
  }
 
 
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
+ using LibraryManagement.Shared.Models;
4
 
5
  namespace Frontend.Controllers
6
  {
 
125
  }
126
  }
127
  }
128
+
Frontend/Controllers/BorrowingsController.cs CHANGED
@@ -1,5 +1,5 @@
1
  using Frontend.Models;
2
- using Frontend.Models.Dtos;
3
  using Frontend.Services;
4
  using Microsoft.AspNetCore.Authorization;
5
  using Microsoft.AspNetCore.Mvc;
@@ -107,3 +107,4 @@ namespace Frontend.Controllers
107
  }
108
  }
109
  }
 
 
1
  using Frontend.Models;
2
+ using LibraryManagement.Shared.Models;
3
  using Frontend.Services;
4
  using Microsoft.AspNetCore.Authorization;
5
  using Microsoft.AspNetCore.Mvc;
 
107
  }
108
  }
109
  }
110
+
Frontend/Controllers/HomeController.cs CHANGED
@@ -25,7 +25,7 @@ namespace Frontend.Controllers
25
  ViewBag.TotalMembers = 0;
26
  ViewBag.LoyaltyPoints = 0;
27
  ViewBag.LoyaltyStatus = "Not Authenticated";
28
- ViewBag.RecentBooks = Enumerable.Empty<Frontend.Models.Dtos.BookDto>();
29
 
30
  try
31
  {
@@ -90,3 +90,4 @@ namespace Frontend.Controllers
90
  }
91
  }
92
  }
 
 
25
  ViewBag.TotalMembers = 0;
26
  ViewBag.LoyaltyPoints = 0;
27
  ViewBag.LoyaltyStatus = "Not Authenticated";
28
+ ViewBag.RecentBooks = Enumerable.Empty<LibraryManagement.Shared.Models.BookDto>();
29
 
30
  try
31
  {
 
90
  }
91
  }
92
  }
93
+
Frontend/Controllers/MembersController.cs CHANGED
@@ -1,6 +1,6 @@
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
- using Frontend.Models.Dtos;
4
  using Microsoft.AspNetCore.Authorization;
5
 
6
  namespace Frontend.Controllers
@@ -18,12 +18,12 @@ namespace Frontend.Controllers
18
  public async Task<IActionResult> Index(int page = 1)
19
  {
20
  const int pageSize = 10;
21
- var allUsers = await _apiClient.GetUsersAsync() ?? Enumerable.Empty<Frontend.Models.Dtos.UserDto>();
22
  var totalCount = allUsers.Count();
23
  var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
24
  page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
25
 
26
- var pagedResult = new Frontend.Models.PagedResult<Frontend.Models.Dtos.UserDto>
27
  {
28
  Items = allUsers.Skip((page - 1) * pageSize).Take(pageSize),
29
  CurrentPage = page,
@@ -153,3 +153,4 @@ namespace Frontend.Controllers
153
  }
154
  }
155
  }
 
 
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
+ using LibraryManagement.Shared.Models;
4
  using Microsoft.AspNetCore.Authorization;
5
 
6
  namespace Frontend.Controllers
 
18
  public async Task<IActionResult> Index(int page = 1)
19
  {
20
  const int pageSize = 10;
21
+ var allUsers = await _apiClient.GetUsersAsync() ?? Enumerable.Empty<LibraryManagement.Shared.Models.UserDto>();
22
  var totalCount = allUsers.Count();
23
  var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
24
  page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
25
 
26
+ var pagedResult = new Frontend.Models.PagedResult<LibraryManagement.Shared.Models.UserDto>
27
  {
28
  Items = allUsers.Skip((page - 1) * pageSize).Take(pageSize),
29
  CurrentPage = page,
 
153
  }
154
  }
155
  }
156
+
Frontend/Controllers/MembershipController.cs CHANGED
@@ -1,6 +1,6 @@
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
- using Frontend.Models.Dtos;
4
  using Frontend.Models;
5
  using Microsoft.AspNetCore.Authorization;
6
  using System.Threading.Tasks;
@@ -66,3 +66,4 @@ namespace Frontend.Controllers
66
  }
67
  }
68
  }
 
 
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
+ using LibraryManagement.Shared.Models;
4
  using Frontend.Models;
5
  using Microsoft.AspNetCore.Authorization;
6
  using System.Threading.Tasks;
 
66
  }
67
  }
68
  }
69
+
Frontend/Controllers/RewardsController.cs CHANGED
@@ -1,5 +1,5 @@
1
  using Frontend.Models;
2
- using Frontend.Models.Dtos;
3
  using Frontend.Services;
4
  using Microsoft.AspNetCore.Authorization;
5
  using Microsoft.AspNetCore.Mvc;
@@ -90,3 +90,4 @@ namespace Frontend.Controllers
90
  }
91
  }
92
  }
 
 
1
  using Frontend.Models;
2
+ using LibraryManagement.Shared.Models;
3
  using Frontend.Services;
4
  using Microsoft.AspNetCore.Authorization;
5
  using Microsoft.AspNetCore.Mvc;
 
90
  }
91
  }
92
  }
93
+
Frontend/Controllers/SubscriptionsController.cs CHANGED
@@ -1,6 +1,6 @@
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
- using Frontend.Models.Dtos;
4
  using Frontend.Models;
5
  using Microsoft.AspNetCore.Authorization;
6
  using System.Threading.Tasks;
@@ -53,3 +53,4 @@ namespace Frontend.Controllers
53
  }
54
  }
55
  }
 
 
1
  using Microsoft.AspNetCore.Mvc;
2
  using Frontend.Services;
3
+ using LibraryManagement.Shared.Models;
4
  using Frontend.Models;
5
  using Microsoft.AspNetCore.Authorization;
6
  using System.Threading.Tasks;
 
53
  }
54
  }
55
  }
56
+
Frontend/Frontend.csproj CHANGED
@@ -1,5 +1,9 @@
1
  <Project Sdk="Microsoft.NET.Sdk.Web">
2
 
 
 
 
 
3
  <PropertyGroup>
4
  <TargetFramework>net8.0</TargetFramework>
5
  <Nullable>enable</Nullable>
 
1
  <Project Sdk="Microsoft.NET.Sdk.Web">
2
 
3
+ <ItemGroup>
4
+ <ProjectReference Include="..\LibraryManagement.Shared\LibraryManagement.Shared.csproj" />
5
+ </ItemGroup>
6
+
7
  <PropertyGroup>
8
  <TargetFramework>net8.0</TargetFramework>
9
  <Nullable>enable</Nullable>
Frontend/Models/Dtos/LibraryDtos.cs DELETED
@@ -1,285 +0,0 @@
1
- using System.Text.Json.Serialization;
2
-
3
- namespace Frontend.Models.Dtos
4
- {
5
- // Auth DTOs
6
- public class RegisterRequest
7
- {
8
- public string FullName { get; set; } = string.Empty;
9
- public string Email { get; set; } = string.Empty;
10
- public string Password { get; set; } = string.Empty;
11
- public string? PhoneNumber { get; set; }
12
- public string? Address { get; set; }
13
- public string? StudentId { get; set; }
14
- }
15
-
16
- public class LoginRequest
17
- {
18
- public string Email { get; set; } = string.Empty;
19
- public string Password { get; set; } = string.Empty;
20
- }
21
-
22
- public class AuthResponse
23
- {
24
- public string Token { get; set; } = string.Empty;
25
- public string FullName { get; set; } = string.Empty;
26
- public string Role { get; set; } = string.Empty;
27
- public DateTime Expiry { get; set; }
28
- }
29
-
30
- // Book DTOs
31
- public class BookDto
32
- {
33
- public int Id { get; set; }
34
- public string Title { get; set; } = string.Empty;
35
- public string Isbn { get; set; } = string.Empty;
36
- public string Author { get; set; } = string.Empty;
37
- public string Status { get; set; } = string.Empty;
38
- public bool IsActive { get; set; }
39
- public string? Description { get; set; }
40
- public int TotalCopies { get; set; }
41
- public int AvailableCopies { get; set; }
42
- public DateTime CreatedAt { get; set; }
43
- public DateTime? UpdatedAt { get; set; }
44
- public string? CoverUrl { get; set; }
45
- public List<BookCategoryDto> Categories { get; set; } = new();
46
- }
47
-
48
- public class BookCategoryDto
49
- {
50
- public int Id { get; set; }
51
- public string Name { get; set; } = string.Empty;
52
- }
53
-
54
- public class BookCreateRequest
55
- {
56
- public string Title { get; set; } = string.Empty;
57
- public string Isbn { get; set; } = string.Empty;
58
- public string Author { get; set; } = string.Empty;
59
- public string? Description { get; set; }
60
- public int TotalCopies { get; set; }
61
- public List<int>? CategoryIds { get; set; }
62
- }
63
-
64
- public class BookUpdateRequest
65
- {
66
- public string Title { get; set; } = string.Empty;
67
- public string Author { get; set; } = string.Empty;
68
- public string? Description { get; set; }
69
- public int TotalCopies { get; set; }
70
- public List<int>? CategoryIds { get; set; }
71
- }
72
-
73
- // Borrowing DTOs
74
- public class BorrowingDto
75
- {
76
- public Guid Id { get; set; }
77
- public Guid UserId { get; set; }
78
- public string UserEmail { get; set; } = string.Empty;
79
- public int BookId { get; set; }
80
- public string BookTitle { get; set; } = string.Empty;
81
- public DateTime BorrowDate { get; set; }
82
- public DateTime DueDate { get; set; }
83
- public DateTime? ReturnDate { get; set; } // This is ActualReturnDate in some contexts
84
- public string Status { get; set; } = string.Empty;
85
- public decimal FineAmount { get; set; }
86
- public bool IsFinePaid { get; set; }
87
- }
88
-
89
- public class BorrowRequest
90
- {
91
- public int BookId { get; set; }
92
- }
93
-
94
- // Category DTOs
95
- public class CategoryDto
96
- {
97
- public int Id { get; set; }
98
- public string Name { get; set; } = string.Empty;
99
- }
100
-
101
- public class CategoryCreateRequest
102
- {
103
- public string Name { get; set; } = string.Empty;
104
- }
105
-
106
- // Subscription & Membership DTOs
107
- public class MembershipDto
108
- {
109
- public int Id { get; set; }
110
- public string Type { get; set; } = string.Empty;
111
- public int MaxBooks { get; set; }
112
- public int BorrowingDays { get; set; }
113
- public decimal Price { get; set; }
114
- public int DurationMonths { get; set; }
115
- public string? RewardId { get; set; }
116
- }
117
-
118
- public class SubscriptionDto
119
- {
120
- public Guid Id { get; set; }
121
- public Guid UserId { get; set; }
122
- public int MembershipId { get; set; }
123
- public string MembershipType { get; set; } = string.Empty;
124
- public string UserName { get; set; } = string.Empty;
125
- public string UserEmail { get; set; } = string.Empty;
126
- public DateTime StartDate { get; set; }
127
- public DateTime ExpiryDate { get; set; }
128
- public bool IsActive { get; set; }
129
- public bool IsExpired => DateTime.UtcNow > ExpiryDate;
130
- }
131
-
132
- public class SubscribeRequest
133
- {
134
- public int MembershipId { get; set; }
135
- }
136
-
137
- public class AdminSubscribeRequest
138
- {
139
- public Guid UserId { get; set; }
140
- public int MembershipId { get; set; }
141
- }
142
-
143
- // Loyalty DTOs
144
- public class LoyaltyAccountDto
145
- {
146
- [JsonPropertyName("id")]
147
- public string Id { get; set; } = string.Empty;
148
-
149
- // Some responses use `accountId` instead of `id`.
150
- [JsonPropertyName("accountId")]
151
- public string? AccountId { get; set; }
152
-
153
- [JsonPropertyName("externalUserId")]
154
- public string ExternalUserId { get; set; } = string.Empty;
155
-
156
- [JsonPropertyName("currentBalance")]
157
- public double CurrentBalance { get; set; }
158
-
159
- [JsonPropertyName("tier")]
160
- public string Tier { get; set; } = string.Empty;
161
-
162
- [JsonPropertyName("lifetimePoints")]
163
- public double LifetimePoints { get; set; }
164
- }
165
-
166
- public class LoyaltyRewardDto
167
- {
168
- [JsonPropertyName("id")]
169
- public string Id { get; set; } = string.Empty;
170
-
171
- [JsonPropertyName("name")]
172
- public string Name { get; set; } = string.Empty;
173
-
174
- [JsonPropertyName("description")]
175
- public string Description { get; set; } = string.Empty;
176
-
177
- [JsonPropertyName("pointCost")]
178
- public double PointCost { get; set; }
179
-
180
- [JsonPropertyName("stockQuantity")]
181
- public int StockQuantity { get; set; }
182
-
183
- [JsonPropertyName("isActive")]
184
- public bool IsActive { get; set; }
185
- }
186
-
187
- public class ClaimRewardRequestDto
188
- {
189
- public string RewardId { get; set; } = string.Empty;
190
- public string? Notes { get; set; }
191
- }
192
-
193
- // User DTOs
194
- public class UserDto
195
- {
196
- public Guid Id { get; set; }
197
- public string FullName { get; set; } = string.Empty;
198
- public string Email { get; set; } = string.Empty;
199
- public string? PhoneNumber { get; set; }
200
- public string Role { get; set; } = string.Empty;
201
- public bool IsActive { get; set; }
202
- public string? StudentId { get; set; }
203
- public string? Address { get; set; }
204
- public DateTime CreatedAt { get; set; }
205
- public DateTime? UpdatedAt { get; set; }
206
- public bool? BanStatus { get; set; }
207
- public DateTime? SuspensionEndDate { get; set; }
208
- }
209
-
210
- public class UserCreateRequest
211
- {
212
- public string FullName { get; set; } = string.Empty;
213
- public string Email { get; set; } = string.Empty;
214
- public string Password { get; set; } = string.Empty;
215
- public string? PhoneNumber { get; set; }
216
- public string? StudentId { get; set; }
217
- public string? Address { get; set; }
218
- }
219
-
220
- public class UserUpdateRequest
221
- {
222
- public string FullName { get; set; } = string.Empty;
223
- public string? PhoneNumber { get; set; }
224
- public string? StudentId { get; set; }
225
- public string? Address { get; set; }
226
- }
227
-
228
- public class UserRoleUpdateRequest
229
- {
230
- public string Role { get; set; } = string.Empty;
231
- }
232
-
233
- // Points History DTOs
234
- public class PointHistoryEntryDto
235
- {
236
- [JsonPropertyName("id")]
237
- public int Id { get; set; }
238
- [JsonPropertyName("accountId")]
239
- public string AccountId { get; set; } = string.Empty;
240
- [JsonPropertyName("externalUserId")]
241
- public string? ExternalUserId { get; set; }
242
- [JsonPropertyName("pointDelta")]
243
- public double PointDelta { get; set; }
244
- [JsonPropertyName("eventKey")]
245
- public string EventKey { get; set; } = string.Empty;
246
- [JsonPropertyName("description")]
247
- public string Description { get; set; } = string.Empty;
248
- [JsonPropertyName("referenceId")]
249
- public string? ReferenceId { get; set; }
250
- [JsonPropertyName("createdAt")]
251
- public DateTime CreatedAt { get; set; }
252
-
253
- [JsonPropertyName("rewardId")]
254
- public string? RewardId { get; set; }
255
-
256
- [JsonPropertyName("rewardName")]
257
- public string? RewardName { get; set; }
258
-
259
- [JsonPropertyName("redemptionStatus")]
260
- public string? RedemptionStatus { get; set; }
261
-
262
- [JsonPropertyName("redeemedAt")]
263
- public DateTime? RedeemedAt { get; set; }
264
- }
265
-
266
- public class UserPointsHistoryDto
267
- {
268
- [JsonPropertyName("userId")]
269
- public Guid UserId { get; set; }
270
- [JsonPropertyName("userName")]
271
- public string UserName { get; set; } = string.Empty;
272
- [JsonPropertyName("userEmail")]
273
- public string UserEmail { get; set; } = string.Empty;
274
- [JsonPropertyName("accountId")]
275
- public string AccountId { get; set; } = string.Empty;
276
- [JsonPropertyName("currentBalance")]
277
- public double CurrentBalance { get; set; }
278
- [JsonPropertyName("tier")]
279
- public string Tier { get; set; } = string.Empty;
280
- [JsonPropertyName("history")]
281
- public IEnumerable<PointHistoryEntryDto> History { get; set; } = Enumerable.Empty<PointHistoryEntryDto>();
282
- [JsonPropertyName("redemptions")]
283
- public IEnumerable<LoyaltyRedemptionDto> Redemptions { get; set; } = Enumerable.Empty<LoyaltyRedemptionDto>();
284
- }
285
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Frontend/Models/Dtos/LoyaltyRedemptionDto.cs DELETED
@@ -1,32 +0,0 @@
1
- using System;
2
- using System.Text.Json.Serialization;
3
-
4
- namespace Frontend.Models.Dtos
5
- {
6
- public class LoyaltyRedemptionDto
7
- {
8
- [JsonPropertyName("id")]
9
- public string Id { get; set; } = string.Empty;
10
-
11
- [JsonPropertyName("systemId")]
12
- public string SystemId { get; set; } = string.Empty;
13
-
14
- [JsonPropertyName("externalUserId")]
15
- public string ExternalUserId { get; set; } = string.Empty;
16
-
17
- [JsonPropertyName("rewardId")]
18
- public string? RewardId { get; set; }
19
-
20
- [JsonPropertyName("rewardName")]
21
- public string RewardName { get; set; } = string.Empty;
22
-
23
- [JsonPropertyName("pointCost")]
24
- public double PointCost { get; set; }
25
-
26
- [JsonPropertyName("status")]
27
- public string Status { get; set; } = string.Empty;
28
-
29
- [JsonPropertyName("redeemedAt")]
30
- public DateTime RedeemedAt { get; set; }
31
- }
32
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Frontend/Models/MemberDetailsViewModel.cs CHANGED
@@ -1,4 +1,4 @@
1
- using Frontend.Models.Dtos;
2
  using System.Collections.Generic;
3
 
4
  namespace Frontend.Models
@@ -10,3 +10,4 @@ namespace Frontend.Models
10
  public List<MembershipDto> AvailableMemberships { get; set; } = new();
11
  }
12
  }
 
 
1
+ using LibraryManagement.Shared.Models;
2
  using System.Collections.Generic;
3
 
4
  namespace Frontend.Models
 
10
  public List<MembershipDto> AvailableMemberships { get; set; } = new();
11
  }
12
  }
13
+
Frontend/Models/MembershipViewModel.cs CHANGED
@@ -1,4 +1,4 @@
1
- using Frontend.Models.Dtos;
2
  using System.Collections.Generic;
3
 
4
  namespace Frontend.Models
@@ -12,3 +12,4 @@ namespace Frontend.Models
12
  public Dictionary<string, double> RewardPointCosts { get; set; } = new Dictionary<string, double>();
13
  }
14
  }
 
 
1
+ using LibraryManagement.Shared.Models;
2
  using System.Collections.Generic;
3
 
4
  namespace Frontend.Models
 
12
  public Dictionary<string, double> RewardPointCosts { get; set; } = new Dictionary<string, double>();
13
  }
14
  }
15
+
Frontend/Models/RewardsViewModel.cs CHANGED
@@ -1,4 +1,4 @@
1
- using Frontend.Models.Dtos;
2
 
3
  namespace Frontend.Models
4
  {
@@ -19,3 +19,4 @@ namespace Frontend.Models
19
  public IEnumerable<UserPointsHistoryDto> AllMembersHistory { get; set; } = Enumerable.Empty<UserPointsHistoryDto>();
20
  }
21
  }
 
 
1
+ using LibraryManagement.Shared.Models;
2
 
3
  namespace Frontend.Models
4
  {
 
19
  public IEnumerable<UserPointsHistoryDto> AllMembersHistory { get; set; } = Enumerable.Empty<UserPointsHistoryDto>();
20
  }
21
  }
22
+
Frontend/Models/SubscriptionsViewModel.cs CHANGED
@@ -1,4 +1,4 @@
1
- using Frontend.Models.Dtos;
2
 
3
  namespace Frontend.Models
4
  {
@@ -8,3 +8,4 @@ namespace Frontend.Models
8
  public List<SubscriptionDto> ActiveSubscriptions { get; set; } = new();
9
  }
10
  }
 
 
1
+ using LibraryManagement.Shared.Models;
2
 
3
  namespace Frontend.Models
4
  {
 
8
  public List<SubscriptionDto> ActiveSubscriptions { get; set; } = new();
9
  }
10
  }
11
+
Frontend/Services/LibraryApiClient.cs CHANGED
@@ -1,11 +1,21 @@
 
1
  using System.Net.Http.Json;
2
  using System.Text.Json;
3
- using Frontend.Models.Dtos;
4
 
5
  namespace Frontend.Services
6
  {
7
  public class LibraryApiClient
8
  {
 
 
 
 
 
 
 
 
 
9
  private readonly HttpClient _httpClient;
10
  private readonly IHttpContextAccessor _httpContextAccessor;
11
 
@@ -232,11 +242,8 @@ namespace Frontend.Services
232
  {
233
  try
234
  {
235
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
236
- var response = await _httpClient.GetAsync($"api/v1/accounts/{accountId}/history");
237
- if (response.IsSuccessStatusCode)
238
- return await response.Content.ReadFromJsonAsync<IEnumerable<PointHistoryEntryDto>>(options) ?? Enumerable.Empty<PointHistoryEntryDto>();
239
- return Enumerable.Empty<PointHistoryEntryDto>();
240
  }
241
  catch { return Enumerable.Empty<PointHistoryEntryDto>(); }
242
  }
@@ -244,7 +251,7 @@ namespace Frontend.Services
244
  public async Task<IEnumerable<UserPointsHistoryDto>> GetAllMembersPointsHistoryAsync()
245
  {
246
  try {
247
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
248
  var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history");
249
  var raw = await response.Content.ReadAsStringAsync();
250
  if (!response.IsSuccessStatusCode)
@@ -300,22 +307,6 @@ namespace Frontend.Services
300
  }
301
  }
302
 
303
- public async Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync()
304
- {
305
- try
306
- {
307
- // Fetch from the external loyalty API directly or via our backend proxy
308
- // The user specified the URL: http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS
309
- // We'll use a new HttpClient or just the existing one if configured correctly.
310
- // However, our backend doesn't have a proxy for this yet.
311
- // For simplicity, let's assume we call it directly or via a backend endpoint we'll add.
312
- // Better: Let's add a backend proxy in LoyaltyController to avoid CORS issues if this was a browser app.
313
- // In ASP.NET Core MVC, the server-side HttpClient can call it directly.
314
- var client = new HttpClient();
315
- return await client.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS") ?? Enumerable.Empty<LoyaltyRewardDto>();
316
- }
317
- catch { return Enumerable.Empty<LoyaltyRewardDto>(); }
318
- }
319
 
320
  #region Users
321
  public async Task<IEnumerable<UserDto>> GetUsersAsync()
@@ -359,7 +350,7 @@ namespace Frontend.Services
359
  var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending");
360
  if (response.IsSuccessStatusCode)
361
  {
362
- var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
363
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
364
  }
365
  return Enumerable.Empty<LoyaltyRedemptionDto>();
@@ -397,3 +388,7 @@ namespace Frontend.Services
397
  }
398
  }
399
  }
 
 
 
 
 
1
+ using System.Text.Json.Serialization;
2
  using System.Net.Http.Json;
3
  using System.Text.Json;
4
+ using LibraryManagement.Shared.Models; // Changed from LibraryManagement.Shared.Models
5
 
6
  namespace Frontend.Services
7
  {
8
  public class LibraryApiClient
9
  {
10
+ public async Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync()
11
+ {
12
+ try
13
+ {
14
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
15
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("api/Loyalty/rewards", options) ?? Enumerable.Empty<LoyaltyRewardDto>();
16
+ }
17
+ catch { return Enumerable.Empty<LoyaltyRewardDto>(); }
18
+ }
19
  private readonly HttpClient _httpClient;
20
  private readonly IHttpContextAccessor _httpContextAccessor;
21
 
 
242
  {
243
  try
244
  {
245
+ // Call the Backend's proxy endpoint instead of trying to hit the external API directly
246
+ return await _httpClient.GetFromJsonAsync<IEnumerable<PointHistoryEntryDto>>("api/Loyalty/my-points-history") ?? Enumerable.Empty<PointHistoryEntryDto>();
 
 
 
247
  }
248
  catch { return Enumerable.Empty<PointHistoryEntryDto>(); }
249
  }
 
251
  public async Task<IEnumerable<UserPointsHistoryDto>> GetAllMembersPointsHistoryAsync()
252
  {
253
  try {
254
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
255
  var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history");
256
  var raw = await response.Content.ReadAsStringAsync();
257
  if (!response.IsSuccessStatusCode)
 
307
  }
308
  }
309
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
 
311
  #region Users
312
  public async Task<IEnumerable<UserDto>> GetUsersAsync()
 
350
  var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending");
351
  if (response.IsSuccessStatusCode)
352
  {
353
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
354
  return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
355
  }
356
  return Enumerable.Empty<LoyaltyRedemptionDto>();
 
388
  }
389
  }
390
  }
391
+
392
+
393
+
394
+
Frontend/Views/Auth/Login.cshtml CHANGED
@@ -1,4 +1,4 @@
1
- @model Frontend.Models.Dtos.LoginRequest
2
  @{
3
  ViewData["Title"] = "Sign In";
4
  Layout = "_Layout";
@@ -47,3 +47,4 @@
47
  </form>
48
  </div>
49
  </div>
 
 
1
+ @model LibraryManagement.Shared.Models.LoginRequest
2
  @{
3
  ViewData["Title"] = "Sign In";
4
  Layout = "_Layout";
 
47
  </form>
48
  </div>
49
  </div>
50
+
Frontend/Views/Auth/Register.cshtml CHANGED
@@ -1,4 +1,4 @@
1
- @model Frontend.Models.Dtos.RegisterRequest
2
  @{
3
  ViewData["Title"] = "Create Account";
4
  Layout = "_Layout";
@@ -54,3 +54,4 @@
54
  </form>
55
  </div>
56
  </div>
 
 
1
+ @model LibraryManagement.Shared.Models.RegisterRequest
2
  @{
3
  ViewData["Title"] = "Create Account";
4
  Layout = "_Layout";
 
54
  </form>
55
  </div>
56
  </div>
57
+
Frontend/Views/Books/Create.cshtml CHANGED
@@ -1,4 +1,4 @@
1
- @model Frontend.Models.Dtos.BookCreateRequest
2
  @{
3
  ViewData["Title"] = "Add New Book";
4
  }
@@ -72,7 +72,7 @@
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
  {
@@ -106,3 +106,4 @@
106
  @section Scripts {
107
  @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
108
  }
 
 
1
+ @model LibraryManagement.Shared.Models.BookCreateRequest
2
  @{
3
  ViewData["Title"] = "Add New Book";
4
  }
 
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<LibraryManagement.Shared.Models.CategoryDto>;
76
  }
77
  @if (allCategories != null && allCategories.Any())
78
  {
 
106
  @section Scripts {
107
  @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
108
  }
109
+
Frontend/Views/Books/Details.cshtml CHANGED
@@ -1,4 +1,4 @@
1
- @model Frontend.Models.Dtos.BookDto
2
  @{
3
  ViewData["Title"] = Model.Title;
4
  }
@@ -75,3 +75,4 @@
75
  </div>
76
  </div>
77
  </div>
 
 
1
+ @model LibraryManagement.Shared.Models.BookDto
2
  @{
3
  ViewData["Title"] = Model.Title;
4
  }
 
75
  </div>
76
  </div>
77
  </div>
78
+
Frontend/Views/Books/Edit.cshtml CHANGED
@@ -1,4 +1,4 @@
1
- @model Frontend.Models.Dtos.BookUpdateRequest
2
  @{
3
  ViewData["Title"] = "Edit Book";
4
  }
@@ -63,7 +63,7 @@
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())
@@ -99,3 +99,4 @@
99
  @section Scripts {
100
  @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
101
  }
 
 
1
+ @model LibraryManagement.Shared.Models.BookUpdateRequest
2
  @{
3
  ViewData["Title"] = "Edit Book";
4
  }
 
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<LibraryManagement.Shared.Models.CategoryDto>;
67
  var selectedIds = Model.CategoryIds ?? new List<int>();
68
  }
69
  @if (allCategories != null && allCategories.Any())
 
99
  @section Scripts {
100
  @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
101
  }
102
+
Frontend/Views/Books/Index.cshtml CHANGED
@@ -1,4 +1,4 @@
1
- @model Frontend.Models.PagedResult<Frontend.Models.Dtos.BookDto>
2
  @{
3
  ViewData["Title"] = "Books Catalog";
4
  }
@@ -37,7 +37,7 @@
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())
@@ -191,3 +191,4 @@
191
  </nav>
192
  </div>
193
  }
 
 
1
+ @model Frontend.Models.PagedResult<LibraryManagement.Shared.Models.BookDto>
2
  @{
3
  ViewData["Title"] = "Books Catalog";
4
  }
 
37
 
38
  @* Category Filter Pills *@
39
  @{
40
+ var categories = ViewBag.Categories as IEnumerable<LibraryManagement.Shared.Models.CategoryDto> ?? Enumerable.Empty<LibraryManagement.Shared.Models.CategoryDto>();
41
  int? selectedCategoryId = ViewBag.SelectedCategoryId;
42
  }
43
  @if (categories.Any())
 
191
  </nav>
192
  </div>
193
  }
194
+