Dynamic Sidebar implemented, with appropriate icons and separately populated category pages

This commit is contained in:
Kira 2022-09-16 11:30:18 -07:00
parent 1ce7b31d8e
commit 6023073b3a
21 changed files with 806 additions and 20 deletions

View File

@ -74,5 +74,42 @@ namespace ShopOnline.Api.Controllers
"Error retrieving data from the database.");
}
}
[HttpGet]
[Route(nameof(GetProductCategories))]
public async Task<ActionResult<IEnumerable<ProductCategoryDto>>> GetProductCategories()
{
try
{
var productCategories = await productRepository.GetCategories();
var productCategoryDtos = productCategories.ConvertToDto();
return Ok(productCategoryDtos);
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error retrieving data from the database.");
}
}
[HttpGet]
[Route("{categoryId}/GetItemsByCategory")]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetItemsByCategory(int categoryId)
{
try
{
var products = await productRepository.GetItemsByCategory(categoryId);
var productCategories = await productRepository.GetCategories();
var productDtos = products.ConvertToDto(productCategories);
return Ok(productDtos);
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError,
"Error retrieving data from the database.");
}
}
}
}

View File

@ -292,22 +292,27 @@ namespace ShopOnline.Api.Data
modelBuilder.Entity<ProductCategory>().HasData(new ProductCategory
{
Id = 1,
Name = "Beauty"
Name = "Beauty",
IconCSS = "fas fa-spa"
});
modelBuilder.Entity<ProductCategory>().HasData(new ProductCategory
{
Id = 2,
Name = "Furniture"
Name = "Furniture",
IconCSS = "fas fa-couch"
});
modelBuilder.Entity<ProductCategory>().HasData(new ProductCategory
{
Id = 3,
Name = "Electronics"
Name = "Electronics",
IconCSS = "fas fa-headphones"
});
modelBuilder.Entity<ProductCategory>().HasData(new ProductCategory
{
Id = 4,
Name = "Shoes"
Name = "Shoes",
IconCSS = "fas fa-shoe-prints"
});
}

View File

@ -4,5 +4,6 @@
{
public int Id { get; set; }
public string Name { get; set; }
public string IconCSS { get; set; }
}
}

View File

@ -5,6 +5,17 @@ namespace ShopOnline.Api.Extensions
{
public static class DtoConversions
{
public static IEnumerable<ProductCategoryDto> ConvertToDto(this IEnumerable<ProductCategory> productCategories)
{
return (from productCategory in productCategories
select new ProductCategoryDto
{
Id = productCategory.Id,
Name = productCategory.Name,
IconCSS = productCategory.IconCSS
}).ToList();
}
public static IEnumerable<ProductDto> ConvertToDto(this IEnumerable<Product> products,
IEnumerable<ProductCategory> productCategories)
{

View File

@ -0,0 +1,420 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using ShopOnline.Api.Data;
#nullable disable
namespace ShopOnline.Api.Migrations
{
[DbContext(typeof(ShopOnlineDbContext))]
[Migration("20220916171516_AddProductCategoryIcons")]
partial class AddProductCategoryIcons
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
modelBuilder.Entity("ShopOnline.Api.Entities.Cart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Carts");
b.HasData(
new
{
Id = 1,
UserId = 1
},
new
{
Id = 2,
UserId = 2
});
});
modelBuilder.Entity("ShopOnline.Api.Entities.CartItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<int>("CartId")
.HasColumnType("int");
b.Property<int>("ProductId")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("CartItems");
});
modelBuilder.Entity("ShopOnline.Api.Entities.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<int>("CategoryId")
.HasColumnType("int");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageURL")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<decimal>("Price")
.HasColumnType("decimal(18,2)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Products");
b.HasData(
new
{
Id = 1,
CategoryId = 1,
Description = "A kit provided by Glossier, containing skin care, hair care and makeup products",
ImageURL = "/Images/Beauty/Beauty1.png",
Name = "Glossier - Beauty Kit",
Price = 100m,
Quantity = 100
},
new
{
Id = 2,
CategoryId = 1,
Description = "A kit provided by Curology, containing skin care products",
ImageURL = "/Images/Beauty/Beauty2.png",
Name = "Curology - Skin Care Kit",
Price = 50m,
Quantity = 45
},
new
{
Id = 3,
CategoryId = 1,
Description = "A kit provided by Curology, containing skin care products",
ImageURL = "/Images/Beauty/Beauty3.png",
Name = "Cocooil - Organic Coconut Oil",
Price = 20m,
Quantity = 30
},
new
{
Id = 4,
CategoryId = 1,
Description = "A kit provided by Schwarzkopf, containing skin care and hair care products",
ImageURL = "/Images/Beauty/Beauty4.png",
Name = "Schwarzkopf - Hair Care and Skin Care Kit",
Price = 50m,
Quantity = 60
},
new
{
Id = 5,
CategoryId = 1,
Description = "Skin Care Kit, containing skin care and hair care products",
ImageURL = "/Images/Beauty/Beauty5.png",
Name = "Skin Care Kit",
Price = 30m,
Quantity = 85
},
new
{
Id = 6,
CategoryId = 3,
Description = "Air Pods - in-ear wireless headphones",
ImageURL = "/Images/Electronic/Electronics1.png",
Name = "Air Pods",
Price = 100m,
Quantity = 120
},
new
{
Id = 7,
CategoryId = 3,
Description = "On-ear Golden Headphones - these headphones are not wireless",
ImageURL = "/Images/Electronic/Electronics2.png",
Name = "On-ear Golden Headphones",
Price = 40m,
Quantity = 200
},
new
{
Id = 8,
CategoryId = 3,
Description = "On-ear Black Headphones - these headphones are not wireless",
ImageURL = "/Images/Electronic/Electronics3.png",
Name = "On-ear Black Headphones",
Price = 40m,
Quantity = 300
},
new
{
Id = 9,
CategoryId = 3,
Description = "Sennheiser Digital Camera - High quality digital camera provided by Sennheiser - includes tripod",
ImageURL = "/Images/Electronic/Electronic4.png",
Name = "Sennheiser Digital Camera with Tripod",
Price = 600m,
Quantity = 20
},
new
{
Id = 10,
CategoryId = 3,
Description = "Canon Digital Camera - High quality digital camera provided by Canon",
ImageURL = "/Images/Electronic/Electronic5.png",
Name = "Canon Digital Camera",
Price = 500m,
Quantity = 15
},
new
{
Id = 11,
CategoryId = 3,
Description = "Gameboy - Provided by Nintendo",
ImageURL = "/Images/Electronic/technology6.png",
Name = "Nintendo Gameboy",
Price = 100m,
Quantity = 60
},
new
{
Id = 12,
CategoryId = 2,
Description = "Very comfortable black leather office chair",
ImageURL = "/Images/Furniture/Furniture1.png",
Name = "Black Leather Office Chair",
Price = 50m,
Quantity = 212
},
new
{
Id = 13,
CategoryId = 2,
Description = "Very comfortable pink leather office chair",
ImageURL = "/Images/Furniture/Furniture2.png",
Name = "Pink Leather Office Chair",
Price = 50m,
Quantity = 112
},
new
{
Id = 14,
CategoryId = 2,
Description = "Very comfortable lounge chair",
ImageURL = "/Images/Furniture/Furniture3.png",
Name = "Lounge Chair",
Price = 70m,
Quantity = 90
},
new
{
Id = 15,
CategoryId = 2,
Description = "Very comfortable Silver lounge chair",
ImageURL = "/Images/Furniture/Furniture4.png",
Name = "Silver Lounge Chair",
Price = 120m,
Quantity = 95
},
new
{
Id = 16,
CategoryId = 2,
Description = "White and blue Porcelain Table Lamp",
ImageURL = "/Images/Furniture/Furniture6.png",
Name = "Porcelain Table Lamp",
Price = 15m,
Quantity = 100
},
new
{
Id = 17,
CategoryId = 2,
Description = "Office Table Lamp",
ImageURL = "/Images/Furniture/Furniture7.png",
Name = "Office Table Lamp",
Price = 20m,
Quantity = 73
},
new
{
Id = 18,
CategoryId = 4,
Description = "Comfortable Puma Sneakers in most sizes",
ImageURL = "/Images/Shoes/Shoes1.png",
Name = "Puma Sneakers",
Price = 100m,
Quantity = 50
},
new
{
Id = 19,
CategoryId = 4,
Description = "Colorful trainsers - available in most sizes",
ImageURL = "/Images/Shoes/Shoes2.png",
Name = "Colorful Trainers",
Price = 150m,
Quantity = 60
},
new
{
Id = 20,
CategoryId = 4,
Description = "Blue Nike Trainers - available in most sizes",
ImageURL = "/Images/Shoes/Shoes3.png",
Name = "Blue Nike Trainers",
Price = 200m,
Quantity = 70
},
new
{
Id = 21,
CategoryId = 4,
Description = "Colorful Hummel Trainers - available in most sizes",
ImageURL = "/Images/Shoes/Shoes4.png",
Name = "Colorful Hummel Trainers",
Price = 120m,
Quantity = 120
},
new
{
Id = 22,
CategoryId = 4,
Description = "Red Nike Trainers - available in most sizes",
ImageURL = "/Images/Shoes/Shoes5.png",
Name = "Red Nike Trainers",
Price = 200m,
Quantity = 100
},
new
{
Id = 23,
CategoryId = 4,
Description = "Birkenstock Sandles - available in most sizes",
ImageURL = "/Images/Shoes/Shoes6.png",
Name = "Birkenstock Sandles",
Price = 50m,
Quantity = 150
});
});
modelBuilder.Entity("ShopOnline.Api.Entities.ProductCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<string>("IconCSS")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("ProductCategories");
b.HasData(
new
{
Id = 1,
IconCSS = "fas fa-spa",
Name = "Beauty"
},
new
{
Id = 2,
IconCSS = "fas fa-couch",
Name = "Furniture"
},
new
{
Id = 3,
IconCSS = "fas fa-headphones",
Name = "Electronics"
},
new
{
Id = 4,
IconCSS = "fas fa-shoe-prints",
Name = "Shoes"
});
});
modelBuilder.Entity("ShopOnline.Api.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<string>("UserName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
b.HasData(
new
{
Id = 1,
UserName = "Bob"
},
new
{
Id = 2,
UserName = "Sarah"
});
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ShopOnline.Api.Migrations
{
public partial class AddProductCategoryIcons : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "IconCSS",
table: "ProductCategories",
type: "nvarchar(max)",
nullable: false,
defaultValue: "");
migrationBuilder.UpdateData(
table: "ProductCategories",
keyColumn: "Id",
keyValue: 1,
column: "IconCSS",
value: "fas fa-spa");
migrationBuilder.UpdateData(
table: "ProductCategories",
keyColumn: "Id",
keyValue: 2,
column: "IconCSS",
value: "fas fa-couch");
migrationBuilder.UpdateData(
table: "ProductCategories",
keyColumn: "Id",
keyValue: 3,
column: "IconCSS",
value: "fas fa-headphones");
migrationBuilder.UpdateData(
table: "ProductCategories",
keyColumn: "Id",
keyValue: 4,
column: "IconCSS",
value: "fas fa-shoe-prints");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IconCSS",
table: "ProductCategories");
}
}
}

View File

@ -345,6 +345,10 @@ namespace ShopOnline.Api.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<string>("IconCSS")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
@ -357,21 +361,25 @@ namespace ShopOnline.Api.Migrations
new
{
Id = 1,
IconCSS = "fas fa-spa",
Name = "Beauty"
},
new
{
Id = 2,
IconCSS = "fas fa-couch",
Name = "Furniture"
},
new
{
Id = 3,
IconCSS = "fas fa-headphones",
Name = "Electronics"
},
new
{
Id = 4,
IconCSS = "fas fa-shoe-prints",
Name = "Shoes"
});
});

View File

@ -8,6 +8,8 @@ namespace ShopOnline.Api.Repositories.Contracts
Task<Product> GetItem(int id);
Task<IEnumerable<ProductCategory>> GetCategories();
Task<ProductCategory> GetCategory(int id);
Task<IEnumerable<Product>> GetItemsByCategory(int id);
}
}

View File

@ -36,5 +36,14 @@ namespace ShopOnline.Api.Repositories
return products;
}
public async Task<IEnumerable<Product>> GetItemsByCategory(int id)
{
var products = await (from product in shopOnlineDbContext.Products
where product.CategoryId == id
select product).ToListAsync();
return products;
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ShopOnline.Models.Dtos
{
public class ProductCategoryDto
{
public int Id { get; set; }
public string Name { get; set; }
public string IconCSS { get; set; }
}
}

View File

@ -1 +1 @@
<div class="lds-spin"><i class="custom fa-regular fa-heart fa-10x" style="background: transparent;"></i></div>
<div class="lds-spin"><i class="custom fa-regular fa-heart fa-9x" style="background: transparent;"></i></div>

View File

@ -22,7 +22,7 @@ else
@foreach (var prodGroup in GetGroupedProductsByCategory())
{
<div class="row mt-3">
<h4>@prodGroup.FirstOrDefault(pg=>pg.CategoryId == prodGroup.Key).CategoryName</h4>
<h3>@prodGroup.FirstOrDefault(pg=>pg.CategoryId == prodGroup.Key).CategoryName</h3>
<DisplayProducts Products="@prodGroup.Take(4)"></DisplayProducts>
</div>
<hr class="mb-3" />

View File

@ -0,0 +1,24 @@
@page "/ProductsByCategory/{CategoryId:int}"
@inherits ProductsByCategoryBase
@if (Products == null && ErrorMessage == null)
{
<div class="row d-flex justify-content-center">
<div class="col-md-1">
<DisplayCustomSpinner />
</div>
</div>
}
else if (ErrorMessage != null)
{
<DisplayError ErrorMessage="@ErrorMessage" />
}
else {
<h2>@CategoryName</h2>
@if(Products.Count() > 0)
{
<div class="row mt-3">
<DisplayProducts Products="@Products"></DisplayProducts>
</div>
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Components;
using ShopOnline.Models.Dtos;
using ShopOnline.Web.Services.Contracts;
namespace ShopOnline.Web.Pages
{
public class ProductsByCategoryBase:ComponentBase
{
[Parameter]
public int CategoryId { get; set; }
[Inject]
public IProductService ProductService { get; set; }
public IEnumerable<ProductDto> Products { get; set; }
public string CategoryName { get; set; }
public string ErrorMessage { get; set; }
protected override async Task OnParametersSetAsync()
{
try
{
Products = await ProductService.GetItemsByCategory(CategoryId);
if(Products != null && Products.Count() > 0)
{
var productDto = Products.FirstOrDefault(p => p.CategoryId == CategoryId);
if(productDto != null)
{
CategoryName = productDto.CategoryName;
}
}
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
}
}
}

View File

@ -6,6 +6,7 @@ namespace ShopOnline.Web.Services.Contracts
{
Task<IEnumerable<ProductDto>> GetItems();
Task<ProductDto> GetItem(int id);
Task<IEnumerable<ProductCategoryDto>> GetProductCategories();
Task<IEnumerable<ProductDto>> GetItemsByCategory(int categoryId);
}
}

View File

@ -65,5 +65,59 @@ namespace ShopOnline.Web.Services
throw;
}
}
public async Task<IEnumerable<ProductDto>> GetItemsByCategory(int categoryId)
{
try
{
var response = await httpClient.GetAsync($"api/Product/{categoryId}/GetItemsByCategory");
if (response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
{
return Enumerable.Empty<ProductDto>();
}
return await response.Content.ReadFromJsonAsync<IEnumerable<ProductDto>>();
}
else
{
var message = await response.Content.ReadAsStringAsync();
throw new Exception($"Http Status Code - {response.StatusCode} Message - {message}");
}
}
catch (Exception)
{
throw;
}
}
public async Task<IEnumerable<ProductCategoryDto>> GetProductCategories()
{
try
{
var response = await httpClient.GetAsync("api/Product/GetProductCategories");
if (response.IsSuccessStatusCode)
{
if(response.StatusCode == System.Net.HttpStatusCode.NoContent)
{
return Enumerable.Empty<ProductCategoryDto>();
}
return await response.Content.ReadFromJsonAsync<IEnumerable<ProductCategoryDto>>();
}
else
{
var message = await response.Content.ReadAsStringAsync();
throw new Exception($"Http Status Code - {response.StatusCode} Message - {message}");
}
}
catch (Exception)
{
throw;
}
}
}
}

View File

@ -3,7 +3,7 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">ShopOnline.Web</a>
<a class="navbar-brand" href=""><span class="fas fa-shopping-cart" aria-hidden="true"></span>&nbsp;ShopOnline</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
@ -14,22 +14,15 @@
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
<span class="fas fa-home" aria-hidden="true"></span>&nbsp;Home
</NavLink>
</div>
<ProductCategoriesNavMenu />
<div class="nav-item px-3 d-sm-none">
<NavLink class="nav-link" href="ShoppingCart">
<span class="fas fa-shopping-cart" aria-hidden="true"></span> Shopping Cart (<b>@shoppingCartItemsCount</b>)
<span class="fas fa-shopping-cart" aria-hidden="true"></span>&nbsp;Shopping Cart (<b>@shoppingCartItemsCount</b>)
</NavLink>
</div>
</nav>

View File

@ -0,0 +1,21 @@
@inherits ProductCategoriesNavMenuBase
@if(ProductCategoryDtos == null && ErrorMessage == null)
{
<DisplaySpinner />
}
else if(ErrorMessage != null)
{
<DisplayError ErrorMessage="@ErrorMessage" />
}
else {
@foreach(var productCategory in ProductCategoryDtos)
{
var link = "/ProductsByCategory/" + productCategory.Id;
<div class="nav-item px-3">
<NavLink class="nav-link" href="@link">
<span class="@productCategory.IconCSS"></span>&nbsp;@productCategory.Name
</NavLink>
</div>
}
}

View File

@ -0,0 +1,62 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.oi {
width: 2rem;
font-size: 1.1rem;
vertical-align: text-top;
top: -2px;
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.25);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Components;
using ShopOnline.Models.Dtos;
using ShopOnline.Web.Services.Contracts;
namespace ShopOnline.Web.Shared
{
public class ProductCategoriesNavMenuBase:ComponentBase
{
[Inject]
public IProductService ProductService { get; set; }
public IEnumerable<ProductCategoryDto> ProductCategoryDtos { get; set; }
public string ErrorMessage { get; set; }
protected override async Task OnInitializedAsync()
{
try
{
ProductCategoryDtos = await ProductService.GetProductCategories();
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
}
}
}

View File

@ -10,3 +10,4 @@
@using ShopOnline.Web.Shared
@using ShopOnline.Models.Dtos
@using ShopOnline.Web.Services.Contracts
@using ShopOnline.Web.Pages