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

add: change the whole BlazorFrontend theme to Mystic Lavender Hue

Browse files
BlazorFrontend/Components/App.razor CHANGED
@@ -21,18 +21,31 @@
21
  sans: ['Inter', 'sans-serif'],
22
  },
23
  colors: {
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  primary: {
25
- 50: '#f8fafc',
26
- 100: '#f1f5f9',
27
- 200: '#e2e8f0',
28
- 300: '#cbd5e1',
29
- 400: '#94a3b8',
30
- 500: '#64748b',
31
- 600: '#475569',
32
- 700: '#334155',
33
- 800: '#1e293b',
34
- 900: '#0f172a',
35
- 950: '#020617',
36
  },
37
  },
38
  }
@@ -42,15 +55,16 @@
42
 
43
  <style type="text/tailwindcss">
44
  @@layer base {
45
- body { @@apply text-slate-900 antialiased h-full; }
 
46
  }
47
  @@layer components {
48
- .nav-link { @@apply flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors text-slate-600 hover:text-slate-900 hover:bg-slate-100; }
49
- .nav-link.active { @@apply bg-slate-900 text-white hover:bg-slate-900 hover:text-white; }
50
 
51
- .card { @@apply bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden; }
52
- .btn-primary { @@apply inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow transition-colors hover:bg-slate-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50; }
53
- .btn-secondary { @@apply inline-flex items-center justify-center rounded-md border border-slate-200 bg-white px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50; }
54
  }
55
  </style>
56
 
@@ -70,7 +84,7 @@
70
  const container = document.getElementById('toast-container');
71
  const toast = document.createElement('div');
72
 
73
- const bgColor = type === 'success' ? 'bg-slate-900' : 'bg-red-600';
74
  const icon = type === 'success'
75
  ? '<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>'
76
  : '<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
 
21
  sans: ['Inter', 'sans-serif'],
22
  },
23
  colors: {
24
+ mystic: {
25
+ 50: '#fcfbff',
26
+ 100: '#e3dfff', // Main Soft Lavender
27
+ 200: '#d3c0cd', // Muted Rose
28
+ 300: '#c1acb9',
29
+ 400: '#b19994', // Warm Taupe
30
+ 500: '#a3877d',
31
+ 600: '#937666', // Muted Brown
32
+ 700: '#7a5f51',
33
+ 800: '#5a4b5d',
34
+ 900: '#3d3a4b', // Dark Slate Purple
35
+ 950: '#2a2736',
36
+ },
37
  primary: {
38
+ 50: '#f4f4f6',
39
+ 100: '#e9e9ed',
40
+ 200: '#d3d2db',
41
+ 300: '#bdbcc9',
42
+ 400: '#908fa7',
43
+ 500: '#646285',
44
+ 600: '#5a5878',
45
+ 700: '#4b4a64',
46
+ 800: '#3d3a4b', // Using mystic-900 as primary base
47
+ 900: '#32303d',
48
+ 950: '#2a2736',
49
  },
50
  },
51
  }
 
55
 
56
  <style type="text/tailwindcss">
57
  @@layer base {
58
+ body { @@apply text-mystic-900 antialiased h-full bg-mystic-100/30; }
59
+ html { @@apply bg-mystic-100/30; }
60
  }
61
  @@layer components {
62
+ .nav-link { @@apply flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors text-mystic-600 hover:text-mystic-900 hover:bg-mystic-200/50; }
63
+ .nav-link.active { @@apply bg-mystic-900 text-white hover:bg-mystic-900 hover:text-white; }
64
 
65
+ .card { @@apply bg-white border border-mystic-200 rounded-xl shadow-sm overflow-hidden; }
66
+ .btn-primary { @@apply inline-flex items-center justify-center rounded-md bg-mystic-900 px-4 py-2 text-sm font-medium text-white shadow transition-colors hover:bg-mystic-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-mystic-950 disabled:pointer-events-none disabled:opacity-50; }
67
+ .btn-secondary { @@apply inline-flex items-center justify-center rounded-md border border-mystic-200 bg-white px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-mystic-100 hover:text-mystic-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-mystic-950 disabled:pointer-events-none disabled:opacity-50; }
68
  }
69
  </style>
70
 
 
84
  const container = document.getElementById('toast-container');
85
  const toast = document.createElement('div');
86
 
87
+ const bgColor = type === 'success' ? 'bg-mystic-900' : 'bg-red-600';
88
  const icon = type === 'success'
89
  ? '<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>'
90
  : '<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
BlazorFrontend/Components/Layout/MainLayout.razor CHANGED
@@ -6,9 +6,9 @@
6
  <div class="flex h-full overflow-hidden">
7
  <!-- Sidebar -->
8
  <aside class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0 z-50">
9
- <div class="flex flex-col flex-grow border-r border-slate-200 bg-white pt-5 pb-4 overflow-y-auto">
10
  <div class="flex items-center flex-shrink-0 px-4 gap-2">
11
- <div class="bg-slate-900 rounded-lg p-1.5 text-white">
12
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
13
  </div>
14
  <span class="text-lg font-bold tracking-tight">Library OS</span>
@@ -71,27 +71,27 @@
71
  </AuthorizeView>
72
  </nav>
73
  </div>
74
- <div class="px-4 py-4 border-t border-slate-100">
75
  <AuthorizeView>
76
  <Authorized Context="authContext">
77
  <div class="flex items-center gap-3">
78
- <div class="h-8 w-8 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-xs uppercase">
79
  @(authContext.User.Identity?.Name?.Substring(0, 2) ?? "US")
80
  </div>
81
  <div class="flex flex-col">
82
  <span class="text-sm font-medium">@authContext.User.Identity?.Name</span>
83
- <span class="text-xs text-slate-500">@authContext.User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Role || c.Type == "role")?.Value</span>
84
  </div>
85
  </div>
86
  </Authorized>
87
  <NotAuthorized>
88
  <div class="flex items-center gap-3">
89
- <div class="h-8 w-8 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-xs uppercase">
90
  GU
91
  </div>
92
  <div class="flex flex-col">
93
  <span class="text-sm font-medium">Guest User</span>
94
- <span class="text-xs text-slate-500">Sign in to borrow</span>
95
  </div>
96
  </div>
97
  </NotAuthorized>
@@ -103,23 +103,23 @@
103
  <!-- Main Content Wrapper -->
104
  <div class="md:pl-64 flex flex-col flex-1 w-0">
105
  <!-- Topbar -->
106
- <header class="flex-shrink-0 flex h-16 bg-white border-b border-slate-200">
107
  <div class="flex-1 px-4 flex justify-between">
108
  <div class="flex-1 flex items-center">
109
  <div class="w-full max-w-sm">
110
  <label for="search" class="sr-only">Search</label>
111
  <div class="relative">
112
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
113
- <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
114
  <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
115
  </svg>
116
  </div>
117
- <input id="search" name="search" class="block w-full pl-10 pr-3 py-2 border border-slate-200 rounded-md leading-5 bg-slate-50 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-900 focus:border-slate-900 sm:text-sm transition-all" placeholder="Search books, authors, ISBN..." type="search">
118
  </div>
119
  </div>
120
  </div>
121
  <div class="ml-4 flex items-center md:ml-6 gap-4">
122
- <button type="button" class="bg-white p-1 rounded-full text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900">
123
  <span class="sr-only">View notifications</span>
124
  <svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
125
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
@@ -147,7 +147,7 @@
147
  else
148
  {
149
  <div class="flex items-center justify-center h-64">
150
- <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900"></div>
151
  </div>
152
  }
153
  </div>
 
6
  <div class="flex h-full overflow-hidden">
7
  <!-- Sidebar -->
8
  <aside class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0 z-50">
9
+ <div class="flex flex-col flex-grow border-r border-mystic-200 bg-white pt-5 pb-4 overflow-y-auto">
10
  <div class="flex items-center flex-shrink-0 px-4 gap-2">
11
+ <div class="bg-mystic-900 rounded-lg p-1.5 text-white">
12
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
13
  </div>
14
  <span class="text-lg font-bold tracking-tight">Library OS</span>
 
71
  </AuthorizeView>
72
  </nav>
73
  </div>
74
+ <div class="px-4 py-4 border-t border-mystic-100">
75
  <AuthorizeView>
76
  <Authorized Context="authContext">
77
  <div class="flex items-center gap-3">
78
+ <div class="h-8 w-8 rounded-full bg-mystic-900 flex items-center justify-center text-white font-bold text-xs uppercase">
79
  @(authContext.User.Identity?.Name?.Substring(0, 2) ?? "US")
80
  </div>
81
  <div class="flex flex-col">
82
  <span class="text-sm font-medium">@authContext.User.Identity?.Name</span>
83
+ <span class="text-xs text-mystic-500">@authContext.User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Role || c.Type == "role")?.Value</span>
84
  </div>
85
  </div>
86
  </Authorized>
87
  <NotAuthorized>
88
  <div class="flex items-center gap-3">
89
+ <div class="h-8 w-8 rounded-full bg-mystic-900 flex items-center justify-center text-white font-bold text-xs uppercase">
90
  GU
91
  </div>
92
  <div class="flex flex-col">
93
  <span class="text-sm font-medium">Guest User</span>
94
+ <span class="text-xs text-mystic-500">Sign in to borrow</span>
95
  </div>
96
  </div>
97
  </NotAuthorized>
 
103
  <!-- Main Content Wrapper -->
104
  <div class="md:pl-64 flex flex-col flex-1 w-0">
105
  <!-- Topbar -->
106
+ <header class="flex-shrink-0 flex h-16 bg-white border-b border-mystic-200">
107
  <div class="flex-1 px-4 flex justify-between">
108
  <div class="flex-1 flex items-center">
109
  <div class="w-full max-w-sm">
110
  <label for="search" class="sr-only">Search</label>
111
  <div class="relative">
112
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
113
+ <svg class="h-4 w-4 text-mystic-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
114
  <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
115
  </svg>
116
  </div>
117
+ <input id="search" name="search" class="block w-full pl-10 pr-3 py-2 border border-mystic-200 rounded-md leading-5 bg-mystic-50 placeholder-mystic-400 focus:outline-none focus:ring-1 focus:ring-mystic-900 focus:border-mystic-900 sm:text-sm transition-all" placeholder="Search books, authors, ISBN..." type="search">
118
  </div>
119
  </div>
120
  </div>
121
  <div class="ml-4 flex items-center md:ml-6 gap-4">
122
+ <button type="button" class="bg-white p-1 rounded-full text-mystic-400 hover:text-mystic-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mystic-900">
123
  <span class="sr-only">View notifications</span>
124
  <svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
125
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
 
147
  else
148
  {
149
  <div class="flex items-center justify-center h-64">
150
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mystic-900"></div>
151
  </div>
152
  }
153
  </div>
BlazorFrontend/Components/Pages/Books.razor CHANGED
@@ -12,10 +12,10 @@
12
  {
13
  <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
14
  <div>
15
- <h1 class="text-3xl font-bold tracking-tight text-slate-900">Books Catalog</h1>
16
- <p class="mt-2 text-slate-500">
17
  Explore and discover your next great read.
18
- <span class="ml-2 text-xs text-slate-400">(@FilteredBooks.Count() total)</span>
19
  </p>
20
  </div>
21
  <AuthorizeView Roles="Librarian">
@@ -32,12 +32,12 @@
32
  <div class="md:col-span-3">
33
  <div class="relative">
34
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
35
- <svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
36
  <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
37
  </svg>
38
  </div>
39
  <input type="text" @bind="_searchQuery" @bind:event="oninput"
40
- class="block w-full pl-10 pr-3 py-2 border border-slate-200 rounded-lg bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent sm:text-sm shadow-sm"
41
  placeholder="Search by title, author or ISBN...">
42
  </div>
43
  </div>
@@ -46,15 +46,15 @@
46
  @if (_categories.Any())
47
  {
48
  <div class="flex flex-wrap items-center gap-2 mb-8 animate-in fade-in duration-700">
49
- <span class="text-xs font-semibold text-slate-400 uppercase tracking-wider mr-1">Filter:</span>
50
  <button @onclick="() => FilterByCategory(null)"
51
- class='inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(_selectedCategoryId == null ? "bg-slate-900 text-white border-slate-900" : "bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:text-slate-900")'>
52
  All
53
  </button>
54
  @foreach (var cat in _categories)
55
  {
56
  <button @onclick="() => FilterByCategory(cat.Id)"
57
- class='inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(_selectedCategoryId == cat.Id ? "bg-slate-900 text-white border-slate-900" : "bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:text-slate-900")'>
58
  @cat.Name
59
  </button>
60
  }
@@ -64,14 +64,14 @@
64
  @if (_isLoading)
65
  {
66
  <div class="py-20 flex justify-center">
67
- <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-slate-900"></div>
68
  </div>
69
  }
70
  else if (!FilteredBooks.Any())
71
  {
72
- <div class="py-20 text-center bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200 animate-in fade-in duration-500">
73
- <svg class="w-16 h-16 mx-auto text-slate-300 mb-4" 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>
74
- <p class="text-slate-500 font-medium">No books found matching your criteria.</p>
75
  </div>
76
  }
77
  else
@@ -80,7 +80,7 @@
80
  @foreach (var book in FilteredBooks)
81
  {
82
  <div class="group card hover:shadow-lg transition-all duration-300 flex flex-col h-full">
83
- <div @onclick="() => ShowDetails(book.Id)" class="relative aspect-[2/3] w-full bg-slate-100 flex items-center justify-center text-slate-300 overflow-hidden text-center cursor-pointer group-hover:scale-105 transition-transform duration-500">
84
  @if (!string.IsNullOrWhiteSpace(book.CoverUrl))
85
  {
86
  <img src="@book.CoverUrl" alt="@book.Title" class="absolute inset-0 w-full h-full object-cover" />
@@ -89,24 +89,24 @@
89
  {
90
  <div class="flex flex-col items-center gap-4 p-4">
91
  <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" 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>
92
- <span class="text-[10px] font-bold text-slate-400">@book.Isbn</span>
93
  </div>
94
  }
95
 
96
  @if (book.AvailableCopies <= 0)
97
  {
98
- <div class="absolute inset-0 bg-slate-900/60 backdrop-blur-[2px] flex items-center justify-center">
99
- <span class="bg-white text-slate-900 text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded">Out of Stock</span>
100
  </div>
101
  }
102
  </div>
103
  <div class="p-4 flex-grow flex flex-col">
104
  <div class="flex items-start justify-between gap-2">
105
  <div class="min-w-0">
106
- <h3 @onclick="() => ShowDetails(book.Id)" class="text-sm font-bold text-slate-900 truncate group-hover:text-amber-600 transition-colors cursor-pointer">
107
  @book.Title
108
  </h3>
109
- <p class="text-xs text-slate-500 mt-0.5 truncate">@book.Author</p>
110
  </div>
111
  </div>
112
  @if (book.Categories.Any())
@@ -121,7 +121,7 @@
121
  </div>
122
  }
123
  <div class="mt-3 flex items-center justify-between">
124
- <span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">@book.Status</span>
125
  <span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
126
  @book.AvailableCopies / @book.TotalCopies
127
  </span>
@@ -129,7 +129,7 @@
129
  <div class="mt-auto pt-4 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
130
  <button @onclick="() => ShowDetails(book.Id)" class="flex-1 btn-secondary py-1 text-[11px]">Details</button>
131
  <AuthorizeView Roles="Librarian">
132
- <button @onclick="() => ShowEdit(book.Id)" class="p-1.5 rounded-md border border-slate-200 hover:bg-slate-50 text-slate-600">
133
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
134
  </button>
135
  </AuthorizeView>
@@ -143,14 +143,14 @@
143
  else if (_viewMode == ViewMode.Details && _selectedBook != null)
144
  {
145
  <div class="mb-8 animate-in fade-in slide-in-from-left-4 duration-500">
146
- <button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
147
  <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>
148
  Back to Catalog
149
  </button>
150
 
151
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
152
  <div class="lg:col-span-1">
153
- <div class="aspect-[2/3] w-full rounded-2xl bg-slate-100 border border-slate-200 shadow-sm flex items-center justify-center text-slate-300 overflow-hidden">
154
  @if (!string.IsNullOrWhiteSpace(_selectedBook.CoverUrl))
155
  {
156
  <img src="@_selectedBook.CoverUrl" alt="@_selectedBook.Title" class="w-full h-full object-cover" />
@@ -164,25 +164,25 @@ else if (_viewMode == ViewMode.Details && _selectedBook != null)
164
 
165
  <div class="lg:col-span-2 space-y-8">
166
  <div>
167
- <span class="inline-flex items-center rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600 mb-4 tracking-wide uppercase">@_selectedBook.Status</span>
168
- <h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">@_selectedBook.Title</h1>
169
- <p class="text-xl text-slate-500 mt-2 font-medium">by @_selectedBook.Author</p>
170
  </div>
171
 
172
- <div class="prose prose-slate max-w-none">
173
- <h3 class="text-lg font-bold text-slate-900 border-b border-slate-100 pb-2 mb-4 uppercase tracking-wider text-sm">Description</h3>
174
- <p class="text-slate-600 leading-relaxed">
175
  @(string.IsNullOrEmpty(_selectedBook.Description) ? "No description available for this book." : _selectedBook.Description)
176
  </p>
177
  </div>
178
 
179
- <div class="grid grid-cols-2 gap-8 border-y border-slate-100 py-6">
180
  <div>
181
- <h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">ISBN</h4>
182
- <p class="text-sm font-semibold text-slate-900">@_selectedBook.Isbn</p>
183
  </div>
184
  <div>
185
- <h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Availability</h4>
186
  <p class="text-sm font-semibold @(_selectedBook.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
187
  @(_selectedBook.AvailableCopies > 0 ? $"{_selectedBook.AvailableCopies} / {_selectedBook.TotalCopies} units in stock" : "Currently occupied")
188
  </p>
@@ -193,7 +193,7 @@ else if (_viewMode == ViewMode.Details && _selectedBook != null)
193
  <AuthorizeView Roles="Member">
194
  @if (_selectedBook.AvailableCopies > 0)
195
  {
196
- <button @onclick="() => BorrowAsync(_selectedBook.Id)" class="btn-primary py-3 px-8 text-base shadow-lg shadow-slate-200">Borrow This Book</button>
197
  }
198
  else
199
  {
@@ -221,15 +221,15 @@ else if (_viewMode == ViewMode.Details && _selectedBook != null)
221
  else if (_viewMode == ViewMode.Create || _viewMode == ViewMode.Edit)
222
  {
223
  <div class="mb-8 animate-in fade-in slide-in-from-right-4 duration-500">
224
- <button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
225
  <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>
226
  Back to Catalog
227
  </button>
228
 
229
  <div class="max-w-3xl mx-auto">
230
  <div class="mb-8">
231
- <h1 class="text-3xl font-bold tracking-tight text-slate-900">@(_viewMode == ViewMode.Create ? "Add New Book" : "Edit Book")</h1>
232
- <p class="mt-2 text-slate-500">Fill in the details below to @(_viewMode == ViewMode.Create ? "add a new book to" : "update the") library catalog.</p>
233
  </div>
234
 
235
  <div class="card p-6 sm:p-8">
@@ -237,46 +237,46 @@ else if (_viewMode == ViewMode.Create || _viewMode == ViewMode.Edit)
237
  <DataAnnotationsValidator />
238
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
239
  <div class="sm:col-span-2">
240
- <label class="block text-sm font-medium text-slate-700">Book Title <span class="text-rose-500">*</span></label>
241
- <InputText @bind-Value="_formModel.Title" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. The Great Gatsby" required />
242
  </div>
243
 
244
  <div>
245
- <label class="block text-sm font-medium text-slate-700">Author <span class="text-rose-500">*</span></label>
246
- <InputText @bind-Value="_formModel.Author" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. F. Scott Fitzgerald" required />
247
  </div>
248
 
249
  <div>
250
- <label class="block text-sm font-medium text-slate-700">ISBN <span class="text-rose-500">*</span></label>
251
- <InputText @bind-Value="_formModel.Isbn" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. 978-0743273565" required />
252
  </div>
253
 
254
  <div>
255
- <label class="block text-sm font-medium text-slate-700">Total Copies <span class="text-rose-500">*</span></label>
256
- <InputNumber @bind-Value="_formModel.TotalCopies" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="1" required />
257
  </div>
258
 
259
  <div>
260
- <label class="block text-sm font-medium text-slate-700">Cover Image URL</label>
261
- <InputText @bind-Value="_formModel.CoverUrl" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="https://example.com/cover.jpg" />
262
  </div>
263
 
264
  <div class="hidden sm:block"></div>
265
 
266
  <div class="sm:col-span-2">
267
- <label class="block text-sm font-medium text-slate-700">Description</label>
268
- <InputTextArea @bind-Value="_formModel.Description" rows="4" class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Brief summary of the book..."></InputTextArea>
269
  </div>
270
 
271
  <div class="sm:col-span-2">
272
- <label class="block text-sm font-medium text-slate-700 mb-2">Categories</label>
273
  <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
274
  @foreach (var cat in _categories)
275
  {
276
- <label class="flex items-center gap-2 px-3 py-2 rounded-md border border-slate-200 bg-slate-50 hover:bg-slate-100 cursor-pointer text-sm text-slate-700 transition-colors">
277
  <input type="checkbox" checked="@(_formModel.CategoryIds.Contains(cat.Id))"
278
  @onchange='(e) => ToggleCategory(cat.Id, (bool)e.Value!)'
279
- class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900" />
280
  @cat.Name
281
  </label>
282
  }
@@ -284,7 +284,7 @@ else if (_viewMode == ViewMode.Create || _viewMode == ViewMode.Edit)
284
  </div>
285
  </div>
286
 
287
- <div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
288
  <button type="button" @onclick="ShowList" class="btn-secondary">Cancel</button>
289
  <button type="submit" class="btn-primary" disabled="@_isSubmitting">
290
  @(_isSubmitting ? "Saving..." : (_viewMode == ViewMode.Create ? "Add Book" : "Update Book"))
 
12
  {
13
  <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
14
  <div>
15
+ <h1 class="text-3xl font-bold tracking-tight text-mystic-900">Books Catalog</h1>
16
+ <p class="mt-2 text-mystic-500">
17
  Explore and discover your next great read.
18
+ <span class="ml-2 text-xs text-mystic-400">(@FilteredBooks.Count() total)</span>
19
  </p>
20
  </div>
21
  <AuthorizeView Roles="Librarian">
 
32
  <div class="md:col-span-3">
33
  <div class="relative">
34
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
35
+ <svg class="h-4 w-4 text-mystic-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
36
  <path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
37
  </svg>
38
  </div>
39
  <input type="text" @bind="_searchQuery" @bind:event="oninput"
40
+ class="block w-full pl-10 pr-3 py-2 border border-mystic-200 rounded-lg bg-white placeholder-mystic-400 focus:outline-none focus:ring-2 focus:ring-mystic-900 focus:border-transparent sm:text-sm shadow-sm"
41
  placeholder="Search by title, author or ISBN...">
42
  </div>
43
  </div>
 
46
  @if (_categories.Any())
47
  {
48
  <div class="flex flex-wrap items-center gap-2 mb-8 animate-in fade-in duration-700">
49
+ <span class="text-xs font-semibold text-mystic-400 uppercase tracking-wider mr-1">Filter:</span>
50
  <button @onclick="() => FilterByCategory(null)"
51
+ class='inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(_selectedCategoryId == null ? "bg-mystic-900 text-white border-mystic-900" : "bg-white text-mystic-600 border-mystic-200 hover:border-mystic-400 hover:text-mystic-900")'>
52
  All
53
  </button>
54
  @foreach (var cat in _categories)
55
  {
56
  <button @onclick="() => FilterByCategory(cat.Id)"
57
+ class='inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(_selectedCategoryId == cat.Id ? "bg-mystic-900 text-white border-mystic-900" : "bg-white text-mystic-600 border-mystic-200 hover:border-mystic-400 hover:text-mystic-900")'>
58
  @cat.Name
59
  </button>
60
  }
 
64
  @if (_isLoading)
65
  {
66
  <div class="py-20 flex justify-center">
67
+ <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
68
  </div>
69
  }
70
  else if (!FilteredBooks.Any())
71
  {
72
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200 animate-in fade-in duration-500">
73
+ <svg class="w-16 h-16 mx-auto text-mystic-300 mb-4" 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>
74
+ <p class="text-mystic-500 font-medium">No books found matching your criteria.</p>
75
  </div>
76
  }
77
  else
 
80
  @foreach (var book in FilteredBooks)
81
  {
82
  <div class="group card hover:shadow-lg transition-all duration-300 flex flex-col h-full">
83
+ <div @onclick="() => ShowDetails(book.Id)" class="relative aspect-[2/3] w-full bg-mystic-100 flex items-center justify-center text-mystic-300 overflow-hidden text-center cursor-pointer group-hover:scale-105 transition-transform duration-500">
84
  @if (!string.IsNullOrWhiteSpace(book.CoverUrl))
85
  {
86
  <img src="@book.CoverUrl" alt="@book.Title" class="absolute inset-0 w-full h-full object-cover" />
 
89
  {
90
  <div class="flex flex-col items-center gap-4 p-4">
91
  <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" 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>
92
+ <span class="text-[10px] font-bold text-mystic-400">@book.Isbn</span>
93
  </div>
94
  }
95
 
96
  @if (book.AvailableCopies <= 0)
97
  {
98
+ <div class="absolute inset-0 bg-mystic-900/60 backdrop-blur-[2px] flex items-center justify-center">
99
+ <span class="bg-white text-mystic-900 text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded">Out of Stock</span>
100
  </div>
101
  }
102
  </div>
103
  <div class="p-4 flex-grow flex flex-col">
104
  <div class="flex items-start justify-between gap-2">
105
  <div class="min-w-0">
106
+ <h3 @onclick="() => ShowDetails(book.Id)" class="text-sm font-bold text-mystic-900 truncate group-hover:text-mystic-600 transition-colors cursor-pointer">
107
  @book.Title
108
  </h3>
109
+ <p class="text-xs text-mystic-500 mt-0.5 truncate">@book.Author</p>
110
  </div>
111
  </div>
112
  @if (book.Categories.Any())
 
121
  </div>
122
  }
123
  <div class="mt-3 flex items-center justify-between">
124
+ <span class="inline-flex items-center rounded-full bg-mystic-100 px-2 py-0.5 text-[10px] font-medium text-mystic-600">@book.Status</span>
125
  <span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
126
  @book.AvailableCopies / @book.TotalCopies
127
  </span>
 
129
  <div class="mt-auto pt-4 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
130
  <button @onclick="() => ShowDetails(book.Id)" class="flex-1 btn-secondary py-1 text-[11px]">Details</button>
131
  <AuthorizeView Roles="Librarian">
132
+ <button @onclick="() => ShowEdit(book.Id)" class="p-1.5 rounded-md border border-mystic-200 hover:bg-mystic-50 text-mystic-600">
133
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
134
  </button>
135
  </AuthorizeView>
 
143
  else if (_viewMode == ViewMode.Details && _selectedBook != null)
144
  {
145
  <div class="mb-8 animate-in fade-in slide-in-from-left-4 duration-500">
146
+ <button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-mystic-500 hover:text-mystic-900 mb-6 group">
147
  <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>
148
  Back to Catalog
149
  </button>
150
 
151
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
152
  <div class="lg:col-span-1">
153
+ <div class="aspect-[2/3] w-full rounded-2xl bg-mystic-100 border border-mystic-200 shadow-sm flex items-center justify-center text-mystic-300 overflow-hidden">
154
  @if (!string.IsNullOrWhiteSpace(_selectedBook.CoverUrl))
155
  {
156
  <img src="@_selectedBook.CoverUrl" alt="@_selectedBook.Title" class="w-full h-full object-cover" />
 
164
 
165
  <div class="lg:col-span-2 space-y-8">
166
  <div>
167
+ <span class="inline-flex items-center rounded-full bg-mystic-100 px-3 py-1 text-xs font-semibold text-mystic-600 mb-4 tracking-wide uppercase">@_selectedBook.Status</span>
168
+ <h1 class="text-4xl font-extrabold text-mystic-900 tracking-tight">@_selectedBook.Title</h1>
169
+ <p class="text-xl text-mystic-500 mt-2 font-medium">by @_selectedBook.Author</p>
170
  </div>
171
 
172
+ <div class="prose prose-mystic max-w-none">
173
+ <h3 class="text-lg font-bold text-mystic-900 border-b border-mystic-100 pb-2 mb-4 uppercase tracking-wider text-sm">Description</h3>
174
+ <p class="text-mystic-600 leading-relaxed">
175
  @(string.IsNullOrEmpty(_selectedBook.Description) ? "No description available for this book." : _selectedBook.Description)
176
  </p>
177
  </div>
178
 
179
+ <div class="grid grid-cols-2 gap-8 border-y border-mystic-100 py-6">
180
  <div>
181
+ <h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">ISBN</h4>
182
+ <p class="text-sm font-semibold text-mystic-900">@_selectedBook.Isbn</p>
183
  </div>
184
  <div>
185
+ <h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Availability</h4>
186
  <p class="text-sm font-semibold @(_selectedBook.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
187
  @(_selectedBook.AvailableCopies > 0 ? $"{_selectedBook.AvailableCopies} / {_selectedBook.TotalCopies} units in stock" : "Currently occupied")
188
  </p>
 
193
  <AuthorizeView Roles="Member">
194
  @if (_selectedBook.AvailableCopies > 0)
195
  {
196
+ <button @onclick="() => BorrowAsync(_selectedBook.Id)" class="btn-primary py-3 px-8 text-base shadow-lg shadow-mystic-200">Borrow This Book</button>
197
  }
198
  else
199
  {
 
221
  else if (_viewMode == ViewMode.Create || _viewMode == ViewMode.Edit)
222
  {
223
  <div class="mb-8 animate-in fade-in slide-in-from-right-4 duration-500">
224
+ <button @onclick="ShowList" class="inline-flex items-center text-sm font-medium text-mystic-500 hover:text-mystic-900 mb-6 group">
225
  <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>
226
  Back to Catalog
227
  </button>
228
 
229
  <div class="max-w-3xl mx-auto">
230
  <div class="mb-8">
231
+ <h1 class="text-3xl font-bold tracking-tight text-mystic-900">@(_viewMode == ViewMode.Create ? "Add New Book" : "Edit Book")</h1>
232
+ <p class="mt-2 text-mystic-500">Fill in the details below to @(_viewMode == ViewMode.Create ? "add a new book to" : "update the") library catalog.</p>
233
  </div>
234
 
235
  <div class="card p-6 sm:p-8">
 
237
  <DataAnnotationsValidator />
238
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
239
  <div class="sm:col-span-2">
240
+ <label class="block text-sm font-medium text-mystic-700">Book Title <span class="text-rose-500">*</span></label>
241
+ <InputText @bind-Value="_formModel.Title" class="mt-1 block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. The Great Gatsby" required />
242
  </div>
243
 
244
  <div>
245
+ <label class="block text-sm font-medium text-mystic-700">Author <span class="text-rose-500">*</span></label>
246
+ <InputText @bind-Value="_formModel.Author" class="mt-1 block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. F. Scott Fitzgerald" required />
247
  </div>
248
 
249
  <div>
250
+ <label class="block text-sm font-medium text-mystic-700">ISBN <span class="text-rose-500">*</span></label>
251
+ <InputText @bind-Value="_formModel.Isbn" class="mt-1 block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. 978-0743273565" required />
252
  </div>
253
 
254
  <div>
255
+ <label class="block text-sm font-medium text-mystic-700">Total Copies <span class="text-rose-500">*</span></label>
256
+ <InputNumber @bind-Value="_formModel.TotalCopies" class="mt-1 block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="1" required />
257
  </div>
258
 
259
  <div>
260
+ <label class="block text-sm font-medium text-mystic-700">Cover Image URL</label>
261
+ <InputText @bind-Value="_formModel.CoverUrl" class="mt-1 block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="https://example.com/cover.jpg" />
262
  </div>
263
 
264
  <div class="hidden sm:block"></div>
265
 
266
  <div class="sm:col-span-2">
267
+ <label class="block text-sm font-medium text-mystic-700">Description</label>
268
+ <InputTextArea @bind-Value="_formModel.Description" rows="4" class="mt-1 block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Brief summary of the book..."></InputTextArea>
269
  </div>
270
 
271
  <div class="sm:col-span-2">
272
+ <label class="block text-sm font-medium text-mystic-700 mb-2">Categories</label>
273
  <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
274
  @foreach (var cat in _categories)
275
  {
276
+ <label class="flex items-center gap-2 px-3 py-2 rounded-md border border-mystic-200 bg-mystic-50 hover:bg-mystic-100 cursor-pointer text-sm text-mystic-700 transition-colors">
277
  <input type="checkbox" checked="@(_formModel.CategoryIds.Contains(cat.Id))"
278
  @onchange='(e) => ToggleCategory(cat.Id, (bool)e.Value!)'
279
+ class="h-4 w-4 rounded border-mystic-300 text-mystic-900 focus:ring-mystic-900" />
280
  @cat.Name
281
  </label>
282
  }
 
284
  </div>
285
  </div>
286
 
287
+ <div class="pt-5 border-t border-mystic-100 flex justify-end gap-3">
288
  <button type="button" @onclick="ShowList" class="btn-secondary">Cancel</button>
289
  <button type="submit" class="btn-primary" disabled="@_isSubmitting">
290
  @(_isSubmitting ? "Saving..." : (_viewMode == ViewMode.Create ? "Add Book" : "Update Book"))
BlazorFrontend/Components/Pages/Borrowings.razor CHANGED
@@ -7,14 +7,14 @@
7
  <PageTitle>My Loans</PageTitle>
8
 
9
  <div class="mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
10
- <h1 class="text-3xl font-bold tracking-tight text-slate-900">My Loans</h1>
11
- <p class="mt-2 text-slate-500">Track your current borrowings and return upcoming books.</p>
12
  </div>
13
 
14
  @if (_isLoading)
15
  {
16
  <div class="py-20 flex justify-center">
17
- <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-slate-900"></div>
18
  </div>
19
  }
20
  else
@@ -23,44 +23,44 @@ else
23
  <div class="overflow-x-auto">
24
  <table class="w-full text-left border-collapse">
25
  <thead>
26
- <tr class="bg-slate-50/50 border-b border-slate-100">
27
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Book Information</th>
28
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Dates</th>
29
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Status</th>
30
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Fines</th>
31
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest text-right">Actions</th>
32
  </tr>
33
  </thead>
34
- <tbody class="divide-y divide-slate-100">
35
  @if (!_borrowings.Any())
36
  {
37
  <tr>
38
- <td colspan="5" class="px-6 py-12 text-center text-slate-400 italic">
39
  You don't have any borrowing history yet.
40
  </td>
41
  </tr>
42
  }
43
  @foreach (var loan in _borrowings)
44
  {
45
- <tr class="hover:bg-slate-50 transition-colors group">
46
  <td class="px-6 py-4">
47
  <div class="flex items-center gap-3">
48
- <div class="w-10 h-14 bg-slate-100 rounded flex items-center justify-center text-slate-300 group-hover:bg-slate-200 transition-colors">
49
  <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>
50
  </div>
51
  <div class="min-w-0">
52
- <p class="text-sm font-bold text-slate-900 truncate">@loan.BookTitle</p>
53
- <p class="text-[11px] text-slate-500 mt-0.5">Loan ID: #@loan.Id</p>
54
  </div>
55
  </div>
56
  </td>
57
  <td class="px-6 py-4">
58
  <div class="space-y-1">
59
- <p class="text-[11px]"><span class="text-slate-400">Borrowed:</span> <span class="font-medium text-slate-700">@loan.BorrowDate.ToString("MMM dd, yyyy")</span></p>
60
- <p class="text-[11px]"><span class="text-slate-400">Due:</span> <span class="font-medium @(loan.DueDate < DateTime.Now && loan.Status == "Borrowed" ? "text-rose-600" : "text-slate-700")">@loan.DueDate.ToString("MMM dd, yyyy")</span></p>
61
  @if (loan.ReturnDate.HasValue)
62
  {
63
- <p class="text-[11px]"><span class="text-slate-400">Returned:</span> <span class="font-medium text-emerald-600">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span></p>
64
  }
65
  </div>
66
  </td>
@@ -76,7 +76,7 @@ else
76
  @if (loan.FineAmount > 0)
77
  {
78
  <div class="flex flex-col">
79
- <span class="text-xs font-bold @(loan.IsFinePaid ? "text-slate-500 line-through" : "text-rose-600")">
80
  MMK @loan.FineAmount.ToString("N0")
81
  </span>
82
  @if (loan.IsFinePaid)
@@ -87,20 +87,20 @@ else
87
  }
88
  else
89
  {
90
- <span class="text-[11px] text-slate-400">No Fines</span>
91
  }
92
  </td>
93
  <td class="px-6 py-4 text-right">
94
  @if (loan.Status == "Borrowed")
95
  {
96
  <button @onclick="() => RequestReturnAsync(loan)"
97
- class="bg-slate-900 text-white py-1.5 px-4 rounded-xl text-xs font-bold hover:bg-slate-800 transition-all active:scale-95 shadow-sm">
98
  Return Book
99
  </button>
100
  }
101
  else if (loan.Status == "PendingReturn")
102
  {
103
- <span class="text-xs font-medium text-slate-400 italic">Awaiting Approval</span>
104
  }
105
  else
106
  {
 
7
  <PageTitle>My Loans</PageTitle>
8
 
9
  <div class="mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
10
+ <h1 class="text-3xl font-bold tracking-tight text-mystic-900">My Loans</h1>
11
+ <p class="mt-2 text-mystic-500">Track your current borrowings and return upcoming books.</p>
12
  </div>
13
 
14
  @if (_isLoading)
15
  {
16
  <div class="py-20 flex justify-center">
17
+ <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
18
  </div>
19
  }
20
  else
 
23
  <div class="overflow-x-auto">
24
  <table class="w-full text-left border-collapse">
25
  <thead>
26
+ <tr class="bg-mystic-50/50 border-b border-mystic-100">
27
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Book Information</th>
28
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Dates</th>
29
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Status</th>
30
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Fines</th>
31
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest text-right">Actions</th>
32
  </tr>
33
  </thead>
34
+ <tbody class="divide-y divide-mystic-100">
35
  @if (!_borrowings.Any())
36
  {
37
  <tr>
38
+ <td colspan="5" class="px-6 py-12 text-center text-mystic-400 italic">
39
  You don't have any borrowing history yet.
40
  </td>
41
  </tr>
42
  }
43
  @foreach (var loan in _borrowings)
44
  {
45
+ <tr class="hover:bg-mystic-50 transition-colors group">
46
  <td class="px-6 py-4">
47
  <div class="flex items-center gap-3">
48
+ <div class="w-10 h-14 bg-mystic-100 rounded flex items-center justify-center text-mystic-300 group-hover:bg-mystic-200 transition-colors">
49
  <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>
50
  </div>
51
  <div class="min-w-0">
52
+ <p class="text-sm font-bold text-mystic-900 truncate">@loan.BookTitle</p>
53
+ <p class="text-[11px] text-mystic-500 mt-0.5">Loan ID: #@loan.Id</p>
54
  </div>
55
  </div>
56
  </td>
57
  <td class="px-6 py-4">
58
  <div class="space-y-1">
59
+ <p class="text-[11px]"><span class="text-mystic-400">Borrowed:</span> <span class="font-medium text-mystic-700">@loan.BorrowDate.ToString("MMM dd, yyyy")</span></p>
60
+ <p class="text-[11px]"><span class="text-mystic-400">Due:</span> <span class="font-medium @(loan.DueDate < DateTime.Now && loan.Status == "Borrowed" ? "text-rose-600" : "text-mystic-700")">@loan.DueDate.ToString("MMM dd, yyyy")</span></p>
61
  @if (loan.ReturnDate.HasValue)
62
  {
63
+ <p class="text-[11px]"><span class="text-mystic-400">Returned:</span> <span class="font-medium text-emerald-600">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span></p>
64
  }
65
  </div>
66
  </td>
 
76
  @if (loan.FineAmount > 0)
77
  {
78
  <div class="flex flex-col">
79
+ <span class="text-xs font-bold @(loan.IsFinePaid ? "text-mystic-500 line-through" : "text-rose-600")">
80
  MMK @loan.FineAmount.ToString("N0")
81
  </span>
82
  @if (loan.IsFinePaid)
 
87
  }
88
  else
89
  {
90
+ <span class="text-[11px] text-mystic-400">No Fines</span>
91
  }
92
  </td>
93
  <td class="px-6 py-4 text-right">
94
  @if (loan.Status == "Borrowed")
95
  {
96
  <button @onclick="() => RequestReturnAsync(loan)"
97
+ class="bg-mystic-900 text-white py-1.5 px-4 rounded-xl text-xs font-bold hover:bg-mystic-800 transition-all active:scale-95 shadow-sm">
98
  Return Book
99
  </button>
100
  }
101
  else if (loan.Status == "PendingReturn")
102
  {
103
+ <span class="text-xs font-medium text-mystic-400 italic">Awaiting Approval</span>
104
  }
105
  else
106
  {
BlazorFrontend/Components/Pages/BorrowingsManage.razor CHANGED
@@ -11,13 +11,13 @@
11
  <div class="mb-10 animate-in fade-in slide-in-from-top-4 duration-500">
12
  <div class="flex items-center justify-between mb-4">
13
  <div>
14
- <h1 class="text-3xl font-extrabold tracking-tight text-slate-900 border-b border-slate-100 pb-4 mb-2">Manage All Borrowings</h1>
15
- <p class="text-slate-500 font-medium">Monitor and process library loans across all members.</p>
16
  </div>
17
  <div class="flex gap-3">
18
- <div class="card px-4 py-2 flex items-center gap-2 bg-slate-50 border-slate-200">
19
- <span class="text-xs font-bold text-slate-400 uppercase tracking-widest">Total Active:</span>
20
- <span class="text-lg font-bold text-slate-900">@_borrowings.Count(m => m.Status == "Borrowed")</span>
21
  </div>
22
  </div>
23
  </div>
@@ -26,7 +26,7 @@
26
  @if (_isLoading)
27
  {
28
  <div class="py-20 flex justify-center">
29
- <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-slate-900"></div>
30
  </div>
31
  }
32
  else
@@ -35,52 +35,52 @@ else
35
  <div class="overflow-x-auto">
36
  <table class="w-full text-left border-collapse">
37
  <thead>
38
- <tr class="bg-slate-50 border-b border-slate-200">
39
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Book & Loan Info</th>
40
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Member Details</th>
41
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Loan Dates</th>
42
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest text-center">Status</th>
43
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Fines</th>
44
- <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest text-right">Actions</th>
45
  </tr>
46
  </thead>
47
- <tbody class="divide-y divide-slate-100">
48
  @if (!_borrowings.Any())
49
  {
50
  <tr>
51
- <td colspan="6" class="px-6 py-16 text-center text-slate-400 italic bg-white">
52
  No borrowing records found in the system.
53
  </td>
54
  </tr>
55
  }
56
  @foreach (var loan in _borrowings.OrderByDescending(l => l.BorrowDate))
57
  {
58
- <tr class="hover:bg-slate-50/80 transition-colors">
59
  <td class="px-6 py-5">
60
  <div class="flex items-center gap-4">
61
- <div class="w-10 h-14 bg-slate-900/5 rounded-lg flex items-center justify-center text-slate-400 border border-slate-100">
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-slate-900 truncate mb-0.5">@loan.BookTitle</p>
66
- <p class="text-[10px] font-mono text-slate-400 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-slate-800">@loan.UserEmail</p>
73
- <p class="text-[10px] text-slate-500 font-medium tracking-wide">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-slate-400 w-12">Out:</span>
80
- <span class="font-bold text-slate-700">@loan.BorrowDate.ToString("MMM dd, yyyy")</span>
81
  </div>
82
  <div class="flex items-center gap-2 text-[11px]">
83
- <span class="text-slate-400 w-12">Due:</span>
84
  @{
85
  var isOverdue = loan.DueDate < DateTime.Now && loan.Status == "Borrowed";
86
  }
@@ -89,7 +89,7 @@ else
89
  @if (loan.ReturnDate.HasValue)
90
  {
91
  <div class="flex items-center gap-2 text-[11px]">
92
- <span class="text-slate-400 w-12">Back:</span>
93
  <span class="font-bold text-emerald-600">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span>
94
  </div>
95
  }
@@ -107,7 +107,7 @@ else
107
  @if (loan.FineAmount > 0)
108
  {
109
  <div class="flex flex-col">
110
- <span class="text-sm font-extrabold @(loan.IsFinePaid ? "text-slate-400 line-through" : "text-rose-600")">
111
  MMK @loan.FineAmount.ToString("N0")
112
  </span>
113
  @if (loan.IsFinePaid)
@@ -122,7 +122,7 @@ else
122
  }
123
  else
124
  {
125
- <span class="text-[10px] text-slate-300 font-bold italic">No Fines</span>
126
  }
127
  </td>
128
  <td class="px-6 py-5 text-right">
@@ -137,7 +137,7 @@ else
137
  else if (loan.Status == "Borrowed")
138
  {
139
  <button @onclick="() => ForceReturnAsync(loan)"
140
- class="inline-flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-slate-800 transition-all shadow-sm active:scale-95">
141
  Process Return
142
  </button>
143
  }
 
11
  <div class="mb-10 animate-in fade-in slide-in-from-top-4 duration-500">
12
  <div class="flex items-center justify-between mb-4">
13
  <div>
14
+ <h1 class="text-3xl font-extrabold tracking-tight text-mystic-900 border-b border-mystic-100 pb-4 mb-2">Manage All Borrowings</h1>
15
+ <p class="text-mystic-500 font-medium">Monitor and process library loans across all members.</p>
16
  </div>
17
  <div class="flex gap-3">
18
+ <div class="card px-4 py-2 flex items-center gap-2 bg-mystic-50 border-mystic-200">
19
+ <span class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Total Active:</span>
20
+ <span class="text-lg font-bold text-mystic-900">@_borrowings.Count(m => m.Status == "Borrowed")</span>
21
  </div>
22
  </div>
23
  </div>
 
26
  @if (_isLoading)
27
  {
28
  <div class="py-20 flex justify-center">
29
+ <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
30
  </div>
31
  }
32
  else
 
35
  <div class="overflow-x-auto">
36
  <table class="w-full text-left border-collapse">
37
  <thead>
38
+ <tr class="bg-mystic-50 border-b border-mystic-200">
39
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Book & Loan Info</th>
40
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Member Details</th>
41
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Loan Dates</th>
42
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest text-center">Status</th>
43
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Fines</th>
44
+ <th class="px-6 py-4 text-[10px] font-bold text-mystic-400 uppercase tracking-widest text-right">Actions</th>
45
  </tr>
46
  </thead>
47
+ <tbody class="divide-y divide-mystic-100">
48
  @if (!_borrowings.Any())
49
  {
50
  <tr>
51
+ <td colspan="6" class="px-6 py-16 text-center text-mystic-400 italic bg-white">
52
  No borrowing records found in the system.
53
  </td>
54
  </tr>
55
  }
56
  @foreach (var loan in _borrowings.OrderByDescending(l => l.BorrowDate))
57
  {
58
+ <tr class="hover:bg-mystic-50/80 transition-colors">
59
  <td class="px-6 py-5">
60
  <div class="flex items-center gap-4">
61
+ <div class="w-10 h-14 bg-mystic-900/5 rounded-lg flex items-center justify-center text-mystic-400 border border-mystic-100">
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-mystic-900 truncate mb-0.5">@loan.BookTitle</p>
66
+ <p class="text-[10px] font-mono text-mystic-400 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-mystic-800">@loan.UserEmail</p>
73
+ <p class="text-[10px] text-mystic-500 font-medium tracking-wide">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-mystic-400 w-12">Out:</span>
80
+ <span class="font-bold text-mystic-700">@loan.BorrowDate.ToString("MMM dd, yyyy")</span>
81
  </div>
82
  <div class="flex items-center gap-2 text-[11px]">
83
+ <span class="text-mystic-400 w-12">Due:</span>
84
  @{
85
  var isOverdue = loan.DueDate < DateTime.Now && loan.Status == "Borrowed";
86
  }
 
89
  @if (loan.ReturnDate.HasValue)
90
  {
91
  <div class="flex items-center gap-2 text-[11px]">
92
+ <span class="text-mystic-400 w-12">Back:</span>
93
  <span class="font-bold text-emerald-600">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span>
94
  </div>
95
  }
 
107
  @if (loan.FineAmount > 0)
108
  {
109
  <div class="flex flex-col">
110
+ <span class="text-sm font-extrabold @(loan.IsFinePaid ? "text-mystic-400 line-through" : "text-rose-600")">
111
  MMK @loan.FineAmount.ToString("N0")
112
  </span>
113
  @if (loan.IsFinePaid)
 
122
  }
123
  else
124
  {
125
+ <span class="text-[10px] text-mystic-300 font-bold italic">No Fines</span>
126
  }
127
  </td>
128
  <td class="px-6 py-5 text-right">
 
137
  else if (loan.Status == "Borrowed")
138
  {
139
  <button @onclick="() => ForceReturnAsync(loan)"
140
+ class="inline-flex items-center gap-2 bg-mystic-900 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-mystic-800 transition-all shadow-sm active:scale-95">
141
  Process Return
142
  </button>
143
  }
BlazorFrontend/Components/Pages/Home.razor CHANGED
@@ -9,14 +9,14 @@
9
  <PageTitle>Dashboard</PageTitle>
10
 
11
  <div class="mb-10 animate-in fade-in slide-in-from-top-4 duration-500">
12
- <h1 class="text-3xl font-extrabold tracking-tight text-slate-900 border-b border-slate-100 pb-4 mb-2">Welcome Back</h1>
13
- <p class="text-slate-500 font-medium">Here's what's happening with the library today.</p>
14
  </div>
15
 
16
  @if (_isLoading)
17
  {
18
  <div class="flex items-center justify-center min-h-[400px]">
19
- <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900"></div>
20
  </div>
21
  }
22
  else
@@ -56,38 +56,38 @@ else
56
 
57
  <!-- Stats Grid -->
58
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
59
- <div class="card p-6 flex flex-col gap-3 group hover:border-slate-300 transition-all duration-300">
60
  <div class="flex items-center justify-between">
61
- <div class="p-2 bg-slate-50 rounded-lg group-hover:bg-slate-100 transition-colors">
62
- <svg class="w-5 h-5 text-slate-600" 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
- <span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Books</span>
65
  </div>
66
  <div>
67
- <div class="text-3xl font-bold text-slate-900">@_totalBooks</div>
68
- <div class="text-xs text-slate-500 mt-1 font-medium italic">Total in catalog</div>
69
  </div>
70
  </div>
71
 
72
- <div class="card p-6 flex flex-col gap-3 group hover:border-slate-300 transition-all duration-300">
73
  <div class="flex items-center justify-between">
74
  <div class="p-2 bg-emerald-50 rounded-lg group-hover:bg-emerald-100 transition-colors">
75
  <svg class="w-5 h-5 text-emerald-600" 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>
76
  </div>
77
- <span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Active Loans</span>
78
  </div>
79
  <div>
80
- <div class="text-3xl font-bold text-slate-900">@_activeLoans</div>
81
- <div class="text-xs text-slate-500 mt-1 font-medium italic">Checking out right now</div>
82
  </div>
83
  </div>
84
 
85
- <div class="card p-6 flex flex-col gap-3 group hover:border-slate-300 border-l-4 border-l-rose-500 transition-all duration-300">
86
  <div class="flex items-center justify-between">
87
  <div class="p-2 bg-rose-50 rounded-lg group-hover:bg-rose-100 transition-colors">
88
  <svg class="w-5 h-5 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
89
  </div>
90
- <span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest text-rose-500">Overdue</span>
91
  </div>
92
  <div>
93
  <div class="text-3xl font-bold text-rose-600">@_overdueLoans</div>
@@ -95,16 +95,16 @@ else
95
  </div>
96
  </div>
97
 
98
- <div class="card p-6 flex flex-col gap-3 group hover:border-slate-300 transition-all duration-300">
99
  <div class="flex items-center justify-between">
100
  <div class="p-2 bg-blue-50 rounded-lg group-hover:bg-blue-100 transition-colors">
101
  <svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
102
  </div>
103
- <span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Active Members</span>
104
  </div>
105
  <div>
106
- <div class="text-3xl font-bold text-slate-900">@_totalMembers</div>
107
- <div class="text-xs text-slate-500 mt-1 font-medium italic">In the community</div>
108
  </div>
109
  </div>
110
  </div>
@@ -114,8 +114,8 @@ else
114
  {
115
  <div class="mb-12 animate-in fade-in slide-in-from-bottom-6 duration-700 delay-150">
116
  <div class="flex items-center justify-between mb-8">
117
- <h2 class="text-2xl font-bold text-slate-900 tracking-tight">Recent Arrivals</h2>
118
- <a href="Books" class="text-sm font-bold text-slate-500 hover:text-slate-900 flex items-center group">
119
  Browse All
120
  <svg class="w-4 h-4 ml-1 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="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
121
  </a>
@@ -126,17 +126,17 @@ else
126
  {
127
  <div class="group cursor-pointer">
128
  <a href="Books">
129
- <div class="aspect-[2/3] w-full bg-slate-100 rounded-xl mb-3 flex items-center justify-center text-slate-300 group-hover:shadow-lg transition-all duration-300 relative overflow-hidden">
130
  <svg class="w-10 h-10" 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>
131
  @if (book.AvailableCopies <= 0)
132
  {
133
- <div class="absolute inset-0 bg-slate-900/40 backdrop-blur-[1px] flex items-center justify-center">
134
- <span class="bg-white text-slate-900 text-[8px] font-bold uppercase px-2 py-1 rounded">Out</span>
135
  </div>
136
  }
137
  </div>
138
- <h3 class="text-xs font-bold text-slate-900 truncate group-hover:text-amber-600 transition-colors">@book.Title</h3>
139
- <p class="text-[10px] text-slate-500 truncate mt-0.5 font-medium">@book.Author</p>
140
  </a>
141
  </div>
142
  }
@@ -146,26 +146,26 @@ else
146
 
147
  <!-- Quick Actions -->
148
  <div class="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-8 duration-700 delay-300">
149
- <div class="bg-slate-900 text-white rounded-3xl p-8 flex flex-col justify-between overflow-hidden relative group">
150
  <div class="relative z-10">
151
  <h2 class="text-2xl font-bold mb-2">Explore the Library</h2>
152
- <p class="text-slate-400 text-sm max-w-[280px]">Access thousands of books across all categories with a premium member account.</p>
153
  </div>
154
  <div class="mt-8 relative z-10">
155
- <a href="Books" class="inline-flex items-center bg-white text-slate-900 px-6 py-2.5 rounded-full text-sm font-bold hover:bg-slate-100 transition-colors">
156
  View Catalog
157
  </a>
158
  </div>
159
- <svg class="absolute bottom-0 right-0 w-48 h-48 text-slate-800 -mb-12 -mr-12 opacity-50 transition-transform group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24"><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>
160
  </div>
161
 
162
- <div class="bg-amber-50 rounded-3xl p-8 flex flex-col justify-between group border border-amber-100">
163
  <div>
164
- <h2 class="text-2xl font-bold text-slate-900 mb-2">My Current Loans</h2>
165
- <p class="text-slate-600 text-sm max-w-[280px]">Don't forget to return your books on time to avoid late fees.</p>
166
  </div>
167
  <div class="mt-8">
168
- <a href="Borrowings" class="inline-flex items-center text-slate-900 px-6 py-2.5 rounded-full text-sm font-bold border-2 border-slate-900 hover:bg-slate-900 hover:text-white transition-all">
169
  Manage My Loans
170
  </a>
171
  </div>
 
9
  <PageTitle>Dashboard</PageTitle>
10
 
11
  <div class="mb-10 animate-in fade-in slide-in-from-top-4 duration-500">
12
+ <h1 class="text-3xl font-extrabold tracking-tight text-mystic-900 border-b border-mystic-100 pb-4 mb-2">Welcome Back</h1>
13
+ <p class="text-mystic-500 font-medium">Here's what's happening with the library today.</p>
14
  </div>
15
 
16
  @if (_isLoading)
17
  {
18
  <div class="flex items-center justify-center min-h-[400px]">
19
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mystic-900"></div>
20
  </div>
21
  }
22
  else
 
56
 
57
  <!-- Stats Grid -->
58
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
59
+ <div class="card p-6 flex flex-col gap-3 group hover:border-mystic-300 transition-all duration-300">
60
  <div class="flex items-center justify-between">
61
+ <div class="p-2 bg-mystic-50 rounded-lg group-hover:bg-mystic-100 transition-colors">
62
+ <svg class="w-5 h-5 text-mystic-600" 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
+ <span class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Books</span>
65
  </div>
66
  <div>
67
+ <div class="text-3xl font-bold text-mystic-900">@_totalBooks</div>
68
+ <div class="text-xs text-mystic-500 mt-1 font-medium italic">Total in catalog</div>
69
  </div>
70
  </div>
71
 
72
+ <div class="card p-6 flex flex-col gap-3 group hover:border-mystic-300 transition-all duration-300">
73
  <div class="flex items-center justify-between">
74
  <div class="p-2 bg-emerald-50 rounded-lg group-hover:bg-emerald-100 transition-colors">
75
  <svg class="w-5 h-5 text-emerald-600" 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>
76
  </div>
77
+ <span class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Active Loans</span>
78
  </div>
79
  <div>
80
+ <div class="text-3xl font-bold text-mystic-900">@_activeLoans</div>
81
+ <div class="text-xs text-mystic-500 mt-1 font-medium italic">Checking out right now</div>
82
  </div>
83
  </div>
84
 
85
+ <div class="card p-6 flex flex-col gap-3 group hover:border-mystic-300 border-l-4 border-l-rose-500 transition-all duration-300">
86
  <div class="flex items-center justify-between">
87
  <div class="p-2 bg-rose-50 rounded-lg group-hover:bg-rose-100 transition-colors">
88
  <svg class="w-5 h-5 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
89
  </div>
90
+ <span class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest text-rose-500">Overdue</span>
91
  </div>
92
  <div>
93
  <div class="text-3xl font-bold text-rose-600">@_overdueLoans</div>
 
95
  </div>
96
  </div>
97
 
98
+ <div class="card p-6 flex flex-col gap-3 group hover:border-mystic-300 transition-all duration-300">
99
  <div class="flex items-center justify-between">
100
  <div class="p-2 bg-blue-50 rounded-lg group-hover:bg-blue-100 transition-colors">
101
  <svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
102
  </div>
103
+ <span class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest">Active Members</span>
104
  </div>
105
  <div>
106
+ <div class="text-3xl font-bold text-mystic-900">@_totalMembers</div>
107
+ <div class="text-xs text-mystic-500 mt-1 font-medium italic">In the community</div>
108
  </div>
109
  </div>
110
  </div>
 
114
  {
115
  <div class="mb-12 animate-in fade-in slide-in-from-bottom-6 duration-700 delay-150">
116
  <div class="flex items-center justify-between mb-8">
117
+ <h2 class="text-2xl font-bold text-mystic-900 tracking-tight">Recent Arrivals</h2>
118
+ <a href="Books" class="text-sm font-bold text-mystic-500 hover:text-mystic-900 flex items-center group">
119
  Browse All
120
  <svg class="w-4 h-4 ml-1 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="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
121
  </a>
 
126
  {
127
  <div class="group cursor-pointer">
128
  <a href="Books">
129
+ <div class="aspect-[2/3] w-full bg-mystic-100 rounded-xl mb-3 flex items-center justify-center text-mystic-300 group-hover:shadow-lg transition-all duration-300 relative overflow-hidden">
130
  <svg class="w-10 h-10" 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>
131
  @if (book.AvailableCopies <= 0)
132
  {
133
+ <div class="absolute inset-0 bg-mystic-900/40 backdrop-blur-[1px] flex items-center justify-center">
134
+ <span class="bg-white text-mystic-900 text-[8px] font-bold uppercase px-2 py-1 rounded">Out</span>
135
  </div>
136
  }
137
  </div>
138
+ <h3 class="text-xs font-bold text-mystic-900 truncate group-hover:text-mystic-600 transition-colors">@book.Title</h3>
139
+ <p class="text-[10px] text-mystic-500 truncate mt-0.5 font-medium">@book.Author</p>
140
  </a>
141
  </div>
142
  }
 
146
 
147
  <!-- Quick Actions -->
148
  <div class="grid grid-cols-1 md:grid-cols-2 gap-8 animate-in fade-in slide-in-from-bottom-8 duration-700 delay-300">
149
+ <div class="bg-mystic-900 text-white rounded-3xl p-8 flex flex-col justify-between overflow-hidden relative group">
150
  <div class="relative z-10">
151
  <h2 class="text-2xl font-bold mb-2">Explore the Library</h2>
152
+ <p class="text-mystic-400 text-sm max-w-[280px]">Access thousands of books across all categories with a premium member account.</p>
153
  </div>
154
  <div class="mt-8 relative z-10">
155
+ <a href="Books" class="inline-flex items-center bg-white text-mystic-900 px-6 py-2.5 rounded-full text-sm font-bold hover:bg-mystic-100 transition-colors">
156
  View Catalog
157
  </a>
158
  </div>
159
+ <svg class="absolute bottom-0 right-0 w-48 h-48 text-mystic-800 -mb-12 -mr-12 opacity-50 transition-transform group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24"><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>
160
  </div>
161
 
162
+ <div class="bg-mystic-50 rounded-3xl p-8 flex flex-col justify-between group border border-mystic-100">
163
  <div>
164
+ <h2 class="text-2xl font-bold text-mystic-900 mb-2">My Current Loans</h2>
165
+ <p class="text-mystic-600 text-sm max-w-[280px]">Don't forget to return your books on time to avoid late fees.</p>
166
  </div>
167
  <div class="mt-8">
168
+ <a href="Borrowings" class="inline-flex items-center text-mystic-900 px-6 py-2.5 rounded-full text-sm font-bold border-2 border-mystic-900 hover:bg-mystic-900 hover:text-white transition-all">
169
  Manage My Loans
170
  </a>
171
  </div>
BlazorFrontend/Components/Pages/Login.razor CHANGED
@@ -11,13 +11,13 @@
11
  <PageTitle>Sign In</PageTitle>
12
 
13
  <div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
14
- <div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-slate-200 shadow-xl">
15
  <div>
16
- <div class="mx-auto h-12 w-12 bg-slate-900 rounded-xl flex items-center justify-center text-white mb-4">
17
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
18
  </div>
19
- <h2 class="text-center text-3xl font-extrabold text-slate-900 tracking-tight">Welcome back</h2>
20
- <p class="mt-2 text-center text-sm text-slate-500">
21
  Continue where you left off.
22
  </p>
23
  </div>
@@ -34,21 +34,21 @@
34
 
35
  <div class="space-y-4">
36
  <div>
37
- <label class="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
38
- <InputText @bind-Value="_loginRequest.Email" type="email" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="name@example.com" />
39
  <ValidationMessage For="@(() => _loginRequest.Email)" class="text-xs text-red-500 mt-1" />
40
  </div>
41
  <div>
42
  <div class="flex items-center justify-between mb-1">
43
- <label class="block text-sm font-medium text-slate-700">Password</label>
44
  </div>
45
- <InputText type="password" @bind-Value="_loginRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="••••••••" />
46
  <ValidationMessage For="@(() => _loginRequest.Password)" class="text-xs text-red-500 mt-1" />
47
  </div>
48
  </div>
49
 
50
  <div>
51
- <button type="submit" disabled="@_isLoading" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-all disabled:opacity-50">
52
  @if (_isLoading)
53
  {
54
  <span>Signing in...</span>
@@ -61,8 +61,8 @@
61
  </div>
62
 
63
  <div class="text-center">
64
- <span class="text-sm text-slate-500">Don't have an account?</span>
65
- <a href="/register" class="text-sm font-medium text-slate-900 hover:underline ml-1">Create one for free</a>
66
  </div>
67
  </EditForm>
68
  </div>
 
11
  <PageTitle>Sign In</PageTitle>
12
 
13
  <div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
14
+ <div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-mystic-200 shadow-xl">
15
  <div>
16
+ <div class="mx-auto h-12 w-12 bg-mystic-900 rounded-xl flex items-center justify-center text-white mb-4">
17
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
18
  </div>
19
+ <h2 class="text-center text-3xl font-extrabold text-mystic-900 tracking-tight">Welcome back</h2>
20
+ <p class="mt-2 text-center text-sm text-mystic-500">
21
  Continue where you left off.
22
  </p>
23
  </div>
 
34
 
35
  <div class="space-y-4">
36
  <div>
37
+ <label class="block text-sm font-medium text-mystic-700 mb-1">Email Address</label>
38
+ <InputText @bind-Value="_loginRequest.Email" type="email" required class="appearance-none relative block w-full px-3 py-2 border border-mystic-300 placeholder-mystic-400 text-mystic-900 rounded-md focus:outline-none focus:ring-mystic-900 focus:border-mystic-900 focus:z-10 sm:text-sm" placeholder="name@example.com" />
39
  <ValidationMessage For="@(() => _loginRequest.Email)" class="text-xs text-red-500 mt-1" />
40
  </div>
41
  <div>
42
  <div class="flex items-center justify-between mb-1">
43
+ <label class="block text-sm font-medium text-mystic-700">Password</label>
44
  </div>
45
+ <InputText type="password" @bind-Value="_loginRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-mystic-300 placeholder-mystic-400 text-mystic-900 rounded-md focus:outline-none focus:ring-mystic-900 focus:border-mystic-900 focus:z-10 sm:text-sm" placeholder="••••••••" />
46
  <ValidationMessage For="@(() => _loginRequest.Password)" class="text-xs text-red-500 mt-1" />
47
  </div>
48
  </div>
49
 
50
  <div>
51
+ <button type="submit" disabled="@_isLoading" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-mystic-900 hover:bg-mystic-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mystic-900 transition-all disabled:opacity-50">
52
  @if (_isLoading)
53
  {
54
  <span>Signing in...</span>
 
61
  </div>
62
 
63
  <div class="text-center">
64
+ <span class="text-sm text-mystic-500">Don't have an account?</span>
65
+ <a href="/register" class="text-sm font-medium text-mystic-900 hover:underline ml-1">Create one for free</a>
66
  </div>
67
  </EditForm>
68
  </div>
BlazorFrontend/Components/Pages/Members.razor CHANGED
@@ -14,10 +14,10 @@
14
  {
15
  <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
16
  <div>
17
- <h1 class="text-3xl font-bold tracking-tight text-slate-900">Members Directory</h1>
18
- <p class="mt-2 text-slate-500">
19
  Manage library members and administrator accounts.
20
- <span class="ml-2 text-xs text-slate-400">(@(_allUsers?.Count() ?? 0) total)</span>
21
  </p>
22
  </div>
23
  <div class="flex items-center gap-3">
@@ -31,46 +31,46 @@
31
  @if (_isLoading)
32
  {
33
  <div class="flex items-center justify-center min-h-[400px]">
34
- <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900"></div>
35
  </div>
36
  }
37
  else if (_pagedResult != null)
38
  {
39
  <div class="card overflow-hidden animate-in fade-in duration-700">
40
  <div class="overflow-x-auto">
41
- <table class="min-w-full divide-y divide-slate-200">
42
- <thead class="bg-slate-50">
43
  <tr>
44
- <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Member</th>
45
- <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Contact</th>
46
- <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Role</th>
47
- <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
48
  <th scope="col" class="relative px-6 py-3"><span class="sr-only">Actions</span></th>
49
  </tr>
50
  </thead>
51
- <tbody class="bg-white divide-y divide-slate-200">
52
  @foreach (var user in _pagedResult.Items)
53
  {
54
- <tr class="hover:bg-slate-50 transition-colors">
55
  <td class="px-6 py-4 whitespace-nowrap">
56
  <div class="flex items-center">
57
- <div class="h-10 w-10 flex-shrink-0 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-sm">
58
  @(user.FullName?.Substring(0, Math.Min(2, user.FullName.Length)).ToUpper() ?? "U")
59
  </div>
60
  <div class="ml-4">
61
- <div class="text-sm font-medium text-slate-900">
62
  <button @onclick="() => ShowDetails(user.Id)" class="hover:underline text-left">@user.FullName</button>
63
  </div>
64
  @if (!string.IsNullOrEmpty(user.StudentId))
65
  {
66
- <div class="text-xs text-slate-500">ID: @user.StudentId</div>
67
  }
68
  </div>
69
  </div>
70
  </td>
71
  <td class="px-6 py-4 whitespace-nowrap">
72
- <div class="text-sm text-slate-900">@user.Email</div>
73
- <div class="text-xs text-slate-500">@(string.IsNullOrEmpty(user.PhoneNumber) ? "No phone" : user.PhoneNumber)</div>
74
  </td>
75
  <td class="px-6 py-4 whitespace-nowrap">
76
  <span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 @(user.Role == "Admin" ? "bg-amber-100 text-amber-800" : "bg-blue-100 text-blue-800")">
@@ -92,14 +92,14 @@
92
  }
93
  else
94
  {
95
- <span class="inline-flex rounded-full bg-slate-100 px-2 text-xs font-semibold leading-5 text-slate-800">
96
  Inactive
97
  </span>
98
  }
99
  </td>
100
  <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
101
  <div class="flex items-center justify-end gap-3">
102
- <button @onclick="() => ShowDetails(user.Id)" class="text-slate-600 hover:text-slate-900 transition-colors">Details</button>
103
  <button @onclick="() => ShowEdit(user.Id)" class="text-indigo-600 hover:text-indigo-900 transition-colors">Edit</button>
104
  </div>
105
  </td>
@@ -112,17 +112,17 @@
112
  @* Pagination *@
113
  @if (_pagedResult.TotalPages > 1)
114
  {
115
- <div class="px-6 py-4 border-t border-slate-200 flex items-center justify-between bg-white">
116
- <p class="text-sm text-slate-500">
117
- Showing <span class="font-medium text-slate-700">@((_pagedResult.CurrentPage - 1) * _pagedResult.PageSize + 1)</span>
118
 
119
- <span class="font-medium text-slate-700">@(Math.Min(_pagedResult.CurrentPage * _pagedResult.PageSize, _pagedResult.TotalCount))</span>
120
- of <span class="font-medium text-slate-700">@_pagedResult.TotalCount</span> members
121
  </p>
122
  <nav class="flex items-center gap-1" aria-label="Pagination">
123
  @* Previous *@
124
  <button @onclick="() => ChangePage(_pagedResult.CurrentPage - 1)" disabled="@(!_pagedResult.HasPreviousPage)"
125
- class="inline-flex items-center px-3 py-1.5 rounded-md border @(_pagedResult.HasPreviousPage ? "border-slate-200 text-slate-600 hover:bg-slate-50 hover:text-slate-900" : "border-slate-100 text-slate-300 cursor-not-allowed") text-sm font-medium transition-colors">
126
  <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
128
  </svg>
@@ -135,22 +135,22 @@
135
  var pageNumber = p;
136
  if (p == _pagedResult.CurrentPage)
137
  {
138
- <span class="inline-flex items-center justify-center w-8 h-8 rounded-md bg-slate-900 text-white text-sm font-semibold">@p</span>
139
  }
140
  else if (p == 1 || p == _pagedResult.TotalPages || (p >= _pagedResult.CurrentPage - 2 && p <= _pagedResult.CurrentPage + 2))
141
  {
142
  <button @onclick="() => ChangePage(pageNumber)"
143
- class="inline-flex items-center justify-center w-8 h-8 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">@p</button>
144
  }
145
  else if (p == _pagedResult.CurrentPage - 3 || p == _pagedResult.CurrentPage + 3)
146
  {
147
- <span class="inline-flex items-center justify-center w-8 h-8 text-sm text-slate-400">…</span>
148
  }
149
  }
150
 
151
  @* Next *@
152
  <button @onclick="() => ChangePage(_pagedResult.CurrentPage + 1)" disabled="@(!_pagedResult.HasNextPage)"
153
- class="inline-flex items-center px-3 py-1.5 rounded-md border @(_pagedResult.HasNextPage ? "border-slate-200 text-slate-600 hover:bg-slate-50 hover:text-slate-900" : "border-slate-100 text-slate-300 cursor-not-allowed") text-sm font-medium transition-colors">
154
  Next
155
  <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
@@ -165,14 +165,14 @@
165
  else if (_currentView == ViewMode.Details && _selectedUser != null)
166
  {
167
  <div class="animate-in fade-in slide-in-from-left-4 duration-500">
168
- <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
169
  <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>
170
  Back to Directory
171
  </button>
172
 
173
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
174
  <div class="lg:col-span-1">
175
- <div class="aspect-square w-full rounded-2xl bg-slate-900 flex flex-col items-center justify-center text-white p-6 shadow-xl">
176
  <div class="text-6xl font-black mb-4 tracking-tighter">
177
  @(_selectedUser.User.FullName?.Substring(0, Math.Min(2, _selectedUser.User.FullName.Length)).ToUpper() ?? "U")
178
  </div>
@@ -185,7 +185,7 @@ else if (_currentView == ViewMode.Details && _selectedUser != null)
185
 
186
  <div class="lg:col-span-2 space-y-8">
187
  <div>
188
- <h1 class="text-4xl font-extrabold text-slate-900 tracking-tight mb-2">Member Profile</h1>
189
  <div class="flex items-center gap-3">
190
  @if (_selectedUser.User.BanStatus == true)
191
  {
@@ -198,31 +198,31 @@ else if (_currentView == ViewMode.Details && _selectedUser != null)
198
  </div>
199
  </div>
200
 
201
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-8 border-y border-slate-100 py-6">
202
  <div>
203
- <h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Email</h4>
204
- <p class="text-sm font-semibold text-slate-900">@_selectedUser.User.Email</p>
205
  </div>
206
  <div>
207
- <h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Phone</h4>
208
- <p class="text-sm font-semibold text-slate-900">@(string.IsNullOrEmpty(_selectedUser.User.PhoneNumber) ? "-" : _selectedUser.User.PhoneNumber)</p>
209
  </div>
210
  <div>
211
- <h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Student ID</h4>
212
- <p class="text-sm font-semibold text-slate-900">@(string.IsNullOrEmpty(_selectedUser.User.StudentId) ? "-" : _selectedUser.User.StudentId)</p>
213
  </div>
214
  <div>
215
- <h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Joined</h4>
216
- <p class="text-sm font-semibold text-slate-900">@_selectedUser.User.CreatedAt.ToString("MMM dd, yyyy")</p>
217
  </div>
218
  <div class="sm:col-span-2">
219
- <h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Address</h4>
220
- <p class="text-sm font-semibold text-slate-900">@(string.IsNullOrEmpty(_selectedUser.User.Address) ? "-" : _selectedUser.User.Address)</p>
221
  </div>
222
  </div>
223
 
224
  <div class="flex items-center gap-4 pt-4 flex-wrap">
225
- <button @onclick="() => ShowEdit(_selectedUser.User.Id)" class="btn-primary py-3 px-8 text-base shadow-lg shadow-slate-200">Edit Details</button>
226
 
227
  <button @onclick="UpdateRole" class="btn-secondary py-3 px-8 text-base">
228
  Make @(_selectedUser.User.Role == "Admin" ? "Member" : "Admin")
@@ -234,8 +234,8 @@ else if (_currentView == ViewMode.Details && _selectedUser != null)
234
  </div>
235
 
236
  <!-- Subscription Management -->
237
- <div class="mt-8 pt-8 border-t border-slate-100">
238
- <h3 class="text-2xl font-bold text-slate-900 mb-6">Manage Subscription</h3>
239
 
240
  @if (_selectedUser.CurrentSubscription != null && _selectedUser.CurrentSubscription.IsActive)
241
  {
@@ -254,23 +254,23 @@ else if (_currentView == ViewMode.Details && _selectedUser != null)
254
  }
255
  else
256
  {
257
- <div class="bg-slate-50 border border-slate-200 rounded-xl p-4 mb-6">
258
- <p class="text-slate-600 font-medium">No active subscription for this member.</p>
259
  </div>
260
  }
261
 
262
- <h4 class="text-lg font-semibold text-slate-900 mb-4">Assign New Plan</h4>
263
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
264
  @foreach (var plan in _selectedUser.AvailableMemberships)
265
  {
266
  var isCurrent = _selectedUser.CurrentSubscription?.IsActive == true && _selectedUser.CurrentSubscription.MembershipId == plan.Id;
267
- <div class="card p-4 relative @(isCurrent ? "ring-2 ring-slate-900 border-slate-900" : "")">
268
  @if (isCurrent)
269
  {
270
- <div class="absolute top-0 right-0 -mt-2 -mr-2 bg-slate-900 text-white text-xs font-bold px-2 py-1 rounded-md">Current</div>
271
  }
272
- <h5 class="font-bold text-slate-900 text-lg mb-1">@plan.Type</h5>
273
- <p class="text-slate-500 font-medium text-sm mb-4">@plan.Price.ToString("N0") MMK / @plan.DurationMonths mo</p>
274
 
275
  @if (!isCurrent)
276
  {
@@ -291,15 +291,15 @@ else if (_currentView == ViewMode.Details && _selectedUser != null)
291
  else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
292
  {
293
  <div class="animate-in fade-in slide-in-from-bottom-4 duration-500">
294
- <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
295
  <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>
296
  Back to Directory
297
  </button>
298
 
299
  <div class="max-w-3xl mx-auto">
300
  <div class="mb-8">
301
- <h1 class="text-3xl font-bold tracking-tight text-slate-900">@(_currentView == ViewMode.Create ? "Add New Member" : "Edit Member Details")</h1>
302
- <p class="mt-2 text-slate-500">@(_currentView == ViewMode.Create ? "Register a new member to the library system." : $"Update details for {_editUser?.FullName}.")</p>
303
  </div>
304
 
305
  <div class="card p-6 sm:p-8">
@@ -309,46 +309,46 @@ else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
309
  <DataAnnotationsValidator />
310
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
311
  <div class="sm:col-span-2">
312
- <label class="block text-sm font-medium text-slate-700">Full Name <span class="text-rose-500">*</span></label>
313
  <div class="mt-1">
314
- <InputText @bind-Value="_createRequest.FullName" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="John Doe" />
315
  <ValidationMessage For="@(() => _createRequest.FullName)" class="mt-1 text-xs text-rose-500" />
316
  </div>
317
  </div>
318
- <div>
319
- <label class="block text-sm font-medium text-slate-700">Email Address <span class="text-rose-500">*</span></label>
320
  <div class="mt-1">
321
- <InputText @bind-Value="_createRequest.Email" type="email" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="you@domain.com" />
322
  <ValidationMessage For="@(() => _createRequest.Email)" class="mt-1 text-xs text-rose-500" />
323
  </div>
324
  </div>
325
  <div>
326
- <label class="block text-sm font-medium text-slate-700">Password <span class="text-rose-500">*</span></label>
327
  <div class="mt-1">
328
- <InputText @bind-Value="_createRequest.Password" type="password" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" />
329
  <ValidationMessage For="@(() => _createRequest.Password)" class="mt-1 text-xs text-rose-500" />
330
  </div>
331
  </div>
332
  <div>
333
- <label class="block text-sm font-medium text-slate-700">Phone Number</label>
334
  <div class="mt-1">
335
- <InputText @bind-Value="_createRequest.PhoneNumber" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="+123456789" />
336
  </div>
337
  </div>
338
  <div>
339
- <label class="block text-sm font-medium text-slate-700">Student ID</label>
340
  <div class="mt-1">
341
- <InputText @bind-Value="_createRequest.StudentId" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Optional" />
342
  </div>
343
  </div>
344
  <div class="sm:col-span-2">
345
- <label class="block text-sm font-medium text-slate-700">Address</label>
346
  <div class="mt-1">
347
- <InputTextArea @bind-Value="_createRequest.Address" rows="3" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Full address details" />
348
  </div>
349
  </div>
350
  </div>
351
- <div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
352
  <button type="button" @onclick="BackToList" class="btn-secondary">Cancel</button>
353
  <button type="submit" class="btn-primary">Create Member</button>
354
  </div>
@@ -360,32 +360,32 @@ else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
360
  <DataAnnotationsValidator />
361
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
362
  <div class="sm:col-span-2">
363
- <label class="block text-sm font-medium text-slate-700">Full Name <span class="text-rose-500">*</span></label>
364
  <div class="mt-1">
365
- <InputText @bind-Value="_updateRequest.FullName" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" />
366
  <ValidationMessage For="@(() => _updateRequest.FullName)" class="mt-1 text-xs text-rose-500" />
367
  </div>
368
  </div>
369
  <div>
370
- <label class="block text-sm font-medium text-slate-700">Phone Number</label>
371
  <div class="mt-1">
372
- <InputText @bind-Value="_updateRequest.PhoneNumber" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" />
373
  </div>
374
  </div>
375
  <div>
376
- <label class="block text-sm font-medium text-slate-700">Student ID</label>
377
  <div class="mt-1">
378
- <InputText @bind-Value="_updateRequest.StudentId" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" />
379
  </div>
380
  </div>
381
  <div class="sm:col-span-2">
382
- <label class="block text-sm font-medium text-slate-700">Address</label>
383
  <div class="mt-1">
384
- <InputTextArea @bind-Value="_updateRequest.Address" rows="3" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" />
385
  </div>
386
  </div>
387
  </div>
388
- <div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
389
  <button type="button" @onclick="BackToList" class="btn-secondary">Cancel</button>
390
  <button type="submit" class="btn-primary">Update Member</button>
391
  </div>
 
14
  {
15
  <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
16
  <div>
17
+ <h1 class="text-3xl font-bold tracking-tight text-mystic-900">Members Directory</h1>
18
+ <p class="mt-2 text-mystic-500">
19
  Manage library members and administrator accounts.
20
+ <span class="ml-2 text-xs text-mystic-400">(@(_allUsers?.Count() ?? 0) total)</span>
21
  </p>
22
  </div>
23
  <div class="flex items-center gap-3">
 
31
  @if (_isLoading)
32
  {
33
  <div class="flex items-center justify-center min-h-[400px]">
34
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mystic-900"></div>
35
  </div>
36
  }
37
  else if (_pagedResult != null)
38
  {
39
  <div class="card overflow-hidden animate-in fade-in duration-700">
40
  <div class="overflow-x-auto">
41
+ <table class="min-w-full divide-y divide-mystic-200">
42
+ <thead class="bg-mystic-50">
43
  <tr>
44
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Member</th>
45
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Contact</th>
46
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Role</th>
47
+ <th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Status</th>
48
  <th scope="col" class="relative px-6 py-3"><span class="sr-only">Actions</span></th>
49
  </tr>
50
  </thead>
51
+ <tbody class="bg-white divide-y divide-mystic-200">
52
  @foreach (var user in _pagedResult.Items)
53
  {
54
+ <tr class="hover:bg-mystic-50 transition-colors">
55
  <td class="px-6 py-4 whitespace-nowrap">
56
  <div class="flex items-center">
57
+ <div class="h-10 w-10 flex-shrink-0 rounded-full bg-mystic-900 flex items-center justify-center text-white font-bold text-sm">
58
  @(user.FullName?.Substring(0, Math.Min(2, user.FullName.Length)).ToUpper() ?? "U")
59
  </div>
60
  <div class="ml-4">
61
+ <div class="text-sm font-medium text-mystic-900">
62
  <button @onclick="() => ShowDetails(user.Id)" class="hover:underline text-left">@user.FullName</button>
63
  </div>
64
  @if (!string.IsNullOrEmpty(user.StudentId))
65
  {
66
+ <div class="text-xs text-mystic-500">ID: @user.StudentId</div>
67
  }
68
  </div>
69
  </div>
70
  </td>
71
  <td class="px-6 py-4 whitespace-nowrap">
72
+ <div class="text-sm text-mystic-900">@user.Email</div>
73
+ <div class="text-xs text-mystic-500">@(string.IsNullOrEmpty(user.PhoneNumber) ? "No phone" : user.PhoneNumber)</div>
74
  </td>
75
  <td class="px-6 py-4 whitespace-nowrap">
76
  <span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 @(user.Role == "Admin" ? "bg-amber-100 text-amber-800" : "bg-blue-100 text-blue-800")">
 
92
  }
93
  else
94
  {
95
+ <span class="inline-flex rounded-full bg-mystic-100 px-2 text-xs font-semibold leading-5 text-mystic-800">
96
  Inactive
97
  </span>
98
  }
99
  </td>
100
  <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
101
  <div class="flex items-center justify-end gap-3">
102
+ <button @onclick="() => ShowDetails(user.Id)" class="text-mystic-600 hover:text-mystic-900 transition-colors">Details</button>
103
  <button @onclick="() => ShowEdit(user.Id)" class="text-indigo-600 hover:text-indigo-900 transition-colors">Edit</button>
104
  </div>
105
  </td>
 
112
  @* Pagination *@
113
  @if (_pagedResult.TotalPages > 1)
114
  {
115
+ <div class="px-6 py-4 border-t border-mystic-200 flex items-center justify-between bg-white">
116
+ <p class="text-sm text-mystic-500">
117
+ Showing <span class="font-medium text-mystic-700">@((_pagedResult.CurrentPage - 1) * _pagedResult.PageSize + 1)</span>
118
 
119
+ <span class="font-medium text-mystic-700">@(Math.Min(_pagedResult.CurrentPage * _pagedResult.PageSize, _pagedResult.TotalCount))</span>
120
+ of <span class="font-medium text-mystic-700">@_pagedResult.TotalCount</span> members
121
  </p>
122
  <nav class="flex items-center gap-1" aria-label="Pagination">
123
  @* Previous *@
124
  <button @onclick="() => ChangePage(_pagedResult.CurrentPage - 1)" disabled="@(!_pagedResult.HasPreviousPage)"
125
+ class="inline-flex items-center px-3 py-1.5 rounded-md border @(_pagedResult.HasPreviousPage ? "border-mystic-200 text-mystic-600 hover:bg-mystic-50 hover:text-mystic-900" : "border-mystic-100 text-mystic-300 cursor-not-allowed") text-sm font-medium transition-colors">
126
  <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
128
  </svg>
 
135
  var pageNumber = p;
136
  if (p == _pagedResult.CurrentPage)
137
  {
138
+ <span class="inline-flex items-center justify-center w-8 h-8 rounded-md bg-mystic-900 text-white text-sm font-semibold">@p</span>
139
  }
140
  else if (p == 1 || p == _pagedResult.TotalPages || (p >= _pagedResult.CurrentPage - 2 && p <= _pagedResult.CurrentPage + 2))
141
  {
142
  <button @onclick="() => ChangePage(pageNumber)"
143
+ class="inline-flex items-center justify-center w-8 h-8 rounded-md border border-mystic-200 text-sm font-medium text-mystic-600 hover:bg-mystic-50 hover:text-mystic-900 transition-colors">@p</button>
144
  }
145
  else if (p == _pagedResult.CurrentPage - 3 || p == _pagedResult.CurrentPage + 3)
146
  {
147
+ <span class="inline-flex items-center justify-center w-8 h-8 text-sm text-mystic-400">…</span>
148
  }
149
  }
150
 
151
  @* Next *@
152
  <button @onclick="() => ChangePage(_pagedResult.CurrentPage + 1)" disabled="@(!_pagedResult.HasNextPage)"
153
+ class="inline-flex items-center px-3 py-1.5 rounded-md border @(_pagedResult.HasNextPage ? "border-mystic-200 text-mystic-600 hover:bg-mystic-50 hover:text-mystic-900" : "border-mystic-100 text-mystic-300 cursor-not-allowed") text-sm font-medium transition-colors">
154
  Next
155
  <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
156
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
 
165
  else if (_currentView == ViewMode.Details && _selectedUser != null)
166
  {
167
  <div class="animate-in fade-in slide-in-from-left-4 duration-500">
168
+ <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-mystic-500 hover:text-mystic-900 mb-6 group">
169
  <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>
170
  Back to Directory
171
  </button>
172
 
173
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
174
  <div class="lg:col-span-1">
175
+ <div class="aspect-square w-full rounded-2xl bg-mystic-900 flex flex-col items-center justify-center text-white p-6 shadow-xl">
176
  <div class="text-6xl font-black mb-4 tracking-tighter">
177
  @(_selectedUser.User.FullName?.Substring(0, Math.Min(2, _selectedUser.User.FullName.Length)).ToUpper() ?? "U")
178
  </div>
 
185
 
186
  <div class="lg:col-span-2 space-y-8">
187
  <div>
188
+ <h1 class="text-4xl font-extrabold text-mystic-900 tracking-tight mb-2">Member Profile</h1>
189
  <div class="flex items-center gap-3">
190
  @if (_selectedUser.User.BanStatus == true)
191
  {
 
198
  </div>
199
  </div>
200
 
201
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-8 border-y border-mystic-100 py-6">
202
  <div>
203
+ <h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Email</h4>
204
+ <p class="text-sm font-semibold text-mystic-900">@_selectedUser.User.Email</p>
205
  </div>
206
  <div>
207
+ <h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Phone</h4>
208
+ <p class="text-sm font-semibold text-mystic-900">@(string.IsNullOrEmpty(_selectedUser.User.PhoneNumber) ? "-" : _selectedUser.User.PhoneNumber)</p>
209
  </div>
210
  <div>
211
+ <h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Student ID</h4>
212
+ <p class="text-sm font-semibold text-mystic-900">@(string.IsNullOrEmpty(_selectedUser.User.StudentId) ? "-" : _selectedUser.User.StudentId)</p>
213
  </div>
214
  <div>
215
+ <h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Joined</h4>
216
+ <p class="text-sm font-semibold text-mystic-900">@_selectedUser.User.CreatedAt.ToString("MMM dd, yyyy")</p>
217
  </div>
218
  <div class="sm:col-span-2">
219
+ <h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Address</h4>
220
+ <p class="text-sm font-semibold text-mystic-900">@(string.IsNullOrEmpty(_selectedUser.User.Address) ? "-" : _selectedUser.User.Address)</p>
221
  </div>
222
  </div>
223
 
224
  <div class="flex items-center gap-4 pt-4 flex-wrap">
225
+ <button @onclick="() => ShowEdit(_selectedUser.User.Id)" class="btn-primary py-3 px-8 text-base shadow-lg shadow-mystic-200">Edit Details</button>
226
 
227
  <button @onclick="UpdateRole" class="btn-secondary py-3 px-8 text-base">
228
  Make @(_selectedUser.User.Role == "Admin" ? "Member" : "Admin")
 
234
  </div>
235
 
236
  <!-- Subscription Management -->
237
+ <div class="mt-8 pt-8 border-t border-mystic-100">
238
+ <h3 class="text-2xl font-bold text-mystic-900 mb-6">Manage Subscription</h3>
239
 
240
  @if (_selectedUser.CurrentSubscription != null && _selectedUser.CurrentSubscription.IsActive)
241
  {
 
254
  }
255
  else
256
  {
257
+ <div class="bg-mystic-50 border border-mystic-200 rounded-xl p-4 mb-6">
258
+ <p class="text-mystic-600 font-medium">No active subscription for this member.</p>
259
  </div>
260
  }
261
 
262
+ <h4 class="text-lg font-semibold text-mystic-900 mb-4">Assign New Plan</h4>
263
  <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
264
  @foreach (var plan in _selectedUser.AvailableMemberships)
265
  {
266
  var isCurrent = _selectedUser.CurrentSubscription?.IsActive == true && _selectedUser.CurrentSubscription.MembershipId == plan.Id;
267
+ <div class="card p-4 relative @(isCurrent ? "ring-2 ring-mystic-900 border-mystic-900" : "")">
268
  @if (isCurrent)
269
  {
270
+ <div class="absolute top-0 right-0 -mt-2 -mr-2 bg-mystic-900 text-white text-xs font-bold px-2 py-1 rounded-md">Current</div>
271
  }
272
+ <h5 class="font-bold text-mystic-900 text-lg mb-1">@plan.Type</h5>
273
+ <p class="text-mystic-500 font-medium text-sm mb-4">@plan.Price.ToString("N0") MMK / @plan.DurationMonths mo</p>
274
 
275
  @if (!isCurrent)
276
  {
 
291
  else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
292
  {
293
  <div class="animate-in fade-in slide-in-from-bottom-4 duration-500">
294
+ <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-mystic-500 hover:text-mystic-900 mb-6 group">
295
  <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>
296
  Back to Directory
297
  </button>
298
 
299
  <div class="max-w-3xl mx-auto">
300
  <div class="mb-8">
301
+ <h1 class="text-3xl font-bold tracking-tight text-mystic-900">@(_currentView == ViewMode.Create ? "Add New Member" : "Edit Member Details")</h1>
302
+ <p class="mt-2 text-mystic-500">@(_currentView == ViewMode.Create ? "Register a new member to the library system." : $"Update details for {_editUser?.FullName}.")</p>
303
  </div>
304
 
305
  <div class="card p-6 sm:p-8">
 
309
  <DataAnnotationsValidator />
310
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
311
  <div class="sm:col-span-2">
312
+ <label class="block text-sm font-medium text-mystic-700">Full Name <span class="text-rose-500">*</span></label>
313
  <div class="mt-1">
314
+ <InputText @bind-Value="_createRequest.FullName" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="John Doe" />
315
  <ValidationMessage For="@(() => _createRequest.FullName)" class="mt-1 text-xs text-rose-500" />
316
  </div>
317
  </div>
318
+ <div>
319
+ <label class="block text-sm font-medium text-mystic-700">Email Address <span class="text-rose-500">*</span></label>
320
  <div class="mt-1">
321
+ <InputText @bind-Value="_createRequest.Email" type="email" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="you@domain.com" />
322
  <ValidationMessage For="@(() => _createRequest.Email)" class="mt-1 text-xs text-rose-500" />
323
  </div>
324
  </div>
325
  <div>
326
+ <label class="block text-sm font-medium text-mystic-700">Password <span class="text-rose-500">*</span></label>
327
  <div class="mt-1">
328
+ <InputText @bind-Value="_createRequest.Password" type="password" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
329
  <ValidationMessage For="@(() => _createRequest.Password)" class="mt-1 text-xs text-rose-500" />
330
  </div>
331
  </div>
332
  <div>
333
+ <label class="block text-sm font-medium text-mystic-700">Phone Number</label>
334
  <div class="mt-1">
335
+ <InputText @bind-Value="_createRequest.PhoneNumber" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="+123456789" />
336
  </div>
337
  </div>
338
  <div>
339
+ <label class="block text-sm font-medium text-mystic-700">Student ID</label>
340
  <div class="mt-1">
341
+ <InputText @bind-Value="_createRequest.StudentId" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Optional" />
342
  </div>
343
  </div>
344
  <div class="sm:col-span-2">
345
+ <label class="block text-sm font-medium text-mystic-700">Address</label>
346
  <div class="mt-1">
347
+ <InputTextArea @bind-Value="_createRequest.Address" rows="3" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Full address details" />
348
  </div>
349
  </div>
350
  </div>
351
+ <div class="pt-5 border-t border-mystic-100 flex justify-end gap-3">
352
  <button type="button" @onclick="BackToList" class="btn-secondary">Cancel</button>
353
  <button type="submit" class="btn-primary">Create Member</button>
354
  </div>
 
360
  <DataAnnotationsValidator />
361
  <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
362
  <div class="sm:col-span-2">
363
+ <label class="block text-sm font-medium text-mystic-700">Full Name <span class="text-rose-500">*</span></label>
364
  <div class="mt-1">
365
+ <InputText @bind-Value="_updateRequest.FullName" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
366
  <ValidationMessage For="@(() => _updateRequest.FullName)" class="mt-1 text-xs text-rose-500" />
367
  </div>
368
  </div>
369
  <div>
370
+ <label class="block text-sm font-medium text-mystic-700">Phone Number</label>
371
  <div class="mt-1">
372
+ <InputText @bind-Value="_updateRequest.PhoneNumber" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
373
  </div>
374
  </div>
375
  <div>
376
+ <label class="block text-sm font-medium text-mystic-700">Student ID</label>
377
  <div class="mt-1">
378
+ <InputText @bind-Value="_updateRequest.StudentId" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
379
  </div>
380
  </div>
381
  <div class="sm:col-span-2">
382
+ <label class="block text-sm font-medium text-mystic-700">Address</label>
383
  <div class="mt-1">
384
+ <InputTextArea @bind-Value="_updateRequest.Address" rows="3" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
385
  </div>
386
  </div>
387
  </div>
388
+ <div class="pt-5 border-t border-mystic-100 flex justify-end gap-3">
389
  <button type="button" @onclick="BackToList" class="btn-secondary">Cancel</button>
390
  <button type="submit" class="btn-primary">Update Member</button>
391
  </div>
BlazorFrontend/Components/Pages/Membership.razor CHANGED
@@ -11,7 +11,7 @@
11
  @if (_isLoading)
12
  {
13
  <div class="flex items-center justify-center min-h-[400px]">
14
- <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-slate-900"></div>
15
  </div>
16
  }
17
  else if (_viewModel != null)
@@ -20,21 +20,21 @@ else if (_viewModel != null)
20
 
21
  <div class="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
22
  <!-- Header with Loyalty Summary -->
23
- <div class="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-white p-8 rounded-2xl shadow-sm border border-slate-100">
24
  <div>
25
- <h1 class="text-3xl font-extrabold text-slate-900 tracking-tight">Membership</h1>
26
- <p class="text-slate-500 mt-1">Manage your subscription and library benefits</p>
27
  </div>
28
 
29
  @if (_viewModel.LoyaltyAccount != null)
30
  {
31
- <div class="flex items-center gap-4 bg-slate-50 px-6 py-4 rounded-xl border border-slate-200/60">
32
  <div class="bg-amber-100 p-2.5 rounded-lg text-amber-600">
33
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/></svg>
34
  </div>
35
  <div>
36
- <div class="text-xs font-bold text-slate-400 uppercase tracking-widest">Available Points</div>
37
- <div class="text-2xl font-black text-slate-900 leading-none">@_viewModel.LoyaltyAccount.CurrentBalance.ToString("N0")</div>
38
  </div>
39
  </div>
40
  }
@@ -51,7 +51,7 @@ else if (_viewModel != null)
51
 
52
  @if (hasActiveSub && _viewModel.CurrentSubscription != null)
53
  {
54
- <div class="relative overflow-hidden bg-slate-900 rounded-3xl p-8 text-white group shadow-xl">
55
  <!-- Decorative element -->
56
  <div class="absolute -right-20 -top-20 w-64 h-64 bg-white/5 rounded-full blur-3xl group-hover:bg-white/10 transition-colors duration-500"></div>
57
 
@@ -68,16 +68,16 @@ else if (_viewModel != null)
68
 
69
  <div class="grid grid-cols-2 gap-8 py-8 border-t border-white/10">
70
  <div>
71
- <div class="text-slate-400 text-sm font-medium mb-1">Start Date</div>
72
  <div class="text-lg font-bold">@_viewModel.CurrentSubscription.StartDate.ToString("MMM dd, yyyy")</div>
73
  </div>
74
  <div>
75
- <div class="text-slate-400 text-sm font-medium mb-1">Expiry Date</div>
76
  <div class="text-lg font-bold">@_viewModel.CurrentSubscription.ExpiryDate.ToString("MMM dd, yyyy")</div>
77
  </div>
78
  </div>
79
 
80
- <div class="pt-6 flex items-center gap-2 text-slate-400 text-sm italic">
81
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
82
  Pro-tip: Maintain your subscription to earn loyalty points every month!
83
  </div>
@@ -86,13 +86,13 @@ else if (_viewModel != null)
86
  }
87
  else
88
  {
89
- <div class="bg-white border-2 border-dashed border-slate-200 rounded-3xl p-12 text-center">
90
- <div class="bg-slate-50 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 text-slate-400">
91
  <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
92
  </div>
93
- <h3 class="text-xl font-bold text-slate-900">No Active Subscription</h3>
94
- <p class="text-slate-500 mt-2 mb-8 max-w-xs mx-auto">Get a membership to borrow books and unlock premium library features.</p>
95
- <a href="Membership#plans" class="btn-primary px-8 py-3 rounded-xl shadow-lg shadow-slate-900/10">Browse Plans</a>
96
  </div>
97
  }
98
  </section>
@@ -101,7 +101,7 @@ else if (_viewModel != null)
101
  @if (_viewModel.QueuedMemberships.Any())
102
  {
103
  <section>
104
- <h2 class="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
105
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-amber-500"><path d="M10 10 2.5 2.5"/><path d="M21.5 2.5 14 10"/><path d="m21.5 21.5-7.5-7.5"/><path d="M10 14 2.5 21.5"/><circle cx="12" cy="12" r="3"/></svg>
106
  Queued Memberships (@_viewModel.QueuedMemberships.Count)
107
  </h2>
@@ -114,15 +114,15 @@ else if (_viewModel != null)
114
  <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
115
  </div>
116
  <div>
117
- <div class="font-bold text-slate-900">@queued.MembershipType Membership</div>
118
- <div class="text-sm text-slate-500">
119
  Activates on @queued.StartDate.ToString("MMM dd, yyyy") &bull; Expires @queued.ExpiryDate.ToString("MMM dd, yyyy")
120
  </div>
121
  </div>
122
  </div>
123
  <div class="text-right">
124
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700 border border-amber-200 uppercase tracking-wider">Queued</span>
125
- <div class="text-xs text-slate-400 mt-1">Will activate automatically</div>
126
  </div>
127
  </div>
128
  }
@@ -133,8 +133,8 @@ else if (_viewModel != null)
133
 
134
  <!-- Right Side Panel: Benefits -->
135
  <div class="space-y-8">
136
- <div class="bg-white border border-slate-100 rounded-3xl p-8 shadow-sm">
137
- <h3 class="text-lg font-bold text-slate-900 mb-6">Your Benefits</h3>
138
  <ul class="space-y-6">
139
  @{
140
  var currentPlan = _viewModel.AvailableMemberships.FirstOrDefault(m => hasActiveSub && _viewModel.CurrentSubscription != null && m.Type == _viewModel.CurrentSubscription.MembershipType);
@@ -142,15 +142,15 @@ else if (_viewModel != null)
142
  <li class="flex gap-4">
143
  <div class="shrink-0 h-10 w-10 bg-indigo-50 text-indigo-600 rounded-xl flex items-center justify-center font-bold">@((currentPlan?.MaxBooks ?? 1))</div>
144
  <div>
145
- <div class="font-bold text-slate-900">Max Books</div>
146
- <div class="text-sm text-slate-500">Number of books you can borrow simultaneously.</div>
147
  </div>
148
  </li>
149
  <li class="flex gap-4">
150
  <div class="shrink-0 h-10 w-10 bg-emerald-50 text-emerald-600 rounded-xl flex items-center justify-center font-bold">@((currentPlan?.BorrowingDays ?? 14))</div>
151
  <div>
152
- <div class="font-bold text-slate-900">Borrowing Days</div>
153
- <div class="text-sm text-slate-500">Duration you can keep each borrowed book.</div>
154
  </div>
155
  </li>
156
  <li class="flex gap-4">
@@ -158,13 +158,13 @@ else if (_viewModel != null)
158
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
159
  </div>
160
  <div>
161
- <div class="font-bold text-slate-900">Digital Access</div>
162
- <div class="text-sm text-slate-500">Browse and search the full library catalog online.</div>
163
  </div>
164
  </li>
165
  </ul>
166
 
167
- <div class="mt-10 p-5 bg-slate-900 rounded-2xl text-white text-center">
168
  <p class="text-sm font-medium mb-3">Want more books?</p>
169
  <a href="Membership#plans" class="text-sm font-bold text-indigo-400 hover:text-indigo-300 transition-colors">Upgrade Plan &rarr;</a>
170
  </div>
@@ -175,8 +175,8 @@ else if (_viewModel != null)
175
  <!-- Explore All Plans Section -->
176
  <section id="plans" class="pt-10">
177
  <div class="text-center mb-10">
178
- <h2 class="text-3xl font-black text-slate-900">Explore Membership Plans</h2>
179
- <p class="text-slate-500 mt-2">Find the perfect library experience for your reading habits</p>
180
  </div>
181
 
182
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
@@ -184,33 +184,33 @@ else if (_viewModel != null)
184
  {
185
  var isCurrent = hasActiveSub && _viewModel.CurrentSubscription != null && plan.Type == _viewModel.CurrentSubscription.MembershipType;
186
 
187
- <div class="group relative bg-white border @(isCurrent ? "border-slate-900 ring-2 ring-slate-900 ring-offset-2" : "border-slate-200") rounded-3xl p-8 transition-all duration-300 hover:shadow-2xl hover:-translate-y-2">
188
  @if (isCurrent)
189
  {
190
- <div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-slate-900 text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest shadow-xl">Current Plan</div>
191
  }
192
 
193
  <div class="mb-8">
194
- <h3 class="text-sm font-black text-slate-400 uppercase tracking-widest mb-2">@plan.Type</h3>
195
  <div class="flex items-baseline">
196
- <span class="text-3xl font-black text-slate-900">@plan.Price.ToString("N0")</span>
197
- <span class="text-slate-500 font-bold ml-1 text-sm uppercase">MMK</span>
198
  </div>
199
- <div class="text-slate-400 text-xs font-bold mt-1">/ @(plan.DurationMonths == 1 ? "Month" : $"{plan.DurationMonths} Months")</div>
200
  </div>
201
 
202
  <ul class="space-y-4 mb-10">
203
  <li class="flex items-center gap-3 text-sm">
204
- <div class="h-5 w-5 rounded-full bg-slate-100 flex items-center justify-center text-slate-900">
205
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
206
  </div>
207
- <span class="text-slate-600 font-bold">@plan.MaxBooks Books limit</span>
208
  </li>
209
  <li class="flex items-center gap-3 text-sm">
210
- <div class="h-5 w-5 rounded-full bg-slate-100 flex items-center justify-center text-slate-900">
211
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
212
  </div>
213
- <span class="text-slate-600 font-bold">@plan.BorrowingDays Days keep</span>
214
  </li>
215
  @if (!string.IsNullOrEmpty(plan.RewardId) && _viewModel.RewardPointCosts.TryGetValue(plan.RewardId, out var pointCost))
216
  {
@@ -225,17 +225,17 @@ else if (_viewModel != null)
225
 
226
  @if (isCurrent)
227
  {
228
- <button disabled class="w-full py-4 rounded-2xl bg-slate-100 text-slate-400 font-bold text-sm cursor-not-allowed">Active Now</button>
229
  }
230
  else
231
  {
232
  <div class="space-y-3">
233
- <a href="Borrowings" class="block w-full py-4 rounded-2xl bg-white border-2 border-slate-900 text-slate-900 text-center font-bold text-sm transition-all duration-200 hover:bg-slate-900 hover:text-white">Pay Cash</a>
234
 
235
  @if (!string.IsNullOrEmpty(plan.RewardId) && _viewModel.RewardPointCosts.ContainsKey(plan.RewardId))
236
  {
237
  <button @onclick="() => RedeemMembership(plan.RewardId, plan.Type, _viewModel.RewardPointCosts[plan.RewardId])"
238
- class="w-full py-4 rounded-2xl bg-amber-400 text-slate-900 font-black text-sm hover:bg-amber-500 transition-all shadow-lg shadow-amber-400/20">
239
  Redeem with Points
240
  </button>
241
  }
 
11
  @if (_isLoading)
12
  {
13
  <div class="flex items-center justify-center min-h-[400px]">
14
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mystic-900"></div>
15
  </div>
16
  }
17
  else if (_viewModel != null)
 
20
 
21
  <div class="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700">
22
  <!-- Header with Loyalty Summary -->
23
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-white p-8 rounded-2xl shadow-sm border border-mystic-100">
24
  <div>
25
+ <h1 class="text-3xl font-extrabold text-mystic-900 tracking-tight">Membership</h1>
26
+ <p class="text-mystic-500 mt-1">Manage your subscription and library benefits</p>
27
  </div>
28
 
29
  @if (_viewModel.LoyaltyAccount != null)
30
  {
31
+ <div class="flex items-center gap-4 bg-mystic-50 px-6 py-4 rounded-xl border border-mystic-200/60">
32
  <div class="bg-amber-100 p-2.5 rounded-lg text-amber-600">
33
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/><path d="M7 6h1v4"/><path d="m16.71 13.88.7.71-2.82 2.82"/></svg>
34
  </div>
35
  <div>
36
+ <div class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Available Points</div>
37
+ <div class="text-2xl font-black text-mystic-900 leading-none">@_viewModel.LoyaltyAccount.CurrentBalance.ToString("N0")</div>
38
  </div>
39
  </div>
40
  }
 
51
 
52
  @if (hasActiveSub && _viewModel.CurrentSubscription != null)
53
  {
54
+ <div class="relative overflow-hidden bg-mystic-900 rounded-3xl p-8 text-white group shadow-xl">
55
  <!-- Decorative element -->
56
  <div class="absolute -right-20 -top-20 w-64 h-64 bg-white/5 rounded-full blur-3xl group-hover:bg-white/10 transition-colors duration-500"></div>
57
 
 
68
 
69
  <div class="grid grid-cols-2 gap-8 py-8 border-t border-white/10">
70
  <div>
71
+ <div class="text-mystic-400 text-sm font-medium mb-1">Start Date</div>
72
  <div class="text-lg font-bold">@_viewModel.CurrentSubscription.StartDate.ToString("MMM dd, yyyy")</div>
73
  </div>
74
  <div>
75
+ <div class="text-mystic-400 text-sm font-medium mb-1">Expiry Date</div>
76
  <div class="text-lg font-bold">@_viewModel.CurrentSubscription.ExpiryDate.ToString("MMM dd, yyyy")</div>
77
  </div>
78
  </div>
79
 
80
+ <div class="pt-6 flex items-center gap-2 text-mystic-400 text-sm italic">
81
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
82
  Pro-tip: Maintain your subscription to earn loyalty points every month!
83
  </div>
 
86
  }
87
  else
88
  {
89
+ <div class="bg-white border-2 border-dashed border-mystic-200 rounded-3xl p-12 text-center">
90
+ <div class="bg-mystic-50 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 text-mystic-400">
91
  <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
92
  </div>
93
+ <h3 class="text-xl font-bold text-mystic-900">No Active Subscription</h3>
94
+ <p class="text-mystic-500 mt-2 mb-8 max-w-xs mx-auto">Get a membership to borrow books and unlock premium library features.</p>
95
+ <a href="Membership#plans" class="btn-primary px-8 py-3 rounded-xl shadow-lg shadow-mystic-900/10">Browse Plans</a>
96
  </div>
97
  }
98
  </section>
 
101
  @if (_viewModel.QueuedMemberships.Any())
102
  {
103
  <section>
104
+ <h2 class="text-xl font-bold text-mystic-900 mb-4 flex items-center gap-2">
105
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-amber-500"><path d="M10 10 2.5 2.5"/><path d="M21.5 2.5 14 10"/><path d="m21.5 21.5-7.5-7.5"/><path d="M10 14 2.5 21.5"/><circle cx="12" cy="12" r="3"/></svg>
106
  Queued Memberships (@_viewModel.QueuedMemberships.Count)
107
  </h2>
 
114
  <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
115
  </div>
116
  <div>
117
+ <div class="font-bold text-mystic-900">@queued.MembershipType Membership</div>
118
+ <div class="text-sm text-mystic-500">
119
  Activates on @queued.StartDate.ToString("MMM dd, yyyy") &bull; Expires @queued.ExpiryDate.ToString("MMM dd, yyyy")
120
  </div>
121
  </div>
122
  </div>
123
  <div class="text-right">
124
  <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700 border border-amber-200 uppercase tracking-wider">Queued</span>
125
+ <div class="text-xs text-mystic-400 mt-1">Will activate automatically</div>
126
  </div>
127
  </div>
128
  }
 
133
 
134
  <!-- Right Side Panel: Benefits -->
135
  <div class="space-y-8">
136
+ <div class="bg-white border border-mystic-100 rounded-3xl p-8 shadow-sm">
137
+ <h3 class="text-lg font-bold text-mystic-900 mb-6">Your Benefits</h3>
138
  <ul class="space-y-6">
139
  @{
140
  var currentPlan = _viewModel.AvailableMemberships.FirstOrDefault(m => hasActiveSub && _viewModel.CurrentSubscription != null && m.Type == _viewModel.CurrentSubscription.MembershipType);
 
142
  <li class="flex gap-4">
143
  <div class="shrink-0 h-10 w-10 bg-indigo-50 text-indigo-600 rounded-xl flex items-center justify-center font-bold">@((currentPlan?.MaxBooks ?? 1))</div>
144
  <div>
145
+ <div class="font-bold text-mystic-900">Max Books</div>
146
+ <div class="text-sm text-mystic-500">Number of books you can borrow simultaneously.</div>
147
  </div>
148
  </li>
149
  <li class="flex gap-4">
150
  <div class="shrink-0 h-10 w-10 bg-emerald-50 text-emerald-600 rounded-xl flex items-center justify-center font-bold">@((currentPlan?.BorrowingDays ?? 14))</div>
151
  <div>
152
+ <div class="font-bold text-mystic-900">Borrowing Days</div>
153
+ <div class="text-sm text-mystic-500">Duration you can keep each borrowed book.</div>
154
  </div>
155
  </li>
156
  <li class="flex gap-4">
 
158
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>
159
  </div>
160
  <div>
161
+ <div class="font-bold text-mystic-900">Digital Access</div>
162
+ <div class="text-sm text-mystic-500">Browse and search the full library catalog online.</div>
163
  </div>
164
  </li>
165
  </ul>
166
 
167
+ <div class="mt-10 p-5 bg-mystic-900 rounded-2xl text-white text-center">
168
  <p class="text-sm font-medium mb-3">Want more books?</p>
169
  <a href="Membership#plans" class="text-sm font-bold text-indigo-400 hover:text-indigo-300 transition-colors">Upgrade Plan &rarr;</a>
170
  </div>
 
175
  <!-- Explore All Plans Section -->
176
  <section id="plans" class="pt-10">
177
  <div class="text-center mb-10">
178
+ <h2 class="text-3xl font-black text-mystic-900">Explore Membership Plans</h2>
179
+ <p class="text-mystic-500 mt-2">Find the perfect library experience for your reading habits</p>
180
  </div>
181
 
182
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
 
184
  {
185
  var isCurrent = hasActiveSub && _viewModel.CurrentSubscription != null && plan.Type == _viewModel.CurrentSubscription.MembershipType;
186
 
187
+ <div class="group relative bg-white border @(isCurrent ? "border-mystic-900 ring-2 ring-mystic-900 ring-offset-2" : "border-mystic-200") rounded-3xl p-8 transition-all duration-300 hover:shadow-2xl hover:-translate-y-2">
188
  @if (isCurrent)
189
  {
190
+ <div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-mystic-900 text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest shadow-xl">Current Plan</div>
191
  }
192
 
193
  <div class="mb-8">
194
+ <h3 class="text-sm font-black text-mystic-400 uppercase tracking-widest mb-2">@plan.Type</h3>
195
  <div class="flex items-baseline">
196
+ <span class="text-3xl font-black text-mystic-900">@plan.Price.ToString("N0")</span>
197
+ <span class="text-mystic-500 font-bold ml-1 text-sm uppercase">MMK</span>
198
  </div>
199
+ <div class="text-mystic-400 text-xs font-bold mt-1">/ @(plan.DurationMonths == 1 ? "Month" : $"{plan.DurationMonths} Months")</div>
200
  </div>
201
 
202
  <ul class="space-y-4 mb-10">
203
  <li class="flex items-center gap-3 text-sm">
204
+ <div class="h-5 w-5 rounded-full bg-mystic-100 flex items-center justify-center text-mystic-900">
205
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
206
  </div>
207
+ <span class="text-mystic-600 font-bold">@plan.MaxBooks Books limit</span>
208
  </li>
209
  <li class="flex items-center gap-3 text-sm">
210
+ <div class="h-5 w-5 rounded-full bg-mystic-100 flex items-center justify-center text-mystic-900">
211
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
212
  </div>
213
+ <span class="text-mystic-600 font-bold">@plan.BorrowingDays Days keep</span>
214
  </li>
215
  @if (!string.IsNullOrEmpty(plan.RewardId) && _viewModel.RewardPointCosts.TryGetValue(plan.RewardId, out var pointCost))
216
  {
 
225
 
226
  @if (isCurrent)
227
  {
228
+ <button disabled class="w-full py-4 rounded-2xl bg-mystic-100 text-mystic-400 font-bold text-sm cursor-not-allowed">Active Now</button>
229
  }
230
  else
231
  {
232
  <div class="space-y-3">
233
+ <a href="Borrowings" class="block w-full py-4 rounded-2xl bg-white border-2 border-mystic-900 text-mystic-900 text-center font-bold text-sm transition-all duration-200 hover:bg-mystic-900 hover:text-white">Pay Cash</a>
234
 
235
  @if (!string.IsNullOrEmpty(plan.RewardId) && _viewModel.RewardPointCosts.ContainsKey(plan.RewardId))
236
  {
237
  <button @onclick="() => RedeemMembership(plan.RewardId, plan.Type, _viewModel.RewardPointCosts[plan.RewardId])"
238
+ class="w-full py-4 rounded-2xl bg-amber-400 text-mystic-900 font-black text-sm hover:bg-amber-500 transition-all shadow-lg shadow-amber-400/20">
239
  Redeem with Points
240
  </button>
241
  }
BlazorFrontend/Components/Pages/Register.razor CHANGED
@@ -10,13 +10,13 @@
10
  <PageTitle>Register</PageTitle>
11
 
12
  <div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
13
- <div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-slate-200 shadow-xl">
14
  <div>
15
- <div class="mx-auto h-12 w-12 bg-slate-900 rounded-xl flex items-center justify-center text-white mb-4">
16
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
17
  </div>
18
- <h2 class="text-center text-3xl font-extrabold text-slate-900 tracking-tight">Join our Library</h2>
19
- <p class="mt-2 text-center text-sm text-slate-500">
20
  Start your reading journey today.
21
  </p>
22
  </div>
@@ -33,29 +33,29 @@
33
 
34
  <div class="space-y-4">
35
  <div>
36
- <label class="block text-sm font-medium text-slate-700 mb-1">Full Name</label>
37
- <InputText @bind-Value="_registerRequest.FullName" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="John Doe" />
38
  <ValidationMessage For="@(() => _registerRequest.FullName)" class="text-xs text-red-500 mt-1" />
39
  </div>
40
  <div>
41
- <label class="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
42
- <InputText type="email" @bind-Value="_registerRequest.Email" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="name@example.com" />
43
  <ValidationMessage For="@(() => _registerRequest.Email)" class="text-xs text-red-500 mt-1" />
44
  </div>
45
  <div>
46
- <label class="block text-sm font-medium text-slate-700 mb-1">Phone Number</label>
47
- <InputText type="tel" @bind-Value="_registerRequest.PhoneNumber" class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="09123456789" />
48
  <ValidationMessage For="@(() => _registerRequest.PhoneNumber)" class="text-xs text-red-500 mt-1" />
49
  </div>
50
  <div>
51
- <label class="block text-sm font-medium text-slate-700 mb-1">Password</label>
52
- <InputText type="password" @bind-Value="_registerRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="••••••••" />
53
  <ValidationMessage For="@(() => _registerRequest.Password)" class="text-xs text-red-500 mt-1" />
54
  </div>
55
  </div>
56
 
57
  <div>
58
- <button type="submit" disabled="@_isLoading" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-all disabled:opacity-50">
59
  @if (_isLoading)
60
  {
61
  <span>Creating account...</span>
@@ -68,8 +68,8 @@
68
  </div>
69
 
70
  <div class="text-center">
71
- <span class="text-sm text-slate-500">Already have an account?</span>
72
- <a href="/login" class="text-sm font-medium text-slate-900 hover:underline ml-1">Sign in</a>
73
  </div>
74
  </EditForm>
75
  </div>
 
10
  <PageTitle>Register</PageTitle>
11
 
12
  <div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
13
+ <div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-mystic-200 shadow-xl">
14
  <div>
15
+ <div class="mx-auto h-12 w-12 bg-mystic-900 rounded-xl flex items-center justify-center text-white mb-4">
16
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
17
  </div>
18
+ <h2 class="text-center text-3xl font-extrabold text-mystic-900 tracking-tight">Join our Library</h2>
19
+ <p class="mt-2 text-center text-sm text-mystic-500">
20
  Start your reading journey today.
21
  </p>
22
  </div>
 
33
 
34
  <div class="space-y-4">
35
  <div>
36
+ <label class="block text-sm font-medium text-mystic-700 mb-1">Full Name</label>
37
+ <InputText @bind-Value="_registerRequest.FullName" required class="appearance-none relative block w-full px-3 py-2 border border-mystic-300 placeholder-mystic-400 text-mystic-900 rounded-md focus:outline-none focus:ring-mystic-900 focus:border-mystic-900 focus:z-10 sm:text-sm" placeholder="John Doe" />
38
  <ValidationMessage For="@(() => _registerRequest.FullName)" class="text-xs text-red-500 mt-1" />
39
  </div>
40
  <div>
41
+ <label class="block text-sm font-medium text-mystic-700 mb-1">Email Address</label>
42
+ <InputText type="email" @bind-Value="_registerRequest.Email" required class="appearance-none relative block w-full px-3 py-2 border border-mystic-300 placeholder-mystic-400 text-mystic-900 rounded-md focus:outline-none focus:ring-mystic-900 focus:border-mystic-900 focus:z-10 sm:text-sm" placeholder="name@example.com" />
43
  <ValidationMessage For="@(() => _registerRequest.Email)" class="text-xs text-red-500 mt-1" />
44
  </div>
45
  <div>
46
+ <label class="block text-sm font-medium text-mystic-700 mb-1">Phone Number</label>
47
+ <InputText type="tel" @bind-Value="_registerRequest.PhoneNumber" class="appearance-none relative block w-full px-3 py-2 border border-mystic-300 placeholder-mystic-400 text-mystic-900 rounded-md focus:outline-none focus:ring-mystic-900 focus:border-mystic-900 focus:z-10 sm:text-sm" placeholder="09123456789" />
48
  <ValidationMessage For="@(() => _registerRequest.PhoneNumber)" class="text-xs text-red-500 mt-1" />
49
  </div>
50
  <div>
51
+ <label class="block text-sm font-medium text-mystic-700 mb-1">Password</label>
52
+ <InputText type="password" @bind-Value="_registerRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-mystic-300 placeholder-mystic-400 text-mystic-900 rounded-md focus:outline-none focus:ring-mystic-900 focus:border-mystic-900 focus:z-10 sm:text-sm" placeholder="••••••••" />
53
  <ValidationMessage For="@(() => _registerRequest.Password)" class="text-xs text-red-500 mt-1" />
54
  </div>
55
  </div>
56
 
57
  <div>
58
+ <button type="submit" disabled="@_isLoading" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-mystic-900 hover:bg-mystic-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-mystic-900 transition-all disabled:opacity-50">
59
  @if (_isLoading)
60
  {
61
  <span>Creating account...</span>
 
68
  </div>
69
 
70
  <div class="text-center">
71
+ <span class="text-sm text-mystic-500">Already have an account?</span>
72
+ <a href="/login" class="text-sm font-medium text-mystic-900 hover:underline ml-1">Sign in</a>
73
  </div>
74
  </EditForm>
75
  </div>
BlazorFrontend/Components/Pages/Rewards.razor CHANGED
@@ -8,29 +8,29 @@
8
 
9
  <div class="mb-8 flex flex-col md:flex-row md:items-center justify-between gap-6 animate-in fade-in slide-in-from-top-4 duration-500">
10
  <div>
11
- <h1 class="text-3xl font-extrabold tracking-tight text-slate-900">Loyalty Rewards</h1>
12
- <p class="text-slate-500 mt-1">Earn points, redeem rewards, and track your loyalty journey.</p>
13
  </div>
14
  @if (_account != null)
15
  {
16
  <div class="flex items-center gap-4">
17
  <div class="text-center bg-amber-50 border border-amber-200 px-6 py-3 rounded-2xl shadow-sm">
18
  <p class="text-[10px] font-bold text-amber-600 uppercase tracking-widest">Balance</p>
19
- <p class="text-2xl font-black text-slate-900">@_account.CurrentBalance.ToString("N0")</p>
20
  <p class="text-xs text-amber-600 font-semibold">Points</p>
21
  </div>
22
- <div class="text-center bg-slate-50 border border-slate-200 px-6 py-3 rounded-2xl shadow-sm">
23
- <p class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Tier</p>
24
- <p class="text-lg font-black text-slate-900">@_account.Tier</p>
25
  </div>
26
  </div>
27
  }
28
  </div>
29
 
30
- <div class="mb-6 border-b border-slate-200">
31
  <nav class="-mb-px flex gap-6">
32
  <button @onclick='() => _activeTab = "redeem"'
33
- class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "redeem" ? "border-slate-900 text-slate-900" : "border-transparent text-slate-400 hover:text-slate-700")'>
34
  <span class="inline-flex items-center gap-1.5">
35
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12v7a2 2 0 01-2 2H6a2 2 0 01-2-2v-7m16 0H4m16 0l-2.586-2.586a2 2 0 00-2.828 0L12 12m0 0L9.414 9.414a2 2 0 00-2.828 0L4 12m8 0V5" />
@@ -39,7 +39,7 @@
39
  </span>
40
  </button>
41
  <button @onclick='() => _activeTab = "pending"'
42
- class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "pending" ? "border-slate-900 text-slate-900" : "border-transparent text-slate-400 hover:text-slate-700")'>
43
  <span class="inline-flex items-center gap-1.5">
44
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
@@ -48,14 +48,14 @@
48
  </span>
49
  @if (_pendingRedemptions.Any())
50
  {
51
- <span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-[10px] font-black rounded-full bg-amber-400 text-slate-900">@_pendingRedemptions.Count()</span>
52
  }
53
  </button>
54
  <button @onclick='() => _activeTab = "history"'
55
- class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "history" ? "border-slate-900 text-slate-900" : "border-transparent text-slate-400 hover:text-slate-700")'>
56
  <span class="inline-flex items-center gap-1.5">
57
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-6m4 6V7m4 10v-3M5 21h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
59
  </svg>
60
  Points History
61
  </span>
@@ -66,7 +66,7 @@
66
  @if (_isLoading)
67
  {
68
  <div class="py-20 flex justify-center">
69
- <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-slate-900"></div>
70
  </div>
71
  }
72
  else
@@ -76,8 +76,8 @@ else
76
  <div class="animate-in fade-in duration-300">
77
  @if (!_availableRewards.Any())
78
  {
79
- <div class="py-20 text-center bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200">
80
- <p class="text-slate-400 font-medium">No active rewards available. Check back soon!</p>
81
  </div>
82
  }
83
  else
@@ -86,32 +86,32 @@ else
86
  @foreach (var reward in _availableRewards)
87
  {
88
  var canAfford = _account != null && _account.CurrentBalance >= reward.PointCost;
89
- <div class="group relative bg-white border border-slate-200 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden">
90
- <div class="h-1.5 @(canAfford ? "bg-gradient-to-r from-amber-400 to-orange-400" : "bg-slate-200")"></div>
91
  <div class="p-6">
92
  <div class="flex justify-between items-start mb-4">
93
- <div class="p-3 @(canAfford ? "bg-amber-50 text-amber-600" : "bg-slate-100 text-slate-400") rounded-xl">
94
  <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="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path></svg>
95
  </div>
96
  <div class="text-right">
97
- <span class="text-2xl font-black text-slate-900">@reward.PointCost.ToString("N0")</span>
98
- <span class="text-[10px] block font-bold text-slate-400 uppercase">pts required</span>
99
  </div>
100
  </div>
101
- <h3 class="text-base font-bold text-slate-900 mb-1 truncate">@reward.Name</h3>
102
- <p class="text-sm text-slate-500 mb-5 line-clamp-2 h-10">@reward.Description</p>
103
- <div class="flex items-center justify-between pt-4 border-t border-slate-100">
104
- <span class="text-xs font-semibold text-slate-500">@reward.StockQuantity in stock</span>
105
  @if (canAfford)
106
  {
107
  <button @onclick="() => RedeemAsync(reward)"
108
- class="bg-slate-900 text-white px-4 py-2 rounded-xl text-xs font-bold hover:bg-amber-500 transition-all active:scale-95">
109
  Redeem
110
  </button>
111
  }
112
  else
113
  {
114
- <span class="text-xs font-bold text-slate-400 bg-slate-100 px-3 py-2 rounded-xl">Need more pts</span>
115
  }
116
  </div>
117
  </div>
@@ -123,12 +123,12 @@ else
123
  }
124
  </div>
125
  }
126
- <div class="mt-8 p-7 bg-slate-900 rounded-3xl text-white flex flex-col md:flex-row items-center justify-between gap-5">
127
  <div>
128
  <h2 class="text-lg font-bold mb-1">How to earn more points?</h2>
129
- <p class="text-slate-400 text-sm">Borrow books, subscribe, and return on time to earn points!</p>
130
  </div>
131
- <a href="Books" class="bg-amber-400 text-slate-900 px-6 py-3 rounded-2xl font-black hover:bg-amber-300 transition-all text-sm whitespace-nowrap">Browse Books →</a>
132
  </div>
133
  </div>
134
  }
@@ -137,8 +137,8 @@ else
137
  <div class="animate-in fade-in duration-300">
138
  @if (!_pendingRedemptions.Any())
139
  {
140
- <div class="py-20 text-center bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200">
141
- <p class="text-slate-400 font-medium">No pending redemptions — you're all clear!</p>
142
  </div>
143
  }
144
  else
@@ -152,8 +152,8 @@ else
152
  <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 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>
153
  </div>
154
  <div>
155
- <p class="font-bold text-slate-900 text-sm">@r.RewardName</p>
156
- <p class="text-xs text-slate-400">Claimed @r.RedeemedAt.ToString("MMM dd, yyyy") · @r.PointCost.ToString("N0") pts</p>
157
  </div>
158
  </div>
159
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700">
@@ -171,44 +171,44 @@ else
171
  <div class="animate-in fade-in duration-300">
172
  @if (!_pointsHistory.Any())
173
  {
174
- <div class="py-20 text-center bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200">
175
- <p class="text-slate-400 font-medium">No points history yet. Start borrowing books to earn points!</p>
176
  </div>
177
  }
178
  else
179
  {
180
- <div class="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm">
181
- <table class="min-w-full divide-y divide-slate-100">
182
- <thead class="bg-slate-50">
183
  <tr>
184
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Date</th>
185
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Type</th>
186
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Details</th>
187
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Spent on</th>
188
- <th class="px-6 py-3 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">Points</th>
189
  </tr>
190
  </thead>
191
- <tbody class="divide-y divide-slate-50">
192
  @foreach (var h in _pointsHistory.OrderByDescending(x => x.CreatedAt))
193
  {
194
  var isPos = h.PointDelta >= 0;
195
  var label = PrettyEvent(h.EventKey, h.PointDelta);
196
  string? spentOn = h.RewardName;
197
 
198
- <tr class="hover:bg-slate-50 transition-colors">
199
- <td class="px-6 py-3 text-sm text-slate-500 whitespace-nowrap">@h.CreatedAt.ToString("MMM dd, yyyy")</td>
200
  <td class="px-6 py-3">
201
  <span class="px-2.5 py-0.5 rounded-full text-xs font-bold @(isPos ? "bg-emerald-100 text-emerald-700" : "bg-rose-100 text-rose-700")">@label</span>
202
  </td>
203
- <td class="px-6 py-3 text-sm text-slate-600 max-w-xs truncate">@h.Description</td>
204
- <td class="px-6 py-3 text-sm text-slate-600 max-w-xs truncate">
205
  @if (!string.IsNullOrWhiteSpace(spentOn))
206
  {
207
- <span class="font-semibold text-slate-700">@spentOn</span>
208
  }
209
  else
210
  {
211
- <span class="text-slate-400">—</span>
212
  }
213
  </td>
214
  <td class="px-6 py-3 text-right font-black text-base @(isPos ? "text-emerald-600" : "text-rose-600")">
 
8
 
9
  <div class="mb-8 flex flex-col md:flex-row md:items-center justify-between gap-6 animate-in fade-in slide-in-from-top-4 duration-500">
10
  <div>
11
+ <h1 class="text-3xl font-extrabold tracking-tight text-mystic-900">Loyalty Rewards</h1>
12
+ <p class="text-mystic-900 mt-1">Earn points, redeem rewards, and track your loyalty journey.</p>
13
  </div>
14
  @if (_account != null)
15
  {
16
  <div class="flex items-center gap-4">
17
  <div class="text-center bg-amber-50 border border-amber-200 px-6 py-3 rounded-2xl shadow-sm">
18
  <p class="text-[10px] font-bold text-amber-600 uppercase tracking-widest">Balance</p>
19
+ <p class="text-2xl font-black text-mystic-900">@_account.CurrentBalance.ToString("N0")</p>
20
  <p class="text-xs text-amber-600 font-semibold">Points</p>
21
  </div>
22
+ <div class="text-center bg-mystic-50 border border-mystic-200 px-6 py-3 rounded-2xl shadow-sm">
23
+ <p class="text-[10px] font-bold text-mystic-500 uppercase tracking-widest">Tier</p>
24
+ <p class="text-lg font-black text-mystic-900">@_account.Tier</p>
25
  </div>
26
  </div>
27
  }
28
  </div>
29
 
30
+ <div class="mb-6 border-b border-mystic-200">
31
  <nav class="-mb-px flex gap-6">
32
  <button @onclick='() => _activeTab = "redeem"'
33
+ class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "redeem" ? "border-mystic-900 text-mystic-900" : "border-transparent text-mystic-400 hover:text-mystic-700")'>
34
  <span class="inline-flex items-center gap-1.5">
35
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12v7a2 2 0 01-2 2H6a2 2 0 01-2-2v-7m16 0H4m16 0l-2.586-2.586a2 2 0 00-2.828 0L12 12m0 0L9.414 9.414a2 2 0 00-2.828 0L4 12m8 0V5" />
 
39
  </span>
40
  </button>
41
  <button @onclick='() => _activeTab = "pending"'
42
+ class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "pending" ? "border-mystic-900 text-mystic-900" : "border-transparent text-mystic-400 hover:text-mystic-700")'>
43
  <span class="inline-flex items-center gap-1.5">
44
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
 
48
  </span>
49
  @if (_pendingRedemptions.Any())
50
  {
51
+ <span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-[10px] font-black rounded-full bg-amber-400 text-mystic-900">@_pendingRedemptions.Count()</span>
52
  }
53
  </button>
54
  <button @onclick='() => _activeTab = "history"'
55
+ class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "history" ? "border-mystic-900 text-mystic-900" : "border-transparent text-mystic-400 hover:text-mystic-700")'>
56
  <span class="inline-flex items-center gap-1.5">
57
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-6m4 6V7m4 10v-3M5 21h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2-2v14a2 2 0 002 2z" />
59
  </svg>
60
  Points History
61
  </span>
 
66
  @if (_isLoading)
67
  {
68
  <div class="py-20 flex justify-center">
69
+ <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
70
  </div>
71
  }
72
  else
 
76
  <div class="animate-in fade-in duration-300">
77
  @if (!_availableRewards.Any())
78
  {
79
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
80
+ <p class="text-mystic-400 font-medium">No active rewards available. Check back soon!</p>
81
  </div>
82
  }
83
  else
 
86
  @foreach (var reward in _availableRewards)
87
  {
88
  var canAfford = _account != null && _account.CurrentBalance >= reward.PointCost;
89
+ <div class="group relative bg-white border border-mystic-200 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden">
90
+ <div class="h-1.5 @(canAfford ? "bg-gradient-to-r from-amber-400 to-orange-400" : "bg-mystic-200")"></div>
91
  <div class="p-6">
92
  <div class="flex justify-between items-start mb-4">
93
+ <div class="p-3 @(canAfford ? "bg-amber-50 text-amber-600" : "bg-mystic-100 text-mystic-400") rounded-xl">
94
  <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="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"></path></svg>
95
  </div>
96
  <div class="text-right">
97
+ <span class="text-2xl font-black text-mystic-900">@reward.PointCost.ToString("N0")</span>
98
+ <span class="text-[10px] block font-bold text-mystic-400 uppercase">pts required</span>
99
  </div>
100
  </div>
101
+ <h3 class="text-base font-bold text-mystic-900 mb-1 truncate">@reward.Name</h3>
102
+ <p class="text-sm text-mystic-500 mb-5 line-clamp-2 h-10">@reward.Description</p>
103
+ <div class="flex items-center justify-between pt-4 border-t border-mystic-100">
104
+ <span class="text-xs font-semibold text-mystic-500">@reward.StockQuantity in stock</span>
105
  @if (canAfford)
106
  {
107
  <button @onclick="() => RedeemAsync(reward)"
108
+ class="bg-mystic-900 text-white px-4 py-2 rounded-xl text-xs font-bold hover:bg-amber-500 transition-all active:scale-95">
109
  Redeem
110
  </button>
111
  }
112
  else
113
  {
114
+ <span class="text-xs font-bold text-mystic-400 bg-mystic-100 px-3 py-2 rounded-xl">Need more pts</span>
115
  }
116
  </div>
117
  </div>
 
123
  }
124
  </div>
125
  }
126
+ <div class="mt-8 p-7 bg-mystic-900 rounded-3xl text-white flex flex-col md:flex-row items-center justify-between gap-5">
127
  <div>
128
  <h2 class="text-lg font-bold mb-1">How to earn more points?</h2>
129
+ <p class="text-mystic-400 text-sm">Borrow books, subscribe, and return on time to earn points!</p>
130
  </div>
131
+ <a href="Books" class="bg-amber-400 text-mystic-900 px-6 py-3 rounded-2xl font-black hover:bg-amber-300 transition-all text-sm whitespace-nowrap">Browse Books →</a>
132
  </div>
133
  </div>
134
  }
 
137
  <div class="animate-in fade-in duration-300">
138
  @if (!_pendingRedemptions.Any())
139
  {
140
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
141
+ <p class="text-mystic-400 font-medium">No pending redemptions — you're all clear!</p>
142
  </div>
143
  }
144
  else
 
152
  <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 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>
153
  </div>
154
  <div>
155
+ <p class="font-bold text-mystic-900 text-sm">@r.RewardName</p>
156
+ <p class="text-xs text-mystic-400">Claimed @r.RedeemedAt.ToString("MMM dd, yyyy") · @r.PointCost.ToString("N0") pts</p>
157
  </div>
158
  </div>
159
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700">
 
171
  <div class="animate-in fade-in duration-300">
172
  @if (!_pointsHistory.Any())
173
  {
174
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
175
+ <p class="text-mystic-400 font-medium">No points history yet. Start borrowing books to earn points!</p>
176
  </div>
177
  }
178
  else
179
  {
180
+ <div class="bg-white border border-mystic-200 rounded-2xl overflow-hidden shadow-sm">
181
+ <table class="min-w-full divide-y divide-mystic-100">
182
+ <thead class="bg-mystic-50">
183
  <tr>
184
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Date</th>
185
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Type</th>
186
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Details</th>
187
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Spent on</th>
188
+ <th class="px-6 py-3 text-right text-xs font-bold text-mystic-500 uppercase tracking-wider">Points</th>
189
  </tr>
190
  </thead>
191
+ <tbody class="divide-y divide-mystic-50">
192
  @foreach (var h in _pointsHistory.OrderByDescending(x => x.CreatedAt))
193
  {
194
  var isPos = h.PointDelta >= 0;
195
  var label = PrettyEvent(h.EventKey, h.PointDelta);
196
  string? spentOn = h.RewardName;
197
 
198
+ <tr class="hover:bg-mystic-50 transition-colors">
199
+ <td class="px-6 py-3 text-sm text-mystic-500 whitespace-nowrap">@h.CreatedAt.ToString("MMM dd, yyyy")</td>
200
  <td class="px-6 py-3">
201
  <span class="px-2.5 py-0.5 rounded-full text-xs font-bold @(isPos ? "bg-emerald-100 text-emerald-700" : "bg-rose-100 text-rose-700")">@label</span>
202
  </td>
203
+ <td class="px-6 py-3 text-sm text-mystic-600 max-w-xs truncate">@h.Description</td>
204
+ <td class="px-6 py-3 text-sm text-mystic-600 max-w-xs truncate">
205
  @if (!string.IsNullOrWhiteSpace(spentOn))
206
  {
207
+ <span class="font-semibold text-mystic-700">@spentOn</span>
208
  }
209
  else
210
  {
211
+ <span class="text-mystic-400">—</span>
212
  }
213
  </td>
214
  <td class="px-6 py-3 text-right font-black text-base @(isPos ? "text-emerald-600" : "text-rose-600")">
BlazorFrontend/Components/Pages/RewardsManage.razor CHANGED
@@ -10,25 +10,25 @@
10
 
11
  <div class="mb-8 flex flex-col md:flex-row md:items-center justify-between gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
12
  <div>
13
- <h1 class="text-3xl font-extrabold tracking-tight text-slate-900">Rewards Management</h1>
14
- <p class="text-slate-500 mt-1">Fulfill pending redemptions and review all members' loyalty activity.</p>
15
  </div>
16
  <div class="flex items-center gap-4">
17
  <div class="bg-amber-50 border border-amber-200 px-5 py-2.5 rounded-xl text-center shadow-sm">
18
  <p class="text-[10px] font-bold text-amber-600 uppercase tracking-widest">Pending</p>
19
- <p class="text-2xl font-black text-slate-900">@_pendingRedemptions.Count()</p>
20
  </div>
21
- <div class="bg-slate-50 border border-slate-200 px-5 py-2.5 rounded-xl text-center shadow-sm">
22
- <p class="text-[10px] font-bold text-slate-500 uppercase tracking-widest">Members w/ History</p>
23
- <p class="text-2xl font-black text-slate-900">@_allMembersHistory.Count()</p>
24
  </div>
25
  </div>
26
  </div>
27
 
28
- <div class="mb-6 border-b border-slate-200">
29
  <nav class="-mb-px flex gap-6">
30
  <button @onclick='() => _activeTab = "pending"'
31
- class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "pending" ? "border-slate-900 text-slate-900" : "border-transparent text-slate-400 hover:text-slate-700")'>
32
  <span class="inline-flex items-center gap-1.5">
33
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
@@ -41,7 +41,7 @@
41
  }
42
  </button>
43
  <button @onclick='() => _activeTab = "history"'
44
- class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "history" ? "border-slate-900 text-slate-900" : "border-transparent text-slate-400 hover:text-slate-700")'>
45
  <span class="inline-flex items-center gap-1.5">
46
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
47
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-6m4 6V7m4 10v-3M5 21h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
@@ -55,7 +55,7 @@
55
  @if (_isLoading)
56
  {
57
  <div class="py-20 flex justify-center">
58
- <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-slate-900"></div>
59
  </div>
60
  }
61
  else
@@ -65,43 +65,43 @@ else
65
  <div class="animate-in fade-in duration-300">
66
  @if (!_pendingRedemptions.Any())
67
  {
68
- <div class="py-20 text-center bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200">
69
- <svg class="w-16 h-16 mx-auto opacity-20 text-slate-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
70
- <p class="text-slate-500 font-medium">No pending redemptions. All caught up!</p>
71
  </div>
72
  }
73
  else
74
  {
75
- <div class="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm">
76
- <table class="min-w-full divide-y divide-slate-100">
77
- <thead class="bg-slate-50">
78
  <tr>
79
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Member</th>
80
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Reward</th>
81
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Points</th>
82
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Claimed</th>
83
- <th class="px-6 py-3 text-left text-xs font-bold text-slate-500 uppercase tracking-wider">Status</th>
84
- <th class="px-6 py-3 text-right text-xs font-bold text-slate-500 uppercase tracking-wider">Action</th>
85
  </tr>
86
  </thead>
87
- <tbody class="divide-y divide-slate-100">
88
  @foreach (var r in _pendingRedemptions.OrderBy(x => x.RedeemedAt))
89
  {
90
  <tr class="hover:bg-amber-50/40 transition-colors">
91
  <td class="px-6 py-4">
92
  <div class="flex items-center gap-3">
93
- <div class="w-8 h-8 rounded-full bg-slate-900 flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
94
  @(r.ExternalUserId.Length >= 2 ? r.ExternalUserId.Substring(0, 2).ToUpper() : "U")
95
  </div>
96
- <span class="text-sm font-medium text-slate-700 font-mono text-xs">@r.ExternalUserId</span>
97
  </div>
98
  </td>
99
  <td class="px-6 py-4">
100
- <p class="text-sm font-bold text-slate-900">@r.RewardName</p>
101
- <p class="text-xs text-slate-400">ID: @(r.Id.Length > 8 ? r.Id[..8] : r.Id)...</p>
102
  </td>
103
- <td class="px-6 py-4 text-sm font-bold text-slate-700">@r.PointCost.ToString("N0") pts</td>
104
- <td class="px-6 py-4 text-sm text-slate-500 whitespace-nowrap">@r.RedeemedAt.ToString("MMM dd, yyyy")</td>
105
  <td class="px-6 py-4">
106
  <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700">
107
  <span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
@@ -128,8 +128,8 @@ else
128
  <div class="animate-in fade-in duration-300">
129
  @if (!_allMembersHistory.Any())
130
  {
131
- <div class="py-20 text-center bg-slate-50 rounded-3xl border-2 border-dashed border-slate-200">
132
- <p class="text-slate-400 font-medium">No loyalty history found for any members yet.</p>
133
  </div>
134
  }
135
  else
@@ -137,40 +137,40 @@ else
137
  <!-- Search bar -->
138
  <div class="mb-4">
139
  <input type="text" placeholder="Filter by member name or email..."
140
- class="w-full max-w-sm px-4 py-2.5 border border-slate-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-slate-900 bg-white"
141
  @bind="_searchQuery" @bind:event="oninput" />
142
  </div>
143
 
144
  <div class="space-y-4">
145
  @foreach (var member in FilteredMembers)
146
  {
147
- <div class="bg-white border border-slate-200 rounded-2xl overflow-hidden shadow-sm">
148
  <!-- Member header -->
149
- <div class="flex items-center justify-between px-6 py-4 bg-slate-50 border-b border-slate-100 cursor-pointer"
150
  @onclick="() => ToggleMember(member.AccountId)">
151
  <div class="flex items-center gap-4">
152
- <div class="w-10 h-10 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
153
  @(member.UserName.Length >= 2 ? member.UserName.Substring(0, 2).ToUpper() : "?")
154
  </div>
155
  <div>
156
- <p class="font-bold text-slate-900 text-sm">@member.UserName</p>
157
- <p class="text-xs text-slate-400">@member.UserEmail</p>
158
  </div>
159
  </div>
160
  <div class="flex items-center gap-6">
161
  <div class="text-right">
162
- <p class="text-xs font-bold text-slate-400 uppercase tracking-widest">Balance</p>
163
- <p class="font-black text-slate-900">@member.CurrentBalance.ToString("N0") pts</p>
164
  </div>
165
  <div class="text-right">
166
- <p class="text-xs font-bold text-slate-400 uppercase tracking-widest">Tier</p>
167
- <span class="text-xs font-bold px-2 py-0.5 rounded-full bg-slate-200 text-slate-700">@member.Tier</span>
168
  </div>
169
  <div class="text-right">
170
- <p class="text-xs font-bold text-slate-400 uppercase tracking-widest">Transactions</p>
171
- <p class="font-black text-slate-900">@member.History.Count()</p>
172
  </div>
173
- <svg class="w-4 h-4 text-slate-400 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>
174
  </div>
175
  </div>
176
 
@@ -180,41 +180,41 @@ else
180
  <div class="overflow-x-auto animate-in slide-in-from-top-2 duration-300">
181
  @if (!member.History.Any())
182
  {
183
- <p class="px-6 py-4 text-sm text-slate-400 italic">No transactions found for this member.</p>
184
  }
185
  else
186
  {
187
- <table class="min-w-full divide-y divide-slate-50">
188
  <thead>
189
- <tr class="bg-slate-50/50">
190
- <th class="px-6 py-2 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">Date</th>
191
- <th class="px-6 py-2 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">Type</th>
192
- <th class="px-6 py-2 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">Details</th>
193
- <th class="px-6 py-2 text-left text-[10px] font-bold text-slate-400 uppercase tracking-wider">Spent on</th>
194
- <th class="px-6 py-2 text-right text-[10px] font-bold text-slate-400 uppercase tracking-wider">Points</th>
195
  </tr>
196
  </thead>
197
- <tbody class="divide-y divide-slate-50">
198
  @foreach (var h in member.History.OrderByDescending(x => x.CreatedAt))
199
  {
200
  var isPos = h.PointDelta >= 0;
201
  var label = PrettyEvent(h.EventKey, h.PointDelta);
202
  string? spentOn = h.RewardName;
203
 
204
- <tr class="hover:bg-slate-50 transition-colors">
205
- <td class="px-6 py-2.5 text-xs text-slate-500 whitespace-nowrap">@h.CreatedAt.ToString("MMM dd, yyyy")</td>
206
  <td class="px-6 py-2.5">
207
  <span class="px-2 py-0.5 rounded-full text-[10px] font-bold @(isPos ? "bg-emerald-100 text-emerald-700" : "bg-rose-100 text-rose-700")">@label</span>
208
  </td>
209
- <td class="px-6 py-2.5 text-xs text-slate-600 max-w-xs truncate">@h.Description</td>
210
- <td class="px-6 py-2.5 text-xs text-slate-600 max-w-xs truncate">
211
  @if (!string.IsNullOrWhiteSpace(spentOn))
212
  {
213
- <span class="font-semibold text-slate-700">@spentOn</span>
214
  }
215
  else
216
  {
217
- <span class="text-slate-400">—</span>
218
  }
219
  </td>
220
  <td class="px-6 py-2.5 text-right font-black text-sm @(isPos ? "text-emerald-600" : "text-rose-600")">
 
10
 
11
  <div class="mb-8 flex flex-col md:flex-row md:items-center justify-between gap-4 animate-in fade-in slide-in-from-top-4 duration-500">
12
  <div>
13
+ <h1 class="text-3xl font-extrabold tracking-tight text-mystic-900">Rewards Management</h1>
14
+ <p class="text-mystic-900 mt-1">Fulfill pending redemptions and review all members' loyalty activity.</p>
15
  </div>
16
  <div class="flex items-center gap-4">
17
  <div class="bg-amber-50 border border-amber-200 px-5 py-2.5 rounded-xl text-center shadow-sm">
18
  <p class="text-[10px] font-bold text-amber-600 uppercase tracking-widest">Pending</p>
19
+ <p class="text-2xl font-black text-mystic-900">@_pendingRedemptions.Count()</p>
20
  </div>
21
+ <div class="bg-mystic-50 border border-mystic-200 px-5 py-2.5 rounded-xl text-center shadow-sm">
22
+ <p class="text-[10px] font-bold text-mystic-500 uppercase tracking-widest">Members w/ History</p>
23
+ <p class="text-2xl font-black text-mystic-900">@_allMembersHistory.Count()</p>
24
  </div>
25
  </div>
26
  </div>
27
 
28
+ <div class="mb-6 border-b border-mystic-200">
29
  <nav class="-mb-px flex gap-6">
30
  <button @onclick='() => _activeTab = "pending"'
31
+ class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "pending" ? "border-mystic-900 text-mystic-900" : "border-transparent text-mystic-400 hover:text-mystic-700")'>
32
  <span class="inline-flex items-center gap-1.5">
33
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
 
41
  }
42
  </button>
43
  <button @onclick='() => _activeTab = "history"'
44
+ class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "history" ? "border-mystic-900 text-mystic-900" : "border-transparent text-mystic-400 hover:text-mystic-700")'>
45
  <span class="inline-flex items-center gap-1.5">
46
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
47
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-6m4 6V7m4 10v-3M5 21h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
 
55
  @if (_isLoading)
56
  {
57
  <div class="py-20 flex justify-center">
58
+ <div class="animate-spin rounded-full h-10 w-10 border-b-2 border-mystic-900"></div>
59
  </div>
60
  }
61
  else
 
65
  <div class="animate-in fade-in duration-300">
66
  @if (!_pendingRedemptions.Any())
67
  {
68
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
69
+ <svg class="w-16 h-16 mx-auto opacity-20 text-mystic-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
70
+ <p class="text-mystic-500 font-medium">No pending redemptions. All caught up!</p>
71
  </div>
72
  }
73
  else
74
  {
75
+ <div class="bg-white border border-mystic-200 rounded-2xl overflow-hidden shadow-sm">
76
+ <table class="min-w-full divide-y divide-mystic-100">
77
+ <thead class="bg-mystic-50">
78
  <tr>
79
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Member</th>
80
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Reward</th>
81
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Points</th>
82
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Claimed</th>
83
+ <th class="px-6 py-3 text-left text-xs font-bold text-mystic-500 uppercase tracking-wider">Status</th>
84
+ <th class="px-6 py-3 text-right text-xs font-bold text-mystic-500 uppercase tracking-wider">Action</th>
85
  </tr>
86
  </thead>
87
+ <tbody class="divide-y divide-mystic-100">
88
  @foreach (var r in _pendingRedemptions.OrderBy(x => x.RedeemedAt))
89
  {
90
  <tr class="hover:bg-amber-50/40 transition-colors">
91
  <td class="px-6 py-4">
92
  <div class="flex items-center gap-3">
93
+ <div class="w-8 h-8 rounded-full bg-mystic-900 flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
94
  @(r.ExternalUserId.Length >= 2 ? r.ExternalUserId.Substring(0, 2).ToUpper() : "U")
95
  </div>
96
+ <span class="text-sm font-medium text-mystic-700 font-mono text-xs">@r.ExternalUserId</span>
97
  </div>
98
  </td>
99
  <td class="px-6 py-4">
100
+ <p class="text-sm font-bold text-mystic-900">@r.RewardName</p>
101
+ <p class="text-xs text-mystic-400">ID: @(r.Id.Length > 8 ? r.Id[..8] : r.Id)...</p>
102
  </td>
103
+ <td class="px-6 py-4 text-sm font-bold text-mystic-700">@r.PointCost.ToString("N0") pts</td>
104
+ <td class="px-6 py-4 text-sm text-mystic-500 whitespace-nowrap">@r.RedeemedAt.ToString("MMM dd, yyyy")</td>
105
  <td class="px-6 py-4">
106
  <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700">
107
  <span class="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse"></span>
 
128
  <div class="animate-in fade-in duration-300">
129
  @if (!_allMembersHistory.Any())
130
  {
131
+ <div class="py-20 text-center bg-mystic-50 rounded-3xl border-2 border-dashed border-mystic-200">
132
+ <p class="text-mystic-400 font-medium">No loyalty history found for any members yet.</p>
133
  </div>
134
  }
135
  else
 
137
  <!-- Search bar -->
138
  <div class="mb-4">
139
  <input type="text" placeholder="Filter by member name or email..."
140
+ class="w-full max-w-sm px-4 py-2.5 border border-mystic-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-mystic-900 bg-white"
141
  @bind="_searchQuery" @bind:event="oninput" />
142
  </div>
143
 
144
  <div class="space-y-4">
145
  @foreach (var member in FilteredMembers)
146
  {
147
+ <div class="bg-white border border-mystic-200 rounded-2xl overflow-hidden shadow-sm">
148
  <!-- Member header -->
149
+ <div class="flex items-center justify-between px-6 py-4 bg-mystic-50 border-b border-mystic-100 cursor-pointer"
150
  @onclick="() => ToggleMember(member.AccountId)">
151
  <div class="flex items-center gap-4">
152
+ <div class="w-10 h-10 rounded-full bg-mystic-900 flex items-center justify-center text-white font-bold text-sm flex-shrink-0">
153
  @(member.UserName.Length >= 2 ? member.UserName.Substring(0, 2).ToUpper() : "?")
154
  </div>
155
  <div>
156
+ <p class="font-bold text-mystic-900 text-sm">@member.UserName</p>
157
+ <p class="text-xs text-mystic-400">@member.UserEmail</p>
158
  </div>
159
  </div>
160
  <div class="flex items-center gap-6">
161
  <div class="text-right">
162
+ <p class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Balance</p>
163
+ <p class="font-black text-mystic-900">@member.CurrentBalance.ToString("N0") pts</p>
164
  </div>
165
  <div class="text-right">
166
+ <p class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Tier</p>
167
+ <span class="text-xs font-bold px-2 py-0.5 rounded-full bg-mystic-200 text-mystic-700">@member.Tier</span>
168
  </div>
169
  <div class="text-right">
170
+ <p class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Transactions</p>
171
+ <p class="font-black text-mystic-900">@member.History.Count()</p>
172
  </div>
173
+ <svg class="w-4 h-4 text-mystic-400 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>
174
  </div>
175
  </div>
176
 
 
180
  <div class="overflow-x-auto animate-in slide-in-from-top-2 duration-300">
181
  @if (!member.History.Any())
182
  {
183
+ <p class="px-6 py-4 text-sm text-mystic-400 italic">No transactions found for this member.</p>
184
  }
185
  else
186
  {
187
+ <table class="min-w-full divide-y divide-mystic-50">
188
  <thead>
189
+ <tr class="bg-mystic-50/50">
190
+ <th class="px-6 py-2 text-left text-[10px] font-bold text-mystic-400 uppercase tracking-wider">Date</th>
191
+ <th class="px-6 py-2 text-left text-[10px] font-bold text-mystic-400 uppercase tracking-wider">Type</th>
192
+ <th class="px-6 py-2 text-left text-[10px] font-bold text-mystic-400 uppercase tracking-wider">Details</th>
193
+ <th class="px-6 py-2 text-left text-[10px] font-bold text-mystic-400 uppercase tracking-wider">Spent on</th>
194
+ <th class="px-6 py-2 text-right text-[10px] font-bold text-mystic-400 uppercase tracking-wider">Points</th>
195
  </tr>
196
  </thead>
197
+ <tbody class="divide-y divide-mystic-50">
198
  @foreach (var h in member.History.OrderByDescending(x => x.CreatedAt))
199
  {
200
  var isPos = h.PointDelta >= 0;
201
  var label = PrettyEvent(h.EventKey, h.PointDelta);
202
  string? spentOn = h.RewardName;
203
 
204
+ <tr class="hover:bg-mystic-50 transition-colors">
205
+ <td class="px-6 py-2.5 text-xs text-mystic-500 whitespace-nowrap">@h.CreatedAt.ToString("MMM dd, yyyy")</td>
206
  <td class="px-6 py-2.5">
207
  <span class="px-2 py-0.5 rounded-full text-[10px] font-bold @(isPos ? "bg-emerald-100 text-emerald-700" : "bg-rose-100 text-rose-700")">@label</span>
208
  </td>
209
+ <td class="px-6 py-2.5 text-xs text-mystic-600 max-w-xs truncate">@h.Description</td>
210
+ <td class="px-6 py-2.5 text-xs text-mystic-600 max-w-xs truncate">
211
  @if (!string.IsNullOrWhiteSpace(spentOn))
212
  {
213
+ <span class="font-semibold text-mystic-700">@spentOn</span>
214
  }
215
  else
216
  {
217
+ <span class="text-mystic-400">—</span>
218
  }
219
  </td>
220
  <td class="px-6 py-2.5 text-right font-black text-sm @(isPos ? "text-emerald-600" : "text-rose-600")">