From af02cbc6491c705671ca6b674eabea353a0e3e3a Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 19:48:06 +0200 Subject: [PATCH 1/2] Working filter --- .../Components/Pages/Home.razor.cs | 39 ++++++++++++-- .../Components/Pages/ProductView.razor.css | 51 +++++++++++++------ 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/MidrandBookshop/Components/Pages/Home.razor.cs b/MidrandBookshop/Components/Pages/Home.razor.cs index 5372ac5..f7ce3f2 100644 --- a/MidrandBookshop/Components/Pages/Home.razor.cs +++ b/MidrandBookshop/Components/Pages/Home.razor.cs @@ -47,19 +47,52 @@ public partial class Home : ComponentBase { var data = ProductsCollection.AsEnumerable(); - // Category filtering restricts rendering solely when checking the open catalog + // 1. Category Filtering if (ActiveCategory != "All" && !AuthorId.HasValue) + { data = data.Where(p => ProductPrimaryCategoryCache.ContainsKey(p.Id) && ProductPrimaryCategoryCache[p.Id] == ActiveCategory); + } - // Text matching is completely restricted from evaluating author metadata properties + // 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, + "500-1000" => price >= 500m && price <= 1000m, + "over-1000" => price > 1000m, + _ => true + }; + }); + } + + // 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); + } + + // 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 + }; + return data; } } diff --git a/MidrandBookshop/Components/Pages/ProductView.razor.css b/MidrandBookshop/Components/Pages/ProductView.razor.css index 898af69..87b9198 100644 --- a/MidrandBookshop/Components/Pages/ProductView.razor.css +++ b/MidrandBookshop/Components/Pages/ProductView.razor.css @@ -1,7 +1,6 @@ /* ========================================================================== Structural Layout Containers ========================================================================== */ - .product-container { max-width: 1200px; margin: 0 auto; @@ -18,7 +17,6 @@ /* ========================================================================== Breadcrumb Navigation Links ========================================================================== */ - .breadcrumb { font-size: 0.85rem; margin-bottom: 2.5rem; @@ -49,7 +47,6 @@ /* ========================================================================== Left Section: Media Gallery Components ========================================================================== */ - .gallery-section { display: flex; flex-direction: column; @@ -76,16 +73,21 @@ mix-blend-mode: multiply; } -/* Dynamic Overlaid Attribute Badges */ +/* Dynamic Overlaid Attribute Badges - Centered flawlessly without stacking */ .format-badges { - position: absolute; - bottom: 1.5rem; - left: 50%; - transform: translateX(-50%); - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - white-space: nowrap; + position: absolute !important; + bottom: 1.5rem !important; + left: 50% !important; /* Move anchor point to exactly half-width */ + right: auto !important; + transform: translateX(-50%) !important; /* Offset width symmetrically to center perfectly */ + display: flex !important; + flex-direction: row !important; + flex-wrap: nowrap !important; /* Firmly prevents items from stacking vertically */ + justify-content: center !important; + align-items: center !important; + gap: 0.5rem !important; + width: max-content !important; /* Expands safely according to badges text values */ + max-width: 90% !important; /* Keeps bounding box clean of parent borders */ } .badge { @@ -96,6 +98,8 @@ border-radius: 100px; font-weight: 600; border: 1px solid transparent; + display: inline-block !important; + white-space: nowrap !important; /* Protects badge inner labels from broken line wrapping */ } .badge-physical { @@ -148,7 +152,6 @@ /* ========================================================================== Right Section: Product Details & Typography Controls ========================================================================== */ - .details-section { display: flex; flex-direction: column; @@ -209,7 +212,6 @@ /* ========================================================================== Interactive E-Commerce Selection Bars ========================================================================== */ - .purchase-actions { display: flex; gap: 1rem; @@ -274,7 +276,6 @@ /* ========================================================================== Informational Text Elements & Links ========================================================================== */ - .info-block { margin-bottom: 2rem; } @@ -321,7 +322,6 @@ /* ========================================================================== Responsive Adaptations & Breakpoints ========================================================================== */ - @media (max-width: 992px) { .product-layout { grid-template-columns: 1fr; @@ -338,6 +338,25 @@ padding: 1rem 1rem; } + .main-image-wrapper { + min-height: 320px; + padding: 2rem 1rem 4.5rem 1rem; /* Extra padding baseline clearance */ + } + + .main-image { + max-height: 240px; /* Fluidly limits image height to avoid overlay overlapping */ + } + + .format-badges { + bottom: 1.25rem !important; /* Fits beautifully aligned at the bottom center */ + gap: 0.4rem !important; + } + + .badge { + font-size: 0.65rem !important; /* Clean uniform scaling factor to clear margins */ + padding: 0.25rem 0.65rem !important; + } + .product-title { font-size: 1.85rem; } -- 2.47.3 From 01a026445273de5a1b8c7908b00c971e3f6edb47 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 21:00:13 +0200 Subject: [PATCH 2/2] Working search --- MidrandBookshop/Components/Layout/CartItem.cs | 10 ++ .../Components/Layout/MainLayout.razor | 71 +--------- .../Components/Layout/MainLayout.razor.cs | 131 ++++++++++++++++++ .../Components/Layout/MainLayout.razor.css | 62 ++++----- .../Components/Pages/Home.razor.cs | 56 +++++--- .../Components/Pages/Home.razor.css | 2 +- MidrandBookshop/MidrandBookshop.csproj | 4 +- 7 files changed, 220 insertions(+), 116 deletions(-) create mode 100644 MidrandBookshop/Components/Layout/CartItem.cs create mode 100644 MidrandBookshop/Components/Layout/MainLayout.razor.cs 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 @@ - + - + -- 2.47.3