Spaces:
Sleeping
Sleeping
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 +32 -18
- BlazorFrontend/Components/Layout/MainLayout.razor +12 -12
- BlazorFrontend/Components/Pages/Books.razor +52 -52
- BlazorFrontend/Components/Pages/Borrowings.razor +22 -22
- BlazorFrontend/Components/Pages/BorrowingsManage.razor +28 -28
- BlazorFrontend/Components/Pages/Home.razor +34 -34
- BlazorFrontend/Components/Pages/Login.razor +11 -11
- BlazorFrontend/Components/Pages/Members.razor +79 -79
- BlazorFrontend/Components/Pages/Membership.razor +44 -44
- BlazorFrontend/Components/Pages/Register.razor +15 -15
- BlazorFrontend/Components/Pages/Rewards.razor +50 -50
- BlazorFrontend/Components/Pages/RewardsManage.razor +59 -59
BlazorFrontend/Components/App.razor
CHANGED
|
@@ -21,18 +21,31 @@
|
|
| 21 |
sans: ['Inter', 'sans-serif'],
|
| 22 |
},
|
| 23 |
colors: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
primary: {
|
| 25 |
-
50: '#
|
| 26 |
-
100: '#
|
| 27 |
-
200: '#
|
| 28 |
-
300: '#
|
| 29 |
-
400: '#
|
| 30 |
-
500: '#
|
| 31 |
-
600: '#
|
| 32 |
-
700: '#
|
| 33 |
-
800: '#
|
| 34 |
-
900: '#
|
| 35 |
-
950: '#
|
| 36 |
},
|
| 37 |
},
|
| 38 |
}
|
|
@@ -42,15 +55,16 @@
|
|
| 42 |
|
| 43 |
<style type="text/tailwindcss">
|
| 44 |
@@layer base {
|
| 45 |
-
body { @@apply text-
|
|
|
|
| 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-
|
| 49 |
-
.nav-link.active { @@apply bg-
|
| 50 |
|
| 51 |
-
.card { @@apply bg-white border border-
|
| 52 |
-
.btn-primary { @@apply inline-flex items-center justify-center rounded-md bg-
|
| 53 |
-
.btn-secondary { @@apply inline-flex items-center justify-center rounded-md border border-
|
| 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-
|
| 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-
|
| 10 |
<div class="flex items-center flex-shrink-0 px-4 gap-2">
|
| 11 |
-
<div class="bg-
|
| 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-
|
| 75 |
<AuthorizeView>
|
| 76 |
<Authorized Context="authContext">
|
| 77 |
<div class="flex items-center gap-3">
|
| 78 |
-
<div class="h-8 w-8 rounded-full bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 16 |
-
<p class="mt-2 text-
|
| 17 |
Explore and discover your next great read.
|
| 18 |
-
<span class="ml-2 text-xs text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 68 |
</div>
|
| 69 |
}
|
| 70 |
else if (!FilteredBooks.Any())
|
| 71 |
{
|
| 72 |
-
<div class="py-20 text-center bg-
|
| 73 |
-
<svg class="w-16 h-16 mx-auto text-
|
| 74 |
-
<p class="text-
|
| 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-
|
| 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-
|
| 93 |
</div>
|
| 94 |
}
|
| 95 |
|
| 96 |
@if (book.AvailableCopies <= 0)
|
| 97 |
{
|
| 98 |
-
<div class="absolute inset-0 bg-
|
| 99 |
-
<span class="bg-white text-
|
| 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-
|
| 107 |
@book.Title
|
| 108 |
</h3>
|
| 109 |
-
<p class="text-xs text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 168 |
-
<h1 class="text-4xl font-extrabold text-
|
| 169 |
-
<p class="text-xl text-
|
| 170 |
</div>
|
| 171 |
|
| 172 |
-
<div class="prose prose-
|
| 173 |
-
<h3 class="text-lg font-bold text-
|
| 174 |
-
<p class="text-
|
| 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-
|
| 180 |
<div>
|
| 181 |
-
<h4 class="text-[10px] font-bold text-
|
| 182 |
-
<p class="text-sm font-semibold text-
|
| 183 |
</div>
|
| 184 |
<div>
|
| 185 |
-
<h4 class="text-[10px] font-bold text-
|
| 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-
|
| 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-
|
| 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-
|
| 232 |
-
<p class="mt-2 text-
|
| 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-
|
| 241 |
-
<InputText @bind-Value="_formModel.Title" class="mt-1 block w-full rounded-md border-
|
| 242 |
</div>
|
| 243 |
|
| 244 |
<div>
|
| 245 |
-
<label class="block text-sm font-medium text-
|
| 246 |
-
<InputText @bind-Value="_formModel.Author" class="mt-1 block w-full rounded-md border-
|
| 247 |
</div>
|
| 248 |
|
| 249 |
<div>
|
| 250 |
-
<label class="block text-sm font-medium text-
|
| 251 |
-
<InputText @bind-Value="_formModel.Isbn" class="mt-1 block w-full rounded-md border-
|
| 252 |
</div>
|
| 253 |
|
| 254 |
<div>
|
| 255 |
-
<label class="block text-sm font-medium text-
|
| 256 |
-
<InputNumber @bind-Value="_formModel.TotalCopies" class="mt-1 block w-full rounded-md border-
|
| 257 |
</div>
|
| 258 |
|
| 259 |
<div>
|
| 260 |
-
<label class="block text-sm font-medium text-
|
| 261 |
-
<InputText @bind-Value="_formModel.CoverUrl" class="mt-1 block w-full rounded-md border-
|
| 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-
|
| 268 |
-
<InputTextArea @bind-Value="_formModel.Description" rows="4" class="mt-1 block w-full rounded-md border-
|
| 269 |
</div>
|
| 270 |
|
| 271 |
<div class="sm:col-span-2">
|
| 272 |
-
<label class="block text-sm font-medium text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 11 |
-
<p class="mt-2 text-
|
| 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-
|
| 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-
|
| 27 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 28 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 29 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 30 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 31 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 32 |
</tr>
|
| 33 |
</thead>
|
| 34 |
-
<tbody class="divide-y divide-
|
| 35 |
@if (!_borrowings.Any())
|
| 36 |
{
|
| 37 |
<tr>
|
| 38 |
-
<td colspan="5" class="px-6 py-12 text-center text-
|
| 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-
|
| 46 |
<td class="px-6 py-4">
|
| 47 |
<div class="flex items-center gap-3">
|
| 48 |
-
<div class="w-10 h-14 bg-
|
| 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-
|
| 53 |
-
<p class="text-[11px] text-
|
| 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-
|
| 60 |
-
<p class="text-[11px]"><span class="text-
|
| 61 |
@if (loan.ReturnDate.HasValue)
|
| 62 |
{
|
| 63 |
-
<p class="text-[11px]"><span class="text-
|
| 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-
|
| 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-
|
| 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-
|
| 98 |
Return Book
|
| 99 |
</button>
|
| 100 |
}
|
| 101 |
else if (loan.Status == "PendingReturn")
|
| 102 |
{
|
| 103 |
-
<span class="text-xs font-medium text-
|
| 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-
|
| 15 |
-
<p class="text-
|
| 16 |
</div>
|
| 17 |
<div class="flex gap-3">
|
| 18 |
-
<div class="card px-4 py-2 flex items-center gap-2 bg-
|
| 19 |
-
<span class="text-xs font-bold text-
|
| 20 |
-
<span class="text-lg font-bold text-
|
| 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-
|
| 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-
|
| 39 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 40 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 41 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 42 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 43 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 44 |
-
<th class="px-6 py-4 text-[10px] font-bold text-
|
| 45 |
</tr>
|
| 46 |
</thead>
|
| 47 |
-
<tbody class="divide-y divide-
|
| 48 |
@if (!_borrowings.Any())
|
| 49 |
{
|
| 50 |
<tr>
|
| 51 |
-
<td colspan="6" class="px-6 py-16 text-center text-
|
| 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-
|
| 59 |
<td class="px-6 py-5">
|
| 60 |
<div class="flex items-center gap-4">
|
| 61 |
-
<div class="w-10 h-14 bg-
|
| 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-
|
| 66 |
-
<p class="text-[10px] font-mono text-
|
| 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-
|
| 73 |
-
<p class="text-[10px] text-
|
| 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-
|
| 80 |
-
<span class="font-bold text-
|
| 81 |
</div>
|
| 82 |
<div class="flex items-center gap-2 text-[11px]">
|
| 83 |
-
<span class="text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 13 |
-
<p class="text-
|
| 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-
|
| 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-
|
| 60 |
<div class="flex items-center justify-between">
|
| 61 |
-
<div class="p-2 bg-
|
| 62 |
-
<svg class="w-5 h-5 text-
|
| 63 |
</div>
|
| 64 |
-
<span class="text-[10px] font-bold text-
|
| 65 |
</div>
|
| 66 |
<div>
|
| 67 |
-
<div class="text-3xl font-bold text-
|
| 68 |
-
<div class="text-xs text-
|
| 69 |
</div>
|
| 70 |
</div>
|
| 71 |
|
| 72 |
-
<div class="card p-6 flex flex-col gap-3 group hover:border-
|
| 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-
|
| 78 |
</div>
|
| 79 |
<div>
|
| 80 |
-
<div class="text-3xl font-bold text-
|
| 81 |
-
<div class="text-xs text-
|
| 82 |
</div>
|
| 83 |
</div>
|
| 84 |
|
| 85 |
-
<div class="card p-6 flex flex-col gap-3 group hover:border-
|
| 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-
|
| 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-
|
| 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-
|
| 104 |
</div>
|
| 105 |
<div>
|
| 106 |
-
<div class="text-3xl font-bold text-
|
| 107 |
-
<div class="text-xs text-
|
| 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-
|
| 118 |
-
<a href="Books" class="text-sm font-bold text-
|
| 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-
|
| 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-
|
| 134 |
-
<span class="bg-white text-
|
| 135 |
</div>
|
| 136 |
}
|
| 137 |
</div>
|
| 138 |
-
<h3 class="text-xs font-bold text-
|
| 139 |
-
<p class="text-[10px] text-
|
| 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-
|
| 150 |
<div class="relative z-10">
|
| 151 |
<h2 class="text-2xl font-bold mb-2">Explore the Library</h2>
|
| 152 |
-
<p class="text-
|
| 153 |
</div>
|
| 154 |
<div class="mt-8 relative z-10">
|
| 155 |
-
<a href="Books" class="inline-flex items-center bg-white text-
|
| 156 |
View Catalog
|
| 157 |
</a>
|
| 158 |
</div>
|
| 159 |
-
<svg class="absolute bottom-0 right-0 w-48 h-48 text-
|
| 160 |
</div>
|
| 161 |
|
| 162 |
-
<div class="bg-
|
| 163 |
<div>
|
| 164 |
-
<h2 class="text-2xl font-bold text-
|
| 165 |
-
<p class="text-
|
| 166 |
</div>
|
| 167 |
<div class="mt-8">
|
| 168 |
-
<a href="Borrowings" class="inline-flex items-center text-
|
| 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-
|
| 15 |
<div>
|
| 16 |
-
<div class="mx-auto h-12 w-12 bg-
|
| 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-
|
| 20 |
-
<p class="mt-2 text-center text-sm text-
|
| 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-
|
| 38 |
-
<InputText @bind-Value="_loginRequest.Email" type="email" required class="appearance-none relative block w-full px-3 py-2 border border-
|
| 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-
|
| 44 |
</div>
|
| 45 |
-
<InputText type="password" @bind-Value="_loginRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-
|
| 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-
|
| 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-
|
| 65 |
-
<a href="/register" class="text-sm font-medium text-
|
| 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-
|
| 18 |
-
<p class="mt-2 text-
|
| 19 |
Manage library members and administrator accounts.
|
| 20 |
-
<span class="ml-2 text-xs text-
|
| 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-
|
| 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-
|
| 42 |
-
<thead class="bg-
|
| 43 |
<tr>
|
| 44 |
-
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-
|
| 45 |
-
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-
|
| 46 |
-
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-
|
| 47 |
-
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-
|
| 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-
|
| 52 |
@foreach (var user in _pagedResult.Items)
|
| 53 |
{
|
| 54 |
-
<tr class="hover:bg-
|
| 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-
|
| 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-
|
| 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-
|
| 67 |
}
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
</td>
|
| 71 |
<td class="px-6 py-4 whitespace-nowrap">
|
| 72 |
-
<div class="text-sm text-
|
| 73 |
-
<div class="text-xs text-
|
| 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-
|
| 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-
|
| 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-
|
| 116 |
-
<p class="text-sm text-
|
| 117 |
-
Showing <span class="font-medium text-
|
| 118 |
–
|
| 119 |
-
<span class="font-medium text-
|
| 120 |
-
of <span class="font-medium text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 202 |
<div>
|
| 203 |
-
<h4 class="text-[10px] font-bold text-
|
| 204 |
-
<p class="text-sm font-semibold text-
|
| 205 |
</div>
|
| 206 |
<div>
|
| 207 |
-
<h4 class="text-[10px] font-bold text-
|
| 208 |
-
<p class="text-sm font-semibold text-
|
| 209 |
</div>
|
| 210 |
<div>
|
| 211 |
-
<h4 class="text-[10px] font-bold text-
|
| 212 |
-
<p class="text-sm font-semibold text-
|
| 213 |
</div>
|
| 214 |
<div>
|
| 215 |
-
<h4 class="text-[10px] font-bold text-
|
| 216 |
-
<p class="text-sm font-semibold text-
|
| 217 |
</div>
|
| 218 |
<div class="sm:col-span-2">
|
| 219 |
-
<h4 class="text-[10px] font-bold text-
|
| 220 |
-
<p class="text-sm font-semibold text-
|
| 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-
|
| 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-
|
| 238 |
-
<h3 class="text-2xl font-bold text-
|
| 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-
|
| 258 |
-
<p class="text-
|
| 259 |
</div>
|
| 260 |
}
|
| 261 |
|
| 262 |
-
<h4 class="text-lg font-semibold text-
|
| 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-
|
| 268 |
@if (isCurrent)
|
| 269 |
{
|
| 270 |
-
<div class="absolute top-0 right-0 -mt-2 -mr-2 bg-
|
| 271 |
}
|
| 272 |
-
<h5 class="font-bold text-
|
| 273 |
-
<p class="text-
|
| 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-
|
| 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-
|
| 302 |
-
<p class="mt-2 text-
|
| 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-
|
| 313 |
<div class="mt-1">
|
| 314 |
-
<InputText @bind-Value="_createRequest.FullName" class="block w-full rounded-md border-
|
| 315 |
<ValidationMessage For="@(() => _createRequest.FullName)" class="mt-1 text-xs text-rose-500" />
|
| 316 |
</div>
|
| 317 |
</div>
|
| 318 |
-
|
| 319 |
-
<label class="block text-sm font-medium text-
|
| 320 |
<div class="mt-1">
|
| 321 |
-
<InputText @bind-Value="_createRequest.Email" type="email" class="block w-full rounded-md border-
|
| 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-
|
| 327 |
<div class="mt-1">
|
| 328 |
-
<InputText @bind-Value="_createRequest.Password" type="password" class="block w-full rounded-md border-
|
| 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-
|
| 334 |
<div class="mt-1">
|
| 335 |
-
<InputText @bind-Value="_createRequest.PhoneNumber" class="block w-full rounded-md border-
|
| 336 |
</div>
|
| 337 |
</div>
|
| 338 |
<div>
|
| 339 |
-
<label class="block text-sm font-medium text-
|
| 340 |
<div class="mt-1">
|
| 341 |
-
<InputText @bind-Value="_createRequest.StudentId" class="block w-full rounded-md border-
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
<div class="sm:col-span-2">
|
| 345 |
-
<label class="block text-sm font-medium text-
|
| 346 |
<div class="mt-1">
|
| 347 |
-
<InputTextArea @bind-Value="_createRequest.Address" rows="3" class="block w-full rounded-md border-
|
| 348 |
</div>
|
| 349 |
</div>
|
| 350 |
</div>
|
| 351 |
-
<div class="pt-5 border-t border-
|
| 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-
|
| 364 |
<div class="mt-1">
|
| 365 |
-
<InputText @bind-Value="_updateRequest.FullName" class="block w-full rounded-md border-
|
| 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-
|
| 371 |
<div class="mt-1">
|
| 372 |
-
<InputText @bind-Value="_updateRequest.PhoneNumber" class="block w-full rounded-md border-
|
| 373 |
</div>
|
| 374 |
</div>
|
| 375 |
<div>
|
| 376 |
-
<label class="block text-sm font-medium text-
|
| 377 |
<div class="mt-1">
|
| 378 |
-
<InputText @bind-Value="_updateRequest.StudentId" class="block w-full rounded-md border-
|
| 379 |
</div>
|
| 380 |
</div>
|
| 381 |
<div class="sm:col-span-2">
|
| 382 |
-
<label class="block text-sm font-medium text-
|
| 383 |
<div class="mt-1">
|
| 384 |
-
<InputTextArea @bind-Value="_updateRequest.Address" rows="3" class="block w-full rounded-md border-
|
| 385 |
</div>
|
| 386 |
</div>
|
| 387 |
</div>
|
| 388 |
-
<div class="pt-5 border-t border-
|
| 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-
|
| 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-
|
| 24 |
<div>
|
| 25 |
-
<h1 class="text-3xl font-extrabold text-
|
| 26 |
-
<p class="text-
|
| 27 |
</div>
|
| 28 |
|
| 29 |
@if (_viewModel.LoyaltyAccount != null)
|
| 30 |
{
|
| 31 |
-
<div class="flex items-center gap-4 bg-
|
| 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-
|
| 37 |
-
<div class="text-2xl font-black text-
|
| 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-
|
| 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-
|
| 72 |
<div class="text-lg font-bold">@_viewModel.CurrentSubscription.StartDate.ToString("MMM dd, yyyy")</div>
|
| 73 |
</div>
|
| 74 |
<div>
|
| 75 |
-
<div class="text-
|
| 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-
|
| 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-
|
| 90 |
-
<div class="bg-
|
| 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-
|
| 94 |
-
<p class="text-
|
| 95 |
-
<a href="Membership#plans" class="btn-primary px-8 py-3 rounded-xl shadow-lg shadow-
|
| 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-
|
| 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-
|
| 118 |
-
<div class="text-sm text-
|
| 119 |
Activates on @queued.StartDate.ToString("MMM dd, yyyy") • 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-
|
| 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-
|
| 137 |
-
<h3 class="text-lg font-bold text-
|
| 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-
|
| 146 |
-
<div class="text-sm text-
|
| 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-
|
| 153 |
-
<div class="text-sm text-
|
| 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-
|
| 162 |
-
<div class="text-sm text-
|
| 163 |
</div>
|
| 164 |
</li>
|
| 165 |
</ul>
|
| 166 |
|
| 167 |
-
<div class="mt-10 p-5 bg-
|
| 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 →</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-
|
| 179 |
-
<p class="text-
|
| 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-
|
| 188 |
@if (isCurrent)
|
| 189 |
{
|
| 190 |
-
<div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-
|
| 191 |
}
|
| 192 |
|
| 193 |
<div class="mb-8">
|
| 194 |
-
<h3 class="text-sm font-black text-
|
| 195 |
<div class="flex items-baseline">
|
| 196 |
-
<span class="text-3xl font-black text-
|
| 197 |
-
<span class="text-
|
| 198 |
</div>
|
| 199 |
-
<div class="text-
|
| 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-
|
| 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-
|
| 208 |
</li>
|
| 209 |
<li class="flex items-center gap-3 text-sm">
|
| 210 |
-
<div class="h-5 w-5 rounded-full bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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") • 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 →</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-
|
| 14 |
<div>
|
| 15 |
-
<div class="mx-auto h-12 w-12 bg-
|
| 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-
|
| 19 |
-
<p class="mt-2 text-center text-sm text-
|
| 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-
|
| 37 |
-
<InputText @bind-Value="_registerRequest.FullName" required class="appearance-none relative block w-full px-3 py-2 border border-
|
| 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-
|
| 42 |
-
<InputText type="email" @bind-Value="_registerRequest.Email" required class="appearance-none relative block w-full px-3 py-2 border border-
|
| 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-
|
| 47 |
-
<InputText type="tel" @bind-Value="_registerRequest.PhoneNumber" class="appearance-none relative block w-full px-3 py-2 border border-
|
| 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-
|
| 52 |
-
<InputText type="password" @bind-Value="_registerRequest.Password" required class="appearance-none relative block w-full px-3 py-2 border border-
|
| 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-
|
| 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-
|
| 72 |
-
<a href="/login" class="text-sm font-medium text-
|
| 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-
|
| 12 |
-
<p class="text-
|
| 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-
|
| 20 |
<p class="text-xs text-amber-600 font-semibold">Points</p>
|
| 21 |
</div>
|
| 22 |
-
<div class="text-center bg-
|
| 23 |
-
<p class="text-[10px] font-bold text-
|
| 24 |
-
<p class="text-lg font-black text-
|
| 25 |
</div>
|
| 26 |
</div>
|
| 27 |
}
|
| 28 |
</div>
|
| 29 |
|
| 30 |
-
<div class="mb-6 border-b border-
|
| 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-
|
| 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-
|
| 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-
|
| 52 |
}
|
| 53 |
</button>
|
| 54 |
<button @onclick='() => _activeTab = "history"'
|
| 55 |
-
class='pb-3 text-sm font-semibold border-b-2 transition-all @(_activeTab == "history" ? "border-
|
| 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
|
| 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-
|
| 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-
|
| 80 |
-
<p class="text-
|
| 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-
|
| 90 |
-
<div class="h-1.5 @(canAfford ? "bg-gradient-to-r from-amber-400 to-orange-400" : "bg-
|
| 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-
|
| 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-
|
| 98 |
-
<span class="text-[10px] block font-bold text-
|
| 99 |
</div>
|
| 100 |
</div>
|
| 101 |
-
<h3 class="text-base font-bold text-
|
| 102 |
-
<p class="text-sm text-
|
| 103 |
-
<div class="flex items-center justify-between pt-4 border-t border-
|
| 104 |
-
<span class="text-xs font-semibold text-
|
| 105 |
@if (canAfford)
|
| 106 |
{
|
| 107 |
<button @onclick="() => RedeemAsync(reward)"
|
| 108 |
-
class="bg-
|
| 109 |
Redeem
|
| 110 |
</button>
|
| 111 |
}
|
| 112 |
else
|
| 113 |
{
|
| 114 |
-
<span class="text-xs font-bold text-
|
| 115 |
}
|
| 116 |
</div>
|
| 117 |
</div>
|
|
@@ -123,12 +123,12 @@ else
|
|
| 123 |
}
|
| 124 |
</div>
|
| 125 |
}
|
| 126 |
-
<div class="mt-8 p-7 bg-
|
| 127 |
<div>
|
| 128 |
<h2 class="text-lg font-bold mb-1">How to earn more points?</h2>
|
| 129 |
-
<p class="text-
|
| 130 |
</div>
|
| 131 |
-
<a href="Books" class="bg-amber-400 text-
|
| 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-
|
| 141 |
-
<p class="text-
|
| 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-
|
| 156 |
-
<p class="text-xs text-
|
| 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-
|
| 175 |
-
<p class="text-
|
| 176 |
</div>
|
| 177 |
}
|
| 178 |
else
|
| 179 |
{
|
| 180 |
-
<div class="bg-white border border-
|
| 181 |
-
<table class="min-w-full divide-y divide-
|
| 182 |
-
<thead class="bg-
|
| 183 |
<tr>
|
| 184 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 185 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 186 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 187 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 188 |
-
<th class="px-6 py-3 text-right text-xs font-bold text-
|
| 189 |
</tr>
|
| 190 |
</thead>
|
| 191 |
-
<tbody class="divide-y divide-
|
| 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-
|
| 199 |
-
<td class="px-6 py-3 text-sm text-
|
| 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-
|
| 204 |
-
<td class="px-6 py-3 text-sm text-
|
| 205 |
@if (!string.IsNullOrWhiteSpace(spentOn))
|
| 206 |
{
|
| 207 |
-
<span class="font-semibold text-
|
| 208 |
}
|
| 209 |
else
|
| 210 |
{
|
| 211 |
-
<span class="text-
|
| 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-
|
| 14 |
-
<p class="text-
|
| 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-
|
| 20 |
</div>
|
| 21 |
-
<div class="bg-
|
| 22 |
-
<p class="text-[10px] font-bold text-
|
| 23 |
-
<p class="text-2xl font-black text-
|
| 24 |
</div>
|
| 25 |
</div>
|
| 26 |
</div>
|
| 27 |
|
| 28 |
-
<div class="mb-6 border-b border-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 69 |
-
<svg class="w-16 h-16 mx-auto opacity-20 text-
|
| 70 |
-
<p class="text-
|
| 71 |
</div>
|
| 72 |
}
|
| 73 |
else
|
| 74 |
{
|
| 75 |
-
<div class="bg-white border border-
|
| 76 |
-
<table class="min-w-full divide-y divide-
|
| 77 |
-
<thead class="bg-
|
| 78 |
<tr>
|
| 79 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 80 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 81 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 82 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 83 |
-
<th class="px-6 py-3 text-left text-xs font-bold text-
|
| 84 |
-
<th class="px-6 py-3 text-right text-xs font-bold text-
|
| 85 |
</tr>
|
| 86 |
</thead>
|
| 87 |
-
<tbody class="divide-y divide-
|
| 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-
|
| 94 |
@(r.ExternalUserId.Length >= 2 ? r.ExternalUserId.Substring(0, 2).ToUpper() : "U")
|
| 95 |
</div>
|
| 96 |
-
<span class="text-sm font-medium text-
|
| 97 |
</div>
|
| 98 |
</td>
|
| 99 |
<td class="px-6 py-4">
|
| 100 |
-
<p class="text-sm font-bold text-
|
| 101 |
-
<p class="text-xs text-
|
| 102 |
</td>
|
| 103 |
-
<td class="px-6 py-4 text-sm font-bold text-
|
| 104 |
-
<td class="px-6 py-4 text-sm text-
|
| 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-
|
| 132 |
-
<p class="text-
|
| 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-
|
| 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-
|
| 148 |
<!-- Member header -->
|
| 149 |
-
<div class="flex items-center justify-between px-6 py-4 bg-
|
| 150 |
@onclick="() => ToggleMember(member.AccountId)">
|
| 151 |
<div class="flex items-center gap-4">
|
| 152 |
-
<div class="w-10 h-10 rounded-full bg-
|
| 153 |
@(member.UserName.Length >= 2 ? member.UserName.Substring(0, 2).ToUpper() : "?")
|
| 154 |
</div>
|
| 155 |
<div>
|
| 156 |
-
<p class="font-bold text-
|
| 157 |
-
<p class="text-xs text-
|
| 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-
|
| 163 |
-
<p class="font-black text-
|
| 164 |
</div>
|
| 165 |
<div class="text-right">
|
| 166 |
-
<p class="text-xs font-bold text-
|
| 167 |
-
<span class="text-xs font-bold px-2 py-0.5 rounded-full bg-
|
| 168 |
</div>
|
| 169 |
<div class="text-right">
|
| 170 |
-
<p class="text-xs font-bold text-
|
| 171 |
-
<p class="font-black text-
|
| 172 |
</div>
|
| 173 |
-
<svg class="w-4 h-4 text-
|
| 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-
|
| 184 |
}
|
| 185 |
else
|
| 186 |
{
|
| 187 |
-
<table class="min-w-full divide-y divide-
|
| 188 |
<thead>
|
| 189 |
-
<tr class="bg-
|
| 190 |
-
<th class="px-6 py-2 text-left text-[10px] font-bold text-
|
| 191 |
-
<th class="px-6 py-2 text-left text-[10px] font-bold text-
|
| 192 |
-
<th class="px-6 py-2 text-left text-[10px] font-bold text-
|
| 193 |
-
<th class="px-6 py-2 text-left text-[10px] font-bold text-
|
| 194 |
-
<th class="px-6 py-2 text-right text-[10px] font-bold text-
|
| 195 |
</tr>
|
| 196 |
</thead>
|
| 197 |
-
<tbody class="divide-y divide-
|
| 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-
|
| 205 |
-
<td class="px-6 py-2.5 text-xs text-
|
| 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-
|
| 210 |
-
<td class="px-6 py-2.5 text-xs text-
|
| 211 |
@if (!string.IsNullOrWhiteSpace(spentOn))
|
| 212 |
{
|
| 213 |
-
<span class="font-semibold text-
|
| 214 |
}
|
| 215 |
else
|
| 216 |
{
|
| 217 |
-
<span class="text-
|
| 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")">
|