diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor b/MidrandBookshop/Components/Layout/MainLayout.razor
index a766e28..3090d41 100644
--- a/MidrandBookshop/Components/Layout/MainLayout.razor
+++ b/MidrandBookshop/Components/Layout/MainLayout.razor
@@ -158,7 +158,7 @@
@if (CartItems.Any())
{
- @CartItems.Sum(i => i.Quantity)
+ @ShoppingCart.Items.Count
}
diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor.cs b/MidrandBookshop/Components/Layout/MainLayout.razor.cs
index db11298..015302b 100644
--- a/MidrandBookshop/Components/Layout/MainLayout.razor.cs
+++ b/MidrandBookshop/Components/Layout/MainLayout.razor.cs
@@ -1,11 +1,19 @@
-using Microsoft.AspNetCore.Components.Routing;
-using Microsoft.AspNetCore.Components.Web;
-using Microsoft.AspNetCore.WebUtilities;
+using MidrandBookshop.Services.ShoppingCart;
+using MidrandBookshop.Services.ShoppingCart.Models;
namespace MidrandBookshop.Components.Layout;
-public partial class MainLayout : IDisposable
+public partial class MainLayout(CartService cartService) : IDisposable
{
+ [Inject]
+ private AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
+
+ private Cart ShoppingCart => cartService.ShoppingCart;
+
+ private AuthenticationState? AuthState { get; set; }
+ private System.Security.Claims.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;
@@ -18,19 +26,25 @@ public partial class MainLayout : IDisposable
new CartItem { Id = 3, Title = "Album Architectures, Maputo", Author = "Guedes Archive", Price = 350, Quantity = 1 }
};
- private void TriggerHeaderLogout()
+ protected override async Task OnInitializedAsync()
{
- // Force tear-down of the active client websocket pipeline safely
- Navigation.NavigateTo("/logout", forceLoad: true);
- }
+ AuthState = await AuthStateProvider.GetAuthenticationStateAsync();
+ User = AuthState!.User;
- protected override void OnInitialized()
- {
Navigation.LocationChanged += OnLocationChanged;
+ cartService.OnCartChanged += CartService_OnCartChanged;
SyncSearchQueryFromUrl();
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ await cartService.LoadCartFromStorageAsync();
+ }
+
+ private async void CartService_OnCartChanged() => await InvokeAsync(StateHasChanged);
+
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
SyncSearchQueryFromUrl();
@@ -107,7 +121,8 @@ public partial class MainLayout : IDisposable
}
private void RemoveFromCart(CartItem item) => CartItems.Remove(item);
- private int GetCartTotal() => CartItems.Sum(item => item.Price * item.Quantity);
+
+ private decimal GetCartTotal() => ShoppingCart?.TotalPrice ?? 0.00m;
private void RedirectToCart()
{
@@ -124,6 +139,9 @@ public partial class MainLayout : IDisposable
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
+ cartService.OnCartChanged -= CartService_OnCartChanged;
+
+ GC.SuppressFinalize(this);
}
public class CartItem
diff --git a/MidrandBookshop/Components/Pages/Account.razor b/MidrandBookshop/Components/Pages/Account.razor
index 0e55d84..b8dbb6a 100644
--- a/MidrandBookshop/Components/Pages/Account.razor
+++ b/MidrandBookshop/Components/Pages/Account.razor
@@ -225,77 +225,4 @@
-
-
-@code {
- private bool showAddForm = false;
- private AddressItem? editingAddress = null;
- private string newAddressName = "";
- private string newStreetAddress = "";
- private string newCity = "";
- private string newPostalCode = "";
- private bool isBilling, isShipping;
-
- private List orderHistory = new()
- {
- 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", Total = 720.00 },
- 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 Books Warehouse", Status = "Delivered", Total = 890.00 }
- };
-
- private List savedAddresses = new()
- {
- new AddressItem { Id = 1, Name = "Home Address", Street = "12 Main Road", City = "Midrand", PostalCode = "1685", IsBilling = true, IsShipping = true, IsPrimary = true },
- new AddressItem { Id = 2, Name = "Corporate Office", Street = "45 Challink Street", City = "Halfway House", PostalCode = "1682", IsBilling = true, IsShipping = false, IsPrimary = false },
- new AddressItem { Id = 3, Name = "Midrand Books Warehouse", Street = "Unit 8, Corporate Park North", City = "Randjespark", PostalCode = "1683", IsBilling = false, IsShipping = true, IsPrimary = false }
- };
-
- private void TriggerLogout() => Navigation.NavigateTo("/logout", forceLoad: true);
- private void DownloadInvoice(string orderId) { /* Handle download sequence here */ }
- private void OpenAddForm() { editingAddress = null; showAddForm = true; }
-
- private void SaveAddress()
- {
- if (!string.IsNullOrWhiteSpace(newAddressName) && !string.IsNullOrWhiteSpace(newStreetAddress))
- {
- var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1;
- savedAddresses.Add(new AddressItem
- {
- Id = nextId,
- Name = newAddressName,
- Street = newStreetAddress,
- City = newCity,
- PostalCode = newPostalCode,
- IsBilling = isBilling,
- IsShipping = isShipping,
- IsPrimary = !savedAddresses.Any()
- });
- ResetAddForm();
- }
- }
-
- private void ResetAddForm() { newAddressName = ""; newStreetAddress = ""; newCity = ""; newPostalCode = ""; isBilling = isShipping = showAddForm = false; }
- private void StartEditing(AddressItem addr) { showAddForm = false; editingAddress = new AddressItem { Id = addr.Id, Name = addr.Name, Street = addr.Street, City = addr.City, PostalCode = addr.PostalCode, IsBilling = addr.IsBilling, IsShipping = addr.IsShipping, IsPrimary = addr.IsPrimary }; }
- private void CancelEditing() => editingAddress = null;
-
- private void UpdateAddress()
- {
- if (editingAddress != null)
- {
- var target = savedAddresses.FirstOrDefault(a => a.Id == editingAddress.Id);
- if (target != null) { target.Name = editingAddress.Name; target.Street = editingAddress.Street; target.City = editingAddress.City; target.PostalCode = editingAddress.PostalCode; target.IsBilling = editingAddress.IsBilling; target.IsShipping = editingAddress.IsShipping; }
- editingAddress = null;
- }
- }
-
- private void DeleteAddress(AddressItem addr) { if (editingAddress?.Id == addr.Id) editingAddress = null; savedAddresses.Remove(addr); if (addr.IsPrimary && savedAddresses.Any()) savedAddresses.First().IsPrimary = true; }
-
- private void SetPrimary(AddressItem target, ChangeEventArgs e)
- {
- var isChecked = (bool)(e.Value ?? false);
- if (isChecked) { foreach (var addr in savedAddresses) addr.IsPrimary = (addr.Id == target.Id); }
- else target.IsPrimary = false;
- }
-
- public class AddressItem { public int Id { get; set; } public string Name { get; set; } = ""; public string Street { get; set; } = ""; public string City { get; set; } = ""; public string PostalCode { get; set; } = ""; public bool IsBilling { get; set; } public bool IsShipping { get; set; } public bool IsPrimary { get; set; } }
- public class OrderItem { public string OrderId { get; set; } = ""; public string ProductId { get; set; } = ""; public string ProductTitle { get; set; } = ""; public DateTime OrderDate { get; set; } public string ShippingAddressName { get; set; } = ""; public string Status { get; set; } = ""; public double Total { get; set; } }
-}
\ No newline at end of file
+
\ No newline at end of file
diff --git a/MidrandBookshop/Components/Pages/Account.razor.cs b/MidrandBookshop/Components/Pages/Account.razor.cs
new file mode 100644
index 0000000..52416b7
--- /dev/null
+++ b/MidrandBookshop/Components/Pages/Account.razor.cs
@@ -0,0 +1,75 @@
+namespace MidrandBookshop.Components.Pages;
+
+public partial class Account
+{
+ private bool showAddForm = false;
+ private AddressItem? editingAddress = null;
+ private string newAddressName = "";
+ private string newStreetAddress = "";
+ private string newCity = "";
+ private string newPostalCode = "";
+ private bool isBilling, isShipping;
+
+ private List orderHistory = new()
+ {
+ 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", Total = 720.00 },
+ 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 Books Warehouse", Status = "Delivered", Total = 890.00 }
+ };
+
+ private List savedAddresses = new()
+ {
+ new AddressItem { Id = 1, Name = "Home Address", Street = "12 Main Road", City = "Midrand", PostalCode = "1685", IsBilling = true, IsShipping = true, IsPrimary = true },
+ new AddressItem { Id = 2, Name = "Corporate Office", Street = "45 Challink Street", City = "Halfway House", PostalCode = "1682", IsBilling = true, IsShipping = false, IsPrimary = false },
+ new AddressItem { Id = 3, Name = "Midrand Books Warehouse", Street = "Unit 8, Corporate Park North", City = "Randjespark", PostalCode = "1683", IsBilling = false, IsShipping = true, IsPrimary = false }
+ };
+
+ private void TriggerLogout() => Navigation.NavigateTo("/logout", forceLoad: true);
+ private void DownloadInvoice(string orderId) { /* Handle download sequence here */ }
+ private void OpenAddForm() { editingAddress = null; showAddForm = true; }
+
+ private void SaveAddress()
+ {
+ if (!string.IsNullOrWhiteSpace(newAddressName) && !string.IsNullOrWhiteSpace(newStreetAddress))
+ {
+ var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1;
+ savedAddresses.Add(new AddressItem
+ {
+ Id = nextId,
+ Name = newAddressName,
+ Street = newStreetAddress,
+ City = newCity,
+ PostalCode = newPostalCode,
+ IsBilling = isBilling,
+ IsShipping = isShipping,
+ IsPrimary = !savedAddresses.Any()
+ });
+ ResetAddForm();
+ }
+ }
+
+ private void ResetAddForm() { newAddressName = ""; newStreetAddress = ""; newCity = ""; newPostalCode = ""; isBilling = isShipping = showAddForm = false; }
+ private void StartEditing(AddressItem addr) { showAddForm = false; editingAddress = new AddressItem { Id = addr.Id, Name = addr.Name, Street = addr.Street, City = addr.City, PostalCode = addr.PostalCode, IsBilling = addr.IsBilling, IsShipping = addr.IsShipping, IsPrimary = addr.IsPrimary }; }
+ private void CancelEditing() => editingAddress = null;
+
+ private void UpdateAddress()
+ {
+ if (editingAddress != null)
+ {
+ var target = savedAddresses.FirstOrDefault(a => a.Id == editingAddress.Id);
+ if (target != null) { target.Name = editingAddress.Name; target.Street = editingAddress.Street; target.City = editingAddress.City; target.PostalCode = editingAddress.PostalCode; target.IsBilling = editingAddress.IsBilling; target.IsShipping = editingAddress.IsShipping; }
+ editingAddress = null;
+ }
+ }
+
+ private void DeleteAddress(AddressItem addr) { if (editingAddress?.Id == addr.Id) editingAddress = null; savedAddresses.Remove(addr); if (addr.IsPrimary && savedAddresses.Any()) savedAddresses.First().IsPrimary = true; }
+
+ private void SetPrimary(AddressItem target, ChangeEventArgs e)
+ {
+ var isChecked = (bool)(e.Value ?? false);
+ if (isChecked) { foreach (var addr in savedAddresses) addr.IsPrimary = (addr.Id == target.Id); }
+ else target.IsPrimary = false;
+ }
+
+ public class AddressItem { public int Id { get; set; } public string Name { get; set; } = ""; public string Street { get; set; } = ""; public string City { get; set; } = ""; public string PostalCode { get; set; } = ""; public bool IsBilling { get; set; } public bool IsShipping { get; set; } public bool IsPrimary { get; set; } }
+ public class OrderItem { public string OrderId { get; set; } = ""; public string ProductId { get; set; } = ""; public string ProductTitle { get; set; } = ""; public DateTime OrderDate { get; set; } public string ShippingAddressName { get; set; } = ""; public string Status { get; set; } = ""; public double Total { get; set; } }
+}
diff --git a/MidrandBookshop/Components/Pages/ProductView.razor.cs b/MidrandBookshop/Components/Pages/ProductView.razor.cs
index 09090e3..dcafa21 100644
--- a/MidrandBookshop/Components/Pages/ProductView.razor.cs
+++ b/MidrandBookshop/Components/Pages/ProductView.razor.cs
@@ -3,6 +3,7 @@ using LiteCharms.Features.MidrandBooks.Authors;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Products;
using LiteCharms.Features.MidrandBooks.Products.Models;
+using MidrandBookshop.Services.ShoppingCart;
namespace MidrandBookshop.Components.Pages;
@@ -12,10 +13,12 @@ public partial class ProductView : ComponentBase
[Inject] private ProductService ProductService { get; set; } = default!;
[Inject] private AuthorService AuthorService { get; set; } = default!;
+ [Inject] private CartService CartService { get; set; } = default!;
[Inject] private NavigationManager Navigation { get; set; } = default!;
protected bool IsLoading { get; private set; } = true;
protected Product? CurrentProduct { get; private set; }
+ protected ProductPrice? CurrentPrice { get; private set; }
protected decimal LivePrice { get; private set; } = 0.00m;
protected string AuthorName { get; private set; } = "Unknown Author";
protected string PrimaryCategory { get; private set; } = "General";
@@ -44,6 +47,7 @@ public partial class ProductView : ComponentBase
var priceResult = await ProductService.GetProductPriceAsync(BookId);
LivePrice = priceResult.IsSuccess ? priceResult.Value.Amount : 0m;
+ CurrentPrice = priceResult.IsSuccess ? priceResult.Value : null;
var categoryResult = await ProductService.GetProductCategoriesAsync(BookId);
if (categoryResult.IsSuccess && categoryResult.Value.Length > 0)
@@ -73,6 +77,7 @@ public partial class ProductView : ComponentBase
catch (Exception)
{
CurrentProduct = null;
+ CurrentPrice = null;
}
finally
{
@@ -80,14 +85,34 @@ public partial class ProductView : ComponentBase
}
}
- protected void IncreaseQty() => Quantity++;
- protected void DecreaseQty() { if (Quantity > 1) Quantity--; }
+ protected void IncreaseQty()
+ {
+ Quantity++;
- protected void HandleAddToCart()
+ if (CurrentPrice is not null)
+ CartService.UpdateQuantity(CurrentPrice!.Id, Quantity);
+ }
+ protected void DecreaseQty()
+ {
+ if (Quantity > 1)
+ {
+ Quantity--;
+
+ CartService.UpdateQuantity(CurrentPrice!.Id, Quantity);
+ }
+ }
+
+ protected async void HandleAddToCart()
{
if (CurrentProduct == null) return;
+
+ if (CurrentPrice is not null)
+ {
+ CartService.AddItem(CurrentPrice);
+ Quantity++;
+ }
}
-
+
protected void ViewAllAuthorBooks()
{
if (CurrentAuthor is not null)
diff --git a/MidrandBookshop/MidrandBookshop.csproj b/MidrandBookshop/MidrandBookshop.csproj
index 2bcaf5e..f9c9cce 100644
--- a/MidrandBookshop/MidrandBookshop.csproj
+++ b/MidrandBookshop/MidrandBookshop.csproj
@@ -18,13 +18,13 @@
-
+
-
+
@@ -51,6 +51,10 @@
+
+
+
+
diff --git a/MidrandBookshop/Program.cs b/MidrandBookshop/Program.cs
index 1172d23..0c2f01b 100644
--- a/MidrandBookshop/Program.cs
+++ b/MidrandBookshop/Program.cs
@@ -3,6 +3,7 @@ using LiteCharms.Features.Mediator;
using LiteCharms.Features.MidrandBooks.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
using MidrandBookshop.Components;
+using MidrandBookshop.Services.ShoppingCart;
using static LiteCharms.Features.Extensions.Quartz;
var builder = WebApplication.CreateBuilder(args);
@@ -26,6 +27,7 @@ builder.Services.AddEmailServiceBus();
builder.Services.AddHttpClient();
builder.Services.AddShopServices();
+builder.Services.AddScoped();
builder.Services.AddHashServices(builder.Configuration);
builder.Services.AddMidrandShopDatabase(builder.Configuration);
diff --git a/MidrandBookshop/Services/ShoppingCart/CartService.cs b/MidrandBookshop/Services/ShoppingCart/CartService.cs
new file mode 100644
index 0000000..0f84fb6
--- /dev/null
+++ b/MidrandBookshop/Services/ShoppingCart/CartService.cs
@@ -0,0 +1,153 @@
+using LiteCharms.Features.Browser;
+using LiteCharms.Features.Hasher;
+using LiteCharms.Features.MidrandBooks.Products.Models;
+using MidrandBookshop.Services.ShoppingCart.Models;
+
+namespace MidrandBookshop.Services.ShoppingCart;
+
+public sealed class CartService(LocalStorageService localStorage)
+{
+ private readonly string CartStorageKey = HashService.ToMd5Hash(nameof(Cart)).Value;
+
+ public Cart ShoppingCart { get; private set; } = new();
+
+ public event Action? OnCartChanged;
+
+ public Cart GetCart() => ShoppingCart;
+
+ public void NotifyStateChanged() => OnCartChanged?.Invoke();
+
+ public async Task LoadCartFromStorageAsync()
+ {
+ var loadResult = await localStorage.GetAsync(CartStorageKey);
+
+ if (loadResult.IsFailed) await localStorage.SaveAsync(CartStorageKey, ShoppingCart);
+
+ if (loadResult.IsSuccess) ShoppingCart = loadResult.Value;
+
+ NotifyStateChanged();
+ }
+
+ public async Task SaveCartToStorageAsync() => await localStorage.SaveAsync(CartStorageKey, ShoppingCart);
+
+ public void AddItem(ProductPrice productPrice)
+ {
+ var itemExists = false;
+
+ for (var i = 0; i < ShoppingCart.Items.Count; i++)
+ {
+ if (ShoppingCart.Items[i].Price!.Id == productPrice.Id)
+ {
+ ShoppingCart.Items[i].Quantity++;
+ ShoppingCart.Items[i].Amount += productPrice.Amount;
+
+ itemExists = true;
+
+ break;
+ }
+ }
+
+ if (!itemExists)
+ ShoppingCart.Items.Add(new CartItem
+ {
+ Price = productPrice,
+ Amount = productPrice.Amount,
+ Quantity = 1,
+ });
+
+ CalculateTotalPrice();
+ NotifyStateChanged();
+ }
+
+ public void UpdateQuantity(long productPriceId, int newQuantity)
+ {
+ if (newQuantity <= 0)
+ {
+ RemoveAllSameItem(productPriceId);
+ NotifyStateChanged();
+
+ return;
+ }
+
+ for (var i = 0; i < ShoppingCart.Items.Count; i++)
+ {
+ if (ShoppingCart.Items[i].Price!.Id == productPriceId)
+ {
+ var oldQuantity = ShoppingCart.Items[i].Quantity;
+ var pricePerUnit = ShoppingCart.Items[i].Price!.Amount;
+
+ ShoppingCart.Items[i].Quantity = newQuantity;
+ ShoppingCart.Items[i].Amount = pricePerUnit * newQuantity;
+ break;
+ }
+ }
+
+ CalculateTotalPrice();
+ NotifyStateChanged();
+ }
+
+ public void RemoveOneItem(long productPriceId)
+ {
+ for (var i = 0; i < ShoppingCart.Items.Count; i++)
+ {
+ if (ShoppingCart.Items[i].Price!.Id == productPriceId)
+ {
+ if (ShoppingCart.Items[i].Quantity <= 1)
+ {
+ ShoppingCart.Items.RemoveAt(i);
+ }
+ else
+ {
+ ShoppingCart.Items[i].Quantity--;
+ ShoppingCart.Items[i].Amount -= ShoppingCart.Items[i].Price!.Amount;
+ }
+
+ break;
+ }
+ }
+
+ CalculateTotalPrice();
+ NotifyStateChanged();
+ }
+
+ public void RemoveAllSameItem(long productPriceId)
+ {
+ if (ShoppingCart.Items.Count == 0) return;
+
+ var item = ShoppingCart.Items.FirstOrDefault(i => i.Price?.Id == productPriceId);
+
+ if (item is not null) ShoppingCart.Items.Remove(item);
+
+ CalculateTotalPrice();
+ NotifyStateChanged();
+ }
+
+ public void Clear()
+ {
+ if(ShoppingCart.CustomerId is not null || ShoppingCart.OrderId is not null)
+ {
+ ShoppingCart.TotalPrice = 0;
+ ShoppingCart.TotalVat = 0;
+ ShoppingCart.Items.Clear();
+
+ return;
+ }
+
+ ShoppingCart = new Cart();
+
+ NotifyStateChanged();
+ }
+
+ public decimal CalculateTotalPrice()
+ {
+ if (ShoppingCart.Items.Count == 0) return 0;
+
+ var gross = ShoppingCart.Items.Sum(i => i.Amount);
+
+ if (!ShoppingCart.IsVatInclusive) ShoppingCart.TotalVat = gross * ShoppingCart.VatRate;
+
+ ShoppingCart.TotalPrice = gross + ShoppingCart.TotalVat;
+
+ return ShoppingCart.TotalPrice;
+ }
+}
diff --git a/MidrandBookshop/Services/ShoppingCart/Models/Cart.cs b/MidrandBookshop/Services/ShoppingCart/Models/Cart.cs
new file mode 100644
index 0000000..cc9f043
--- /dev/null
+++ b/MidrandBookshop/Services/ShoppingCart/Models/Cart.cs
@@ -0,0 +1,18 @@
+namespace MidrandBookshop.Services.ShoppingCart.Models;
+
+public sealed class Cart
+{
+ public long? CustomerId { get; set; }
+
+ public long? OrderId { get; set; }
+
+ public decimal TotalPrice { get; set; }
+
+ public decimal TotalVat { get; set; }
+
+ public decimal VatRate { get; set; } = 0.15m;
+
+ public bool IsVatInclusive { get; set; } = true;
+
+ public IList Items { get; set; } = [];
+}
diff --git a/MidrandBookshop/Services/ShoppingCart/Models/CartItem.cs b/MidrandBookshop/Services/ShoppingCart/Models/CartItem.cs
new file mode 100644
index 0000000..3c891d9
--- /dev/null
+++ b/MidrandBookshop/Services/ShoppingCart/Models/CartItem.cs
@@ -0,0 +1,12 @@
+using LiteCharms.Features.MidrandBooks.Products.Models;
+
+namespace MidrandBookshop.Services.ShoppingCart.Models;
+
+public sealed class CartItem
+{
+ public ProductPrice? Price { get; set; }
+
+ public long Quantity { get; set; }
+
+ public decimal Amount { get; set; }
+}