OnSaveClick { get; set; }
private string LoadPicture => string.IsNullOrEmpty(_item.PictureBase64) ? string.IsNullOrEmpty(_item.PictureUri) ? string.Empty : $"{_item.PictureUri}" : $"data:image/png;base64, {_item.PictureBase64}";
private bool HasPicture => !(string.IsNullOrEmpty(_item.PictureBase64) && string.IsNullOrEmpty(_item.PictureUri));
@@ -142,7 +143,8 @@
private async Task SaveClick()
{
- await new BlazorAdmin.Services.CatalogItemServices.Edit(Auth).HandleAsync(_item);
+ await CatalogItemService.Edit(_item);
+ await OnSaveClick.InvokeAsync(null);
await Close();
}
@@ -152,7 +154,7 @@
await new Css(JSRuntime).HideBodyOverflow();
- _item = await new GetById(Auth).HandleAsync(id);
+ _item = await CatalogItemService.GetById(id);
_modalDisplay = "block;";
_modalClass = "Show";
@@ -168,7 +170,6 @@
_modalDisplay = "none";
_modalClass = "";
_showEditModal = false;
- await OnCloseClick.InvokeAsync(null);
}
private async Task ChangeFile(IFileListEntry[] files)
diff --git a/src/BlazorAdmin/Pages/CatalogItemPage/List.razor b/src/BlazorAdmin/Pages/CatalogItemPage/List.razor
index cae60cb..2672f0e 100644
--- a/src/BlazorAdmin/Pages/CatalogItemPage/List.razor
+++ b/src/BlazorAdmin/Pages/CatalogItemPage/List.razor
@@ -1,9 +1,6 @@
@page "/admin"
@attribute [Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)]
@inject AuthService Auth
-@inject BlazorAdmin.Services.CatalogItemServices.ListPaged CatalogItemListPaged
-@inject BlazorAdmin.Services.CatalogTypeServices.List TypeList
-@inject BlazorAdmin.Services.CatalogBrandServices.List BrandList
@inherits BlazorAdmin.Helpers.BlazorComponent
@namespace BlazorAdmin.Pages.CatalogItemPage
@@ -42,8 +39,8 @@ else
|
- @Services.CatalogTypeServices.List.GetTypeName(catalogTypes, item.CatalogTypeId) |
- @Services.CatalogBrandServices.List.GetBrandName(catalogBrands, item.CatalogBrandId) |
+ @item.CatalogType |
+ @item.CatalogBrand |
@item.Id |
@item.Name |
@item.Description |
@@ -63,7 +60,7 @@ else
-
-
-
+
+
+
}
diff --git a/src/BlazorAdmin/Pages/CatalogItemPage/List.razor.cs b/src/BlazorAdmin/Pages/CatalogItemPage/List.razor.cs
index 18d434c..9b12a73 100644
--- a/src/BlazorAdmin/Pages/CatalogItemPage/List.razor.cs
+++ b/src/BlazorAdmin/Pages/CatalogItemPage/List.razor.cs
@@ -1,14 +1,22 @@
using BlazorAdmin.Helpers;
-using BlazorAdmin.Services.CatalogBrandServices;
-using BlazorAdmin.Services.CatalogItemServices;
-using BlazorAdmin.Services.CatalogTypeServices;
using System.Collections.Generic;
using System.Threading.Tasks;
+using BlazorShared.Interfaces;
+using BlazorShared.Models;
namespace BlazorAdmin.Pages.CatalogItemPage
{
public partial class List : BlazorComponent
{
+ [Microsoft.AspNetCore.Components.Inject]
+ public ICatalogItemService CatalogItemService { get; set; }
+
+ [Microsoft.AspNetCore.Components.Inject]
+ public ICatalogBrandService CatalogBrandService { get; set; }
+
+ [Microsoft.AspNetCore.Components.Inject]
+ public ICatalogTypeService CatalogTypeService { get; set; }
+
private List catalogItems = new List();
private List catalogTypes = new List();
private List catalogBrands = new List();
@@ -22,9 +30,9 @@ namespace BlazorAdmin.Pages.CatalogItemPage
{
if (firstRender)
{
- catalogItems = await CatalogItemListPaged.HandleAsync(50);
- catalogTypes = await TypeList.HandleAsync();
- catalogBrands = await BrandList.HandleAsync();
+ catalogItems = await CatalogItemService.List();
+ catalogTypes = await CatalogTypeService.List();
+ catalogBrands = await CatalogBrandService.List();
CallRequestRefresh();
}
@@ -54,7 +62,7 @@ namespace BlazorAdmin.Pages.CatalogItemPage
private async Task ReloadCatalogItems()
{
- catalogItems = await new BlazorAdmin.Services.CatalogItemServices.ListPaged(Auth).HandleAsync(50);
+ catalogItems = await CatalogItemService.List();
StateHasChanged();
}
}
diff --git a/src/BlazorAdmin/Program.cs b/src/BlazorAdmin/Program.cs
index a2adbee..89080ad 100644
--- a/src/BlazorAdmin/Program.cs
+++ b/src/BlazorAdmin/Program.cs
@@ -1,11 +1,14 @@
+using BlazorAdmin.Services;
+using Blazored.LocalStorage;
+using BlazorShared;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
-using Microsoft.Extensions.DependencyInjection;
-using BlazorAdmin.Services;
-using Blazored.LocalStorage;
-using Microsoft.AspNetCore.Components.Authorization;
namespace BlazorAdmin
{
@@ -16,18 +19,36 @@ namespace BlazorAdmin
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add("admin");
- builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+ var baseUrlConfig = new BaseUrlConfiguration();
+ builder.Configuration.Bind(BaseUrlConfiguration.CONFIG_NAME, baseUrlConfig);
+ builder.Services.AddScoped(sp => baseUrlConfig);
- builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddScoped(sp => new HttpClient() { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+
+ builder.Services.AddScoped();
+
+ builder.Services.AddScoped();
+ builder.Services.AddScoped();
builder.Services.AddAuthorizationCore();
- builder.Services.AddSingleton();
- builder.Services.AddSingleton(sp => (CustomAuthStateProvider)sp.GetRequiredService());
+ builder.Services.AddScoped();
+ builder.Services.AddScoped(sp => (CustomAuthStateProvider)sp.GetRequiredService());
builder.Services.AddBlazorServices();
- await builder.Build().RunAsync();
+ builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
+
+ await ClearLocalStorageCache(builder.Services);
+
+ builder.Build().RunAsync();
+ }
+
+ private static async Task ClearLocalStorageCache(IServiceCollection services)
+ {
+ var sp = services.BuildServiceProvider();
+ var localStorageService = sp.GetRequiredService();
+
+ await localStorageService.RemoveItemAsync("brands");
}
}
}
diff --git a/src/BlazorAdmin/Services/AuthService.cs b/src/BlazorAdmin/Services/AuthService.cs
index f426dbe..5c0721a 100644
--- a/src/BlazorAdmin/Services/AuthService.cs
+++ b/src/BlazorAdmin/Services/AuthService.cs
@@ -1,11 +1,8 @@
using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
using System.Threading.Tasks;
using BlazorAdmin.JavaScript;
using Blazored.LocalStorage;
using Microsoft.JSInterop;
-using BlazorShared.Authorization;
namespace BlazorAdmin.Services
{
@@ -14,143 +11,32 @@ namespace BlazorAdmin.Services
private readonly HttpClient _httpClient;
private readonly ILocalStorageService _localStorage;
private readonly IJSRuntime _jSRuntime;
- private static bool InDocker { get; set; }
-
- public string ApiUrl => Constants.GetApiUrl(InDocker);
public bool IsLoggedIn { get; set; }
- public string UserName { get; set; }
- public AuthService(HttpClient httpClient, ILocalStorageService localStorage, IJSRuntime jSRuntime)
+ public AuthService(HttpClient httpClient,
+ ILocalStorageService localStorage,
+ IJSRuntime jSRuntime)
{
_httpClient = httpClient;
_localStorage = localStorage;
_jSRuntime = jSRuntime;
}
- public HttpClient GetHttpClient()
- {
- return _httpClient;
- }
-
public async Task Logout()
{
- await DeleteLocalStorage();
await DeleteCookies();
- RemoveAuthorizationHeader();
- UserName = null;
IsLoggedIn = false;
await LogoutIdentityManager();
}
- public async Task RefreshLoginInfo()
- {
- await SetLoginData();
- }
-
- public async Task RefreshLoginInfoFromCookie()
- {
- var token = await new Cookies(_jSRuntime).GetCookie("token");
- await SaveTokenInLocalStorage(token);
-
- var username = await new Cookies(_jSRuntime).GetCookie("username");
- await SaveUsernameInLocalStorage(username);
-
- var inDocker = await new Cookies(_jSRuntime).GetCookie("inDocker");
- await SaveInDockerInLocalStorage(inDocker);
-
- await RefreshLoginInfo();
- }
-
- public async Task GetToken()
- {
-
- var token = await _localStorage.GetItemAsync("authToken");
- return token;
- }
-
- public async Task GetTokenFromController()
- {
- return await _httpClient.GetFromJsonAsync("User");
- }
-
- public async Task GetUsername()
- {
- var username = await _localStorage.GetItemAsync("username");
- return username;
- }
-
- public async Task GetInDocker()
- {
- return (await _localStorage.GetItemAsync("inDocker")).ToLower() == "true";
- }
-
private async Task LogoutIdentityManager()
{
await _httpClient.PostAsync("Identity/Account/Logout", null);
}
- private async Task DeleteLocalStorage()
- {
- await _localStorage.RemoveItemAsync("authToken");
- await _localStorage.RemoveItemAsync("username");
- await _localStorage.RemoveItemAsync("inDocker");
- }
-
private async Task DeleteCookies()
{
await new Cookies(_jSRuntime).DeleteCookie("token");
- await new Cookies(_jSRuntime).DeleteCookie("username");
- await new Cookies(_jSRuntime).DeleteCookie("inDocker");
}
-
- private async Task SetLoginData()
- {
- IsLoggedIn = !string.IsNullOrEmpty(await GetToken());
- UserName = await GetUsername();
- InDocker = await GetInDocker();
- await SetAuthorizationHeader();
- }
-
- private void RemoveAuthorizationHeader()
- {
- if (_httpClient.DefaultRequestHeaders.Contains("Authorization"))
- {
- _httpClient.DefaultRequestHeaders.Remove("Authorization");
- }
- }
-
- private async Task SaveTokenInLocalStorage(string token)
- {
- if (string.IsNullOrEmpty(token))
- {
- return;
- }
- await _localStorage.SetItemAsync("authToken", token);
- }
-
- private async Task SaveUsernameInLocalStorage(string username)
- {
- if (string.IsNullOrEmpty(username))
- {
- return;
- }
- await _localStorage.SetItemAsync("username", username);
- }
-
- private async Task SaveInDockerInLocalStorage(string inDocker)
- {
- if (string.IsNullOrEmpty(inDocker))
- {
- return;
- }
- await _localStorage.SetItemAsync("inDocker", inDocker);
- }
-
- private async Task SetAuthorizationHeader()
- {
- var token = await GetToken();
- _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
- }
-
}
}
diff --git a/src/BlazorAdmin/Services/CacheEntry.cs b/src/BlazorAdmin/Services/CacheEntry.cs
new file mode 100644
index 0000000..97abd8e
--- /dev/null
+++ b/src/BlazorAdmin/Services/CacheEntry.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace BlazorAdmin.Services
+{
+ public class CacheEntry
+ {
+ public CacheEntry(T item)
+ {
+ Value = item;
+ }
+ public CacheEntry()
+ {
+
+ }
+
+ public T Value { get; set; }
+ public DateTime DateCreated { get; set; } = DateTime.UtcNow;
+ }
+}
diff --git a/src/BlazorAdmin/Services/CachedCatalogBrandServiceDecorator.cs b/src/BlazorAdmin/Services/CachedCatalogBrandServiceDecorator.cs
new file mode 100644
index 0000000..b076490
--- /dev/null
+++ b/src/BlazorAdmin/Services/CachedCatalogBrandServiceDecorator.cs
@@ -0,0 +1,59 @@
+using Blazored.LocalStorage;
+using BlazorShared.Interfaces;
+using BlazorShared.Models;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace BlazorAdmin.Services
+{
+ public class CachedCatalogBrandServiceDecorator : ICatalogBrandService
+ {
+ // TODO: Make a generic decorator for any LookupData type
+ private readonly ILocalStorageService _localStorageService;
+ private readonly CatalogBrandService _catalogBrandService;
+ private ILogger _logger;
+
+ public CachedCatalogBrandServiceDecorator(ILocalStorageService localStorageService,
+ CatalogBrandService catalogBrandService,
+ ILogger logger)
+ {
+ _localStorageService = localStorageService;
+ _catalogBrandService = catalogBrandService;
+ _logger = logger;
+
+ }
+
+ public async Task GetById(int id)
+ {
+ return (await List()).FirstOrDefault(x => x.Id == id);
+ }
+
+ public async Task> List()
+ {
+ string key = "brands";
+ var cacheEntry = await _localStorageService.GetItemAsync>>(key);
+ if (cacheEntry != null)
+ {
+ _logger.LogInformation("Loading brands from local storage.");
+ // TODO: Get Default Cache Duration from Config
+ if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
+ {
+ return cacheEntry.Value;
+ }
+ else
+ {
+ _logger.LogInformation("Cache expired; removing brands from local storage.");
+ await _localStorageService.RemoveItemAsync(key);
+ }
+ }
+
+ var brands = await _catalogBrandService.List();
+ var entry = new CacheEntry>(brands);
+ await _localStorageService.SetItemAsync(key, entry);
+ return brands;
+ }
+ }
+}
diff --git a/src/BlazorAdmin/Services/CachedCatalogItemServiceDecorator.cs b/src/BlazorAdmin/Services/CachedCatalogItemServiceDecorator.cs
new file mode 100644
index 0000000..885cb94
--- /dev/null
+++ b/src/BlazorAdmin/Services/CachedCatalogItemServiceDecorator.cs
@@ -0,0 +1,114 @@
+using Blazored.LocalStorage;
+using BlazorShared.Interfaces;
+using BlazorShared.Models;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace BlazorAdmin.Services
+{
+ public class CachedCatalogItemServiceDecorator : ICatalogItemService
+ {
+ private readonly ILocalStorageService _localStorageService;
+ private readonly CatalogItemService _catalogItemService;
+ private ILogger _logger;
+
+ public CachedCatalogItemServiceDecorator(ILocalStorageService localStorageService,
+ CatalogItemService catalogItemService,
+ ILogger logger)
+ {
+ _localStorageService = localStorageService;
+ _catalogItemService = catalogItemService;
+ _logger = logger;
+ }
+
+ public async Task> ListPaged(int pageSize)
+ {
+ string key = "items";
+ var cacheEntry = await _localStorageService.GetItemAsync>>(key);
+ if (cacheEntry != null)
+ {
+ _logger.LogInformation("Loading items from local storage.");
+ if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
+ {
+ return cacheEntry.Value;
+ }
+ else
+ {
+ _logger.LogInformation($"Loading {key} from local storage.");
+ await _localStorageService.RemoveItemAsync(key);
+ }
+ }
+
+ var items = await _catalogItemService.ListPaged(pageSize);
+ var entry = new CacheEntry>(items);
+ await _localStorageService.SetItemAsync(key, entry);
+ return items;
+ }
+
+ public async Task> List()
+ {
+ string key = "items";
+ var cacheEntry = await _localStorageService.GetItemAsync>>(key);
+ if (cacheEntry != null)
+ {
+ _logger.LogInformation("Loading items from local storage.");
+ if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
+ {
+ return cacheEntry.Value;
+ }
+ else
+ {
+ _logger.LogInformation($"Loading {key} from local storage.");
+ await _localStorageService.RemoveItemAsync(key);
+ }
+ }
+
+ var items = await _catalogItemService.List();
+ var entry = new CacheEntry>(items);
+ await _localStorageService.SetItemAsync(key, entry);
+ return items;
+ }
+
+ public async Task GetById(int id)
+ {
+ return (await List()).FirstOrDefault(x => x.Id == id);
+ }
+
+ public async Task Create(CreateCatalogItemRequest catalogItem)
+ {
+ var result = await _catalogItemService.Create(catalogItem);
+ await RefreshLocalStorageList();
+
+ return result;
+ }
+
+ public async Task Edit(CatalogItem catalogItem)
+ {
+ var result = await _catalogItemService.Edit(catalogItem);
+ await RefreshLocalStorageList();
+
+ return result;
+ }
+
+ public async Task Delete(int id)
+ {
+ var result = await _catalogItemService.Delete(id);
+ await RefreshLocalStorageList();
+
+ return result;
+ }
+
+ private async Task RefreshLocalStorageList()
+ {
+ string key = "items";
+
+ await _localStorageService.RemoveItemAsync(key);
+ var items = await _catalogItemService.List();
+ var entry = new CacheEntry>(items);
+ await _localStorageService.SetItemAsync(key, entry);
+ }
+ }
+}
diff --git a/src/BlazorAdmin/Services/CachedCatalogTypeServiceDecorator.cs b/src/BlazorAdmin/Services/CachedCatalogTypeServiceDecorator.cs
new file mode 100644
index 0000000..86c0cc0
--- /dev/null
+++ b/src/BlazorAdmin/Services/CachedCatalogTypeServiceDecorator.cs
@@ -0,0 +1,57 @@
+using Blazored.LocalStorage;
+using BlazorShared.Interfaces;
+using BlazorShared.Models;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace BlazorAdmin.Services
+{
+ public class CachedCatalogTypeServiceDecorator : ICatalogTypeService
+ {
+ // TODO: Make a generic decorator for any LookupData type
+ private readonly ILocalStorageService _localStorageService;
+ private readonly CatalogTypeService _catalogTypeService;
+ private ILogger _logger;
+
+ public CachedCatalogTypeServiceDecorator(ILocalStorageService localStorageService,
+ CatalogTypeService catalogTypeService,
+ ILogger logger)
+ {
+ _localStorageService = localStorageService;
+ _catalogTypeService = catalogTypeService;
+ _logger = logger;
+ }
+
+ public async Task GetById(int id)
+ {
+ return (await List()).FirstOrDefault(x => x.Id == id);
+ }
+
+ public async Task> List()
+ {
+ string key = "types";
+ var cacheEntry = await _localStorageService.GetItemAsync>>(key);
+ if (cacheEntry != null)
+ {
+ _logger.LogInformation("Loading types from local storage.");
+ if (cacheEntry.DateCreated.AddMinutes(1) > DateTime.UtcNow)
+ {
+ return cacheEntry.Value;
+ }
+ else
+ {
+ _logger.LogInformation("Cache expired; removing types from local storage.");
+ await _localStorageService.RemoveItemAsync(key);
+ }
+ }
+
+ var types = await _catalogTypeService.List();
+ var entry = new CacheEntry>(types);
+ await _localStorageService.SetItemAsync(key, entry);
+ return types;
+ }
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogBrandService.cs b/src/BlazorAdmin/Services/CatalogBrandService.cs
new file mode 100644
index 0000000..263d6ea
--- /dev/null
+++ b/src/BlazorAdmin/Services/CatalogBrandService.cs
@@ -0,0 +1,41 @@
+using BlazorShared;
+using BlazorShared.Interfaces;
+using BlazorShared.Models;
+using Microsoft.Extensions.Logging;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Threading.Tasks;
+
+
+namespace BlazorAdmin.Services
+{
+ public class CatalogBrandService : ICatalogBrandService
+ {
+ // TODO: Make a generic service for any LookupData type
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+ private string _apiUrl;
+
+ public CatalogBrandService(HttpClient httpClient,
+ BaseUrlConfiguration baseUrlConfiguration,
+ ILogger logger)
+ {
+ _httpClient = httpClient;
+ _logger = logger;
+ _apiUrl = baseUrlConfiguration.ApiBase;
+ }
+
+ public async Task GetById(int id)
+ {
+ return (await List()).FirstOrDefault(x => x.Id == id);
+ }
+
+ public async Task> List()
+ {
+ _logger.LogInformation("Fetching brands from API.");
+ return (await _httpClient.GetFromJsonAsync($"{_apiUrl}catalog-brands"))?.CatalogBrands;
+ }
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogBrandServices/List.CatalogBrand.cs b/src/BlazorAdmin/Services/CatalogBrandServices/List.CatalogBrand.cs
deleted file mode 100644
index b8d8a5b..0000000
--- a/src/BlazorAdmin/Services/CatalogBrandServices/List.CatalogBrand.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace BlazorAdmin.Services.CatalogBrandServices
-{
- public class CatalogBrand
- {
- public int Id { get; set; }
- public string Name { get; set; }
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogBrandServices/List.cs b/src/BlazorAdmin/Services/CatalogBrandServices/List.cs
deleted file mode 100644
index 454840a..0000000
--- a/src/BlazorAdmin/Services/CatalogBrandServices/List.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Http.Json;
-using System.Threading.Tasks;
-
-namespace BlazorAdmin.Services.CatalogBrandServices
-{
- public class List
- {
- private readonly AuthService _authService;
- private readonly HttpClient _httpClient;
-
- public List(AuthService authService, HttpClient httpClient)
- {
- _authService = authService;
- _httpClient = httpClient;
- }
-
- public async Task> HandleAsync()
- {
- return (await _httpClient.GetFromJsonAsync($"{_authService.ApiUrl}catalog-brands"))?.CatalogBrands;
- }
-
- public static string GetBrandName(IEnumerable brands, int brandId)
- {
- var type = brands.FirstOrDefault(t => t.Id == brandId);
-
- return type == null ? "None" : type.Name;
- }
-
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemService.cs b/src/BlazorAdmin/Services/CatalogItemService.cs
new file mode 100644
index 0000000..a33afae
--- /dev/null
+++ b/src/BlazorAdmin/Services/CatalogItemService.cs
@@ -0,0 +1,102 @@
+using BlazorShared;
+using BlazorShared.Interfaces;
+using BlazorShared.Models;
+using Microsoft.Extensions.Logging;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+
+namespace BlazorAdmin.Services
+{
+ public class CatalogItemService : ICatalogItemService
+ {
+ private readonly ICatalogBrandService _brandService;
+ private readonly ICatalogTypeService _typeService;
+ private readonly HttpService _httpService;
+ private readonly ILogger _logger;
+ private string _apiUrl;
+
+ public CatalogItemService(ICatalogBrandService brandService,
+ ICatalogTypeService typeService,
+ HttpService httpService,
+ BaseUrlConfiguration baseUrlConfiguration,
+ ILogger logger)
+ {
+ _brandService = brandService;
+ _typeService = typeService;
+
+ _httpService = httpService;
+ _logger = logger;
+ _apiUrl = baseUrlConfiguration.ApiBase;
+ }
+
+ public async Task Create(CreateCatalogItemRequest catalogItem)
+ {
+ return (await _httpService.HttpPost("catalog-items", catalogItem)).CatalogItem;
+ }
+
+ public async Task Edit(CatalogItem catalogItem)
+ {
+ return (await _httpService.HttpPut("catalog-items", catalogItem)).CatalogItem;
+ }
+
+ public async Task Delete(int catalogItemId)
+ {
+ return (await _httpService.HttpDelete("catalog-items", catalogItemId)).Status;
+ }
+
+ public async Task GetById(int id)
+ {
+ var brandListTask = _brandService.List();
+ var typeListTask = _typeService.List();
+ var itemGetTask = _httpService.HttpGet($"catalog-items/{id}");
+ await Task.WhenAll(brandListTask, typeListTask, itemGetTask);
+ var brands = brandListTask.Result;
+ var types = typeListTask.Result;
+ var catalogItem = itemGetTask.Result.CatalogItem;
+ catalogItem.CatalogBrand = brands.FirstOrDefault(b => b.Id == catalogItem.CatalogBrandId)?.Name;
+ catalogItem.CatalogType = types.FirstOrDefault(t => t.Id == catalogItem.CatalogTypeId)?.Name;
+ return catalogItem;
+ }
+
+ public async Task> ListPaged(int pageSize)
+ {
+ _logger.LogInformation("Fetching catalog items from API.");
+
+ var brandListTask = _brandService.List();
+ var typeListTask = _typeService.List();
+ var itemListTask = _httpService.HttpGet($"catalog-items?PageSize=10");
+ await Task.WhenAll(brandListTask, typeListTask, itemListTask);
+ var brands = brandListTask.Result;
+ var types = typeListTask.Result;
+ var items = itemListTask.Result.CatalogItems;
+ foreach (var item in items)
+ {
+ item.CatalogBrand = brands.FirstOrDefault(b => b.Id == item.CatalogBrandId)?.Name;
+ item.CatalogType = types.FirstOrDefault(t => t.Id == item.CatalogTypeId)?.Name;
+ }
+ return items;
+ }
+
+ public async Task> List()
+ {
+ _logger.LogInformation("Fetching catalog items from API.");
+
+ var brandListTask = _brandService.List();
+ var typeListTask = _typeService.List();
+ //TODO: Need to change the api to support full list
+ var itemListTask = _httpService.HttpGet($"catalog-items?PageSize=100");
+ await Task.WhenAll(brandListTask, typeListTask, itemListTask);
+ var brands = brandListTask.Result;
+ var types = typeListTask.Result;
+ var items = itemListTask.Result.CatalogItems;
+ foreach (var item in items)
+ {
+ item.CatalogBrand = brands.FirstOrDefault(b => b.Id == item.CatalogBrandId)?.Name;
+ item.CatalogType = types.FirstOrDefault(t => t.Id == item.CatalogTypeId)?.Name;
+ }
+ return items;
+ }
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/Create.CreateCatalogItemResult.cs b/src/BlazorAdmin/Services/CatalogItemServices/Create.CreateCatalogItemResult.cs
deleted file mode 100644
index 37cf03f..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/Create.CreateCatalogItemResult.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class CreateCatalogItemResult
- {
- public CatalogItem CatalogItem { get; set; } = new CatalogItem();
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/Create.cs b/src/BlazorAdmin/Services/CatalogItemServices/Create.cs
deleted file mode 100644
index 74b9cc8..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/Create.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading.Tasks;
-
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class Create
- {
- private readonly HttpService _httpService;
-
- public Create(AuthService authService)
- {
- _httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
- }
-
- public async Task HandleAsync(CreateCatalogItemRequest catalogItem)
- {
- return (await _httpService.HttpPost("catalog-items", catalogItem)).CatalogItem;
- }
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/Delete.DeleteCatalogItemResult.cs b/src/BlazorAdmin/Services/CatalogItemServices/Delete.DeleteCatalogItemResult.cs
deleted file mode 100644
index 8436815..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/Delete.DeleteCatalogItemResult.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class DeleteCatalogItemResult
- {
- public string Status { get; set; } = "Deleted";
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/Delete.cs b/src/BlazorAdmin/Services/CatalogItemServices/Delete.cs
deleted file mode 100644
index 86f82b9..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/Delete.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading.Tasks;
-
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class Delete
- {
- private readonly HttpService _httpService;
-
- public Delete(AuthService authService)
- {
- _httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
- }
-
- public async Task HandleAsync(int catalogItemId)
- {
- return (await _httpService.HttpDelete("catalog-items", catalogItemId)).Status;
- }
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/Edit.cs b/src/BlazorAdmin/Services/CatalogItemServices/Edit.cs
deleted file mode 100644
index a5fd6d3..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/Edit.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading.Tasks;
-
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class Edit
- {
- private readonly HttpService _httpService;
-
- public Edit(AuthService authService)
- {
- _httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
- }
-
- public async Task HandleAsync(CatalogItem catalogItem)
- {
- return (await _httpService.HttpPut("catalog-items", catalogItem)).CatalogItem;
- }
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/GetById.GetByIdCatalogItemResult.cs b/src/BlazorAdmin/Services/CatalogItemServices/GetById.GetByIdCatalogItemResult.cs
deleted file mode 100644
index e7e9f1d..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/GetById.GetByIdCatalogItemResult.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class GetByIdCatalogItemResult
- {
- public CatalogItem CatalogItem { get; set; } = new CatalogItem();
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/GetById.cs b/src/BlazorAdmin/Services/CatalogItemServices/GetById.cs
deleted file mode 100644
index 01347f3..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/GetById.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading.Tasks;
-
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class GetById
- {
- private readonly HttpService _httpService;
-
- public GetById(AuthService authService)
- {
- _httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
- }
-
- public async Task HandleAsync(int catalogItemId)
- {
- return (await _httpService.HttpGet($"catalog-items/{catalogItemId}")).CatalogItem;
- }
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/ListPaged.cs b/src/BlazorAdmin/Services/CatalogItemServices/ListPaged.cs
deleted file mode 100644
index 85a1e10..0000000
--- a/src/BlazorAdmin/Services/CatalogItemServices/ListPaged.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace BlazorAdmin.Services.CatalogItemServices
-{
- public class ListPaged
- {
- private readonly HttpService _httpService;
-
- public ListPaged(AuthService authService)
- {
- _httpService = new HttpService(authService.GetHttpClient(), authService.ApiUrl);
- }
-
- public async Task> HandleAsync(int pageSize)
- {
- return (await _httpService.HttpGet($"catalog-items?PageSize={pageSize}")).CatalogItems;
- }
-
- }
-}
\ No newline at end of file
diff --git a/src/BlazorAdmin/Services/CatalogTypeService.cs b/src/BlazorAdmin/Services/CatalogTypeService.cs
new file mode 100644
index 0000000..a41fc7d
--- /dev/null
+++ b/src/BlazorAdmin/Services/CatalogTypeService.cs
@@ -0,0 +1,40 @@
+using BlazorShared;
+using BlazorShared.Interfaces;
+using BlazorShared.Models;
+using Microsoft.Extensions.Logging;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Threading.Tasks;
+
+namespace BlazorAdmin.Services
+{
+ public class CatalogTypeService : ICatalogTypeService
+ {
+ // TODO: Make a generic service for any LookupData type
+ private readonly HttpClient _httpClient;
+ private readonly ILogger _logger;
+ private string _apiUrl;
+
+ public CatalogTypeService(HttpClient httpClient,
+ BaseUrlConfiguration baseUrlConfiguration,
+ ILogger logger)
+ {
+ _httpClient = httpClient;
+ _logger = logger;
+ _apiUrl = baseUrlConfiguration.ApiBase;
+ }
+
+ public async Task GetById(int id)
+ {
+ return (await List()).FirstOrDefault(x => x.Id == id);
+ }
+
+ public async Task> List()
+ {
+ _logger.LogInformation("Fetching types from API.");
+ return (await _httpClient.GetFromJsonAsync($"{_apiUrl}catalog-types"))?.CatalogTypes;
+ }
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogTypeServices/List.CatalogType.cs b/src/BlazorAdmin/Services/CatalogTypeServices/List.CatalogType.cs
deleted file mode 100644
index 0a22de3..0000000
--- a/src/BlazorAdmin/Services/CatalogTypeServices/List.CatalogType.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace BlazorAdmin.Services.CatalogTypeServices
-{
- public class CatalogType
- {
- public int Id { get; set; }
- public string Name { get; set; }
- }
-}
diff --git a/src/BlazorAdmin/Services/CatalogTypeServices/List.cs b/src/BlazorAdmin/Services/CatalogTypeServices/List.cs
deleted file mode 100644
index 174fb80..0000000
--- a/src/BlazorAdmin/Services/CatalogTypeServices/List.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Http.Json;
-using System.Threading.Tasks;
-
-namespace BlazorAdmin.Services.CatalogTypeServices
-{
- public class List
- {
- private readonly AuthService _authService;
- private readonly HttpClient _httpClient;
-
- public List(AuthService authService, HttpClient httpClient)
- {
- _authService = authService;
- _httpClient = httpClient;
- }
-
- public async Task> HandleAsync()
- {
- return (await _httpClient.GetFromJsonAsync($"{_authService.ApiUrl}catalog-types"))?.CatalogTypes;
- }
-
- public static string GetTypeName(IEnumerable types, int typeId)
- {
- var type = types.FirstOrDefault(t => t.Id == typeId);
-
- return type == null ? "None" : type.Name;
- }
-
- }
-}
diff --git a/src/BlazorAdmin/Services/HttpService.cs b/src/BlazorAdmin/Services/HttpService.cs
index 7344864..489a7a4 100644
--- a/src/BlazorAdmin/Services/HttpService.cs
+++ b/src/BlazorAdmin/Services/HttpService.cs
@@ -1,4 +1,5 @@
-using System.Net.Http;
+using BlazorShared;
+using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
@@ -10,10 +11,11 @@ namespace BlazorAdmin.Services
private readonly HttpClient _httpClient;
private readonly string _apiUrl;
- public HttpService(HttpClient httpClient, string apiUrl)
+
+ public HttpService(HttpClient httpClient, BaseUrlConfiguration baseUrlConfiguration)
{
_httpClient = httpClient;
- _apiUrl = apiUrl;
+ _apiUrl = baseUrlConfiguration.ApiBase;
}
public async Task HttpGet(string uri)
diff --git a/src/BlazorAdmin/ServicesConfiguration.cs b/src/BlazorAdmin/ServicesConfiguration.cs
index dc13468..fb4302b 100644
--- a/src/BlazorAdmin/ServicesConfiguration.cs
+++ b/src/BlazorAdmin/ServicesConfiguration.cs
@@ -1,22 +1,21 @@
-using BlazorAdmin.Services.CatalogItemServices;
+using BlazorAdmin.Services;
+using BlazorShared.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace BlazorAdmin
{
public static class ServicesConfiguration
{
- public static IServiceCollection AddBlazorServices(this IServiceCollection service)
+ public static IServiceCollection AddBlazorServices(this IServiceCollection services)
{
- service.AddScoped();
- service.AddScoped();
- service.AddScoped();
- service.AddScoped();
- service.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
- service.AddScoped();
- service.AddScoped();
-
- return service;
+ return services;
}
}
}
diff --git a/src/BlazorAdmin/Shared/CustomInputSelect.cs b/src/BlazorAdmin/Shared/CustomInputSelect.cs
new file mode 100644
index 0000000..238a79a
--- /dev/null
+++ b/src/BlazorAdmin/Shared/CustomInputSelect.cs
@@ -0,0 +1,38 @@
+using Microsoft.AspNetCore.Components.Forms;
+
+namespace BlazorAdmin.Shared
+{
+ ///
+ /// This is needed until 5.0 ships with native support
+ /// https://www.pragimtech.com/blog/blazor/inputselect-does-not-support-system.int32/
+ ///
+ ///
+ public class CustomInputSelect : InputSelect
+ {
+ protected override bool TryParseValueFromString(string value, out TValue result,
+ out string validationErrorMessage)
+ {
+ if (typeof(TValue) == typeof(int))
+ {
+ if (int.TryParse(value, out var resultInt))
+ {
+ result = (TValue)(object)resultInt;
+ validationErrorMessage = null;
+ return true;
+ }
+ else
+ {
+ result = default;
+ validationErrorMessage =
+ $"The selected value {value} is not a valid number.";
+ return false;
+ }
+ }
+ else
+ {
+ return base.TryParseValueFromString(value, out result,
+ out validationErrorMessage);
+ }
+ }
+ }
+}
diff --git a/src/BlazorAdmin/Shared/MainLayout.razor b/src/BlazorAdmin/Shared/MainLayout.razor
index c9438fa..7070aae 100644
--- a/src/BlazorAdmin/Shared/MainLayout.razor
+++ b/src/BlazorAdmin/Shared/MainLayout.razor
@@ -1,4 +1,4 @@
-@inject AuthService Auth
+@inject AuthenticationStateProvider AuthStateProvider
@inject IJSRuntime JSRuntime
@inherits BlazorAdmin.Helpers.BlazorLayoutComponent
@@ -25,8 +25,9 @@
{
if (firstRender)
{
- await Auth.RefreshLoginInfoFromCookie();
- if (!Auth.IsLoggedIn)
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
+
+ if(authState.User == null)
{
await new Route(JSRuntime).RouteOutside("/Identity/Account/Login");
}
@@ -35,5 +36,4 @@
await base.OnAfterRenderAsync(firstRender);
}
-
}
diff --git a/src/BlazorAdmin/Shared/NavMenu.razor b/src/BlazorAdmin/Shared/NavMenu.razor
index 129eb57..49c44cd 100644
--- a/src/BlazorAdmin/Shared/NavMenu.razor
+++ b/src/BlazorAdmin/Shared/NavMenu.razor
@@ -1,5 +1,4 @@
-@inject AuthService Auth
-@inherits BlazorAdmin.Helpers.BlazorComponent
+@inherits BlazorAdmin.Helpers.BlazorComponent
eShopOnWeb Admin
@code {
private bool collapseNavMenu = true;
- public string UserName { get; set; }
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
diff --git a/src/BlazorAdmin/_Imports.razor b/src/BlazorAdmin/_Imports.razor
index 3d938ec..c53df61 100644
--- a/src/BlazorAdmin/_Imports.razor
+++ b/src/BlazorAdmin/_Imports.razor
@@ -11,9 +11,8 @@
@using BlazorAdmin
@using BlazorAdmin.Shared
@using BlazorAdmin.Services
-@using BlazorAdmin.Services.CatalogBrandServices
-@using BlazorAdmin.Services.CatalogItemServices
-@using BlazorAdmin.Services.CatalogTypeServices
@using BlazorAdmin.JavaScript
@using BlazorShared.Authorization
+@using BlazorShared.Interfaces
@using BlazorInputFile
+@using BlazorShared.Models
diff --git a/src/BlazorAdmin/wwwroot/appsettings.Docker.json b/src/BlazorAdmin/wwwroot/appsettings.Docker.json
new file mode 100644
index 0000000..98445e3
--- /dev/null
+++ b/src/BlazorAdmin/wwwroot/appsettings.Docker.json
@@ -0,0 +1,6 @@
+{
+ "baseUrls": {
+ "apiBase": "http://localhost:5200/api/",
+ "webBase": "http://host.docker.internal:5106/"
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorAdmin/wwwroot/appsettings.json b/src/BlazorAdmin/wwwroot/appsettings.json
new file mode 100644
index 0000000..cefa244
--- /dev/null
+++ b/src/BlazorAdmin/wwwroot/appsettings.json
@@ -0,0 +1,14 @@
+{
+ "baseUrls": {
+ "apiBase": "https://localhost:5099/api/",
+ "webBase": "https://localhost:44315/"
+ },
+ "Logging": {
+ "IncludeScopes": false,
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "System": "Warning"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/BlazorAdmin/wwwroot/sample-data/weather.json b/src/BlazorAdmin/wwwroot/sample-data/weather.json
deleted file mode 100644
index 06463c0..0000000
--- a/src/BlazorAdmin/wwwroot/sample-data/weather.json
+++ /dev/null
@@ -1,27 +0,0 @@
-[
- {
- "date": "2018-05-06",
- "temperatureC": 1,
- "summary": "Freezing"
- },
- {
- "date": "2018-05-07",
- "temperatureC": 14,
- "summary": "Bracing"
- },
- {
- "date": "2018-05-08",
- "temperatureC": -13,
- "summary": "Freezing"
- },
- {
- "date": "2018-05-09",
- "temperatureC": -16,
- "summary": "Balmy"
- },
- {
- "date": "2018-05-10",
- "temperatureC": -2,
- "summary": "Chilly"
- }
-]
diff --git a/src/BlazorShared/Authorization/Constants.cs b/src/BlazorShared/Authorization/Constants.cs
index d787c83..4ddc121 100644
--- a/src/BlazorShared/Authorization/Constants.cs
+++ b/src/BlazorShared/Authorization/Constants.cs
@@ -6,23 +6,5 @@
{
public const string ADMINISTRATORS = "Administrators";
}
- public static string GetApiUrl(bool inDocker) =>
- inDocker ? DOCKER_API_URL : API_URL;
-
- public static string GetWebUrl(bool inDocker) =>
- inDocker ? DOCKER_WEB_URL : WEB_URL;
-
- public static string GetWebUrlInternal(bool inDocker) =>
- inDocker ? DOCKER_WEB_URL.Replace("localhost", "host.docker.internal") : WEB_URL;
-
- public static string GetOriginWebUrl(bool inDocker) =>
- GetWebUrl(inDocker).TrimEnd('/');
-
- private const string API_URL = "https://localhost:5099/api/";
- private const string DOCKER_API_URL = "http://localhost:5200/api/";
-
- private const string WEB_URL = "https://localhost:44315/";
- private const string DOCKER_WEB_URL = "http://localhost:5106/";
-
}
}
diff --git a/src/BlazorShared/Authorization/UserInfo.cs b/src/BlazorShared/Authorization/UserInfo.cs
index 19cbd11..9ae24b8 100644
--- a/src/BlazorShared/Authorization/UserInfo.cs
+++ b/src/BlazorShared/Authorization/UserInfo.cs
@@ -8,6 +8,7 @@ namespace BlazorShared.Authorization
public bool IsAuthenticated { get; set; }
public string NameClaimType { get; set; }
public string RoleClaimType { get; set; }
+ public string Token { get; set; }
public IEnumerable Claims { get; set; }
}
}
diff --git a/src/BlazorShared/BaseUrlConfiguration.cs b/src/BlazorShared/BaseUrlConfiguration.cs
new file mode 100644
index 0000000..419735f
--- /dev/null
+++ b/src/BlazorShared/BaseUrlConfiguration.cs
@@ -0,0 +1,10 @@
+namespace BlazorShared
+{
+ public class BaseUrlConfiguration
+ {
+ public const string CONFIG_NAME = "baseUrls";
+
+ public string ApiBase { get; set; }
+ public string WebBase { get; set; }
+ }
+}
diff --git a/src/BlazorShared/BlazorShared.csproj b/src/BlazorShared/BlazorShared.csproj
index 07b614b..0e231e9 100644
--- a/src/BlazorShared/BlazorShared.csproj
+++ b/src/BlazorShared/BlazorShared.csproj
@@ -6,4 +6,8 @@
BlazorShared
+
+
+
+
diff --git a/src/BlazorShared/Interfaces/ICatalogBrandService.cs b/src/BlazorShared/Interfaces/ICatalogBrandService.cs
new file mode 100644
index 0000000..ba2f920
--- /dev/null
+++ b/src/BlazorShared/Interfaces/ICatalogBrandService.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BlazorShared.Models;
+
+namespace BlazorShared.Interfaces
+{
+ public interface ICatalogBrandService
+ {
+ Task> List();
+ Task GetById(int id);
+ }
+}
diff --git a/src/BlazorShared/Interfaces/ICatalogItemService.cs b/src/BlazorShared/Interfaces/ICatalogItemService.cs
new file mode 100644
index 0000000..3b277d3
--- /dev/null
+++ b/src/BlazorShared/Interfaces/ICatalogItemService.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BlazorShared.Models;
+
+namespace BlazorShared.Interfaces
+{
+ public interface ICatalogItemService
+ {
+ Task Create(CreateCatalogItemRequest catalogItem);
+ Task Edit(CatalogItem catalogItem);
+ Task Delete(int id);
+ Task GetById(int id);
+ Task> ListPaged(int pageSize);
+ Task> List();
+ }
+}
diff --git a/src/BlazorShared/Interfaces/ICatalogTypeService.cs b/src/BlazorShared/Interfaces/ICatalogTypeService.cs
new file mode 100644
index 0000000..437ff34
--- /dev/null
+++ b/src/BlazorShared/Interfaces/ICatalogTypeService.cs
@@ -0,0 +1,12 @@
+using BlazorShared.Models;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace BlazorShared.Interfaces
+{
+ public interface ICatalogTypeService
+ {
+ Task> List();
+ Task GetById(int id);
+ }
+}
diff --git a/src/BlazorShared/Models/CatalogBrand.cs b/src/BlazorShared/Models/CatalogBrand.cs
new file mode 100644
index 0000000..631a23b
--- /dev/null
+++ b/src/BlazorShared/Models/CatalogBrand.cs
@@ -0,0 +1,6 @@
+namespace BlazorShared.Models
+{
+ public class CatalogBrand : LookupData
+ {
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogBrandServices/List.CatalogBrandResult.cs b/src/BlazorShared/Models/CatalogBrandResponse.cs
similarity index 62%
rename from src/BlazorAdmin/Services/CatalogBrandServices/List.CatalogBrandResult.cs
rename to src/BlazorShared/Models/CatalogBrandResponse.cs
index 88493ea..ba01032 100644
--- a/src/BlazorAdmin/Services/CatalogBrandServices/List.CatalogBrandResult.cs
+++ b/src/BlazorShared/Models/CatalogBrandResponse.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
-namespace BlazorAdmin.Services.CatalogBrandServices
+namespace BlazorShared.Models
{
- public class CatalogBrandResult
+ public class CatalogBrandResponse
{
public List CatalogBrands { get; set; } = new List();
}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/CatalogItem.cs b/src/BlazorShared/Models/CatalogItem.cs
similarity index 79%
rename from src/BlazorAdmin/Services/CatalogItemServices/CatalogItem.cs
rename to src/BlazorShared/Models/CatalogItem.cs
index a732225..ddd0d00 100644
--- a/src/BlazorAdmin/Services/CatalogItemServices/CatalogItem.cs
+++ b/src/BlazorShared/Models/CatalogItem.cs
@@ -4,15 +4,17 @@ using System.IO;
using System.Threading.Tasks;
using BlazorInputFile;
-namespace BlazorAdmin.Services.CatalogItemServices
+namespace BlazorShared.Models
{
public class CatalogItem
{
public int Id { get; set; }
public int CatalogTypeId { get; set; }
+ public string CatalogType { get; set; } = "NotSet";
public int CatalogBrandId { get; set; }
+ public string CatalogBrand { get; set; } = "NotSet";
[Required(ErrorMessage = "The Name field is required")]
public string Name { get; set; }
@@ -60,14 +62,17 @@ namespace BlazorAdmin.Services.CatalogItemServices
public static async Task DataToBase64(IFileListEntry fileItem)
{
- using var reader = new StreamReader(fileItem.Data);
+ using ( var reader = new StreamReader(fileItem.Data))
+ {
+ using (var memStream = new MemoryStream())
+ {
+ await reader.BaseStream.CopyToAsync(memStream);
+ var fileData = memStream.ToArray();
+ var encodedBase64 = Convert.ToBase64String(fileData);
- await using var memStream = new MemoryStream();
- await reader.BaseStream.CopyToAsync(memStream);
- var fileData = memStream.ToArray();
- var encodedBase64 = Convert.ToBase64String(fileData);
-
- return encodedBase64;
+ return encodedBase64;
+ }
+ }
}
private static bool IsExtensionValid(string fileName)
diff --git a/src/BlazorShared/Models/CatalogType.cs b/src/BlazorShared/Models/CatalogType.cs
new file mode 100644
index 0000000..ed7dad9
--- /dev/null
+++ b/src/BlazorShared/Models/CatalogType.cs
@@ -0,0 +1,6 @@
+namespace BlazorShared.Models
+{
+ public class CatalogType : LookupData
+ {
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogTypeServices/List.CatalogTypeResult.cs b/src/BlazorShared/Models/CatalogTypeResponse.cs
similarity index 62%
rename from src/BlazorAdmin/Services/CatalogTypeServices/List.CatalogTypeResult.cs
rename to src/BlazorShared/Models/CatalogTypeResponse.cs
index a897967..664732e 100644
--- a/src/BlazorAdmin/Services/CatalogTypeServices/List.CatalogTypeResult.cs
+++ b/src/BlazorShared/Models/CatalogTypeResponse.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
-namespace BlazorAdmin.Services.CatalogTypeServices
+namespace BlazorShared.Models
{
- public class CatalogTypeResult
+ public class CatalogTypeResponse
{
public List CatalogTypes { get; set; } = new List();
}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/Create.CreateCatalogItemRequest.cs b/src/BlazorShared/Models/CreateCatalogItemRequest.cs
similarity index 94%
rename from src/BlazorAdmin/Services/CatalogItemServices/Create.CreateCatalogItemRequest.cs
rename to src/BlazorShared/Models/CreateCatalogItemRequest.cs
index 1c4a343..0894109 100644
--- a/src/BlazorAdmin/Services/CatalogItemServices/Create.CreateCatalogItemRequest.cs
+++ b/src/BlazorShared/Models/CreateCatalogItemRequest.cs
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
-namespace BlazorAdmin.Services.CatalogItemServices
+namespace BlazorShared.Models
{
public class CreateCatalogItemRequest
{
diff --git a/src/BlazorShared/Models/CreateCatalogItemResponse.cs b/src/BlazorShared/Models/CreateCatalogItemResponse.cs
new file mode 100644
index 0000000..e51d3ff
--- /dev/null
+++ b/src/BlazorShared/Models/CreateCatalogItemResponse.cs
@@ -0,0 +1,7 @@
+namespace BlazorShared.Models
+{
+ public class CreateCatalogItemResponse
+ {
+ public CatalogItem CatalogItem { get; set; } = new CatalogItem();
+ }
+}
diff --git a/src/BlazorShared/Models/DeleteCatalogItemResponse.cs b/src/BlazorShared/Models/DeleteCatalogItemResponse.cs
new file mode 100644
index 0000000..3cd1e22
--- /dev/null
+++ b/src/BlazorShared/Models/DeleteCatalogItemResponse.cs
@@ -0,0 +1,7 @@
+namespace BlazorShared.Models
+{
+ public class DeleteCatalogItemResponse
+ {
+ public string Status { get; set; } = "Deleted";
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/Edit.EditCatalogItemResult.cs b/src/BlazorShared/Models/EditCatalogItemResponse.cs
similarity index 70%
rename from src/BlazorAdmin/Services/CatalogItemServices/Edit.EditCatalogItemResult.cs
rename to src/BlazorShared/Models/EditCatalogItemResponse.cs
index 9f303eb..ed690d6 100644
--- a/src/BlazorAdmin/Services/CatalogItemServices/Edit.EditCatalogItemResult.cs
+++ b/src/BlazorShared/Models/EditCatalogItemResponse.cs
@@ -1,4 +1,4 @@
-namespace BlazorAdmin.Services.CatalogItemServices
+namespace BlazorShared.Models
{
public class EditCatalogItemResult
{
diff --git a/src/BlazorShared/Models/LookupData.cs b/src/BlazorShared/Models/LookupData.cs
new file mode 100644
index 0000000..53e029b
--- /dev/null
+++ b/src/BlazorShared/Models/LookupData.cs
@@ -0,0 +1,8 @@
+namespace BlazorShared.Models
+{
+ public abstract class LookupData
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+}
diff --git a/src/BlazorAdmin/Services/CatalogItemServices/ListPaged.PagedCatalogItemResult.cs b/src/BlazorShared/Models/PagedCatalogItemResponse.cs
similarity index 67%
rename from src/BlazorAdmin/Services/CatalogItemServices/ListPaged.PagedCatalogItemResult.cs
rename to src/BlazorShared/Models/PagedCatalogItemResponse.cs
index 531e78a..3395b1c 100644
--- a/src/BlazorAdmin/Services/CatalogItemServices/ListPaged.PagedCatalogItemResult.cs
+++ b/src/BlazorShared/Models/PagedCatalogItemResponse.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
-namespace BlazorAdmin.Services.CatalogItemServices
+namespace BlazorShared.Models
{
- public class PagedCatalogItemResult
+ public class PagedCatalogItemResponse
{
public List CatalogItems { get; set; } = new List();
public int PageCount { get; set; } = 0;
diff --git a/src/PublicApi/CatalogItemEndpoints/Create.cs b/src/PublicApi/CatalogItemEndpoints/Create.cs
index 68a6b2c..4b946b8 100644
--- a/src/PublicApi/CatalogItemEndpoints/Create.cs
+++ b/src/PublicApi/CatalogItemEndpoints/Create.cs
@@ -1,10 +1,8 @@
-using System;
-using System.IO;
+using System.IO;
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Swashbuckle.AspNetCore.Annotations;
diff --git a/src/PublicApi/Program.cs b/src/PublicApi/Program.cs
index 9d9b469..86d7537 100644
--- a/src/PublicApi/Program.cs
+++ b/src/PublicApi/Program.cs
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
namespace Microsoft.eShopWeb.PublicApi
{
@@ -43,6 +44,13 @@ namespace Microsoft.eShopWeb.PublicApi
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
+ .ConfigureAppConfiguration((builderContext, config) =>
+ {
+ var env = builderContext.HostingEnvironment;
+ config
+ .AddJsonFile("appsettings.json")
+ .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
+ })
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
diff --git a/src/PublicApi/Properties/launchSettings.json b/src/PublicApi/Properties/launchSettings.json
index 098f415..fbed524 100644
--- a/src/PublicApi/Properties/launchSettings.json
+++ b/src/PublicApi/Properties/launchSettings.json
@@ -25,13 +25,6 @@
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5099;http://localhost:5098"
- },
- "Docker": {
- "commandName": "Docker",
- "launchBrowser": true,
- "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
- "publishAllPorts": true,
- "useSSL": true
}
}
}
\ No newline at end of file
diff --git a/src/PublicApi/Startup.cs b/src/PublicApi/Startup.cs
index f0d0a20..ae00094 100644
--- a/src/PublicApi/Startup.cs
+++ b/src/PublicApi/Startup.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using AutoMapper;
+using BlazorShared;
using BlazorShared.Authorization;
using MediatR;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@@ -28,7 +29,6 @@ namespace Microsoft.eShopWeb.PublicApi
public class Startup
{
private const string CORS_POLICY = "CorsPolicy";
- public static bool InDocker => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
public Startup(IConfiguration configuration)
{
@@ -46,6 +46,11 @@ namespace Microsoft.eShopWeb.PublicApi
//ConfigureProductionServices(services);
}
+ public void ConfigureDockerServices(IServiceCollection services)
+ {
+ ConfigureDevelopmentServices(services);
+ }
+
private void ConfigureInMemoryDatabases(IServiceCollection services)
{
services.AddDbContext(c =>
@@ -90,7 +95,10 @@ namespace Microsoft.eShopWeb.PublicApi
services.AddSingleton(new UriComposer(Configuration.Get()));
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
services.AddScoped();
- services.AddScoped(x => new WebFileSystem($"{Constants.GetWebUrlInternal(Startup.InDocker)}File"));
+
+ var baseUrlConfig = new BaseUrlConfiguration();
+ Configuration.Bind(BaseUrlConfiguration.CONFIG_NAME, baseUrlConfig);
+ services.AddScoped(x => new WebFileSystem($"{baseUrlConfig.WebBase}File"));
services.AddMemoryCache();
@@ -112,15 +120,12 @@ namespace Microsoft.eShopWeb.PublicApi
};
});
-
services.AddCors(options =>
{
options.AddPolicy(name: CORS_POLICY,
builder =>
{
- builder.WithOrigins("http://localhost:44319",
- "https://localhost:44319",
- Constants.GetOriginWebUrl(InDocker));
+ builder.WithOrigins(baseUrlConfig.WebBase.Replace("host.docker.internal", "localhost").TrimEnd('/'));
builder.AllowAnyMethod();
builder.AllowAnyHeader();
});
diff --git a/src/PublicApi/appsettings.Development.json b/src/PublicApi/appsettings.Development.json
index 8983e0f..4af1b00 100644
--- a/src/PublicApi/appsettings.Development.json
+++ b/src/PublicApi/appsettings.Development.json
@@ -1,4 +1,8 @@
{
+ "baseUrls": {
+ "apiBase": "https://localhost:5099/api/",
+ "webBase": "https://localhost:44315/"
+ },
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/src/PublicApi/appsettings.Docker.json b/src/PublicApi/appsettings.Docker.json
new file mode 100644
index 0000000..f3bcd06
--- /dev/null
+++ b/src/PublicApi/appsettings.Docker.json
@@ -0,0 +1,17 @@
+{
+ "ConnectionStrings": {
+ "CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;",
+ "IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;"
+ },
+ "baseUrls": {
+ "apiBase": "http://localhost:5200/api/",
+ "webBase": "http://host.docker.internal:5106/"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/src/PublicApi/appsettings.json b/src/PublicApi/appsettings.json
index fab9773..13b6949 100644
--- a/src/PublicApi/appsettings.json
+++ b/src/PublicApi/appsettings.json
@@ -1,4 +1,8 @@
{
+ "baseUrls": {
+ "apiBase": "https://localhost:5099/api/",
+ "webBase": "https://localhost:44315/"
+ },
"ConnectionStrings": {
"CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;",
"IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;"
diff --git a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs
index 4c775f6..820136e 100644
--- a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs
+++ b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs
@@ -22,16 +22,12 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
private readonly SignInManager _signInManager;
private readonly ILogger _logger;
private readonly IBasketService _basketService;
- private readonly AuthService _authService;
- private readonly ITokenClaimsService _tokenClaimsService;
- public LoginModel(SignInManager signInManager, ILogger logger, IBasketService basketService, AuthService authService, ITokenClaimsService tokenClaimsService)
+ public LoginModel(SignInManager signInManager, ILogger logger, IBasketService basketService)
{
_signInManager = signInManager;
_logger = logger;
_basketService = basketService;
- _authService = authService;
- _tokenClaimsService = tokenClaimsService;
}
[BindProperty]
@@ -88,8 +84,6 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
if (result.Succeeded)
{
- var token = await _tokenClaimsService.GetTokenAsync(Input.Email);
- CreateAuthCookie(Input.Email, token, Startup.InDocker);
_logger.LogInformation("User logged in.");
await TransferAnonymousBasketToUserAsync(Input.Email);
return LocalRedirect(returnUrl);
@@ -114,15 +108,6 @@ namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
return Page();
}
- private void CreateAuthCookie(string username, string token, bool inDocker)
- {
- var cookieOptions = new CookieOptions();
- cookieOptions.Expires = DateTime.Today.AddYears(10);
- Response.Cookies.Append("token", token, cookieOptions);
- Response.Cookies.Append("username", username, cookieOptions);
- Response.Cookies.Append("inDocker", inDocker.ToString(), cookieOptions);
- }
-
private async Task TransferAnonymousBasketToUserAsync(string userName)
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
diff --git a/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml
index bacc0ae..ea2a0df 100644
--- a/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml
+++ b/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml
@@ -1,8 +1,8 @@
-
+
-
+
diff --git a/src/Web/Views/Shared/_ValidationScriptsPartial.cshtml b/src/Web/Views/Shared/_ValidationScriptsPartial.cshtml
index 27e0ea7..df42c5e 100644
--- a/src/Web/Views/Shared/_ValidationScriptsPartial.cshtml
+++ b/src/Web/Views/Shared/_ValidationScriptsPartial.cshtml
@@ -1,4 +1,4 @@
-
+
diff --git a/src/Web/appsettings.Development.json b/src/Web/appsettings.Development.json
index e203e94..bd6e47b 100644
--- a/src/Web/appsettings.Development.json
+++ b/src/Web/appsettings.Development.json
@@ -1,4 +1,8 @@
{
+ "baseUrls": {
+ "apiBase": "https://localhost:5099/api/",
+ "webBase": "https://localhost:44315/"
+ },
"Logging": {
"LogLevel": {
"Default": "Debug",
diff --git a/src/Web/appsettings.Docker.json b/src/Web/appsettings.Docker.json
new file mode 100644
index 0000000..0b4b57a
--- /dev/null
+++ b/src/Web/appsettings.Docker.json
@@ -0,0 +1,17 @@
+{
+ "ConnectionStrings": {
+ "CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;",
+ "IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;"
+ },
+ "baseUrls": {
+ "apiBase": "http://localhost:5200/api/",
+ "webBase": "http://host.docker.internal:5106/"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ }
+}
diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json
index dd8d5ff..c2bc659 100644
--- a/src/Web/appsettings.json
+++ b/src/Web/appsettings.json
@@ -1,4 +1,8 @@
{
+ "baseUrls": {
+ "apiBase": "https://localhost:5099/api/",
+ "webBase": "https://localhost:44315/"
+ },
"ConnectionStrings": {
"CatalogConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.CatalogDb;",
"IdentityConnection": "Server=(localdb)\\mssqllocaldb;Integrated Security=true;Initial Catalog=Microsoft.eShopOnWeb.Identity;"
diff --git a/src/Web/wwwroot/css/site.min.css b/src/Web/wwwroot/css/site.min.css
index dd35a47..50f91d7 100644
--- a/src/Web/wwwroot/css/site.min.css
+++ b/src/Web/wwwroot/css/site.min.css
@@ -1 +1 @@
-@font-face{font-family:Montserrat;font-weight:400;src:url(".../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%;bottom:0}.esh-app-footer-brand{height:50px;width:230px}.esh-app-header{margin:15px}.esh-app-wrapper{display:flex;min-height:100vh;flex-direction:column;justify-content:space-between}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-header-back:hover{color:#fff;transition:color .35s}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:10px;width:10rem;overflow:hidden;padding:.5rem;position:absolute;right:0;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:10rem;transition:height .35s}.esh-identity-item{cursor:pointer;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item.is-disabled{opacity:.5;pointer-events:none}.esh-pager-item--navigable{cursor:pointer;display:inline-block}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 2.5vw}}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-items-margin-left1{margin-left:1px}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus-badge-inoperative{background-color:#f00;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{-webkit-appearance:none;background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;min-width:140px;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-left:.5rem;margin-top:.65rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");content:'';height:7px;position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;margin-top:-1.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{margin-bottom:1.5rem;text-align:center;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:0;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{font-size:28px;font-weight:900;text-align:center}.esh-catalog-price::before{content:'$'}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}
\ No newline at end of file
+@font-face{font-family:Montserrat;font-weight:400;src:url(".../fonts/Montserrat-Regular.eot?") format("eot"),url("../fonts/Montserrat-Regular.woff") format("woff"),url("../fonts/Montserrat-Regular.ttf") format("truetype"),url("../fonts/Montserrat-Regular.svg#Montserrat") format("svg")}@font-face{font-family:Montserrat;font-weight:700;src:url("../fonts/Montserrat-Bold.eot?") format("eot"),url("../fonts/Montserrat-Bold.woff") format("woff"),url("../fonts/Montserrat-Bold.ttf") format("truetype"),url("../fonts/Montserrat-Bold.svg#Montserrat") format("svg")}html,body{font-family:Montserrat,sans-serif;font-size:16px;font-weight:400;z-index:10}*,*::after,*::before{box-sizing:border-box}.preloading{color:#00a69c;display:block;font-size:1.5rem;left:50%;position:fixed;top:50%;transform:translate(-50%,-50%)}select::-ms-expand{display:none}@media screen and (min-width:992px){.form-input{max-width:360px;width:360px}}.form-input{border-radius:0;height:45px;padding:10px}.form-input-small{max-width:100px !important}.form-input-medium{width:150px !important}.alert{padding-left:0}.alert-danger{background-color:transparent;border:0;color:#fb0d0d;font-size:12px}a,a:active,a:hover,a:visited{color:#000;text-decoration:none;transition:color .35s}a:hover,a:active{color:#75b918;transition:color .35s}.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%;bottom:0}.esh-app-footer-brand{height:50px;width:230px}.esh-app-header{margin:15px}.esh-app-wrapper{display:flex;min-height:100vh;flex-direction:column;justify-content:space-between}.esh-header{background-color:#00a69c;height:4rem}.esh-header-back{color:rgba(255,255,255,.5);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-header-back:hover{color:#fff;transition:color .35s}.esh-identity{line-height:3rem;position:relative;text-align:right}.esh-identity-section{display:inline-block;width:100%}.esh-identity-name{display:inline-block}.esh-identity-name--upper{text-transform:uppercase}@media screen and (max-width:768px){.esh-identity-name{font-size:.85rem}}.esh-identity-image{display:inline-block}.esh-identity-drop{background:#fff;height:10px;width:10rem;overflow:hidden;padding:.5rem;position:absolute;right:0;top:2.5rem;transition:height .35s}.esh-identity:hover .esh-identity-drop{border:1px solid #eee;height:14rem;transition:height .35s;z-index:10}.esh-identity-item{cursor:pointer;transition:color .35s}.esh-identity-item:hover{color:#75b918;transition:color .35s}.esh-pager-wrapper{padding-top:1rem;text-align:center}.esh-pager-item{margin:0 5vw}.esh-pager-item.is-disabled{opacity:.5;pointer-events:none}.esh-pager-item--navigable{cursor:pointer;display:inline-block}.esh-pager-item--navigable:hover{color:#83d01b}@media screen and (max-width:1280px){.esh-pager-item{font-size:.85rem}}@media screen and (max-width:1024px){.esh-pager-item{margin:0 2.5vw}}.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-items-margin-left1{margin-left:1px}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}.esh-basket-checkout:visited{color:#fff}.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus-badge-inoperative{background-color:#f00;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}.esh-catalog-hero{background-image:url("../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{-webkit-appearance:none;background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;min-width:140px;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-left:.5rem;margin-top:.65rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../images/arrow-down.png");content:'';height:7px;position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;margin-top:-1.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{margin-bottom:1.5rem;text-align:center;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:0;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{font-size:28px;font-weight:900;text-align:center}.esh-catalog-price::before{content:'$'}.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}.esh-orders-detail-section{padding-bottom:30px}.esh-orders-detail-title{font-size:25px}
\ No newline at end of file
diff --git a/src/Web/wwwroot/images/products/5.jpg b/src/Web/wwwroot/images/products/5.jpg
new file mode 100644
index 0000000..690649b
Binary files /dev/null and b/src/Web/wwwroot/images/products/5.jpg differ