Yuyuqt commited on
Commit
d8a7fc4
·
1 Parent(s): d3d3b46

add: authorized pages, firebase

Browse files
Backend/Features/Notification/NotificationService.cs CHANGED
@@ -32,16 +32,16 @@ public class NotificationService : INotificationService
32
  Data = data
33
  };
34
 
35
- try
36
- {
37
- string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
38
- return !string.IsNullOrEmpty(response);
39
  }
40
  catch (Exception ex)
41
  {
42
- Console.WriteLine($"Error sending FCM message: {ex.Message}");
43
- return false;
44
- }
45
  }
46
 
47
  public async Task<bool> SendAndSaveNotificationAsync(Guid userId, string? token, string title, string body, string type = "Info", string? actionLink = null, string? actionText = null)
 
32
  Data = data
33
  };
34
 
35
+ try
36
+
37
+ {string response = await FirebaseMessaging.DefaultInstance.SendAsync(message);
38
+ return !string.IsNullOrEmpty(response);
39
  }
40
  catch (Exception ex)
41
  {
42
+ Console.WriteLine($"Error sending FCM message: {ex.Message}");
43
+ return false;
44
+ }
45
  }
46
 
47
  public async Task<bool> SendAndSaveNotificationAsync(Guid userId, string? token, string title, string body, string type = "Info", string? actionLink = null, string? actionText = null)
Backend/Program.cs CHANGED
@@ -20,10 +20,10 @@ using Google.Apis.Auth.OAuth2;
20
  var builder = WebApplication.CreateBuilder(args);
21
 
22
  // Firebase Admin SDK Initialization
23
- //FirebaseApp.Create(new AppOptions()
24
- //{
25
- // Credential = GoogleCredential.FromFile("LibraryFirebase.json")
26
- //});
27
 
28
  builder.Services.AddCors(options =>
29
  {
 
20
  var builder = WebApplication.CreateBuilder(args);
21
 
22
  // Firebase Admin SDK Initialization
23
+ FirebaseApp.Create(new AppOptions()
24
+ {
25
+ Credential = GoogleCredential.FromFile("LibraryFirebase.json")
26
+ });
27
 
28
  builder.Services.AddCors(options =>
29
  {
BlazorWebAssembly/Layout/MainLayout.razor CHANGED
@@ -3,6 +3,8 @@
3
  @using Microsoft.AspNetCore.Components.Authorization
4
  @inject ThemeService ThemeService
5
  @inject AuthenticationStateProvider AuthStateProvider
 
 
6
 
7
  <AuthorizeView>
8
  <Authorized>
@@ -54,7 +56,7 @@
54
  </div>
55
  </header>
56
  <main class="flex-1 overflow-y-auto p-8 scrollbar-hide">
57
- <div class="max-w-6xl mx-auto">
58
  @Body
59
  </div>
60
  </main>
@@ -74,7 +76,8 @@
74
  </a>
75
  <div class="hidden md:flex items-center gap-8">
76
  <NavLink href="/" Match="NavLinkMatch.All" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">Home</NavLink>
77
- <NavLink href="/books" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">Books</NavLink>
 
78
  <NavLink href="/about" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">About Us</NavLink>
79
  <NavLink href="/contact" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">Contact</NavLink>
80
  </div>
@@ -91,21 +94,57 @@
91
  <main class="flex-grow">
92
  @Body
93
  </main>
94
- <footer class="py-12 border-t border-border bg-card">
95
- <div class="max-w-6xl mx-auto px-8 flex flex-col md:flex-row justify-between items-center gap-8">
96
- <div class="flex items-center gap-3 opacity-50 grayscale">
97
- <div class="bg-main rounded-lg p-1.5 text-bg-body">
98
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  </div>
100
- <span class="text-sm font-black tracking-tighter uppercase">LibraryLuxe</span>
101
  </div>
102
- <p class="text-[10px] font-bold text-muted uppercase tracking-[0.2em]">&copy; 2026 LibraryLuxe Ecosystem. All rights reserved.</p>
103
- <div class="flex gap-6">
104
- <a href="#" class="text-[10px] font-black uppercase tracking-widest text-muted hover:text-primary">Privacy</a>
105
- <a href="#" class="text-[10px] font-black uppercase tracking-widest text-muted hover:text-primary">Terms</a>
 
 
106
  </div>
107
  </div>
108
  </footer>
 
109
  </div>
110
  </NotAuthorized>
111
  </AuthorizeView>
@@ -115,4 +154,29 @@
115
  {
116
  await ThemeService.InitializeAsync();
117
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
 
3
  @using Microsoft.AspNetCore.Components.Authorization
4
  @inject ThemeService ThemeService
5
  @inject AuthenticationStateProvider AuthStateProvider
6
+ @inject IJSRuntime JS
7
+ @inject LibraryApiClient ApiClient
8
 
9
  <AuthorizeView>
10
  <Authorized>
 
56
  </div>
57
  </header>
58
  <main class="flex-1 overflow-y-auto p-8 scrollbar-hide">
59
+ <div class="max-w-7xl mx-auto w-full">
60
  @Body
61
  </div>
62
  </main>
 
76
  </a>
77
  <div class="hidden md:flex items-center gap-8">
78
  <NavLink href="/" Match="NavLinkMatch.All" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">Home</NavLink>
79
+ <NavLink href="/membership" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">Membership</NavLink>
80
+ <NavLink href="/features" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">Features</NavLink>
81
  <NavLink href="/about" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">About Us</NavLink>
82
  <NavLink href="/contact" class="text-xs font-black uppercase tracking-widest text-muted hover:text-primary transition-colors">Contact</NavLink>
83
  </div>
 
94
  <main class="flex-grow">
95
  @Body
96
  </main>
97
+ <footer class="py-20 border-t border-border bg-card">
98
+ <div class="max-w-7xl mx-auto px-8">
99
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
100
+ <div class="col-span-1 md:col-span-2">
101
+ <div class="flex items-center gap-3 mb-6">
102
+ <div class="bg-primary rounded-xl p-2 text-white">
103
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
104
+ </div>
105
+ <span class="text-xl font-black tracking-tighter uppercase">LibraryLuxe</span>
106
+ </div>
107
+ <p class="text-muted text-sm max-w-sm leading-relaxed mb-8">
108
+ Redefining the modern library experience with digital-first solutions and community-driven rewards.
109
+ </p>
110
+ <div class="flex gap-4">
111
+ <!-- Social Icons Placeholder -->
112
+ <div class="w-10 h-10 rounded-xl bg-body border border-border flex items-center justify-center text-muted hover:text-primary transition-colors cursor-pointer">
113
+ <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/></svg>
114
+ </div>
115
+ <div class="w-10 h-10 rounded-xl bg-body border border-border flex items-center justify-center text-muted hover:text-primary transition-colors cursor-pointer">
116
+ <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ <div>
121
+ <h4 class="text-xs font-black uppercase tracking-[0.2em] text-main mb-6">Navigation</h4>
122
+ <ul class="space-y-4">
123
+ <li><a href="/" class="text-sm text-muted hover:text-primary transition-colors">Home</a></li>
124
+ <li><a href="/membership" class="text-sm text-muted hover:text-primary transition-colors">Membership</a></li>
125
+ <li><a href="/features" class="text-sm text-muted hover:text-primary transition-colors">Features</a></li>
126
+ <li><a href="/faq" class="text-sm text-muted hover:text-primary transition-colors">FAQ</a></li>
127
+ </ul>
128
+ </div>
129
+ <div>
130
+ <h4 class="text-xs font-black uppercase tracking-[0.2em] text-main mb-6">Legal</h4>
131
+ <ul class="space-y-4">
132
+ <li><a href="/privacy" class="text-sm text-muted hover:text-primary transition-colors">Privacy Policy</a></li>
133
+ <li><a href="/terms" class="text-sm text-muted hover:text-primary transition-colors">Terms of Service</a></li>
134
+ <li><a href="/contact" class="text-sm text-muted hover:text-primary transition-colors">Contact Support</a></li>
135
+ </ul>
136
  </div>
 
137
  </div>
138
+ <div class="pt-8 border-t border-border flex flex-col md:row justify-between items-center gap-4">
139
+ <p class="text-[10px] font-bold text-muted uppercase tracking-[0.2em]">&copy; 2026 LibraryLuxe Ecosystem. Built with passion for readers.</p>
140
+ <div class="flex items-center gap-2">
141
+ <div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
142
+ <span class="text-[10px] font-black uppercase tracking-widest text-muted">System Operational</span>
143
+ </div>
144
  </div>
145
  </div>
146
  </footer>
147
+
148
  </div>
149
  </NotAuthorized>
150
  </AuthorizeView>
 
154
  {
155
  await ThemeService.InitializeAsync();
156
  }
157
+
158
+ protected override async Task OnAfterRenderAsync(bool firstRender)
159
+ {
160
+ if (firstRender)
161
+ {
162
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
163
+ var user = authState.User;
164
+
165
+ if (user.Identity?.IsAuthenticated == true)
166
+ {
167
+ try
168
+ {
169
+ var token = await JS.InvokeAsync<string>("getFcmToken");
170
+ if (!string.IsNullOrEmpty(token))
171
+ {
172
+ await ApiClient.UpdateFcmTokenAsync(token);
173
+ }
174
+ }
175
+ catch (Exception ex)
176
+ {
177
+ Console.WriteLine($"FCM Token Error: {ex.Message}");
178
+ }
179
+ }
180
+ }
181
+ }
182
  }
BlazorWebAssembly/Layout/NavMenu.razor CHANGED
@@ -37,6 +37,14 @@
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
 
 
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
+ <NavLink class="nav-link-v" href="/borrowings/manage">
41
+ <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>
42
+ Manage Borrowings
43
+ </NavLink>
44
+ <NavLink class="nav-link-v" href="/rewards/manage">
45
+ <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>
46
+ Manage Rewards
47
+ </NavLink>
48
  </AuthorizeView>
49
  </nav>
50
 
BlazorWebAssembly/Models/MemberDetailsViewModel.cs ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using LibraryManagement.Shared.Models;
2
+ using System.Collections.Generic;
3
+
4
+ namespace BlazorWebAssembly.Models
5
+ {
6
+ public class MemberDetailsViewModel
7
+ {
8
+ public UserDto User { get; set; } = new();
9
+ public SubscriptionDto? CurrentSubscription { get; set; }
10
+ public List<MembershipDto> AvailableMemberships { get; set; } = new();
11
+ }
12
+ }
BlazorWebAssembly/Models/PagedResult.cs ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ namespace BlazorWebAssembly.Models
2
+ {
3
+ public class PagedResult<T>
4
+ {
5
+ public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
6
+ public int TotalCount { get; set; }
7
+ public int PageSize { get; set; }
8
+ public int CurrentPage { get; set; }
9
+ public int TotalPages { get; set; }
10
+ public bool HasPreviousPage => CurrentPage > 1;
11
+ public bool HasNextPage => CurrentPage < TotalPages;
12
+ }
13
+ }
BlazorWebAssembly/Pages/About.razor CHANGED
@@ -1,24 +1,101 @@
1
  @page "/about"
2
 
3
- <div class="max-w-4xl mx-auto py-24 px-8 animate-fade">
4
- <div class="text-center mb-16">
5
- <h1 class="text-6xl font-black tracking-tighter text-main mb-6 uppercase">Our Story</h1>
6
- <p class="text-xl text-muted leading-relaxed">Redefining the relationship between readers and literature through technology and community.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  </div>
8
 
9
- <div class="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
10
- <div class="space-y-8">
11
- <h2 class="text-3xl font-black text-main tracking-tight">A Library without Borders.</h2>
12
- <p class="text-muted leading-relaxed">Founded in 2024, LibraryLuxe was born from a simple idea: that books should be accessible, organized, and rewarding. We've built a system that doesn't just manage inventory, but fosters a culture of reading.</p>
13
- <div class="flex gap-4">
14
- <div class="w-px h-12 bg-primary"></div>
15
- <p class="italic text-main font-medium">"Reading is the ultimate superpower. We just provide the cape."</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  </div>
17
  </div>
18
- <div class="bg-card aspect-square rounded-[3rem] border border-border shadow-2xl flex items-center justify-center p-12">
19
- <div class="w-full h-full rounded-2xl bg-primary/5 border border-primary/20 flex items-center justify-center">
20
- <svg class="w-24 h-24 text-primary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" 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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
  </div>
 
 
 
 
 
23
  </div>
24
  </div>
 
 
1
  @page "/about"
2
 
3
+ <div class="max-w-7xl mx-auto py-24 px-8 animate-fade space-y-32">
4
+ <!-- Hero / Story -->
5
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-24 items-center">
6
+ <div>
7
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Our Origin</h2>
8
+ <h1 class="text-7xl font-black tracking-tighter text-main mb-8 uppercase leading-[0.9]">A Library <br /> <span class="text-primary italic">Without Borders.</span></h1>
9
+ <p class="text-xl text-muted leading-relaxed mb-10">
10
+ Founded in 2024, LibraryLuxe was born from a simple idea: that books should be accessible, organized, and rewarding. We've built a system that doesn't just manage inventory, but fosters a culture of reading.
11
+ </p>
12
+ <div class="bg-card p-8 rounded-3xl border border-border border-l-4 border-l-primary shadow-sm">
13
+ <p class="italic text-main font-medium text-lg leading-relaxed">
14
+ "Reading is the ultimate superpower. We just provide the cape. Our mission is to bridge the gap between classic literature and modern convenience."
15
+ </p>
16
+ <div class="mt-4 flex items-center gap-3">
17
+ <div class="w-8 h-8 rounded-full bg-primary/20"></div>
18
+ <span class="text-xs font-bold uppercase tracking-widest text-muted">Founding Vision</span>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ <div class="relative">
23
+ <div class="bg-card aspect-square rounded-[4rem] border border-border shadow-2xl flex items-center justify-center p-12 relative z-10 overflow-hidden">
24
+ <div class="absolute inset-0 bg-gradient-to-br from-primary/10 to-emerald-500/10"></div>
25
+ <svg class="w-48 h-48 text-primary opacity-20 relative z-20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" 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>
26
+ </div>
27
+ <div class="absolute -bottom-10 -right-10 w-64 h-64 bg-primary/5 rounded-full blur-3xl"></div>
28
+ </div>
29
  </div>
30
 
31
+ <!-- How it Works -->
32
+ <section>
33
+ <div class="text-center mb-20">
34
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">The Process</h2>
35
+ <h3 class="text-5xl font-black text-main tracking-tight">How LibraryLuxe Works</h3>
36
+ </div>
37
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-12 relative">
38
+ <div class="hidden md:block absolute top-1/2 left-0 w-full h-px bg-border -z-10"></div>
39
+ <!-- Step 1 -->
40
+ <div class="bg-body p-8 relative">
41
+ <div class="w-16 h-16 bg-card border border-border rounded-2xl flex items-center justify-center text-2xl font-black text-primary mb-8 shadow-xl">1</div>
42
+ <h4 class="text-xl font-bold text-main mb-4 uppercase">Create Account</h4>
43
+ <p class="text-muted text-sm leading-relaxed">Sign up in seconds and choose a membership plan that fits your reading habits.</p>
44
+ </div>
45
+ <!-- Step 2 -->
46
+ <div class="bg-body p-8 relative">
47
+ <div class="w-16 h-16 bg-card border border-border rounded-2xl flex items-center justify-center text-2xl font-black text-primary mb-8 shadow-xl">2</div>
48
+ <h4 class="text-xl font-bold text-main mb-4 uppercase">Borrow & Read</h4>
49
+ <p class="text-muted text-sm leading-relaxed">Browse thousands of titles and borrow them instantly. Physical books or digital access, your choice.</p>
50
+ </div>
51
+ <!-- Step 3 -->
52
+ <div class="bg-body p-8 relative">
53
+ <div class="w-16 h-16 bg-card border border-border rounded-2xl flex items-center justify-center text-2xl font-black text-primary mb-8 shadow-xl">3</div>
54
+ <h4 class="text-xl font-bold text-main mb-4 uppercase">Earn Rewards</h4>
55
+ <p class="text-muted text-sm leading-relaxed">Return books on time to earn loyalty points. Use points to upgrade your membership or unlock perks.</p>
56
  </div>
57
  </div>
58
+ </section>
59
+
60
+ <!-- Why Choose Us -->
61
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
62
+ <div class="bg-card p-12 rounded-[3rem] border border-border">
63
+ <h4 class="text-2xl font-black text-main mb-8 uppercase tracking-tight">Why Choose Us?</h4>
64
+ <div class="space-y-6">
65
+ <div class="flex gap-4">
66
+ <div class="w-6 h-6 rounded-full bg-green-500/10 flex items-center justify-center text-green-500 mt-1">
67
+ <svg class="w-4 h-4" 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>
68
+ </div>
69
+ <div>
70
+ <h5 class="font-bold text-main">Eco-Friendly Reading</h5>
71
+ <p class="text-sm text-muted">Our digital-first approach reduces paper waste and logistics footprint.</p>
72
+ </div>
73
+ </div>
74
+ <div class="flex gap-4">
75
+ <div class="w-6 h-6 rounded-full bg-green-500/10 flex items-center justify-center text-green-500 mt-1">
76
+ <svg class="w-4 h-4" 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>
77
+ </div>
78
+ <div>
79
+ <h5 class="font-bold text-main">Community Driven</h5>
80
+ <p class="text-sm text-muted">Join reading clubs and community events hosted right in our ecosystem.</p>
81
+ </div>
82
+ </div>
83
+ <div class="flex gap-4">
84
+ <div class="w-6 h-6 rounded-full bg-green-500/10 flex items-center justify-center text-green-500 mt-1">
85
+ <svg class="w-4 h-4" 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>
86
+ </div>
87
+ <div>
88
+ <h5 class="font-bold text-main">Smart Notifications</h5>
89
+ <p class="text-sm text-muted">Advanced Firebase integration keeps you informed about your library activity.</p>
90
+ </div>
91
+ </div>
92
  </div>
93
  </div>
94
+ <div class="bg-primary p-12 rounded-[3rem] text-white flex flex-col justify-center">
95
+ <h4 class="text-4xl font-black tracking-tighter mb-6 uppercase">Join the <br/> Revolution.</h4>
96
+ <p class="text-white/80 mb-8 leading-relaxed font-medium">We're not just a library; we're a community of life-long learners. Be part of our growing family today.</p>
97
+ <a href="/register" class="bg-white text-primary px-8 py-4 rounded-2xl font-black text-center hover:bg-white/90 transition-all uppercase tracking-widest text-sm">Join LibraryLuxe</a>
98
+ </div>
99
  </div>
100
  </div>
101
+
BlazorWebAssembly/Pages/Books.razor CHANGED
@@ -1,143 +1,698 @@
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
  }
 
1
  @page "/books"
2
  @using LibraryManagement.Shared.Models
3
  @using BlazorWebAssembly.Services
4
+ @using Microsoft.AspNetCore.Authorization
5
  @inject LibraryApiClient ApiClient
6
+ @inject WishlistService WishlistService
7
+ @inject IJSRuntime JS
8
  @inject NavigationManager Navigation
9
 
10
+ <div class="max-w-7xl mx-auto py-10 px-6 animate-fade">
11
+ @if (_viewMode == ViewMode.List)
12
+ {
13
+ <!-- Header -->
14
+ <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-12 gap-6">
15
+ <div>
16
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Book Collection</h1>
17
+ <p class="text-muted text-lg">Discover your next favorite story among our curated selection. <span class="text-xs">(@FilteredBooks.Count() total)</span></p>
18
+ </div>
19
+ <div class="flex items-center gap-4 w-full md:w-auto">
20
+ <div class="relative w-full md:w-96">
21
+ <input type="text" @bind="_searchQuery" @bind:event="oninput"
22
+ placeholder="Search by title, author, or ISBN..."
23
+ 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 text-main" />
24
+ </div>
25
+ <AuthorizeView Roles="Librarian">
26
+ <button @onclick="ShowCreate" class="bg-primary text-white px-6 py-3 rounded-xl font-bold hover:bg-primary-hover whitespace-nowrap">
27
+ Add Book
28
+ </button>
29
+ </AuthorizeView>
30
+ </div>
31
  </div>
32
+
33
+ <!-- Quick Filters -->
34
+ <div class="flex flex-wrap items-center gap-3 mb-6">
35
+ <button @onclick='() => SetQuickFilter("All")'
36
+ class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "All" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
37
+ All Books
38
+ </button>
39
+ <button @onclick='() => SetQuickFilter("New Arrivals")'
40
+ class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "New Arrivals" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
41
+ New Arrivals
42
+ </button>
43
+ <button @onclick='() => SetQuickFilter("Trending")'
44
+ class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "Trending" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
45
+ Trending
46
  </button>
47
+ <button @onclick='() => SetQuickFilter("Recommended")'
48
+ class='px-5 py-2.5 rounded-full text-sm font-bold border transition-all @(_activeQuickFilter == "Recommended" ? "bg-main text-body border-main shadow-md" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
49
+ Recommended for You
50
+ </button>
51
+ </div>
52
+
53
+ <!-- Category Filter Dropdown equivalent -->
54
+ @if (_categories.Any())
55
+ {
56
+ <div class="flex flex-wrap gap-2 mb-10">
57
+ <button @onclick="() => FilterByCategory(null)"
58
+ class='px-4 py-1.5 rounded-full text-[11px] font-bold border transition-all @(_selectedCategoryId == null ? "bg-primary text-white border-primary shadow-sm" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
59
+ All Categories
60
+ </button>
61
+ @foreach (var cat in _categories)
62
+ {
63
+ <button @onclick="() => FilterByCategory(cat.Id)"
64
+ class='px-4 py-1.5 rounded-full text-[11px] font-bold border transition-all @(_selectedCategoryId == cat.Id ? "bg-primary text-white border-primary shadow-sm" : "bg-card text-muted border-border hover:border-primary hover:text-primary")'>
65
+ @cat.Name
66
+ </button>
67
+ }
68
+ </div>
69
  }
 
70
 
71
+ <!-- Loading State -->
72
+ @if (_isLoading)
73
+ {
74
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
75
+ @for (int i = 0; i < 8; i++)
76
+ {
77
+ <div class="animate-pulse bg-card rounded-xl border border-border h-96"></div>
78
+ }
79
+ </div>
80
+ }
81
+ else if (!FilteredBooks.Any())
82
+ {
83
+ <div class="text-center py-20 bg-card rounded-2xl border border-dashed border-border">
84
+ <h3 class="text-xl font-bold text-main mb-2">No results found</h3>
85
+ <p class="text-muted">Try adjusting your search or filters.</p>
86
+ </div>
87
+ }
88
+ else
89
+ {
90
+ <!-- Books Grid -->
91
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
92
+ @foreach (var book in PaginatedBooks)
93
+ {
94
+ <div class="group bg-card rounded-xl border border-border overflow-hidden hover:shadow-xl transition-all duration-300 flex flex-col h-full">
95
+ <!-- Cover -->
96
+ <div class="aspect-[2/3] bg-body relative overflow-hidden cursor-pointer" @onclick="() => ShowDetails(book.Id)">
97
+ @if (!string.IsNullOrEmpty(book.CoverUrl))
98
+ {
99
+ <img src="@book.CoverUrl" alt="@book.Title" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
100
+ }
101
+ else
102
+ {
103
+ <div class="w-full h-full flex items-center justify-center text-muted opacity-20">
104
+ <span class="font-bold uppercase tracking-widest text-xs">No Cover</span>
105
+ </div>
106
+ }
107
+
108
+ @if (book.AvailableCopies <= 0)
109
+ {
110
+ <div class="absolute inset-0 bg-black/60 backdrop-blur-[2px] flex items-center justify-center">
111
+ <span class="bg-white text-black px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest">Out of Stock</span>
112
+ </div>
113
+ }
114
+ </div>
115
+
116
+ <!-- Content -->
117
+ <div class="p-5 flex-grow flex flex-col">
118
+ <h3 class="font-bold text-main line-clamp-1 mb-1 group-hover:text-primary transition-colors cursor-pointer" @onclick="() => ShowDetails(book.Id)">@book.Title</h3>
119
+ <p class="text-xs text-muted mb-3 font-medium">@book.Author</p>
120
+
121
+ <div class="flex items-center gap-2 mb-4 flex-wrap">
122
+ @foreach (var cat in book.Categories.Take(2))
123
+ {
124
+ <span class="text-[9px] font-bold uppercase tracking-wider text-primary bg-primary/10 px-2 py-0.5 rounded-full">@cat.Name</span>
125
+ }
126
+ </div>
127
+
128
+ <div class="mt-auto flex flex-col gap-4">
129
+ <div class="flex items-center justify-between">
130
+ <span class="text-[10px] font-bold text-muted bg-body px-2 py-1 rounded border border-border">@book.Status</span>
131
+ <span class="text-xs font-bold @(book.AvailableCopies > 0 ? "text-green-500" : "text-red-400")">
132
+ @book.AvailableCopies / @book.TotalCopies
133
+ </span>
134
+ </div>
135
+
136
+ <div class="flex items-center gap-2">
137
+ <button @onclick="() => ShowDetails(book.Id)" class="flex-1 bg-body text-main border border-border py-1.5 rounded-lg text-[11px] font-bold hover:bg-card">Details</button>
138
+
139
+ <AuthorizeView Roles="Member">
140
+ <div class="flex gap-1">
141
+ <button @onclick="() => ToggleFavourite(book)"
142
+ class='p-1.5 rounded-lg border transition-all @(IsFavourite(book.Id) ? "border-red-200 bg-red-500/10 text-red-500" : "border-border text-muted hover:bg-red-500/10 hover:text-red-500")'
143
+ title='@(IsFavourite(book.Id) ? "Remove from Favourites" : "Add to Favourites")'>
144
+ <svg class="w-4 h-4" fill='@(IsFavourite(book.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
145
+ <path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001z"></path>
146
+ </svg>
147
+ </button>
148
+ @if (book.AvailableCopies == 0)
149
+ {
150
+ <button @onclick="() => ToggleWishlist(book)"
151
+ class='p-1.5 rounded-lg border transition-all @(IsInWishlist(book.Id) ? "border-amber-200 bg-amber-500/10 text-amber-500" : "border-border text-muted hover:bg-amber-500/10 hover:text-amber-500")'
152
+ title='@(IsInWishlist(book.Id) ? "Remove from Wishlist" : "Add to Wishlist")'>
153
+ <svg class="w-4 h-4" fill='@(IsInWishlist(book.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
154
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
155
+ </svg>
156
+ </button>
157
+ }
158
+ </div>
159
+ </AuthorizeView>
160
+ <AuthorizeView Roles="Librarian">
161
+ <button @onclick="() => ShowEdit(book.Id)" class="p-1.5 rounded-lg border border-border hover:bg-body text-muted">
162
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
163
+ </button>
164
+ </AuthorizeView>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ }
170
+ </div>
171
+
172
+ <!-- Pagination -->
173
+ @if (TotalPages > 1)
174
  {
175
+ <div class="mt-12 flex items-center justify-center gap-2">
176
+ <button disabled="@(_currentPage == 1)" @onclick="() => _currentPage--"
177
+ class="p-2 rounded-lg border border-border text-muted hover:bg-body disabled:opacity-30 disabled:cursor-not-allowed transition-all">
178
+ <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="M15 19l-7-7 7-7"></path></svg>
179
+ </button>
180
+
181
+ @for (int i = 1; i <= TotalPages; i++)
182
+ {
183
+ var pageNumber = i;
184
+ <button @onclick="() => _currentPage = pageNumber"
185
+ class='w-10 h-10 rounded-lg border text-sm font-bold transition-all @(_currentPage == pageNumber ? "bg-main border-main text-body shadow-md" : "bg-card border-border text-muted hover:border-primary hover:text-primary")'>
186
+ @pageNumber
187
+ </button>
188
+ }
189
+
190
+ <button disabled="@(_currentPage == TotalPages)" @onclick="() => _currentPage++"
191
+ class="p-2 rounded-lg border border-border text-muted hover:bg-body disabled:opacity-30 disabled:cursor-not-allowed transition-all">
192
+ <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="M9 5l7 7-7 7"></path></svg>
193
+ </button>
194
+ </div>
195
  }
196
+ }
 
 
 
 
 
 
 
197
  }
198
+ else if (_viewMode == ViewMode.Details && _selectedBook != null)
199
  {
200
+ <div class="mb-8 animate-fade">
201
+ <button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
202
+ <svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
203
+ Back to Catalog
204
+ </button>
205
+
206
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
207
+ <div class="lg:col-span-1">
208
+ <div class="aspect-[2/3] w-full rounded-3xl bg-body border border-border shadow-sm flex items-center justify-center text-muted overflow-hidden">
209
+ @if (!string.IsNullOrWhiteSpace(_selectedBook.CoverUrl))
210
  {
211
+ <img src="@_selectedBook.CoverUrl" alt="@_selectedBook.Title" class="w-full h-full object-cover" />
212
  }
213
  else
214
  {
215
+ <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
 
 
 
 
 
 
 
 
 
216
  }
217
  </div>
218
+ </div>
219
 
220
+ <div class="lg:col-span-2 space-y-8">
221
+ <div>
222
+ <span class="inline-flex items-center rounded-full bg-body border border-border px-3 py-1 text-xs font-semibold text-muted mb-4 tracking-wide uppercase">@_selectedBook.Status</span>
223
+ <h1 class="text-4xl font-extrabold text-main tracking-tight">@_selectedBook.Title</h1>
224
+ <p class="text-xl text-muted mt-2 font-medium">by @_selectedBook.Author</p>
225
+ </div>
226
+
227
+ <div class="prose max-w-none">
228
+ <h3 class="text-lg font-bold text-main border-b border-border pb-2 mb-4 uppercase tracking-wider text-sm">Description</h3>
229
+ <p class="text-muted leading-relaxed">
230
+ @(string.IsNullOrEmpty(_selectedBook.Description) ? "No description available for this book." : _selectedBook.Description)
231
+ </p>
232
+ </div>
233
+
234
+ <div class="grid grid-cols-2 gap-8 border-y border-border py-6">
235
+ <div>
236
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-1">ISBN</h4>
237
+ <p class="text-sm font-semibold text-main">@_selectedBook.Isbn</p>
238
  </div>
239
+ <div>
240
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-1">Availability</h4>
241
+ <p class="text-sm font-semibold @(_selectedBook.AvailableCopies > 0 ? "text-green-500" : "text-red-500")">
242
+ @(_selectedBook.AvailableCopies > 0 ? $"{_selectedBook.AvailableCopies} / {_selectedBook.TotalCopies} units in stock" : "Currently occupied")
243
+ </p>
 
 
 
244
  </div>
245
  </div>
246
+
247
+ <div class="flex flex-wrap items-center gap-4 pt-4">
248
+ <AuthorizeView Roles="Member">
249
+ <div class="flex items-center gap-3">
250
+ @if (_selectedBook.AvailableCopies > 0)
251
+ {
252
+ <button @onclick="() => BorrowAsync(_selectedBook.Id)" class="bg-primary text-white py-3 px-8 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Borrow This Book</button>
253
+ }
254
+ else
255
+ {
256
+ <div class="flex flex-col gap-2">
257
+ <button disabled class="bg-body border border-border text-muted py-3 px-8 rounded-xl font-bold opacity-50 cursor-not-allowed">Out of Stock</button>
258
+ <button @onclick="() => ToggleWishlist(_selectedBook)"
259
+ class='text-xs font-bold flex items-center gap-1.5 transition-colors @(IsInWishlist(_selectedBook.Id) ? "text-amber-500 underline" : "text-muted hover:text-amber-500 hover:underline")'>
260
+ <svg class="w-4 h-4" fill='@(IsInWishlist(_selectedBook.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
261
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path>
262
+ </svg>
263
+ @(IsInWishlist(_selectedBook.Id) ? "In Wishlist (Notifying)" : "Add to Wishlist for Notification")
264
+ </button>
265
+ </div>
266
+ }
267
+ <button @onclick="() => ToggleFavourite(_selectedBook)"
268
+ class='p-3 rounded-xl border transition-all shadow-sm active:scale-95 @(IsFavourite(_selectedBook.Id) ? "border-red-200 bg-red-500/10 text-red-500" : "border-border text-muted hover:bg-red-500/10 hover:text-red-500")'
269
+ title='@(IsFavourite(_selectedBook.Id) ? "Remove from Favourites" : "Add to Favourites")'>
270
+ <svg class="w-6 h-6" fill='@(IsFavourite(_selectedBook.Id) ? "currentColor" : "none")' stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
271
+ <path d="M11.645 20.91l-.007-.003-.022-.012a15.247 15.247 0 01-.383-.218 25.18 25.18 0 01-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0112 5.052 5.5 5.5 0 0116.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 01-4.244 3.17 15.247 15.247 0 01-.383.219l-.022.012-.007.004-.003.001z"></path>
272
+ </svg>
273
+ </button>
274
+ </div>
275
+ </AuthorizeView>
276
+
277
+ <AuthorizeView Roles="Librarian">
278
+ <button @onclick="() => ShowEdit(_selectedBook.Id)" class="bg-body border border-border text-main py-3 px-8 rounded-xl font-bold hover:bg-card">Edit Details</button>
279
+ <button @onclick="() => DeleteAsync(_selectedBook.Id)" class="p-3 text-red-500 hover:text-red-600 hover:bg-red-500/10 rounded-xl transition-colors">
280
+ <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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
281
+ </button>
282
+ </AuthorizeView>
283
+
284
+ <AuthorizeView>
285
+ <NotAuthorized>
286
+ <a href="/login" class="bg-primary text-white py-3 px-8 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20">Sign In to Borrow</a>
287
+ </NotAuthorized>
288
+ </AuthorizeView>
289
+ </div>
290
  </div>
291
+ </div>
292
+ </div>
293
+ }
294
+ else if (_viewMode == ViewMode.Create || _viewMode == ViewMode.Edit)
295
+ {
296
+ <div class="mb-8 animate-fade">
297
+ <button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
298
+ <svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
299
+ Back to Catalog
300
+ </button>
301
+
302
+ <div class="max-w-3xl mx-auto">
303
+ <div class="mb-8">
304
+ <h1 class="text-3xl font-bold tracking-tight text-main">@(_viewMode == ViewMode.Create ? "Add New Book" : "Edit Book")</h1>
305
+ <p class="mt-2 text-muted">Fill in the details below to @(_viewMode == ViewMode.Create ? "add a new book to" : "update the") library catalog.</p>
306
+ </div>
307
+
308
+ <div class="bg-card border border-border p-6 sm:p-8 rounded-3xl shadow-sm">
309
+ <EditForm Model="@_formModel" OnValidSubmit="HandleSubmit" class="space-y-6">
310
+ <DataAnnotationsValidator />
311
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
312
+ <div class="sm:col-span-2">
313
+ <label class="block text-sm font-medium text-main mb-1">Book Title <span class="text-red-500">*</span></label>
314
+ <InputText @bind-Value="_formModel.Title" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="e.g. The Great Gatsby" required />
315
+ </div>
316
+
317
+ <div>
318
+ <label class="block text-sm font-medium text-main mb-1">Author <span class="text-red-500">*</span></label>
319
+ <InputText @bind-Value="_formModel.Author" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="e.g. F. Scott Fitzgerald" required />
320
+ </div>
321
+
322
+ <div>
323
+ <label class="block text-sm font-medium text-main mb-1">ISBN <span class="text-red-500">*</span></label>
324
+ <InputText @bind-Value="_formModel.Isbn" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="e.g. 978-0743273565" required />
325
+ </div>
326
+
327
+ <div>
328
+ <label class="block text-sm font-medium text-main mb-1">Total Copies <span class="text-red-500">*</span></label>
329
+ <InputNumber @bind-Value="_formModel.TotalCopies" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="1" required />
330
+ </div>
331
+
332
+ <div>
333
+ <label class="block text-sm font-medium text-main mb-1">Cover Image URL</label>
334
+ <InputText @bind-Value="_formModel.CoverUrl" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="https://example.com/cover.jpg" />
335
+ </div>
336
+
337
+ <div class="hidden sm:block"></div>
338
+
339
+ <div class="sm:col-span-2">
340
+ <label class="block text-sm font-medium text-main mb-1">Description</label>
341
+ <InputTextArea @bind-Value="_formModel.Description" rows="4" class="w-full bg-body border border-border rounded-xl px-4 py-2 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main" placeholder="Brief summary of the book..."></InputTextArea>
342
+ </div>
343
+
344
+ <div class="sm:col-span-2">
345
+ <label class="block text-sm font-medium text-main mb-2">Categories</label>
346
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
347
+ @foreach (var cat in _categories)
348
+ {
349
+ <label class="flex items-center gap-2 px-3 py-2 rounded-xl border border-border bg-body hover:bg-card cursor-pointer text-sm text-main transition-colors">
350
+ <input type="checkbox" checked="@(_formModel.CategoryIds.Contains(cat.Id))"
351
+ @onchange='(e) => ToggleCategory(cat.Id, (bool)e.Value!)'
352
+ class="h-4 w-4 rounded border-border text-primary focus:ring-primary" />
353
+ @cat.Name
354
+ </label>
355
+ }
356
+ </div>
357
+ </div>
358
+ </div>
359
+
360
+ <div class="pt-6 border-t border-border flex justify-end gap-3 mt-8">
361
+ <button type="button" @onclick="ShowList" class="bg-body border border-border text-main px-6 py-2.5 rounded-xl font-bold hover:bg-card">Cancel</button>
362
+ <button type="submit" class="bg-primary text-white px-6 py-2.5 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20" disabled="@_isSubmitting">
363
+ @(_isSubmitting ? "Saving..." : (_viewMode == ViewMode.Create ? "Add Book" : "Update Book"))
364
+ </button>
365
+ </div>
366
+ </EditForm>
367
+ </div>
368
+ </div>
369
  </div>
370
  }
371
  </div>
372
 
373
  @code {
374
+ private enum ViewMode { List, Details, Create, Edit }
375
+ private ViewMode _viewMode = ViewMode.List;
376
  private bool _isLoading = true;
377
+ private bool _isSubmitting = false;
378
  private string _searchQuery = "";
379
  private int? _selectedCategoryId;
380
+ private string _activeQuickFilter = "All";
381
+
382
+ // Pagination
383
+ private int _currentPage = 1;
384
+ private int _pageSize = 12;
385
+
386
+ private IEnumerable<BookDto> _books = Enumerable.Empty<BookDto>();
387
+ private IEnumerable<CategoryDto> _categories = Enumerable.Empty<CategoryDto>();
388
+ private IEnumerable<BorrowingDto> _myBorrowings = Enumerable.Empty<BorrowingDto>();
389
+ private IEnumerable<BorrowingDto> _allBorrowings = Enumerable.Empty<BorrowingDto>();
390
+ private BookDto? _selectedBook;
391
+
392
+ // Form Model
393
+ private BookFormModel _formModel = new();
394
 
395
  protected override async Task OnInitializedAsync()
396
  {
397
+ await LoadDataAsync();
398
  }
399
 
400
+ private async Task LoadDataAsync()
401
  {
402
  _isLoading = true;
403
  try
404
  {
405
+ _books = await ApiClient.GetBooksAsync();
406
+ _categories = await ApiClient.GetCategoriesAsync();
407
+
408
+ // Fetch borrowings for Trending and Recommended
409
+ _myBorrowings = await ApiClient.GetMyBorrowingsAsync();
410
+ try {
411
+ _allBorrowings = await ApiClient.GetAllBorrowingsAsync();
412
+ } catch { }
413
+
414
+ _currentPage = 1;
415
+ }
416
+ catch (Exception ex)
417
+ {
418
+ Console.WriteLine($"Error: {ex.Message}");
419
+ }
420
+ finally
421
+ {
422
+ _isLoading = false;
423
+ }
424
+ }
425
+
426
+ private IEnumerable<BookDto> FilteredBooks
427
+ {
428
+ get
429
+ {
430
+ var books = _books;
431
+
432
+ // Apply Search
433
+ if (!string.IsNullOrWhiteSpace(_searchQuery))
434
+ {
435
+ books = books.Where(b => b.Title.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) ||
436
+ b.Author.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) ||
437
+ b.Isbn.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase));
438
+ }
439
+
440
+ // Apply Category Filter
441
+ if (_selectedCategoryId != null)
442
+ {
443
+ books = books.Where(b => b.Categories.Any(c => c.Id == _selectedCategoryId));
444
+ }
445
+
446
+ // Apply Quick Filter
447
+ switch (_activeQuickFilter)
448
+ {
449
+ case "New Arrivals":
450
+ books = books.Where(b => b.CreatedAt >= DateTime.UtcNow.AddDays(-7));
451
+ break;
452
+ case "Trending":
453
+ var trendingIds = _allBorrowings
454
+ .Where(br => br.BorrowDate >= DateTime.UtcNow.AddDays(-7))
455
+ .GroupBy(br => br.BookId)
456
+ .OrderByDescending(g => g.Count())
457
+ .Select(g => g.Key)
458
+ .Take(12)
459
+ .ToList();
460
+
461
+ if (trendingIds.Any())
462
+ books = books.Where(b => trendingIds.Contains(b.Id));
463
+ else
464
+ books = Enumerable.Empty<BookDto>(); // Or show nothing if no data
465
+ break;
466
+ case "Recommended":
467
+ var myCategoryIds = _myBorrowings.SelectMany(br => _books.FirstOrDefault(b => b.Id == br.BookId)?.Categories.Select(c => c.Id) ?? Enumerable.Empty<int>())
468
+ .Concat(_favourites.SelectMany(b => b.Categories.Select(c => c.Id)))
469
+ .Concat(_wishlist.SelectMany(b => b.Categories.Select(c => c.Id)))
470
+ .Distinct()
471
+ .ToList();
472
+
473
+ if (myCategoryIds.Any())
474
+ books = books.Where(b => b.Categories.Any(c => myCategoryIds.Contains(c.Id))).Take(8);
475
+ break;
476
+ }
477
+
478
+ return books;
479
  }
 
 
480
  }
481
 
482
+ private IEnumerable<BookDto> PaginatedBooks => FilteredBooks
483
+ .Skip((_currentPage - 1) * _pageSize)
484
+ .Take(_pageSize);
485
+
486
+ private int TotalPages => (int)Math.Ceiling(FilteredBooks.Count() / (double)_pageSize);
487
+
488
+ private void SetQuickFilter(string filter)
489
+ {
490
+ _activeQuickFilter = filter;
491
+ _selectedCategoryId = null; // Clear category when switching quick filters
492
+ _currentPage = 1;
493
+ }
494
 
495
  private void FilterByCategory(int? id)
496
  {
497
  _selectedCategoryId = id;
498
+ _currentPage = 1;
499
+ }
500
+
501
+ private void ShowList()
502
+ {
503
+ _viewMode = ViewMode.List;
504
+ _selectedBook = null;
505
+ }
506
+
507
+ private async Task ShowDetails(int id)
508
+ {
509
+ _isLoading = true;
510
+ _selectedBook = await ApiClient.GetBookAsync(id);
511
+ _viewMode = ViewMode.Details;
512
+ _isLoading = false;
513
+ }
514
+
515
+ private void ShowCreate()
516
+ {
517
+ _formModel = new BookFormModel();
518
+ _viewMode = ViewMode.Create;
519
+ }
520
+
521
+ private async Task ShowEdit(int id)
522
+ {
523
+ _isLoading = true;
524
+ var book = await ApiClient.GetBookAsync(id);
525
+ if (book != null)
526
+ {
527
+ _selectedBook = book;
528
+ _formModel = new BookFormModel
529
+ {
530
+ Id = book.Id,
531
+ Title = book.Title,
532
+ Author = book.Author,
533
+ Isbn = book.Isbn,
534
+ Description = book.Description,
535
+ TotalCopies = book.TotalCopies,
536
+ CoverUrl = book.CoverUrl,
537
+ CategoryIds = book.Categories.Select(c => c.Id).ToList()
538
+ };
539
+ _viewMode = ViewMode.Edit;
540
+ }
541
+ _isLoading = false;
542
+ }
543
+
544
+ private void ToggleCategory(int id, bool isChecked)
545
+ {
546
+ if (isChecked && !_formModel.CategoryIds.Contains(id))
547
+ _formModel.CategoryIds.Add(id);
548
+ else if (!isChecked)
549
+ _formModel.CategoryIds.Remove(id);
550
+ }
551
+
552
+ private List<BookDto> _wishlist = new();
553
+ private List<BookDto> _favourites = new();
554
+
555
+ protected override async Task OnAfterRenderAsync(bool firstRender)
556
+ {
557
+ if (firstRender)
558
+ {
559
+ await LoadCollectionsAsync();
560
+ StateHasChanged();
561
+ }
562
+ }
563
+
564
+ private async Task LoadCollectionsAsync()
565
+ {
566
+ _wishlist = await WishlistService.GetWishlistAsync();
567
+ _favourites = await WishlistService.GetFavouritesAsync();
568
+ }
569
+
570
+ private bool IsFavourite(int id) => _favourites.Any(b => b.Id == id);
571
+ private bool IsInWishlist(int id) => _wishlist.Any(b => b.Id == id);
572
+
573
+ private async Task ToggleFavourite(BookDto book)
574
+ {
575
+ if (IsFavourite(book.Id))
576
+ {
577
+ await WishlistService.RemoveFromFavouriteAsync(book.Id);
578
+ await JS.InvokeVoidAsync("alert", $"Removed \"{book.Title}\" from favourites");
579
+ }
580
+ else
581
+ {
582
+ await WishlistService.AddToFavouritesAsync(book);
583
+ await JS.InvokeVoidAsync("alert", $"Added \"{book.Title}\" to favourites!");
584
+ }
585
+ await LoadCollectionsAsync();
586
+ }
587
+
588
+ private async Task ToggleWishlist(BookDto book)
589
+ {
590
+ if (IsInWishlist(book.Id))
591
+ {
592
+ await WishlistService.RemoveFromWishlistAsync(book.Id);
593
+ await JS.InvokeVoidAsync("alert", $"Removed \"{book.Title}\" from wishlist");
594
+ }
595
+ else
596
+ {
597
+ await WishlistService.AddToWishlistAsync(book);
598
+ await JS.InvokeVoidAsync("alert", $"Added \"{book.Title}\" to wishlist!");
599
+ }
600
+ await LoadCollectionsAsync();
601
+ }
602
+
603
+ private async Task HandleSubmit()
604
+ {
605
+ _isSubmitting = true;
606
+ try
607
+ {
608
+ if (_viewMode == ViewMode.Create)
609
+ {
610
+ var request = new BookCreateRequest
611
+ {
612
+ Title = _formModel.Title,
613
+ Author = _formModel.Author,
614
+ Isbn = _formModel.Isbn,
615
+ Description = _formModel.Description,
616
+ TotalCopies = _formModel.TotalCopies,
617
+ CoverUrl = _formModel.CoverUrl,
618
+ CategoryIds = _formModel.CategoryIds
619
+ };
620
+ var result = await ApiClient.CreateBookAsync(request);
621
+ if (result != null)
622
+ {
623
+ await JS.InvokeVoidAsync("alert", "Book added successfully!");
624
+ await LoadDataAsync();
625
+ ShowList();
626
+ }
627
+ }
628
+ else
629
+ {
630
+ var request = new BookUpdateRequest
631
+ {
632
+ Title = _formModel.Title,
633
+ Author = _formModel.Author,
634
+ Description = _formModel.Description,
635
+ TotalCopies = _formModel.TotalCopies,
636
+ CoverUrl = _formModel.CoverUrl,
637
+ CategoryIds = _formModel.CategoryIds
638
+ };
639
+ var result = await ApiClient.UpdateBookAsync(_formModel.Id, request);
640
+ if (result != null)
641
+ {
642
+ await JS.InvokeVoidAsync("alert", "Book updated successfully!");
643
+ await LoadDataAsync();
644
+ ShowList();
645
+ }
646
+ }
647
+ }
648
+ catch (Exception ex)
649
+ {
650
+ await JS.InvokeVoidAsync("alert", $"Error: {ex.Message}");
651
+ }
652
+ finally
653
+ {
654
+ _isSubmitting = false;
655
+ }
656
+ }
657
+
658
+ private async Task DeleteAsync(int id)
659
+ {
660
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to remove this book from the catalog?");
661
+ if (!confirmed) return;
662
+
663
+ var success = await ApiClient.DeleteBookAsync(id);
664
+ if (success)
665
+ {
666
+ await JS.InvokeVoidAsync("alert", "Book deleted successfully");
667
+ await LoadDataAsync();
668
+ ShowList();
669
+ }
670
+ }
671
+
672
+ private async Task BorrowAsync(int id)
673
+ {
674
+ var result = await ApiClient.BorrowBookAsync(new BorrowRequest { BookId = id });
675
+ if (result != null)
676
+ {
677
+ await JS.InvokeVoidAsync("alert", "Book borrowed successfully! Check 'My Loans' for details.");
678
+ await LoadDataAsync();
679
+ await ShowDetails(id);
680
+ }
681
+ else
682
+ {
683
+ await JS.InvokeVoidAsync("alert", "Failed to borrow book. Check your loan limit.");
684
+ }
685
  }
686
 
687
+ private class BookFormModel
688
  {
689
+ public int Id { get; set; }
690
+ public string Title { get; set; } = "";
691
+ public string Author { get; set; } = "";
692
+ public string Isbn { get; set; } = "";
693
+ public string? Description { get; set; }
694
+ public int TotalCopies { get; set; } = 1;
695
+ public string? CoverUrl { get; set; }
696
+ public List<int> CategoryIds { get; set; } = new();
697
  }
698
  }
BlazorWebAssembly/Pages/Borrowings.razor CHANGED
@@ -3,6 +3,7 @@
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 -->
@@ -54,28 +55,51 @@
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
  }
@@ -106,10 +130,21 @@
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
 
3
  @using BlazorWebAssembly.Services
4
  @inject LibraryApiClient ApiClient
5
  @inject NavigationManager Navigation
6
+ @inject IJSRuntime JS
7
 
8
  <div class="max-w-6xl mx-auto py-12 px-6">
9
  <!-- Header -->
 
55
  <p class="text-sm text-muted font-medium mb-4 md:mb-0">ID: @loan.BookId</p>
56
  </div>
57
 
58
+ <!-- Dates & Fines -->
59
+ <div class="grid grid-cols-2 md:grid-cols-3 gap-8 px-8 border-x border-border hidden lg:grid flex-grow">
60
  <div>
61
  <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-1">Borrowed</p>
62
  <p class="text-sm font-bold text-main">@loan.BorrowDate.ToString("MMM dd, yyyy")</p>
63
  </div>
64
  <div>
65
  <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-1">Due Date</p>
66
+ <p class="text-sm font-bold @(loan.DueDate < DateTime.UtcNow && loan.Status == "Borrowed" ? "text-red-500" : "text-main")">
67
  @loan.DueDate.ToString("MMM dd, yyyy")
68
  </p>
69
  </div>
70
+ <div class="hidden md:block">
71
+ @if (loan.ReturnDate.HasValue)
72
+ {
73
+ <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-1">Returned</p>
74
+ <p class="text-sm font-bold text-green-500">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</p>
75
+ }
76
+ else if (loan.FineAmount > 0)
77
+ {
78
+ <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-1">Fine</p>
79
+ <p class="text-sm font-bold @(loan.IsFinePaid ? "text-muted line-through" : "text-red-500")">
80
+ MMK @loan.FineAmount.ToString("N0")
81
+ </p>
82
+ @if (loan.IsFinePaid)
83
+ {
84
+ <span class="text-[10px] text-green-500 font-black uppercase">Paid</span>
85
+ }
86
+ }
87
+ else
88
+ {
89
+ <p class="text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-1">Fine</p>
90
+ <p class="text-sm font-bold text-muted">No Fines</p>
91
+ }
92
+ </div>
93
  </div>
94
 
95
  <!-- Status & Action -->
96
  <div class="flex flex-col md:items-end gap-3 min-w-[150px]">
97
  <span class='px-4 py-1.5 rounded-full text-[10px] font-black uppercase tracking-widest text-center @GetStatusColor(loan.Status)'>
98
+ @(loan.Status == "PendingReturn" ? "Pending" : loan.Status)
99
  </span>
100
  @if (loan.Status == "Borrowed")
101
  {
102
+ <button @onclick="() => ReturnRequestAsync(loan)" class="text-xs font-bold text-primary hover:underline">
103
  Request Return
104
  </button>
105
  }
 
130
  finally { _isLoading = false; }
131
  }
132
 
133
+ private async Task ReturnRequestAsync(BorrowingDto loan)
134
  {
135
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Request return for \"{loan.BookTitle}\"? It will be marked as pending until approved by a librarian.");
136
+ if (!confirmed) return;
137
+
138
+ var success = await ApiClient.RequestReturnAsync(loan.Id);
139
+ if (success)
140
+ {
141
+ await JS.InvokeVoidAsync("alert", "Return request submitted successfully!");
142
+ await LoadData();
143
+ }
144
+ else
145
+ {
146
+ await JS.InvokeVoidAsync("alert", "Failed to submit return request.");
147
+ }
148
  }
149
 
150
  private string GetStatusColor(string status) => status switch
BlazorWebAssembly/Pages/BorrowingsManage.razor ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/borrowings/manage"
2
+ @using BlazorWebAssembly.Services
3
+ @using LibraryManagement.Shared.Models
4
+ @using Microsoft.AspNetCore.Authorization
5
+ @inject LibraryApiClient ApiClient
6
+ @inject IJSRuntime JS
7
+ @attribute [Authorize(Roles = "Librarian")]
8
+
9
+ <div class="max-w-7xl mx-auto py-10 px-6 animate-fade">
10
+ <div class="flex flex-col md:flex-row md:items-center justify-between mb-10 gap-6">
11
+ <div>
12
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Manage Borrowings</h1>
13
+ <p class="text-muted text-lg font-medium">Monitor and process library loans across all members.</p>
14
+ </div>
15
+ <div class="flex gap-3">
16
+ <div class="bg-card px-6 py-3 flex items-center gap-3 border border-border rounded-xl shadow-sm">
17
+ <span class="text-xs font-black text-muted uppercase tracking-widest">Total Active:</span>
18
+ <span class="text-2xl font-black text-primary">@_borrowings.Count(m => m.Status == "Borrowed")</span>
19
+ </div>
20
+ </div>
21
+ </div>
22
+
23
+ @if (_isLoading)
24
+ {
25
+ <div class="space-y-4">
26
+ @for (int i = 0; i < 5; i++)
27
+ {
28
+ <div class="animate-pulse bg-card rounded-xl border border-border h-24 w-full"></div>
29
+ }
30
+ </div>
31
+ }
32
+ else
33
+ {
34
+ <div class="bg-card rounded-2xl border border-border overflow-hidden shadow-sm">
35
+ <div class="overflow-x-auto">
36
+ <table class="w-full text-left border-collapse min-w-max whitespace-nowrap">
37
+ <thead class="bg-body border-b border-border">
38
+ <tr>
39
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Book & Loan Info</th>
40
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Member Details</th>
41
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Loan Dates</th>
42
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest text-center">Status</th>
43
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Fines</th>
44
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest text-right">Actions</th>
45
+ </tr>
46
+ </thead>
47
+ <tbody class="divide-y divide-border">
48
+ @if (!_borrowings.Any())
49
+ {
50
+ <tr>
51
+ <td colspan="6" class="px-6 py-16 text-center text-muted italic bg-body">
52
+ No borrowing records found in the system.
53
+ </td>
54
+ </tr>
55
+ }
56
+ @foreach (var loan in PaginatedBorrowings)
57
+ {
58
+ <tr class="hover:bg-body transition-colors group">
59
+ <td class="px-6 py-5">
60
+ <div class="flex items-center gap-4">
61
+ <div class="w-10 h-14 bg-primary/10 rounded-lg flex items-center justify-center text-primary border border-primary/20">
62
+ <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 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>
63
+ </div>
64
+ <div class="min-w-0">
65
+ <p class="text-sm font-bold text-main truncate mb-0.5">@loan.BookTitle</p>
66
+ <p class="text-[10px] font-bold text-muted uppercase tracking-tight">ID: #@loan.Id</p>
67
+ </div>
68
+ </div>
69
+ </td>
70
+ <td class="px-6 py-5">
71
+ <div class="flex flex-col gap-1">
72
+ <p class="text-sm font-semibold text-main">@loan.UserEmail</p>
73
+ <p class="text-[10px] text-muted font-bold tracking-widest uppercase">UID: #@loan.UserId</p>
74
+ </div>
75
+ </td>
76
+ <td class="px-6 py-5">
77
+ <div class="grid gap-1">
78
+ <div class="flex items-center gap-2 text-[11px]">
79
+ <span class="text-muted font-bold uppercase tracking-widest w-12">Out:</span>
80
+ <span class="font-bold text-main">@loan.BorrowDate.ToString("MMM dd, yyyy")</span>
81
+ </div>
82
+ <div class="flex items-center gap-2 text-[11px]">
83
+ <span class="text-muted font-bold uppercase tracking-widest w-12">Due:</span>
84
+ @{
85
+ var isOverdue = loan.DueDate < DateTime.Now && loan.Status == "Borrowed";
86
+ }
87
+ <span class="font-bold @(isOverdue ? "text-red-500 animate-pulse" : "text-amber-500")">@loan.DueDate.ToString("MMM dd, yyyy")</span>
88
+ </div>
89
+ @if (loan.ReturnDate.HasValue)
90
+ {
91
+ <div class="flex items-center gap-2 text-[11px]">
92
+ <span class="text-muted font-bold uppercase tracking-widest w-12">Back:</span>
93
+ <span class="font-bold text-green-500">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span>
94
+ </div>
95
+ }
96
+ </div>
97
+ </td>
98
+ <td class="px-6 py-5 text-center">
99
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest
100
+ @(loan.Status == "Borrowed" ? "bg-amber-500/10 text-amber-500" :
101
+ loan.Status == "PendingReturn" ? "bg-blue-500/10 text-blue-500 shadow-sm" :
102
+ "bg-green-500/10 text-green-500")">
103
+ @(loan.Status == "PendingReturn" ? "Pending Approval" : loan.Status)
104
+ </span>
105
+ </td>
106
+ <td class="px-6 py-5">
107
+ @if (loan.FineAmount > 0)
108
+ {
109
+ <div class="flex flex-col">
110
+ <span class="text-sm font-extrabold @(loan.IsFinePaid ? "text-muted line-through" : "text-red-500")">
111
+ MMK @loan.FineAmount.ToString("N0")
112
+ </span>
113
+ @if (loan.IsFinePaid)
114
+ {
115
+ <span class="text-[9px] text-green-500 font-black uppercase mt-0.5 tracking-tighter">Settled</span>
116
+ }
117
+ else
118
+ {
119
+ <span class="text-[9px] text-red-400 font-black uppercase mt-0.5 tracking-tighter">Pending</span>
120
+ }
121
+ </div>
122
+ }
123
+ else
124
+ {
125
+ <span class="text-[10px] text-muted font-bold italic">No Fines</span>
126
+ }
127
+ </td>
128
+ <td class="px-6 py-5 text-right">
129
+ @if (loan.Status == "PendingReturn")
130
+ {
131
+ <button @onclick="() => ApproveReturnAsync(loan)"
132
+ class="inline-flex items-center gap-2 bg-green-500/10 text-green-500 border border-green-500/20 px-4 py-2 rounded-lg text-xs font-bold hover:bg-green-500 hover:text-white transition-all shadow-sm active:scale-95">
133
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg>
134
+ Approve Return
135
+ </button>
136
+ }
137
+ else if (loan.Status == "Borrowed")
138
+ {
139
+ <button @onclick="() => ForceReturnAsync(loan)"
140
+ class="inline-flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-primary-hover transition-all shadow-sm active:scale-95">
141
+ Process Return
142
+ </button>
143
+ }
144
+ else
145
+ {
146
+ <div class="text-green-500 flex justify-end">
147
+ <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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
148
+ </div>
149
+ }
150
+ </td>
151
+ </tr>
152
+ }
153
+ </tbody>
154
+ </table>
155
+ </div>
156
+
157
+ @if (TotalPages > 1)
158
+ {
159
+ <div class="px-6 py-4 border-t border-border flex items-center justify-between bg-body">
160
+ <p class="text-xs text-muted">
161
+ Showing <span class="font-bold text-main">@((_currentPage - 1) * _pageSize + 1)</span> to
162
+ <span class="font-bold text-main">@(Math.Min(_currentPage * _pageSize, _borrowings.Count()))</span> of
163
+ <span class="font-bold text-main">@_borrowings.Count()</span> records
164
+ </p>
165
+ <div class="flex items-center gap-2">
166
+ <button disabled="@(_currentPage == 1)" @onclick="() => _currentPage--"
167
+ class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
168
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
169
+ </button>
170
+
171
+ <span class="text-xs font-bold text-main px-2 tracking-tight">Page @_currentPage of @TotalPages</span>
172
+
173
+ <button disabled="@(_currentPage == TotalPages)" @onclick="() => _currentPage++"
174
+ class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
175
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
176
+ </button>
177
+ </div>
178
+ </div>
179
+ }
180
+ </div>
181
+ }
182
+ </div>
183
+
184
+ @code {
185
+ private bool _isLoading = true;
186
+ private IEnumerable<BorrowingDto> _borrowings = Enumerable.Empty<BorrowingDto>();
187
+
188
+ // Pagination
189
+ private int _currentPage = 1;
190
+ private int _pageSize = 10;
191
+
192
+ private IEnumerable<BorrowingDto> PaginatedBorrowings => _borrowings
193
+ .OrderByDescending(l => l.BorrowDate)
194
+ .Skip((_currentPage - 1) * _pageSize)
195
+ .Take(_pageSize);
196
+
197
+ private int TotalPages => (int)Math.Ceiling(_borrowings.Count() / (double)_pageSize);
198
+
199
+ protected override async Task OnInitializedAsync()
200
+ {
201
+ await LoadDataAsync();
202
+ }
203
+
204
+ private async Task LoadDataAsync()
205
+ {
206
+ _isLoading = true;
207
+ try
208
+ {
209
+ _borrowings = await ApiClient.GetAllBorrowingsAsync();
210
+ _currentPage = 1;
211
+ }
212
+ catch (Exception ex)
213
+ {
214
+ await JS.InvokeVoidAsync("alert", $"Error loading all borrowings: {ex.Message}");
215
+ }
216
+ finally
217
+ {
218
+ _isLoading = false;
219
+ }
220
+ }
221
+
222
+ private async Task ApproveReturnAsync(BorrowingDto loan)
223
+ {
224
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Approve return for \"{loan.BookTitle}\" borrowed by {loan.UserEmail}?");
225
+ if (!confirmed) return;
226
+
227
+ var result = await ApiClient.ReturnBookAsync(loan.Id);
228
+ if (result != null)
229
+ {
230
+ await JS.InvokeVoidAsync("alert", "Return approved successfully!");
231
+ await LoadDataAsync();
232
+ }
233
+ else
234
+ {
235
+ await JS.InvokeVoidAsync("alert", "Failed to approve return.");
236
+ }
237
+ }
238
+
239
+ private async Task ForceReturnAsync(BorrowingDto loan)
240
+ {
241
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Force process return for \"{loan.BookTitle}\" (Member: {loan.UserEmail})? Use this if the member returned the book in person.");
242
+ if (!confirmed) return;
243
+
244
+ var result = await ApiClient.ReturnBookAsync(loan.Id);
245
+ if (result != null)
246
+ {
247
+ await JS.InvokeVoidAsync("alert", "Book marked as returned!");
248
+ await LoadDataAsync();
249
+ }
250
+ else
251
+ {
252
+ await JS.InvokeVoidAsync("alert", "Failed to process return.");
253
+ }
254
+ }
255
+ }
BlazorWebAssembly/Pages/Contact.razor CHANGED
@@ -1,49 +1,101 @@
1
  @page "/contact"
2
 
3
- <div class="max-w-6xl mx-auto py-24 px-8 animate-fade">
4
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-24">
5
  <div>
6
- <h1 class="text-6xl font-black tracking-tighter text-main mb-8 uppercase">Get in <br /><span class="text-primary italic">Touch.</span></h1>
7
- <p class="text-xl text-muted leading-relaxed mb-12">Have questions about our membership plans or need help with your account? Our librarians are here to help.</p>
 
8
 
9
- <div class="space-y-8">
10
- <div class="flex items-center gap-6">
11
- <div class="w-14 h-14 bg-primary/10 rounded-2xl flex items-center justify-center text-primary">
12
  <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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
13
  </div>
14
- <div>
15
- <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-1">Email Us</p>
16
- <p class="text-lg font-bold text-main">hello@libraryluxe.com</p>
17
- </div>
18
  </div>
19
- <div class="flex items-center gap-6">
20
- <div class="w-14 h-14 bg-primary/10 rounded-2xl flex items-center justify-center text-primary">
21
  <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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
22
  </div>
23
- <div>
24
- <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-1">Visit Us</p>
25
- <p class="text-lg font-bold text-main">123 Literature Lane, Booktown</p>
26
- </div>
27
  </div>
28
  </div>
 
 
 
 
 
29
  </div>
30
 
31
- <div class="bg-card p-10 rounded-[3rem] border border-border shadow-2xl">
 
 
 
 
32
  <form class="space-y-6">
33
- <div>
34
- <label class="block text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-3">Your Name</label>
35
- <input type="text" class="w-full bg-body border border-border rounded-xl px-4 py-4 focus:ring-2 focus:ring-primary outline-none transition-all" placeholder="John Doe" />
 
 
 
 
 
 
36
  </div>
37
  <div>
38
- <label class="block text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-3">Email Address</label>
39
- <input type="email" class="w-full bg-body border border-border rounded-xl px-4 py-4 focus:ring-2 focus:ring-primary outline-none transition-all" placeholder="name@example.com" />
 
 
 
 
 
40
  </div>
41
  <div>
42
- <label class="block text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-3">Message</label>
43
- <textarea rows="4" class="w-full bg-body border border-border rounded-xl px-4 py-4 focus:ring-2 focus:ring-primary outline-none transition-all" placeholder="How can we help?"></textarea>
44
  </div>
45
- <button type="button" class="w-full py-5 bg-primary text-white rounded-xl font-black uppercase tracking-widest hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Send Message</button>
46
  </form>
47
  </div>
48
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </div>
 
 
1
  @page "/contact"
2
 
3
+ <div class="max-w-7xl mx-auto py-24 px-8 animate-fade space-y-32">
4
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-24">
5
  <div>
6
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Get in Touch</h2>
7
+ <h1 class="text-7xl font-black tracking-tighter text-main mb-8 uppercase leading-[0.9]">Let's <br /><span class="text-primary italic">Connect.</span></h1>
8
+ <p class="text-xl text-muted leading-relaxed mb-12">Have questions about our membership plans or need help with your account? Our librarians are here to help you 24/7.</p>
9
 
10
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-8 mb-12">
11
+ <div class="p-8 bg-card rounded-3xl border border-border hover:border-primary/30 transition-colors">
12
+ <div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center text-primary mb-6">
13
  <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="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
14
  </div>
15
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-1">Email Us</p>
16
+ <p class="text-sm font-bold text-main">hello@libraryluxe.com</p>
 
 
17
  </div>
18
+ <div class="p-8 bg-card rounded-3xl border border-border hover:border-primary/30 transition-colors">
19
+ <div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center text-primary mb-6">
20
  <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="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
21
  </div>
22
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-1">Visit Us</p>
23
+ <p class="text-sm font-bold text-main">123 Literature Lane, NY</p>
 
 
24
  </div>
25
  </div>
26
+
27
+ <div class="bg-primary/5 p-8 rounded-3xl border border-primary/10">
28
+ <h4 class="text-xs font-black uppercase tracking-widest text-primary mb-4">Response Time</h4>
29
+ <p class="text-sm text-main font-medium leading-relaxed">We typically respond to all inquiries within 2 business hours. For urgent account issues, please use our internal support ticket system after signing in.</p>
30
+ </div>
31
  </div>
32
 
33
+ <div class="bg-card p-12 rounded-[3.5rem] border border-border shadow-2xl relative">
34
+ <div class="absolute top-0 right-0 p-8">
35
+ <svg class="w-12 h-12 text-primary opacity-10" fill="currentColor" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z"/></svg>
36
+ </div>
37
+ <h3 class="text-2xl font-black text-main mb-10 uppercase tracking-tight">Send a Message</h3>
38
  <form class="space-y-6">
39
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
40
+ <div>
41
+ <label class="block text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-3">First Name</label>
42
+ <input type="text" class="w-full bg-body border border-border rounded-2xl px-6 py-5 focus:ring-2 focus:ring-primary outline-none transition-all" placeholder="John" />
43
+ </div>
44
+ <div>
45
+ <label class="block text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-3">Email Address</label>
46
+ <input type="email" class="w-full bg-body border border-border rounded-2xl px-6 py-5 focus:ring-2 focus:ring-primary outline-none transition-all" placeholder="john@example.com" />
47
+ </div>
48
  </div>
49
  <div>
50
+ <label class="block text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-3">Subject</label>
51
+ <select class="w-full bg-body border border-border rounded-2xl px-6 py-5 focus:ring-2 focus:ring-primary outline-none transition-all appearance-none">
52
+ <option>General Inquiry</option>
53
+ <option>Membership Issues</option>
54
+ <option>Technical Support</option>
55
+ <option>Partnership</option>
56
+ </select>
57
  </div>
58
  <div>
59
+ <label class="block text-[10px] font-black text-muted uppercase tracking-[0.2em] mb-3">Your Message</label>
60
+ <textarea rows="5" class="w-full bg-body border border-border rounded-2xl px-6 py-5 focus:ring-2 focus:ring-primary outline-none transition-all" placeholder="Tell us more about your inquiry..."></textarea>
61
  </div>
62
+ <button type="button" class="w-full py-6 bg-primary text-white rounded-2xl font-black uppercase tracking-widest hover:bg-primary-hover shadow-2xl shadow-primary/30 transition-all transform hover:scale-[1.02] active:scale-95">Dispatch Message</button>
63
  </form>
64
  </div>
65
  </div>
66
+
67
+ <!-- FAQ Snippet -->
68
+ <section>
69
+ <div class="text-center mb-16">
70
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Quick Answers</h2>
71
+ <h3 class="text-5xl font-black text-main tracking-tight">Frequently Asked Questions</h3>
72
+ </div>
73
+ <div class="max-w-3xl mx-auto space-y-4">
74
+ <div class="bg-card border border-border rounded-2xl p-6 hover:border-primary/30 transition-colors cursor-pointer group">
75
+ <div class="flex items-center justify-between">
76
+ <h4 class="font-bold text-main">How do I upgrade my membership?</h4>
77
+ <svg class="w-5 h-5 text-muted group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
78
+ </div>
79
+ <p class="mt-4 text-sm text-muted hidden group-hover:block animate-fade">You can upgrade your membership at any time from your account dashboard. The price will be prorated based on your current cycle.</p>
80
+ </div>
81
+ <div class="bg-card border border-border rounded-2xl p-6 hover:border-primary/30 transition-colors cursor-pointer group">
82
+ <div class="flex items-center justify-between">
83
+ <h4 class="font-bold text-main">What happens if I return a book late?</h4>
84
+ <svg class="w-5 h-5 text-muted group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
85
+ </div>
86
+ <p class="mt-4 text-sm text-muted hidden group-hover:block animate-fade">Late returns may incur a small fee and deduction of loyalty points. We recommend using our renewal feature to avoid penalties.</p>
87
+ </div>
88
+ <div class="bg-card border border-border rounded-2xl p-6 hover:border-primary/30 transition-colors cursor-pointer group">
89
+ <div class="flex items-center justify-between">
90
+ <h4 class="font-bold text-main">Can I access digital books offline?</h4>
91
+ <svg class="w-5 h-5 text-muted group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
92
+ </div>
93
+ <p class="mt-4 text-sm text-muted hidden group-hover:block animate-fade">Yes, our mobile app supports offline reading for all digital titles borrowed under your plan.</p>
94
+ </div>
95
+ </div>
96
+ <div class="text-center mt-12">
97
+ <a href="/faq" class="text-xs font-black text-primary uppercase tracking-widest hover:underline">View All FAQs</a>
98
+ </div>
99
+ </section>
100
  </div>
101
+
BlazorWebAssembly/Pages/FAQ.razor ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/faq"
2
+
3
+ <div class="max-w-4xl mx-auto py-24 px-8 animate-fade">
4
+ <div class="text-center mb-20">
5
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Support Center</h2>
6
+ <h1 class="text-6xl font-black tracking-tighter text-main mb-8 uppercase">Common Questions</h1>
7
+ <p class="text-xl text-muted max-w-2xl mx-auto">
8
+ Everything you need to know about LibraryLuxe, our membership plans, and our community.
9
+ </p>
10
+ </div>
11
+
12
+ <div class="space-y-6">
13
+ <!-- Category: General -->
14
+ <div class="mb-12">
15
+ <h3 class="text-xs font-black text-muted uppercase tracking-[0.2em] mb-8 border-b border-border pb-4">General Information</h3>
16
+ <div class="space-y-4">
17
+ <details class="group bg-card border border-border rounded-3xl overflow-hidden transition-all hover:border-primary/30">
18
+ <summary class="flex items-center justify-between p-8 cursor-pointer list-none">
19
+ <span class="text-lg font-bold text-main">What is LibraryLuxe?</span>
20
+ <div class="w-8 h-8 rounded-full bg-body border border-border flex items-center justify-center group-open:rotate-180 transition-transform">
21
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
22
+ </div>
23
+ </summary>
24
+ <div class="px-8 pb-8 text-muted leading-relaxed">
25
+ LibraryLuxe is a next-generation library management ecosystem designed to bridge the gap between physical books and digital convenience. We offer a reward-based reading system, smart notifications, and a global community for book lovers.
26
+ </div>
27
+ </details>
28
+ <details class="group bg-card border border-border rounded-3xl overflow-hidden transition-all hover:border-primary/30">
29
+ <summary class="flex items-center justify-between p-8 cursor-pointer list-none">
30
+ <span class="text-lg font-bold text-main">Is there a mobile app?</span>
31
+ <div class="w-8 h-8 rounded-full bg-body border border-border flex items-center justify-center group-open:rotate-180 transition-transform">
32
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
33
+ </div>
34
+ </summary>
35
+ <div class="px-8 pb-8 text-muted leading-relaxed">
36
+ Yes! Our mobile app is available for both iOS and Android. It features offline reading, instant barcode scanning for borrowing, and real-time alerts.
37
+ </div>
38
+ </details>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Category: Membership -->
43
+ <div class="mb-12">
44
+ <h3 class="text-xs font-black text-muted uppercase tracking-[0.2em] mb-8 border-b border-border pb-4">Membership & Billing</h3>
45
+ <div class="space-y-4">
46
+ <details class="group bg-card border border-border rounded-3xl overflow-hidden transition-all hover:border-primary/30">
47
+ <summary class="flex items-center justify-between p-8 cursor-pointer list-none">
48
+ <span class="text-lg font-bold text-main">Can I cancel my membership anytime?</span>
49
+ <div class="w-8 h-8 rounded-full bg-body border border-border flex items-center justify-center group-open:rotate-180 transition-transform">
50
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
51
+ </div>
52
+ </summary>
53
+ <div class="px-8 pb-8 text-muted leading-relaxed">
54
+ Absolutely. We don't believe in locking you down. You can cancel your subscription from your account settings at any time. You'll retain access until the end of your current billing period.
55
+ </div>
56
+ </details>
57
+ <details class="group bg-card border border-border rounded-3xl overflow-hidden transition-all hover:border-primary/30">
58
+ <summary class="flex items-center justify-between p-8 cursor-pointer list-none">
59
+ <span class="text-lg font-bold text-main">How do loyalty points work?</span>
60
+ <div class="w-8 h-8 rounded-full bg-body border border-border flex items-center justify-center group-open:rotate-180 transition-transform">
61
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
62
+ </div>
63
+ </summary>
64
+ <div class="px-8 pb-8 text-muted leading-relaxed">
65
+ You earn points for every book you return on time, every review you write, and every milestone you hit. These points can be used to pay for your membership, enter exclusive giveaways, or buy digital gift cards.
66
+ </div>
67
+ </details>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Contact CTA -->
73
+ <div class="mt-20 p-12 bg-primary/5 rounded-[3rem] border border-primary/10 text-center">
74
+ <h4 class="text-2xl font-black text-main mb-4 uppercase tracking-tight">Still have questions?</h4>
75
+ <p class="text-muted mb-8">If you couldn't find what you were looking for, our team is ready to help.</p>
76
+ <a href="/contact" class="inline-block bg-primary text-white px-10 py-4 rounded-2xl font-black uppercase tracking-widest text-xs hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Contact Support</a>
77
+ </div>
78
+ </div>
BlazorWebAssembly/Pages/Features.razor ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/features"
2
+
3
+ <div class="max-w-7xl mx-auto py-24 px-8 animate-fade space-y-32">
4
+ <div class="text-center">
5
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Deep Dive</h2>
6
+ <h1 class="text-7xl font-black tracking-tighter text-main mb-8 uppercase leading-[0.9]">Advanced <br /> <span class="text-primary italic">Capabilities.</span></h1>
7
+ <p class="text-xl text-muted max-w-2xl mx-auto leading-relaxed">
8
+ LibraryLuxe is built on a foundation of modern technology to provide you with a seamless and rewarding experience.
9
+ </p>
10
+ </div>
11
+
12
+ <!-- Feature Block 1: Loyalty -->
13
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-24 items-center">
14
+ <div class="order-2 lg:order-1">
15
+ <div class="bg-card aspect-video rounded-[3rem] border border-border shadow-2xl flex items-center justify-center relative overflow-hidden">
16
+ <div class="absolute inset-0 bg-primary opacity-5"></div>
17
+ <div class="relative z-10 flex flex-col items-center gap-6">
18
+ <div class="w-24 h-24 bg-primary rounded-3xl shadow-2xl shadow-primary/30 flex items-center justify-center text-white text-4xl font-black">L</div>
19
+ <span class="text-[10px] font-black text-muted uppercase tracking-[0.4em]">Loyalty Dashboard</span>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ <div class="order-1 lg:order-2">
24
+ <h3 class="text-4xl font-black text-main mb-6 uppercase tracking-tight">Smart Rewards <br/> Ecosystem</h3>
25
+ <p class="text-lg text-muted leading-relaxed mb-8">
26
+ Our proprietary loyalty system rewards positive reading habits. By gamifying the library experience, we've increased on-time returns by 40% and overall community engagement by 60%.
27
+ </p>
28
+ <ul class="space-y-4">
29
+ <li class="flex items-center gap-4">
30
+ <div class="w-2 h-2 rounded-full bg-primary"></div>
31
+ <span class="font-bold text-main">Point Multipliers for Gold Members</span>
32
+ </li>
33
+ <li class="flex items-center gap-4">
34
+ <div class="w-2 h-2 rounded-full bg-primary"></div>
35
+ <span class="font-bold text-main">Milestone Badges & Achievements</span>
36
+ </li>
37
+ <li class="flex items-center gap-4">
38
+ <div class="w-2 h-2 rounded-full bg-primary"></div>
39
+ <span class="font-bold text-main">Internal Rewards Marketplace</span>
40
+ </li>
41
+ </ul>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Feature Block 2: Notifications -->
46
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-24 items-center">
47
+ <div>
48
+ <h3 class="text-4xl font-black text-main mb-6 uppercase tracking-tight">Instant Alerts <br/> via Firebase</h3>
49
+ <p class="text-lg text-muted leading-relaxed mb-8">
50
+ Stay connected with real-time push notifications. Whether it's a book you've wishlisted becoming available or a reminder about a due date, we ensure you never miss a beat.
51
+ </p>
52
+ <div class="grid grid-cols-2 gap-6">
53
+ <div class="p-6 bg-card rounded-2xl border border-border">
54
+ <h5 class="font-black text-primary text-xs uppercase mb-2">Back in Stock</h5>
55
+ <p class="text-xs text-muted">Get alerted when high-demand titles return.</p>
56
+ </div>
57
+ <div class="p-6 bg-card rounded-2xl border border-border">
58
+ <h5 class="font-black text-primary text-xs uppercase mb-2">Due Soon</h5>
59
+ <p class="text-xs text-muted">Gentle reminders 48h before expiry.</p>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ <div class="bg-card aspect-video rounded-[3rem] border border-border shadow-2xl flex items-center justify-center relative overflow-hidden">
64
+ <div class="absolute inset-0 bg-emerald-500 opacity-5"></div>
65
+ <div class="relative z-10 flex flex-col items-center gap-6">
66
+ <div class="w-16 h-16 bg-emerald-500 rounded-2xl shadow-xl shadow-emerald-500/20 flex items-center justify-center text-white">
67
+ <svg class="w-8 h-8" 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>
68
+ </div>
69
+ <span class="text-[10px] font-black text-muted uppercase tracking-[0.4em]">Real-time Infrastructure</span>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Recommendation System -->
75
+ <section class="bg-card p-20 rounded-[4rem] border border-border text-center">
76
+ <h3 class="text-4xl font-black text-main mb-8 uppercase tracking-tight italic">Personalized Recommendations</h3>
77
+ <p class="text-xl text-muted max-w-3xl mx-auto mb-12">
78
+ Our intelligent algorithms analyze your reading history and preferences to suggest your next favorite book. The more you read, the smarter it gets.
79
+ </p>
80
+ <div class="flex flex-wrap justify-center gap-8 opacity-40 grayscale">
81
+ <div class="w-32 h-44 bg-body rounded-xl border border-border"></div>
82
+ <div class="w-32 h-44 bg-body rounded-xl border border-border"></div>
83
+ <div class="w-32 h-44 bg-body rounded-xl border border-border"></div>
84
+ <div class="w-32 h-44 bg-body rounded-xl border border-border"></div>
85
+ </div>
86
+ </section>
87
+ </div>
BlazorWebAssembly/Pages/Home.razor CHANGED
@@ -17,133 +17,330 @@
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
- <a href="/membership" class="text-xs font-black text-primary uppercase tracking-widest hover:underline">Manage Plan</a>
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
  <NotAuthorized>
63
  <!-- Guest Landing Page -->
64
- <div class="space-y-24 animate-fade py-12">
65
  <!-- Hero Section -->
66
- <section class="text-center">
67
- <h1 class="text-7xl font-black tracking-tighter text-main mb-8 leading-tight">
68
- Where Every Page <br /> <span class="text-primary italic">Finds a Purpose.</span>
69
- </h1>
70
- <p class="text-xl text-muted max-w-3xl mx-auto leading-relaxed mb-12">
71
- 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.
72
- </p>
73
- <div class="flex items-center justify-center gap-6">
74
- <a href="/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 inline-block text-center">
75
- Start Reading Now
76
- </a>
77
- <a href="/books" class="bg-card border border-border text-main px-10 py-5 rounded-2xl font-black text-lg hover:bg-body transition-all inline-block text-center">
78
- Explore Catalog
79
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
  </section>
82
 
83
- <!-- Stats Bar -->
84
- <div class="grid grid-cols-1 md:grid-cols-3 gap-12 border-y border-border py-16">
85
- <div class="text-center">
86
- <h3 class="text-5xl font-black text-main mb-2">12,000+</h3>
87
- <p class="text-muted font-bold uppercase tracking-widest text-xs">Verified Books</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </div>
89
- <div class="text-center border-x border-border">
90
- <h3 class="text-5xl font-black text-main mb-2">5.2k</h3>
91
- <p class="text-muted font-bold uppercase tracking-widest text-xs">Active Readers</p>
 
 
 
 
92
  </div>
93
- <div class="text-center">
94
- <h3 class="text-5xl font-black text-main mb-2">150+</h3>
95
- <p class="text-muted font-bold uppercase tracking-widest text-xs">Daily Borrowings</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  </div>
97
- </div>
98
 
99
- <!-- Features -->
100
- <section class="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
101
- <div class="space-y-8">
102
- <h2 class="text-4xl font-black text-main tracking-tight">Experience Library <br /> Management redefined.</h2>
103
- <div class="space-y-6">
104
- <div class="flex gap-4">
105
- <div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center text-primary flex-shrink-0">
106
- <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>
107
- </div>
108
- <div>
109
- <h4 class="font-bold text-main">Loyalty Reward System</h4>
110
- <p class="text-sm text-muted">Earn points for every book you return on time and exchange them for membership perks.</p>
 
 
 
 
 
 
 
 
111
  </div>
112
  </div>
113
- <div class="flex gap-4">
114
- <div class="w-12 h-12 bg-primary/10 rounded-xl flex items-center justify-center text-primary flex-shrink-0">
115
- <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>
 
 
 
 
 
 
 
 
 
 
116
  </div>
117
- <div>
118
- <h4 class="font-bold text-main">Instant Notifications</h4>
119
- <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>
 
 
 
 
 
 
120
  </div>
121
  </div>
122
  </div>
123
  </div>
124
- <div class="bg-card aspect-video rounded-[3rem] border border-border shadow-2xl flex items-center justify-center relative overflow-hidden">
125
- <div class="absolute inset-0 bg-primary opacity-5"></div>
126
- <span class="text-muted font-black uppercase tracking-[0.3em] text-xs">Preview of Interface</span>
 
 
 
 
 
 
 
 
 
 
 
 
127
  </div>
128
  </section>
129
  </div>
130
  </NotAuthorized>
 
131
  </AuthorizeView>
132
 
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
  }
 
17
  <p class="text-lg text-muted">Here is what's happening with your library account today.</p>
18
  </section>
19
 
20
+ <AuthorizeView Roles="Librarian" Context="LibrarianContext">
21
+ <Authorized>
22
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
23
+ <div class="bg-card p-6 flex flex-col gap-3 group border border-border rounded-3xl transition-all shadow-sm">
24
+ <div class="flex items-center justify-between">
25
+ <span class="text-[10px] font-black text-muted uppercase tracking-widest">Total Books</span>
26
+ </div>
27
+ <div class="text-3xl font-black text-main">@_totalBooks</div>
28
+ </div>
29
+ <div class="bg-card p-6 flex flex-col gap-3 group border border-border rounded-3xl transition-all shadow-sm">
30
+ <div class="flex items-center justify-between">
31
+ <span class="text-[10px] font-black text-muted uppercase tracking-widest">Active Loans</span>
32
+ </div>
33
+ <div class="text-3xl font-black text-main">@_activeLoans</div>
34
+ </div>
35
+ <div class="bg-card p-6 flex flex-col gap-3 group border border-border rounded-3xl transition-all shadow-sm border-l-4 border-l-red-500">
36
+ <div class="flex items-center justify-between">
37
+ <span class="text-[10px] font-black text-red-500 uppercase tracking-widest">Overdue</span>
38
+ </div>
39
+ <div class="text-3xl font-black text-red-600">@_overdueLoans</div>
40
+ </div>
41
+ <div class="bg-card p-6 flex flex-col gap-3 group border border-border rounded-3xl transition-all shadow-sm">
42
+ <div class="flex items-center justify-between">
43
+ <span class="text-[10px] font-black text-muted uppercase tracking-widest">Active Members</span>
44
+ </div>
45
+ <div class="text-3xl font-black text-main">@_totalMembers</div>
46
+ </div>
47
  </div>
48
+ </Authorized>
49
+ </AuthorizeView>
50
 
51
+ <AuthorizeView Roles="Member" Context="MemberContext">
52
+ <Authorized>
53
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
54
+ <!-- Loyalty Points -->
55
+ <div class="bg-card p-8 rounded-3xl border border-border shadow-sm hover:shadow-md transition-all">
56
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-4">Loyalty Balance</p>
57
+ <div class="flex items-baseline gap-2 mb-4">
58
+ <h3 class="text-5xl font-black text-main">@(_loyaltyAccount?.CurrentBalance.ToString("N0") ?? "0")</h3>
59
+ <span class="text-xs font-bold text-primary">POINTS</span>
60
+ </div>
61
+ <div class="flex items-center justify-between">
62
+ <span class="text-xs font-bold text-muted uppercase">Tier: @(_loyaltyAccount?.Tier ?? "Member")</span>
63
+ </div>
64
+ </div>
65
 
66
+ <!-- Active Loans -->
67
+ <div class="bg-card p-8 rounded-3xl border border-border shadow-sm hover:shadow-md transition-all">
68
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-4">Current Loans</p>
69
+ <h3 class="text-5xl font-black text-main mb-4">@_activeLoans</h3>
70
+ <div class="flex gap-2">
71
+ @if (_overdueLoans > 0)
72
+ {
73
+ <span class="px-3 py-1 bg-red-500/10 text-red-600 text-[10px] font-black uppercase rounded-lg">@_overdueLoans Overdue</span>
74
+ }
75
+ else
76
+ {
77
+ <span class="px-3 py-1 bg-green-500/10 text-green-600 text-[10px] font-black uppercase rounded-lg">Healthy</span>
78
+ }
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Subscription -->
83
+ <div class="bg-card p-8 rounded-3xl border border-border shadow-sm hover:shadow-md transition-all">
84
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest mb-4">Membership</p>
85
+ <h3 class="text-2xl font-black text-main mb-1 uppercase">@(_currentSub?.MembershipType ?? "Free")</h3>
86
+ <p class="text-xs text-muted mb-6">@( _currentSub != null ? "Active Status" : "No Active Plan" )</p>
87
+ <a href="/membership" class="text-xs font-black text-primary uppercase tracking-widest hover:underline">Manage Plan</a>
88
+ </div>
89
+ </div>
90
+ </Authorized>
91
+ </AuthorizeView>
92
 
93
  <section>
94
+ <h2 class="text-xl font-bold mb-6 text-main">Recent Arrivals</h2>
95
+ @if (_recentBooks.Any())
96
+ {
97
+ <div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6">
98
+ @foreach (var book in _recentBooks)
99
+ {
100
+ <a href="/books" class="group cursor-pointer">
101
+ <div class="bg-card aspect-[2/3] rounded-2xl border border-border overflow-hidden relative mb-2 group-hover:border-primary/50 transition-colors">
102
+ <div class="absolute inset-0 flex items-center justify-center text-primary/20 group-hover:text-primary/40 transition-colors">
103
+ <svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
104
+ </div>
105
+ </div>
106
+ <h3 class="text-xs font-bold text-main truncate">@book.Title</h3>
107
+ <p class="text-[10px] text-muted truncate">@book.Author</p>
108
+ </a>
109
+ }
110
+ </div>
111
+ }
112
+ else
113
+ {
114
+ <div class="text-muted text-sm italic">Loading recent arrivals...</div>
115
+ }
116
  </section>
117
  </div>
118
  </Authorized>
119
  <NotAuthorized>
120
  <!-- Guest Landing Page -->
121
+ <div class="space-y-32 animate-fade pb-24">
122
  <!-- Hero Section -->
123
+ <section class="relative pt-20 overflow-hidden">
124
+ <div class="max-w-7xl mx-auto px-8 relative z-10">
125
+ <div class="text-center">
126
+ <div class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-primary/10 text-primary text-[10px] font-black uppercase tracking-widest mb-8 animate-bounce">
127
+ <span class="relative flex h-2 w-2">
128
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
129
+ <span class="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
130
+ </span>
131
+ New: Audiobooks Collection Now Live
132
+ </div>
133
+ <h1 class="text-8xl font-black tracking-tighter text-main mb-8 leading-[0.9]">
134
+ Where Every Page <br />
135
+ <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary to-emerald-500 italic">Finds a Purpose.</span>
136
+ </h1>
137
+ <p class="text-xl text-muted max-w-2xl mx-auto leading-relaxed mb-12">
138
+ Join the next generation of readers in a digital-first library experience. Access thousands of titles, earn loyalty rewards, and track your progress seamlessly.
139
+ </p>
140
+ <div class="flex flex-col sm:flex-row items-center justify-center gap-6">
141
+ <a href="/register" class="w-full sm:w-auto bg-primary text-white px-10 py-5 rounded-2xl font-black text-lg hover:bg-primary-hover shadow-2xl shadow-primary/30 transition-all transform hover:scale-105 active:scale-95 inline-block text-center">
142
+ Start Reading Now
143
+ </a>
144
+ <a href="/membership" class="w-full sm:w-auto bg-card border border-border text-main px-10 py-5 rounded-2xl font-black text-lg hover:bg-body transition-all inline-block text-center">
145
+ View Memberships
146
+ </a>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ <!-- Background Decorative Elements -->
151
+ <div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full -z-10 pointer-events-none opacity-20">
152
+ <div class="absolute top-20 left-1/4 w-96 h-96 bg-primary/30 rounded-full blur-[120px]"></div>
153
+ <div class="absolute bottom-0 right-1/4 w-96 h-96 bg-emerald-500/20 rounded-full blur-[120px]"></div>
154
  </div>
155
  </section>
156
 
157
+ <!-- Stats Grid -->
158
+ <div class="max-w-7xl mx-auto px-8">
159
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-8 bg-card border border-border p-12 rounded-[3rem] shadow-xl">
160
+ <div class="text-center">
161
+ <h3 class="text-5xl font-black text-main mb-2 tracking-tighter">12k+</h3>
162
+ <p class="text-muted font-bold uppercase tracking-widest text-[10px]">Verified Books</p>
163
+ </div>
164
+ <div class="text-center border-l border-border">
165
+ <h3 class="text-5xl font-black text-main mb-2 tracking-tighter">5.2k</h3>
166
+ <p class="text-muted font-bold uppercase tracking-widest text-[10px]">Active Readers</p>
167
+ </div>
168
+ <div class="text-center border-l border-border">
169
+ <h3 class="text-5xl font-black text-main mb-2 tracking-tighter">98%</h3>
170
+ <p class="text-muted font-bold uppercase tracking-widest text-[10px]">Satisfaction</p>
171
+ </div>
172
+ <div class="text-center border-l border-border">
173
+ <h3 class="text-5xl font-black text-main mb-2 tracking-tighter">24/7</h3>
174
+ <p class="text-muted font-bold uppercase tracking-widest text-[10px]">Digital Access</p>
175
+ </div>
176
  </div>
177
+ </div>
178
+
179
+ <!-- Features Section -->
180
+ <section class="max-w-7xl mx-auto px-8">
181
+ <div class="text-center mb-20">
182
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Core Ecosystem</h2>
183
+ <h3 class="text-5xl font-black text-main tracking-tight">Everything you need to <br/> master your reading journey.</h3>
184
  </div>
185
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
186
+ <!-- Feature 1 -->
187
+ <div class="group bg-card p-10 rounded-[2.5rem] border border-border hover:border-primary/50 transition-all hover:shadow-2xl hover:shadow-primary/5">
188
+ <div class="w-14 h-14 bg-primary/10 rounded-2xl flex items-center justify-center text-primary mb-8 group-hover:scale-110 transition-transform">
189
+ <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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>
190
+ </div>
191
+ <h4 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">Borrow Thousands</h4>
192
+ <p class="text-muted text-sm leading-relaxed">Access our vast physical and digital catalog with a single click. No more waiting in lines.</p>
193
+ </div>
194
+ <!-- Feature 2 -->
195
+ <div class="group bg-card p-10 rounded-[2.5rem] border border-border hover:border-primary/50 transition-all hover:shadow-2xl hover:shadow-primary/5">
196
+ <div class="w-14 h-14 bg-primary/10 rounded-2xl flex items-center justify-center text-primary mb-8 group-hover:scale-110 transition-transform">
197
+ <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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>
198
+ </div>
199
+ <h4 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">Track Reading</h4>
200
+ <p class="text-muted text-sm leading-relaxed">Visualize your reading habits with detailed analytics and personal dashboards.</p>
201
+ </div>
202
+ <!-- Feature 3 -->
203
+ <div class="group bg-card p-10 rounded-[2.5rem] border border-border hover:border-primary/50 transition-all hover:shadow-2xl hover:shadow-primary/5">
204
+ <div class="w-14 h-14 bg-primary/10 rounded-2xl flex items-center justify-center text-primary mb-8 group-hover:scale-110 transition-transform">
205
+ <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" 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>
206
+ </div>
207
+ <h4 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">Stay Notified</h4>
208
+ <p class="text-muted text-sm leading-relaxed">Never miss a due date with Firebase-powered instant renewal notifications.</p>
209
+ </div>
210
+ <!-- Feature 4 -->
211
+ <div class="group bg-card p-10 rounded-[2.5rem] border border-border hover:border-primary/50 transition-all hover:shadow-2xl hover:shadow-primary/5">
212
+ <div class="w-14 h-14 bg-primary/10 rounded-2xl flex items-center justify-center text-primary mb-8 group-hover:scale-110 transition-transform">
213
+ <svg class="w-7 h-7" 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>
214
+ </div>
215
+ <h4 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">Earn Loyalty</h4>
216
+ <p class="text-muted text-sm leading-relaxed">Collect points for every interaction and redeem them for premium membership tiers.</p>
217
+ </div>
218
  </div>
219
+ </section>
220
 
221
+ <!-- Testimonials -->
222
+ <section class="bg-body border-y border-border py-32 relative overflow-hidden">
223
+ <div class="max-w-7xl mx-auto px-8 relative z-10">
224
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-24 items-center">
225
+ <div>
226
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Wall of Praise</h2>
227
+ <h3 class="text-6xl font-black text-main tracking-tighter mb-8 leading-tight">Loved by <br/> modern readers.</h3>
228
+ <p class="text-lg text-muted leading-relaxed mb-12">
229
+ Don't just take our word for it. Join thousands of satisfied members who have transformed their reading habits.
230
+ </p>
231
+ <div class="flex gap-4">
232
+ <div class="flex -space-x-4">
233
+ @for(int i=1; i<=4; i++) {
234
+ <div class="w-12 h-12 rounded-full border-4 border-card bg-primary/20 flex items-center justify-center text-xs font-bold">U@i</div>
235
+ }
236
+ </div>
237
+ <div class="flex flex-col justify-center">
238
+ <span class="text-sm font-black text-main uppercase">5,000+ Active Members</span>
239
+ <span class="text-[10px] text-muted font-bold uppercase tracking-widest">Global Community</span>
240
+ </div>
241
  </div>
242
  </div>
243
+ <div class="space-y-6">
244
+ <div class="bg-card p-10 rounded-[2.5rem] border border-border shadow-xl transform hover:-rotate-1 transition-transform">
245
+ <div class="flex gap-1 text-primary mb-6">
246
+ @for(int i=0; i<5; i++) { <svg class="w-4 h-4 fill-current" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg> }
247
+ </div>
248
+ <p class="text-main font-medium italic mb-6 leading-relaxed">"The loyalty system is a game changer. I've earned three months of free premium just by returning my books on time!"</p>
249
+ <div class="flex items-center gap-4">
250
+ <div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center font-black text-xs text-primary">SC</div>
251
+ <div>
252
+ <h5 class="text-sm font-bold text-main">Sarah Chen</h5>
253
+ <p class="text-[10px] text-muted font-bold uppercase">Gold Member</p>
254
+ </div>
255
+ </div>
256
  </div>
257
+ <div class="bg-card p-10 rounded-[2.5rem] border border-border shadow-xl transform translate-x-8 hover:rotate-1 transition-transform">
258
+ <p class="text-main font-medium italic mb-6 leading-relaxed">"Finally a library system that feels like it belongs in 2026. The interface is stunning and the notifications are so helpful."</p>
259
+ <div class="flex items-center gap-4">
260
+ <div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center font-black text-xs text-primary">JM</div>
261
+ <div>
262
+ <h5 class="text-sm font-bold text-main">James Miller</h5>
263
+ <p class="text-[10px] text-muted font-bold uppercase">Active Reader</p>
264
+ </div>
265
+ </div>
266
  </div>
267
  </div>
268
  </div>
269
  </div>
270
+ </section>
271
+
272
+ <!-- Final CTA -->
273
+ <section class="max-w-7xl mx-auto px-8">
274
+ <div class="bg-primary p-16 md:p-24 rounded-[4rem] text-center text-white relative overflow-hidden shadow-2xl shadow-primary/40">
275
+ <div class="relative z-10">
276
+ <h2 class="text-5xl md:text-7xl font-black tracking-tighter mb-8 leading-tight">Ready to start your <br/> next chapter?</h2>
277
+ <p class="text-xl text-white/80 max-w-2xl mx-auto mb-12 font-medium">Join thousands of readers today and get your first month of Premium for free.</p>
278
+ <div class="flex flex-col sm:flex-row items-center justify-center gap-6">
279
+ <a href="/register" class="w-full sm:w-auto bg-white text-primary px-12 py-6 rounded-2xl font-black text-xl hover:bg-white/90 transition-all transform hover:scale-105 shadow-xl">Create Free Account</a>
280
+ <a href="/contact" class="w-full sm:w-auto border-2 border-white/30 text-white px-12 py-6 rounded-2xl font-black text-xl hover:bg-white/10 transition-all">Talk to Librarian</a>
281
+ </div>
282
+ </div>
283
+ <!-- CTA Decoration -->
284
+ <div class="absolute top-0 right-0 -translate-y-1/2 translate-x-1/2 w-[600px] h-[600px] bg-white/10 rounded-full blur-[100px]"></div>
285
  </div>
286
  </section>
287
  </div>
288
  </NotAuthorized>
289
+
290
  </AuthorizeView>
291
 
292
 
293
  @code {
294
+ private int _totalBooks = 0;
295
+ private int _activeLoans = 0;
296
+ private int _overdueLoans = 0;
297
+ private int _totalMembers = 0;
298
+ private IEnumerable<BookDto> _recentBooks = Enumerable.Empty<BookDto>();
299
  private LoyaltyAccountDto? _loyaltyAccount;
300
+ private SubscriptionDto? _currentSub;
301
+ private bool _isLoading = true;
302
 
303
  protected override async Task OnInitializedAsync()
304
  {
305
  try
306
  {
307
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
308
+ var user = authState.User;
309
+
310
+ if (user.Identity?.IsAuthenticated == true)
311
  {
312
+ var books = await ApiClient.GetBooksAsync();
313
+ _totalBooks = books.Count();
314
+ _recentBooks = books.Take(6);
315
+
316
+ if (user.IsInRole("Member"))
317
+ {
318
+ try {
319
+ var borrowings = await ApiClient.GetMyBorrowingsAsync();
320
+ _activeLoans = borrowings.Count(b => b.Status == "Borrowed");
321
+ _overdueLoans = borrowings.Count(b => b.Status == "Overdue" || (b.Status == "Borrowed" && b.DueDate < DateTime.UtcNow));
322
+ _loyaltyAccount = await ApiClient.GetMyLoyaltyAccountAsync();
323
+ _currentSub = await ApiClient.GetMySubscriptionAsync();
324
+ } catch {}
325
+ }
326
+
327
+ if (user.IsInRole("Librarian"))
328
+ {
329
+ try {
330
+ var allBorrowings = await ApiClient.GetAllBorrowingsAsync();
331
+ _activeLoans = allBorrowings.Count(b => b.Status == "Borrowed");
332
+ _overdueLoans = allBorrowings.Count(b => b.Status == "Overdue" || (b.Status == "Borrowed" && b.DueDate < DateTime.UtcNow));
333
+
334
+ var members = await ApiClient.GetUsersAsync();
335
+ _totalMembers = members.Count();
336
+ } catch {}
337
+ }
338
  }
339
  }
340
  catch { }
341
+ finally
342
+ {
343
+ _isLoading = false;
344
+ }
345
  }
346
  }
BlazorWebAssembly/Pages/Members.razor ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/members"
2
+ @using BlazorWebAssembly.Services
3
+ @using LibraryManagement.Shared.Models
4
+ @using BlazorWebAssembly.Models
5
+ @using Microsoft.AspNetCore.Authorization
6
+ @attribute [Authorize(Roles = "Librarian")]
7
+ @inject LibraryApiClient ApiClient
8
+ @inject IJSRuntime JS
9
+ @inject NavigationManager Navigation
10
+
11
+ <div class="max-w-7xl mx-auto py-10 px-6 animate-fade">
12
+ @if (_currentView == ViewMode.List)
13
+ {
14
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
15
+ <div>
16
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Members Directory</h1>
17
+ <p class="text-muted text-lg">
18
+ Manage library members and administrator accounts.
19
+ <span class="ml-2 text-xs text-muted font-bold">(@(_allUsers?.Count() ?? 0) total)</span>
20
+ </p>
21
+ </div>
22
+ <div class="flex items-center gap-3">
23
+ <button @onclick="ShowCreate" class="bg-primary text-white px-6 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all whitespace-nowrap flex items-center">
24
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
25
+ Add Member
26
+ </button>
27
+ </div>
28
+ </div>
29
+
30
+ @if (_isLoading)
31
+ {
32
+ <div class="space-y-4">
33
+ @for (int i = 0; i < 5; i++)
34
+ {
35
+ <div class="animate-pulse bg-card rounded-xl border border-border h-24 w-full"></div>
36
+ }
37
+ </div>
38
+ }
39
+ else if (_pagedResult != null)
40
+ {
41
+ <div class="bg-card rounded-2xl border border-border overflow-hidden shadow-sm">
42
+ <div class="overflow-x-auto">
43
+ <table class="w-full text-left border-collapse min-w-max whitespace-nowrap">
44
+ <thead class="bg-body border-b border-border">
45
+ <tr>
46
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Member</th>
47
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Contact</th>
48
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Role</th>
49
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Status</th>
50
+ <th class="px-6 py-4 text-right text-[10px] font-bold text-muted uppercase tracking-widest">Actions</th>
51
+ </tr>
52
+ </thead>
53
+ <tbody class="divide-y divide-border">
54
+ @foreach (var user in _pagedResult.Items)
55
+ {
56
+ <tr class="hover:bg-body transition-colors group">
57
+ <td class="px-6 py-4">
58
+ <div class="flex items-center gap-4">
59
+ <div class="h-10 w-10 flex-shrink-0 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
60
+ @(user.FullName?.Substring(0, Math.Min(2, user.FullName.Length)).ToUpper() ?? "U")
61
+ </div>
62
+ <div>
63
+ <button @onclick="() => ShowDetails(user.Id)" class="text-sm font-bold text-main hover:text-primary transition-colors text-left">@user.FullName</button>
64
+ @if (!string.IsNullOrEmpty(user.StudentId))
65
+ {
66
+ <div class="text-xs text-muted font-medium mt-0.5">ID: @user.StudentId</div>
67
+ }
68
+ </div>
69
+ </div>
70
+ </td>
71
+ <td class="px-6 py-4">
72
+ <div class="text-sm text-main font-medium">@user.Email</div>
73
+ <div class="text-xs text-muted">@(string.IsNullOrEmpty(user.PhoneNumber) ? "No phone" : user.PhoneNumber)</div>
74
+ </td>
75
+ <td class="px-6 py-4">
76
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase @(user.Role == "Admin" ? "bg-amber-500/10 text-amber-500" : "bg-blue-500/10 text-blue-500")">
77
+ @user.Role
78
+ </span>
79
+ </td>
80
+ <td class="px-6 py-4">
81
+ @if (user.BanStatus == true)
82
+ {
83
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-red-500/10 text-red-500">
84
+ Suspended
85
+ </span>
86
+ }
87
+ else if (user.IsActive)
88
+ {
89
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-green-500/10 text-green-500">
90
+ Active
91
+ </span>
92
+ }
93
+ else
94
+ {
95
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-body text-muted border border-border">
96
+ Inactive
97
+ </span>
98
+ }
99
+ </td>
100
+ <td class="px-6 py-4 text-right">
101
+ <div class="flex items-center justify-end gap-2">
102
+ <button @onclick="() => ShowDetails(user.Id)" class="p-2 rounded-lg border border-border text-muted hover:bg-primary hover:text-white hover:border-primary transition-all">
103
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
104
+ </button>
105
+ <button @onclick="() => ShowEdit(user.Id)" class="p-2 rounded-lg border border-border text-muted hover:bg-primary hover:text-white hover:border-primary transition-all">
106
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
107
+ </button>
108
+ </div>
109
+ </td>
110
+ </tr>
111
+ }
112
+ </tbody>
113
+ </table>
114
+ </div>
115
+
116
+ @if (_pagedResult.TotalPages > 1)
117
+ {
118
+ <div class="px-6 py-4 border-t border-border flex items-center justify-between bg-body">
119
+ <p class="text-xs text-muted">
120
+ Showing <span class="font-bold text-main">@((_pagedResult.CurrentPage - 1) * _pagedResult.PageSize + 1)</span>
121
+ to
122
+ <span class="font-bold text-main">@(Math.Min(_pagedResult.CurrentPage * _pagedResult.PageSize, _pagedResult.TotalCount))</span>
123
+ of <span class="font-bold text-main">@_pagedResult.TotalCount</span> members
124
+ </p>
125
+ <div class="flex items-center gap-2">
126
+ <button @onclick="() => ChangePage(_pagedResult.CurrentPage - 1)" disabled="@(!_pagedResult.HasPreviousPage)"
127
+ class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
128
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
129
+ </button>
130
+
131
+ @for (int p = 1; p <= _pagedResult.TotalPages; p++)
132
+ {
133
+ var pageNumber = p;
134
+ if (p == _pagedResult.CurrentPage)
135
+ {
136
+ <span class="w-8 h-8 flex items-center justify-center rounded-lg bg-main text-body text-xs font-bold">@p</span>
137
+ }
138
+ else if (p == 1 || p == _pagedResult.TotalPages || (p >= _pagedResult.CurrentPage - 1 && p <= _pagedResult.CurrentPage + 1))
139
+ {
140
+ <button @onclick="() => ChangePage(pageNumber)"
141
+ class="w-8 h-8 flex items-center justify-center rounded-lg border border-border text-xs font-bold text-muted hover:text-primary hover:border-primary transition-colors">@p</button>
142
+ }
143
+ else if (p == _pagedResult.CurrentPage - 2 || p == _pagedResult.CurrentPage + 2)
144
+ {
145
+ <span class="w-8 h-8 flex items-center justify-center text-xs text-muted">...</span>
146
+ }
147
+ }
148
+
149
+ <button @onclick="() => ChangePage(_pagedResult.CurrentPage + 1)" disabled="@(!_pagedResult.HasNextPage)"
150
+ class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
151
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
152
+ </button>
153
+ </div>
154
+ </div>
155
+ }
156
+ </div>
157
+ }
158
+ }
159
+ else if (_currentView == ViewMode.Details && _selectedUser != null)
160
+ {
161
+ <div class="animate-fade">
162
+ <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
163
+ <svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
164
+ Back to Directory
165
+ </button>
166
+
167
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
168
+ <div class="lg:col-span-1">
169
+ <div class="aspect-square w-full rounded-3xl bg-card border border-border flex flex-col items-center justify-center text-main p-6 shadow-sm relative overflow-hidden">
170
+ <div class="absolute inset-0 bg-primary/5"></div>
171
+ <div class="relative z-10 flex flex-col items-center">
172
+ <div class="w-32 h-32 rounded-full bg-main text-body flex items-center justify-center text-5xl font-black mb-6 shadow-xl">
173
+ @(_selectedUser.User.FullName?.Substring(0, Math.Min(2, _selectedUser.User.FullName.Length)).ToUpper() ?? "U")
174
+ </div>
175
+ <h2 class="text-2xl font-bold text-center mb-2">@_selectedUser.User.FullName</h2>
176
+ <div class="inline-flex rounded-full px-4 py-1.5 text-[10px] font-black uppercase tracking-widest @(_selectedUser.User.Role == "Admin" ? "bg-amber-500/10 text-amber-500 border border-amber-500/20" : "bg-blue-500/10 text-blue-500 border border-blue-500/20")">
177
+ @_selectedUser.User.Role
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+
183
+ <div class="lg:col-span-2 space-y-8">
184
+ <div>
185
+ <h1 class="text-4xl font-extrabold text-main tracking-tight mb-4">Member Profile</h1>
186
+ <div class="flex items-center gap-3">
187
+ @if (_selectedUser.User.BanStatus == true)
188
+ {
189
+ <span class="inline-flex items-center rounded-full bg-red-500/10 border border-red-500/20 px-4 py-1.5 text-[10px] font-black text-red-500 tracking-widest uppercase">Suspended</span>
190
+ }
191
+ else if (_selectedUser.User.IsActive)
192
+ {
193
+ <span class="inline-flex items-center rounded-full bg-green-500/10 border border-green-500/20 px-4 py-1.5 text-[10px] font-black text-green-500 tracking-widest uppercase">Active</span>
194
+ }
195
+ </div>
196
+ </div>
197
+
198
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-8 border-y border-border py-8">
199
+ <div>
200
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Email Address</h4>
201
+ <p class="text-sm font-semibold text-main">@_selectedUser.User.Email</p>
202
+ </div>
203
+ <div>
204
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Phone Number</h4>
205
+ <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.PhoneNumber) ? "-" : _selectedUser.User.PhoneNumber)</p>
206
+ </div>
207
+ <div>
208
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Student ID</h4>
209
+ <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.StudentId) ? "-" : _selectedUser.User.StudentId)</p>
210
+ </div>
211
+ <div>
212
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Joined Date</h4>
213
+ <p class="text-sm font-semibold text-main">@_selectedUser.User.CreatedAt.ToString("MMMM dd, yyyy")</p>
214
+ </div>
215
+ <div class="sm:col-span-2">
216
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Physical Address</h4>
217
+ <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.Address) ? "-" : _selectedUser.User.Address)</p>
218
+ </div>
219
+ </div>
220
+
221
+ <div class="flex flex-wrap items-center gap-4 pt-2">
222
+ <button @onclick="() => ShowEdit(_selectedUser.User.Id)" class="bg-primary text-white py-3 px-8 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Edit Details</button>
223
+
224
+ <button @onclick="UpdateRole" class="bg-body border border-border text-main py-3 px-8 rounded-xl font-bold hover:bg-card transition-colors">
225
+ Make @(_selectedUser.User.Role == "Admin" ? "Member" : "Admin")
226
+ </button>
227
+
228
+ <button @onclick="DeleteUser" class="p-3 text-red-500 hover:text-red-600 hover:bg-red-500/10 rounded-xl transition-colors ml-auto border border-transparent hover:border-red-500/20">
229
+ <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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
230
+ </button>
231
+ </div>
232
+
233
+ <!-- Subscription Management -->
234
+ <div class="mt-12 pt-12 border-t border-border">
235
+ <h3 class="text-2xl font-bold text-main mb-8">Manage Subscription</h3>
236
+
237
+ @if (_selectedUser.CurrentSubscription != null && _selectedUser.CurrentSubscription.IsActive)
238
+ {
239
+ <div class="bg-green-500/5 border border-green-500/20 rounded-2xl p-6 mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
240
+ <div>
241
+ <span class="text-[10px] text-green-600 font-black uppercase tracking-widest mb-1 block">Active Plan</span>
242
+ <h4 class="text-2xl font-black text-green-700">@_selectedUser.CurrentSubscription.MembershipType</h4>
243
+ </div>
244
+ <div class="sm:text-right">
245
+ <div class="text-[10px] text-green-600/70 font-black uppercase tracking-widest mb-1">Expires On</div>
246
+ <div class="font-bold text-green-700">@_selectedUser.CurrentSubscription.ExpiryDate.ToString("MMMM dd, yyyy")</div>
247
+ </div>
248
+ </div>
249
+ }
250
+ else
251
+ {
252
+ <div class="bg-card border border-dashed border-border rounded-2xl p-6 mb-8 text-center">
253
+ <p class="text-muted font-medium">No active subscription for this member.</p>
254
+ </div>
255
+ }
256
+
257
+ <h4 class="text-xs font-black text-muted uppercase tracking-[0.2em] mb-6">Assign New Plan</h4>
258
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
259
+ @foreach (var plan in _selectedUser.AvailableMemberships)
260
+ {
261
+ var isCurrent = _selectedUser.CurrentSubscription?.IsActive == true && _selectedUser.CurrentSubscription.MembershipId == plan.Id;
262
+ <div class="bg-card rounded-2xl border p-6 flex flex-col transition-all relative @(isCurrent ? "border-primary shadow-lg shadow-primary/10" : "border-border hover:border-primary/50")">
263
+ @if (isCurrent)
264
+ {
265
+ <div class="absolute -top-3 right-6 bg-primary text-white text-[9px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-sm">Current</div>
266
+ }
267
+ <h5 class="font-bold text-main text-xl mb-1">@plan.Type</h5>
268
+ <p class="text-muted font-medium text-sm mb-6 flex-grow">@plan.Price.ToString("N0") MMK / @plan.DurationMonths mo</p>
269
+
270
+ @if (!isCurrent)
271
+ {
272
+ <button @onclick="() => AssignSubscription(plan.Id, plan.Type)" class="w-full bg-body border border-border text-main py-2.5 rounded-xl text-sm font-bold hover:bg-primary hover:text-white hover:border-primary transition-colors">Assign Plan</button>
273
+ }
274
+ else
275
+ {
276
+ <button disabled class="w-full bg-body border border-border text-muted py-2.5 rounded-xl text-sm font-bold opacity-50 cursor-not-allowed">Current Plan</button>
277
+ }
278
+ </div>
279
+ }
280
+ </div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ }
286
+ else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
287
+ {
288
+ <div class="animate-fade">
289
+ <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
290
+ <svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
291
+ Back to Directory
292
+ </button>
293
+
294
+ <div class="max-w-3xl mx-auto">
295
+ <div class="mb-10 text-center">
296
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-3">@(_currentView == ViewMode.Create ? "Register New Member" : "Edit Member Details")</h1>
297
+ <p class="text-lg text-muted">@(_currentView == ViewMode.Create ? "Add a new reader to the library system." : $"Update profile details for {_editUser?.FullName}.")</p>
298
+ </div>
299
+
300
+ <div class="bg-card border border-border p-8 sm:p-10 rounded-3xl shadow-sm">
301
+ @if (_currentView == ViewMode.Create)
302
+ {
303
+ <EditForm Model="@_createRequest" OnValidSubmit="HandleCreate" class="space-y-6">
304
+ <DataAnnotationsValidator />
305
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
306
+ <div class="sm:col-span-2">
307
+ <label class="block text-sm font-bold text-main mb-2">Full Name <span class="text-red-500">*</span></label>
308
+ <InputText @bind-Value="_createRequest.FullName" 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 text-main transition-all" placeholder="John Doe" />
309
+ <ValidationMessage For="@(() => _createRequest.FullName)" class="mt-1 text-xs text-red-500 font-medium" />
310
+ </div>
311
+ <div>
312
+ <label class="block text-sm font-bold text-main mb-2">Email Address <span class="text-red-500">*</span></label>
313
+ <InputText @bind-Value="_createRequest.Email" type="email" 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 text-main transition-all" placeholder="you@domain.com" />
314
+ <ValidationMessage For="@(() => _createRequest.Email)" class="mt-1 text-xs text-red-500 font-medium" />
315
+ </div>
316
+ <div>
317
+ <label class="block text-sm font-bold text-main mb-2">Password <span class="text-red-500">*</span></label>
318
+ <InputText @bind-Value="_createRequest.Password" type="password" 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 text-main transition-all" placeholder="••••••••" />
319
+ <ValidationMessage For="@(() => _createRequest.Password)" class="mt-1 text-xs text-red-500 font-medium" />
320
+ </div>
321
+ <div>
322
+ <label class="block text-sm font-bold text-main mb-2">Phone Number</label>
323
+ <InputText @bind-Value="_createRequest.PhoneNumber" 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 text-main transition-all" placeholder="+123456789" />
324
+ </div>
325
+ <div>
326
+ <label class="block text-sm font-bold text-main mb-2">Student ID</label>
327
+ <InputText @bind-Value="_createRequest.StudentId" 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 text-main transition-all" placeholder="Optional" />
328
+ </div>
329
+ <div class="sm:col-span-2">
330
+ <label class="block text-sm font-bold text-main mb-2">Address</label>
331
+ <InputTextArea @bind-Value="_createRequest.Address" rows="3" 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 text-main transition-all" placeholder="Full physical address" />
332
+ </div>
333
+ </div>
334
+ <div class="pt-8 border-t border-border flex justify-end gap-4 mt-8">
335
+ <button type="button" @onclick="BackToList" class="bg-body border border-border text-main px-8 py-3 rounded-xl font-bold hover:bg-card transition-colors">Cancel</button>
336
+ <button type="submit" class="bg-primary text-white px-8 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Create Member</button>
337
+ </div>
338
+ </EditForm>
339
+ }
340
+ else if (_currentView == ViewMode.Edit)
341
+ {
342
+ <EditForm Model="@_updateRequest" OnValidSubmit="HandleUpdate" class="space-y-6">
343
+ <DataAnnotationsValidator />
344
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
345
+ <div class="sm:col-span-2">
346
+ <label class="block text-sm font-bold text-main mb-2">Full Name <span class="text-red-500">*</span></label>
347
+ <InputText @bind-Value="_updateRequest.FullName" 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 text-main transition-all" />
348
+ <ValidationMessage For="@(() => _updateRequest.FullName)" class="mt-1 text-xs text-red-500 font-medium" />
349
+ </div>
350
+ <div>
351
+ <label class="block text-sm font-bold text-main mb-2">Phone Number</label>
352
+ <InputText @bind-Value="_updateRequest.PhoneNumber" 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 text-main transition-all" />
353
+ </div>
354
+ <div>
355
+ <label class="block text-sm font-bold text-main mb-2">Student ID</label>
356
+ <InputText @bind-Value="_updateRequest.StudentId" 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 text-main transition-all" />
357
+ </div>
358
+ <div class="sm:col-span-2">
359
+ <label class="block text-sm font-bold text-main mb-2">Address</label>
360
+ <InputTextArea @bind-Value="_updateRequest.Address" rows="3" 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 text-main transition-all" />
361
+ </div>
362
+ </div>
363
+ <div class="pt-8 border-t border-border flex justify-end gap-4 mt-8">
364
+ <button type="button" @onclick="BackToList" class="bg-body border border-border text-main px-8 py-3 rounded-xl font-bold hover:bg-card transition-colors">Cancel</button>
365
+ <button type="submit" class="bg-primary text-white px-8 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Update Member</button>
366
+ </div>
367
+ </EditForm>
368
+ }
369
+ </div>
370
+ </div>
371
+ </div>
372
+ }
373
+ </div>
374
+
375
+ @code {
376
+ private enum ViewMode { List, Details, Create, Edit }
377
+ private ViewMode _currentView = ViewMode.List;
378
+
379
+ private IEnumerable<UserDto>? _allUsers;
380
+ private PagedResult<UserDto>? _pagedResult;
381
+ private bool _isLoading = true;
382
+ private const int PageSize = 10;
383
+
384
+ private MemberDetailsViewModel? _selectedUser;
385
+ private UserCreateRequest _createRequest = new();
386
+ private UserUpdateRequest _updateRequest = new();
387
+ private UserDto? _editUser;
388
+
389
+ protected override async Task OnInitializedAsync()
390
+ {
391
+ await LoadUsers();
392
+ }
393
+
394
+ private async Task LoadUsers()
395
+ {
396
+ _isLoading = true;
397
+ try
398
+ {
399
+ _allUsers = await ApiClient.GetUsersAsync();
400
+ ApplyPagination(1);
401
+ }
402
+ catch (Exception ex)
403
+ {
404
+ await JS.InvokeVoidAsync("alert", "Error loading users: " + ex.Message);
405
+ }
406
+ finally
407
+ {
408
+ _isLoading = false;
409
+ }
410
+ }
411
+
412
+ private void ApplyPagination(int page)
413
+ {
414
+ if (_allUsers == null) return;
415
+
416
+ var totalCount = _allUsers.Count();
417
+ var totalPages = (int)Math.Ceiling(totalCount / (double)PageSize);
418
+ page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
419
+
420
+ _pagedResult = new PagedResult<UserDto>
421
+ {
422
+ Items = _allUsers.Skip((page - 1) * PageSize).Take(PageSize),
423
+ CurrentPage = page,
424
+ TotalPages = totalPages,
425
+ TotalCount = totalCount,
426
+ PageSize = PageSize
427
+ };
428
+ }
429
+
430
+ private void ChangePage(int page)
431
+ {
432
+ ApplyPagination(page);
433
+ }
434
+
435
+ private async Task ShowDetails(Guid userId)
436
+ {
437
+ _isLoading = true;
438
+ try
439
+ {
440
+ var user = await ApiClient.GetUserAsync(userId);
441
+ if (user == null) return;
442
+
443
+ var memberships = await ApiClient.GetMembershipsAsync();
444
+ var subscription = await ApiClient.GetUserSubscriptionAsync(userId);
445
+
446
+ _selectedUser = new MemberDetailsViewModel
447
+ {
448
+ User = user,
449
+ AvailableMemberships = memberships.ToList(),
450
+ CurrentSubscription = subscription
451
+ };
452
+ _currentView = ViewMode.Details;
453
+ }
454
+ catch (Exception ex)
455
+ {
456
+ await JS.InvokeVoidAsync("alert", "Error loading member details: " + ex.Message);
457
+ }
458
+ finally
459
+ {
460
+ _isLoading = false;
461
+ }
462
+ }
463
+
464
+ private void ShowCreate()
465
+ {
466
+ _createRequest = new();
467
+ _currentView = ViewMode.Create;
468
+ }
469
+
470
+ private async Task ShowEdit(Guid userId)
471
+ {
472
+ _isLoading = true;
473
+ try
474
+ {
475
+ _editUser = _allUsers?.FirstOrDefault(u => u.Id == userId);
476
+ if (_editUser == null)
477
+ {
478
+ _editUser = await ApiClient.GetUserAsync(userId);
479
+ }
480
+
481
+ if (_editUser != null)
482
+ {
483
+ _updateRequest = new UserUpdateRequest
484
+ {
485
+ FullName = _editUser.FullName,
486
+ PhoneNumber = _editUser.PhoneNumber,
487
+ StudentId = _editUser.StudentId,
488
+ Address = _editUser.Address
489
+ };
490
+ _currentView = ViewMode.Edit;
491
+ }
492
+ }
493
+ finally
494
+ {
495
+ _isLoading = false;
496
+ }
497
+ }
498
+
499
+ private void BackToList()
500
+ {
501
+ _currentView = ViewMode.List;
502
+ _selectedUser = null;
503
+ _editUser = null;
504
+ }
505
+
506
+ private async Task HandleCreate()
507
+ {
508
+ try
509
+ {
510
+ var result = await ApiClient.CreateUserAsync(_createRequest);
511
+ if (result != null)
512
+ {
513
+ await JS.InvokeVoidAsync("alert", "Member created successfully!");
514
+ await LoadUsers();
515
+ BackToList();
516
+ }
517
+ else
518
+ {
519
+ await JS.InvokeVoidAsync("alert", "Failed to create member.");
520
+ }
521
+ }
522
+ catch (Exception ex)
523
+ {
524
+ await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
525
+ }
526
+ }
527
+
528
+ private async Task HandleUpdate()
529
+ {
530
+ if (_editUser == null) return;
531
+ try
532
+ {
533
+ var result = await ApiClient.UpdateUserAsync(_editUser.Id, _updateRequest);
534
+ if (result != null)
535
+ {
536
+ await JS.InvokeVoidAsync("alert", "Member updated successfully!");
537
+ await LoadUsers();
538
+ BackToList();
539
+ }
540
+ }
541
+ catch (Exception ex)
542
+ {
543
+ await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
544
+ }
545
+ }
546
+
547
+ private async Task DeleteUser()
548
+ {
549
+ if (_selectedUser == null) return;
550
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", "Are you sure you want to completely remove this member? This action cannot be undone.");
551
+ if (!confirmed) return;
552
+
553
+ try
554
+ {
555
+ var success = await ApiClient.DeleteUserAsync(_selectedUser.User.Id);
556
+ if (success)
557
+ {
558
+ await JS.InvokeVoidAsync("alert", "Member removed.");
559
+ await LoadUsers();
560
+ BackToList();
561
+ }
562
+ }
563
+ catch (Exception ex)
564
+ {
565
+ await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
566
+ }
567
+ }
568
+
569
+ private async Task UpdateRole()
570
+ {
571
+ if (_selectedUser == null) return;
572
+ var newRole = _selectedUser.User.Role == "Admin" ? "Member" : "Admin";
573
+ try
574
+ {
575
+ var success = await ApiClient.UpdateUserRoleAsync(_selectedUser.User.Id, newRole);
576
+ if (success)
577
+ {
578
+ await JS.InvokeVoidAsync("alert", $"Role updated to {newRole}.");
579
+ await ShowDetails(_selectedUser.User.Id); // Refresh details
580
+ }
581
+ }
582
+ catch (Exception ex)
583
+ {
584
+ await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
585
+ }
586
+ }
587
+
588
+ private async Task AssignSubscription(int membershipId, string planName)
589
+ {
590
+ if (_selectedUser == null) return;
591
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Assign {planName} plan to this member?");
592
+ if (!confirmed) return;
593
+
594
+ try
595
+ {
596
+ var request = new AdminSubscribeRequest { UserId = _selectedUser.User.Id, MembershipId = membershipId };
597
+ var result = await ApiClient.AdminSubscribeAsync(request);
598
+ if (result != null)
599
+ {
600
+ await JS.InvokeVoidAsync("alert", "Subscription assigned successfully.");
601
+ await ShowDetails(_selectedUser.User.Id); // Refresh details
602
+ }
603
+ }
604
+ catch (Exception ex)
605
+ {
606
+ await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
607
+ }
608
+ }
609
+ }
BlazorWebAssembly/Pages/Membership.razor CHANGED
@@ -3,11 +3,13 @@
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>
@@ -18,83 +20,116 @@
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();
@@ -121,10 +156,18 @@
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
  }
 
3
  @using BlazorWebAssembly.Services
4
  @inject LibraryApiClient ApiClient
5
  @inject NavigationManager Navigation
6
+ @inject IJSRuntime JS
7
 
8
+ <div class="max-w-7xl mx-auto py-24 px-8 animate-fade">
9
  <!-- Header -->
10
+ <div class="text-center mb-24">
11
+ <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Pricing Plans</h2>
12
+ <h1 class="text-7xl font-black tracking-tighter text-main mb-8 uppercase leading-[0.9]">Choose Your <br /> <span class="text-primary italic">Journey.</span></h1>
13
  <p class="text-xl text-muted max-w-2xl mx-auto leading-relaxed">
14
  From casual readers to devoted bookworms, we have a plan that perfectly matches your literary appetite.
15
  </p>
 
20
  <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
21
  @for (int i = 0; i < 3; i++)
22
  {
23
+ <div class="animate-pulse bg-card rounded-[3rem] border border-border h-[600px]"></div>
24
  }
25
  </div>
26
  }
27
  else
28
  {
29
+ <!-- Current Subscription Banner (Only for Auth) -->
30
+ <AuthorizeView>
31
+ <Authorized>
32
+ @if (_currentSub != null)
33
+ {
34
+ <div class="bg-primary/5 border border-primary/20 rounded-[2.5rem] p-10 mb-20 flex flex-col md:flex-row items-center justify-between gap-8 shadow-sm">
35
+ <div class="flex items-center gap-8">
36
+ <div class="w-20 h-20 bg-primary rounded-3xl flex items-center justify-center text-white text-4xl font-black shadow-lg shadow-primary/20">
37
+ @(_currentSub.MembershipType?[..1].ToUpper())
38
+ </div>
39
+ <div>
40
+ <h3 class="text-3xl font-black text-main uppercase tracking-tight">@_currentSub.MembershipType Plan</h3>
41
+ <p class="text-muted font-medium">Active until <span class="text-main font-bold">@_currentSub.ExpiryDate.ToString("MMMM dd, yyyy")</span></p>
42
+ </div>
43
+ </div>
44
+ <div class="flex items-center gap-6">
45
+ <div class="flex flex-col items-end">
46
+ <span class="px-4 py-1.5 bg-green-500/10 text-green-600 rounded-full text-[10px] font-black uppercase tracking-widest mb-2">Active Status</span>
47
+ <span class="text-xs text-muted font-bold uppercase tracking-widest">Auto-renews monthly</span>
48
+ </div>
49
+ </div>
50
  </div>
51
+ }
52
+ </Authorized>
53
+ </AuthorizeView>
 
 
 
54
 
55
  <!-- Membership Plans -->
56
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-10">
57
  @foreach (var plan in _memberships)
58
  {
59
+ <div class='relative bg-card rounded-[3.5rem] border p-12 flex flex-col transition-all duration-700 hover:-translate-y-4 @(IsCurrentPlan(plan.Id) ? "border-primary shadow-2xl shadow-primary/10 scale-105 z-10" : "border-border shadow-xl hover:shadow-2xl")'>
60
+ @if (plan.Price > 20 && !IsCurrentPlan(plan.Id))
61
+ {
62
+ <div class="absolute -top-5 left-1/2 -translate-x-1/2 bg-emerald-500 text-white px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-emerald-500/30">
63
+ Most Popular
64
+ </div>
65
+ }
66
  @if (IsCurrentPlan(plan.Id))
67
  {
68
+ <div class="absolute -top-5 left-1/2 -translate-x-1/2 bg-primary text-white px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-primary/30">
69
+ Your Current Plan
70
  </div>
71
  }
72
 
73
  <div class="mb-10">
74
+ <h3 class="text-sm font-black text-muted uppercase tracking-[0.3em] mb-6">@plan.Type</h3>
75
+ <div class="flex flex-col gap-1">
76
+ <div class="flex items-baseline gap-2 flex-wrap">
77
+ <span class="text-5xl font-black text-main tracking-tighter">@plan.Price.ToString("N0")</span>
78
+ <span class="text-2xl font-bold text-muted">mmk</span>
79
+ </div>
80
+ <span class="text-muted font-bold uppercase text-sm tracking-widest mt-1">/ @plan.DurationMonths mo</span>
81
  </div>
82
  </div>
83
 
84
+ <div class="space-y-6 mb-16 flex-grow">
85
+ <div class="flex items-center gap-5 group/item">
86
+ <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0">
87
+ <svg class="w-4 h-4" 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>
88
  </div>
89
+ <span class="text-main font-bold text-lg">Borrow <span class="text-primary">@plan.MaxBooks</span> books</span>
90
  </div>
91
+ <div class="flex items-center gap-5 group/item">
92
+ <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0">
93
+ <svg class="w-4 h-4" 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>
94
  </div>
95
+ <span class="text-main font-bold text-lg">@plan.BorrowingDays Day period</span>
96
  </div>
97
+ <div class="flex items-center gap-5 group/item">
98
+ <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0">
99
+ <svg class="w-4 h-4" 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>
100
  </div>
101
+ <span class="text-main font-bold text-lg">Digital access included</span>
102
+ </div>
103
+ <div class="flex items-center gap-5 group/item">
104
+ <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0">
105
+ <svg class="w-4 h-4" 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>
106
+ </div>
107
+ <span class="text-main font-bold text-lg">Loyalty point multiplier</span>
108
  </div>
109
  </div>
110
 
111
  <button @onclick="() => SubscribeAsync(plan)"
112
  disabled="@IsCurrentPlan(plan.Id)"
113
+ class='w-full py-6 rounded-[2rem] font-black uppercase tracking-widest text-sm transition-all @(IsCurrentPlan(plan.Id) ? "bg-body text-muted border border-border cursor-not-allowed" : "bg-primary text-white hover:bg-primary-hover hover:shadow-2xl hover:shadow-primary/30 transform hover:scale-[1.02] active:scale-95")'>
114
+ @(IsCurrentPlan(plan.Id) ? "Current Plan" : "Select Plan")
115
  </button>
116
  </div>
117
  }
118
  </div>
119
+
120
+ <div class="mt-24 text-center">
121
+ <p class="text-muted font-medium mb-8">All plans include 24/7 support and access to our community events.</p>
122
+ <div class="flex items-center justify-center gap-12 opacity-50 grayscale hover:grayscale-0 transition-all">
123
+ <!-- Trust Badges Placeholder -->
124
+ <span class="font-black text-xl uppercase tracking-tighter">Secure Payment</span>
125
+ <span class="font-black text-xl uppercase tracking-tighter">Cancel Anytime</span>
126
+ <span class="font-black text-xl uppercase tracking-tighter">No Hidden Fees</span>
127
+ </div>
128
+ </div>
129
  }
130
  </div>
131
 
132
+
133
  @code {
134
  private bool _isLoading = true;
135
  private List<MembershipDto> _memberships = new();
 
156
 
157
  private async Task SubscribeAsync(MembershipDto plan)
158
  {
159
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Are you sure you want to subscribe to {plan.Type} for {plan.Price} mmk?");
160
+ if (!confirmed) return;
161
+
162
  var result = await ApiClient.SubscribeAsync(new SubscribeRequest { MembershipId = plan.Id });
163
  if (result != null)
164
  {
165
+ await JS.InvokeVoidAsync("alert", "Subscription successful!");
166
  await LoadData();
167
  }
168
+ else
169
+ {
170
+ await JS.InvokeVoidAsync("alert", "Subscription failed. Please check your account balance or try again.");
171
+ }
172
  }
173
  }
BlazorWebAssembly/Pages/Privacy.razor ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/privacy"
2
+
3
+ <div class="max-w-4xl mx-auto py-24 px-8 animate-fade">
4
+ <h1 class="text-5xl font-black tracking-tighter text-main mb-8 uppercase">Privacy Policy</h1>
5
+ <p class="text-sm text-muted mb-12 font-bold uppercase tracking-widest">Last Updated: May 2026</p>
6
+
7
+ <div class="space-y-12 text-muted leading-relaxed">
8
+ <section>
9
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">1. Information We Collect</h2>
10
+ <p>
11
+ We collect information you provide directly to us when you create an account, subscribe to a membership, or interact with our library services. This includes your name, email address, payment information, and reading history.
12
+ </p>
13
+ </section>
14
+
15
+ <section>
16
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">2. How We Use Your Information</h2>
17
+ <p>
18
+ We use the information we collect to maintain your account, process transactions, provide personalized recommendations, and send you important notifications regarding your loans and memberships.
19
+ </p>
20
+ </section>
21
+
22
+ <section>
23
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">3. Data Security</h2>
24
+ <p>
25
+ We implement a variety of security measures to maintain the safety of your personal information. Your sensitive data is encrypted via SSL technology and stored behind secured networks.
26
+ </p>
27
+ </section>
28
+
29
+ <section>
30
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">4. Cookies</h2>
31
+ <p>
32
+ We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction.
33
+ </p>
34
+ </section>
35
+
36
+ <section class="p-8 bg-card rounded-3xl border border-border">
37
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">Contact Privacy Officer</h2>
38
+ <p class="mb-4">If you have any questions regarding this privacy policy, you may contact our privacy officer at:</p>
39
+ <p class="font-bold text-primary">privacy@libraryluxe.com</p>
40
+ </section>
41
+ </div>
42
+ </div>
BlazorWebAssembly/Pages/Rewards.razor CHANGED
@@ -3,6 +3,7 @@
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 -->
@@ -94,7 +95,11 @@
94
  if (success)
95
  {
96
  await LoadData();
97
- // Show success message
 
 
 
 
98
  }
99
  }
100
  }
 
3
  @using BlazorWebAssembly.Services
4
  @inject LibraryApiClient ApiClient
5
  @inject NavigationManager Navigation
6
+ @inject IJSRuntime JS
7
 
8
  <div class="max-w-6xl mx-auto py-12 px-6">
9
  <!-- Header -->
 
95
  if (success)
96
  {
97
  await LoadData();
98
+ await JS.InvokeVoidAsync("alert", msg);
99
+ }
100
+ else
101
+ {
102
+ await JS.InvokeVoidAsync("alert", msg);
103
  }
104
  }
105
  }
BlazorWebAssembly/Pages/RewardsManage.razor ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/rewards/manage"
2
+ @using BlazorWebAssembly.Services
3
+ @using LibraryManagement.Shared.Models
4
+ @using Microsoft.AspNetCore.Authorization
5
+ @inject LibraryApiClient ApiClient
6
+ @inject IJSRuntime JS
7
+ @attribute [Authorize(Roles = "Librarian")]
8
+
9
+ <div class="max-w-7xl mx-auto py-10 px-6 animate-fade">
10
+ <div class="flex flex-col md:flex-row md:items-center justify-between mb-10 gap-6">
11
+ <div>
12
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Rewards Management</h1>
13
+ <p class="text-muted text-lg font-medium">Fulfill pending redemptions and review all members' loyalty activity.</p>
14
+ </div>
15
+ <div class="flex items-center gap-4">
16
+ <div class="bg-card px-6 py-3 flex items-center gap-4 border border-border rounded-xl shadow-sm">
17
+ <div class="text-center">
18
+ <p class="text-[10px] font-black text-amber-500 uppercase tracking-widest">Pending</p>
19
+ <p class="text-2xl font-black text-main">@_pendingRedemptions.Count()</p>
20
+ </div>
21
+ <div class="w-px h-8 bg-border"></div>
22
+ <div class="text-center">
23
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest">Members</p>
24
+ <p class="text-2xl font-black text-main">@_allMembersHistory.Count()</p>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="mb-8 border-b border-border flex gap-8">
31
+ <button @onclick='() => _activeTab = "pending"'
32
+ class='pb-4 text-sm font-bold border-b-2 transition-all relative @(_activeTab == "pending" ? "border-primary text-main" : "border-transparent text-muted hover:text-main")'>
33
+ <span class="flex items-center gap-2">
34
+ Pending Redemptions
35
+ @if (_pendingRedemptions.Any())
36
+ {
37
+ <span class="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-[10px] font-black rounded-full bg-red-500 text-white">@_pendingRedemptions.Count()</span>
38
+ }
39
+ </span>
40
+ </button>
41
+ <button @onclick='() => _activeTab = "history"'
42
+ class='pb-4 text-sm font-bold border-b-2 transition-all @(_activeTab == "history" ? "border-primary text-main" : "border-transparent text-muted hover:text-main")'>
43
+ Members Points History
44
+ </button>
45
+ </div>
46
+
47
+ @if (_isLoading)
48
+ {
49
+ <div class="space-y-4">
50
+ @for (int i = 0; i < 5; i++)
51
+ {
52
+ <div class="animate-pulse bg-card rounded-xl border border-border h-20 w-full"></div>
53
+ }
54
+ </div>
55
+ }
56
+ else
57
+ {
58
+ @if (_activeTab == "pending")
59
+ {
60
+ <div class="animate-fade">
61
+ @if (!_pendingRedemptions.Any())
62
+ {
63
+ <div class="py-24 text-center bg-card rounded-3xl border border-dashed border-border shadow-sm">
64
+ <p class="text-muted font-bold text-lg mb-2">No pending redemptions</p>
65
+ <p class="text-muted/70 text-sm">All member requests have been fulfilled.</p>
66
+ </div>
67
+ }
68
+ else
69
+ {
70
+ <div class="bg-card rounded-2xl border border-border overflow-hidden shadow-sm">
71
+ <table class="w-full text-left border-collapse min-w-max whitespace-nowrap">
72
+ <thead class="bg-body border-b border-border">
73
+ <tr>
74
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Member</th>
75
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Reward</th>
76
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Points</th>
77
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Claimed Date</th>
78
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Status</th>
79
+ <th class="px-6 py-4 text-right text-[10px] font-bold text-muted uppercase tracking-widest">Action</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody class="divide-y divide-border">
83
+ @foreach (var r in _pendingRedemptions.OrderBy(x => x.RedeemedAt))
84
+ {
85
+ <tr class="hover:bg-body transition-colors">
86
+ <td class="px-6 py-4">
87
+ <div class="flex items-center gap-3">
88
+ <div class="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center text-xs font-bold shadow-sm">
89
+ @(r.ExternalUserId.Length >= 2 ? r.ExternalUserId.Substring(0, 2).ToUpper() : "U")
90
+ </div>
91
+ <span class="text-sm font-bold text-main">@r.ExternalUserId</span>
92
+ </div>
93
+ </td>
94
+ <td class="px-6 py-4">
95
+ <p class="text-sm font-bold text-main">@r.RewardName</p>
96
+ <p class="text-[10px] text-muted font-bold tracking-widest uppercase">ID: @(r.Id.Length > 8 ? r.Id[..8] : r.Id)</p>
97
+ </td>
98
+ <td class="px-6 py-4 text-sm font-black text-primary">@r.PointCost.ToString("N0") pts</td>
99
+ <td class="px-6 py-4 text-sm font-medium text-muted">@r.RedeemedAt.ToString("MMM dd, yyyy")</td>
100
+ <td class="px-6 py-4">
101
+ <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-amber-500/10 text-amber-500">
102
+ <span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
103
+ Pending
104
+ </span>
105
+ </td>
106
+ <td class="px-6 py-4 text-right">
107
+ <button @onclick="() => FulfillAsync(r)"
108
+ class="inline-flex items-center gap-1.5 bg-green-500/10 text-green-500 border border-green-500/20 px-4 py-2 rounded-lg text-xs font-bold hover:bg-green-500 hover:text-white transition-all shadow-sm active:scale-95">
109
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path></svg>
110
+ Fulfill
111
+ </button>
112
+ </td>
113
+ </tr>
114
+ }
115
+ </tbody>
116
+ </table>
117
+ </div>
118
+ }
119
+ </div>
120
+ }
121
+ else if (_activeTab == "history")
122
+ {
123
+ <div class="animate-fade">
124
+ @if (!_allMembersHistory.Any())
125
+ {
126
+ <div class="py-24 text-center bg-card rounded-3xl border border-dashed border-border shadow-sm">
127
+ <p class="text-muted font-bold text-lg">No loyalty history found.</p>
128
+ </div>
129
+ }
130
+ else
131
+ {
132
+ <div class="mb-6">
133
+ <input type="text" placeholder="Filter by member name or email..."
134
+ class="w-full max-w-md bg-card border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main shadow-sm transition-all"
135
+ @bind="_searchQuery" @bind:event="oninput" />
136
+ </div>
137
+
138
+ <div class="space-y-4">
139
+ @foreach (var member in FilteredMembers)
140
+ {
141
+ <div class="bg-card border border-border rounded-2xl overflow-hidden shadow-sm hover:border-primary/50 transition-colors">
142
+ <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between px-6 py-5 cursor-pointer hover:bg-body transition-colors"
143
+ @onclick="() => ToggleMember(member.AccountId)">
144
+ <div class="flex items-center gap-4 mb-4 sm:mb-0">
145
+ <div class="w-12 h-12 rounded-full bg-primary/10 text-primary border border-primary/20 flex items-center justify-center font-bold shadow-inner flex-shrink-0">
146
+ @(member.UserName.Length >= 2 ? member.UserName.Substring(0, 2).ToUpper() : "?")
147
+ </div>
148
+ <div>
149
+ <p class="font-bold text-main text-lg">@member.UserName</p>
150
+ <p class="text-xs font-medium text-muted">@member.UserEmail</p>
151
+ </div>
152
+ </div>
153
+ <div class="flex items-center gap-6 w-full sm:w-auto justify-between sm:justify-end">
154
+ <div class="text-right">
155
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest">Balance</p>
156
+ <p class="font-black text-main text-lg">@member.CurrentBalance.ToString("N0") pts</p>
157
+ </div>
158
+ <div class="text-right hidden md:block">
159
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest">Tier</p>
160
+ <span class="text-xs font-bold text-primary">@member.Tier</span>
161
+ </div>
162
+ <div class="text-right hidden sm:block">
163
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest">History</p>
164
+ <p class="font-bold text-main text-sm">@member.History.Count() items</p>
165
+ </div>
166
+ <svg class="w-5 h-5 text-muted transition-transform @(_expandedMembers.Contains(member.AccountId) ? "rotate-180" : "")" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
167
+ </div>
168
+ </div>
169
+
170
+ @if (_expandedMembers.Contains(member.AccountId))
171
+ {
172
+ <div class="border-t border-border bg-body animate-fade-in-down">
173
+ @if (!member.History.Any())
174
+ {
175
+ <p class="px-6 py-6 text-sm text-muted font-medium text-center">No transactions found for this member.</p>
176
+ }
177
+ else
178
+ {
179
+ <div class="overflow-x-auto">
180
+ <table class="min-w-full text-left min-w-max whitespace-nowrap">
181
+ <thead>
182
+ <tr class="bg-card">
183
+ <th class="px-6 py-3 text-[10px] font-bold text-muted uppercase tracking-widest">Date</th>
184
+ <th class="px-6 py-3 text-[10px] font-bold text-muted uppercase tracking-widest">Type</th>
185
+ <th class="px-6 py-3 text-[10px] font-bold text-muted uppercase tracking-widest">Details</th>
186
+ <th class="px-6 py-3 text-[10px] font-bold text-muted uppercase tracking-widest">Subject</th>
187
+ <th class="px-6 py-3 text-right text-[10px] font-bold text-muted uppercase tracking-widest">Points</th>
188
+ </tr>
189
+ </thead>
190
+ <tbody class="divide-y divide-border">
191
+ @foreach (var h in member.History.OrderByDescending(x => x.CreatedAt))
192
+ {
193
+ var isPos = h.PointDelta >= 0;
194
+ var label = PrettyEvent(h.EventKey, h.PointDelta);
195
+ string? spentOn = h.RewardName;
196
+
197
+ <tr class="hover:bg-card transition-colors">
198
+ <td class="px-6 py-3 text-xs font-medium text-muted whitespace-nowrap">@h.CreatedAt.ToString("MMM dd, yyyy")</td>
199
+ <td class="px-6 py-3">
200
+ <span class="px-2 py-0.5 rounded-sm text-[9px] font-black uppercase tracking-widest border @(isPos ? "bg-green-500/10 text-green-500 border-green-500/20" : "bg-red-500/10 text-red-500 border-red-500/20")">@label</span>
201
+ </td>
202
+ <td class="px-6 py-3 text-xs font-medium text-main max-w-xs truncate">@h.Description</td>
203
+ <td class="px-6 py-3 text-xs font-bold text-muted max-w-xs truncate">
204
+ @if (!string.IsNullOrWhiteSpace(spentOn))
205
+ {
206
+ <span class="text-main">@spentOn</span>
207
+ }
208
+ else
209
+ {
210
+ <span>—</span>
211
+ }
212
+ </td>
213
+ <td class="px-6 py-3 text-right font-black text-sm @(isPos ? "text-green-500" : "text-red-500")">
214
+ @(isPos ? "+" : "")@h.PointDelta.ToString("N0")
215
+ </td>
216
+ </tr>
217
+ }
218
+ </tbody>
219
+ </table>
220
+ </div>
221
+ }
222
+ </div>
223
+ }
224
+ </div>
225
+ }
226
+ </div>
227
+ }
228
+ </div>
229
+ }
230
+ }
231
+ </div>
232
+
233
+ @code {
234
+ private string _activeTab = "pending";
235
+ private bool _isLoading = true;
236
+ private string _searchQuery = "";
237
+ private IEnumerable<LoyaltyRedemptionDto> _pendingRedemptions = Enumerable.Empty<LoyaltyRedemptionDto>();
238
+ private IEnumerable<UserPointsHistoryDto> _allMembersHistory = Enumerable.Empty<UserPointsHistoryDto>();
239
+ private HashSet<string> _expandedMembers = new();
240
+
241
+ protected override async Task OnInitializedAsync()
242
+ {
243
+ await LoadDataAsync();
244
+ }
245
+
246
+ private async Task LoadDataAsync()
247
+ {
248
+ _isLoading = true;
249
+ try
250
+ {
251
+ _pendingRedemptions = await ApiClient.GetPendingRedemptionsAsync();
252
+ _allMembersHistory = await ApiClient.GetAllMembersPointsHistoryAsync();
253
+ }
254
+ catch (Exception ex)
255
+ {
256
+ await JS.InvokeVoidAsync("alert", $"Error loading management data: {ex.Message}");
257
+ }
258
+ finally
259
+ {
260
+ _isLoading = false;
261
+ }
262
+ }
263
+
264
+ private IEnumerable<UserPointsHistoryDto> FilteredMembers =>
265
+ string.IsNullOrWhiteSpace(_searchQuery)
266
+ ? _allMembersHistory
267
+ : _allMembersHistory.Where(m => m.UserName.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) ||
268
+ m.UserEmail.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase));
269
+
270
+ private void ToggleMember(string accountId)
271
+ {
272
+ if (_expandedMembers.Contains(accountId))
273
+ _expandedMembers.Remove(accountId);
274
+ else
275
+ _expandedMembers.Add(accountId);
276
+ }
277
+
278
+ private async Task FulfillAsync(LoyaltyRedemptionDto redemption)
279
+ {
280
+ bool confirmed = await JS.InvokeAsync<bool>("confirm", "Fulfill this redemption and grant membership to the member?");
281
+ if (!confirmed) return;
282
+
283
+ var result = await ApiClient.FulfillRedemptionAsync(redemption.Id);
284
+ if (result.Success)
285
+ {
286
+ await JS.InvokeVoidAsync("alert", result.Message);
287
+ await LoadDataAsync();
288
+ }
289
+ else
290
+ {
291
+ await JS.InvokeVoidAsync("alert", result.Message);
292
+ }
293
+ }
294
+
295
+ private string PrettyEvent(string? eventKey, double points)
296
+ {
297
+ var key = (eventKey ?? string.Empty).Trim().ToUpperInvariant();
298
+
299
+ return key switch
300
+ {
301
+ "RETURN" or "BOOK_RETURN" or "BORROW_RETURN" => "RETURN",
302
+ "SIGNUP" or "REGISTER" => "SIGNUP",
303
+ "SUBSCRIBE" or "SUBSCRIPTION" or "MEMBERSHIP" => "SUBSCRIBE",
304
+ "REDEEM" or "REDEMPTION" or "CLAIM_REWARD" => "REDEEM",
305
+ _ => points < 0 ? "SPENT" : (string.IsNullOrEmpty(key) ? "EARNED" : key)
306
+ };
307
+ }
308
+ }
BlazorWebAssembly/Pages/Terms.razor ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @page "/terms"
2
+
3
+ <div class="max-w-4xl mx-auto py-24 px-8 animate-fade">
4
+ <h1 class="text-5xl font-black tracking-tighter text-main mb-8 uppercase">Terms of Service</h1>
5
+ <p class="text-sm text-muted mb-12 font-bold uppercase tracking-widest">Effective Date: May 2026</p>
6
+
7
+ <div class="space-y-12 text-muted leading-relaxed">
8
+ <section>
9
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">1. Acceptance of Terms</h2>
10
+ <p>
11
+ By accessing and using LibraryLuxe, you agree to be bound by these Terms of Service and all applicable laws and regulations.
12
+ </p>
13
+ </section>
14
+
15
+ <section>
16
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">2. Use License</h2>
17
+ <p>
18
+ Permission is granted to temporarily download one copy of the materials on LibraryLuxe's website for personal, non-commercial transitory viewing only.
19
+ </p>
20
+ </section>
21
+
22
+ <section>
23
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">3. User Conduct</h2>
24
+ <p>
25
+ Users are expected to return borrowed materials on time. Repeated late returns may lead to account suspension or loyalty point penalties.
26
+ </p>
27
+ </section>
28
+
29
+ <section>
30
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">4. Account Termination</h2>
31
+ <p>
32
+ We reserve the right to terminate or suspend access to our service immediately, without prior notice or liability, for any reason whatsoever, including without limitation if you breach the Terms.
33
+ </p>
34
+ </section>
35
+
36
+ <section class="p-8 bg-card rounded-3xl border border-border">
37
+ <h2 class="text-xl font-bold text-main mb-4 uppercase tracking-tight">Questions?</h2>
38
+ <p class="mb-4">If you have any questions about these Terms, please contact us.</p>
39
+ <a href="/contact" class="text-primary font-bold hover:underline">Contact Support</a>
40
+ </section>
41
+ </div>
42
+ </div>
BlazorWebAssembly/Pages/UserInsights.razor CHANGED
@@ -6,7 +6,7 @@
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>
@@ -16,85 +16,255 @@
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
  }
 
6
  @using Microsoft.AspNetCore.Authorization
7
  @attribute [Authorize(Roles = "Librarian")]
8
 
9
+ <div class="max-w-7xl mx-auto py-12 px-6 animate-fade">
10
  <!-- Header -->
11
  <div class="flex flex-col md:flex-row justify-between items-start md:items-end mb-16 gap-6">
12
  <div>
 
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 shadow-sm">
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">@(_totalMembers)</span>
22
  </div>
23
+ <div class="bg-primary/10 border border-primary/20 px-6 py-3 rounded-2xl flex flex-col text-primary shadow-sm">
24
+ <span class="text-[10px] font-black text-primary uppercase tracking-[0.2em]">Active Subs</span>
25
+ <span class="text-2xl font-black text-primary">@(_activeSubscriptions)</span>
26
  </div>
27
  </div>
28
  </div>
29
 
30
+ @if (_isLoading)
31
+ {
32
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-16">
33
+ @for (int i = 0; i < 3; i++)
34
+ {
35
+ <div class="bg-card rounded-[2rem] border border-border p-10 shadow-sm animate-pulse h-96"></div>
36
+ }
37
+ </div>
38
+ }
39
+ else
40
+ {
41
+ <!-- Metrics Grid -->
42
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-16">
43
+
44
+ <!-- Most Active Readers -->
45
+ <div class="bg-card rounded-[2rem] border border-border p-10 shadow-sm flex flex-col">
46
+ <div class="flex items-center justify-between mb-8 border-b border-border pb-4">
47
+ <h3 class="text-xl font-bold text-main uppercase tracking-widest text-xs">Top Readers</h3>
48
+ <div class="w-8 h-8 rounded-full bg-blue-500/10 text-blue-500 flex items-center justify-center">
49
+ <svg class="w-4 h-4" 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>
50
  </div>
51
+ </div>
52
+ <div class="space-y-6 flex-grow">
53
+ @if (!_activeReaders.Any())
54
+ {
55
+ <p class="text-muted text-sm italic text-center py-10">No data available yet.</p>
56
+ }
57
+ @foreach (var (user, index) in _activeReaders.Take(5).Select((u, i) => (u, i)))
58
+ {
59
+ var maxRaw = _activeReaders.Max(u => u.RawValue);
60
+ var pct = maxRaw > 0 ? (user.RawValue / maxRaw) * 100 : 0;
61
+ <div class="flex items-center gap-4">
62
+ <span class="w-8 h-8 rounded-lg bg-body flex items-center justify-center font-black text-muted text-xs">@(index + 1)</span>
63
+ <div class="flex-grow">
64
+ <p class="font-bold text-main text-sm truncate max-w-[150px]">@user.Name</p>
65
+ <p class="text-[10px] text-muted font-black uppercase tracking-widest">@user.ValueLabel</p>
66
+ </div>
67
+ <div class="w-16 bg-body h-1.5 rounded-full overflow-hidden">
68
+ <div class="bg-blue-500 h-full rounded-full" style="width: @pct%"></div>
69
+ </div>
70
+ </div>
71
+ }
72
+ </div>
73
  </div>
 
74
 
75
+ <!-- Loyalty Leaders -->
76
+ <div class="bg-card rounded-[2rem] border border-border p-10 shadow-sm flex flex-col">
77
+ <div class="flex items-center justify-between mb-8 border-b border-border pb-4">
78
+ <h3 class="text-xl font-bold text-main uppercase tracking-widest text-xs">Point Leaders</h3>
79
+ <div class="w-8 h-8 rounded-full bg-amber-500/10 text-amber-500 flex items-center justify-center">
80
+ <svg class="w-4 h-4" 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>
 
 
 
 
 
 
 
 
 
81
  </div>
82
+ </div>
83
+ <div class="space-y-6 flex-grow">
84
+ @if (!_loyalty.Any())
85
+ {
86
+ <p class="text-muted text-sm italic text-center py-10">No data available yet.</p>
87
+ }
88
+ @foreach (var (user, index) in _loyalty.Take(5).Select((u, i) => (u, i)))
89
+ {
90
+ <div class="flex items-center gap-4">
91
+ <div class="w-10 h-10 rounded-full bg-amber-500/10 flex items-center justify-center text-amber-500 font-black text-xs">
92
+ @(user.Name.Length >= 2 ? user.Name.Substring(0, 2).ToUpper() : "?")
93
+ </div>
94
+ <div class="flex-grow">
95
+ <p class="font-bold text-main text-sm truncate max-w-[150px]">@user.Name</p>
96
+ <p class="text-[10px] text-muted font-black uppercase tracking-widest">@user.ValueLabel</p>
97
+ </div>
98
+ <span class="text-[10px] font-black text-amber-500 bg-amber-500/10 px-2 py-1 rounded-md">#@(index + 1)</span>
99
+ </div>
100
+ }
101
+ </div>
102
  </div>
 
103
 
104
+ <!-- Top Subscribers -->
105
+ <div class="bg-card rounded-[2rem] border border-border p-10 shadow-sm flex flex-col">
106
+ <div class="flex items-center justify-between mb-8 border-b border-border pb-4">
107
+ <h3 class="text-xl font-bold text-main uppercase tracking-widest text-xs">Top Subscribers</h3>
108
+ <div class="w-8 h-8 rounded-full bg-emerald-500/10 text-emerald-500 flex items-center justify-center">
109
+ <svg class="w-4 h-4" 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>
110
+ </div>
 
 
 
111
  </div>
112
+ <div class="space-y-6 flex-grow">
113
+ @if (!_subscribers.Any())
114
+ {
115
+ <p class="text-muted text-sm italic text-center py-10">No data available yet.</p>
116
+ }
117
+ @foreach (var (user, index) in _subscribers.Take(5).Select((u, i) => (u, i)))
118
+ {
119
+ <div class="flex items-center gap-4">
120
+ <span class="w-8 h-8 rounded-lg bg-emerald-500/10 text-emerald-600 flex items-center justify-center font-black text-xs border border-emerald-500/20">@(index + 1)</span>
121
+ <div class="flex-grow">
122
+ <p class="font-bold text-main text-sm truncate max-w-[150px]">@user.Name</p>
123
+ <p class="text-[10px] text-muted font-black uppercase tracking-widest">@user.ValueLabel</p>
124
+ </div>
125
+ </div>
126
+ }
127
  </div>
128
  </div>
129
  </div>
130
+
131
+ <div class="bg-card border border-border rounded-2xl overflow-hidden shadow-sm">
132
+ <div class="p-6 border-b border-border bg-body">
133
+ <h3 class="text-lg font-bold text-main">Full Member Insights (All Users)</h3>
134
+ </div>
135
+ <div class="overflow-x-auto">
136
+ <table class="w-full text-left border-collapse">
137
+ <thead>
138
+ <tr class="bg-body border-b border-border">
139
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Member</th>
140
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Joined Date</th>
141
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest text-right">Lifetime Points</th>
142
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest text-right">Books Read</th>
143
+ </tr>
144
+ </thead>
145
+ <tbody class="divide-y divide-border">
146
+ @foreach (var user in _allUsers.OrderByDescending(u => u.CreatedAt))
147
+ {
148
+ var points = _loyalty.FirstOrDefault(l => l.UserId == user.Id)?.RawValue ?? 0;
149
+ var books = _activeReaders.FirstOrDefault(r => r.UserId == user.Id)?.RawValue ?? 0;
150
+
151
+ <tr class="hover:bg-body transition-colors">
152
+ <td class="px-6 py-5">
153
+ <div class="flex items-center gap-3">
154
+ <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xs border border-primary/20">
155
+ @(user.FullName?.Substring(0, Math.Min(2, user.FullName.Length)).ToUpper() ?? "U")
156
+ </div>
157
+ <p class="text-sm font-bold text-main">@user.FullName</p>
158
+ </div>
159
+ </td>
160
+ <td class="px-6 py-5 text-sm font-medium text-muted">@user.CreatedAt.ToString("MMM dd, yyyy")</td>
161
+ <td class="px-6 py-5 text-right font-black text-amber-500">@points.ToString("N0")</td>
162
+ <td class="px-6 py-5 text-right font-black text-blue-500">@books.ToString("N0")</td>
163
+ </tr>
164
+ }
165
+ </tbody>
166
+ </table>
167
+ </div>
168
+ </div>
169
+ }
170
  </div>
171
 
172
  @code {
173
+ private bool _isLoading = true;
174
+
175
+ private List<InsightItem> _subscribers = new();
176
+ private List<InsightItem> _loyalty = new();
177
+ private List<InsightItem> _activeReaders = new();
178
+
179
+ private List<UserDto> _allUsers = new();
180
+ private int _totalMembers = 0;
181
+ private int _activeSubscriptions = 0;
182
+
183
  protected override async Task OnInitializedAsync()
184
  {
185
+ await LoadInsightsAsync();
186
+ }
187
+
188
+ private async Task LoadInsightsAsync()
189
+ {
190
+ _isLoading = true;
191
+ try
192
+ {
193
+ var users = await ApiClient.GetUsersAsync();
194
+ _allUsers = users.ToList();
195
+ _totalMembers = _allUsers.Count;
196
+
197
+ var borrowings = await ApiClient.GetAllBorrowingsAsync();
198
+ var subscriptions = await ApiClient.GetAllSubscriptionsAsync();
199
+ var memberships = await ApiClient.GetMembershipsAsync();
200
+ var loyaltyHistory = await ApiClient.GetAllMembersPointsHistoryAsync();
201
+
202
+ _activeSubscriptions = subscriptions.Count(s => s.IsActive);
203
+
204
+ // 1. Top Subscribers (Total Spent)
205
+ var membershipPriceMap = memberships.ToDictionary(m => m.Id, m => m.Price);
206
+ _subscribers = subscriptions
207
+ .GroupBy(s => s.UserId)
208
+ .Select(g => {
209
+ var user = _allUsers.FirstOrDefault(u => u.Id == g.Key);
210
+ var totalSpent = g.Sum(s => membershipPriceMap.TryGetValue(s.MembershipId, out var price) ? price : 0);
211
+ return new InsightItem {
212
+ UserId = g.Key,
213
+ Name = user?.FullName ?? "Unknown User",
214
+ RawValue = (double)totalSpent,
215
+ ValueLabel = $"MMK {totalSpent:N0}"
216
+ };
217
+ })
218
+ .OrderByDescending(i => i.RawValue)
219
+ .ToList();
220
+
221
+ // 2. Top Loyalty (Earned Points only)
222
+ _loyalty = loyaltyHistory
223
+ .Select(lh => {
224
+ var user = _allUsers.FirstOrDefault(u => u.Id == lh.UserId);
225
+ var totalEarned = lh.History.Where(h => h.PointDelta > 0).Sum(h => h.PointDelta);
226
+ return new InsightItem {
227
+ UserId = lh.UserId,
228
+ Name = user?.FullName ?? lh.UserName,
229
+ RawValue = totalEarned,
230
+ ValueLabel = $"{totalEarned:N0} pts"
231
+ };
232
+ })
233
+ .OrderByDescending(i => i.RawValue)
234
+ .ToList();
235
+
236
+ // 3. Most Active (Approved Returns only)
237
+ _activeReaders = borrowings
238
+ .Where(b => b.Status == "Returned")
239
+ .GroupBy(b => b.UserId)
240
+ .Select(g => {
241
+ var user = _allUsers.FirstOrDefault(u => u.Id == g.Key);
242
+ var count = g.Count();
243
+ return new InsightItem {
244
+ UserId = g.Key,
245
+ Name = user?.FullName ?? "Unknown User",
246
+ RawValue = count,
247
+ ValueLabel = $"{count} books"
248
+ };
249
+ })
250
+ .OrderByDescending(i => i.RawValue)
251
+ .ToList();
252
+ }
253
+ catch (Exception ex)
254
+ {
255
+ Console.WriteLine($"Error loading insights: {ex.Message}");
256
+ }
257
+ finally
258
+ {
259
+ _isLoading = false;
260
+ }
261
+ }
262
+
263
+ private class InsightItem
264
+ {
265
+ public Guid UserId { get; set; }
266
+ public string Name { get; set; } = "";
267
+ public double RawValue { get; set; }
268
+ public string ValueLabel { get; set; } = "";
269
  }
270
  }
BlazorWebAssembly/Pages/Wishlist.razor CHANGED
@@ -2,7 +2,9 @@
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 -->
@@ -56,7 +58,7 @@
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>
@@ -66,14 +68,32 @@
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
  }
 
2
  @using LibraryManagement.Shared.Models
3
  @using BlazorWebAssembly.Services
4
  @inject LibraryApiClient ApiClient
5
+ @inject WishlistService WishlistService
6
  @inject NavigationManager Navigation
7
+ @inject IJSRuntime JS
8
 
9
  <div class="max-w-6xl mx-auto py-12 px-6">
10
  <!-- Header -->
 
58
  <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")'>
59
  @(book.AvailableCopies > 0 ? "Available" : "Waiting")
60
  </span>
61
+ <button @onclick="() => RemoveFromWishlistAsync(book.Id)" class="text-xs font-bold text-red-400 hover:text-red-500 transition-colors">Remove</button>
62
  </div>
63
  </div>
64
  </div>
 
68
  </div>
69
 
70
  @code {
71
+ private bool _isLoading = true;
72
  private List<BookDto> _wishlistItems = 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
+ _wishlistItems = await WishlistService.GetWishlistAsync();
85
+ }
86
+ catch { }
87
+ finally
88
+ {
89
+ _isLoading = false;
90
+ }
91
+ }
92
+
93
+ private async Task RemoveFromWishlistAsync(int bookId)
94
+ {
95
+ await WishlistService.RemoveFromWishlistAsync(bookId);
96
+ await LoadData();
97
+ await JS.InvokeVoidAsync("alert", "Removed from wishlist");
98
  }
99
  }
BlazorWebAssembly/Program.cs CHANGED
@@ -32,4 +32,7 @@ builder.Services.AddScoped<ThemeService>();
32
  // Register LibraryApiClient
33
  builder.Services.AddScoped<LibraryApiClient>();
34
 
 
 
 
35
  await builder.Build().RunAsync();
 
32
  // Register LibraryApiClient
33
  builder.Services.AddScoped<LibraryApiClient>();
34
 
35
+ // Register WishlistService
36
+ builder.Services.AddScoped<WishlistService>();
37
+
38
  await builder.Build().RunAsync();
BlazorWebAssembly/Providers/JwtAuthenticationStateProvider.cs CHANGED
@@ -26,9 +26,23 @@ namespace BlazorWebAssembly.Providers
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)
@@ -57,16 +71,20 @@ namespace BlazorWebAssembly.Providers
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
  }
 
26
  if (string.IsNullOrWhiteSpace(token))
27
  return _anonymous;
28
 
29
+ var claims = ParseClaimsFromJwt(token);
30
+
31
+ // Check expiration
32
+ var expClaim = claims.FirstOrDefault(c => c.Type == "exp");
33
+ if (expClaim != null && long.TryParse(expClaim.Value, out long expTime))
34
+ {
35
+ var expDateTime = DateTimeOffset.FromUnixTimeSeconds(expTime).UtcDateTime;
36
+ if (expDateTime <= DateTime.UtcNow)
37
+ {
38
+ await _localStorage.RemoveItemAsync("authToken");
39
+ return _anonymous;
40
+ }
41
+ }
42
+
43
  _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
44
 
45
+ return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt")));
46
  }
47
 
48
  public void NotifyUserAuthentication(string token)
 
71
  {
72
  foreach (var kvp in keyValuePairs)
73
  {
74
+ string claimType = kvp.Key;
75
+ if (claimType == "role" || claimType == "Role") claimType = ClaimTypes.Role;
76
+ else if (claimType == "unique_name" || claimType == "name") claimType = ClaimTypes.Name;
77
+
78
  if (kvp.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
79
  {
80
  foreach (var item in element.EnumerateArray())
81
  {
82
+ claims.Add(new Claim(claimType, item.ToString()));
83
  }
84
  }
85
  else
86
  {
87
+ claims.Add(new Claim(claimType, kvp.Value.ToString() ?? ""));
88
  }
89
  }
90
  }
BlazorWebAssembly/Services/LibraryApiClient.cs CHANGED
@@ -1,6 +1,6 @@
 
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
@@ -8,12 +8,30 @@ namespace BlazorWebAssembly.Services
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
  {
@@ -26,6 +44,13 @@ namespace BlazorWebAssembly.Services
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);
@@ -67,12 +92,6 @@ namespace BlazorWebAssembly.Services
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
@@ -80,6 +99,23 @@ namespace BlazorWebAssembly.Services
80
  {
81
  return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
82
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  #endregion
84
 
85
  #region Subscriptions
@@ -90,41 +126,273 @@ namespace BlazorWebAssembly.Services
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
  }
 
1
+ using System.Text.Json.Serialization;
2
  using System.Net.Http.Json;
3
  using System.Text.Json;
 
4
  using LibraryManagement.Shared.Models;
5
 
6
  namespace BlazorWebAssembly.Services
 
8
  public class LibraryApiClient
9
  {
10
  private readonly HttpClient _httpClient;
 
11
  public LibraryApiClient(HttpClient httpClient)
12
  {
13
  _httpClient = httpClient;
14
  }
15
 
16
+ #region Auth
17
+ public async Task<AuthResponse?> LoginAsync(LoginRequest request)
18
+ {
19
+ var response = await _httpClient.PostAsJsonAsync("api/auth/login", request);
20
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<AuthResponse>() : null;
21
+ }
22
+
23
+ public async Task<AuthResponse?> RegisterAsync(RegisterRequest request)
24
+ {
25
+ var response = await _httpClient.PostAsJsonAsync("api/auth/register", request);
26
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<AuthResponse>() : null;
27
+ }
28
+
29
+ public async Task<object?> GetProfileAsync()
30
+ {
31
+ return await _httpClient.GetFromJsonAsync<object>("api/auth/me");
32
+ }
33
+ #endregion
34
+
35
  #region Books
36
  public async Task<IEnumerable<BookDto>> GetBooksAsync(int? categoryId = null)
37
  {
 
44
  return await _httpClient.GetFromJsonAsync<BookDto>($"api/books/{id}");
45
  }
46
 
47
+ public async Task<List<BookDto>> GetBooksByIdsAsync(List<int> ids)
48
+ {
49
+ if (ids == null || !ids.Any()) return new List<BookDto>();
50
+ var idsStr = string.Join(",", ids);
51
+ return await _httpClient.GetFromJsonAsync<List<BookDto>>($"api/books/by-ids?ids={idsStr}") ?? new List<BookDto>();
52
+ }
53
+
54
  public async Task<BookDto?> CreateBookAsync(BookCreateRequest request)
55
  {
56
  var response = await _httpClient.PostAsJsonAsync("api/books", request);
 
92
  {
93
  return await _httpClient.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings") ?? Enumerable.Empty<BorrowingDto>();
94
  }
 
 
 
 
 
 
95
  #endregion
96
 
97
  #region Categories
 
99
  {
100
  return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
101
  }
102
+
103
+ public async Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync()
104
+ {
105
+ return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories/with-books") ?? Enumerable.Empty<CategoryDto>();
106
+ }
107
+
108
+ public async Task<CategoryDto?> CreateCategoryAsync(CategoryCreateRequest request)
109
+ {
110
+ var response = await _httpClient.PostAsJsonAsync("api/categories", request);
111
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<CategoryDto>() : null;
112
+ }
113
+
114
+ public async Task<bool> DeleteCategoryAsync(int id)
115
+ {
116
+ var response = await _httpClient.DeleteAsync($"api/categories/{id}");
117
+ return response.IsSuccessStatusCode;
118
+ }
119
  #endregion
120
 
121
  #region Subscriptions
 
126
 
127
  public async Task<SubscriptionDto?> GetMySubscriptionAsync()
128
  {
129
+ try
130
+ {
131
+ return await _httpClient.GetFromJsonAsync<SubscriptionDto>("api/subscriptions/me");
132
+ }
133
  catch { return null; }
134
  }
135
 
136
+ public async Task<IEnumerable<SubscriptionDto>> GetMyAllSubscriptionsAsync()
137
+ {
138
+ try
139
+ {
140
+ return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/me/all") ?? Enumerable.Empty<SubscriptionDto>();
141
+ }
142
+ catch { return Enumerable.Empty<SubscriptionDto>(); }
143
+ }
144
+
145
  public async Task<SubscriptionDto?> SubscribeAsync(SubscribeRequest request)
146
  {
147
  var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe", request);
148
  return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
149
  }
150
+
151
+ public async Task<SubscriptionDto?> GetUserSubscriptionAsync(Guid userId)
152
+ {
153
+ try
154
+ {
155
+ return await _httpClient.GetFromJsonAsync<SubscriptionDto>($"api/subscriptions/user/{userId}");
156
+ }
157
+ catch { return null; }
158
+ }
159
+
160
+ public async Task<SubscriptionDto?> AdminSubscribeAsync(AdminSubscribeRequest request)
161
+ {
162
+ var response = await _httpClient.PostAsJsonAsync("api/subscriptions/admin-subscribe", request);
163
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
164
+ }
165
+
166
+ public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
167
+ {
168
+ return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/all") ?? Enumerable.Empty<SubscriptionDto>();
169
+ }
170
  #endregion
171
 
 
172
  public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
173
  {
174
  try
175
  {
176
+ var response = await _httpClient.GetAsync("api/Loyalty/my-account");
177
+ if (response.IsSuccessStatusCode)
178
+ {
179
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
180
+ return await response.Content.ReadFromJsonAsync<LoyaltyAccountDto>(options);
181
+ }
182
+ return null;
183
+ }
184
+ catch
185
+ {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetMyRedemptionsAsync()
191
+ {
192
+ try
193
+ {
194
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>("api/Loyalty/my-redemptions") ?? Enumerable.Empty<LoyaltyRedemptionDto>();
195
+ }
196
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
197
+ }
198
+
199
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetMyPendingRedemptionsAsync()
200
+ {
201
+ try
202
+ {
203
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>("api/Loyalty/my-pending-redemptions") ?? Enumerable.Empty<LoyaltyRedemptionDto>();
204
+ }
205
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
206
+ }
207
+
208
+ public async Task<IEnumerable<PointHistoryEntryDto>> GetPointsHistoryAsync(string accountId)
209
+ {
210
+ try
211
+ {
212
+ return await _httpClient.GetFromJsonAsync<IEnumerable<PointHistoryEntryDto>>("api/Loyalty/my-points-history") ?? Enumerable.Empty<PointHistoryEntryDto>();
213
+ }
214
+ catch { return Enumerable.Empty<PointHistoryEntryDto>(); }
215
+ }
216
+
217
+ public async Task<IEnumerable<UserPointsHistoryDto>> GetAllMembersPointsHistoryAsync()
218
+ {
219
+ try {
220
  var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
221
+ var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history");
222
+ var raw = await response.Content.ReadAsStringAsync();
223
+ if (!response.IsSuccessStatusCode)
224
+ return Enumerable.Empty<UserPointsHistoryDto>();
225
+
226
+ var data = JsonSerializer.Deserialize<List<UserPointsHistoryDto>>(raw, options) ?? new List<UserPointsHistoryDto>();
227
+ foreach (var member in data)
228
+ {
229
+ member.History = member.History?.ToList() ?? new List<PointHistoryEntryDto>();
230
+ member.Redemptions = member.Redemptions?.ToList() ?? new List<LoyaltyRedemptionDto>();
231
+ }
232
+ return data;
233
+ } catch { return Enumerable.Empty<UserPointsHistoryDto>(); }
234
+ }
235
+
236
+ public async Task<bool> RequestReturnAsync(Guid borrowingId)
237
+ {
238
+ var response = await _httpClient.PostAsync($"api/borrowings/return-request/{borrowingId}", null);
239
+ return response.IsSuccessStatusCode;
240
+ }
241
+
242
+ public async Task<(bool Success, string Message)> ClaimRewardAsync(string rewardId, string? notes = null)
243
+ {
244
+ try
245
+ {
246
+ var request = new ClaimRewardRequestDto { RewardId = rewardId, Notes = notes };
247
+ var response = await _httpClient.PostAsJsonAsync("api/Loyalty/claim", request);
248
+
249
+ if (response.IsSuccessStatusCode)
250
+ {
251
+ var result = await response.Content.ReadFromJsonAsync<JsonDocument>();
252
+ string msg = "Reward claimed successfully!";
253
+ if (result != null && result.RootElement.TryGetProperty("message", out var msgElement))
254
+ {
255
+ msg = msgElement.GetString() ?? msg;
256
+ }
257
+ return (true, msg);
258
+ }
259
+ else
260
+ {
261
+ var error = await response.Content.ReadFromJsonAsync<JsonDocument>();
262
+ string msg = "Failed to claim reward.";
263
+ if (error != null && error.RootElement.TryGetProperty("message", out var msgElement))
264
+ {
265
+ msg = msgElement.GetString() ?? msg;
266
+ }
267
+ return (false, msg);
268
+ }
269
+ }
270
+ catch (Exception ex)
271
+ {
272
+ return (false, $"An error occurred: {ex.Message}");
273
  }
 
274
  }
275
 
276
  public async Task<IEnumerable<LoyaltyRewardDto>> GetActiveRewardsAsync()
277
  {
278
+ try
279
+ {
280
+ return await _httpClient.GetFromJsonAsync<IEnumerable<LoyaltyRewardDto>>("api/Loyalty/rewards") ?? Enumerable.Empty<LoyaltyRewardDto>();
281
+ }
282
+ catch { return Enumerable.Empty<LoyaltyRewardDto>(); }
283
+ }
284
+
285
+ #region Users
286
+ public async Task<IEnumerable<UserDto>> GetUsersAsync()
287
+ {
288
+ return await _httpClient.GetFromJsonAsync<IEnumerable<UserDto>>("api/users") ?? Enumerable.Empty<UserDto>();
289
  }
290
 
291
+ public async Task<UserDto?> GetUserAsync(Guid id)
292
  {
293
+ return await _httpClient.GetFromJsonAsync<UserDto>($"api/users/{id}");
294
+ }
295
+
296
+ public async Task<UserDto?> CreateUserAsync(UserCreateRequest request)
297
+ {
298
+ var response = await _httpClient.PostAsJsonAsync("api/users", request);
299
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<UserDto>() : null;
300
+ }
301
+
302
+ public async Task<UserDto?> UpdateUserAsync(Guid id, UserUpdateRequest request)
303
+ {
304
+ var response = await _httpClient.PutAsJsonAsync($"api/users/{id}", request);
305
+ return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<UserDto>() : null;
306
+ }
307
+
308
+ public async Task<bool> UpdateUserRoleAsync(Guid id, string role)
309
+ {
310
+ var response = await _httpClient.PutAsJsonAsync($"api/users/role/{id}", new UserRoleUpdateRequest { Role = role });
311
+ return response.IsSuccessStatusCode;
312
+ }
313
+
314
+ public async Task<bool> DeleteUserAsync(Guid id)
315
+ {
316
+ var response = await _httpClient.DeleteAsync($"api/users/{id}");
317
+ return response.IsSuccessStatusCode;
318
+ }
319
+ #endregion
320
+
321
+ public async Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync()
322
+ {
323
+ try
324
+ {
325
+ var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending");
326
+ if (response.IsSuccessStatusCode)
327
+ {
328
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };
329
+ return await response.Content.ReadFromJsonAsync<IEnumerable<LoyaltyRedemptionDto>>(options) ?? Enumerable.Empty<LoyaltyRedemptionDto>();
330
+ }
331
+ return Enumerable.Empty<LoyaltyRedemptionDto>();
332
+ }
333
+ catch { return Enumerable.Empty<LoyaltyRedemptionDto>(); }
334
+ }
335
+
336
+ public async Task<(bool Success, string Message)> FulfillRedemptionAsync(string id)
337
+ {
338
+ try
339
+ {
340
+ var response = await _httpClient.PostAsync($"api/Loyalty/admin/redemptions/{id}/fulfill", null);
341
+ if (response.IsSuccessStatusCode)
342
+ {
343
+ var result = await response.Content.ReadFromJsonAsync<JsonDocument>();
344
+ string msg = "Redemption fulfilled successfully!";
345
+ if (result != null && result.RootElement.TryGetProperty("message", out var msgElement))
346
+ {
347
+ msg = msgElement.GetString() ?? msg;
348
+ }
349
+ return (true, msg);
350
+ }
351
+ else
352
+ {
353
+ var error = await response.Content.ReadFromJsonAsync<JsonDocument>();
354
+ string msg = "Failed to fulfill redemption.";
355
+ if (error != null && error.RootElement.TryGetProperty("message", out var msgElement))
356
+ {
357
+ msg = msgElement.GetString() ?? msg;
358
+ }
359
+ return (false, msg);
360
+ }
361
+ }
362
+ catch (Exception ex) { return (false, $"Error: {ex.Message}"); }
363
+ }
364
+
365
+ #region Notifications
366
+ public async Task<IEnumerable<NotificationDto>> GetNotificationsAsync()
367
+ {
368
+ return await _httpClient.GetFromJsonAsync<IEnumerable<NotificationDto>>("api/notifications") ?? Enumerable.Empty<NotificationDto>();
369
+ }
370
+
371
+ public async Task<int> GetUnreadNotificationCountAsync()
372
+ {
373
+ try
374
+ {
375
+ return await _httpClient.GetFromJsonAsync<int>("api/notifications/unread-count");
376
+ }
377
+ catch { return 0; }
378
+ }
379
+
380
+ public async Task<bool> MarkNotificationsReadAsync()
381
+ {
382
+ var response = await _httpClient.PostAsJsonAsync("api/notifications/mark-read", new { });
383
+ return response.IsSuccessStatusCode;
384
+ }
385
+
386
+ public async Task<bool> UpdateFcmTokenAsync(string fcmToken)
387
+ {
388
+ var response = await _httpClient.PostAsJsonAsync("api/users/fcm-token", new { fcmToken });
389
+ return response.IsSuccessStatusCode;
390
+ }
391
+
392
+ public async Task<bool> SyncWishlistAsync(List<int> bookIds)
393
+ {
394
+ var response = await _httpClient.PostAsJsonAsync("api/wishlist/sync", bookIds);
395
+ return response.IsSuccessStatusCode;
396
  }
397
  #endregion
398
  }
BlazorWebAssembly/Services/WishlistService.cs ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using LibraryManagement.Shared.Models;
2
+ using Blazored.LocalStorage;
3
+ using System.Text.Json;
4
+
5
+ namespace BlazorWebAssembly.Services;
6
+
7
+ public class WishlistService
8
+ {
9
+ private readonly ILocalStorageService _localStorage;
10
+ private readonly LibraryApiClient _apiClient;
11
+ private const string WishlistKey = "user_wishlist";
12
+ private const string FavouriteKey = "user_favourites";
13
+
14
+ public WishlistService(ILocalStorageService localStorage, LibraryApiClient apiClient)
15
+ {
16
+ _localStorage = localStorage;
17
+ _apiClient = apiClient;
18
+ }
19
+
20
+ public async Task SyncCurrentWishlistAsync()
21
+ {
22
+ var list = await GetWishlistAsync();
23
+ await SyncWithServerAsync(list);
24
+ }
25
+
26
+ private async Task SyncWithServerAsync(List<BookDto> list)
27
+ {
28
+ try
29
+ {
30
+ var bookIds = list.Select(b => b.Id).ToList();
31
+ await _apiClient.SyncWishlistAsync(bookIds);
32
+ }
33
+ catch { }
34
+ }
35
+
36
+ public async Task<List<BookDto>> GetWishlistAsync()
37
+ {
38
+ try
39
+ {
40
+ var result = await _localStorage.GetItemAsStringAsync(WishlistKey);
41
+ if (!string.IsNullOrWhiteSpace(result))
42
+ {
43
+ var localList = JsonSerializer.Deserialize<List<BookDto>>(result) ?? new List<BookDto>();
44
+ if (!localList.Any()) return localList;
45
+
46
+ // Refresh from server
47
+ var latestList = await _apiClient.GetBooksByIdsAsync(localList.Select(b => b.Id).ToList());
48
+ if (latestList.Any())
49
+ {
50
+ // Update local storage with fresh data
51
+ await _localStorage.SetItemAsStringAsync(WishlistKey, JsonSerializer.Serialize(latestList));
52
+ return latestList;
53
+ }
54
+ return localList;
55
+ }
56
+ }
57
+ catch { }
58
+ return new List<BookDto>();
59
+ }
60
+
61
+ public async Task AddToWishlistAsync(BookDto book)
62
+ {
63
+ var list = await GetWishlistAsync();
64
+ if (!list.Any(b => b.Id == book.Id))
65
+ {
66
+ list.Add(book);
67
+ await _localStorage.SetItemAsStringAsync(WishlistKey, JsonSerializer.Serialize(list));
68
+ await SyncWithServerAsync(list);
69
+ }
70
+ }
71
+
72
+ public async Task RemoveFromWishlistAsync(int bookId)
73
+ {
74
+ var list = await GetWishlistAsync();
75
+ var item = list.FirstOrDefault(b => b.Id == bookId);
76
+ if (item != null)
77
+ {
78
+ list.Remove(item);
79
+ await _localStorage.SetItemAsStringAsync(WishlistKey, JsonSerializer.Serialize(list));
80
+ await SyncWithServerAsync(list);
81
+ }
82
+ }
83
+
84
+ public async Task<List<BookDto>> GetFavouritesAsync()
85
+ {
86
+ try
87
+ {
88
+ var result = await _localStorage.GetItemAsStringAsync(FavouriteKey);
89
+ if (!string.IsNullOrWhiteSpace(result))
90
+ {
91
+ var localList = JsonSerializer.Deserialize<List<BookDto>>(result) ?? new List<BookDto>();
92
+ if (!localList.Any()) return localList;
93
+
94
+ // Refresh from server
95
+ var latestList = await _apiClient.GetBooksByIdsAsync(localList.Select(b => b.Id).ToList());
96
+ if (latestList.Any())
97
+ {
98
+ // Update local storage with fresh data
99
+ await _localStorage.SetItemAsStringAsync(FavouriteKey, JsonSerializer.Serialize(latestList));
100
+ return latestList;
101
+ }
102
+ return localList;
103
+ }
104
+ }
105
+ catch { }
106
+ return new List<BookDto>();
107
+ }
108
+
109
+ public async Task AddToFavouritesAsync(BookDto book)
110
+ {
111
+ var list = await GetFavouritesAsync();
112
+ if (!list.Any(b => b.Id == book.Id))
113
+ {
114
+ list.Add(book);
115
+ await _localStorage.SetItemAsStringAsync(FavouriteKey, JsonSerializer.Serialize(list));
116
+ }
117
+ }
118
+
119
+ public async Task RemoveFromFavouriteAsync(int bookId)
120
+ {
121
+ var list = await GetFavouritesAsync();
122
+ var item = list.FirstOrDefault(b => b.Id == bookId);
123
+ if (item != null)
124
+ {
125
+ list.Remove(item);
126
+ await _localStorage.SetItemAsStringAsync(FavouriteKey, JsonSerializer.Serialize(list));
127
+ }
128
+ }
129
+ }
BlazorWebAssembly/wwwroot/firebase-init.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
2
+ import { getMessaging, getToken, onMessage } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-messaging.js";
3
+
4
+ const firebaseConfig = {
5
+ apiKey: "AIzaSyBodr7GJuCFdJfBYV8XBY77P9A4Wrxz_CA",
6
+ authDomain: "library-ths.firebaseapp.com",
7
+ projectId: "library-ths",
8
+ storageBucket: "library-ths.firebasestorage.app",
9
+ messagingSenderId: "222408896403",
10
+ appId: "1:222408896403:web:8533e26175c043e40a0266",
11
+ measurementId: "G-LVVGH9T1K3"
12
+ };
13
+
14
+ const app = initializeApp(firebaseConfig);
15
+ const messaging = getMessaging(app);
16
+
17
+ window.getFcmToken = async () => {
18
+ try {
19
+ const permission = await Notification.requestPermission();
20
+ if (permission === 'granted') {
21
+ // IMPORTANT: Replace 'YOUR_VAPID_KEY' with your actual key from
22
+ // Firebase Console -> Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates
23
+ console.log('Registering service worker...');
24
+ const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js');
25
+ console.log('Service worker registered successfully:', registration.scope);
26
+
27
+ const token = await getToken(messaging, {
28
+ vapidKey: 'BNgDM2Eq7TPwap0FhF-xv0CK6cS5EfshHOiC8z4916XnewFWYKdAfhwHctsgte5q8jQicQO8PEDVeymlXToasVQ',
29
+ serviceWorkerRegistration: registration
30
+ });
31
+ return token;
32
+ } else {
33
+ throw new Error('Permission not granted for notifications. Current state: ' + permission);
34
+ }
35
+ } catch (error) {
36
+ console.error('Error getting FCM token:', error);
37
+ throw new Error(error.message || error.toString());
38
+ }
39
+ };
40
+
41
+ onMessage(messaging, (payload) => {
42
+ console.log('Foreground Message received: ', payload);
43
+
44
+ const title = payload.notification.title;
45
+ const body = payload.notification.body;
46
+
47
+ if (window.showToast) {
48
+ window.showToast(title + ": " + body, 'success');
49
+ }
50
+ });
BlazorWebAssembly/wwwroot/firebase-messaging-sw.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Scripts for firebase and messaging
2
+ // Service worker တွေမှာ import အစား importScripts ကို သုံးပါတယ်
3
+ importScripts('https://www.gstatic.com/firebasejs/11.6.1/firebase-app-compat.js');
4
+ importScripts('https://www.gstatic.com/firebasejs/11.6.1/firebase-messaging-compat.js');
5
+
6
+ // --- Firebase Config ---
7
+ // Step 2 ကနေ ရခဲ့တဲ့ ကျွန်တော်တို့ရဲ့ Firebase Project Configuration ကို ဒီနေရာမှာ ထည့်ပေးပါ
8
+ const firebaseConfig = {
9
+ apiKey: "AIzaSyBodr7GJuCFdJfBYV8XBY77P9A4Wrxz_CA",
10
+ authDomain: "library-ths.firebaseapp.com",
11
+ projectId: "library-ths",
12
+ storageBucket: "library-ths.firebasestorage.app",
13
+ messagingSenderId: "222408896403",
14
+ appId: "1:222408896403:web:8533e26175c043e40a0266",
15
+ measurementId: "G-LVVGH9T1K3"
16
+ };
17
+
18
+ // Service worker မှာ Firebase app ကို initialize လုပ်ခြင်း
19
+ firebase.initializeApp(firebaseConfig);
20
+
21
+ // Background မှာ message တွေ လက်ခံနိုင်ဖို့ Firebase Messaging instance ကို ရယူခြင်း
22
+ const messaging = firebase.messaging();
23
+
24
+ // Background မှာရောက်လာတဲ့ message တွေကို ကိုင်တွယ်ဖြေရှင်းမယ့် အပိုင်း
25
+ messaging.onBackgroundMessage(function(payload) {
26
+ console.log('[firebase-messaging-sw.js] Received background message ', payload);
27
+
28
+ const title = payload.notification.title;
29
+ const body = payload.notification.body;
30
+
31
+ const notificationOptions = {
32
+ body: body,
33
+ icon: '/favicon.png'
34
+ };
35
+
36
+ self.registration.showNotification(title, notificationOptions);
37
+ });
BlazorWebAssembly/wwwroot/index.html CHANGED
@@ -63,6 +63,14 @@
63
  </div>
64
 
65
  <script src="_framework/blazor.webassembly.js"></script>
 
 
 
 
 
 
 
 
66
  </body>
67
 
68
  <style>
 
63
  </div>
64
 
65
  <script src="_framework/blazor.webassembly.js"></script>
66
+
67
+ <!-- Firebase Push Notifications -->
68
+ <script type="module" src="firebase-init.js"></script>
69
+ <script>
70
+ window.showToast = function (message, type) {
71
+ alert(message); // simple fallback, can be enhanced with UI toast
72
+ };
73
+ </script>
74
  </body>
75
 
76
  <style>