diff --git a/MidrandBookshop/Components/BookCard.razor b/MidrandBookshop/Components/BookCard.razor index c3aec62..d28ae15 100644 --- a/MidrandBookshop/Components/BookCard.razor +++ b/MidrandBookshop/Components/BookCard.razor @@ -1,26 +1,64 @@ -
-
+
+
+ +
+ +
+ @if (IsNew) + { + New + } + else + { +
+ } + +
+ +
+ @if (!string.IsNullOrWhiteSpace(BookImageUrl)) + { + @Title + } + else + { +
+
+ @Category.ToUpper()
EDITION +
+
+ } +
-
- New -
-
- @Title +
+
+ @Title + by @Author +
+ R @Price.ToString("N0")
-
- @Title - $@Price -
@code { - [Parameter] public string Title { get; set; } = ""; + [Parameter] public long Id { get; set; } + [Parameter] public string Title { get; set; } = string.Empty; + [Parameter] public string Author { get; set; } = string.Empty; [Parameter] public decimal Price { get; set; } - [Parameter] public string BookImageUrl { get; set; } = ""; + [Parameter] public string Category { get; set; } = string.Empty; + [Parameter] public bool IsNew { get; set; } + [Parameter] public string BookImageUrl { get; set; } = string.Empty; + + [Parameter] public EventCallback OnCardClick { get; set; } } \ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/Home.razor b/MidrandBookshop/Components/Pages/Home.razor index 06a0c4c..5c4f1c7 100644 --- a/MidrandBookshop/Components/Pages/Home.razor +++ b/MidrandBookshop/Components/Pages/Home.razor @@ -1,6 +1,5 @@ @page "/" @rendermode InteractiveServer -@inject NavigationManager Navigation

@@ -12,123 +11,133 @@

-
+ @if (AuthorId.HasValue && !string.IsNullOrEmpty(ActiveAuthorFilterName)) + { +
+
+ Author Collection + Archived items from @ActiveAuthorFilterName +
+ +
+ } + +
- @foreach (var category in MainCategories) + @if (!AuthorId.HasValue) { - var catName = category; - + } + + @if (DynamicExtendedCategories.Count > 0) + { + + } + } + else + { + } - -
-
- - -
- -
-
- @if (ShowExpandedCategories) + @if (ShowExpandedCategories && DynamicExtendedCategories.Count > 0 && !AuthorId.HasValue) { -
- @foreach (var category in DynamicExtendedCategories) - { - var catName = category; - - } +
+

Curated Subgenre Tag Index

+
+ @foreach (var subCategory in DynamicExtendedCategories) + { + var subName = subCategory; + + } +
} @if (ShowFilterMenu) { -
-
+
+
-

SORT ORDER

-
- - - - -
+ +
-

PRICE RANGE

-
- - - - + +
+ + + +
-
-

RELEASE AVAILABILITY

-
- - +
+
+ +
-
+
+ + +
} -
-
-
-
-
- -
- @if (!PaginatedBooks.Any()) { -
-

NO PRODUCTS MATCH YOUR TARGET SELECTION SPECIFICATIONS

+
+ +

No archived books match your active criteria.

+ Try adjusting your category definitions or search phrase query.
} else if (CurrentViewMode == ViewMode.Grid) @@ -136,59 +145,36 @@
@foreach (var book in PaginatedBooks) { -
-
-
- @if (book.IsNew) - { - New - } - else - { -
- } - -
-
-
- @book.Category.ToUpper()
EDITION -
-
-
-
-
-

@book.Title

-

by @book.Author

-
- R @book.Price.ToString("N0") -
-
+ }
} - else + else if (CurrentViewMode == ViewMode.List) { -
+
@foreach (var book in PaginatedBooks) {
- @book.Title - by @book.Author - @book.Category.ToUpper() + @book.Name + by @(ActiveAuthorFilterName ?? "Multiple Authors") + @ProductPrimaryCategoryCache[book.Id].ToUpper()
- @if (book.IsNew) + @if (book.Enabled) { NEW } - R @book.Price.ToString("N0") + R @ProductPriceCache[book.Id].ToString("N0") @@ -211,139 +197,6 @@ - - - - - - -@code { - public enum ViewMode { Grid, List } - private ViewMode CurrentViewMode = ViewMode.Grid; - - [CascadingParameter] - public string SharedSearchQuery { get; set; } = string.Empty; - - private string ActiveCategory = "All"; - private bool ShowExpandedCategories = false; - private bool ShowFilterMenu = false; - - private string SelectedSortOption = "default"; - private string ActivePriceFilter = "all"; - private bool OnlyShowNew = false; - - private List MainCategories = new() { "All", "Graphic Design", "Product Design", "Architecture" }; - private List DynamicExtendedCategories = new(); - - private int ItemsPerPage = 12; - private int VisibleCount = 12; - - public class BookItem - { - public long Id { get; set; } // Refactored to hold unique record indices of type long - public string Title { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public decimal Price { get; set; } - public string Category { get; set; } = string.Empty; - public bool IsNew { get; set; } - public string Isbn { get; set; } = string.Empty; - } - - private List BooksCollection = new(); - - private IEnumerable FilteredData - { - get - { - var data = BooksCollection.AsEnumerable(); - - if (!string.IsNullOrWhiteSpace(SharedSearchQuery)) - { - var q = SharedSearchQuery.Trim(); - data = data.Where(b => - b.Title.Contains(q, StringComparison.OrdinalIgnoreCase) || - b.Author.Contains(q, StringComparison.OrdinalIgnoreCase) || - b.Isbn.Contains(q, StringComparison.OrdinalIgnoreCase) - ); - } - - if (ActiveCategory != "All") - { - data = data.Where(b => b.Category.Equals(ActiveCategory, StringComparison.OrdinalIgnoreCase)); - } - - if (OnlyShowNew) { data = data.Where(b => b.IsNew); } - - data = ActivePriceFilter switch - { - "under-500" => data.Where(b => b.Price < 500), - "500-1000" => data.Where(b => b.Price >= 500 && b.Price <= 1000), - "over-1000" => data.Where(b => b.Price > 1000), - _ => data - }; - - return data; - } - } - - private IEnumerable SortedAndFilteredBooks => SelectedSortOption switch - { - "price-low" => FilteredData.OrderBy(b => b.Price), - "price-high" => FilteredData.OrderByDescending(b => b.Price), - "title-asc" => FilteredData.OrderBy(b => b.Title), - _ => FilteredData - }; - - private IEnumerable PaginatedBooks => SortedAndFilteredBooks.Take(VisibleCount); - private int TotalFilteredCount => FilteredData.Count(); - private bool HasMoreItems => VisibleCount < TotalFilteredCount; - - protected override void OnInitialized() - { - var extraSourceCategories = new[] { "Fine Arts", "Science", "Photography", "Typography", "Interior Design", "Industrialism", "Fashion", "Curation Studies" }; - DynamicExtendedCategories.AddRange(extraSourceCategories); - - // Updated mock items to supply long IDs matching your screenshot items - BooksCollection.Add(new BookItem { Id = 1L, Title = "Letters from M/M (Paris)", Author = "M/M Paris", Price = 720, Category = "Graphic Design", IsNew = true, Isbn = "9782915173" }); - BooksCollection.Add(new BookItem { Id = 2L, Title = "Daan Paans: Floating Signifiers", Author = "Daan Paans", Price = 540, Category = "Product Design", IsNew = true, Isbn = "9789492051" }); - BooksCollection.Add(new BookItem { Id = 3L, Title = "Album Architectures, Maputo", Author = "Guedes Archive", Price = 350, Category = "Architecture", IsNew = true, Isbn = "9780620751" }); - - var designPrefixes = new[] { "Minimalist", "Monolithic", "Architectural", "Japanese", "Scandinavian" }; - var designNouns = new[] { "Structures", "Typologies", "Forms & Spaces", "Systems Matrix", "Graphic Ephemera" }; - var designers = new[] { "J. Morrison", "K. Fujita", "Studio Bouroullec", "Es Devlin", "Kenya Hara" }; - - var entireCategoryPool = MainCategories.Concat(DynamicExtendedCategories).Where(c => c != "All").ToArray(); - var random = new Random(42); - - for (int i = 4; i <= 60; i++) - { - BooksCollection.Add(new BookItem - { - Id = (long)i, - Title = $"{designPrefixes[random.Next(designPrefixes.Length)]} {designNouns[random.Next(designNouns.Length)]} (Vol. {random.Next(1, 4)})", - Author = designers[random.Next(designers.Length)], - Price = random.Next(25, 135) * 10, - Category = entireCategoryPool[random.Next(entireCategoryPool.Length)], - IsNew = random.NextDouble() > 0.7, - Isbn = $"978000000{i}" - }); - } - } - - // Handles the explicit page transition routing - 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 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 + onclick="window.scrollTo({top: 0, behavior: 'smooth'}); return false;"> + + \ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/Home.razor.cs b/MidrandBookshop/Components/Pages/Home.razor.cs new file mode 100644 index 0000000..5372ac5 --- /dev/null +++ b/MidrandBookshop/Components/Pages/Home.razor.cs @@ -0,0 +1,170 @@ +using LiteCharms.Features.MidrandBooks; +using LiteCharms.Features.MidrandBooks.AuthorBooks; +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Categories; +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Products.Models; +using LiteCharms.Features.Models; + +namespace MidrandBookshop.Components.Pages; + +public partial class Home : ComponentBase +{ + [Inject] private ProductService ProductService { get; set; } = default!; + [Inject] private BooksService BooksService { get; set; } = default!; + [Inject] private AuthorService AuthorService { get; set; } = default!; + [Inject] private CategoryService CategoryService { get; set; } = default!; + [Inject] private NavigationManager Navigation { get; set; } = default!; + + [CascadingParameter] public string SharedSearchQuery { get; set; } = string.Empty; + [SupplyParameterFromQuery] public long? AuthorId { get; set; } + + public enum ViewMode { Grid, List } + private ViewMode CurrentViewMode = ViewMode.Grid; + private string ActiveCategory = "All"; + private bool ShowExpandedCategories = false; + private bool ShowFilterMenu = false; + private string SelectedSortOption = "default"; + private string ActivePriceFilter = "all"; + private bool OnlyShowNew = false; + + private List MainCategories { get; set; } = ["All"]; + private List DynamicExtendedCategories { get; set; } = []; + + private int ItemsPerPage = 12; + private int VisibleCount = 12; + + private List ProductsCollection { get; set; } = []; + + protected string? ActiveAuthorFilterName { get; private set; } + + private Dictionary ProductPriceCache { get; set; } = []; + private Dictionary ProductPrimaryCategoryCache { get; set; } = []; + + private IEnumerable FilteredData + { + get + { + var data = ProductsCollection.AsEnumerable(); + + // Category filtering restricts rendering solely when checking the open catalog + 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 + if (!string.IsNullOrWhiteSpace(SharedSearchQuery)) + { + var q = SharedSearchQuery.Trim(); + + data = data.Where(p => (p.Name ?? "").Contains(q, StringComparison.OrdinalIgnoreCase)); + } + + return data; + } + } + + private IEnumerable PaginatedBooks => FilteredData.Take(VisibleCount); + + private bool HasMoreItems => FilteredData.Count() > VisibleCount; + + protected override async Task OnParametersSetAsync() => await LoadCatalogDataAsync(); + + private async Task LoadCatalogDataAsync() + { + ProductsCollection.Clear(); + ProductPriceCache.Clear(); + ProductPrimaryCategoryCache.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); + + if (authorResult.IsSuccess && authorResult.Value != null) + { + var author = authorResult.Value; + + ActiveAuthorFilterName = author.PublisherType == PublisherTypes.Company && !string.IsNullOrWhiteSpace(author.Company) + ? author.Company + : $"{author.Name} {author.LastName}".Trim(); + } + + var authorBooksResult = await BooksService.GetBooksByAuthorAsync(AuthorId.Value); + + if (authorBooksResult.IsSuccess && authorBooksResult.Value != null) + { + foreach (var authorBook in authorBooksResult.Value) + { + if (authorBook.Product != null) + { + var product = authorBook.Product; + + ProductsCollection.Add(product); + + ProductPriceCache[product.Id] = product.Price?.Amount ?? 0m; + + var categoryResult = await ProductService.GetProductCategoriesAsync(product.Id); + + ProductPrimaryCategoryCache[product.Id] = (categoryResult.IsSuccess && categoryResult.Value.Length > 0) + ? (categoryResult.Value[0].Name ?? "General") + : "General"; + } + } + } + return; + } + + // Pipeline B: Safe structural fallback mapping utilizing the exact backend DateRange object setup + var selectionRange = new DateRange + { + From = new DateOnly(2020, 1, 1), + To = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(1)), + MaxRecords = 100 + }; + + var allProductsResult = await ProductService.GetProductsAsync(0, selectionRange); + + if (allProductsResult.IsSuccess && allProductsResult.Value != null) + { + ProductsCollection = allProductsResult.Value.ToList(); + + foreach (var product in ProductsCollection) + { + var priceResult = await ProductService.GetProductPriceAsync(product.Id); + ProductPriceCache[product.Id] = priceResult.IsSuccess ? priceResult.Value.Amount : 0m; + + var categoryResult = await ProductService.GetProductCategoriesAsync(product.Id); + ProductPrimaryCategoryCache[product.Id] = (categoryResult.IsSuccess && categoryResult.Value.Length > 0) + ? (categoryResult.Value[0].Name ?? "General") + : "General"; + } + } + + var categoriesResult = await CategoryService.GetCategoriesAsync(); + + if (categoriesResult.IsSuccess && categoriesResult.Value != null) + { + var cleanNames = categoriesResult.Value + .Select(c => c.Name) + .Where(n => !string.IsNullOrEmpty(n)).Cast() + .ToList(); + + MainCategories = ["All", .. cleanNames.Take(3)]; + DynamicExtendedCategories = [.. cleanNames.Skip(3)]; + } + } + + private void ClearAuthorFilter() => Navigation.NavigateTo("/"); + 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 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/Product.razor b/MidrandBookshop/Components/Pages/Product.razor deleted file mode 100644 index aca17d0..0000000 --- a/MidrandBookshop/Components/Pages/Product.razor +++ /dev/null @@ -1,137 +0,0 @@ -@page "/product/{BookId:long}" -@inject NavigationManager Navigation - -
- - -
- - -
-
- @AuthorName -
- @for (int i = 1; i <= 5; i++) - { - - } - (@CurrentRating.ToString("F1")) -
-
- -

@BookTitle

-
R @Price.ToString("N2")
- -
-
- - @Quantity - -
- -
- -
- -
-

Description

-

@BookDescription

-
- -
-

About the Author

-

@AuthorBio

- -
-
-
-
- -@code { - [Parameter] public long BookId { get; set; } - - // Mock State - In production, pull these via a Service using BookId inside OnInitialized - private string BookTitle { get; set; } = "Letters from M/M (Paris)"; - private string AuthorName { get; set; } = "M/M Paris"; - private string AuthorBio { get; set; } = "M/M Paris is an art and design partnership consisting of Michaël Amzalag and Mathias Augustyniak, established in 1992. Renowned globally for their influence on fashion, music, and contemporary art layout structures."; - private string BookDescription { get; set; } = "An exquisite archive tracking visual graphics, typography, and structural design curation over three decades. Beautifully bound with matte-coated plates and custom layouts."; - private decimal Price { get; set; } = 720.00m; - - // Dynamic Ratings State - private double CurrentRating { get; set; } = 4.7; - - // Formats supported - private bool IsPhysicalBook { get; set; } = true; - private bool IsEBook { get; set; } = true; - private bool CanReadOnline { get; set; } = false; - - // Image Caching Gallery State - private string ActiveImageUrl { get; set; } = "images/book-cover-large.png"; - private List Thumbnails { get; set; } = new() - { - "images/book-cover-large.png", - "images/book-inside-1.png", - "images/book-inside-2.png" - }; - - private int Quantity { get; set; } = 1; - - protected override void OnInitialized() - { - // Default the gallery viewer context logic - if (Thumbnails.Any()) - { - ActiveImageUrl = Thumbnails.First(); - } - } - - private void IncreaseQty() => Quantity++; - private void DecreaseQty() { if (Quantity > 1) Quantity--; } - - private void HandleAddToCart() - { - // Event logic hooked into your structural state layout - } - - private void ViewAllAuthorBooks() - { - Navigation.NavigateTo($"/catalog?author={Uri.EscapeDataString(AuthorName)}"); - } -} \ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/ProductView.razor b/MidrandBookshop/Components/Pages/ProductView.razor new file mode 100644 index 0000000..e22aafe --- /dev/null +++ b/MidrandBookshop/Components/Pages/ProductView.razor @@ -0,0 +1,126 @@ +@page "/product/{BookId:long}" +@rendermode InteractiveServer + +
+ @if (IsLoading) + { +
+
+ Loading product details... +
+

Loading curated details...

+
+ } + else if (CurrentProduct == null) + { +
+ + + + + +

Artifact Not Found

+

The requested book could not be found in our current catalog.

+ +
+ } + else + { + + +
+ + +
+
+ @AuthorName +
+ @for (int i = 1; i <= 5; i++) + { + + } + (4.8) +
+
+ +

@CurrentProduct.Name

+
R @LivePrice.ToString("N2")
+ +
+
+ + @Quantity + +
+ +
+ +
+ + @if (!string.IsNullOrWhiteSpace(CurrentProduct.Description)) + { +
+

Description

+

@CurrentProduct.Description

+
+ } + +
+

About the Author

+

+ @(string.IsNullOrWhiteSpace(CurrentProduct.Metadata?.Manufacturer) + ? "Details regarding this publisher/author are maintained inside our curated archives." + : $"{CurrentProduct.Metadata.Manufacturer} is globally recognized for their direct contribution to this specific genre spectrum.") +

+ +
+
+
+ } +
\ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/ProductView.razor.cs b/MidrandBookshop/Components/Pages/ProductView.razor.cs new file mode 100644 index 0000000..99e591e --- /dev/null +++ b/MidrandBookshop/Components/Pages/ProductView.razor.cs @@ -0,0 +1,104 @@ +using LiteCharms.Features.MidrandBooks; +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Authors.Models; +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Products.Models; + +namespace MidrandBookshop.Components.Pages; + +public partial class ProductView : ComponentBase +{ + [Parameter] public long BookId { get; set; } + + [Inject] private ProductService ProductService { get; set; } = default!; + [Inject] private AuthorService AuthorService { get; set; } = default!; + [Inject] private NavigationManager Navigation { get; set; } = default!; + + protected bool IsLoading { get; private set; } = true; + protected Product? CurrentProduct { 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"; + + protected string ActiveImageUrl { get; set; } = string.Empty; + protected List Thumbnails { get; private set; } = []; + protected int Quantity { get; private set; } = 1; + + protected Author? CurrentAuthor { get; private set; } + + protected override async Task OnParametersSetAsync() + { + try + { + IsLoading = true; + CurrentProduct = null; + CurrentAuthor = null; + Thumbnails.Clear(); + + // 1. Resolve product listing details + var productResult = await ProductService.GetProductAsync(BookId); + + if (productResult.IsSuccess && productResult.Value != null) + { + CurrentProduct = productResult.Value; + AuthorName = CurrentProduct.Metadata?.Manufacturer ?? "Unknown Author"; + + // 2. Load pricing data + var priceResult = await ProductService.GetProductPriceAsync(BookId); + LivePrice = priceResult.IsSuccess ? priceResult.Value.Amount : 0m; + + // 3. Extract active catalog categories + var categoryResult = await ProductService.GetProductCategoriesAsync(BookId); + if (categoryResult.IsSuccess && categoryResult.Value.Length > 0) + { + PrimaryCategory = categoryResult.Value[0].Name ?? "General"; + } + + // 4. Retrieve complete contextual model through the newly instantiated AuthorService lookup + var authorResult = await AuthorService.GetAuthorByProductIdAsync(BookId); + if (authorResult.IsSuccess && authorResult.Value != null) + { + CurrentAuthor = authorResult.Value; + + // Format fully qualified author text cleanly depending on their publisher model details + if (CurrentAuthor.PublisherType == PublisherTypes.Company && !string.IsNullOrWhiteSpace(CurrentAuthor.Company)) + AuthorName = CurrentAuthor.Company; + else + AuthorName = $"{CurrentAuthor.Name} {CurrentAuthor.LastName}".Trim(); + } + + // 5. Build presentation image viewer variables + if (!string.IsNullOrWhiteSpace(CurrentProduct.ImageUrl)) + Thumbnails.Add(CurrentProduct.ImageUrl); + + if (CurrentProduct.ThumbnailUrls != null && CurrentProduct.ThumbnailUrls?.Length != 0) + foreach (var url in CurrentProduct.ThumbnailUrls!) + if (!string.IsNullOrWhiteSpace(url) && !Thumbnails.Contains(url)) Thumbnails.Add(url); + + ActiveImageUrl = Thumbnails.FirstOrDefault() ?? string.Empty; + } + } + catch (Exception) + { + CurrentProduct = null; + } + finally + { + IsLoading = false; + } + } + + protected void IncreaseQty() => Quantity++; + protected void DecreaseQty() { if (Quantity > 1) Quantity--; } + + protected void HandleAddToCart() + { + if (CurrentProduct == null) return; + } + + protected void ViewAllAuthorBooks() + { + if (CurrentAuthor is not null) + Navigation.NavigateTo($"/?authorId={CurrentAuthor.Id}"); + } +} \ No newline at end of file diff --git a/MidrandBookshop/Components/Pages/Product.razor.css b/MidrandBookshop/Components/Pages/ProductView.razor.css similarity index 62% rename from MidrandBookshop/Components/Pages/Product.razor.css rename to MidrandBookshop/Components/Pages/ProductView.razor.css index 1e64af7..898af69 100644 --- a/MidrandBookshop/Components/Pages/Product.razor.css +++ b/MidrandBookshop/Components/Pages/ProductView.razor.css @@ -1,20 +1,36 @@ -.product-container { +/* ========================================================================== + Structural Layout Containers + ========================================================================== */ + +.product-container { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem; font-family: system-ui, -apple-system, sans-serif; } -/* Breadcrumbs */ +.product-layout { + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: 4rem; +} + +/* ========================================================================== + Breadcrumb Navigation Links + ========================================================================== */ + .breadcrumb { font-size: 0.85rem; margin-bottom: 2.5rem; color: #8c8c8c; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; } .crumb-link { cursor: pointer; - transition: color 0.2s; + transition: color 0.2s ease; } .crumb-link:hover { @@ -30,52 +46,46 @@ font-weight: 500; } -/* Two-Column Grid Layout Structure */ -.product-layout { - display: grid; - grid-template-columns: 1.1fr 0.9fr; - gap: 4rem; -} +/* ========================================================================== + Left Section: Media Gallery Components + ========================================================================== */ -@media (max-width: 992px) { - .product-layout { - grid-template-columns: 1fr; - gap: 2.5rem; - } -} - -/* Left Section: Visual Images/Thumbnails Layout */ .gallery-section { display: flex; flex-direction: column; gap: 1rem; + width: 100%; } .main-image-wrapper { position: relative; background-color: #f9f9f9; border-radius: 8px; - padding: 3rem; + padding: 3rem 3rem 4.5rem 3rem; display: flex; justify-content: center; align-items: center; min-height: 480px; + width: 100%; } .main-image { max-height: 450px; + max-width: 100%; object-fit: contain; mix-blend-mode: multiply; } -/* Format Badges Overlay Styles */ +/* Dynamic Overlaid Attribute Badges */ .format-badges { position: absolute; - top: 1rem; - left: 1rem; + bottom: 1.5rem; + left: 50%; + transform: translateX(-50%); display: flex; flex-wrap: wrap; gap: 0.5rem; + white-space: nowrap; } .badge { @@ -105,9 +115,10 @@ border-color: #bbf7d0; } -/* Thumbnails container */ +/* Interactive Gallery Thumbnails Grid */ .thumbnail-grid { display: flex; + flex-wrap: wrap; gap: 1rem; } @@ -122,7 +133,8 @@ transition: all 0.2s ease; } - .thumbnail-wrapper:hover, .thumbnail-wrapper.active { + .thumbnail-wrapper:hover, + .thumbnail-wrapper.active { border-color: #111; } @@ -133,7 +145,10 @@ mix-blend-mode: multiply; } -/* Right Section: Core Text Typography Controls */ +/* ========================================================================== + Right Section: Product Details & Typography Controls + ========================================================================== */ + .details-section { display: flex; flex-direction: column; @@ -144,6 +159,8 @@ justify-content: space-between; align-items: center; margin-bottom: 0.75rem; + flex-wrap: wrap; + gap: 0.5rem; } .author-name { @@ -151,7 +168,7 @@ color: #666; } -/* Dynamic Stars C# rendering mapping */ +/* Review Star Components */ .rating-stars { display: flex; align-items: center; @@ -164,7 +181,7 @@ } .star.filled { - color: #111; /* Solid black stars fits your clean aesthetic perfectly */ + color: #ffc107; } .rating-text { @@ -175,7 +192,7 @@ .product-title { font-size: 2.5rem; - font-family: 'Playfair Display', serif, Georgia; /* Fits the luxury typography tone */ + font-family: 'Playfair Display', serif, Georgia; font-weight: 400; line-height: 1.2; margin-bottom: 1rem; @@ -189,7 +206,10 @@ color: #111; } -/* Standard E-commerce Action Bar Block */ +/* ========================================================================== + Interactive E-Commerce Selection Bars + ========================================================================== */ + .purchase-actions { display: flex; gap: 1rem; @@ -215,7 +235,7 @@ align-items: center; justify-content: center; border-radius: 50%; - transition: background-color 0.2s; + transition: background-color 0.2s ease; } .qty-btn:hover { @@ -237,7 +257,8 @@ font-weight: 500; letter-spacing: 0.02em; cursor: pointer; - transition: background-color 0.2s; + transition: background-color 0.2s ease; + padding: 0.5rem 1.5rem; } .btn-add-to-cart:hover { @@ -250,7 +271,10 @@ margin-bottom: 2rem; } -/* General Layout Text and Cross-Selling */ +/* ========================================================================== + Informational Text Elements & Links + ========================================================================== */ + .info-block { margin-bottom: 2rem; } @@ -263,7 +287,8 @@ margin-bottom: 0.75rem; } -.description-text, .author-bio { +.description-text, +.author-bio { font-size: 0.95rem; line-height: 1.6; color: #444; @@ -292,3 +317,44 @@ .btn-text-link:hover { color: #444; } + +/* ========================================================================== + Responsive Adaptations & Breakpoints + ========================================================================== */ + +@media (max-width: 992px) { + .product-layout { + grid-template-columns: 1fr; + gap: 2.5rem; + } + + .main-image-wrapper { + min-height: 380px; + } +} + +@media (max-width: 576px) { + .product-container { + padding: 1rem 1rem; + } + + .product-title { + font-size: 1.85rem; + } + + .purchase-actions { + flex-direction: column; + gap: 0.75rem; + } + + .quantity-picker { + justify-content: space-between; + width: 100%; + padding: 0.5rem; + } + + .btn-add-to-cart { + width: 100%; + padding: 0.85rem; + } +} diff --git a/MidrandBookshop/MidrandBookshop.csproj b/MidrandBookshop/MidrandBookshop.csproj index 23c37e0..fb22307 100644 --- a/MidrandBookshop/MidrandBookshop.csproj +++ b/MidrandBookshop/MidrandBookshop.csproj @@ -18,13 +18,13 @@ - + - + @@ -51,6 +51,7 @@ +