Files
midrandbooks/MidrandBookshop/Components/CartAndCheckout.razor
T
2026-07-18 12:08:14 +02:00

371 lines
22 KiB
Plaintext

@namespace MidrandBooks.Components
@using Microsoft.AspNetCore.Components
@using MidrandBooks.Models
@if (IsOpen)
{
<div class="fixed inset-0 z-50 overflow-hidden bg-slate-950/60 backdrop-blur-sm flex justify-end">
<!-- Cart Drawer Panel -->
<div class="w-full max-w-md bg-white border-l border-slate-200 h-full shadow-2xl flex flex-col justify-between relative animate-in slide-in-from-right duration-200">
<!-- Header -->
<header class="p-5 border-b border-slate-100 flex items-center justify-between">
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-slate-800" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
<h3 class="font-serif text-lg font-bold text-slate-800">Secure Checkout Gate</h3>
</div>
<button @onclick="OnClose" class="p-2 rounded-xl hover:bg-slate-50 text-slate-400 hover:text-slate-600 cursor-pointer">
<svg class="h-5 w-5" 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" />
</svg>
</button>
</header>
<!-- Main Content Scroll Area -->
<div class="flex-1 overflow-y-auto p-5 space-y-6">
@if (CurrentStep == "cart")
{
<!-- Step 1: Cart Listing -->
@if (CartItems.Count > 0)
{
<div class="space-y-4">
@foreach (var item in CartItems)
{
<div class="flex gap-3.5 p-3 rounded-2xl border border-slate-100 bg-slate-50/50">
<div class="h-16 w-11 rounded overflow-hidden shadow-sm flex-shrink-0 border border-slate-100 bg-slate-200">
@if (!string.IsNullOrEmpty(item.Book.CoverUrl))
{
<img src="@item.Book.CoverUrl" alt="@item.Book.Title" class="h-full w-full object-cover" />
}
else
{
<div class="h-full w-full bg-gradient-to-br @item.Book.CoverColor flex items-center justify-center">
<span class="text-[5px] text-white/50">BOOK</span>
</div>
}
</div>
<div class="flex-1 min-w-0">
<h4 class="text-xs font-serif font-bold text-slate-800 truncate">@item.Book.Title</h4>
<p class="text-[10px] text-slate-400">By @item.Book.Author</p>
<span class="inline-block mt-1 text-[8px] font-mono font-bold uppercase rounded px-1.5 py-0.5 @(item.Format == "ebook" ? "bg-indigo-50 text-indigo-700" : "bg-violet-50 text-violet-700")">
@item.Format
</span>
</div>
<div class="text-right flex flex-col justify-between items-end">
<button @onclick="() => RemoveItem(item)" class="p-1.5 text-slate-400 hover:text-red-500 rounded-lg hover:bg-slate-100 transition-colors cursor-pointer">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<span class="font-mono text-xs font-bold text-slate-800">R @item.Price.ToString("F2")</span>
</div>
</div>
}
</div>
}
else
{
<div class="text-center py-16 space-y-3">
<svg class="mx-auto h-12 w-12 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
</svg>
<h4 class="font-serif text-sm font-bold text-slate-700">Your shopping cart is empty</h4>
<p class="text-[10px] text-slate-400 max-w-xs mx-auto leading-relaxed">
Browse our catalogs and add premium South African manuscripts or immersive studio audiobooks to check out.
</p>
</div>
}
}
else if (CurrentStep == "details")
{
<!-- Step 2: PayFast Details Form -->
<div class="space-y-4">
<div class="space-y-1">
<span class="text-[9px] font-mono font-bold text-violet-600 uppercase tracking-widest block">Secure Payment Methods</span>
<h3 class="font-serif text-base font-bold text-slate-800">Complete South African Settlement</h3>
</div>
<!-- Payment Method Toggle -->
<div class="grid grid-cols-2 gap-2 p-1 bg-slate-100 rounded-xl">
<button @onclick='() => SetPaymentMethod("card")' class="py-2 rounded-lg text-xs font-bold transition-all cursor-pointer @(PaymentMethod == "card" ? "bg-white text-slate-900 shadow-sm" : "text-slate-500 hover:text-slate-800")">
Credit / Debit Card
</button>
<button @onclick='() => SetPaymentMethod("eft")' class="py-2 rounded-lg text-xs font-bold transition-all cursor-pointer @(PaymentMethod == "eft" ? "bg-white text-slate-900 shadow-sm" : "text-slate-500 hover:text-slate-800")">
Secure Instant EFT
</button>
</div>
@if (PaymentMethod == "card")
{
<!-- Card Form -->
<div class="space-y-3.5 pt-2">
<div class="space-y-1">
<label class="text-[9px] font-mono font-bold text-slate-500 uppercase block">Cardholder Name</label>
<input type="text" placeholder="e.g. A K Mthembu" @bind="CardName" class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3.5 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15" />
</div>
<div class="space-y-1">
<label class="text-[9px] font-mono font-bold text-slate-500 uppercase block">Card Number</label>
<input type="text" placeholder="xxxx xxxx xxxx xxxx" @bind="CardNumber" class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3.5 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15" />
</div>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1">
<label class="text-[9px] font-mono font-bold text-slate-500 uppercase block">Expiry Date</label>
<input type="text" placeholder="MM/YY" @bind="CardExpiry" class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3.5 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15" />
</div>
<div class="space-y-1">
<label class="text-[9px] font-mono font-bold text-slate-500 uppercase block">CVV</label>
<input type="text" placeholder="123" @bind="CardCvv" class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3.5 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15" />
</div>
</div>
</div>
}
else
{
<!-- EFT Bank Form -->
<div class="space-y-3.5 pt-2">
<div class="space-y-1">
<label class="text-[9px] font-mono font-bold text-slate-500 uppercase block">Select SA Bank</label>
<select @bind="SelectedBank" class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs text-slate-800 outline-none">
<option value="Capitec">Capitec Bank</option>
<option value="Standard Bank">Standard Bank</option>
<option value="FNB">First National Bank (FNB)</option>
<option value="Nedbank">Nedbank</option>
<option value="Absa">Absa</option>
</select>
</div>
<div class="space-y-1">
<label class="text-[9px] font-mono font-bold text-slate-500 uppercase block">Online Banking Username</label>
<input type="text" placeholder="e.g. 10482901" @bind="EftUsername" class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3.5 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15" />
</div>
<div class="space-y-1">
<label class="text-[9px] font-mono font-bold text-slate-500 uppercase block">PIN / Password</label>
<input type="password" placeholder="••••••••" @bind="EftPassword" class="w-full rounded-xl border border-slate-200 bg-slate-50 px-3.5 py-2.5 text-xs text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15" />
</div>
</div>
}
</div>
}
else if (CurrentStep == "processing")
{
<!-- Step 3: Secure Processing Sandbox -->
<div class="text-center py-16 space-y-6">
<div class="relative mx-auto h-16 w-16 flex items-center justify-center">
<div class="absolute inset-0 rounded-full border-4 border-violet-100 border-t-violet-600 animate-spin" />
<svg class="h-6 w-6 text-violet-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<div class="space-y-2">
<h4 class="font-serif text-sm font-bold text-slate-800">Processing Secure Transaction</h4>
<p class="text-[11px] text-slate-500 max-w-xs mx-auto leading-relaxed">
@ProcessingStatus
</p>
</div>
<div class="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
<div class="bg-violet-600 h-full transition-all duration-300" style="width: @(ProcessingProgress)%" />
</div>
</div>
}
else if (CurrentStep == "otp")
{
<!-- Step 4: OTP Screen -->
<div class="space-y-4 pt-4">
<div class="text-center space-y-2">
<div class="mx-auto h-12 w-12 rounded-full bg-amber-50 text-amber-600 flex items-center justify-center border border-amber-100">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<h4 class="font-serif text-sm font-bold text-slate-800">Dual-Factor Verification Required</h4>
<p class="text-[10px] text-slate-500 leading-relaxed max-w-xs mx-auto">
Enter the simulated SMS OTP code dispatched to secure checkout settlement: <strong class="text-slate-800 font-mono text-xs">@GeneratedOtp</strong>
</p>
</div>
<div class="space-y-2 max-w-xs mx-auto">
<input type="text" placeholder="e.g. 482903" @bind="OtpInput" class="w-full text-center tracking-[0.5em] font-mono text-lg rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-slate-800 outline-none focus:bg-white focus:ring-2 focus:ring-violet-500/15" />
@if (!string.IsNullOrEmpty(OtpError))
{
<span class="block text-center text-[10px] font-semibold text-red-500">@OtpError</span>
}
</div>
</div>
}
else if (CurrentStep == "success")
{
<!-- Step 5: Purchase Successful -->
<div class="text-center py-12 space-y-6">
<div class="mx-auto h-16 w-16 rounded-full bg-emerald-50 text-emerald-600 border border-emerald-100 flex items-center justify-center">
<svg class="h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="space-y-2">
<h3 class="font-serif text-base font-bold text-slate-800">Authorization Passed</h3>
<p class="text-[11px] text-slate-500 leading-relaxed max-w-xs mx-auto">
Thank you! Your PayFast checkout transaction was approved. Your newly acquired digital books have been unlocked and compiled into your Secure Library.
</p>
</div>
</div>
}
</div>
<!-- Footer Billing Info & Action Button -->
<footer class="p-5 border-t border-slate-100 bg-slate-50/50 space-y-4">
@if (CurrentStep == "cart" || CurrentStep == "details")
{
<div class="space-y-2 text-xs">
<div class="flex justify-between text-slate-500">
<span>Subtotal (ZAR)</span>
<span class="font-mono font-medium">R @Subtotal().ToString("F2")</span>
</div>
<div class="flex justify-between text-slate-500">
<span>VAT (15% SA)</span>
<span class="font-mono font-medium">R @Vat().ToString("F2")</span>
</div>
<div class="flex justify-between font-serif text-sm font-bold text-slate-800 border-t border-slate-100 pt-2">
<span>Total Due</span>
<span class="font-mono">R @Total().ToString("F2")</span>
</div>
</div>
}
@if (CurrentStep == "cart")
{
<button @onclick="ProceedToDetails" disabled="@(CartItems.Count == 0)" class="w-full py-3.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs shadow-md cursor-pointer transition-all disabled:opacity-50 flex items-center justify-center gap-1.5">
Proceed to Checkout
</button>
}
else if (CurrentStep == "details")
{
<button @onclick="ProcessSecureCheckout" class="w-full py-3.5 rounded-xl bg-violet-600 hover:bg-violet-500 text-white font-bold text-xs shadow-md cursor-pointer transition-all flex items-center justify-center gap-1.5">
Authorize Payment
</button>
}
else if (CurrentStep == "otp")
{
<button @onclick="VerifyOtp" class="w-full py-3.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs shadow-md cursor-pointer transition-all flex items-center justify-center gap-1.5">
Submit OTP Code
</button>
}
else if (CurrentStep == "success")
{
<button @onclick="CloseAndFinish" class="w-full py-3.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold text-xs shadow-md cursor-pointer transition-all flex items-center justify-center gap-1.5">
Open My Library
</button>
}
</footer>
</div>
</div>
}
@code {
[Parameter] public bool IsOpen { get; set; }
[Parameter] public EventCallback OnClose { get; set; }
[Parameter] public List<CartItem> CartItems { get; set; } = new();
[Parameter] public EventCallback<List<CartItem>> OnCheckoutSuccess { get; set; }
[Parameter] public EventCallback<CartItem> OnRemoveFromCart { get; set; }
private string CurrentStep { get; set; } = "cart"; // "cart", "details", "processing", "otp", "success"
private string PaymentMethod { get; set; } = "card"; // "card", "eft"
// Processing parameters
private string ProcessingStatus { get; set; } = string.Empty;
private int ProcessingProgress { get; set; } = 0;
// Form inputs
private string CardName { get; set; } = string.Empty;
private string CardNumber { get; set; } = string.Empty;
private string CardExpiry { get; set; } = string.Empty;
private string CardCvv { get; set; } = string.Empty;
private string SelectedBank { get; set; } = "Capitec";
private string EftUsername { get; set; } = string.Empty;
private string EftPassword { get; set; } = string.Empty;
// OTP Parameters
private string GeneratedOtp { get; set; } = "482903";
private string OtpInput { get; set; } = string.Empty;
private string OtpError { get; set; } = string.Empty;
private double Subtotal()
{
return CartItems.Sum(i => i.Price);
}
private double Vat()
{
return Subtotal() * 0.15;
}
private double Total()
{
return Subtotal(); // ZAR pricing includes VAT
}
private void ProceedToDetails()
{
CurrentStep = "details";
}
private void SetPaymentMethod(string method)
{
PaymentMethod = method;
}
private void RemoveItem(CartItem item)
{
OnRemoveFromCart.InvokeAsync(item);
}
private async Task ProcessSecureCheckout()
{
CurrentStep = "processing";
ProcessingProgress = 15;
ProcessingStatus = "Establishing secure proxy to South African gateway nodes...";
await Task.Delay(1000);
ProcessingProgress = 45;
ProcessingStatus = "Securing socket with SSL 256-bit encryption...";
await Task.Delay(1000);
ProcessingProgress = 75;
ProcessingStatus = "Contacting issuing merchant bank for authorization...";
await Task.Delay(1000);
ProcessingProgress = 90;
ProcessingStatus = "Spawning 3D Secure 2.0 dual-factor authentication challenge...";
await Task.Delay(800);
GeneratedOtp = new Random().Next(100000, 999999).ToString();
OtpError = string.Empty;
OtpInput = string.Empty;
CurrentStep = "otp";
}
private void VerifyOtp()
{
if (OtpInput.Trim() == GeneratedOtp)
{
CurrentStep = "success";
OnCheckoutSuccess.InvokeAsync(CartItems);
}
else
{
OtpError = "The authentication code is incorrect. Please check SMS dispatch.";
}
}
private void CloseAndFinish()
{
CurrentStep = "cart"; // reset step for next load
OnClose.InvokeAsync();
}
}