diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor b/MidrandBookshop/Components/Layout/MainLayout.razor index e874b22..d19704e 100644 --- a/MidrandBookshop/Components/Layout/MainLayout.razor +++ b/MidrandBookshop/Components/Layout/MainLayout.razor @@ -204,7 +204,7 @@
  • - + @@ -216,15 +216,15 @@
  • - + Sign In
  • - + - Order Status + Order History
  • diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor.cs b/MidrandBookshop/Components/Layout/MainLayout.razor.cs index 830d77a..a338bbd 100644 --- a/MidrandBookshop/Components/Layout/MainLayout.razor.cs +++ b/MidrandBookshop/Components/Layout/MainLayout.razor.cs @@ -5,15 +5,10 @@ namespace MidrandBookshop.Components.Layout; public partial class MainLayout(CartService cartService) : IDisposable { - [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; [Inject] public IToastService ToastService { get; set; } = default!; private Cart ShoppingCart => cartService.ShoppingCart; - private AuthenticationState? AuthState { get; set; } - private ClaimsPrincipal? User { get; set; } - private bool IsAuthenticated => User?.Identity?.IsAuthenticated ?? false; - private string SearchInputBuffer { get; set; } = string.Empty; private string GlobalSearchQuery { get; set; } = string.Empty; private bool IsSearchActive { get; set; } = false; @@ -21,13 +16,11 @@ public partial class MainLayout(CartService cartService) : IDisposable protected override async Task OnInitializedAsync() { - AuthState = await AuthStateProvider.GetAuthenticationStateAsync(); - User = AuthState!.User; - Navigation.LocationChanged += OnLocationChanged; cartService.OnCartChanged += CartService_OnCartChanged; - await cartService.LoadCartFromStorageAsync(); + if (cartService.ShoppingCart.Items.Count == 0) + await cartService.LoadCartFromStorageAsync(); SyncSearchQueryFromUrl(); } @@ -122,16 +115,49 @@ public partial class MainLayout(CartService cartService) : IDisposable private decimal GetCartTotal() => ShoppingCart?.TotalAmount ?? 0.00m; - private void RedirectToCart() + private async Task RedirectToCart() { IsCartOpen = false; + + await cartService.SaveCartToStorageAsync(); + Navigation.NavigateTo("/cart"); } - private void RedirectToCheckout() + private async Task RedirectToCheckout() { IsCartOpen = false; - Navigation.NavigateTo("/checkout"); + + await cartService.SaveCartToStorageAsync(); + + Navigation.NavigateTo("/checkout", forceLoad: true); + } + + private async Task RedirectToAccount() + { + IsCartOpen = false; + + await cartService.SaveCartToStorageAsync(); + + Navigation.NavigateTo("/account", forceLoad: true); + } + + private async Task HandleLogin() + { + IsCartOpen = false; + + await cartService.SaveCartToStorageAsync(); + + Navigation.NavigateTo("/login", forceLoad: true); + } + + private async Task HandleLogout() + { + IsCartOpen = false; + + await cartService.SaveCartToStorageAsync(); + + Navigation.NavigateTo("/logout", forceLoad: true); } public void Dispose() diff --git a/MidrandBookshop/Components/Pages/Checkout.razor b/MidrandBookshop/Components/Pages/Checkout.razor index aea6f69..ca2317b 100644 --- a/MidrandBookshop/Components/Pages/Checkout.razor +++ b/MidrandBookshop/Components/Pages/Checkout.razor @@ -3,74 +3,248 @@ @rendermode InteractiveServer @attribute [Authorize] -
    -

    Checkout

    -
    -
    -
    -
    Your Items
    - @foreach (var item in ShoppingCart.Items) - { -
    -
    @item.Product!.Name
    @($"{item.Author!.Name} {item.Author.LastName}")
    -
    -
    - - @item.Quantity - +
    + + + @if (IsProcessing) + { +
    +
    +
    + + + + +
    +
    +

    + Securing Your Order +

    +

    + Please stand by. We are preparing your payment portal and transferring you securely to PayFast. +

    +
    +
    + + + + + Bank-Grade 256-Bit SSL Connection +
    +
    +
    + } + + +
    + Secure Checkout +

    Review Your Order

    +
    + + @if (ShoppingCart.Items.Any() == false) + { + +
    +
    + + + + + +
    +
    +

    Your cart is empty

    +

    You cannot proceed to payment without selected titles.

    +
    + + Browse Book Catalogue + +
    + } + else + { +
    + +
    + + +
    +
    +
    Your Selection
    + + @ShoppingCart.Items.Count Items + +
    + +
    + @foreach (var item in ShoppingCart.Items) + { +
    +
    +
    @item.Product!.Name
    + + By @($"{item.Author!.Name} {item.Author.LastName}") + +
    +
    +
    + + @item.Quantity + +
    + +
    - + } +
    +
    + + +
    +
    Fulfillment Option
    + +
    + + +
    + +
    + + FREE +
    + + +
    + +
    + + R 60.00
    - } +
    + + +
    +
    +
    Delivery Instructions
    + Optional +
    +

    + Add any specific details for our dispatch team (e.g., gate access codes, complex navigation, or safe drop-off preferences). +

    +
    + +
    + + @(OrderNotes?.Length ?? 0) / 500 characters + +
    +
    +
    + + +
    +
    Billing Settings
    +
    + + +
    +
    -
    -
    Shipping Method
    -
    - - -
    -
    - - + +
    +
    +
    Summary Breakdown
    + +
    +
    + Items Subtotal + R @ShoppingCart.TotalAmount.ToString("F2") +
    + + +
    + @if (ShoppingCart.TotalVat > 0) + { + Value Added Tax (VAT 15%) + R @ShoppingCart.TotalVat.ToString("F2") + } + else + { + Value Added Tax (VAT) + + (Price is VAT inclusive) + + } +
    + +
    + Fulfillment Courier Fee + + @if (ShippingCost == 0) + { + FREE + } + else + { + R @($"{ShippingCost:F2}") + } + +
    + +
    + +
    + Total Due Amount +
    + + R @($"{ShoppingCart.TotalAmount + ShoppingCart.TotalVat + ShippingCost:F2}") + + ZAR Currency +
    +
    +
    + +
    -
    -
    Shipping Address
    -
    - - -
    -
    + + @if (IsProcessing == true && CheckoutPayload?.Count > 0) + { +
    + @foreach (var field in CheckoutPayload) + { + + } +
    + }
    - -
    -
    -
    Order Summary
    -
    SubtotalR @ShoppingCart.TotalAmount.ToString("F2")
    -
    VAT (15%)R @ShoppingCart.TotalVat.ToString("F2")
    -
    ShippingR @($"{ShippingCost:F2}")
    -
    -
    - Total Due -

    R @($"{ShoppingCart.TotalAmount + ShoppingCart.TotalVat + ShippingCost:F2}")

    -
    - -
    -
    - - @if (IsProcessing == true && CheckoutPayload?.Count > 0) - { -
    - @foreach (var field in CheckoutPayload) - { - - } -
    - } -
    + }
    \ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/Checkout.razor.cs b/MidrandBookshop/Components/Pages/Checkout.razor.cs index c82d88d..f5e1c46 100644 --- a/MidrandBookshop/Components/Pages/Checkout.razor.cs +++ b/MidrandBookshop/Components/Pages/Checkout.razor.cs @@ -29,6 +29,7 @@ public partial class Checkout() private decimal ShippingCost = 0; private bool IsSameAddress = true; + public string? OrderNotes { get; set; } private Dictionary CheckoutPayload { get; set; } = []; protected override async Task OnInitializedAsync() @@ -38,6 +39,9 @@ public partial class Checkout() Navigation.LocationChanged += OnLocationChanged; CartService.OnCartChanged += CartService_OnCartChanged; + + if (CartService.ShoppingCart.Items.Count == 0) + await CartService.LoadCartFromStorageAsync(); } private async void CartService_OnCartChanged() => await InvokeAsync(StateHasChanged); @@ -113,7 +117,7 @@ public partial class Checkout() if (paymentGen.IsSuccess) paymentId = paymentGen.Value; - if(paymentGen.IsFailed) + if (paymentGen.IsFailed) { var paymentFetch = await PaymentService.GetOrderPaymentAsync(orderId, CancellationToken); @@ -161,7 +165,7 @@ public partial class Checkout() await JSRuntime.InvokeVoidAsync("eval", "document.getElementById('payfastForm').submit();"); } - catch(Exception ex) + catch (Exception ex) { ToastService.ShowError($"Failed to perform checkout: {ex.Message}", "Checkout"); diff --git a/MidrandBookshop/Components/Pages/Checkout.razor.css b/MidrandBookshop/Components/Pages/Checkout.razor.css new file mode 100644 index 0000000..0db0b33 --- /dev/null +++ b/MidrandBookshop/Components/Pages/Checkout.razor.css @@ -0,0 +1,347 @@ +/* ========================================================================== + Midrand Books — Checkout Layout Polish & Tightening + ========================================================================== */ + +/* --- 🛠️ 1. Global Page Wrapper Boundary Constraints --- */ +.checkout-page-container { + max-width: 1140px; + margin: 0 auto; + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +/* --- 2. Page Typography & Headers --- */ +.checkout-header { + margin-bottom: 2.5rem !important; +} + +.checkout-main-title { + font-size: 2.25rem; + letter-spacing: -0.03em; + color: #111111; +} + +.tracking-wider { + letter-spacing: 0.12em; +} + +/* --- 3. Custom Structural Content Panels --- */ +.checkout-section-panel { + background-color: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 12px; + padding: 2rem; +} + +.panel-title { + font-size: 1.1rem; + letter-spacing: -0.01em; + color: #111111; +} + +/* --- 🛠️ 4. Items Manifest Row & Stepper Controls --- */ +.checkout-items-stack .checkout-item-row { + display: flex; + align-items: center; /* Locks elements on a clean vertical axis line */ + justify-content: space-between; + gap: 2rem; + border-bottom: 1px dashed rgba(0, 0, 0, 0.08); +} + + .checkout-items-stack .checkout-item-row:last-child { + border-bottom: none; + } + +.item-meta-details { + max-width: 65%; /* Brounds long item titles elegantly */ +} + +.item-product-name { + font-size: 0.95rem; + line-height: 1.4; + color: #1A1A1A; +} + +.premium-quantity-stepper { + display: inline-flex; + align-items: center; + background-color: #F8F9FA; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 30px; + padding: 3px; +} + + .premium-quantity-stepper .step-btn { + background: none; + border: none; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + color: #555555; + border-radius: 50%; + transition: background-color 0.2s ease, color 0.2s ease; + } + + .premium-quantity-stepper .step-btn:hover { + background-color: #FFFFFF; + color: #000000; + } + + .premium-quantity-stepper .step-value { + min-width: 28px; + text-align: center; + font-size: 0.85rem; + font-weight: 600; + color: #111111; + } + +.btn-clean-remove { + background: none; + border: none; + color: #DC3545; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.05em; + padding: 4px 8px; + border-radius: 4px; + transition: background-color 0.2s ease; +} + + .btn-clean-remove:hover { + background-color: rgba(220, 53, 69, 0.06); + } + +/* --- 5. Interactive Selectable Radio Selection Cards --- */ +.visual-hidden { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.premium-selectable-card { + display: flex; + align-items: center; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 10px; + padding: 1.25rem; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1); + background-color: #FFFFFF; +} + + .premium-selectable-card:hover { + border-color: #111111; + background-color: #FAFBFB; + } + + .premium-selectable-card.active { + border-color: #000000; + box-shadow: inset 0 0 0 1px #000000; + background-color: #FFFFFF; + } + +.card-indicator-circle { + width: 16px; + height: 16px; + border: 1px solid rgba(0, 0, 0, 0.25); + border-radius: 50%; + margin-right: 1.25rem; + position: relative; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.premium-selectable-card.active .card-indicator-circle { + border-color: #000000; + background-color: #000000; +} + + .premium-selectable-card.active .card-indicator-circle::after { + content: ''; + position: absolute; + width: 6px; + height: 6px; + background-color: #FFFFFF; + border-radius: 50%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + +.card-label-title { + font-size: 0.9rem; + line-height: 1.3; +} + +.card-label-desc { + font-size: 0.78rem !important; +} + +.card-price-tag { + font-size: 0.85rem; + letter-spacing: -0.01em; +} + +/* --- 6. Form Checkbox Options --- */ +.context-clickable { + cursor: pointer; + transition: background-color 0.2s ease; +} + + .context-clickable:hover { + background-color: #FAFBFB; + } + +.custom-box-tick { + cursor: pointer; +} + +/* --- 🛠️ 7. Sticky Right Sidebar Order Ledger --- */ +.sticky-summary-card { + background-color: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + border-radius: 12px; + position: sticky; + top: 120px; /* Safe breathing room underneath the global nav header */ + box-shadow: 0 12px 34px -10px rgba(0, 0, 0, 0.03); +} + +.small-summary-heading { + font-size: 0.78rem; + color: #666666; +} + +.price-summary-ledger .ledger-row { + padding: 0.25rem 0; +} + +.summary-divider { + border-color: rgba(0, 0, 0, 0.06); + opacity: 1; +} + +.extra-small { + font-size: 0.68rem; +} + +/* --- 8. Core Checkout Action Button --- */ +.btn-premium-action { + background-color: #111111; + color: #FFFFFF; + border: none; + border-radius: 50px; + font-weight: 600; + font-size: 0.95rem; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} + + .btn-premium-action:hover:not(:disabled) { + background-color: #222222; + transform: translateY(-1px); + box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.15); + } + + .btn-premium-action:disabled { + background-color: #CCCCCC; + color: #888888; + cursor: not-allowed; + } + +/* ========================================================================== + Full-Screen Handover Processing Overlay + ========================================================================== */ +.processing-screen-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: #FFFFFF; + z-index: 3000; +} + +.processing-card-box { + max-width: 420px; + padding: 2rem; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; +} + +.payment-vector-loader { + width: 48px; + height: 48px; +} + +.loader-track { + stroke: rgba(0, 0, 0, 0.06); + stroke-width: 2.5; + fill: none; +} + +.loader-handshake-ring { + stroke: #111111; + stroke-width: 2.5; + stroke-linecap: round; + fill: none; + transform-origin: center; + animation: spinHandoverLoop 1s linear infinite; +} + +.secure-badge { + display: inline-flex; + align-items: center; + gap: 0.4rem; + color: #666666; + opacity: 0.6; + border: 1px solid rgba(0, 0, 0, 0.08); + padding: 0.35rem 0.85rem; + border-radius: 50px; + font-size: 0.75rem; + font-weight: 500; +} + +@keyframes spinHandoverLoop { + 100% { + transform: rotate(360deg); + } +} + +/* --- 9. Premium Plaintext Field & Textarea Structure --- */ +.premium-textarea-wrapper { + position: relative; +} + +.premium-plaintext-field { + background-color: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 8px; + padding: 0.85rem 1rem; + font-size: 0.9rem; + line-height: 1.5; + color: #1A1A1A; + resize: none; /* Disables ugly drag handles to maintain design proportions */ + transition: all 0.2s ease; +} + + .premium-plaintext-field:focus { + background-color: #FFFFFF; + border-color: #000000; + box-shadow: none; /* Strip standard blue Bootstrap glowing halos */ + outline: none; + } + + .premium-plaintext-field::placeholder { + color: #A0A0A0; + font-size: 0.88rem; + } \ No newline at end of file