diff --git a/MidrandBookshop/Components/Layout/CartItem.cs b/MidrandBookshop/Components/Layout/CartItem.cs new file mode 100644 index 0000000..93278d3 --- /dev/null +++ b/MidrandBookshop/Components/Layout/CartItem.cs @@ -0,0 +1,10 @@ +namespace MidrandBookshop.Components.Layout; + +public class CartItem + { + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + public int Price { get; set; } + public int Quantity { get; set; } + } \ No newline at end of file diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor b/MidrandBookshop/Components/Layout/MainLayout.razor index 4b58446..aaace25 100644 --- a/MidrandBookshop/Components/Layout/MainLayout.razor +++ b/MidrandBookshop/Components/Layout/MainLayout.razor @@ -2,8 +2,6 @@ @inject NavigationManager Navigation
- - @* --- CART SYSTEM SIDE PANEL BACKDROP LAYER --- *@
@@ -77,9 +75,7 @@ }
- @* --- TOP FIXED LAYOUT AREA --- *@
- @* Decorative Background SVG Watermark Line Graphic *@
@@ -138,15 +134,16 @@ + @bind="SearchInputBuffer" + @bind:event="oninput" + @onkeydown="HandleSearchKeyDown" />
-
- @* --- MAIN INDEPENDENT SCROLL LAYER --- *@
@@ -247,56 +243,3 @@
-@code { - private string GlobalSearchQuery { get; set; } = string.Empty; - private bool IsSearchActive { get; set; } = false; - private bool IsCartOpen { get; set; } = false; - - private List CartItems = new() - { - new CartItem { Id = 1, Title = "Letters from M/M (Paris)", Author = "M/M Paris", Price = 720, Quantity = 1 }, - new CartItem { Id = 2, Title = "Daan Paans: Floating Signifiers", Author = "Daan Paans", Price = 540, Quantity = 1 }, - new CartItem { Id = 3, Title = "Album Architectures, Maputo", Author = "Guedes Archive", Price = 350, Quantity = 1 } - }; - - private void ToggleGlobalSearch() => IsSearchActive = !IsSearchActive; - private void ToggleCart() => IsCartOpen = !IsCartOpen; - - private void OnSearchInput(ChangeEventArgs e) - { - GlobalSearchQuery = e.Value?.ToString() ?? string.Empty; - } - - private void ChangeQuantity(CartItem item, int delta) - { - item.Quantity += delta; - if (item.Quantity <= 0) - { - CartItems.Remove(item); - } - } - - private void RemoveFromCart(CartItem item) => CartItems.Remove(item); - private int GetCartTotal() => CartItems.Sum(item => item.Price * item.Quantity); - - private void RedirectToCart() - { - IsCartOpen = false; - Navigation.NavigateTo("/cart"); - } - - private void RedirectToCheckout() - { - IsCartOpen = false; - Navigation.NavigateTo("/checkout"); - } - - public class CartItem - { - public int Id { get; set; } - public string Title { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public int Price { get; set; } - public int Quantity { get; set; } - } -} \ No newline at end of file diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor.cs b/MidrandBookshop/Components/Layout/MainLayout.razor.cs new file mode 100644 index 0000000..b0bf51d --- /dev/null +++ b/MidrandBookshop/Components/Layout/MainLayout.razor.cs @@ -0,0 +1,131 @@ +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.WebUtilities; + +namespace MidrandBookshop.Components.Layout; + +public partial class MainLayout : IDisposable +{ + private string SearchInputBuffer { get; set; } = string.Empty; + private string GlobalSearchQuery { get; set; } = string.Empty; + private bool IsSearchActive { get; set; } = false; + private bool IsCartOpen { get; set; } = false; + + private List CartItems = new() + { + new CartItem { Id = 1, Title = "Letters from M/M (Paris)", Author = "M/M Paris", Price = 720, Quantity = 1 }, + new CartItem { Id = 2, Title = "Daan Paans: Floating Signifiers", Author = "Daan Paans", Price = 540, Quantity = 1 }, + new CartItem { Id = 3, Title = "Album Architectures, Maputo", Author = "Guedes Archive", Price = 350, Quantity = 1 } + }; + + protected override void OnInitialized() + { + Navigation.LocationChanged += OnLocationChanged; + + SyncSearchQueryFromUrl(); + } + + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + SyncSearchQueryFromUrl(); + StateHasChanged(); + } + + private void SyncSearchQueryFromUrl() + { + var uri = Navigation.ToAbsoluteUri(Navigation.Uri); + var queryParameters = QueryHelpers.ParseQuery(uri.Query); + + if (queryParameters.TryGetValue("q", out var queryVal) && !string.IsNullOrWhiteSpace(queryVal)) + { + GlobalSearchQuery = queryVal.ToString(); + SearchInputBuffer = GlobalSearchQuery; + IsSearchActive = true; + } + else + { + GlobalSearchQuery = string.Empty; + SearchInputBuffer = string.Empty; + IsSearchActive = false; + } + } + + private void ToggleGlobalSearch() + { + if (!IsSearchActive) + IsSearchActive = true; + else + CommitSearchNavigation(); + } + + private void HandleSearchKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter") CommitSearchNavigation(); + } + + private void CommitSearchNavigation() + { + var uri = Navigation.ToAbsoluteUri(Navigation.Uri); + var queryParameters = QueryHelpers.ParseQuery(uri.Query); + + var newRouteParams = new Dictionary(); + + foreach (var param in queryParameters) + { + if (param.Key != "q") + { + newRouteParams[param.Key] = param.Value.ToString(); + } + } + + if (!string.IsNullOrWhiteSpace(SearchInputBuffer)) + newRouteParams["q"] = SearchInputBuffer.Trim(); + else + newRouteParams["q"] = null; + + var baseRoute = uri.AbsolutePath.StartsWith("/product/", StringComparison.OrdinalIgnoreCase) ? "/" : uri.AbsolutePath; + var updatedUri = Navigation.GetUriWithQueryParameters(baseRoute, newRouteParams); + + Navigation.NavigateTo(updatedUri); + } + + private void ToggleCart() => IsCartOpen = !IsCartOpen; + + private void ChangeQuantity(CartItem item, int delta) + { + item.Quantity += delta; + if (item.Quantity <= 0) + { + CartItems.Remove(item); + } + } + + private void RemoveFromCart(CartItem item) => CartItems.Remove(item); + private int GetCartTotal() => CartItems.Sum(item => item.Price * item.Quantity); + + private void RedirectToCart() + { + IsCartOpen = false; + Navigation.NavigateTo("/cart"); + } + + private void RedirectToCheckout() + { + IsCartOpen = false; + Navigation.NavigateTo("/checkout"); + } + + public void Dispose() + { + Navigation.LocationChanged -= OnLocationChanged; + } + + public class CartItem + { + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + public int Price { get; set; } + public int Quantity { get; set; } + } +} \ No newline at end of file diff --git a/MidrandBookshop/Components/Layout/MainLayout.razor.css b/MidrandBookshop/Components/Layout/MainLayout.razor.css index 2c6966e..857fed0 100644 --- a/MidrandBookshop/Components/Layout/MainLayout.razor.css +++ b/MidrandBookshop/Components/Layout/MainLayout.razor.css @@ -20,10 +20,10 @@ transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1); } -.cart-overlay.is-visible { - opacity: 1; - pointer-events: auto; -} + .cart-overlay.is-visible { + opacity: 1; + pointer-events: auto; + } .cart-drawer { position: fixed; @@ -37,10 +37,10 @@ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); } -.cart-drawer.is-open { - transform: translateX(-420px); - pointer-events: auto; -} + .cart-drawer.is-open { + transform: translateX(-420px); + pointer-events: auto; + } .cart-badge { position: absolute; @@ -71,16 +71,16 @@ padding: 2px 4px; } -.quantity-picker button { - font-size: 0.85rem; - font-weight: 600; - line-height: 1; -} + .quantity-picker button { + font-size: 0.85rem; + font-weight: 600; + line-height: 1; + } -.quantity-picker button:hover { - background-color: rgba(0,0,0,0.05); - border-radius: 50%; -} + .quantity-picker button:hover { + background-color: rgba(0,0,0,0.05); + border-radius: 50%; + } .custom-site-footer { width: 100%; @@ -120,9 +120,9 @@ transition: color 0.2s ease; } -.footer-contact-link:hover { - color: #FFFFFF !important; -} + .footer-contact-link:hover { + color: #FFFFFF !important; + } .footer-section-heading { font-size: 0.65rem; @@ -138,9 +138,9 @@ transition: color 0.2s ease; } -.footer-nav-link:hover { - color: #FFFFFF !important; -} + .footer-nav-link:hover { + color: #FFFFFF !important; + } .footer-meta-item { font-size: 0.8rem; @@ -179,15 +179,15 @@ transition: transform 0.2s ease, background-color 0.2s ease; } -.btn-back-to-top:hover { - background-color: #333333; - transform: translateY(-2px); -} + .btn-back-to-top:hover { + background-color: #333333; + transform: translateY(-2px); + } -.btn-back-to-top:active { - transform: translateY(0); -} + .btn-back-to-top:active { + transform: translateY(0); + } .scroll-container { scroll-behavior: smooth; -} \ No newline at end of file +} diff --git a/MidrandBookshop/Components/Pages/Home.razor.cs b/MidrandBookshop/Components/Pages/Home.razor.cs index f7ce3f2..aedb677 100644 --- a/MidrandBookshop/Components/Pages/Home.razor.cs +++ b/MidrandBookshop/Components/Pages/Home.razor.cs @@ -16,7 +16,7 @@ public partial class Home : ComponentBase [Inject] private CategoryService CategoryService { get; set; } = default!; [Inject] private NavigationManager Navigation { get; set; } = default!; - [CascadingParameter] public string SharedSearchQuery { get; set; } = string.Empty; + [SupplyParameterFromQuery(Name = "q")] public string? SharedSearchQuery { get; set; } [SupplyParameterFromQuery] public long? AuthorId { get; set; } public enum ViewMode { Grid, List } @@ -40,6 +40,7 @@ public partial class Home : ComponentBase private Dictionary ProductPriceCache { get; set; } = []; private Dictionary ProductPrimaryCategoryCache { get; set; } = []; + private Dictionary ProductAuthorCache { get; set; } = []; private IEnumerable FilteredData { @@ -47,26 +48,24 @@ public partial class Home : ComponentBase { var data = ProductsCollection.AsEnumerable(); - // 1. Category Filtering if (ActiveCategory != "All" && !AuthorId.HasValue) { data = data.Where(p => ProductPrimaryCategoryCache.ContainsKey(p.Id) && ProductPrimaryCategoryCache[p.Id] == ActiveCategory); } - // 2. Text Search Query Matching if (!string.IsNullOrWhiteSpace(SharedSearchQuery)) { var q = SharedSearchQuery.Trim(); data = data.Where(p => (p.Name ?? "").Contains(q, StringComparison.OrdinalIgnoreCase)); } - // 3. FIX: Price Tier Constraint Selection Layout if (ActivePriceFilter != "all") { data = data.Where(p => { var price = ProductPriceCache.TryGetValue(p.Id, out var amt) ? amt : 0m; + return ActivePriceFilter switch { "under-500" => price < 500m, @@ -77,20 +76,14 @@ public partial class Home : ComponentBase }); } - // 4. FIX: New Acquisition Flag Status Check - if (OnlyShowNew) - { - // Utilizing your mapping configuration: book.Enabled defines new arrival status - data = data.Where(p => p.Enabled); - } + if (OnlyShowNew) data = data.Where(p => p.Enabled); - // 5. FIX: Collection Sorting Pipeline Extensions data = SelectedSortOption switch { "price-low" => data.OrderBy(p => ProductPriceCache.TryGetValue(p.Id, out var amt) ? amt : 0m), "price-high" => data.OrderByDescending(p => ProductPriceCache.TryGetValue(p.Id, out var amt) ? amt : 0m), "title-asc" => data.OrderBy(p => p.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase), - "default" or _ => data // Fallback to raw catalog chronological order sequence + "default" or _ => data }; return data; @@ -108,9 +101,9 @@ public partial class Home : ComponentBase ProductsCollection.Clear(); ProductPriceCache.Clear(); ProductPrimaryCategoryCache.Clear(); + ProductAuthorCache.Clear(); ActiveAuthorFilterName = null; - // Pipeline A: Extract scoped books directly associated with an ID token parameter if (AuthorId.HasValue) { var authorResult = await AuthorService.GetAuthorAsync(AuthorId.Value); @@ -135,9 +128,10 @@ public partial class Home : ComponentBase var product = authorBook.Product; ProductsCollection.Add(product); - ProductPriceCache[product.Id] = product.Price?.Amount ?? 0m; + ProductAuthorCache[product.Id] = ActiveAuthorFilterName ?? "Unknown Author"; + var categoryResult = await ProductService.GetProductCategoriesAsync(product.Id); ProductPrimaryCategoryCache[product.Id] = (categoryResult.IsSuccess && categoryResult.Value.Length > 0) @@ -149,7 +143,6 @@ public partial class Home : ComponentBase return; } - // Pipeline B: Safe structural fallback mapping utilizing the exact backend DateRange object setup var selectionRange = new DateRange { From = new DateOnly(2020, 1, 1), @@ -172,6 +165,10 @@ public partial class Home : ComponentBase ProductPrimaryCategoryCache[product.Id] = (categoryResult.IsSuccess && categoryResult.Value.Length > 0) ? (categoryResult.Value[0].Name ?? "General") : "General"; + + ProductAuthorCache[product.Id] = !string.IsNullOrWhiteSpace(product.Metadata?.Manufacturer) + ? product.Metadata.Manufacturer + : "Unknown Author"; } } @@ -189,15 +186,38 @@ public partial class Home : ComponentBase } } - private void ClearAuthorFilter() => Navigation.NavigateTo("/"); + private void ClearAuthorFilter() + { + var newUri = Navigation.GetUriWithQueryParameters("/", new Dictionary { { "authorId", null } }); + + Navigation.NavigateTo(newUri); + } + + private void ResetFilters() + { + SelectedSortOption = "default"; + ActivePriceFilter = "all"; + OnlyShowNew = false; + VisibleCount = ItemsPerPage; + } + private void NavigateToProduct(long id) => Navigation.NavigateTo($"/product/{id}"); private void SetViewMode(ViewMode targetMode) => CurrentViewMode = targetMode; - private void SelectCategory(string categoryName) { ActiveCategory = categoryName; VisibleCount = ItemsPerPage; } + + private void SelectCategory(string categoryName) + { + ActiveCategory = categoryName; + VisibleCount = ItemsPerPage; + + var updatedUri = Navigation.GetUriWithQueryParameters(Navigation.Uri, new Dictionary {{ "q", null }}); + + Navigation.NavigateTo(updatedUri); + } + private void ToggleExtraCategories() => ShowExpandedCategories = !ShowExpandedCategories; private void ToggleFilterMenu() => ShowFilterMenu = !ShowFilterMenu; private void ChangeSort(string sortOption) => SelectedSortOption = sortOption; private void ChangePriceFilter(string priceBracket) { ActivePriceFilter = priceBracket; VisibleCount = ItemsPerPage; } private void ToggleNewArrivalsOnly(ChangeEventArgs e) { OnlyShowNew = e.Value is bool b && b; VisibleCount = ItemsPerPage; } - private void ResetFilters() { SelectedSortOption = "default"; ActivePriceFilter = "all"; OnlyShowNew = false; VisibleCount = ItemsPerPage; } private void LoadNextPage() { if (HasMoreItems) VisibleCount += ItemsPerPage; } } \ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/Home.razor.css b/MidrandBookshop/Components/Pages/Home.razor.css index 7e5cdc4..82cda5c 100644 --- a/MidrandBookshop/Components/Pages/Home.razor.css +++ b/MidrandBookshop/Components/Pages/Home.razor.css @@ -169,4 +169,4 @@ html { color: #ffffff !important; border-color: #ffffff !important; box-shadow: 0 0 0 2px #1a1a1a, 0 6px 16px rgba(0, 0, 0, 0.25) !important; - } \ No newline at end of file + } diff --git a/MidrandBookshop/MidrandBookshop.csproj b/MidrandBookshop/MidrandBookshop.csproj index fb22307..ccff6e2 100644 --- a/MidrandBookshop/MidrandBookshop.csproj +++ b/MidrandBookshop/MidrandBookshop.csproj @@ -18,13 +18,13 @@ - + - +