Yuyuqt
fix: add Librarian approval for pay in cash, balance update loading time fix, membership approval page added
6469525
Raw
History Blame Contribute Delete
18.7 kB
@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);
}
}
}