Stable run with tailwind
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
@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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user