├── BlazorEcommerce ├── Client │ ├── Pages │ │ ├── Admin │ │ │ ├── Products.razor.css │ │ │ ├── ProductTypes.razor.css │ │ │ ├── Categories.razor.css │ │ │ ├── EditProduct.razor.css │ │ │ ├── ProductTypes.razor │ │ │ ├── Products.razor │ │ │ └── Categories.razor │ │ ├── Counter.razor │ │ ├── OrderSuccess.razor │ │ ├── OrderDetails.razor.css │ │ ├── Orders.razor.css │ │ ├── ProductDetails.razor.css │ │ ├── Cart.razor.css │ │ ├── Index.razor │ │ ├── Orders.razor │ │ ├── FetchData.razor │ │ ├── OrderDetails.razor │ │ ├── Profile.razor │ │ ├── Register.razor │ │ ├── Login.razor │ │ ├── Cart.razor │ │ └── ProductDetails.razor │ ├── wwwroot │ │ ├── favicon.ico │ │ ├── icon-192.png │ │ ├── css │ │ │ ├── open-iconic │ │ │ │ ├── font │ │ │ │ │ └── fonts │ │ │ │ │ │ ├── open-iconic.eot │ │ │ │ │ │ ├── open-iconic.otf │ │ │ │ │ │ ├── open-iconic.ttf │ │ │ │ │ │ └── open-iconic.woff │ │ │ │ ├── ICON-LICENSE │ │ │ │ ├── README.md │ │ │ │ └── FONT-LICENSE │ │ │ └── app.css │ │ └── index.html │ ├── Shared │ │ ├── HomeButton.razor.css │ │ ├── AdminMenu.razor.css │ │ ├── UserButton.razor.css │ │ ├── HomeButton.razor │ │ ├── MainLayout.razor │ │ ├── ShopLayout.razor │ │ ├── SurveyPrompt.razor │ │ ├── ProductList.razor.css │ │ ├── CartCounter.razor │ │ ├── FeaturedProducts.razor.css │ │ ├── AdminMenu.razor │ │ ├── NavMenu.razor.css │ │ ├── ShopNavMenu.razor.css │ │ ├── NavMenu.razor │ │ ├── ShopNavMenu.razor │ │ ├── ShopLayout.razor.css │ │ ├── Search.razor │ │ ├── FeaturedProducts.razor │ │ ├── MainLayout.razor.css │ │ ├── UserButton.razor │ │ ├── ProductList.razor │ │ └── AddressForm.razor │ ├── Services │ │ ├── AddressService │ │ │ ├── IAddressService.cs │ │ │ └── AddressService.cs │ │ ├── OrderService │ │ │ ├── IOrderService.cs │ │ │ └── OrderService.cs │ │ ├── AuthService │ │ │ ├── IAuthService.cs │ │ │ └── AuthService.cs │ │ ├── ProductTypeService │ │ │ ├── IProductTypeService.cs │ │ │ └── ProductTypeService.cs │ │ ├── CartService │ │ │ ├── ICartService.cs │ │ │ └── CartService.cs │ │ ├── CategoryService │ │ │ ├── ICategoryService.cs │ │ │ └── CategoryService.cs │ │ └── ProductService │ │ │ ├── IProductService.cs │ │ │ └── ProductService.cs │ ├── App.razor │ ├── BlazorEcommerce.Client.csproj │ ├── _Imports.razor │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ └── CustomAuthStateProvider.cs ├── Server │ ├── appsettings.Development.json │ ├── Services │ │ ├── AddressService │ │ │ ├── IAddressService.cs │ │ │ └── AddressService.cs │ │ ├── PaymentService │ │ │ ├── IPaymentService.cs │ │ │ └── PaymentService.cs │ │ ├── OrderService │ │ │ ├── IOrderService.cs │ │ │ └── OrderService.cs │ │ ├── ProductTypeService │ │ │ ├── IProductTypeService.cs │ │ │ └── ProductTypeService.cs │ │ ├── CategoryService │ │ │ ├── ICategoryService.cs │ │ │ └── CategoryService.cs │ │ ├── AuthService │ │ │ ├── IAuthService.cs │ │ │ └── AuthService.cs │ │ ├── CartService │ │ │ ├── ICartService.cs │ │ │ └── CartService.cs │ │ └── ProductService │ │ │ └── IProductService.cs │ ├── appsettings.json │ ├── Migrations │ │ ├── 20211205210009_UserRole.cs │ │ ├── 20211124204804_CartItems.cs │ │ ├── 20211112205817_CreateInitial.cs │ │ ├── 20211117210144_Users.cs │ │ ├── 20211116221155_FeaturedProducts.cs │ │ ├── 20220429135440_Images.cs │ │ ├── 20211205212215_CategoryFlags.cs │ │ ├── 20211112205817_CreateInitial.Designer.cs │ │ ├── 20211204205151_UserAddress.cs │ │ ├── 20211129201337_Orders.cs │ │ ├── 20211112210614_ProductSeeding.cs │ │ ├── 20211114114952_Categories.cs │ │ ├── 20211112210614_ProductSeeding.Designer.cs │ │ └── 20211114115758_SeedMoreProducts.cs │ ├── Pages │ │ ├── Error.cshtml.cs │ │ └── Error.cshtml │ ├── Controllers │ │ ├── AddressController.cs │ │ ├── OrderController.cs │ │ ├── WeatherForecastController.cs │ │ ├── PaymentController.cs │ │ ├── ProductTypeController.cs │ │ ├── CategoryController.cs │ │ ├── AuthController.cs │ │ ├── CartController.cs │ │ └── ProductController.cs │ ├── Properties │ │ └── launchSettings.json │ ├── BlazorEcommerce.Server.csproj │ └── Program.cs └── Shared │ ├── Image.cs │ ├── BlazorEcommerce.Shared.csproj │ ├── WeatherForecast.cs │ ├── ServiceResponse.cs │ ├── ProductSearchResult.cs │ ├── CartItem.cs │ ├── OrderDetailsResponse.cs │ ├── UserLogin.cs │ ├── OrderOverviewResponse.cs │ ├── ProductType.cs │ ├── OrderDetailsProductResponse.cs │ ├── UserChangePassword.cs │ ├── Order.cs │ ├── User.cs │ ├── CartProductResponse.cs │ ├── UserRegister.cs │ ├── Category.cs │ ├── OrderItem.cs │ ├── Address.cs │ ├── ProductVariant.cs │ └── Product.cs ├── BlazorEcommerce.sln └── .gitattributes /BlazorEcommerce/Client/Pages/Admin/Products.razor.css: -------------------------------------------------------------------------------- 1 | img { 2 | max-height: 100px; 3 | max-width: 100px; 4 | } 5 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickgod/BlazorEcommerce/HEAD/BlazorEcommerce/Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickgod/BlazorEcommerce/HEAD/BlazorEcommerce/Client/wwwroot/icon-192.png -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Admin/ProductTypes.razor.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | padding: 6px; 4 | } 5 | 6 | .col { 7 | flex: 1; 8 | } 9 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/HomeButton.razor.css: -------------------------------------------------------------------------------- 1 | .home-button { 2 | white-space: nowrap; 3 | margin-right: 10px; 4 | transform: rotate(-5deg); 5 | } 6 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/AdminMenu.razor.css: -------------------------------------------------------------------------------- 1 | .top-row a { 2 | margin-left: 0 !important; 3 | } 4 | 5 | .dropdown-item:hover { 6 | background-color: white; 7 | } 8 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickgod/BlazorEcommerce/HEAD/BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickgod/BlazorEcommerce/HEAD/BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickgod/BlazorEcommerce/HEAD/BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patrickgod/BlazorEcommerce/HEAD/BlazorEcommerce/Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/UserButton.razor.css: -------------------------------------------------------------------------------- 1 | .show-menu { 2 | display: block; 3 | } 4 | 5 | .user-button { 6 | margin-left: .5em; 7 | } 8 | 9 | .top-row a { 10 | margin-left: 0; 11 | } 12 | 13 | .dropdown-item:hover { 14 | background-color: white; 15 | } 16 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/AddressService/IAddressService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.AddressService 2 | { 3 | public interface IAddressService 4 | { 5 | Task
GetAddress(); 6 | Task
AddOrUpdateAddress(Address address); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/AddressService/IAddressService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.AddressService 2 | { 3 | public interface IAddressService 4 | { 5 | Task> GetAddress(); 6 | Task> AddOrUpdateAddress(Address address); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/HomeButton.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 7 | 8 | @code { 9 | private void GoToHome() 10 | { 11 | NavigationManager.NavigateTo(""); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/OrderService/IOrderService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.OrderService 2 | { 3 | public interface IOrderService 4 | { 5 | Task PlaceOrder(); 6 | Task> GetOrders(); 7 | Task GetOrderDetails(int orderId); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/PaymentService/IPaymentService.cs: -------------------------------------------------------------------------------- 1 | using Stripe.Checkout; 2 | 3 | namespace BlazorEcommerce.Server.Services.PaymentService 4 | { 5 | public interface IPaymentService 6 | { 7 | Task CreateCheckoutSession(); 8 | Task> FulfillOrder(HttpRequest request); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/Image.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class Image 10 | { 11 | public int Id { get; set; } 12 | public string Data { get; set; } = string.Empty; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/BlazorEcommerce.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Shared 2 | { 3 | public class WeatherForecast 4 | { 5 | public DateTime Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public string? Summary { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | } 13 | } -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/OrderService/IOrderService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.OrderService 2 | { 3 | public interface IOrderService 4 | { 5 | Task> PlaceOrder(int userId); 6 | Task>> GetOrders(); 7 | Task> GetOrderDetails(int orderId); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Admin/Categories.razor.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | font-weight: 600; 4 | text-align: center; 5 | border-bottom: 1px solid lightgray; 6 | margin-bottom: 6px; 7 | padding-bottom: 6px; 8 | } 9 | 10 | .row { 11 | display: flex; 12 | padding: 6px; 13 | } 14 | 15 | .col { 16 | flex: 1; 17 | } 18 | 19 | .col-visible { 20 | text-align: center; 21 | } 22 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 | Counter 4 | 5 |

Counter

6 | 7 |

Current count: @currentCount

8 | 9 | 10 | 11 | @code { 12 | private int currentCount = 0; 13 | 14 | private void IncrementCount() 15 | { 16 | currentCount++; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "server=localhost\\sqlexpress;database=blazorecommerce;trusted_connection=true" 4 | }, 5 | "AppSettings": { 6 | "Token": "my top secret key" 7 | }, 8 | "Logging": { 9 | "LogLevel": { 10 | "Default": "Information", 11 | "Microsoft.AspNetCore": "Warning" 12 | } 13 | }, 14 | "AllowedHosts": "*" 15 | } 16 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/OrderSuccess.razor: -------------------------------------------------------------------------------- 1 | @page "/order-success" 2 | @inject ICartService CartService 3 | 4 | Thank you! 5 | 6 |

Thank you!

7 | 8 | Thank you for your order! You can check your orders here. 9 | 10 | @code { 11 | protected override async Task OnInitializedAsync() 12 | { 13 | await CartService.GetCartItemsCount(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/AuthService/IAuthService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.AuthService 2 | { 3 | public interface IAuthService 4 | { 5 | Task> Register(UserRegister request); 6 | Task> Login(UserLogin request); 7 | Task> ChangePassword(UserChangePassword request); 8 | Task IsUserAuthenticated(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/ServiceResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class ServiceResponse 10 | { 11 | public T? Data { get; set; } 12 | public bool Success { get; set; } = true; 13 | public string Message { get; set; } = string.Empty; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/OrderDetails.razor.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | padding: 6px; 4 | } 5 | 6 | .image-wrapper { 7 | width: 150px; 8 | text-align: center; 9 | } 10 | 11 | .image { 12 | max-height: 150px; 13 | max-width: 150px; 14 | padding: 6px; 15 | } 16 | 17 | .name { 18 | flex-grow: 1; 19 | padding: 6px; 20 | } 21 | 22 | .product-price { 23 | font-weight: 600; 24 | text-align: right; 25 | } 26 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/ProductSearchResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class ProductSearchResult 10 | { 11 | public List Products { get; set; } = new List(); 12 | public int Pages { get; set; } 13 | public int CurrentPage { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/CartItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class CartItem 10 | { 11 | public int UserId { get; set; } 12 | public int ProductId { get; set; } 13 | public int ProductTypeId { get; set; } 14 | public int Quantity { get; set; } = 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/OrderDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class OrderDetailsResponse 10 | { 11 | public DateTime OrderDate { get; set; } 12 | public decimal TotalPrice { get; set; } 13 | public List Products { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/ProductTypeService/IProductTypeService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.ProductTypeService 2 | { 3 | public interface IProductTypeService 4 | { 5 | Task>> GetProductTypes(); 6 | Task>> AddProductType(ProductType productType); 7 | Task>> UpdateProductType(ProductType productType); 8 | 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/UserLogin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorEcommerce.Shared 9 | { 10 | public class UserLogin 11 | { 12 | [Required] 13 | public string Email { get; set; } = string.Empty; 14 | [Required] 15 | public string Password { get; set; } = string.Empty; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Admin/EditProduct.razor.css: -------------------------------------------------------------------------------- 1 | img { 2 | max-height: 200px; 3 | max-width: 200px; 4 | } 5 | 6 | .row { 7 | display: flex; 8 | padding: 6px; 9 | } 10 | 11 | .col { 12 | flex: 1; 13 | } 14 | 15 | .header { 16 | display: flex; 17 | font-weight: 600; 18 | text-align: center; 19 | border-bottom: 1px solid lightgray; 20 | margin-bottom: 6px; 21 | padding-bottom: 6px; 22 | } 23 | 24 | .col-visible { 25 | text-align: center; 26 | } 27 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/ProductTypeService/IProductTypeService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.ProductTypeService 2 | { 3 | public interface IProductTypeService 4 | { 5 | event Action OnChange; 6 | public List ProductTypes { get; set; } 7 | Task GetProductTypes(); 8 | Task AddProductType(ProductType productType); 9 | Task UpdateProductType(ProductType productType); 10 | ProductType CreateNewProductType(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/OrderOverviewResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class OrderOverviewResponse 10 | { 11 | public int Id { get; set; } 12 | public DateTime OrderDate { get; set; } 13 | public decimal TotalPrice { get; set; } 14 | public string Product { get; set; } 15 | public string ProductImageUrl { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/CartService/ICartService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.CartService 2 | { 3 | public interface ICartService 4 | { 5 | event Action OnChange; 6 | Task AddToCart(CartItem cartItem); 7 | Task> GetCartProducts(); 8 | Task RemoveProductFromCart(int productId, int productTypeId); 9 | Task UpdateQuantity(CartProductResponse product); 10 | Task StoreCartItems(bool emptyLocalCart); 11 | Task GetCartItemsCount(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/CategoryService/ICategoryService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.CategoryService 2 | { 3 | public interface ICategoryService 4 | { 5 | Task>> GetCategories(); 6 | Task>> GetAdminCategories(); 7 | Task>> AddCategory(Category category); 8 | Task>> UpdateCategory(Category category); 9 | Task>> DeleteCategory(int id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/AuthService/IAuthService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.AuthService 2 | { 3 | public interface IAuthService 4 | { 5 | Task> Register(User user, string password); 6 | Task UserExists(string email); 7 | Task> Login(string email, string password); 8 | Task> ChangePassword(int userId, string newPassword); 9 | int GetUserId(); 10 | string GetUserEmail(); 11 | Task GetUserByEmail(string email); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/ProductType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorEcommerce.Shared 9 | { 10 | public class ProductType 11 | { 12 | public int Id { get; set; } 13 | public string Name { get; set; } = string.Empty; 14 | [NotMapped] 15 | public bool Editing { get; set; } = false; 16 | [NotMapped] 17 | public bool IsNew { get; set; } = false; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Orders.razor.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | padding: 6px; 4 | border: 1px solid; 5 | border-color: lightgray; 6 | border-radius: 6px; 7 | margin-bottom: 10px; 8 | } 9 | 10 | .image-wrapper { 11 | width: 150px; 12 | text-align: center; 13 | } 14 | 15 | .image { 16 | max-height: 150px; 17 | max-width: 150px; 18 | padding: 6px; 19 | } 20 | 21 | .details { 22 | flex-grow: 1; 23 | padding: 6px; 24 | } 25 | 26 | .order-price { 27 | font-weight: 600; 28 | font-size: 1.2em; 29 | text-align: right; 30 | } 31 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/ShopLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 |
11 | 12 | 15 | 16 |
17 | @Body 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/OrderDetailsProductResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class OrderDetailsProductResponse 10 | { 11 | public int ProductId { get; set; } 12 | public string Title { get; set; } 13 | public string ProductType { get; set; } 14 | public string ImageUrl { get; set; } 15 | public int Quantity { get; set; } 16 | public decimal TotalPrice { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/CategoryService/ICategoryService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.CategoryService 2 | { 3 | public interface ICategoryService 4 | { 5 | event Action OnChange; 6 | List Categories { get; set; } 7 | List AdminCategories { get; set; } 8 | Task GetCategories(); 9 | Task GetAdminCategories(); 10 | Task AddCategory(Category category); 11 | Task UpdateCategory(Category category); 12 | Task DeleteCategory(int categoryId); 13 | Category CreateNewCategory(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 |
2 | 3 | @Title 4 | 5 | 6 | Please take our 7 | brief survey 8 | 9 | and tell us what you think. 10 |
11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string? Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/UserChangePassword.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorEcommerce.Shared 9 | { 10 | public class UserChangePassword 11 | { 12 | [Required, StringLength(100, MinimumLength = 6)] 13 | public string Password { get; set; } = string.Empty; 14 | [Compare("Password", ErrorMessage = "The passwords do not match.")] 15 | public string ConfirmPassword { get; set; } = string.Empty; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorEcommerce.Shared 9 | { 10 | public class Order 11 | { 12 | public int Id { get; set; } 13 | public int UserId { get; set; } 14 | public DateTime OrderDate { get; set; } = DateTime.Now; 15 | 16 | [Column(TypeName = "decimal(18,2)")] 17 | public decimal TotalPrice { get; set; } 18 | public List OrderItems { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class User 10 | { 11 | public int Id { get; set; } 12 | public string Email { get; set; } = string.Empty; 13 | public byte[] PasswordHash { get; set; } 14 | public byte[] PasswordSalt { get; set; } 15 | public DateTime DateCreated { get; set; } = DateTime.Now; 16 | public Address Address { get; set; } 17 | public string Role { get; set; } = "Customer"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/ProductDetails.razor.css: -------------------------------------------------------------------------------- 1 | .media-img { 2 | max-height: 500px; 3 | max-width: 500px; 4 | border-radius: 6px; 5 | margin: 0 10px 10px 10px; 6 | } 7 | 8 | .media-img-wrapper { 9 | max-width: 500px; 10 | max-height: 500px; 11 | text-align: center; 12 | } 13 | 14 | .original-price { 15 | text-decoration: line-through; 16 | } 17 | 18 | @media (max-width: 1023.98px) { 19 | .media { 20 | flex-direction: column; 21 | } 22 | 23 | .media-img { 24 | max-width: 300px; 25 | } 26 | 27 | .media-img-wrapper { 28 | width: 100%; 29 | height: auto; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/CartProductResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class CartProductResponse 10 | { 11 | public int ProductId { get; set; } 12 | public string Title { get; set; } = string.Empty; 13 | public int ProductTypeId { get; set; } 14 | public string ProductType { get; set; } = string.Empty; 15 | public string ImageUrl { get; set; } = string.Empty; 16 | public decimal Price { get; set; } 17 | public int Quantity { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/ProductList.razor.css: -------------------------------------------------------------------------------- 1 | .media-img { 2 | max-height: 200px; 3 | max-width: 200px; 4 | border-radius: 6px; 5 | margin-bottom: 10px; 6 | transition: transform .2s; 7 | } 8 | 9 | .media-img:hover { 10 | transform: scale(1.1); 11 | } 12 | 13 | .media-img-wrapper { 14 | width: 200px; 15 | text-align: center; 16 | } 17 | 18 | .page-selection { 19 | margin-right: 15px; 20 | margin-bottom: 30px; 21 | } 22 | 23 | @media (max-width: 1023.98px) { 24 | .media { 25 | flex-direction: column; 26 | } 27 | 28 | .media-img-wrapper { 29 | width: 100%; 30 | height: auto; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/UserRegister.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorEcommerce.Shared 9 | { 10 | public class UserRegister 11 | { 12 | [Required, EmailAddress] 13 | public string Email { get; set; } = string.Empty; 14 | [Required, StringLength(100, MinimumLength = 6)] 15 | public string Password { get; set; } = string.Empty; 16 | [Compare("Password", ErrorMessage = "The passwords do not match.")] 17 | public string ConfirmPassword { get; set; } = string.Empty; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/CartCounter.razor: -------------------------------------------------------------------------------- 1 | @inject ICartService CartService 2 | @inject ISyncLocalStorageService LocalStorage 3 | @implements IDisposable 4 | 5 | 6 | 7 | @GetCartItemsCount() 8 | 9 | 10 | @code { 11 | private int GetCartItemsCount() 12 | { 13 | var count = LocalStorage.GetItem("cartItemsCount"); 14 | return count; 15 | } 16 | 17 | protected override void OnInitialized() 18 | { 19 | CartService.OnChange += StateHasChanged; 20 | } 21 | 22 | public void Dispose() 23 | { 24 | CartService.OnChange -= StateHasChanged; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Cart.razor.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | padding: 6px; 4 | } 5 | 6 | .image-wrapper { 7 | width: 150px; 8 | text-align: center; 9 | } 10 | 11 | .image { 12 | max-height: 150px; 13 | max-width: 150px; 14 | padding: 6px; 15 | } 16 | 17 | .name { 18 | flex-grow: 1; 19 | padding: 6px; 20 | } 21 | 22 | .cart-product-price { 23 | font-weight: 600; 24 | text-align: right; 25 | } 26 | 27 | .btn-delete { 28 | background: none; 29 | border: none; 30 | padding: 0px; 31 | color: red; 32 | font-size: 12px; 33 | } 34 | 35 | .btn-delete:hover { 36 | text-decoration: underline; 37 | } 38 | 39 | .input-quantity { 40 | width: 70px; 41 | } 42 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/Category.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorEcommerce.Shared 9 | { 10 | public class Category 11 | { 12 | public int Id { get; set; } 13 | public string Name { get; set; } = string.Empty; 14 | public string Url { get; set; } = string.Empty; 15 | public bool Visible { get; set; } = true; 16 | public bool Deleted { get; set; } = false; 17 | [NotMapped] 18 | public bool Editing { get; set; } = false; 19 | [NotMapped] 20 | public bool IsNew { get; set; } = false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/FeaturedProducts.razor.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | overflow-x: auto; 5 | justify-content: center; 6 | } 7 | 8 | img { 9 | max-width: 200px; 10 | max-height: 200px; 11 | border-radius: 6px; 12 | transition: transform .2s; 13 | margin-bottom: 10px; 14 | } 15 | 16 | img:hover { 17 | transform: scale(1.1) rotate(5deg); 18 | } 19 | 20 | .featured-product { 21 | margin: 10px; 22 | text-align: center; 23 | padding: 10px; 24 | border: 1px solid lightgray; 25 | border-radius: 10px; 26 | max-width: 200px; 27 | } 28 | 29 | @media (max-width: 1023.98px) { 30 | .container { 31 | justify-content: flex-start; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/OrderItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorEcommerce.Shared 9 | { 10 | public class OrderItem 11 | { 12 | public Order Order { get; set; } 13 | public int OrderId { get; set; } 14 | public Product Product { get; set; } 15 | public int ProductId { get; set; } 16 | public ProductType ProductType { get; set; } 17 | public int ProductTypeId { get; set; } 18 | public int Quantity { get; set; } 19 | [Column(TypeName = "decimal(18,2)")] 20 | public decimal TotalPrice { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/CartService/ICartService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.CartService 2 | { 3 | public interface ICartService 4 | { 5 | Task>> GetCartProducts(List cartItems); 6 | Task>> StoreCartItems(List cartItems); 7 | Task> GetCartItemsCount(); 8 | Task>> GetDbCartProducts(int? userId = null); 9 | Task> AddToCart(CartItem cartItem); 10 | Task> UpdateQuantity(CartItem cartItem); 11 | Task> RemoveItemFromCart(int productId, int productTypeId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/Address.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace BlazorEcommerce.Shared 8 | { 9 | public class Address 10 | { 11 | public int Id { get; set; } 12 | public int UserId { get; set; } 13 | public string FirstName { get; set; } = string.Empty; 14 | public string LastName { get; set; } = string.Empty; 15 | public string Street { get; set; } = string.Empty; 16 | public string City { get; set; } = string.Empty; 17 | public string State { get; set; } = string.Empty; 18 | public string Zip { get; set; } = string.Empty; 19 | public string Country { get; set; } = string.Empty; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/AdminMenu.razor: -------------------------------------------------------------------------------- 1 | @inject AuthenticationStateProvider AuthStateProvider 2 | @using System.Security.Claims 3 | 4 | @if (authorized) 5 | { 6 | Categories 7 | Product Types 8 | Products 9 |
10 | } 11 | 12 | @code { 13 | bool authorized = false; 14 | 15 | protected override async Task OnInitializedAsync() 16 | { 17 | string role = (await AuthStateProvider.GetAuthenticationStateAsync()) 18 | .User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role).Value; 19 | if (role.Contains("Admin")) 20 | { 21 | authorized = true; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211205210009_UserRole.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class UserRole : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.AddColumn( 12 | name: "Role", 13 | table: "Users", 14 | type: "nvarchar(max)", 15 | nullable: false, 16 | defaultValue: ""); 17 | } 18 | 19 | protected override void Down(MigrationBuilder migrationBuilder) 20 | { 21 | migrationBuilder.DropColumn( 22 | name: "Role", 23 | table: "Users"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | 5 | namespace BlazorEcommerce.Server.Pages 6 | { 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/AddressService/AddressService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.AddressService 2 | { 3 | public class AddressService : IAddressService 4 | { 5 | private readonly HttpClient _http; 6 | 7 | public AddressService(HttpClient http) 8 | { 9 | _http = http; 10 | } 11 | 12 | public async Task
AddOrUpdateAddress(Address address) 13 | { 14 | var response = await _http.PostAsJsonAsync("api/address", address); 15 | return response.Content 16 | .ReadFromJsonAsync>().Result.Data; 17 | } 18 | 19 | public async Task
GetAddress() 20 | { 21 | var response = await _http 22 | .GetFromJsonAsync>("api/address"); 23 | return response.Data; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @page "/search/{searchText}/{page:int}" 3 | @page "/{categoryUrl}" 4 | @inject IProductService ProductService 5 | 6 | My Shop 7 | 8 | @if (SearchText == null && CategoryUrl == null) 9 | { 10 | 11 | } 12 | else 13 | { 14 | 15 | } 16 | 17 | @code { 18 | [Parameter] 19 | public string? CategoryUrl { get; set; } = null; 20 | 21 | [Parameter] 22 | public string? SearchText { get; set; } = null; 23 | 24 | [Parameter] 25 | public int Page { get; set; } = 1; 26 | 27 | protected override async Task OnParametersSetAsync() 28 | { 29 | if (SearchText != null) 30 | { 31 | await ProductService.SearchProducts(SearchText, Page); 32 | } 33 | else 34 | { 35 | await ProductService.GetProducts(CategoryUrl); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Whoops! You're not allowed to see this page.

7 |
Please login or register for a new account.
8 |
9 |
10 | 11 |
12 | 13 | Not found 14 | 15 |

Sorry, there's nothing at this address.

16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/ProductService/IProductService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.ProductService 2 | { 3 | public interface IProductService 4 | { 5 | event Action ProductsChanged; 6 | List Products { get; set; } 7 | List AdminProducts { get; set; } 8 | string Message { get; set; } 9 | int CurrentPage { get; set; } 10 | int PageCount { get; set; } 11 | string LastSearchText { get; set; } 12 | Task GetProducts(string? categoryUrl = null); 13 | Task> GetProduct(int productId); 14 | Task SearchProducts(string searchText, int page); 15 | Task> GetProductSearchSuggestions(string searchText); 16 | Task GetAdminProducts(); 17 | Task CreateProduct(Product product); 18 | Task UpdateProduct(Product product); 19 | Task DeleteProduct(Product product); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/ProductService/IProductService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.ProductService 2 | { 3 | public interface IProductService 4 | { 5 | Task>> GetProductsAsync(); 6 | Task> GetProductAsync(int productId); 7 | Task>> GetProductsByCategory(string categoryUrl); 8 | Task> SearchProducts(string searchText, int page); 9 | Task>> GetProductSearchSuggestions(string searchText); 10 | Task>> GetFeaturedProducts(); 11 | Task>> GetAdminProducts(); 12 | Task> CreateProduct(Product product); 13 | Task> UpdateProduct(Product product); 14 | Task> DeleteProduct(int productId); 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/BlazorEcommerce.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/AddressController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace BlazorEcommerce.Server.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | [Authorize] 10 | public class AddressController : ControllerBase 11 | { 12 | private readonly IAddressService _addressService; 13 | 14 | public AddressController(IAddressService addressService) 15 | { 16 | _addressService = addressService; 17 | } 18 | 19 | [HttpGet] 20 | public async Task>> GetAddress() 21 | { 22 | return await _addressService.GetAddress(); 23 | } 24 | 25 | [HttpPost] 26 | public async Task>> AddOrUpdateAddress(Address address) 27 | { 28 | return await _addressService.AddOrUpdateAddress(address); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using BlazorEcommerce.Client 10 | @using BlazorEcommerce.Client.Shared 11 | @using BlazorEcommerce.Shared 12 | @using BlazorEcommerce.Client.Services.ProductService 13 | @using BlazorEcommerce.Client.Services.CategoryService 14 | @using BlazorEcommerce.Client.Services.CartService 15 | @using BlazorEcommerce.Client.Services.AuthService 16 | @using BlazorEcommerce.Client.Services.OrderService 17 | @using BlazorEcommerce.Client.Services.AddressService 18 | @using BlazorEcommerce.Client.Services.ProductTypeService 19 | @using Blazored.LocalStorage 20 | @using Microsoft.AspNetCore.Components.Authorization 21 | @using Microsoft.AspNetCore.Authorization 22 | @using MudBlazor 23 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/OrderController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace BlazorEcommerce.Server.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | [ApiController] 8 | public class OrderController : ControllerBase 9 | { 10 | private readonly IOrderService _orderService; 11 | 12 | public OrderController(IOrderService orderService) 13 | { 14 | _orderService = orderService; 15 | } 16 | 17 | [HttpGet] 18 | public async Task>>> GetOrders() 19 | { 20 | var result = await _orderService.GetOrders(); 21 | return Ok(result); 22 | } 23 | 24 | [HttpGet("{orderId}")] 25 | public async Task>> GetOrdersDetails(int orderId) 26 | { 27 | var result = await _orderService.GetOrderDetails(orderId); 28 | return Ok(result); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Orders.razor: -------------------------------------------------------------------------------- 1 | @page "/orders" 2 | @inject IOrderService OrderService 3 | 4 |

Orders

5 | 6 | @if (orders == null) 7 | { 8 | Loading your orders... 9 | } 10 | else if (orders.Count <= 0) 11 | { 12 | You have no orders, yet. 13 | } 14 | else 15 | { 16 | foreach (var order in orders) 17 | { 18 |
19 |
20 | 21 |
22 |
23 |

@order.Product

24 | @order.OrderDate
25 | Show more... 26 |
27 |
$@order.TotalPrice
28 |
29 | } 30 | } 31 | 32 | 33 | @code { 34 | List orders = null; 35 | 36 | protected override async Task OnInitializedAsync() 37 | { 38 | orders = await OrderService.GetOrders(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/ProductVariant.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.Json.Serialization; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlazorEcommerce.Shared 10 | { 11 | public class ProductVariant 12 | { 13 | [JsonIgnore] 14 | public Product? Product { get; set; } 15 | public int ProductId { get; set; } 16 | public ProductType? ProductType { get; set; } 17 | public int ProductTypeId { get; set; } 18 | 19 | [Column(TypeName = "decimal(18,2)")] 20 | public decimal Price { get; set; } 21 | 22 | [Column(TypeName = "decimal(18,2)")] 23 | public decimal OriginalPrice { get; set; } 24 | public bool Visible { get; set; } = true; 25 | public bool Deleted { get; set; } = false; 26 | [NotMapped] 27 | public bool Editing { get; set; } = false; 28 | [NotMapped] 29 | public bool IsNew { get; set; } = false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:49651", 7 | "sslPort": 44374 8 | } 9 | }, 10 | "profiles": { 11 | "BlazorEcommerce": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:7226;http://localhost:5226", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BlazorEcommerce 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
Loading...
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:49651", 7 | "sslPort": 44374 8 | } 9 | }, 10 | "profiles": { 11 | "BlazorEcommerce.Server": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:7226;http://localhost:5226", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace BlazorEcommerce.Server.Controllers 4 | { 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class WeatherForecastController : ControllerBase 8 | { 9 | private static readonly string[] Summaries = new[] 10 | { 11 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 12 | }; 13 | 14 | private readonly ILogger _logger; 15 | 16 | public WeatherForecastController(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | [HttpGet] 22 | public IEnumerable Get() 23 | { 24 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 25 | { 26 | Date = DateTime.Now.AddDays(index), 27 | TemperatureC = Random.Shared.Next(-20, 55), 28 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 29 | }) 30 | .ToArray(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/PaymentController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace BlazorEcommerce.Server.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class PaymentController : ControllerBase 10 | { 11 | private readonly IPaymentService _paymentService; 12 | 13 | public PaymentController(IPaymentService paymentService) 14 | { 15 | _paymentService = paymentService; 16 | } 17 | 18 | [HttpPost("checkout"), Authorize] 19 | public async Task> CreateCheckoutSession() 20 | { 21 | var session = await _paymentService.CreateCheckoutSession(); 22 | return Ok(session.Url); 23 | } 24 | 25 | [HttpPost] 26 | public async Task>> FulfillOrder() 27 | { 28 | var response = await _paymentService.FulfillOrder(Request); 29 | if(!response.Success) 30 | return BadRequest(response.Message); 31 | 32 | return Ok(response); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BlazorEcommerce/Shared/Product.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlazorEcommerce.Shared 10 | { 11 | public class Product 12 | { 13 | public int Id { get; set; } 14 | [Required] 15 | public string Title { get; set; } = string.Empty; 16 | public string Description { get; set; } = string.Empty; 17 | public string ImageUrl { get; set; } = string.Empty; 18 | public List Images { get; set; } = new List(); 19 | public Category? Category { get; set; } 20 | public int CategoryId { get; set; } 21 | public bool Featured { get; set; } = false; 22 | public List Variants { get; set; } = new List(); 23 | public bool Visible { get; set; } = true; 24 | public bool Deleted { get; set; } = false; 25 | [NotMapped] 26 | public bool Editing { get; set; } = false; 27 | [NotMapped] 28 | public bool IsNew { get; set; } = false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211124204804_CartItems.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class CartItems : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "CartItems", 13 | columns: table => new 14 | { 15 | UserId = table.Column(type: "int", nullable: false), 16 | ProductId = table.Column(type: "int", nullable: false), 17 | ProductTypeId = table.Column(type: "int", nullable: false), 18 | Quantity = table.Column(type: "int", nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_CartItems", x => new { x.UserId, x.ProductId, x.ProductTypeId }); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "CartItems"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/FetchData.razor: -------------------------------------------------------------------------------- 1 | @page "/fetchdata" 2 | @using BlazorEcommerce.Shared 3 | @inject HttpClient Http 4 | 5 | Weather forecast 6 | 7 |

Weather forecast

8 | 9 |

This component demonstrates fetching data from the server.

10 | 11 | @if (forecasts == null) 12 | { 13 |

Loading...

14 | } 15 | else 16 | { 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @foreach (var forecast in forecasts) 28 | { 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 36 | 37 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
38 | } 39 | 40 | @code { 41 | private WeatherForecast[]? forecasts; 42 | 43 | protected override async Task OnInitializedAsync() 44 | { 45 | forecasts = await Http.GetFromJsonAsync("WeatherForecast"); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/OrderDetails.razor: -------------------------------------------------------------------------------- 1 | @page "/orders/{orderId:int}" 2 | @inject IOrderService OrderService 3 | 4 | @if (order == null) 5 | { 6 | Loading order... 7 | } 8 | else 9 | { 10 |

Order from @order.OrderDate

11 | 12 |
13 | @foreach (var product in order.Products) 14 | { 15 |
16 |
17 | 18 |
19 |
20 |
@product.Title
21 | @product.ProductType
22 | Quantity: @product.Quantity 23 |
24 |
$@product.TotalPrice
25 |
26 | } 27 |
28 | Total: $@order.TotalPrice 29 |
30 |
31 | } 32 | 33 | @code { 34 | [Parameter] 35 | public int OrderId { get; set; } 36 | 37 | OrderDetailsResponse order = null; 38 | 39 | protected override async Task OnInitializedAsync() 40 | { 41 | order = await OrderService.GetOrderDetails(OrderId); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/BlazorEcommerce.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/ShopNavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | padding-top: 0.5rem; 25 | } 26 | 27 | .nav-item ::deep a { 28 | color: #d7d7d7; 29 | border-radius: 4px; 30 | height: 3rem; 31 | display: flex; 32 | align-items: center; 33 | line-height: 3rem; 34 | } 35 | 36 | .nav-item ::deep a.active { 37 | background-color: rgba(255,255,255,0.25); 38 | color: white; 39 | } 40 | 41 | .nav-item ::deep a:hover { 42 | background-color: rgba(255,255,255,0.1); 43 | color: white; 44 | } 45 | 46 | .flex-nav { 47 | flex-direction: column; 48 | } 49 | 50 | @media (min-width: 641px) { 51 | .navbar-toggler, .navbar-toggler-wrapper { 52 | display: none; 53 | } 54 | 55 | .flex-nav { 56 | flex-direction: row; 57 | display: flex; 58 | } 59 | 60 | .collapse { 61 | /* Never collapse the sidebar for wide screens */ 62 | display: block; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211112205817_CreateInitial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class CreateInitial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Products", 13 | columns: table => new 14 | { 15 | Id = table.Column(type: "int", nullable: false) 16 | .Annotation("SqlServer:Identity", "1, 1"), 17 | Title = table.Column(type: "nvarchar(max)", nullable: false), 18 | Description = table.Column(type: "nvarchar(max)", nullable: false), 19 | ImageUrl = table.Column(type: "nvarchar(max)", nullable: false), 20 | Price = table.Column(type: "decimal(18,2)", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Products", x => x.Id); 25 | }); 26 | } 27 | 28 | protected override void Down(MigrationBuilder migrationBuilder) 29 | { 30 | migrationBuilder.DropTable( 31 | name: "Products"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211117210144_Users.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace BlazorEcommerce.Server.Migrations 7 | { 8 | public partial class Users : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "Users", 14 | columns: table => new 15 | { 16 | Id = table.Column(type: "int", nullable: false) 17 | .Annotation("SqlServer:Identity", "1, 1"), 18 | Email = table.Column(type: "nvarchar(max)", nullable: false), 19 | PasswordHash = table.Column(type: "varbinary(max)", nullable: false), 20 | PasswordSalt = table.Column(type: "varbinary(max)", nullable: false), 21 | DateCreated = table.Column(type: "datetime2", nullable: false) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_Users", x => x.Id); 26 | }); 27 | } 28 | 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropTable( 32 | name: "Users"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @inject ICategoryService CategoryService 2 | 3 | 11 | 12 |
13 | 28 |
29 | 30 | @code { 31 | private bool collapseNavMenu = true; 32 | 33 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 34 | 35 | private void ToggleNavMenu() 36 | { 37 | collapseNavMenu = !collapseNavMenu; 38 | } 39 | 40 | protected override async Task OnInitializedAsync() 41 | { 42 | await CategoryService.GetCategories(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Profile.razor: -------------------------------------------------------------------------------- 1 | @page "/profile" 2 | @inject IAuthService AuthService 3 | @attribute [Authorize] 4 | 5 | 6 |

Hi! You're logged in with @context.User.Identity.Name.

7 |
8 | 9 |
Delivery Address
10 | 11 |

12 | 13 |
Change Password
14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 |
29 | @message 30 | 31 | @code { 32 | UserChangePassword request = new UserChangePassword(); 33 | string message = string.Empty; 34 | 35 | private async Task ChangePassword() 36 | { 37 | var result = await AuthService.ChangePassword(request); 38 | message = result.Message; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/ProductTypeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace BlazorEcommerce.Server.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | [Authorize(Roles = "Admin")] 10 | public class ProductTypeController : ControllerBase 11 | { 12 | private readonly IProductTypeService _productTypeService; 13 | 14 | public ProductTypeController(IProductTypeService productTypeService) 15 | { 16 | _productTypeService = productTypeService; 17 | } 18 | 19 | [HttpGet] 20 | public async Task>>> GetProductTypes() 21 | { 22 | var response = await _productTypeService.GetProductTypes(); 23 | return Ok(response); 24 | } 25 | 26 | [HttpPost] 27 | public async Task>>> AddProductType(ProductType productType) 28 | { 29 | var response = await _productTypeService.AddProductType(productType); 30 | return Ok(response); 31 | } 32 | 33 | [HttpPut] 34 | public async Task>>> UpdateProductType(ProductType productType) 35 | { 36 | var response = await _productTypeService.UpdateProductType(productType); 37 | return Ok(response); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211116221155_FeaturedProducts.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class FeaturedProducts : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.AddColumn( 12 | name: "Featured", 13 | table: "Products", 14 | type: "bit", 15 | nullable: false, 16 | defaultValue: false); 17 | 18 | migrationBuilder.UpdateData( 19 | table: "Products", 20 | keyColumn: "Id", 21 | keyValue: 1, 22 | column: "Featured", 23 | value: true); 24 | 25 | migrationBuilder.UpdateData( 26 | table: "Products", 27 | keyColumn: "Id", 28 | keyValue: 5, 29 | column: "Featured", 30 | value: true); 31 | 32 | migrationBuilder.UpdateData( 33 | table: "Products", 34 | keyColumn: "Id", 35 | keyValue: 9, 36 | column: "Featured", 37 | value: true); 38 | } 39 | 40 | protected override void Down(MigrationBuilder migrationBuilder) 41 | { 42 | migrationBuilder.DropColumn( 43 | name: "Featured", 44 | table: "Products"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/ShopNavMenu.razor: -------------------------------------------------------------------------------- 1 | @inject ICategoryService CategoryService 2 | @implements IDisposable 3 | 4 | 11 | 12 |
13 | 28 |
29 | 30 | @code { 31 | private bool collapseNavMenu = true; 32 | 33 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 34 | 35 | private void ToggleNavMenu() 36 | { 37 | collapseNavMenu = !collapseNavMenu; 38 | } 39 | 40 | protected override async Task OnInitializedAsync() 41 | { 42 | await CategoryService.GetCategories(); 43 | CategoryService.OnChange += StateHasChanged; 44 | } 45 | 46 | public void Dispose() 47 | { 48 | CategoryService.OnChange -= StateHasChanged; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/AuthService/AuthService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.AuthService 2 | { 3 | public class AuthService : IAuthService 4 | { 5 | private readonly HttpClient _http; 6 | private readonly AuthenticationStateProvider _authStateProvider; 7 | 8 | public AuthService(HttpClient http, AuthenticationStateProvider authStateProvider) 9 | { 10 | _http = http; 11 | _authStateProvider = authStateProvider; 12 | } 13 | 14 | public async Task> ChangePassword(UserChangePassword request) 15 | { 16 | var result = await _http.PostAsJsonAsync("api/auth/change-password", request.Password); 17 | return await result.Content.ReadFromJsonAsync>(); 18 | } 19 | 20 | public async Task IsUserAuthenticated() 21 | { 22 | return (await _authStateProvider.GetAuthenticationStateAsync()).User.Identity.IsAuthenticated; 23 | } 24 | 25 | public async Task> Login(UserLogin request) 26 | { 27 | var result = await _http.PostAsJsonAsync("api/auth/login", request); 28 | return await result.Content.ReadFromJsonAsync>(); 29 | } 30 | 31 | public async Task> Register(UserRegister request) 32 | { 33 | var result = await _http.PostAsJsonAsync("api/auth/register", request); 34 | return await result.Content.ReadFromJsonAsync>(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20220429135440_Images.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class Images : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Images", 13 | columns: table => new 14 | { 15 | Id = table.Column(type: "int", nullable: false) 16 | .Annotation("SqlServer:Identity", "1, 1"), 17 | Data = table.Column(type: "nvarchar(max)", nullable: false), 18 | ProductId = table.Column(type: "int", nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Images", x => x.Id); 23 | table.ForeignKey( 24 | name: "FK_Images_Products_ProductId", 25 | column: x => x.ProductId, 26 | principalTable: "Products", 27 | principalColumn: "Id"); 28 | }); 29 | 30 | migrationBuilder.CreateIndex( 31 | name: "IX_Images_ProductId", 32 | table: "Images", 33 | column: "ProductId"); 34 | } 35 | 36 | protected override void Down(MigrationBuilder migrationBuilder) 37 | { 38 | migrationBuilder.DropTable( 39 | name: "Images"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/ShopLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .nav-menu { 12 | background-image: linear-gradient(180deg, rgb(44, 62, 80) 0%, rgb(52, 73, 94) 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: .5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row.auth { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .top-row { 55 | position: sticky; 56 | top: 0; 57 | z-index: 1; 58 | } 59 | 60 | .top-row.auth ::deep a:first-child { 61 | flex: 1; 62 | text-align: right; 63 | width: 0; 64 | } 65 | 66 | .top-row, article { 67 | padding-left: 2rem !important; 68 | padding-right: 1.5rem !important; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Register.razor: -------------------------------------------------------------------------------- 1 | @page "/register" 2 | @inject IAuthService AuthService 3 | 4 | Register 5 | 6 |

Register

7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 | @message 28 |
29 |
30 | 31 | @code { 32 | UserRegister user = new UserRegister(); 33 | 34 | string message = string.Empty; 35 | string messageCssClass = string.Empty; 36 | 37 | async Task HandleRegistration() 38 | { 39 | var result = await AuthService.Register(user); 40 | message = result.Message; 41 | if (result.Success) 42 | messageCssClass = "text-success"; 43 | else 44 | messageCssClass = "text-danger"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model BlazorEcommerce.Server.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/ProductTypeService/ProductTypeService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.ProductTypeService 2 | { 3 | public class ProductTypeService : IProductTypeService 4 | { 5 | private readonly DataContext _context; 6 | 7 | public ProductTypeService(DataContext context) 8 | { 9 | _context = context; 10 | } 11 | 12 | public async Task>> AddProductType(ProductType productType) 13 | { 14 | productType.Editing = productType.IsNew = false; 15 | _context.ProductTypes.Add(productType); 16 | await _context.SaveChangesAsync(); 17 | 18 | return await GetProductTypes(); 19 | } 20 | 21 | public async Task>> GetProductTypes() 22 | { 23 | var productTypes = await _context.ProductTypes.ToListAsync(); 24 | return new ServiceResponse> { Data = productTypes }; 25 | } 26 | 27 | public async Task>> UpdateProductType(ProductType productType) 28 | { 29 | var dbProductType = await _context.ProductTypes.FindAsync(productType.Id); 30 | if (dbProductType == null) 31 | { 32 | return new ServiceResponse> 33 | { 34 | Success = false, 35 | Message = "Product Type not found." 36 | }; 37 | } 38 | 39 | dbProductType.Name = productType.Name; 40 | await _context.SaveChangesAsync(); 41 | 42 | return await GetProductTypes(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/Search.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | @inject IProductService ProductService 3 | 4 |
5 | 13 | 14 | @foreach (var suggestion in suggestions) 15 | { 16 | 17 | } 18 | 19 |
20 | 23 |
24 |
25 | 26 | @code { 27 | private string searchText = string.Empty; 28 | private List suggestions = new List(); 29 | protected ElementReference searchInput; 30 | 31 | protected override async Task OnAfterRenderAsync(bool firstRender) 32 | { 33 | if (firstRender) 34 | { 35 | await searchInput.FocusAsync(); 36 | } 37 | } 38 | 39 | public void SearchProducts() 40 | { 41 | NavigationManager.NavigateTo($"search/{searchText}/1"); 42 | } 43 | 44 | public async Task HandleSearch(KeyboardEventArgs args) 45 | { 46 | if (args.Key == null || args.Key.Equals("Enter")) 47 | { 48 | SearchProducts(); 49 | } 50 | else if (searchText.Length > 1) 51 | { 52 | suggestions = await ProductService.GetProductSearchSuggestions(searchText); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/FeaturedProducts.razor: -------------------------------------------------------------------------------- 1 | @inject IProductService ProductService 2 | @implements IDisposable 3 | 4 |

Top Products of Today

5 | @if (ProductService.Products == null || ProductService.Products.Count == 0) 6 | { 7 | @ProductService.Message 8 | } 9 | else 10 | { 11 |
12 | @foreach (var product in ProductService.Products) 13 | { 14 | @if (product.Featured) 15 | { 16 | 37 | } 38 | } 39 |
40 | } 41 | 42 | @code { 43 | protected override void OnInitialized() 44 | { 45 | ProductService.ProductsChanged += StateHasChanged; 46 | } 47 | 48 | public void Dispose() 49 | { 50 | ProductService.ProductsChanged -= StateHasChanged; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/ProductTypeService/ProductTypeService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.ProductTypeService 2 | { 3 | public class ProductTypeService : IProductTypeService 4 | { 5 | private readonly HttpClient _http; 6 | 7 | public ProductTypeService(HttpClient http) 8 | { 9 | _http = http; 10 | } 11 | 12 | public List ProductTypes { get; set; } = new List(); 13 | 14 | public event Action OnChange; 15 | 16 | public async Task AddProductType(ProductType productType) 17 | { 18 | var response = await _http.PostAsJsonAsync("api/producttype", productType); 19 | ProductTypes = (await response.Content 20 | .ReadFromJsonAsync>>()).Data; 21 | OnChange.Invoke(); 22 | } 23 | 24 | public ProductType CreateNewProductType() 25 | { 26 | var newProductType = new ProductType { IsNew = true, Editing = true }; 27 | 28 | ProductTypes.Add(newProductType); 29 | OnChange.Invoke(); 30 | return newProductType; 31 | } 32 | 33 | public async Task GetProductTypes() 34 | { 35 | var result = await _http 36 | .GetFromJsonAsync>>("api/producttype"); 37 | ProductTypes = result.Data; 38 | } 39 | 40 | public async Task UpdateProductType(ProductType productType) 41 | { 42 | var response = await _http.PutAsJsonAsync("api/producttype", productType); 43 | ProductTypes = (await response.Content 44 | .ReadFromJsonAsync>>()).Data; 45 | OnChange.Invoke(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row:not(.auth) { 41 | display: none; 42 | } 43 | 44 | .top-row.auth { 45 | justify-content: space-between; 46 | } 47 | 48 | .top-row ::deep a, .top-row ::deep .btn-link { 49 | margin-left: 0; 50 | } 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .page { 55 | flex-direction: row; 56 | } 57 | 58 | .sidebar { 59 | width: 250px; 60 | height: 100vh; 61 | position: sticky; 62 | top: 0; 63 | } 64 | 65 | .top-row { 66 | position: sticky; 67 | top: 0; 68 | z-index: 1; 69 | } 70 | 71 | .top-row.auth ::deep a:first-child { 72 | flex: 1; 73 | text-align: right; 74 | width: 0; 75 | } 76 | 77 | .top-row, article { 78 | padding-left: 2rem !important; 79 | padding-right: 1.5rem !important; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/AddressService/AddressService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.AddressService 2 | { 3 | public class AddressService : IAddressService 4 | { 5 | private readonly DataContext _context; 6 | private readonly IAuthService _authService; 7 | 8 | public AddressService(DataContext context, IAuthService authService) 9 | { 10 | _context = context; 11 | _authService = authService; 12 | } 13 | 14 | public async Task> AddOrUpdateAddress(Address address) 15 | { 16 | var response = new ServiceResponse
(); 17 | var dbAddress = (await GetAddress()).Data; 18 | if (dbAddress == null) 19 | { 20 | address.UserId = _authService.GetUserId(); 21 | _context.Addresses.Add(address); 22 | response.Data = address; 23 | } 24 | else 25 | { 26 | dbAddress.FirstName = address.FirstName; 27 | dbAddress.LastName = address.LastName; 28 | dbAddress.State = address.State; 29 | dbAddress.Country = address.Country; 30 | dbAddress.City = address.City; 31 | dbAddress.Zip = address.Zip; 32 | dbAddress.Street = address.Street; 33 | response.Data = dbAddress; 34 | } 35 | 36 | await _context.SaveChangesAsync(); 37 | 38 | return response; 39 | } 40 | 41 | public async Task> GetAddress() 42 | { 43 | int userId = _authService.GetUserId(); 44 | var address = await _context.Addresses 45 | .FirstOrDefaultAsync(a => a.UserId == userId); 46 | return new ServiceResponse
{ Data = address }; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Program.cs: -------------------------------------------------------------------------------- 1 | global using BlazorEcommerce.Shared; 2 | global using System.Net.Http.Json; 3 | global using BlazorEcommerce.Client.Services.ProductService; 4 | global using BlazorEcommerce.Client.Services.CategoryService; 5 | global using BlazorEcommerce.Client.Services.AuthService; 6 | global using Microsoft.AspNetCore.Components.Authorization; 7 | global using BlazorEcommerce.Client.Services.CartService; 8 | global using BlazorEcommerce.Client.Services.OrderService; 9 | global using BlazorEcommerce.Client.Services.AddressService; 10 | global using BlazorEcommerce.Client.Services.ProductTypeService; 11 | using BlazorEcommerce.Client; 12 | using Microsoft.AspNetCore.Components.Web; 13 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 14 | using Blazored.LocalStorage; 15 | using BlazorEcommerce.Client.Services.ProductTypeService; 16 | using MudBlazor.Services; 17 | 18 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 19 | builder.RootComponents.Add("#app"); 20 | builder.RootComponents.Add("head::after"); 21 | 22 | builder.Services.AddBlazoredLocalStorage(); 23 | builder.Services.AddMudServices(); 24 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 25 | builder.Services.AddScoped(); 26 | builder.Services.AddScoped(); 27 | builder.Services.AddScoped(); 28 | builder.Services.AddScoped(); 29 | builder.Services.AddScoped(); 30 | builder.Services.AddScoped(); 31 | builder.Services.AddScoped(); 32 | builder.Services.AddOptions(); 33 | builder.Services.AddAuthorizationCore(); 34 | builder.Services.AddScoped(); 35 | 36 | await builder.Build().RunAsync(); 37 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211205212215_CategoryFlags.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class CategoryFlags : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.AddColumn( 12 | name: "Deleted", 13 | table: "Categories", 14 | type: "bit", 15 | nullable: false, 16 | defaultValue: false); 17 | 18 | migrationBuilder.AddColumn( 19 | name: "Visible", 20 | table: "Categories", 21 | type: "bit", 22 | nullable: false, 23 | defaultValue: false); 24 | 25 | migrationBuilder.UpdateData( 26 | table: "Categories", 27 | keyColumn: "Id", 28 | keyValue: 1, 29 | column: "Visible", 30 | value: true); 31 | 32 | migrationBuilder.UpdateData( 33 | table: "Categories", 34 | keyColumn: "Id", 35 | keyValue: 2, 36 | column: "Visible", 37 | value: true); 38 | 39 | migrationBuilder.UpdateData( 40 | table: "Categories", 41 | keyColumn: "Id", 42 | keyValue: 3, 43 | column: "Visible", 44 | value: true); 45 | } 46 | 47 | protected override void Down(MigrationBuilder migrationBuilder) 48 | { 49 | migrationBuilder.DropColumn( 50 | name: "Deleted", 51 | table: "Categories"); 52 | 53 | migrationBuilder.DropColumn( 54 | name: "Visible", 55 | table: "Categories"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/OrderService/OrderService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace BlazorEcommerce.Client.Services.OrderService 4 | { 5 | public class OrderService : IOrderService 6 | { 7 | private readonly HttpClient _http; 8 | private readonly AuthenticationStateProvider _authStateProvider; 9 | private readonly NavigationManager _navigationManager; 10 | 11 | public OrderService(HttpClient http, 12 | AuthenticationStateProvider authStateProvider, 13 | NavigationManager navigationManager) 14 | { 15 | _http = http; 16 | _authStateProvider = authStateProvider; 17 | _navigationManager = navigationManager; 18 | } 19 | 20 | public async Task GetOrderDetails(int orderId) 21 | { 22 | var result = await _http.GetFromJsonAsync>($"api/order/{orderId}"); 23 | return result.Data; 24 | } 25 | 26 | public async Task> GetOrders() 27 | { 28 | var result = await _http.GetFromJsonAsync>>("api/order"); 29 | return result.Data; 30 | } 31 | 32 | public async Task PlaceOrder() 33 | { 34 | if (await IsUserAuthenticated()) 35 | { 36 | var result = await _http.PostAsync("api/payment/checkout", null); 37 | var url = await result.Content.ReadAsStringAsync(); 38 | return url; 39 | } 40 | else 41 | { 42 | return "login"; 43 | } 44 | } 45 | private async Task IsUserAuthenticated() 46 | { 47 | return (await _authStateProvider.GetAuthenticationStateAsync()).User.Identity.IsAuthenticated; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/UserButton.razor: -------------------------------------------------------------------------------- 1 | @inject ILocalStorageService LocalStorage 2 | @inject AuthenticationStateProvider AuthenticationStateProvider 3 | @inject NavigationManager NavigationManager 4 | @inject ICartService CartService 5 | 6 | 28 | 29 | @code { 30 | private bool showUserMenu = false; 31 | 32 | private string UserMenuCssClass => showUserMenu ? "show-menu" : null; 33 | 34 | private void ToggleUserMenu() 35 | { 36 | showUserMenu = !showUserMenu; 37 | } 38 | 39 | private async Task HideUserMenu() 40 | { 41 | await Task.Delay(200); 42 | showUserMenu = false; 43 | } 44 | 45 | private async Task Logout() 46 | { 47 | await LocalStorage.RemoveItemAsync("authToken"); 48 | await CartService.GetCartItemsCount(); 49 | await AuthenticationStateProvider.GetAuthenticationStateAsync(); 50 | NavigationManager.NavigateTo(""); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/CategoryController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace BlazorEcommerce.Server.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class CategoryController : ControllerBase 10 | { 11 | private readonly ICategoryService _categoryService; 12 | 13 | public CategoryController(ICategoryService categoryService) 14 | { 15 | _categoryService = categoryService; 16 | } 17 | 18 | [HttpGet] 19 | public async Task>>> GetCategories() 20 | { 21 | var result = await _categoryService.GetCategories(); 22 | return Ok(result); 23 | } 24 | 25 | [HttpGet("admin"), Authorize(Roles = "Admin")] 26 | public async Task>>> GetAdminCategories() 27 | { 28 | var result = await _categoryService.GetAdminCategories(); 29 | return Ok(result); 30 | } 31 | 32 | [HttpDelete("admin/{id}"), Authorize(Roles = "Admin")] 33 | public async Task>>> DeleteCategory(int id) 34 | { 35 | var result = await _categoryService.DeleteCategory(id); 36 | return Ok(result); 37 | } 38 | 39 | [HttpPost("admin"), Authorize(Roles = "Admin")] 40 | public async Task>>> AddCategory(Category category) 41 | { 42 | var result = await _categoryService.AddCategory(category); 43 | return Ok(result); 44 | } 45 | 46 | [HttpPut("admin"), Authorize(Roles = "Admin")] 47 | public async Task>>> UpdateCategory(Category category) 48 | { 49 | var result = await _categoryService.UpdateCategory(category); 50 | return Ok(result); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System.Security.Claims; 5 | 6 | namespace BlazorEcommerce.Server.Controllers 7 | { 8 | [Route("api/[controller]")] 9 | [ApiController] 10 | public class AuthController : ControllerBase 11 | { 12 | private readonly IAuthService _authService; 13 | 14 | public AuthController(IAuthService authService) 15 | { 16 | _authService = authService; 17 | } 18 | 19 | [HttpPost("register")] 20 | public async Task>> Register(UserRegister request) 21 | { 22 | var response = await _authService.Register( 23 | new User 24 | { 25 | Email = request.Email 26 | }, 27 | request.Password); 28 | 29 | if (!response.Success) 30 | { 31 | return BadRequest(response); 32 | } 33 | 34 | return Ok(response); 35 | } 36 | 37 | [HttpPost("login")] 38 | public async Task>> Login(UserLogin request) 39 | { 40 | var response = await _authService.Login(request.Email, request.Password); 41 | if (!response.Success) 42 | { 43 | return BadRequest(response); 44 | } 45 | 46 | return Ok(response); 47 | } 48 | 49 | [HttpPost("change-password"), Authorize] 50 | public async Task>> ChangePassword([FromBody] string newPassword) 51 | { 52 | var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); 53 | var response = await _authService.ChangePassword(int.Parse(userId), newPassword); 54 | 55 | if (!response.Success) 56 | { 57 | return BadRequest(response); 58 | } 59 | 60 | return Ok(response); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211112205817_CreateInitial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using BlazorEcommerce.Server.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorEcommerce.Server.Migrations 12 | { 13 | [DbContext(typeof(DataContext))] 14 | [Migration("20211112205817_CreateInitial")] 15 | partial class CreateInitial 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "6.0.0") 22 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 23 | 24 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); 25 | 26 | modelBuilder.Entity("BlazorEcommerce.Shared.Product", b => 27 | { 28 | b.Property("Id") 29 | .ValueGeneratedOnAdd() 30 | .HasColumnType("int"); 31 | 32 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); 33 | 34 | b.Property("Description") 35 | .IsRequired() 36 | .HasColumnType("nvarchar(max)"); 37 | 38 | b.Property("ImageUrl") 39 | .IsRequired() 40 | .HasColumnType("nvarchar(max)"); 41 | 42 | b.Property("Price") 43 | .HasColumnType("decimal(18,2)"); 44 | 45 | b.Property("Title") 46 | .IsRequired() 47 | .HasColumnType("nvarchar(max)"); 48 | 49 | b.HasKey("Id"); 50 | 51 | b.ToTable("Products"); 52 | }); 53 | #pragma warning restore 612, 618 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Login.razor: -------------------------------------------------------------------------------- 1 | @page "/login" 2 | @using Microsoft.AspNetCore.WebUtilities 3 | @inject IAuthService AuthService 4 | @inject ILocalStorageService LocalStorage 5 | @inject NavigationManager NavigationManager 6 | @inject AuthenticationStateProvider AuthenticationStateProvider 7 | @inject ICartService CartService 8 | 9 | Login 10 | 11 |

Login

12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 | @errorMessage 29 |
30 | 31 | @code { 32 | private UserLogin user = new UserLogin(); 33 | 34 | private string errorMessage = string.Empty; 35 | 36 | private string returnUrl = string.Empty; 37 | 38 | protected override void OnInitialized() 39 | { 40 | var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri); 41 | if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("returnUrl", out var url)) 42 | { 43 | returnUrl = url; 44 | } 45 | } 46 | 47 | private async Task HandleLogin() 48 | { 49 | var result = await AuthService.Login(user); 50 | if (result.Success) 51 | { 52 | errorMessage = string.Empty; 53 | 54 | await LocalStorage.SetItemAsync("authToken", result.Data); 55 | await AuthenticationStateProvider.GetAuthenticationStateAsync(); 56 | await CartService.StoreCartItems(true); 57 | await CartService.GetCartItemsCount(); 58 | NavigationManager.NavigateTo(returnUrl); 59 | } 60 | else 61 | { 62 | errorMessage = result.Message; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /BlazorEcommerce.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorEcommerce.Server", "BlazorEcommerce\Server\BlazorEcommerce.Server.csproj", "{0F0FC238-1551-45A7-BB93-2848E45D1765}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorEcommerce.Client", "BlazorEcommerce\Client\BlazorEcommerce.Client.csproj", "{4822AE95-A67F-4E5D-9829-207876DD1F0E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorEcommerce.Shared", "BlazorEcommerce\Shared\BlazorEcommerce.Shared.csproj", "{A0446615-92FA-4B59-A78B-FF568B990EF0}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {0F0FC238-1551-45A7-BB93-2848E45D1765}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {0F0FC238-1551-45A7-BB93-2848E45D1765}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {0F0FC238-1551-45A7-BB93-2848E45D1765}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {0F0FC238-1551-45A7-BB93-2848E45D1765}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {4822AE95-A67F-4E5D-9829-207876DD1F0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {4822AE95-A67F-4E5D-9829-207876DD1F0E}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {4822AE95-A67F-4E5D-9829-207876DD1F0E}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {4822AE95-A67F-4E5D-9829-207876DD1F0E}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {A0446615-92FA-4B59-A78B-FF568B990EF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {A0446615-92FA-4B59-A78B-FF568B990EF0}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {A0446615-92FA-4B59-A78B-FF568B990EF0}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A0446615-92FA-4B59-A78B-FF568B990EF0}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {A9E3FD42-1120-4D6E-8C2F-E94DA60A1651} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211204205151_UserAddress.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class UserAddress : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Addresses", 13 | columns: table => new 14 | { 15 | Id = table.Column(type: "int", nullable: false) 16 | .Annotation("SqlServer:Identity", "1, 1"), 17 | UserId = table.Column(type: "int", nullable: false), 18 | FirstName = table.Column(type: "nvarchar(max)", nullable: false), 19 | LastName = table.Column(type: "nvarchar(max)", nullable: false), 20 | Street = table.Column(type: "nvarchar(max)", nullable: false), 21 | City = table.Column(type: "nvarchar(max)", nullable: false), 22 | State = table.Column(type: "nvarchar(max)", nullable: false), 23 | Zip = table.Column(type: "nvarchar(max)", nullable: false), 24 | Country = table.Column(type: "nvarchar(max)", nullable: false) 25 | }, 26 | constraints: table => 27 | { 28 | table.PrimaryKey("PK_Addresses", x => x.Id); 29 | table.ForeignKey( 30 | name: "FK_Addresses_Users_UserId", 31 | column: x => x.UserId, 32 | principalTable: "Users", 33 | principalColumn: "Id", 34 | onDelete: ReferentialAction.Cascade); 35 | }); 36 | 37 | migrationBuilder.CreateIndex( 38 | name: "IX_Addresses_UserId", 39 | table: "Addresses", 40 | column: "UserId", 41 | unique: true); 42 | } 43 | 44 | protected override void Down(MigrationBuilder migrationBuilder) 45 | { 46 | migrationBuilder.DropTable( 47 | name: "Addresses"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/CartController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Security.Claims; 4 | 5 | namespace BlazorEcommerce.Server.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class CartController : ControllerBase 10 | { 11 | private readonly ICartService _cartService; 12 | 13 | public CartController(ICartService cartService) 14 | { 15 | _cartService = cartService; 16 | } 17 | 18 | [HttpPost("products")] 19 | public async Task>>> GetCartProducts(List cartItems) 20 | { 21 | var result = await _cartService.GetCartProducts(cartItems); 22 | return Ok(result); 23 | } 24 | 25 | [HttpPost] 26 | public async Task>>> StoreCartItems(List cartItems) 27 | { 28 | var result = await _cartService.StoreCartItems(cartItems); 29 | return Ok(result); 30 | } 31 | 32 | [HttpPost("add")] 33 | public async Task>> AddToCart(CartItem cartItem) 34 | { 35 | var result = await _cartService.AddToCart(cartItem); 36 | return Ok(result); 37 | } 38 | 39 | [HttpPut("update-quantity")] 40 | public async Task>> UpdateQuantity(CartItem cartItem) 41 | { 42 | var result = await _cartService.UpdateQuantity(cartItem); 43 | return Ok(result); 44 | } 45 | 46 | [HttpDelete("{productId}/{productTypeId}")] 47 | public async Task>> RemoveItemFromCart(int productId, int productTypeId) 48 | { 49 | var result = await _cartService.RemoveItemFromCart(productId, productTypeId); 50 | return Ok(result); 51 | } 52 | 53 | [HttpGet("count")] 54 | public async Task>> GetCartItemsCount() 55 | { 56 | return await _cartService.GetCartItemsCount(); 57 | } 58 | 59 | [HttpGet] 60 | public async Task>>> GetDbCartProducts() 61 | { 62 | var result = await _cartService.GetDbCartProducts(); 63 | return Ok(result); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/ProductList.razor: -------------------------------------------------------------------------------- 1 | @inject IProductService ProductService 2 | @implements IDisposable 3 | 4 | @if (ProductService.Products == null || ProductService.Products.Count == 0) 5 | { 6 | @ProductService.Message 7 | } 8 | else 9 | { 10 | 38 | for (var i = 1; i <= ProductService.PageCount; i++) 39 | { 40 | @i 44 | } 45 | } 46 | 47 | @code { 48 | protected override void OnInitialized() 49 | { 50 | ProductService.ProductsChanged += StateHasChanged; 51 | } 52 | 53 | public void Dispose() 54 | { 55 | ProductService.ProductsChanged -= StateHasChanged; 56 | } 57 | 58 | private string GetPriceText(Product product) 59 | { 60 | var variants = product.Variants; 61 | if (variants.Count == 0) 62 | { 63 | return string.Empty; 64 | } 65 | else if (variants.Count == 1) 66 | { 67 | return $"${variants[0].Price}"; 68 | } 69 | decimal minPrice = variants.Min(v => v.Price); 70 | return $"Starting at ${minPrice}"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Admin/ProductTypes.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/product-types" 2 | @inject IProductTypeService ProductTypeService 3 | @implements IDisposable 4 | @attribute [Authorize(Roles = "Admin")] 5 | 6 |

Product Types

7 | 8 | @foreach (var productType in ProductTypeService.ProductTypes) 9 | { 10 | @if (productType.Editing) 11 | { 12 | 13 |
14 |
15 | 16 |
17 |
18 | 21 |
22 |
23 |
24 | } 25 | else 26 | { 27 |
28 |
29 | @productType.Name 30 |
31 |
32 | 35 |
36 |
37 | } 38 | } 39 | 42 | 43 | @code { 44 | ProductType editingProductType = null; 45 | 46 | protected override async Task OnInitializedAsync() 47 | { 48 | await ProductTypeService.GetProductTypes(); 49 | ProductTypeService.OnChange += StateHasChanged; 50 | } 51 | 52 | public void Dispose() 53 | { 54 | ProductTypeService.OnChange -= StateHasChanged; 55 | } 56 | 57 | private void EditProductType(ProductType productType) 58 | { 59 | productType.Editing = true; 60 | editingProductType = productType; 61 | } 62 | 63 | private void CreateNewProductType() 64 | { 65 | editingProductType = ProductTypeService.CreateNewProductType(); 66 | } 67 | 68 | private async Task UpdateProductType() 69 | { 70 | if (editingProductType.IsNew) 71 | await ProductTypeService.AddProductType(editingProductType); 72 | else 73 | await ProductTypeService.UpdateProductType(editingProductType); 74 | editingProductType = new ProductType(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/CategoryService/CategoryService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.CategoryService 2 | { 3 | public class CategoryService : ICategoryService 4 | { 5 | private readonly HttpClient _http; 6 | 7 | public CategoryService(HttpClient http) 8 | { 9 | _http = http; 10 | } 11 | 12 | public List Categories { get; set; } = new List(); 13 | public List AdminCategories { get; set; } = new List(); 14 | 15 | public event Action OnChange; 16 | 17 | public async Task AddCategory(Category category) 18 | { 19 | var response = await _http.PostAsJsonAsync("api/Category/admin", category); 20 | AdminCategories = (await response.Content 21 | .ReadFromJsonAsync>>()).Data; 22 | await GetCategories(); 23 | OnChange.Invoke(); 24 | } 25 | 26 | public Category CreateNewCategory() 27 | { 28 | var newCategory = new Category { IsNew = true, Editing = true }; 29 | AdminCategories.Add(newCategory); 30 | OnChange.Invoke(); 31 | return newCategory; 32 | } 33 | 34 | public async Task DeleteCategory(int categoryId) 35 | { 36 | var response = await _http.DeleteAsync($"api/Category/admin/{categoryId}"); 37 | AdminCategories = (await response.Content 38 | .ReadFromJsonAsync>>()).Data; 39 | await GetCategories(); 40 | OnChange.Invoke(); 41 | } 42 | 43 | public async Task GetAdminCategories() 44 | { 45 | var response = await _http.GetFromJsonAsync>>("api/Category/admin"); 46 | if (response != null && response.Data != null) 47 | AdminCategories = response.Data; 48 | } 49 | 50 | public async Task GetCategories() 51 | { 52 | var response = await _http.GetFromJsonAsync>>("api/Category"); 53 | if (response != null && response.Data != null) 54 | Categories = response.Data; 55 | } 56 | 57 | public async Task UpdateCategory(Category category) 58 | { 59 | var response = await _http.PutAsJsonAsync("api/Category/admin", category); 60 | AdminCategories = (await response.Content 61 | .ReadFromJsonAsync>>()).Data; 62 | await GetCategories(); 63 | OnChange.Invoke(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/CustomAuthStateProvider.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using System.Net.Http.Headers; 4 | using System.Security.Claims; 5 | using System.Text.Json; 6 | 7 | namespace BlazorEcommerce.Client 8 | { 9 | public class CustomAuthStateProvider : AuthenticationStateProvider 10 | { 11 | private readonly ILocalStorageService _localStorageService; 12 | private readonly HttpClient _http; 13 | 14 | public CustomAuthStateProvider(ILocalStorageService localStorageService, HttpClient http) 15 | { 16 | _localStorageService = localStorageService; 17 | _http = http; 18 | } 19 | 20 | public override async Task GetAuthenticationStateAsync() 21 | { 22 | string authToken = await _localStorageService.GetItemAsStringAsync("authToken"); 23 | 24 | var identity = new ClaimsIdentity(); 25 | _http.DefaultRequestHeaders.Authorization = null; 26 | 27 | if (!string.IsNullOrEmpty(authToken)) 28 | { 29 | try 30 | { 31 | identity = new ClaimsIdentity(ParseClaimsFromJwt(authToken), "jwt"); 32 | _http.DefaultRequestHeaders.Authorization = 33 | new AuthenticationHeaderValue("Bearer", authToken.Replace("\"", "")); 34 | } 35 | catch 36 | { 37 | await _localStorageService.RemoveItemAsync("authToken"); 38 | identity = new ClaimsIdentity(); 39 | } 40 | } 41 | 42 | var user = new ClaimsPrincipal(identity); 43 | var state = new AuthenticationState(user); 44 | 45 | NotifyAuthenticationStateChanged(Task.FromResult(state)); 46 | 47 | return state; 48 | } 49 | 50 | private byte[] ParseBase64WithoutPadding(string base64) 51 | { 52 | switch (base64.Length % 4) 53 | { 54 | case 2: base64 += "=="; break; 55 | case 3: base64 += "="; break; 56 | } 57 | return Convert.FromBase64String(base64); 58 | } 59 | 60 | private IEnumerable ParseClaimsFromJwt(string jwt) 61 | { 62 | var payload = jwt.Split('.')[1]; 63 | var jsonBytes = ParseBase64WithoutPadding(payload); 64 | var keyValuePairs = JsonSerializer 65 | .Deserialize>(jsonBytes); 66 | 67 | var claims = keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())); 68 | 69 | return claims; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Admin/Products.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/products" 2 | @inject IProductService ProductService 3 | @inject NavigationManager NavigationManager 4 | @attribute [Authorize(Roles = "Admin")] 5 | 6 |

Products

7 | 8 | @if (ProductService.AdminProducts == null) 9 | { 10 | Loading Products... 11 | } 12 | else 13 | { 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 41 | 42 | 50 | 58 | 59 | 64 | 65 | 66 | 67 |
ProductVariantPriceVisible
32 | @if (!string.IsNullOrEmpty(product.ImageUrl)) 33 | { 34 | 35 | } 36 | else if (product.Images.Count > 0) 37 | { 38 | 39 | } 40 | @product.Title 43 | @foreach (var variant in product.Variants) 44 | { 45 | @variant.ProductType.Name 46 | 47 |
48 | } 49 |
51 | @foreach (var variant in product.Variants) 52 | { 53 | @variant.Price 54 | 55 |
56 | } 57 |
@(product.Visible ? "✔️" : "") 60 | 63 |
68 | } 69 | 70 | @code { 71 | protected override async Task OnInitializedAsync() 72 | { 73 | await ProductService.GetAdminProducts(); 74 | } 75 | 76 | void EditProduct(int productId) 77 | { 78 | NavigationManager.NavigateTo($"admin/product/{productId}"); 79 | } 80 | 81 | void CreateProduct() 82 | { 83 | NavigationManager.NavigateTo("admin/product"); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Shared/AddressForm.razor: -------------------------------------------------------------------------------- 1 | @inject IAddressService AddressService 2 | 3 | @if (address == null) 4 | { 5 | 6 | You haven't specified a delivery address, yet. 7 | 8 | 9 | } 10 | else if (!editAddress) 11 | { 12 |

13 | @address.FirstName @address.LastName
14 | @address.Street
15 | @address.City, @address.State, @address.Zip
16 | @address.Country
17 |

18 | 19 | } 20 | else 21 | { 22 | 23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 |
51 | 52 |
53 | } 54 | 55 | @code { 56 | Address address = null; 57 | bool editAddress = false; 58 | 59 | protected override async Task OnInitializedAsync() 60 | { 61 | address = await AddressService.GetAddress(); 62 | } 63 | 64 | private async Task SubmitAddress() 65 | { 66 | editAddress = false; 67 | address = await AddressService.AddOrUpdateAddress(address); 68 | } 69 | 70 | private void InitAddress() 71 | { 72 | address = new Address(); 73 | editAddress = true; 74 | } 75 | 76 | private void EditAddress() 77 | { 78 | editAddress = true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/CategoryService/CategoryService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Server.Services.CategoryService 2 | { 3 | public class CategoryService : ICategoryService 4 | { 5 | private readonly DataContext _context; 6 | 7 | public CategoryService(DataContext context) 8 | { 9 | _context = context; 10 | } 11 | 12 | public async Task>> AddCategory(Category category) 13 | { 14 | category.Editing = category.IsNew = false; 15 | _context.Categories.Add(category); 16 | await _context.SaveChangesAsync(); 17 | return await GetAdminCategories(); 18 | } 19 | 20 | public async Task>> DeleteCategory(int id) 21 | { 22 | Category category = await GetCategoryById(id); 23 | if (category == null) 24 | { 25 | return new ServiceResponse> 26 | { 27 | Success = false, 28 | Message = "Category not found." 29 | }; 30 | } 31 | 32 | category.Deleted = true; 33 | await _context.SaveChangesAsync(); 34 | 35 | return await GetAdminCategories(); 36 | } 37 | 38 | private async Task GetCategoryById(int id) 39 | { 40 | return await _context.Categories.FirstOrDefaultAsync(c => c.Id == id); 41 | } 42 | 43 | public async Task>> GetAdminCategories() 44 | { 45 | var categories = await _context.Categories 46 | .Where(c => !c.Deleted) 47 | .ToListAsync(); 48 | return new ServiceResponse> 49 | { 50 | Data = categories 51 | }; 52 | } 53 | 54 | public async Task>> GetCategories() 55 | { 56 | var categories = await _context.Categories 57 | .Where(c => !c.Deleted && c.Visible) 58 | .ToListAsync(); 59 | return new ServiceResponse> 60 | { 61 | Data = categories 62 | }; 63 | } 64 | 65 | public async Task>> UpdateCategory(Category category) 66 | { 67 | var dbCategory = await GetCategoryById(category.Id); 68 | if (dbCategory == null) 69 | { 70 | return new ServiceResponse> 71 | { 72 | Success = false, 73 | Message = "Category not found." 74 | }; 75 | } 76 | 77 | dbCategory.Name = category.Name; 78 | dbCategory.Url = category.Url; 79 | dbCategory.Visible = category.Visible; 80 | 81 | await _context.SaveChangesAsync(); 82 | 83 | return await GetAdminCategories(); 84 | 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Program.cs: -------------------------------------------------------------------------------- 1 | global using BlazorEcommerce.Shared; 2 | global using Microsoft.EntityFrameworkCore; 3 | global using BlazorEcommerce.Server.Data; 4 | global using BlazorEcommerce.Server.Services.ProductService; 5 | global using BlazorEcommerce.Server.Services.CategoryService; 6 | global using BlazorEcommerce.Server.Services.CartService; 7 | global using BlazorEcommerce.Server.Services.AuthService; 8 | global using BlazorEcommerce.Server.Services.OrderService; 9 | global using BlazorEcommerce.Server.Services.PaymentService; 10 | global using BlazorEcommerce.Server.Services.AddressService; 11 | global using BlazorEcommerce.Server.Services.ProductTypeService; 12 | using Microsoft.AspNetCore.ResponseCompression; 13 | using Microsoft.AspNetCore.Authentication.JwtBearer; 14 | using Microsoft.IdentityModel.Tokens; 15 | 16 | var builder = WebApplication.CreateBuilder(args); 17 | 18 | // Add services to the container. 19 | 20 | builder.Services.AddDbContext(options => 21 | { 22 | options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); 23 | }); 24 | builder.Services.AddControllersWithViews(); 25 | builder.Services.AddRazorPages(); 26 | 27 | builder.Services.AddEndpointsApiExplorer(); 28 | builder.Services.AddSwaggerGen(); 29 | 30 | builder.Services.AddScoped(); 31 | builder.Services.AddScoped(); 32 | builder.Services.AddScoped(); 33 | builder.Services.AddScoped(); 34 | builder.Services.AddScoped(); 35 | builder.Services.AddScoped(); 36 | builder.Services.AddScoped(); 37 | builder.Services.AddScoped(); 38 | builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 39 | .AddJwtBearer(options => 40 | { 41 | options.TokenValidationParameters = new TokenValidationParameters 42 | { 43 | ValidateIssuerSigningKey = true, 44 | IssuerSigningKey = 45 | new SymmetricSecurityKey(System.Text.Encoding.UTF8 46 | .GetBytes(builder.Configuration.GetSection("AppSettings:Token").Value)), 47 | ValidateIssuer = false, 48 | ValidateAudience = false 49 | }; 50 | }); 51 | builder.Services.AddHttpContextAccessor(); 52 | 53 | var app = builder.Build(); 54 | 55 | app.UseSwaggerUI(); 56 | 57 | // Configure the HTTP request pipeline. 58 | if (app.Environment.IsDevelopment()) 59 | { 60 | app.UseWebAssemblyDebugging(); 61 | } 62 | else 63 | { 64 | app.UseExceptionHandler("/Error"); 65 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 66 | app.UseHsts(); 67 | } 68 | 69 | app.UseSwagger(); 70 | app.UseHttpsRedirection(); 71 | 72 | app.UseBlazorFrameworkFiles(); 73 | app.UseStaticFiles(); 74 | 75 | app.UseRouting(); 76 | 77 | app.UseAuthentication(); 78 | app.UseAuthorization(); 79 | 80 | app.MapRazorPages(); 81 | app.MapControllers(); 82 | app.MapFallbackToFile("index.html"); 83 | 84 | app.Run(); 85 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | h1:focus { 8 | outline: none; 9 | } 10 | 11 | a, .btn-link { 12 | color: #0071c1; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .content { 22 | padding-top: 1.1rem; 23 | } 24 | 25 | .valid.modified:not([type=checkbox]) { 26 | outline: 1px solid #26b050; 27 | } 28 | 29 | .invalid { 30 | outline: 1px solid red; 31 | } 32 | 33 | .validation-message { 34 | color: red; 35 | } 36 | 37 | #blazor-error-ui { 38 | background: lightyellow; 39 | bottom: 0; 40 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 41 | display: none; 42 | left: 0; 43 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 44 | position: fixed; 45 | width: 100%; 46 | z-index: 1000; 47 | } 48 | 49 | #blazor-error-ui .dismiss { 50 | cursor: pointer; 51 | position: absolute; 52 | right: 0.75rem; 53 | top: 0.5rem; 54 | } 55 | 56 | .blazor-error-boundary { 57 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 58 | padding: 1rem 1rem 1rem 3.7rem; 59 | color: white; 60 | } 61 | 62 | .blazor-error-boundary::after { 63 | content: "An error has occurred." 64 | } 65 | 66 | .price { 67 | color: green; 68 | } 69 | 70 | .media { 71 | display: flex; 72 | align-items: flex-start; 73 | } 74 | 75 | .media-body { 76 | flex: 1; 77 | } 78 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Cart.razor: -------------------------------------------------------------------------------- 1 | @page "/cart" 2 | @inject ICartService CartService 3 | @inject IOrderService OrderService 4 | @inject IAuthService AuthService 5 | @inject NavigationManager NavigationManager 6 | 7 | Shopping Cart 8 | 9 |

Shopping Cart

10 | 11 | @if (cartProducts == null || cartProducts.Count == 0) 12 | { 13 | @message 14 | } 15 | else 16 | { 17 |
18 | @foreach (var product in cartProducts) 19 | { 20 |
21 |
22 | 23 |
24 |
25 |
@product.Title
26 | @product.ProductType
27 | 31 | 35 |
36 |
$@(product.Price * product.Quantity)
37 |
38 | } 39 |
40 | Total (@cartProducts.Count): $@cartProducts.Sum(product => @product.Price * product.Quantity) 41 |
42 |
43 | @if (isAuthenticated) 44 | { 45 |
46 |
Delivery Address
47 | 48 |
49 | } 50 | 51 | } 52 | 53 | 54 | 55 | @code { 56 | List cartProducts = null; 57 | string message = "Loading cart..."; 58 | bool isAuthenticated = false; 59 | 60 | protected override async Task OnInitializedAsync() 61 | { 62 | isAuthenticated = await AuthService.IsUserAuthenticated(); 63 | await LoadCart(); 64 | } 65 | 66 | private async Task RemoveProductFromCart(int productId, int productTypeId) 67 | { 68 | await CartService.RemoveProductFromCart(productId, productTypeId); 69 | await LoadCart(); 70 | } 71 | 72 | private async Task LoadCart() 73 | { 74 | await CartService.GetCartItemsCount(); 75 | cartProducts = await CartService.GetCartProducts(); 76 | if (cartProducts == null || cartProducts.Count == 0) 77 | { 78 | message = "Your cart is empty."; 79 | } 80 | } 81 | 82 | private async Task UpdateQuantity(ChangeEventArgs e, CartProductResponse product) 83 | { 84 | product.Quantity = int.Parse(e.Value.ToString()); 85 | if (product.Quantity < 1) 86 | product.Quantity = 1; 87 | await CartService.UpdateQuantity(product); 88 | } 89 | 90 | private async Task PlaceOrder() 91 | { 92 | string url = await OrderService.PlaceOrder(); 93 | NavigationManager.NavigateTo(url); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Controllers/ProductController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace BlazorEcommerce.Server.Controllers 6 | { 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class ProductController : ControllerBase 10 | { 11 | private readonly IProductService _productService; 12 | 13 | public ProductController(IProductService productService) 14 | { 15 | _productService = productService; 16 | } 17 | 18 | [HttpGet("admin"), Authorize(Roles = "Admin")] 19 | public async Task>>> GetAdminProducts() 20 | { 21 | var result = await _productService.GetAdminProducts(); 22 | return Ok(result); 23 | } 24 | 25 | [HttpPost, Authorize(Roles = "Admin")] 26 | public async Task>> CreateProduct(Product product) 27 | { 28 | var result = await _productService.CreateProduct(product); 29 | return Ok(result); 30 | } 31 | 32 | [HttpPut, Authorize(Roles = "Admin")] 33 | public async Task>> UpdateProduct(Product product) 34 | { 35 | var result = await _productService.UpdateProduct(product); 36 | return Ok(result); 37 | } 38 | 39 | [HttpDelete("{id}"), Authorize(Roles = "Admin")] 40 | public async Task>> DeleteProduct(int id) 41 | { 42 | var result = await _productService.DeleteProduct(id); 43 | return Ok(result); 44 | } 45 | 46 | [HttpGet] 47 | public async Task>>> GetProducts() 48 | { 49 | var result = await _productService.GetProductsAsync(); 50 | return Ok(result); 51 | } 52 | 53 | [HttpGet("{productId}")] 54 | public async Task>> GetProduct(int productId) 55 | { 56 | var result = await _productService.GetProductAsync(productId); 57 | return Ok(result); 58 | } 59 | 60 | [HttpGet("category/{categoryUrl}")] 61 | public async Task>>> GetProductsByCategory(string categoryUrl) 62 | { 63 | var result = await _productService.GetProductsByCategory(categoryUrl); 64 | return Ok(result); 65 | } 66 | 67 | [HttpGet("search/{searchText}/{page}")] 68 | public async Task>> SearchProducts(string searchText, int page = 1) 69 | { 70 | var result = await _productService.SearchProducts(searchText, page); 71 | return Ok(result); 72 | } 73 | 74 | [HttpGet("searchsuggestions/{searchText}")] 75 | public async Task>>> GetProductSearchSuggestions(string searchText) 76 | { 77 | var result = await _productService.GetProductSearchSuggestions(searchText); 78 | return Ok(result); 79 | } 80 | 81 | [HttpGet("featured")] 82 | public async Task>>> GetFeaturedProducts() 83 | { 84 | var result = await _productService.GetFeaturedProducts(); 85 | return Ok(result); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211129201337_Orders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace BlazorEcommerce.Server.Migrations 7 | { 8 | public partial class Orders : Migration 9 | { 10 | protected override void Up(MigrationBuilder migrationBuilder) 11 | { 12 | migrationBuilder.CreateTable( 13 | name: "Orders", 14 | columns: table => new 15 | { 16 | Id = table.Column(type: "int", nullable: false) 17 | .Annotation("SqlServer:Identity", "1, 1"), 18 | UserId = table.Column(type: "int", nullable: false), 19 | OrderDate = table.Column(type: "datetime2", nullable: false), 20 | TotalPrice = table.Column(type: "decimal(18,2)", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Orders", x => x.Id); 25 | }); 26 | 27 | migrationBuilder.CreateTable( 28 | name: "OrderItems", 29 | columns: table => new 30 | { 31 | OrderId = table.Column(type: "int", nullable: false), 32 | ProductId = table.Column(type: "int", nullable: false), 33 | ProductTypeId = table.Column(type: "int", nullable: false), 34 | Quantity = table.Column(type: "int", nullable: false), 35 | TotalPrice = table.Column(type: "decimal(18,2)", nullable: false) 36 | }, 37 | constraints: table => 38 | { 39 | table.PrimaryKey("PK_OrderItems", x => new { x.OrderId, x.ProductId, x.ProductTypeId }); 40 | table.ForeignKey( 41 | name: "FK_OrderItems_Orders_OrderId", 42 | column: x => x.OrderId, 43 | principalTable: "Orders", 44 | principalColumn: "Id", 45 | onDelete: ReferentialAction.Cascade); 46 | table.ForeignKey( 47 | name: "FK_OrderItems_Products_ProductId", 48 | column: x => x.ProductId, 49 | principalTable: "Products", 50 | principalColumn: "Id", 51 | onDelete: ReferentialAction.Cascade); 52 | table.ForeignKey( 53 | name: "FK_OrderItems_ProductTypes_ProductTypeId", 54 | column: x => x.ProductTypeId, 55 | principalTable: "ProductTypes", 56 | principalColumn: "Id", 57 | onDelete: ReferentialAction.Cascade); 58 | }); 59 | 60 | migrationBuilder.CreateIndex( 61 | name: "IX_OrderItems_ProductId", 62 | table: "OrderItems", 63 | column: "ProductId"); 64 | 65 | migrationBuilder.CreateIndex( 66 | name: "IX_OrderItems_ProductTypeId", 67 | table: "OrderItems", 68 | column: "ProductTypeId"); 69 | } 70 | 71 | protected override void Down(MigrationBuilder migrationBuilder) 72 | { 73 | migrationBuilder.DropTable( 74 | name: "OrderItems"); 75 | 76 | migrationBuilder.DropTable( 77 | name: "Orders"); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/ProductDetails.razor: -------------------------------------------------------------------------------- 1 | @page "/product/{id:int}" 2 | @inject IProductService ProductService 3 | @inject ICartService CartService 4 | 5 | @if (product == null) 6 | { 7 | @message 8 | } 9 | else 10 | { 11 |
12 |
13 | @if (!string.IsNullOrEmpty(product.ImageUrl)) 14 | { 15 | @product.Title 16 | } 17 | else 18 | { 19 | 20 | 21 | > 22 | 23 | 24 | } 25 |
26 |
27 |

@product.Title

28 |

@product.Description

29 | @if (product.Variants != null && product.Variants.Count > 1) 30 | { 31 |
32 | 38 |
39 | } 40 | @if (GetSelectedVariant() != null) 41 | { 42 | @if (GetSelectedVariant().OriginalPrice > GetSelectedVariant().Price) 43 | { 44 |
45 | $@GetSelectedVariant().OriginalPrice 46 |
47 | } 48 |

49 | $@GetSelectedVariant().Price 50 |

51 | } 52 | 55 |
56 |
57 | 58 | } 59 | 60 | @code { 61 | private Product? product = null; 62 | private string message = string.Empty; 63 | private int currentTypeId = 1; 64 | 65 | [Parameter] 66 | public int Id { get; set; } 67 | 68 | protected override async Task OnParametersSetAsync() 69 | { 70 | message = "Loading product..."; 71 | 72 | var result = await ProductService.GetProduct(Id); 73 | if (!result.Success) 74 | { 75 | message = result.Message; 76 | } 77 | else 78 | { 79 | product = result.Data; 80 | if (product.Variants.Count > 0) 81 | { 82 | currentTypeId = product.Variants[0].ProductTypeId; 83 | } 84 | } 85 | } 86 | 87 | private ProductVariant GetSelectedVariant() 88 | { 89 | var variant = product.Variants.FirstOrDefault(v => v.ProductTypeId == currentTypeId); 90 | return variant; 91 | } 92 | 93 | private async Task AddToCart() 94 | { 95 | var productVariant = GetSelectedVariant(); 96 | var cartItem = new CartItem 97 | { 98 | ProductId = productVariant.ProductId, 99 | ProductTypeId = productVariant.ProductTypeId 100 | }; 101 | 102 | await CartService.AddToCart(cartItem); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211112210614_ProductSeeding.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class ProductSeeding : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.InsertData( 12 | table: "Products", 13 | columns: new[] { "Id", "Description", "ImageUrl", "Price", "Title" }, 14 | values: new object[] { 1, "The Hitchhiker's Guide to the Galaxy[note 1] (sometimes referred to as HG2G,[1] HHGTTG,[2] H2G2,[3] or tHGttG) is a comedy science fiction franchise created by Douglas Adams. Originally a 1978 radio comedy broadcast on BBC Radio 4, it was later adapted to other formats, including stage shows, novels, comic books, a 1981 TV series, a 1984 text-based computer game, and 2005 feature film.", "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg", 9.99m, "The Hitchhiker's Guide to the Galaxy" }); 15 | 16 | migrationBuilder.InsertData( 17 | table: "Products", 18 | columns: new[] { "Id", "Description", "ImageUrl", "Price", "Title" }, 19 | values: new object[] { 2, "Ready Player One is a 2011 science fiction novel, and the debut novel of American author Ernest Cline. The story, set in a dystopia in 2045, follows protagonist Wade Watts on his search for an Easter egg in a worldwide virtual reality game, the discovery of which would lead him to inherit the game creator's fortune. Cline sold the rights to publish the novel in June 2010, in a bidding war to the Crown Publishing Group (a division of Random House).[1] The book was published on August 16, 2011.[2] An audiobook was released the same day; it was narrated by Wil Wheaton, who was mentioned briefly in one of the chapters.[3][4]Ch. 20 In 2012, the book received an Alex Award from the Young Adult Library Services Association division of the American Library Association[5] and won the 2011 Prometheus Award.[6]", "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg", 7.99m, "Ready Player One" }); 20 | 21 | migrationBuilder.InsertData( 22 | table: "Products", 23 | columns: new[] { "Id", "Description", "ImageUrl", "Price", "Title" }, 24 | values: new object[] { 3, "Nineteen Eighty-Four (also stylised as 1984) is a dystopian social science fiction novel and cautionary tale written by English writer George Orwell. It was published on 8 June 1949 by Secker & Warburg as Orwell's ninth and final book completed in his lifetime. Thematically, it centres on the consequences of totalitarianism, mass surveillance and repressive regimentation of people and behaviours within society.[2][3] Orwell, a democratic socialist, modelled the totalitarian government in the novel after Stalinist Russia and Nazi Germany.[2][3][4] More broadly, the novel examines the role of truth and facts within politics and the ways in which they are manipulated.", "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg", 6.99m, "Nineteen Eighty-Four" }); 25 | } 26 | 27 | protected override void Down(MigrationBuilder migrationBuilder) 28 | { 29 | migrationBuilder.DeleteData( 30 | table: "Products", 31 | keyColumn: "Id", 32 | keyValue: 1); 33 | 34 | migrationBuilder.DeleteData( 35 | table: "Products", 36 | keyColumn: "Id", 37 | keyValue: 2); 38 | 39 | migrationBuilder.DeleteData( 40 | table: "Products", 41 | keyColumn: "Id", 42 | keyValue: 3); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/PaymentService/PaymentService.cs: -------------------------------------------------------------------------------- 1 | using Stripe; 2 | using Stripe.Checkout; 3 | 4 | namespace BlazorEcommerce.Server.Services.PaymentService 5 | { 6 | public class PaymentService : IPaymentService 7 | { 8 | private readonly ICartService _cartService; 9 | private readonly IAuthService _authService; 10 | private readonly IOrderService _orderService; 11 | 12 | const string secret = "whsec_cq1WH9CX9U1zxU9EpbBVvWOhfb6e5ysR"; 13 | 14 | public PaymentService(ICartService cartService, 15 | IAuthService authService, 16 | IOrderService orderService) 17 | { 18 | StripeConfiguration.ApiKey = "sk_test_51HnFFuJWja1dketA1LY3VQds3XWpByD5GE8laKrxyNldWKnXXdktvITJiG3PYNDMwpSkrAv33d7JjvHDEUGPPo2E00vkDMlVIb"; 19 | 20 | _cartService = cartService; 21 | _authService = authService; 22 | _orderService = orderService; 23 | } 24 | 25 | public async Task CreateCheckoutSession() 26 | { 27 | var products = (await _cartService.GetDbCartProducts()).Data; 28 | var lineItems = new List(); 29 | products.ForEach(product => lineItems.Add(new SessionLineItemOptions 30 | { 31 | PriceData = new SessionLineItemPriceDataOptions 32 | { 33 | UnitAmountDecimal = product.Price * 100, 34 | Currency = "usd", 35 | ProductData = new SessionLineItemPriceDataProductDataOptions 36 | { 37 | Name = product.Title, 38 | Images = new List { product.ImageUrl } 39 | } 40 | }, 41 | Quantity = product.Quantity 42 | })); 43 | 44 | var options = new SessionCreateOptions 45 | { 46 | CustomerEmail = _authService.GetUserEmail(), 47 | ShippingAddressCollection = 48 | new SessionShippingAddressCollectionOptions 49 | { 50 | AllowedCountries = new List { "US" } 51 | }, 52 | PaymentMethodTypes = new List 53 | { 54 | "card" 55 | }, 56 | LineItems = lineItems, 57 | Mode = "payment", 58 | SuccessUrl = "https://localhost:7226/order-success", 59 | CancelUrl = "https://localhost:7226/cart" 60 | }; 61 | 62 | var service = new SessionService(); 63 | Session session = service.Create(options); 64 | return session; 65 | } 66 | 67 | public async Task> FulfillOrder(HttpRequest request) 68 | { 69 | var json = await new StreamReader(request.Body).ReadToEndAsync(); 70 | try 71 | { 72 | var stripeEvent = EventUtility.ConstructEvent( 73 | json, 74 | request.Headers["Stripe-Signature"], 75 | secret 76 | ); 77 | 78 | if (stripeEvent.Type == Events.CheckoutSessionCompleted) 79 | { 80 | var session = stripeEvent.Data.Object as Session; 81 | var user = await _authService.GetUserByEmail(session.CustomerEmail); 82 | await _orderService.PlaceOrder(user.Id); 83 | } 84 | 85 | return new ServiceResponse { Data = true }; 86 | } 87 | catch (StripeException e) 88 | { 89 | return new ServiceResponse { Data = false, Success = false, Message = e.Message }; 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211114114952_Categories.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class Categories : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.AddColumn( 12 | name: "CategoryId", 13 | table: "Products", 14 | type: "int", 15 | nullable: false, 16 | defaultValue: 0); 17 | 18 | migrationBuilder.CreateTable( 19 | name: "Categories", 20 | columns: table => new 21 | { 22 | Id = table.Column(type: "int", nullable: false) 23 | .Annotation("SqlServer:Identity", "1, 1"), 24 | Name = table.Column(type: "nvarchar(max)", nullable: false), 25 | Url = table.Column(type: "nvarchar(max)", nullable: false) 26 | }, 27 | constraints: table => 28 | { 29 | table.PrimaryKey("PK_Categories", x => x.Id); 30 | }); 31 | 32 | migrationBuilder.InsertData( 33 | table: "Categories", 34 | columns: new[] { "Id", "Name", "Url" }, 35 | values: new object[] { 1, "Books", "books" }); 36 | 37 | migrationBuilder.InsertData( 38 | table: "Categories", 39 | columns: new[] { "Id", "Name", "Url" }, 40 | values: new object[] { 2, "Movies", "movies" }); 41 | 42 | migrationBuilder.InsertData( 43 | table: "Categories", 44 | columns: new[] { "Id", "Name", "Url" }, 45 | values: new object[] { 3, "Video Games", "video-games" }); 46 | 47 | migrationBuilder.UpdateData( 48 | table: "Products", 49 | keyColumn: "Id", 50 | keyValue: 1, 51 | column: "CategoryId", 52 | value: 1); 53 | 54 | migrationBuilder.UpdateData( 55 | table: "Products", 56 | keyColumn: "Id", 57 | keyValue: 2, 58 | column: "CategoryId", 59 | value: 1); 60 | 61 | migrationBuilder.UpdateData( 62 | table: "Products", 63 | keyColumn: "Id", 64 | keyValue: 3, 65 | column: "CategoryId", 66 | value: 1); 67 | 68 | migrationBuilder.CreateIndex( 69 | name: "IX_Products_CategoryId", 70 | table: "Products", 71 | column: "CategoryId"); 72 | 73 | migrationBuilder.AddForeignKey( 74 | name: "FK_Products_Categories_CategoryId", 75 | table: "Products", 76 | column: "CategoryId", 77 | principalTable: "Categories", 78 | principalColumn: "Id", 79 | onDelete: ReferentialAction.Cascade); 80 | } 81 | 82 | protected override void Down(MigrationBuilder migrationBuilder) 83 | { 84 | migrationBuilder.DropForeignKey( 85 | name: "FK_Products_Categories_CategoryId", 86 | table: "Products"); 87 | 88 | migrationBuilder.DropTable( 89 | name: "Categories"); 90 | 91 | migrationBuilder.DropIndex( 92 | name: "IX_Products_CategoryId", 93 | table: "Products"); 94 | 95 | migrationBuilder.DropColumn( 96 | name: "CategoryId", 97 | table: "Products"); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Pages/Admin/Categories.razor: -------------------------------------------------------------------------------- 1 | @page "/admin/categories" 2 | @inject ICategoryService CategoryService 3 | @attribute [Authorize(Roles = "Admin")] 4 | @implements IDisposable 5 | 6 |

Categories

7 | 8 |
9 |
Name
10 |
Url
11 |
Visible
12 |
13 |
14 | 15 | @foreach (var category in CategoryService.AdminCategories) 16 | { 17 | @if (category.Editing) 18 | { 19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 34 | 37 |
38 |
39 |
40 | } 41 | else 42 | { 43 |
44 |
45 | @category.Name 46 |
47 |
48 | @category.Url 49 |
50 |
51 | @(category.Visible ? "✔️" : "") 52 |
53 |
54 | 57 | 60 |
61 |
62 | } 63 | } 64 | 67 | 68 | @code { 69 | Category editingCategory = null; 70 | 71 | protected override async Task OnInitializedAsync() 72 | { 73 | await CategoryService.GetAdminCategories(); 74 | CategoryService.OnChange += StateHasChanged; 75 | } 76 | 77 | public void Dispose() 78 | { 79 | CategoryService.OnChange -= StateHasChanged; 80 | } 81 | 82 | private void CreateNewCategory() 83 | { 84 | editingCategory = CategoryService.CreateNewCategory(); 85 | } 86 | 87 | private void EditCategory(Category category) 88 | { 89 | category.Editing = true; 90 | editingCategory = category; 91 | } 92 | 93 | private async Task UpdateCategory() 94 | { 95 | if (editingCategory.IsNew) 96 | await CategoryService.AddCategory(editingCategory); 97 | else 98 | await CategoryService.UpdateCategory(editingCategory); 99 | editingCategory = new Category(); 100 | } 101 | 102 | private async Task CancelEditing() 103 | { 104 | editingCategory = new Category(); 105 | await CategoryService.GetAdminCategories(); 106 | } 107 | 108 | private async Task DeleteCategory(int id) 109 | { 110 | await CategoryService.DeleteCategory(id); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/ProductService/ProductService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorEcommerce.Client.Services.ProductService 2 | { 3 | public class ProductService : IProductService 4 | { 5 | private readonly HttpClient _http; 6 | 7 | public ProductService(HttpClient http) 8 | { 9 | _http = http; 10 | } 11 | 12 | public List Products { get; set; } = new List(); 13 | public string Message { get; set; } = "Loading products..."; 14 | public int CurrentPage { get; set; } = 1; 15 | public int PageCount { get; set; } = 0; 16 | public string LastSearchText { get; set; } = string.Empty; 17 | public List AdminProducts { get; set; } 18 | 19 | public event Action ProductsChanged; 20 | 21 | public async Task CreateProduct(Product product) 22 | { 23 | var result = await _http.PostAsJsonAsync("api/product", product); 24 | var newProduct = (await result.Content 25 | .ReadFromJsonAsync>()).Data; 26 | return newProduct; 27 | } 28 | 29 | public async Task DeleteProduct(Product product) 30 | { 31 | var result = await _http.DeleteAsync($"api/product/{product.Id}"); 32 | } 33 | 34 | public async Task GetAdminProducts() 35 | { 36 | var result = await _http 37 | .GetFromJsonAsync>>("api/product/admin"); 38 | AdminProducts = result.Data; 39 | CurrentPage = 1; 40 | PageCount = 0; 41 | if (AdminProducts.Count == 0) 42 | Message = "No products found."; 43 | } 44 | 45 | public async Task> GetProduct(int productId) 46 | { 47 | var result = await _http.GetFromJsonAsync>($"api/product/{productId}"); 48 | return result; 49 | } 50 | 51 | public async Task GetProducts(string? categoryUrl = null) 52 | { 53 | var result = categoryUrl == null ? 54 | await _http.GetFromJsonAsync>>("api/product/featured") : 55 | await _http.GetFromJsonAsync>>($"api/product/category/{categoryUrl}"); 56 | if (result != null && result.Data != null) 57 | Products = result.Data; 58 | 59 | CurrentPage = 1; 60 | PageCount = 0; 61 | 62 | if (Products.Count == 0) 63 | Message = "No products found"; 64 | 65 | ProductsChanged.Invoke(); 66 | } 67 | 68 | public async Task> GetProductSearchSuggestions(string searchText) 69 | { 70 | var result = await _http 71 | .GetFromJsonAsync>>($"api/product/searchsuggestions/{searchText}"); 72 | return result.Data; 73 | } 74 | 75 | public async Task SearchProducts(string searchText, int page) 76 | { 77 | LastSearchText = searchText; 78 | var result = await _http 79 | .GetFromJsonAsync>($"api/product/search/{searchText}/{page}"); 80 | if (result != null && result.Data != null) 81 | { 82 | Products = result.Data.Products; 83 | CurrentPage = result.Data.CurrentPage; 84 | PageCount = result.Data.Pages; 85 | } 86 | if (Products.Count == 0) Message = "No products found."; 87 | ProductsChanged?.Invoke(); 88 | } 89 | 90 | public async Task UpdateProduct(Product product) 91 | { 92 | var result = await _http.PutAsJsonAsync($"api/product", product); 93 | var content = await result.Content.ReadFromJsonAsync>(); 94 | return content.Data; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/OrderService/OrderService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace BlazorEcommerce.Server.Services.OrderService 4 | { 5 | public class OrderService : IOrderService 6 | { 7 | private readonly DataContext _context; 8 | private readonly ICartService _cartService; 9 | private readonly IAuthService _authService; 10 | 11 | public OrderService(DataContext context, 12 | ICartService cartService, 13 | IAuthService authService) 14 | { 15 | _context = context; 16 | _cartService = cartService; 17 | _authService = authService; 18 | } 19 | 20 | public async Task> GetOrderDetails(int orderId) 21 | { 22 | var response = new ServiceResponse(); 23 | var order = await _context.Orders 24 | .Include(o => o.OrderItems) 25 | .ThenInclude(oi => oi.Product) 26 | .Include(o => o.OrderItems) 27 | .ThenInclude(oi => oi.ProductType) 28 | .Where(o => o.UserId == _authService.GetUserId() && o.Id == orderId) 29 | .OrderByDescending(o => o.OrderDate) 30 | .FirstOrDefaultAsync(); 31 | 32 | if (order == null) 33 | { 34 | response.Success = false; 35 | response.Message = "Order not found."; 36 | return response; 37 | } 38 | 39 | var orderDetailsResponse = new OrderDetailsResponse 40 | { 41 | OrderDate = order.OrderDate, 42 | TotalPrice = order.TotalPrice, 43 | Products = new List() 44 | }; 45 | 46 | order.OrderItems.ForEach(item => 47 | orderDetailsResponse.Products.Add(new OrderDetailsProductResponse { 48 | ProductId = item.ProductId, 49 | ImageUrl = item.Product.ImageUrl, 50 | ProductType = item.ProductType.Name, 51 | Quantity = item.Quantity, 52 | Title = item.Product.Title, 53 | TotalPrice = item.TotalPrice 54 | })); 55 | 56 | response.Data = orderDetailsResponse; 57 | 58 | return response; 59 | } 60 | 61 | public async Task>> GetOrders() 62 | { 63 | var response = new ServiceResponse>(); 64 | var orders = await _context.Orders 65 | .Include(o => o.OrderItems) 66 | .ThenInclude(oi => oi.Product) 67 | .Where(o => o.UserId == _authService.GetUserId()) 68 | .OrderByDescending(o => o.OrderDate) 69 | .ToListAsync(); 70 | 71 | var orderResponse = new List(); 72 | orders.ForEach(o => orderResponse.Add(new OrderOverviewResponse 73 | { 74 | Id = o.Id, 75 | OrderDate = o.OrderDate, 76 | TotalPrice = o.TotalPrice, 77 | Product = o.OrderItems.Count > 1 ? 78 | $"{o.OrderItems.First().Product.Title} and" + 79 | $" {o.OrderItems.Count - 1} more..." : 80 | o.OrderItems.First().Product.Title, 81 | ProductImageUrl = o.OrderItems.First().Product.ImageUrl 82 | })); 83 | 84 | response.Data = orderResponse; 85 | 86 | return response; 87 | } 88 | 89 | public async Task> PlaceOrder(int userId) 90 | { 91 | var products = (await _cartService.GetDbCartProducts(userId)).Data; 92 | decimal totalPrice = 0; 93 | products.ForEach(product => totalPrice += product.Price * product.Quantity); 94 | 95 | var orderItems = new List(); 96 | products.ForEach(product => orderItems.Add(new OrderItem 97 | { 98 | ProductId = product.ProductId, 99 | ProductTypeId = product.ProductTypeId, 100 | Quantity = product.Quantity, 101 | TotalPrice = product.Price * product.Quantity 102 | })); 103 | 104 | var order = new Order 105 | { 106 | UserId = userId, 107 | OrderDate = DateTime.Now, 108 | TotalPrice = totalPrice, 109 | OrderItems = orderItems 110 | }; 111 | 112 | _context.Orders.Add(order); 113 | 114 | _context.CartItems.RemoveRange(_context.CartItems 115 | .Where(ci => ci.UserId == userId)); 116 | 117 | await _context.SaveChangesAsync(); 118 | 119 | return new ServiceResponse { Data = true }; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211112210614_ProductSeeding.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using BlazorEcommerce.Server.Data; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorEcommerce.Server.Migrations 12 | { 13 | [DbContext(typeof(DataContext))] 14 | [Migration("20211112210614_ProductSeeding")] 15 | partial class ProductSeeding 16 | { 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "6.0.0") 22 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 23 | 24 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); 25 | 26 | modelBuilder.Entity("BlazorEcommerce.Shared.Product", b => 27 | { 28 | b.Property("Id") 29 | .ValueGeneratedOnAdd() 30 | .HasColumnType("int"); 31 | 32 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); 33 | 34 | b.Property("Description") 35 | .IsRequired() 36 | .HasColumnType("nvarchar(max)"); 37 | 38 | b.Property("ImageUrl") 39 | .IsRequired() 40 | .HasColumnType("nvarchar(max)"); 41 | 42 | b.Property("Price") 43 | .HasColumnType("decimal(18,2)"); 44 | 45 | b.Property("Title") 46 | .IsRequired() 47 | .HasColumnType("nvarchar(max)"); 48 | 49 | b.HasKey("Id"); 50 | 51 | b.ToTable("Products"); 52 | 53 | b.HasData( 54 | new 55 | { 56 | Id = 1, 57 | Description = "The Hitchhiker's Guide to the Galaxy[note 1] (sometimes referred to as HG2G,[1] HHGTTG,[2] H2G2,[3] or tHGttG) is a comedy science fiction franchise created by Douglas Adams. Originally a 1978 radio comedy broadcast on BBC Radio 4, it was later adapted to other formats, including stage shows, novels, comic books, a 1981 TV series, a 1984 text-based computer game, and 2005 feature film.", 58 | ImageUrl = "https://upload.wikimedia.org/wikipedia/en/b/bd/H2G2_UK_front_cover.jpg", 59 | Price = 9.99m, 60 | Title = "The Hitchhiker's Guide to the Galaxy" 61 | }, 62 | new 63 | { 64 | Id = 2, 65 | Description = "Ready Player One is a 2011 science fiction novel, and the debut novel of American author Ernest Cline. The story, set in a dystopia in 2045, follows protagonist Wade Watts on his search for an Easter egg in a worldwide virtual reality game, the discovery of which would lead him to inherit the game creator's fortune. Cline sold the rights to publish the novel in June 2010, in a bidding war to the Crown Publishing Group (a division of Random House).[1] The book was published on August 16, 2011.[2] An audiobook was released the same day; it was narrated by Wil Wheaton, who was mentioned briefly in one of the chapters.[3][4]Ch. 20 In 2012, the book received an Alex Award from the Young Adult Library Services Association division of the American Library Association[5] and won the 2011 Prometheus Award.[6]", 66 | ImageUrl = "https://upload.wikimedia.org/wikipedia/en/a/a4/Ready_Player_One_cover.jpg", 67 | Price = 7.99m, 68 | Title = "Ready Player One" 69 | }, 70 | new 71 | { 72 | Id = 3, 73 | Description = "Nineteen Eighty-Four (also stylised as 1984) is a dystopian social science fiction novel and cautionary tale written by English writer George Orwell. It was published on 8 June 1949 by Secker & Warburg as Orwell's ninth and final book completed in his lifetime. Thematically, it centres on the consequences of totalitarianism, mass surveillance and repressive regimentation of people and behaviours within society.[2][3] Orwell, a democratic socialist, modelled the totalitarian government in the novel after Stalinist Russia and Nazi Germany.[2][3][4] More broadly, the novel examines the role of truth and facts within politics and the ways in which they are manipulated.", 74 | ImageUrl = "https://upload.wikimedia.org/wikipedia/commons/c/c3/1984first.jpg", 75 | Price = 6.99m, 76 | Title = "Nineteen Eighty-Four" 77 | }); 78 | }); 79 | #pragma warning restore 612, 618 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/CartService/CartService.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace BlazorEcommerce.Server.Services.CartService 4 | { 5 | public class CartService : ICartService 6 | { 7 | private readonly DataContext _context; 8 | private readonly IAuthService _authService; 9 | 10 | public CartService(DataContext context, IAuthService authService) 11 | { 12 | _context = context; 13 | _authService = authService; 14 | } 15 | 16 | public async Task>> GetCartProducts(List cartItems) 17 | { 18 | var result = new ServiceResponse> 19 | { 20 | Data = new List() 21 | }; 22 | 23 | foreach (var item in cartItems) 24 | { 25 | var product = await _context.Products 26 | .Where(p => p.Id == item.ProductId) 27 | .FirstOrDefaultAsync(); 28 | 29 | if (product == null) 30 | { 31 | continue; 32 | } 33 | 34 | var productVariant = await _context.ProductVariants 35 | .Where(v => v.ProductId == item.ProductId 36 | && v.ProductTypeId == item.ProductTypeId) 37 | .Include(v => v.ProductType) 38 | .FirstOrDefaultAsync(); 39 | 40 | if (productVariant == null) 41 | { 42 | continue; 43 | } 44 | 45 | var cartProduct = new CartProductResponse 46 | { 47 | ProductId = product.Id, 48 | Title = product.Title, 49 | ImageUrl = product.ImageUrl, 50 | Price = productVariant.Price, 51 | ProductType = productVariant.ProductType.Name, 52 | ProductTypeId = productVariant.ProductTypeId, 53 | Quantity = item.Quantity 54 | }; 55 | 56 | result.Data.Add(cartProduct); 57 | } 58 | 59 | return result; 60 | } 61 | 62 | public async Task>> StoreCartItems(List cartItems) 63 | { 64 | cartItems.ForEach(cartItem => cartItem.UserId = _authService.GetUserId()); 65 | _context.CartItems.AddRange(cartItems); 66 | await _context.SaveChangesAsync(); 67 | 68 | return await GetDbCartProducts(); 69 | } 70 | 71 | public async Task> GetCartItemsCount() 72 | { 73 | var count = (await _context.CartItems.Where(ci => ci.UserId == _authService.GetUserId()).ToListAsync()).Count; 74 | return new ServiceResponse { Data = count }; 75 | } 76 | 77 | public async Task>> GetDbCartProducts(int? userId = null) 78 | { 79 | if(userId == null) 80 | userId = _authService.GetUserId(); 81 | 82 | return await GetCartProducts(await _context.CartItems 83 | .Where(ci => ci.UserId == userId).ToListAsync()); 84 | } 85 | 86 | public async Task> AddToCart(CartItem cartItem) 87 | { 88 | cartItem.UserId = _authService.GetUserId(); 89 | 90 | var sameItem = await _context.CartItems 91 | .FirstOrDefaultAsync(ci => ci.ProductId == cartItem.ProductId && 92 | ci.ProductTypeId == cartItem.ProductTypeId && ci.UserId == cartItem.UserId); 93 | if (sameItem == null) 94 | { 95 | _context.CartItems.Add(cartItem); 96 | } 97 | else 98 | { 99 | sameItem.Quantity += cartItem.Quantity; 100 | } 101 | 102 | await _context.SaveChangesAsync(); 103 | 104 | return new ServiceResponse { Data = true }; 105 | } 106 | 107 | public async Task> UpdateQuantity(CartItem cartItem) 108 | { 109 | var dbCartItem = await _context.CartItems 110 | .FirstOrDefaultAsync(ci => ci.ProductId == cartItem.ProductId && 111 | ci.ProductTypeId == cartItem.ProductTypeId && ci.UserId == _authService.GetUserId()); 112 | if (dbCartItem == null) 113 | { 114 | return new ServiceResponse 115 | { 116 | Data = false, 117 | Success = false, 118 | Message = "Cart item does not exist." 119 | }; 120 | } 121 | 122 | dbCartItem.Quantity = cartItem.Quantity; 123 | await _context.SaveChangesAsync(); 124 | 125 | return new ServiceResponse { Data = true }; 126 | } 127 | 128 | public async Task> RemoveItemFromCart(int productId, int productTypeId) 129 | { 130 | var dbCartItem = await _context.CartItems 131 | .FirstOrDefaultAsync(ci => ci.ProductId == productId && 132 | ci.ProductTypeId == productTypeId && ci.UserId == _authService.GetUserId()); 133 | if (dbCartItem == null) 134 | { 135 | return new ServiceResponse 136 | { 137 | Data = false, 138 | Success = false, 139 | Message = "Cart item does not exist." 140 | }; 141 | } 142 | 143 | _context.CartItems.Remove(dbCartItem); 144 | await _context.SaveChangesAsync(); 145 | 146 | return new ServiceResponse { Data = true }; 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Services/AuthService/AuthService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Tokens; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Security.Claims; 4 | using System.Security.Cryptography; 5 | 6 | namespace BlazorEcommerce.Server.Services.AuthService 7 | { 8 | public class AuthService : IAuthService 9 | { 10 | private readonly DataContext _context; 11 | private readonly IConfiguration _configuration; 12 | private readonly IHttpContextAccessor _httpContextAccessor; 13 | 14 | public AuthService(DataContext context, 15 | IConfiguration configuration, 16 | IHttpContextAccessor httpContextAccessor) 17 | { 18 | _context = context; 19 | _configuration = configuration; 20 | _httpContextAccessor = httpContextAccessor; 21 | } 22 | 23 | public int GetUserId() => int.Parse(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier)); 24 | 25 | public string GetUserEmail() => _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.Name); 26 | 27 | public async Task> Login(string email, string password) 28 | { 29 | var response = new ServiceResponse(); 30 | var user = await _context.Users 31 | .FirstOrDefaultAsync(x => x.Email.ToLower().Equals(email.ToLower())); 32 | if (user == null) 33 | { 34 | response.Success = false; 35 | response.Message = "User not found."; 36 | } 37 | else if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt)) 38 | { 39 | response.Success = false; 40 | response.Message = "Wrong password."; 41 | } 42 | else 43 | { 44 | response.Data = CreateToken(user); 45 | } 46 | 47 | return response; 48 | } 49 | 50 | public async Task> Register(User user, string password) 51 | { 52 | if (await UserExists(user.Email)) 53 | { 54 | return new ServiceResponse 55 | { 56 | Success = false, 57 | Message = "User already exists." 58 | }; 59 | } 60 | 61 | CreatePasswordHash(password, out byte[] passwordHash, out byte[] passwordSalt); 62 | 63 | user.PasswordHash = passwordHash; 64 | user.PasswordSalt = passwordSalt; 65 | 66 | _context.Users.Add(user); 67 | await _context.SaveChangesAsync(); 68 | 69 | return new ServiceResponse { Data = user.Id, Message = "Registration successful!" }; 70 | } 71 | 72 | public async Task UserExists(string email) 73 | { 74 | if (await _context.Users.AnyAsync(user => user.Email.ToLower() 75 | .Equals(email.ToLower()))) 76 | { 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | private void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) 83 | { 84 | using (var hmac = new HMACSHA512()) 85 | { 86 | passwordSalt = hmac.Key; 87 | passwordHash = hmac 88 | .ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); 89 | } 90 | } 91 | 92 | private bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt) 93 | { 94 | using (var hmac = new HMACSHA512(passwordSalt)) 95 | { 96 | var computedHash = 97 | hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); 98 | return computedHash.SequenceEqual(passwordHash); 99 | } 100 | } 101 | 102 | private string CreateToken(User user) 103 | { 104 | List claims = new List 105 | { 106 | new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), 107 | new Claim(ClaimTypes.Name, user.Email), 108 | new Claim(ClaimTypes.Role, user.Role) 109 | }; 110 | 111 | var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8 112 | .GetBytes(_configuration.GetSection("AppSettings:Token").Value)); 113 | 114 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); 115 | 116 | var token = new JwtSecurityToken( 117 | claims: claims, 118 | expires: DateTime.Now.AddDays(1), 119 | signingCredentials: creds); 120 | 121 | var jwt = new JwtSecurityTokenHandler().WriteToken(token); 122 | 123 | return jwt; 124 | } 125 | 126 | public async Task> ChangePassword(int userId, string newPassword) 127 | { 128 | var user = await _context.Users.FindAsync(userId); 129 | if (user == null) 130 | { 131 | return new ServiceResponse 132 | { 133 | Success = false, 134 | Message = "User not found." 135 | }; 136 | } 137 | 138 | CreatePasswordHash(newPassword, out byte[] passwordHash, out byte[] passwordSalt); 139 | 140 | user.PasswordHash = passwordHash; 141 | user.PasswordSalt = passwordSalt; 142 | 143 | await _context.SaveChangesAsync(); 144 | 145 | return new ServiceResponse { Data = true, Message = "Password has been changed." }; 146 | } 147 | 148 | public async Task GetUserByEmail(string email) 149 | { 150 | return await _context.Users.FirstOrDefaultAsync(u => u.Email.Equals(email)); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /BlazorEcommerce/Client/Services/CartService/CartService.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | 3 | namespace BlazorEcommerce.Client.Services.CartService 4 | { 5 | public class CartService : ICartService 6 | { 7 | private readonly ILocalStorageService _localStorage; 8 | private readonly HttpClient _http; 9 | private readonly IAuthService _authService; 10 | 11 | public CartService(ILocalStorageService localStorage, HttpClient http, 12 | IAuthService authService) 13 | { 14 | _localStorage = localStorage; 15 | _http = http; 16 | _authService = authService; 17 | } 18 | 19 | public event Action OnChange; 20 | 21 | public async Task AddToCart(CartItem cartItem) 22 | { 23 | if (await _authService.IsUserAuthenticated()) 24 | { 25 | await _http.PostAsJsonAsync("api/cart/add", cartItem); 26 | } 27 | else 28 | { 29 | var cart = await _localStorage.GetItemAsync>("cart"); 30 | if (cart == null) 31 | { 32 | cart = new List(); 33 | } 34 | 35 | var sameItem = cart.Find(x => x.ProductId == cartItem.ProductId && 36 | x.ProductTypeId == cartItem.ProductTypeId); 37 | if (sameItem == null) 38 | { 39 | cart.Add(cartItem); 40 | } 41 | else 42 | { 43 | sameItem.Quantity += cartItem.Quantity; 44 | } 45 | 46 | await _localStorage.SetItemAsync("cart", cart); 47 | } 48 | await GetCartItemsCount(); 49 | } 50 | 51 | public async Task GetCartItemsCount() 52 | { 53 | if (await _authService.IsUserAuthenticated()) 54 | { 55 | var result = await _http.GetFromJsonAsync>("api/cart/count"); 56 | var count = result.Data; 57 | 58 | await _localStorage.SetItemAsync("cartItemsCount", count); 59 | } 60 | else 61 | { 62 | var cart = await _localStorage.GetItemAsync>("cart"); 63 | await _localStorage.SetItemAsync("cartItemsCount", cart != null ? cart.Count : 0); 64 | } 65 | 66 | OnChange.Invoke(); 67 | } 68 | 69 | public async Task> GetCartProducts() 70 | { 71 | if (await _authService.IsUserAuthenticated()) 72 | { 73 | var response = await _http.GetFromJsonAsync>>("api/cart"); 74 | return response.Data; 75 | } 76 | else 77 | { 78 | var cartItems = await _localStorage.GetItemAsync>("cart"); 79 | if (cartItems == null) 80 | return new List(); 81 | var response = await _http.PostAsJsonAsync("api/cart/products", cartItems); 82 | var cartProducts = 83 | await response.Content.ReadFromJsonAsync>>(); 84 | return cartProducts.Data; 85 | } 86 | 87 | } 88 | 89 | public async Task RemoveProductFromCart(int productId, int productTypeId) 90 | { 91 | if (await _authService.IsUserAuthenticated()) 92 | { 93 | await _http.DeleteAsync($"api/cart/{productId}/{productTypeId}"); 94 | } 95 | else 96 | { 97 | var cart = await _localStorage.GetItemAsync>("cart"); 98 | if (cart == null) 99 | { 100 | return; 101 | } 102 | 103 | var cartItem = cart.Find(x => x.ProductId == productId 104 | && x.ProductTypeId == productTypeId); 105 | if (cartItem != null) 106 | { 107 | cart.Remove(cartItem); 108 | await _localStorage.SetItemAsync("cart", cart); 109 | } 110 | } 111 | } 112 | 113 | public async Task StoreCartItems(bool emptyLocalCart = false) 114 | { 115 | var localCart = await _localStorage.GetItemAsync>("cart"); 116 | if (localCart == null) 117 | { 118 | return; 119 | } 120 | 121 | await _http.PostAsJsonAsync("api/cart", localCart); 122 | 123 | if (emptyLocalCart) 124 | { 125 | await _localStorage.RemoveItemAsync("cart"); 126 | } 127 | } 128 | 129 | public async Task UpdateQuantity(CartProductResponse product) 130 | { 131 | if (await _authService.IsUserAuthenticated()) 132 | { 133 | var request = new CartItem 134 | { 135 | ProductId = product.ProductId, 136 | Quantity = product.Quantity, 137 | ProductTypeId = product.ProductTypeId 138 | }; 139 | await _http.PutAsJsonAsync("api/cart/update-quantity", request); 140 | } 141 | else 142 | { 143 | var cart = await _localStorage.GetItemAsync>("cart"); 144 | if (cart == null) 145 | { 146 | return; 147 | } 148 | 149 | var cartItem = cart.Find(x => x.ProductId == product.ProductId 150 | && x.ProductTypeId == product.ProductTypeId); 151 | if (cartItem != null) 152 | { 153 | cartItem.Quantity = product.Quantity; 154 | await _localStorage.SetItemAsync("cart", cart); 155 | } 156 | } 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /BlazorEcommerce/Server/Migrations/20211114115758_SeedMoreProducts.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorEcommerce.Server.Migrations 6 | { 7 | public partial class SeedMoreProducts : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.InsertData( 12 | table: "Products", 13 | columns: new[] { "Id", "CategoryId", "Description", "ImageUrl", "Price", "Title" }, 14 | values: new object[,] 15 | { 16 | { 4, 2, "The Matrix is a 1999 science fiction action film written and directed by the Wachowskis, and produced by Joel Silver. Starring Keanu Reeves, Laurence Fishburne, Carrie-Anne Moss, Hugo Weaving, and Joe Pantoliano, and as the first installment in the Matrix franchise, it depicts a dystopian future in which humanity is unknowingly trapped inside a simulated reality, the Matrix, which intelligent machines have created to distract humans while using their bodies as an energy source. When computer programmer Thomas Anderson, under the hacker alias \"Neo\", uncovers the truth, he \"is drawn into a rebellion against the machines\" along with other people who have been freed from the Matrix.", "https://upload.wikimedia.org/wikipedia/en/c/c1/The_Matrix_Poster.jpg", 4.99m, "The Matrix" }, 17 | { 5, 2, "Back to the Future is a 1985 American science fiction film directed by Robert Zemeckis. Written by Zemeckis and Bob Gale, it stars Michael J. Fox, Christopher Lloyd, Lea Thompson, Crispin Glover, and Thomas F. Wilson. Set in 1985, the story follows Marty McFly (Fox), a teenager accidentally sent back to 1955 in a time-traveling DeLorean automobile built by his eccentric scientist friend Doctor Emmett \"Doc\" Brown (Lloyd). Trapped in the past, Marty inadvertently prevents his future parents' meeting—threatening his very existence—and is forced to reconcile the pair and somehow get back to the future.", "https://upload.wikimedia.org/wikipedia/en/d/d2/Back_to_the_Future.jpg", 3.99m, "Back to the Future" }, 18 | { 6, 2, "Toy Story is a 1995 American computer-animated comedy film produced by Pixar Animation Studios and released by Walt Disney Pictures. The first installment in the Toy Story franchise, it was the first entirely computer-animated feature film, as well as the first feature film from Pixar. The film was directed by John Lasseter (in his feature directorial debut), and written by Joss Whedon, Andrew Stanton, Joel Cohen, and Alec Sokolow from a story by Lasseter, Stanton, Pete Docter, and Joe Ranft. The film features music by Randy Newman, was produced by Bonnie Arnold and Ralph Guggenheim, and was executive-produced by Steve Jobs and Edwin Catmull. The film features the voices of Tom Hanks, Tim Allen, Don Rickles, Wallace Shawn, John Ratzenberger, Jim Varney, Annie Potts, R. Lee Ermey, John Morris, Laurie Metcalf, and Erik von Detten. Taking place in a world where anthropomorphic toys come to life when humans are not present, the plot focuses on the relationship between an old-fashioned pull-string cowboy doll named Woody and an astronaut action figure, Buzz Lightyear, as they evolve from rivals competing for the affections of their owner, Andy Davis, to friends who work together to be reunited with Andy after being separated from him.", "https://upload.wikimedia.org/wikipedia/en/1/13/Toy_Story.jpg", 2.99m, "Toy Story" }, 19 | { 7, 3, "Half-Life 2 is a 2004 first-person shooter game developed and published by Valve. Like the original Half-Life, it combines shooting, puzzles, and storytelling, and adds features such as vehicles and physics-based gameplay.", "https://upload.wikimedia.org/wikipedia/en/2/25/Half-Life_2_cover.jpg", 49.99m, "Half-Life 2" }, 20 | { 8, 3, "Diablo II is an action role-playing hack-and-slash computer video game developed by Blizzard North and published by Blizzard Entertainment in 2000 for Microsoft Windows, Classic Mac OS, and macOS.", "https://upload.wikimedia.org/wikipedia/en/d/d5/Diablo_II_Coverart.png", 9.99m, "Diablo II" }, 21 | { 9, 3, "Day of the Tentacle, also known as Maniac Mansion II: Day of the Tentacle, is a 1993 graphic adventure game developed and published by LucasArts. It is the sequel to the 1987 game Maniac Mansion.", "https://upload.wikimedia.org/wikipedia/en/7/79/Day_of_the_Tentacle_artwork.jpg", 14.99m, "Day of the Tentacle" }, 22 | { 10, 3, "The Xbox is a home video game console and the first installment in the Xbox series of video game consoles manufactured by Microsoft.", "https://upload.wikimedia.org/wikipedia/commons/4/43/Xbox-console.jpg", 159.99m, "Xbox" }, 23 | { 11, 3, "The Super Nintendo Entertainment System (SNES), also known as the Super NES or Super Nintendo, is a 16-bit home video game console developed by Nintendo that was released in 1990 in Japan and South Korea.", "https://upload.wikimedia.org/wikipedia/commons/e/ee/Nintendo-Super-Famicom-Set-FL.jpg", 79.99m, "Super Nintendo Entertainment System" } 24 | }); 25 | } 26 | 27 | protected override void Down(MigrationBuilder migrationBuilder) 28 | { 29 | migrationBuilder.DeleteData( 30 | table: "Products", 31 | keyColumn: "Id", 32 | keyValue: 4); 33 | 34 | migrationBuilder.DeleteData( 35 | table: "Products", 36 | keyColumn: "Id", 37 | keyValue: 5); 38 | 39 | migrationBuilder.DeleteData( 40 | table: "Products", 41 | keyColumn: "Id", 42 | keyValue: 6); 43 | 44 | migrationBuilder.DeleteData( 45 | table: "Products", 46 | keyColumn: "Id", 47 | keyValue: 7); 48 | 49 | migrationBuilder.DeleteData( 50 | table: "Products", 51 | keyColumn: "Id", 52 | keyValue: 8); 53 | 54 | migrationBuilder.DeleteData( 55 | table: "Products", 56 | keyColumn: "Id", 57 | keyValue: 9); 58 | 59 | migrationBuilder.DeleteData( 60 | table: "Products", 61 | keyColumn: "Id", 62 | keyValue: 10); 63 | 64 | migrationBuilder.DeleteData( 65 | table: "Products", 66 | keyColumn: "Id", 67 | keyValue: 11); 68 | } 69 | } 70 | } 71 | --------------------------------------------------------------------------------