Skip to content

Commit e2545b8

Browse files
committed
Administration for Products & Product Variants.
1 parent faec933 commit e2545b8

15 files changed

+1595
-56
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
@page "/admin/product"
2+
@page "/admin/product/{id:int}"
3+
@inject IProductService ProductService
4+
@inject IProductTypeService ProductTypeService
5+
@inject ICategoryService CategoryService
6+
@inject NavigationManager NavigationManager
7+
@inject IJSRuntime JSRuntime
8+
9+
@if (loading)
10+
{
11+
<span>@msg</span>
12+
}
13+
else
14+
{
15+
@if (product.Editing)
16+
{
17+
<h3>Edit "@product.Title"</h3>
18+
}
19+
else if (product.IsNew)
20+
{
21+
<h3>Create a new Product</h3>
22+
}
23+
24+
<EditForm Model="product" OnValidSubmit="AddOrUpdateProduct">
25+
<DataAnnotationsValidator></DataAnnotationsValidator>
26+
<div class="mb-0">
27+
<label for="title">Title</label>
28+
<InputText id="title" @bind-Value="product.Title" class="form-control"></InputText>
29+
</div>
30+
<div class="mb-0">
31+
<label for="imageUrl">Image Url</label>
32+
<InputText id="imageUrl" @bind-Value="product.ImageUrl" class="form-control"></InputText>
33+
</div>
34+
<div class="mb-0">
35+
<img src="@product.ImageUrl" />
36+
</div>
37+
<div class="mb-0">
38+
<label for="description">Description</label>
39+
<InputTextArea id="description" @bind-Value="product.Description" class="form-control"></InputTextArea>
40+
</div>
41+
<hr />
42+
<div class="header">
43+
<div class="col">Product Type / Variant</div>
44+
<div class="col">Price</div>
45+
<div class="col">Original Price</div>
46+
<div class="col">Visible</div>
47+
<div class="col"></div>
48+
</div>
49+
@foreach (var variant in product.Variants)
50+
{
51+
<div class="row">
52+
<div class="col">
53+
<InputSelect disabled="@variant.Deleted" @bind-Value="variant.ProductTypeId" class="form-control">
54+
@foreach (var productType in ProductTypeService.ProductTypes)
55+
{
56+
<option value="@productType.Id.ToString()">@productType.Name</option>
57+
}
58+
</InputSelect>
59+
</div>
60+
<div class="col">
61+
<InputNumber @bind-Value="variant.Price" class="form-control" disabled="@variant.Deleted"></InputNumber>
62+
</div>
63+
<div class="col">
64+
<InputNumber @bind-Value="variant.OriginalPrice" class="form-control" disabled="@variant.Deleted"></InputNumber>
65+
</div>
66+
<div class="col col-visible">
67+
<InputCheckbox @bind-Value="variant.Visible" style="transform:scale(1.5,1.5);" disabled="@variant.Deleted"></InputCheckbox>
68+
</div>
69+
<div class="col">
70+
<button type="button" class="btn btn-primary" disabled="@variant.Deleted" @onclick="@(() => RemoveVariant(variant.ProductTypeId))">
71+
<i class="oi oi-trash"></i>
72+
</button>
73+
</div>
74+
</div>
75+
}
76+
<button type="button" class="btn btn-primary" @onclick="AddVariant">
77+
<i class="oi oi-plus"></i> Add Variant
78+
</button>
79+
<hr />
80+
<div class="mb-0">
81+
<label for="category">Category</label>
82+
<InputSelect id="category" @bind-Value="product.CategoryId" class="form-control">
83+
@foreach (var category in CategoryService.AdminCategories)
84+
{
85+
<option value="@category.Id">@category.Name</option>
86+
}
87+
</InputSelect>
88+
</div>
89+
<hr />
90+
<div class="form-check">
91+
<InputCheckbox id="featured" @bind-Value="product.Featured" class="form-check-input"></InputCheckbox>
92+
<label for="featured" class="form-check-label">Featured</label>
93+
</div>
94+
<div class="form-check">
95+
<InputCheckbox id="visible" @bind-Value="product.Visible" class="form-check-input"></InputCheckbox>
96+
<label for="visible" class="form-check-label">Visible</label>
97+
</div>
98+
<hr />
99+
<button type="submit" class="btn btn-primary float-end">@btnText</button>
100+
<ValidationSummary></ValidationSummary>
101+
</EditForm>
102+
@if (!product.IsNew)
103+
{
104+
<button type="button" class="btn btn-danger float-start" @onclick="DeleteProduct">
105+
Delete Product
106+
</button>
107+
}
108+
}
109+
110+
@code {
111+
[Parameter]
112+
public int Id { get; set; }
113+
114+
Product product = new Product();
115+
bool loading = true;
116+
string btnText = "";
117+
string msg = "Loading...";
118+
119+
protected override async Task OnInitializedAsync()
120+
{
121+
await ProductTypeService.GetProductTypes();
122+
await CategoryService.GetAdminCategories();
123+
}
124+
125+
protected override async Task OnParametersSetAsync()
126+
{
127+
if (Id == 0)
128+
{
129+
product = new Product { IsNew = true };
130+
btnText = "Create Product";
131+
}
132+
else
133+
{
134+
Product dbProduct = (await ProductService.GetProduct(Id)).Data;
135+
if (dbProduct == null)
136+
{
137+
msg = $"Product with Id '{Id}' does not exist!";
138+
return;
139+
}
140+
product = dbProduct;
141+
product.Editing = true;
142+
btnText = "Update Product";
143+
}
144+
loading = false;
145+
}
146+
147+
void RemoveVariant(int productTypeId)
148+
{
149+
var variant = product.Variants.Find(v => v.ProductTypeId == productTypeId);
150+
if (variant == null)
151+
{
152+
return;
153+
}
154+
if (variant.IsNew)
155+
{
156+
product.Variants.Remove(variant);
157+
}
158+
else
159+
{
160+
variant.Deleted = true;
161+
}
162+
}
163+
164+
void AddVariant()
165+
{
166+
product.Variants
167+
.Add(new ProductVariant { IsNew = true, ProductId = product.Id });
168+
}
169+
170+
async void AddOrUpdateProduct()
171+
{
172+
if (product.IsNew)
173+
{
174+
var result = await ProductService.CreateProduct(product);
175+
NavigationManager.NavigateTo($"admin/product/{result.Id}");
176+
}
177+
else
178+
{
179+
product.IsNew = false;
180+
product = await ProductService.UpdateProduct(product);
181+
NavigationManager.NavigateTo($"admin/product/{product.Id}", true);
182+
}
183+
}
184+
185+
async void DeleteProduct()
186+
{
187+
bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
188+
$"Do you really want to delete '{product.Title}'?");
189+
if (confirmed)
190+
{
191+
await ProductService.DeleteProduct(product);
192+
NavigationManager.NavigateTo("admin/products");
193+
}
194+
}
195+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
@page "/admin/products"
2+
@inject IProductService ProductService
3+
@inject NavigationManager NavigationManager
4+
@attribute [Authorize(Roles = "Admin")]
5+
6+
<h3>Products</h3>
7+
8+
@if (ProductService.AdminProducts == null)
9+
{
10+
<span>Loading Products...</span>
11+
}
12+
else
13+
{
14+
<button class="btn btn-primary float-end" @onclick="CreateProduct">
15+
<i class="oi oi-plus"></i> Add new product
16+
</button>
17+
<table class="table">
18+
<thead>
19+
<tr>
20+
<th></th>
21+
<th>Product</th>
22+
<th>Variant</th>
23+
<th>Price</th>
24+
<th>Visible</th>
25+
<th></th>
26+
</tr>
27+
</thead>
28+
<tbody>
29+
<Virtualize Items="ProductService.AdminProducts" Context="product">
30+
<tr>
31+
<td><img src="@product.ImageUrl" /></td>
32+
<td>@product.Title</td>
33+
<td>
34+
@foreach (var variant in product.Variants)
35+
{
36+
<span>@variant.ProductType.Name</span>
37+
38+
<br />
39+
}
40+
</td>
41+
<td>
42+
@foreach (var variant in product.Variants)
43+
{
44+
<span>@variant.Price</span>
45+
46+
<br />
47+
}
48+
</td>
49+
<td>@(product.Visible ? "✔️" : "")</td>
50+
<td>
51+
<button class="btn btn-primary" @onclick="(() => EditProduct(product.Id))">
52+
<i class="oi oi-pencil"></i>
53+
</button>
54+
</td>
55+
</tr>
56+
</Virtualize>
57+
</tbody>
58+
</table>
59+
}
60+
61+
@code {
62+
protected override async Task OnInitializedAsync()
63+
{
64+
await ProductService.GetAdminProducts();
65+
}
66+
67+
void EditProduct(int productId)
68+
{
69+
NavigationManager.NavigateTo($"admin/product/{productId}");
70+
}
71+
72+
void CreateProduct()
73+
{
74+
NavigationManager.NavigateTo("admin/product");
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
img {
2+
max-height: 100px;
3+
max-width: 100px;
4+
}

BlazorEcommerce/Client/Services/ProductService/IProductService.cs

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public interface IProductService
44
{
55
event Action ProductsChanged;
66
List<Product> Products { get; set; }
7+
List<Product> AdminProducts { get; set; }
78
string Message { get; set; }
89
int CurrentPage { get; set; }
910
int PageCount { get; set; }
@@ -12,5 +13,9 @@ public interface IProductService
1213
Task<ServiceResponse<Product>> GetProduct(int productId);
1314
Task SearchProducts(string searchText, int page);
1415
Task<List<string>> GetProductSearchSuggestions(string searchText);
16+
Task GetAdminProducts();
17+
Task<Product> CreateProduct(Product product);
18+
Task<Product> UpdateProduct(Product product);
19+
Task DeleteProduct(Product product);
1520
}
1621
}

BlazorEcommerce/Client/Services/ProductService/ProductService.cs

+32
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,34 @@ public ProductService(HttpClient http)
1414
public int CurrentPage { get; set; } = 1;
1515
public int PageCount { get; set; } = 0;
1616
public string LastSearchText { get; set; } = string.Empty;
17+
public List<Product> AdminProducts { get; set; }
1718

1819
public event Action ProductsChanged;
1920

21+
public async Task<Product> CreateProduct(Product product)
22+
{
23+
var result = await _http.PostAsJsonAsync("api/product", product);
24+
var newProduct = (await result.Content
25+
.ReadFromJsonAsync<ServiceResponse<Product>>()).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<ServiceResponse<List<Product>>>("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+
2045
public async Task<ServiceResponse<Product>> GetProduct(int productId)
2146
{
2247
var result = await _http.GetFromJsonAsync<ServiceResponse<Product>>($"api/product/{productId}");
@@ -61,5 +86,12 @@ public async Task SearchProducts(string searchText, int page)
6186
if (Products.Count == 0) Message = "No products found.";
6287
ProductsChanged?.Invoke();
6388
}
89+
90+
public async Task<Product> UpdateProduct(Product product)
91+
{
92+
var result = await _http.PutAsJsonAsync($"api/product", product);
93+
var content = await result.Content.ReadFromJsonAsync<ServiceResponse<Product>>();
94+
return content.Data;
95+
}
6496
}
6597
}

BlazorEcommerce/Client/Shared/AdminMenu.razor

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{
66
<a class="dropdown-item" href="admin/categories">Categories</a>
77
<a class="dropdown-item" href="admin/product-types">Product Types</a>
8+
<a class="dropdown-item" href="admin/products">Products</a>
89
<hr />
910
}
1011

0 commit comments

Comments
 (0)