Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
6f32ae8
1
Parent(s): 4433399
add: books, borrowings, home, login, membership, register, reward, userinsights, wishlist, themes
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Backend/Features/Loyalty/LoyaltyController.cs +32 -2
- Backend/Features/Loyalty/LoyaltyService.cs +82 -147
- Backend/Program.cs +11 -0
- BlazorFrontend/Services/LibraryApiClient.cs +11 -11
- BlazorWebAssembly/App.razor +30 -12
- BlazorWebAssembly/BlazorWebAssembly.csproj +2 -0
- BlazorWebAssembly/Layout/MainLayout.razor +96 -12
- BlazorWebAssembly/Layout/NavMenu.razor +66 -34
- BlazorWebAssembly/Layout/ThemeSwitcher.razor +29 -0
- BlazorWebAssembly/Pages/Books.razor +143 -0
- BlazorWebAssembly/Pages/Borrowings.razor +122 -0
- BlazorWebAssembly/Pages/Counter.razor +0 -18
- BlazorWebAssembly/Pages/Home.razor +146 -4
- BlazorWebAssembly/Pages/Login.razor +97 -0
- BlazorWebAssembly/Pages/Membership.razor +130 -0
- BlazorWebAssembly/Pages/Register.razor +110 -0
- BlazorWebAssembly/Pages/Rewards.razor +100 -0
- BlazorWebAssembly/Pages/UserInsights.razor +100 -0
- BlazorWebAssembly/Pages/Weather.razor +0 -57
- BlazorWebAssembly/Pages/Wishlist.razor +79 -0
- BlazorWebAssembly/Program.cs +25 -1
- BlazorWebAssembly/Providers/JwtAuthenticationStateProvider.cs +87 -0
- BlazorWebAssembly/Services/AuthService.cs +69 -0
- BlazorWebAssembly/Services/LibraryApiClient.cs +131 -0
- BlazorWebAssembly/Services/ThemeService.cs +40 -0
- BlazorWebAssembly/_Imports.razor +5 -1
- BlazorWebAssembly/wwwroot/css/app.css +65 -91
- BlazorWebAssembly/wwwroot/index.html +68 -7
- Frontend/Controllers/AuthController.cs +2 -1
- Frontend/Controllers/BooksController.cs +2 -1
- Frontend/Controllers/BorrowingsController.cs +2 -1
- Frontend/Controllers/HomeController.cs +2 -1
- Frontend/Controllers/MembersController.cs +4 -3
- Frontend/Controllers/MembershipController.cs +2 -1
- Frontend/Controllers/RewardsController.cs +2 -1
- Frontend/Controllers/SubscriptionsController.cs +2 -1
- Frontend/Frontend.csproj +4 -0
- Frontend/Models/Dtos/LibraryDtos.cs +0 -285
- Frontend/Models/Dtos/LoyaltyRedemptionDto.cs +0 -32
- Frontend/Models/MemberDetailsViewModel.cs +2 -1
- Frontend/Models/MembershipViewModel.cs +2 -1
- Frontend/Models/RewardsViewModel.cs +2 -1
- Frontend/Models/SubscriptionsViewModel.cs +2 -1
- Frontend/Services/LibraryApiClient.cs +19 -24
- Frontend/Views/Auth/Login.cshtml +2 -1
- Frontend/Views/Auth/Register.cshtml +2 -1
- Frontend/Views/Books/Create.cshtml +3 -2
- Frontend/Views/Books/Details.cshtml +2 -1
- Frontend/Views/Books/Edit.cshtml +3 -2
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =>
|
| 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<
|
| 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<
|
| 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 |
-
|
| 55 |
};
|
| 56 |
|
| 57 |
var response = await _httpClient.PostAsJsonAsync("/api/v1/accounts", payload);
|
| 58 |
-
|
| 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 |
-
|
| 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<
|
| 105 |
{
|
| 106 |
try
|
| 107 |
{
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
if (response.IsSuccessStatusCode)
|
| 110 |
{
|
| 111 |
-
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
| 112 |
-
var account = await response.Content.ReadFromJsonAsync<
|
|
|
|
| 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
|
| 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 |
-
|
| 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
|
| 164 |
}
|
| 165 |
}
|
| 166 |
|
|
@@ -168,58 +154,56 @@ namespace Backend.Features.Loyalty
|
|
| 168 |
{
|
| 169 |
try
|
| 170 |
{
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 191 |
if (response.IsSuccessStatusCode)
|
| 192 |
{
|
| 193 |
-
var
|
| 194 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 223 |
return Enumerable.Empty<LoyaltyRedemptionDto>();
|
| 224 |
}
|
| 225 |
}
|
|
@@ -228,107 +212,58 @@ namespace Backend.Features.Loyalty
|
|
| 228 |
{
|
| 229 |
try
|
| 230 |
{
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
| 232 |
return response.IsSuccessStatusCode;
|
| 233 |
}
|
| 234 |
-
catch
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 246 |
if (response.IsSuccessStatusCode)
|
| 247 |
{
|
| 248 |
-
var
|
| 249 |
-
|
| 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
|
| 262 |
return Enumerable.Empty<PointHistoryEntryDto>();
|
| 263 |
}
|
| 264 |
}
|
| 265 |
|
| 266 |
-
public async Task<IEnumerable<
|
| 267 |
{
|
| 268 |
-
|
| 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 |
-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 225 |
-
|
| 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 |
-
|
| 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 |
-
|
| 2 |
-
<
|
| 3 |
-
<
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
<
|
| 3 |
-
<
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
<
|
| 8 |
-
</
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
<
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
</
|
| 21 |
-
|
| 22 |
-
<
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
</
|
| 27 |
-
</
|
| 28 |
-
</div>
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 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 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
}
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 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 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
| 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>
|
| 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 |
-
<
|
| 18 |
-
<
|
| 19 |
-
<
|
| 20 |
-
</
|
| 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
|
| 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
|
| 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
|
| 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<
|
| 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
|
| 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<
|
| 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<
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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 |
-
|
| 236 |
-
|
| 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
|
| 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
|
| 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
|
| 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<
|
| 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
|
| 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
|
| 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<
|
| 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<
|
| 2 |
@{
|
| 3 |
ViewData["Title"] = "Books Catalog";
|
| 4 |
}
|
|
@@ -37,7 +37,7 @@
|
|
| 37 |
|
| 38 |
@* Category Filter Pills *@
|
| 39 |
@{
|
| 40 |
-
var categories = ViewBag.Categories as IEnumerable<
|
| 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 |
+
|