Spaces:
Sleeping
Sleeping
| @page "/Membership" | |
| @using BlazorFrontend.Services | |
| @using LibraryManagement.Shared.Models | |
| @using BlazorFrontend.Models | |
| @inject LibraryApiClient ApiClient | |
| @inject IJSRuntime JSRuntime | |
| @inject NavigationManager Navigation | |
| <PageTitle>My Membership</PageTitle> | |
| @if (_isLoading) | |
| { | |
| <div class="flex items-center justify-center min-h-[400px]"> | |
| <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mystic-900"></div> | |
| </div> | |
| } | |
| else if (_viewModel != null) | |
| { | |
| var hasActiveSub = _viewModel.CurrentSubscription != null && _viewModel.CurrentSubscription.IsActive; | |
| <div class="space-y-10 animate-in fade-in slide-in-from-bottom-4 duration-700"> | |
| <!-- Header with Loyalty Summary --> | |
| <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"> | |
| <div> | |
| <h1 class="text-3xl font-extrabold text-mystic-900 tracking-tight">Membership</h1> | |
| <p class="text-mystic-500 mt-1">Manage your subscription and library benefits</p> | |
| </div> | |
| @if (_viewModel.LoyaltyAccount != null) | |
| { | |
| <div class="flex items-center gap-4 bg-mystic-50 px-6 py-4 rounded-xl border border-mystic-200/60"> | |
| <div class="bg-amber-100 p-2.5 rounded-lg text-amber-600"> | |
| <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> | |
| </div> | |
| <div> | |
| <div class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Available Points</div> | |
| <div class="text-2xl font-black text-mystic-900 leading-none">@_viewModel.LoyaltyAccount.CurrentBalance.ToString("N0")</div> | |
| </div> | |
| </div> | |
| } | |
| <div class="flex items-center gap-4 bg-mystic-50 px-6 py-4 rounded-xl border border-mystic-200/60"> | |
| <div class="bg-indigo-100 p-2.5 rounded-lg text-indigo-600"> | |
| <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"><rect width="20" height="12" x="2" y="6" rx="2"/><circle cx="12" cy="12" r="2"/><path d="M6 12h.01M18 12h.01"/></svg> | |
| </div> | |
| <div> | |
| <div class="text-xs font-bold text-mystic-400 uppercase tracking-widest">Wallet Balance</div> | |
| <div class="text-2xl font-black text-mystic-900 leading-none">@_viewModel.WalletBalance.ToString("N0") <span class="text-xs font-bold text-mystic-400">MMK</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Current Subscription Card --> | |
| <div class="lg:col-span-2 space-y-8"> | |
| <section> | |
| <h2 class="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2"> | |
| <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-slate-400"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg> | |
| Active Plan | |
| </h2> | |
| @if (hasActiveSub && _viewModel.CurrentSubscription != null) | |
| { | |
| <div class="relative overflow-hidden bg-mystic-900 rounded-3xl p-8 text-white group shadow-xl"> | |
| <!-- Decorative element --> | |
| <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> | |
| <div class="relative z-10"> | |
| <div class="flex justify-between items-start mb-10"> | |
| <div> | |
| <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-green-500/20 text-green-400 border border-green-500/30 mb-3 uppercase tracking-wider">Active Status</span> | |
| <h3 class="text-4xl font-black tracking-tight">@_viewModel.CurrentSubscription.MembershipType</h3> | |
| </div> | |
| <div class="h-14 w-14 bg-white/10 rounded-2xl flex items-center justify-center backdrop-blur-md"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" 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> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-8 py-8 border-t border-white/10"> | |
| <div> | |
| <div class="text-mystic-400 text-sm font-medium mb-1">Start Date</div> | |
| <div class="text-lg font-bold">@_viewModel.CurrentSubscription.StartDate.ToString("MMM dd, yyyy")</div> | |
| </div> | |
| <div> | |
| <div class="text-mystic-400 text-sm font-medium mb-1">Expiry Date</div> | |
| <div class="text-lg font-bold">@_viewModel.CurrentSubscription.ExpiryDate.ToString("MMM dd, yyyy")</div> | |
| </div> | |
| </div> | |
| <div class="pt-6 flex items-center gap-2 text-mystic-400 text-sm italic"> | |
| <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> | |
| Pro-tip: Maintain your subscription to earn loyalty points every month! | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| else | |
| { | |
| <div class="bg-white border-2 border-dashed border-mystic-200 rounded-3xl p-12 text-center"> | |
| <div class="bg-mystic-50 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 text-mystic-400"> | |
| <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> | |
| </div> | |
| <h3 class="text-xl font-bold text-mystic-900">No Active Subscription</h3> | |
| <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> | |
| <a href="Membership#plans" class="btn-primary px-8 py-3 rounded-xl shadow-lg shadow-mystic-900/10">Browse Plans</a> | |
| </div> | |
| } | |
| </section> | |
| <!-- Queued Memberships --> | |
| @if (_viewModel.QueuedMemberships.Any()) | |
| { | |
| <section> | |
| <h2 class="text-xl font-bold text-mystic-900 mb-4 flex items-center gap-2"> | |
| <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> | |
| Queued Memberships (@_viewModel.QueuedMemberships.Count) | |
| </h2> | |
| <div class="space-y-3"> | |
| @foreach (var queued in _viewModel.QueuedMemberships) | |
| { | |
| <div class="flex items-center justify-between bg-amber-50 border border-amber-100 p-5 rounded-2xl shadow-sm"> | |
| <div class="flex items-center gap-4"> | |
| <div class="h-12 w-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shadow-sm border border-amber-100"> | |
| <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> | |
| </div> | |
| <div> | |
| <div class="font-bold text-mystic-900">@queued.MembershipType Membership</div> | |
| <div class="text-sm text-mystic-500"> | |
| Activates on @queued.StartDate.ToString("MMM dd, yyyy") • Expires @queued.ExpiryDate.ToString("MMM dd, yyyy") | |
| </div> | |
| </div> | |
| </div> | |
| <div class="text-right"> | |
| <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> | |
| <div class="text-xs text-mystic-400 mt-1">Will activate automatically</div> | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| </section> | |
| } | |
| </div> | |
| <!-- Right Side Panel: Benefits --> | |
| <div class="space-y-8"> | |
| <div class="bg-white border border-mystic-100 rounded-3xl p-8 shadow-sm"> | |
| <h3 class="text-lg font-bold text-mystic-900 mb-6">Your Benefits</h3> | |
| <ul class="space-y-6"> | |
| @{ | |
| var currentPlan = _viewModel.AvailableMemberships.FirstOrDefault(m => hasActiveSub && _viewModel.CurrentSubscription != null && m.Type == _viewModel.CurrentSubscription.MembershipType); | |
| } | |
| <li class="flex gap-4"> | |
| <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> | |
| <div> | |
| <div class="font-bold text-mystic-900">Max Books</div> | |
| <div class="text-sm text-mystic-500">Number of books you can borrow simultaneously.</div> | |
| </div> | |
| </li> | |
| <li class="flex gap-4"> | |
| <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> | |
| <div> | |
| <div class="font-bold text-mystic-900">Borrowing Days</div> | |
| <div class="text-sm text-mystic-500">Duration you can keep each borrowed book.</div> | |
| </div> | |
| </li> | |
| <li class="flex gap-4"> | |
| <div class="shrink-0 h-10 w-10 bg-violet-50 text-violet-600 rounded-xl flex items-center justify-center"> | |
| <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> | |
| </div> | |
| <div> | |
| <div class="font-bold text-mystic-900">Digital Access</div> | |
| <div class="text-sm text-mystic-500">Browse and search the full library catalog online.</div> | |
| </div> | |
| </li> | |
| </ul> | |
| <div class="mt-10 p-5 bg-mystic-900 rounded-2xl text-white text-center"> | |
| <p class="text-sm font-medium mb-3">Want more books?</p> | |
| <a href="Membership#plans" class="text-sm font-bold text-indigo-400 hover:text-indigo-300 transition-colors">Upgrade Plan →</a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Explore All Plans Section --> | |
| <section id="plans" class="pt-10"> | |
| <div class="text-center mb-10"> | |
| <h2 class="text-3xl font-black text-mystic-900">Explore Membership Plans</h2> | |
| <p class="text-mystic-500 mt-2">Find the perfect library experience for your reading habits</p> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| @foreach (var plan in _viewModel.AvailableMemberships) | |
| { | |
| var isCurrent = hasActiveSub && _viewModel.CurrentSubscription != null && plan.Type == _viewModel.CurrentSubscription.MembershipType; | |
| <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"> | |
| @if (isCurrent) | |
| { | |
| <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> | |
| } | |
| <div class="mb-8"> | |
| <h3 class="text-sm font-black text-mystic-400 uppercase tracking-widest mb-2">@plan.Type</h3> | |
| <div class="flex items-baseline"> | |
| <span class="text-3xl font-black text-mystic-900">@plan.Price.ToString("N0")</span> | |
| <span class="text-mystic-500 font-bold ml-1 text-sm uppercase">MMK</span> | |
| </div> | |
| <div class="text-mystic-400 text-xs font-bold mt-1">/ @(plan.DurationMonths == 1 ? "Month" : $"{plan.DurationMonths} Months")</div> | |
| </div> | |
| <ul class="space-y-4 mb-10"> | |
| <li class="flex items-center gap-3 text-sm"> | |
| <div class="h-5 w-5 rounded-full bg-mystic-100 flex items-center justify-center text-mystic-900"> | |
| <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> | |
| </div> | |
| <span class="text-mystic-600 font-bold">@plan.MaxBooks Books limit</span> | |
| </li> | |
| <li class="flex items-center gap-3 text-sm"> | |
| <div class="h-5 w-5 rounded-full bg-mystic-100 flex items-center justify-center text-mystic-900"> | |
| <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> | |
| </div> | |
| <span class="text-mystic-600 font-bold">@plan.BorrowingDays Days keep</span> | |
| </li> | |
| @if (!string.IsNullOrEmpty(plan.RewardId) && _viewModel.RewardPointCosts.TryGetValue(plan.RewardId, out var pointCost)) | |
| { | |
| <li class="flex items-center gap-3 text-sm text-amber-600 font-black"> | |
| <div class="h-5 w-5 rounded-full bg-amber-100 flex items-center justify-center text-amber-600"> | |
| <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"><circle cx="8" cy="8" r="6"/><path d="M18.09 10.37A6 6 0 1 1 10.34 18"/></svg> | |
| </div> | |
| <span>@pointCost.ToString("N0") Points</span> | |
| </li> | |
| } | |
| </ul> | |
| @if (isCurrent) | |
| { | |
| <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> | |
| } | |
| else | |
| { | |
| <div class="space-y-3"> | |
| <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> | |
| <button @onclick="() => BuyWithWallet(plan)" | |
| class="w-full py-4 rounded-2xl bg-mystic-900 text-white font-bold text-sm hover:bg-mystic-800 transition-all shadow-lg shadow-mystic-900/20"> | |
| Buy with Wallet | |
| </button> | |
| @if (!string.IsNullOrEmpty(plan.RewardId) && _viewModel.RewardPointCosts.ContainsKey(plan.RewardId)) | |
| { | |
| <button @onclick="() => RedeemMembership(plan.RewardId, plan.Type, _viewModel.RewardPointCosts[plan.RewardId])" | |
| 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"> | |
| Redeem with Points | |
| </button> | |
| } | |
| </div> | |
| } | |
| </div> | |
| } | |
| </div> | |
| </section> | |
| </div> | |
| } | |
| @code { | |
| private MembershipViewModel? _viewModel; | |
| private bool _isLoading = true; | |
| protected override async Task OnInitializedAsync() | |
| { | |
| try | |
| { | |
| var allSubscriptions = await ApiClient.GetMyAllSubscriptionsAsync(); | |
| var memberships = await ApiClient.GetMembershipsAsync(); | |
| var loyaltyAccount = await ApiClient.GetMyLoyaltyAccountAsync(); | |
| var activeRewards = await ApiClient.GetActiveRewardsAsync(); | |
| var now = DateTime.UtcNow; | |
| var currentSubscription = allSubscriptions | |
| .Where(s => s.StartDate <= now && s.ExpiryDate > now) | |
| .OrderByDescending(s => s.StartDate) | |
| .FirstOrDefault(); | |
| var queuedMemberships = allSubscriptions | |
| .Where(s => s.StartDate > now) | |
| .OrderBy(s => s.StartDate) | |
| .ToList(); | |
| _viewModel = new MembershipViewModel | |
| { | |
| CurrentSubscription = currentSubscription, | |
| AvailableMemberships = memberships.ToList(), | |
| QueuedMemberships = queuedMemberships, | |
| LoyaltyAccount = loyaltyAccount, | |
| RewardPointCosts = activeRewards.ToDictionary(r => r.Id, r => r.PointCost), | |
| WalletBalance = await ApiClient.GetWalletBalanceAsync() | |
| }; | |
| } | |
| catch (Exception ex) | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", "Error loading membership data: " + ex.Message, "error"); | |
| } | |
| finally | |
| { | |
| _isLoading = false; | |
| } | |
| } | |
| private async Task RedeemMembership(string rewardId, string planName, double cost) | |
| { | |
| bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm", $"Are you sure you want to redeem {planName} for {cost:N0} points?"); | |
| if (!confirmed) return; | |
| try | |
| { | |
| var (success, message) = await ApiClient.ClaimRewardAsync(rewardId, "Redeemed via Library Membership Page"); | |
| if (success) | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", message, "success"); | |
| await OnInitializedAsync(); // Refresh data | |
| } | |
| else | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", "Error: " + message, "error"); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", "An unexpected error occurred: " + ex.Message, "error"); | |
| } | |
| } | |
| private async Task BuyWithWallet(MembershipDto plan) | |
| { | |
| try | |
| { | |
| var preview = await ApiClient.GetUpgradePreviewAsync(plan.Id); | |
| if (preview == null || !preview.CanUpgrade) | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", preview?.Message ?? "Cannot upgrade to this plan.", "error"); | |
| return; | |
| } | |
| string confirmMsg = $"Are you sure you want to purchase {plan.Type} membership?\n\n" + | |
| $"Original Price: {preview.OriginalPrice:N0} MMK\n"; | |
| if (preview.DiscountAmount > 0) | |
| { | |
| confirmMsg += $"Upgrade Discount: -{preview.DiscountAmount:N0} MMK\n"; | |
| } | |
| confirmMsg += $"Final Price: {preview.FinalPrice:N0} MMK\n\n" + | |
| $"Your Balance: {_viewModel?.WalletBalance:N0} MMK"; | |
| bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm", confirmMsg); | |
| if (!confirmed) return; | |
| if (_viewModel?.WalletBalance < preview.FinalPrice) | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", "Insufficient wallet balance. Please top up at the library.", "error"); | |
| return; | |
| } | |
| var result = await ApiClient.SubscribeWithWalletAsync(new SubscribeRequest { MembershipId = plan.Id }); | |
| if (result != null) | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", "Subscription purchased successfully!", "success"); | |
| await OnInitializedAsync(); | |
| } | |
| else | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", "Failed to complete purchase.", "error"); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| await JSRuntime.InvokeVoidAsync("showToast", "Error: " + ex.Message, "error"); | |
| } | |
| } | |
| } | |