Spaces:
Sleeping
Sleeping
Yuyuqt
fix: add Librarian approval for pay in cash, balance update loading time fix, membership approval page added
6469525 | @page "/membership" | |
| @using LibraryManagement.Shared.Models | |
| @using BlazorWebAssembly.Services | |
| @inject LibraryApiClient ApiClient | |
| @inject NavigationManager Navigation | |
| @inject IJSRuntime JS | |
| <div class="max-w-7xl mx-auto py-24 px-8 animate-fade"> | |
| <!-- Header --> | |
| <div class="text-center mb-24"> | |
| <h2 class="text-xs font-black text-primary uppercase tracking-[0.3em] mb-4">Pricing Plans</h2> | |
| <h1 class="text-7xl font-black tracking-tighter text-main mb-8 uppercase leading-[0.9]">Choose Your <br /> <span class="text-primary italic">Journey.</span></h1> | |
| <p class="text-xl text-muted max-w-2xl mx-auto leading-relaxed"> | |
| From casual readers to devoted bookworms, we have a plan that perfectly matches your literary appetite. | |
| </p> | |
| </div> | |
| @if (_isLoading) | |
| { | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-8"> | |
| @for (int i = 0; i < 3; i++) | |
| { | |
| <div class="animate-pulse bg-card rounded-[3rem] border border-border h-[600px]"></div> | |
| } | |
| </div> | |
| } | |
| else | |
| { | |
| <!-- Auth Section --> | |
| <AuthorizeView> | |
| <Authorized> | |
| <!-- Current Subscription Banner --> | |
| @if (_currentSub != null) | |
| { | |
| <div class="bg-primary/5 border border-primary/20 rounded-[2.5rem] p-10 mb-12 flex flex-col md:flex-row items-center justify-between gap-8 shadow-sm relative overflow-hidden group"> | |
| <div class="absolute top-0 right-0 p-8 opacity-5 group-hover:opacity-10 transition-opacity"> | |
| <svg class="w-32 h-32" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> | |
| </div> | |
| <div class="flex items-center gap-8 relative z-10"> | |
| <div class="w-20 h-20 bg-primary rounded-3xl flex items-center justify-center text-white text-4xl font-black shadow-lg shadow-primary/20"> | |
| @(_currentSub.MembershipType?[..1].ToUpper()) | |
| </div> | |
| <div> | |
| <h3 class="text-3xl font-black text-main uppercase tracking-tight">@_currentSub.MembershipType Plan</h3> | |
| <p class="text-muted font-medium">Active until <span class="text-main font-bold">@_currentSub.ExpiryDate.ToString("MMMM dd, yyyy")</span></p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-6 relative z-10"> | |
| <div class="flex flex-col items-end"> | |
| <span class="px-4 py-1.5 bg-green-500/10 text-green-600 rounded-full text-[10px] font-black uppercase tracking-widest mb-2">Active Status</span> | |
| <span class="text-xs text-muted font-bold uppercase tracking-widest">Auto-renews monthly</span> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| <!-- Wallet & Loyalty Summary (Always visible for logged in) --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-12"> | |
| <div class="bg-card border border-border rounded-3xl p-6 flex items-center justify-between shadow-sm"> | |
| <div class="flex items-center gap-4"> | |
| <div class="w-12 h-12 bg-primary/10 rounded-2xl flex items-center justify-center text-primary"> | |
| <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg> | |
| </div> | |
| <div> | |
| <p class="text-[10px] font-black text-muted uppercase tracking-widest">Loyalty Points</p> | |
| <p class="text-xl font-black text-main">@(_loyaltyAccount?.CurrentBalance.ToString("N0") ?? "0")</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-card border border-border rounded-3xl p-6 flex items-center justify-between shadow-sm"> | |
| <div class="flex items-center gap-4"> | |
| <div class="w-12 h-12 bg-emerald-500/10 rounded-2xl flex items-center justify-center text-emerald-500"> | |
| <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> | |
| </div> | |
| <div> | |
| <p class="text-[10px] font-black text-muted uppercase tracking-widest">Wallet Balance</p> | |
| <p class="text-xl font-black text-main">@_walletBalance.ToString("N0") <span class="text-xs font-bold text-muted">MMK</span></p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| @if (_queuedRewards.Any()) | |
| { | |
| <div class="mb-20 animate-fade-in-down"> | |
| <div class="flex items-center gap-3 mb-8"> | |
| <div class="h-px flex-grow bg-border"></div> | |
| <h3 class="text-xs font-black text-amber-500 uppercase tracking-[0.4em] px-4 whitespace-nowrap">Queued Rewards</h3> | |
| <div class="h-px flex-grow bg-border"></div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| @foreach (var sub in _queuedRewards) | |
| { | |
| <div class="bg-card border border-amber-500/20 rounded-3xl p-8 flex items-center justify-between shadow-sm hover:shadow-lg transition-all border-l-4 border-l-amber-500 relative overflow-hidden group"> | |
| <div class="absolute top-0 right-0 p-4 opacity-[0.03] group-hover:opacity-[0.07] transition-opacity"> | |
| <svg class="w-20 h-20" fill="currentColor" viewBox="0 0 24 24"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg> | |
| </div> | |
| <div class="flex items-center gap-6 relative z-10"> | |
| <div class="w-14 h-14 bg-amber-500/10 rounded-2xl flex items-center justify-center text-amber-500"> | |
| <svg class="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> | |
| </div> | |
| <div> | |
| <h4 class="text-xl font-black text-main uppercase">@sub.MembershipType</h4> | |
| <p class="text-sm text-muted font-medium">Starts on @sub.StartDate.ToString("MMM dd, yyyy")</p> | |
| </div> | |
| </div> | |
| <div class="text-right relative z-10"> | |
| <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-green-500/10 text-green-500"> | |
| <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg> | |
| Approved | |
| </span> | |
| <p class="text-[10px] text-muted font-bold uppercase tracking-widest mt-2">Queued in Pipeline</p> | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| </div> | |
| } | |
| </Authorized> | |
| </AuthorizeView> | |
| <!-- Membership Plans --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-10"> | |
| @foreach (var plan in _memberships) | |
| { | |
| <div class='relative bg-card rounded-[3.5rem] border p-12 flex flex-col transition-all duration-700 hover:-translate-y-4 @(IsCurrentPlan(plan.Id) ? "border-primary shadow-2xl shadow-primary/10 scale-105 z-10" : "border-border shadow-xl hover:shadow-2xl")'> | |
| @if (plan.Price > 20 && !IsCurrentPlan(plan.Id)) | |
| { | |
| <div class="absolute -top-5 left-1/2 -translate-x-1/2 bg-emerald-500 text-white px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-emerald-500/30"> | |
| Most Popular | |
| </div> | |
| } | |
| @if (IsCurrentPlan(plan.Id)) | |
| { | |
| <div class="absolute -top-5 left-1/2 -translate-x-1/2 bg-primary text-white px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-[0.2em] shadow-lg shadow-primary/30"> | |
| Your Current Plan | |
| </div> | |
| } | |
| <div class="mb-10"> | |
| <h3 class="text-sm font-black text-muted uppercase tracking-[0.3em] mb-6">@plan.Type</h3> | |
| <div class="flex flex-col gap-1"> | |
| <div class="flex items-baseline gap-2 flex-wrap"> | |
| <span class="text-5xl font-black text-main tracking-tighter">@plan.Price.ToString("N0")</span> | |
| <span class="text-2xl font-bold text-muted">mmk</span> | |
| </div> | |
| <span class="text-muted font-bold uppercase text-sm tracking-widest mt-1">/ @plan.DurationMonths mo</span> | |
| </div> | |
| </div> | |
| <div class="space-y-6 mb-16 flex-grow"> | |
| <div class="flex items-center gap-5 group/item"> | |
| <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg> | |
| </div> | |
| <span class="text-main font-bold text-lg">Borrow <span class="text-primary">@plan.MaxBooks</span> books</span> | |
| </div> | |
| <div class="flex items-center gap-5 group/item"> | |
| <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg> | |
| </div> | |
| <span class="text-main font-bold text-lg">@plan.BorrowingDays Day period</span> | |
| </div> | |
| <div class="flex items-center gap-5 group/item"> | |
| <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg> | |
| </div> | |
| <span class="text-main font-bold text-lg">Digital access included</span> | |
| </div> | |
| <div class="flex items-center gap-5 group/item"> | |
| <div class="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary group-hover/item:scale-110 transition-transform flex-shrink-0"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path></svg> | |
| </div> | |
| <span class="text-main font-bold text-lg">Loyalty point multiplier</span> | |
| </div> | |
| </div> | |
| <div class="space-y-4 mt-auto"> | |
| <button @onclick="() => SubscribeAsync(plan)" | |
| disabled="@IsCurrentPlan(plan.Id)" | |
| class='w-full py-5 rounded-[2rem] font-black uppercase tracking-widest text-[10px] transition-all @(IsCurrentPlan(plan.Id) ? "bg-body text-muted border border-border cursor-not-allowed" : "bg-main text-white hover:bg-black transform hover:scale-[1.02] active:scale-95")'> | |
| @(IsCurrentPlan(plan.Id) ? "Active Now" : "Pay Cash at Library") | |
| </button> | |
| @if (!IsCurrentPlan(plan.Id)) | |
| { | |
| <button @onclick="() => BuyWithWallet(plan)" | |
| class="w-full py-5 rounded-[2rem] bg-primary text-white font-black uppercase tracking-widest text-[10px] hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all transform hover:scale-[1.02] active:scale-95"> | |
| Buy with Wallet | |
| </button> | |
| } | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| <div class="mt-24 text-center"> | |
| <p class="text-muted font-medium mb-8">All plans include 24/7 support and access to our community events.</p> | |
| <div class="flex items-center justify-center gap-12 opacity-50 grayscale hover:grayscale-0 transition-all"> | |
| <!-- Trust Badges Placeholder --> | |
| <span class="font-black text-xl uppercase tracking-tighter">Secure Payment</span> | |
| <span class="font-black text-xl uppercase tracking-tighter">Cancel Anytime</span> | |
| <span class="font-black text-xl uppercase tracking-tighter">No Hidden Fees</span> | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| @code { | |
| private bool _isLoading = true; | |
| private List<MembershipDto> _memberships = new(); | |
| private SubscriptionDto? _currentSub; | |
| private List<SubscriptionDto> _queuedRewards = new(); | |
| private decimal _walletBalance; | |
| private LoyaltyAccountDto? _loyaltyAccount; | |
| protected override async Task OnInitializedAsync() | |
| { | |
| await LoadData(); | |
| } | |
| private async Task LoadData() | |
| { | |
| _isLoading = true; | |
| try | |
| { | |
| _memberships = (await ApiClient.GetMembershipsAsync()).ToList(); | |
| _currentSub = await ApiClient.GetMySubscriptionAsync(); | |
| var allSubs = await ApiClient.GetMyAllSubscriptionsAsync(); | |
| _queuedRewards = allSubs | |
| .Where(s => !string.IsNullOrEmpty(s.ExternalRedemptionId) && s.StartDate > DateTime.UtcNow) | |
| .OrderBy(s => s.StartDate) | |
| .ToList(); | |
| _walletBalance = await ApiClient.GetWalletBalanceAsync(); | |
| _loyaltyAccount = await ApiClient.GetMyLoyaltyAccountAsync(); | |
| } | |
| catch { } | |
| finally { _isLoading = false; } | |
| } | |
| private bool IsCurrentPlan(int id) => _currentSub?.MembershipId == id; | |
| private async Task SubscribeAsync(MembershipDto plan) | |
| { | |
| bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Are you sure you want to subscribe to {plan.Type} for {plan.Price:N0} mmk? Please pay the cash at the library counter to activate."); | |
| if (!confirmed) return; | |
| var result = await ApiClient.SubscribeAsync(new SubscribeRequest { MembershipId = plan.Id }); | |
| if (result != null) | |
| { | |
| await JS.InvokeVoidAsync("alert", "Request submitted! Please visit the librarian to pay and activate."); | |
| await LoadData(); | |
| } | |
| } | |
| private async Task BuyWithWallet(MembershipDto plan) | |
| { | |
| try | |
| { | |
| var preview = await ApiClient.GetUpgradePreviewAsync(plan.Id); | |
| if (preview == null || !preview.CanUpgrade) | |
| { | |
| await JS.InvokeVoidAsync("alert", preview?.Message ?? "Cannot upgrade to this plan."); | |
| return; | |
| } | |
| string confirmMsg = $"Purchase {plan.Type} membership?\n\n" + | |
| $"Price: {preview.OriginalPrice:N0} MMK\n"; | |
| if (preview.DiscountAmount > 0) | |
| { | |
| confirmMsg += $"Upgrade Discount: -{preview.DiscountAmount:N0} MMK\n"; | |
| } | |
| confirmMsg += $"Total: {preview.FinalPrice:N0} MMK\n\n" + | |
| $"Your Balance: {_walletBalance:N0} MMK"; | |
| bool confirmed = await JS.InvokeAsync<bool>("confirm", confirmMsg); | |
| if (!confirmed) return; | |
| if (_walletBalance < preview.FinalPrice) | |
| { | |
| await JS.InvokeVoidAsync("alert", "Insufficient wallet balance. Please top up at the library counter."); | |
| return; | |
| } | |
| var result = await ApiClient.SubscribeWithWalletAsync(new SubscribeRequest { MembershipId = plan.Id }); | |
| if (result != null) | |
| { | |
| await JS.InvokeVoidAsync("alert", "Subscription purchased successfully!"); | |
| await LoadData(); | |
| } | |
| else | |
| { | |
| await JS.InvokeVoidAsync("alert", "Failed to complete purchase."); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| await JS.InvokeVoidAsync("alert", "Error: " + ex.Message); | |
| } | |
| } | |
| } | |