Compare commits

..

2 Commits

Author SHA1 Message Date
khwezi 172575bb61 Merge pull request 'Implemented order history' (#102) from accounts into main
Reviewed-on: #102
2026-06-17 22:46:37 +02:00
Khwezi Mngoma 1efe1ff21e Implemented order history
continuous-integration/drone/pr Build is passing
2026-06-17 22:45:40 +02:00
3 changed files with 347 additions and 184 deletions
+41 -17
View File
@@ -17,6 +17,7 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
<span>Order History</span> <span>Order History</span>
</button> </button>
<button class="nav-link text-start d-flex align-items-center gap-2" data-bs-toggle="pill" data-bs-target="#shipping" role="tab"> <button class="nav-link text-start d-flex align-items-center gap-2" data-bs-toggle="pill" data-bs-target="#shipping" role="tab">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
<span>Shipping Address</span> <span>Shipping Address</span>
@@ -39,7 +40,7 @@
<div class="tab-content account-panels-deck"> <div class="tab-content account-panels-deck">
<div class="tab-pane fade show active" id="orders" role="tabpanel"> <div class="tab-pane fade show active" id="orders" role="tabpanel">
<div class="panel-card-wrapper mb-4"> <div class="tab-panel-body">
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Order History</h5> <h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Order History</h5>
@if (orderHistory == null || !orderHistory.Any()) @if (orderHistory == null || !orderHistory.Any())
@@ -54,7 +55,7 @@
@foreach (var order in orderHistory) @foreach (var order in orderHistory)
{ {
<div class="premium-order-card p-4 border rounded-3 bg-white shadow-sm"> <div class="premium-order-card p-4 border rounded-3 bg-white shadow-sm">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 pb-3 border-b-dashed mb-3"> <div class="d-flex justify-content-between align-items-start flex-wrap gap-3 pb-3 border-bottom border-light mb-3">
<div> <div>
<span class="font-monospace text-dark fw-bold d-block h6 mb-1">@order.OrderId</span> <span class="font-monospace text-dark fw-bold d-block h6 mb-1">@order.OrderId</span>
<small class="text-muted d-block mb-1">Ordered on @order.OrderDate.ToString("dd MMMM yyyy")</small> <small class="text-muted d-block mb-1">Ordered on @order.OrderDate.ToString("dd MMMM yyyy")</small>
@@ -63,20 +64,26 @@
<span>Shipped to: <span class="fw-semibold text-dark">@order.ShippingAddressName</span></span> <span>Shipped to: <span class="fw-semibold text-dark">@order.ShippingAddressName</span></span>
</small> </small>
</div> </div>
<div class="text-md-end d-flex flex-column align-items-md-end gap-2"> <div class="text-md-end d-flex flex-column align-items-md-end gap-2">
<div class="d-flex align-items-center gap-1.5 flex-wrap justify-content-md-end"> <div class="d-flex align-items-center gap-1.5 flex-wrap justify-content-md-end">
<span class="badge status-badge-base @GetOrderStatusClass(order.Status)">
Order: @order.Status
</span>
<span class="badge status-badge-base @GetPaymentStatusClass(order.PaymentStatus)"> <span class="badge status-badge-base @GetPaymentStatusClass(order.PaymentStatus)">
Pay: @order.PaymentStatus Pay: @order.PaymentStatus
</span> </span>
<span class="badge status-badge-base @GetStatusClass(order.Status)">
Logistics: @order.Status <span class="badge status-badge-base @GetShippingStatusClass(order.ShippingStatus)">
Logistics: @order.ShippingStatus
</span> </span>
</div> </div>
@if (order.PaymentStatus?.ToLower() == "paid") @if (order.PaymentStatus?.ToLower() == "paid")
{ {
<button class="btn btn-outline-dark btn-premium-sm font-monospace text-uppercase d-inline-flex align-items-center gap-1.5 py-1 px-2.5" <button class="btn btn-outline-dark btn-premium-sm font-monospace text-uppercase d-inline-flex align-items-center gap-1.5 py-1 px-2.5"
style="font-size: 0.7rem;" style="font-size: 0.7rem;" disabled="@string.IsNullOrWhiteSpace(order.InvoiceUrl)"
@onclick="() => DownloadInvoice(order.OrderId)"> @onclick="() => DownloadInvoice(order.OrderId)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
<span>Invoice</span> <span>Invoice</span>
@@ -85,12 +92,32 @@
</div> </div>
</div> </div>
<div class="order-manifest-details d-flex align-items-center justify-content-between gap-4 py-1"> <div class="order-books-manifest manifest-grid-wrap mb-3">
<div class="item-meta"> @foreach (var book in order.PurchasedBooks)
<h6 class="text-dark fw-bold mb-0 small" style="line-height: 1.4;">@order.ProductTitle</h6> {
</div> <div class="manifest-grid-cell">
<div class="item-value text-end flex-shrink-0"> <div class="manifest-book-item border rounded p-2 d-flex align-items-center justify-content-between bg-light bg-opacity-20 h-100">
<span class="font-monospace text-dark fw-bold d-block">R @order.Total.ToString("F2")</span> <div class="d-flex align-items-center gap-2.5 min-w-0">
<div class="book-thumbnail-container flex-shrink-0 border rounded bg-white shadow-xs">
<img src="@book.CoverImageUrl" alt="@book.Title" class="book-thumbnail-img" />
</div>
<div class="book-text-meta min-w-0">
<h6 class="text-dark fw-bold mb-0.5 small text-truncate" title="@book.Title">@book.Title</h6>
<span class="badge bg-white text-secondary border font-monospace extra-small py-0.5">Qty: @book.Quantity</span>
</div>
</div>
<div class="book-row-pricing text-end font-monospace text-dark small ps-2 flex-shrink-0 fw-medium">
R @((book.PriceUnitPrice * book.Quantity).ToString("F2"))
</div>
</div>
</div>
}
</div>
<div class="order-summary-footer border-top pt-3 d-flex align-items-baseline justify-content-between">
<span class="text-muted small text-uppercase font-monospace">Order Total</span>
<div class="text-end">
<span class="font-monospace text-dark fw-bold h5 mb-0 d-block">R @order.Total.ToString("F2")</span>
<small class="text-muted extra-small font-monospace">VAT Inclusive</small> <small class="text-muted extra-small font-monospace">VAT Inclusive</small>
</div> </div>
</div> </div>
@@ -102,7 +129,7 @@
</div> </div>
<div class="tab-pane fade" id="shipping" role="tabpanel"> <div class="tab-pane fade" id="shipping" role="tabpanel">
<div class="panel-card-wrapper mb-4"> <div class="tab-panel-body">
<div class="d-flex justify-content-between align-items-baseline mb-4"> <div class="d-flex justify-content-between align-items-baseline mb-4">
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-0">Saved Addresses</h5> <h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-0">Saved Addresses</h5>
@if (!showAddForm && editingAddress == null) @if (!showAddForm && editingAddress == null)
@@ -194,7 +221,7 @@
</div> </div>
<div class="tab-pane fade" id="profile" role="tabpanel"> <div class="tab-pane fade" id="profile" role="tabpanel">
<div class="panel-card-wrapper mb-4"> <div class="tab-panel-body">
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Profile Settings</h5> <h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Profile Settings</h5>
<div class="profile-hero-banner mb-4 d-flex align-items-center justify-content-between p-4 border rounded-3 bg-light bg-opacity-20 flex-wrap gap-3"> <div class="profile-hero-banner mb-4 d-flex align-items-center justify-content-between p-4 border rounded-3 bg-light bg-opacity-20 flex-wrap gap-3">
@@ -203,9 +230,7 @@
<h5 class="fw-bold text-dark mb-1 h6">@User?.Identity?.Name</h5> <h5 class="fw-bold text-dark mb-1 h6">@User?.Identity?.Name</h5>
<p class="text-muted small mb-0 font-monospace extra-small opacity-75">Secure Connection Authorized</p> <p class="text-muted small mb-0 font-monospace extra-small opacity-75">Secure Connection Authorized</p>
</div> </div>
<span class="badge rounded-pill bg-success bg-opacity-10 text-success border border-success border-opacity-20 font-monospace px-3 py-1.5 small text-uppercase tracking-wide"> <span class="badge rounded-pill bg-success bg-opacity-10 text-success border border-success border-opacity-20 font-monospace px-3 py-1.5 small text-uppercase tracking-wide">Verified</span>
Verified
</span>
</div> </div>
<div class="card p-5 text-center bg-white border rounded-3 shadow-sm my-4"> <div class="card p-5 text-center bg-white border rounded-3 shadow-sm my-4">
@@ -219,7 +244,6 @@
<p class="text-muted small mx-auto mb-4" style="max-width: 480px; line-height: 1.5;"> <p class="text-muted small mx-auto mb-4" style="max-width: 480px; line-height: 1.5;">
For your structural protection, password alterations, account recovery preferences, cross-tenant factors, and core credential manifests are handled through our global Identity Node security layer. For your structural protection, password alterations, account recovery preferences, cross-tenant factors, and core credential manifests are handled through our global Identity Node security layer.
</p> </p>
<a href="https://sts.security.khongisa.co.za/Manage/Index?returnUrl=https://midrandbooks.co.za/account" <a href="https://sts.security.khongisa.co.za/Manage/Index?returnUrl=https://midrandbooks.co.za/account"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
+173 -71
View File
@@ -1,10 +1,26 @@
namespace MidrandBookshop.Components.Pages; using LiteCharms.Features.Hasher;
using LiteCharms.Features.MidrandBooks.Customers;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Payments;
using LiteCharms.Features.MidrandBooks.Products;
namespace MidrandBookshop.Components.Pages;
public partial class Account : ComponentBase public partial class Account : ComponentBase
{ {
[Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
[Inject] private CustomerService CustomerService { get; set; } = default!;
[Inject] private OrderService OrderService { get; set; } = default!;
[Inject] private PaymentService PaymentService { get; set; } = default!;
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private HashService HashService { get; set; } = default!;
[Inject] private CancellationToken CancellationToken { get; set; } = default!;
[Inject] private IToastService ToasterService { get; set; } = default!;
private ClaimsPrincipal? User { get; set; } private ClaimsPrincipal? User { get; set; }
private Customer? customer;
private bool showAddForm = false; private bool showAddForm = false;
private AddressItem? editingAddress = null; private AddressItem? editingAddress = null;
private string newAddressName = ""; private string newAddressName = "";
@@ -13,17 +29,7 @@ public partial class Account : ComponentBase
private string newPostalCode = ""; private string newPostalCode = "";
private bool isBilling, isShipping; private bool isBilling, isShipping;
private List<OrderItem> orderHistory = new() private List<OrderItem> orderHistory = [];
{
// 1. Delivered + Paid (Green Mapping Rules)
new OrderItem { OrderId = "#MB-2026-8712", ProductId = "mastering-css-isolation", ProductTitle = "Mastering CSS Isolation in Modern .NET Web Applications Architecture", OrderDate = new DateTime(2026, 4, 14), ShippingAddressName = "Midrand Warehouse", Status = "Delivered", PaymentStatus = "Paid", Total = 890.00 },
// 2. Shipped + Paid (Amber Logistics + Green Payment Rules)
new OrderItem { OrderId = "#MB-2026-9481", ProductId = "introduction-to-blazor", ProductTitle = "Introduction to Blazor WebAssembly Framework Development", OrderDate = new DateTime(2026, 5, 20), ShippingAddressName = "Home Address", Status = "Shipped", PaymentStatus = "Paid", Total = 720.00 },
// 3. Unshipped + Abandoned/Unpaid (Muted Grey / Soft Red Rules — Hides Invoice Button)
new OrderItem { OrderId = "#MB-2026-1034", ProductId = "csharp-functional-paradigms", ProductTitle = "Advanced Functional Architecture & Monadic Paradigms in Modern C#", OrderDate = new DateTime(2026, 6, 11), ShippingAddressName = "Home Address", Status = "Unshipped", PaymentStatus = "Unpaid", Total = 650.00 }
};
private List<AddressItem> savedAddresses = new() private List<AddressItem> savedAddresses = new()
{ {
@@ -35,64 +41,138 @@ public partial class Account : ComponentBase
{ {
var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var authState = await AuthStateProvider.GetAuthenticationStateAsync();
User = authState?.User; User = authState?.User;
var customerFetch = await CustomerService.GetCustomerAsync(User?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)!.Value!, CancellationToken);
if (customerFetch.IsSuccess)
customer = customerFetch.Value;
await LoadOrdersAsync();
}
private async Task LoadOrdersAsync()
{
if (customer is null)
{
ToasterService.ShowError("There was a problem loading your details, please contact the administrator");
return;
}
var ordersFetch = await OrderService.GetOrdersByCustomerAsync(customer.Id, CancellationToken);
if (ordersFetch.IsFailed)
{
ToasterService.ShowWarning("No orders were found");
return;
}
orderHistory.Clear();
foreach (var order in ordersFetch.Value)
{
var paymentFetch = await PaymentService.GetOrderPaymentAsync(order.Id, CancellationToken);
var orderEntry = new OrderItem
{
OrderDate = order.CreatedAt,
OrderId = HashService.HashEncodeLongId(order.Id).Value,
Status = order.Status.ToString(),
PaymentStatus = paymentFetch.IsSuccess ? paymentFetch.Value.Status.ToString() : "NotPaid",
ShippingStatus = "Processing",
ShippingAddressName = "TBA",
Total = order.Total,
InvoiceUrl = order.InvoiceUrl!,
};
var orderItemsFetch = await OrderService.GetOrderItemsAsync(order.Id, CancellationToken);
if (orderItemsFetch.IsFailed) continue;
foreach (var item in orderItemsFetch.Value)
{
var productPriceFetch = await ProductService.GetProductPriceAsync(item.ProductPriceId, CancellationToken);
if (productPriceFetch.IsFailed) continue;
var productFetch = await ProductService.GetProductAsync(productPriceFetch.Value.ProductId, CancellationToken);
var itemEntry = new PurchasedBook
{
Quantity = item.Quantity,
PriceUnitPrice = productPriceFetch.Value.Amount,
CoverImageUrl = productFetch.Value.ImageUrl!,
Title = productFetch.Value.Name!,
};
orderEntry.PurchasedBooks.Add(itemEntry);
}
orderHistory.Add(orderEntry);
}
} }
private void DownloadInvoice(string orderId) private void DownloadInvoice(string orderId)
{ {
Navigation.NavigateTo($"/api/invoices/download/{orderId.Replace("#", "")}", forceLoad: true); var order = orderHistory.FirstOrDefault(o => o.OrderId == orderId)!;
if (string.IsNullOrWhiteSpace(order.InvoiceUrl))
ToasterService.ShowWarning("Your invoice is currently not availabe for viewing");
else
Navigation.NavigateTo(orderHistory.FirstOrDefault(o => o.OrderId == orderId)!.InvoiceUrl, forceLoad: true);
} }
private string GetStatusClass(string status) => status?.ToLower() switch // Badge Style 1: Core Order Lifecycle Status Mapping
private string GetOrderStatusClass(string? status)
{ {
"delivered" => "status-delivered", // Green return status?.ToLower() switch
"shipped" => "status-shipped", // Amber {
_ => "status-processing" // Muted Architectural Dark Grey "completed" => "order-completed",
}; "processing" => "order-processing",
"cancelled" => "order-cancelled",
private string GetPaymentStatusClass(string paymentStatus) => paymentStatus?.ToLower() switch _ => "order-hold"
{ };
"paid" => "pay-paid", // Green
"refunded" => "pay-refunded", // Grey
_ => "pay-pending" // Red Alert Tone
};
private void EditAddress(AddressItem addr)
{
editingAddress = addr;
showAddForm = false;
newAddressName = addr.Name;
newStreetAddress = addr.Street;
newCity = addr.City;
newPostalCode = addr.PostalCode;
isBilling = addr.IsBilling;
isShipping = addr.IsShipping;
} }
private void CancelAddressActions() // Badge Style 2: Financial Payment Status Mapping
private string GetPaymentStatusClass(string? status)
{ {
showAddForm = false; return status?.ToLower() switch
editingAddress = null; {
ClearFormFields(); "paid" => "pay-paid",
"refunded" => "pay-refunded",
_ => "pay-pending"
};
} }
private void ClearFormFields() // Badge Style 3: Logistics Shipment Status Mapping
private string GetShippingStatusClass(string? status)
{ {
newAddressName = ""; return status?.ToLower() switch
newStreetAddress = ""; {
newCity = ""; "delivered" => "status-delivered",
newPostalCode = ""; "shipped" => "status-shipped",
isBilling = false; _ => "status-processing"
isShipping = false; };
} }
// Implemented to resolve UI registration click events
private void SaveAddress() private void SaveAddress()
{ {
if (string.IsNullOrWhiteSpace(newAddressName) || string.IsNullOrWhiteSpace(newStreetAddress)) return; if (string.IsNullOrWhiteSpace(newAddressName) || string.IsNullOrWhiteSpace(newStreetAddress))
{
ToasterService.ShowWarning("Please fill in the required fields.");
return;
}
if (editingAddress == null) if (editingAddress != null)
{
editingAddress.Name = newAddressName;
editingAddress.Street = newStreetAddress;
editingAddress.City = newCity;
editingAddress.PostalCode = newPostalCode;
editingAddress.IsBilling = isBilling;
editingAddress.IsShipping = isShipping;
}
else
{ {
var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1; var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1;
var newAddr = new AddressItem savedAddresses.Add(new AddressItem
{ {
Id = nextId, Id = nextId,
Name = newAddressName, Name = newAddressName,
@@ -102,26 +182,39 @@ public partial class Account : ComponentBase
IsBilling = isBilling, IsBilling = isBilling,
IsShipping = isShipping, IsShipping = isShipping,
IsPrimary = !savedAddresses.Any() IsPrimary = !savedAddresses.Any()
}; });
savedAddresses.Add(newAddr);
}
else
{
var target = savedAddresses.FirstOrDefault(a => a.Id == editingAddress.Id);
if (target != null)
{
target.Name = newAddressName;
target.Street = newStreetAddress;
target.City = newCity;
target.PostalCode = newPostalCode;
target.IsBilling = isBilling;
target.IsShipping = isShipping;
}
editingAddress = null;
} }
CancelAddressActions();
}
private void CancelAddressActions()
{
showAddForm = false;
editingAddress = null;
ResetFormFields();
}
private void ResetFormFields()
{
newAddressName = "";
newStreetAddress = "";
newCity = "";
newPostalCode = "";
isBilling = false;
isShipping = false;
}
private void EditAddress(AddressItem addr)
{
editingAddress = addr;
newAddressName = addr.Name;
newStreetAddress = addr.Street;
newCity = addr.City;
newPostalCode = addr.PostalCode;
isBilling = addr.IsBilling;
isShipping = addr.IsShipping;
showAddForm = false; showAddForm = false;
ClearFormFields();
} }
private void DeleteAddress(AddressItem addr) private void DeleteAddress(AddressItem addr)
@@ -161,12 +254,21 @@ public partial class Account : ComponentBase
public class OrderItem public class OrderItem
{ {
public string OrderId { get; set; } = ""; public string OrderId { get; set; } = "";
public string ProductId { get; set; } = "";
public string ProductTitle { get; set; } = "";
public DateTime OrderDate { get; set; } public DateTime OrderDate { get; set; }
public string ShippingAddressName { get; set; } = ""; public string ShippingAddressName { get; set; } = "";
public string Status { get; set; } = ""; public string Status { get; set; } = "";
public string PaymentStatus { get; set; } = "Pending"; public string PaymentStatus { get; set; } = "";
public double Total { get; set; } public string ShippingStatus { get; set; } = "";
public string InvoiceUrl { get; set; } = "";
public decimal Total { get; set; }
public List<PurchasedBook> PurchasedBooks { get; set; } = new();
}
public class PurchasedBook
{
public string Title { get; set; } = "";
public string CoverImageUrl { get; set; } = "";
public int Quantity { get; set; }
public decimal PriceUnitPrice { get; set; }
} }
} }
@@ -3,13 +3,25 @@
========================================================================== */ ========================================================================== */
.account-page-container { .account-page-container {
max-width: 1140px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding-left: 1.5rem; padding-left: 1.5rem;
padding-right: 1.5rem; padding-right: 1.5rem;
font-family: system-ui, -apple-system, sans-serif; font-family: system-ui, -apple-system, sans-serif;
} }
.tab-panel-body {
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 768px) {
.tab-panel-body {
padding-left: 2rem;
padding-right: 2rem;
}
}
.account-main-title { .account-main-title {
font-size: 2.25rem; font-size: 2.25rem;
letter-spacing: -0.03em; letter-spacing: -0.03em;
@@ -39,102 +51,99 @@
.account-nav-stack .nav-link.active { .account-nav-stack .nav-link.active {
background-color: #111111 !important; background-color: #111111 !important;
color: #FFFFFF !important; color: #FFFFFF !important;
font-weight: 600;
} }
.account-nav-stack .nav-link:hover:not(.active):not(.nav-logout) { .account-nav-stack .nav-link:hover:not(.active) {
color: #111111;
background-color: rgba(0, 0, 0, 0.04) !important; background-color: rgba(0, 0, 0, 0.04) !important;
transform: translateX(2px); color: #111111;
} }
.account-nav-stack .nav-logout:hover { .nav-logout:hover {
background-color: #FFF5F5 !important; background-color: rgba(220, 53, 69, 0.08) !important;
color: #DC3545 !important;
} }
/* --- Main Tabbed Layout Container Content Structures --- */ /* --- Balanced Status Mapping Badges --- */
.panel-card-wrapper {
background-color: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 2.2rem;
}
.panel-section-title {
font-size: 0.9rem;
color: #666666;
}
/* --- Order History Structured Panel Cards --- */
.premium-order-card {
transition: transform 0.2s ease, border-color 0.2s ease;
}
.premium-order-card:hover {
border-color: rgba(0, 0, 0, 0.15) !important;
}
.border-b-dashed {
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
}
/* Minimalist Curated Logistics and Payment Badges Matrix */
.status-badge-base { .status-badge-base {
font-family: var(--bs-font-monospace); font-family: var(--bs-font-monospace);
font-size: 0.65rem !important; text-transform: uppercase;
letter-spacing: 0.05em; font-size: 0.68rem;
padding: 0.3rem 0.6rem !important;
border-radius: 4px !important;
font-weight: 600; font-weight: 600;
padding: 0.45rem 0.65rem;
letter-spacing: 0.02em;
border-radius: 4px;
} }
/* Fulfillment Matrix Colors */ /* 1. Core Order Lifecycle Badge Styles */
.order-completed {
background-color: #111111 !important;
color: #FFFFFF !important;
border: 1px solid #111111;
}
.order-processing {
background-color: rgba(0, 0, 0, 0.03) !important;
color: #111111 !important;
border: 1px solid rgba(0, 0, 0, 0.15);
}
.order-hold {
background-color: rgba(108, 117, 125, 0.05) !important;
color: #495057 !important;
border: 1px solid rgba(108, 117, 125, 0.2);
}
.order-cancelled {
background-color: rgba(220, 53, 69, 0.04) !important;
color: #842029 !important;
border: 1px solid rgba(220, 53, 69, 0.12);
}
/* 2. Logistics Shipment Status Badge Styles */
.status-delivered { .status-delivered {
background-color: #E2F0D9 !important; /* Soft Green Match */ background-color: rgba(25, 135, 84, 0.06) !important;
color: #385723 !important; color: #198754 !important;
border: 1px solid rgba(56, 87, 35, 0.15); border: 1px solid rgba(25, 135, 84, 0.15);
} }
.status-shipped { .status-shipped {
background-color: #FFF3CD !important; /* Warm Gold Amber */ background-color: rgba(13, 110, 253, 0.06) !important;
color: #856404 !important; color: #0d6efd !important;
border: 1px solid rgba(133, 100, 4, 0.12); border: 1px solid rgba(13, 110, 253, 0.15);
} }
.status-processing { .status-processing {
background-color: #F2F2F2 !important; /* Architectural Neutral Muted Grey */ background-color: rgba(255, 193, 7, 0.08) !important;
color: #595959 !important; color: #b58100 !important;
border: 1px dashed rgba(0, 0, 0, 0.12); border: 1px solid rgba(255, 193, 7, 0.25);
} }
/* Financial Matrix Colors */ /* 3. Financial Payment Status Badge Styles */
.pay-paid { .pay-paid {
background-color: #E2F0D9 !important; /* Soft Green Match */ background-color: rgba(25, 135, 84, 0.06) !important;
color: #385723 !important; color: #198754 !important;
border: 1px solid rgba(56, 87, 35, 0.15); border: 1px solid rgba(25, 135, 84, 0.15);
} }
.pay-pending { .pay-pending {
background-color: #F8D7DA !important; /* soft red alert tint for unpaid/abandoned items */ background-color: rgba(220, 53, 69, 0.06) !important;
color: #721C24 !important; color: #dc3545 !important;
border: 1px solid rgba(114, 28, 36, 0.15); border: 1px solid rgba(220, 53, 69, 0.15);
} }
.pay-refunded { .pay-refunded {
background-color: #E2E3E5 !important; background-color: rgba(108, 117, 125, 0.08) !important;
color: #383D41 !important; color: #6c757d !important;
border: 1px solid rgba(56, 61, 65, 0.15); border: 1px solid rgba(108, 117, 125, 0.2);
} }
/* --- Curated Shipping Identity Panels --- */ /* --- Saved Addresses Section Layout --- */
.address-curated-card { .address-curated-card {
transition: all 0.23s cubic-bezier(0.16, 1, 0.3, 1); transition: border-color 0.2s ease, box-shadow 0.2s ease;
} }
.address-curated-card:hover { .address-curated-card:hover {
border-color: #111111 !important; border-color: #111111 !important;
transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.03) !important;
} }
.border-top-dashed { .border-top-dashed {
@@ -142,22 +151,21 @@
} }
.btn-action-trigger { .btn-action-trigger {
transition: color 0.15s ease; padding: 0;
font-weight: 500;
letter-spacing: 0.04em;
opacity: 0.75;
transition: opacity 0.15s ease;
} }
.btn-action-trigger:hover:not(.text-danger) { .btn-action-trigger:hover {
color: #111111 !important; opacity: 1;
text-decoration: underline;
} }
.btn-action-trigger:hover.text-danger { /* --- Interactive Address Form Fields --- */
color: #A71D2A !important;
text-decoration: underline;
}
/* --- Interactive Form Element Overlays --- */
.premium-plaintext-field { .premium-plaintext-field {
border: 1px solid rgba(0, 0, 0, 0.12); background-color: #FAFAFA;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px; border-radius: 6px;
padding: 0.65rem 0.85rem; padding: 0.65rem 0.85rem;
font-size: 0.9rem; font-size: 0.9rem;
@@ -196,40 +204,69 @@
background: transparent; background: transparent;
border: none; border: none;
color: #666666; color: #666666;
transition: color 0.2s ease; transition: color 0.15s ease;
} }
.btn-clean-cancel:hover { .btn-clean-cancel:hover {
color: #111111; color: #111111;
} }
.border-y { /* --- Profile Settings Identity Pillar Elements --- */
border-top: 1px solid rgba(0, 0, 0, 0.06);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.context-clickable {
cursor: pointer;
user-select: none;
}
/* --- Identity Node Presentation Wrappers --- */
.profile-hero-banner { .profile-hero-banner {
background-color: #FAFAFA;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
} }
.animate-fade-in { /* --- CSS Grid Book Item Layout --- */
animation: layoutFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; .manifest-grid-wrap {
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 1rem;
width: 100%;
} }
@keyframes layoutFadeIn { @media (min-width: 768px) {
from { .manifest-grid-wrap {
opacity: 0; grid-template-columns: repeat(2, 1fr);
transform: translateY(6px);
}
to {
opacity: 1;
transform: translateY(0);
} }
} }
.manifest-grid-cell {
min-width: 0;
}
.manifest-book-item {
transition: background-color 0.15s ease;
}
.manifest-book-item:hover {
background-color: rgba(0, 0, 0, 0.015) !important;
}
.book-thumbnail-container {
width: 42px;
height: 56px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(0, 0, 0, 0.08) !important;
}
.book-thumbnail-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.shadow-xs {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.min-w-0 {
min-width: 0;
}
.order-summary-footer {
border-top: 1px solid rgba(0, 0, 0, 0.06) !important;
}