diff --git a/src/MultiShop/Client/App.razor b/src/MultiShop/Client/App.razor index 7c5979f..426024b 100644 --- a/src/MultiShop/Client/App.razor +++ b/src/MultiShop/Client/App.razor @@ -1,9 +1,8 @@  - - - @if (modulesLoaded) - { - + + + + @if (!authState.User.Identity.IsAuthenticated) @@ -16,26 +15,25 @@ } - - } - else - { -
-
-
- Loading... -
-
-
- Downloading shop modules... + + + +

Sorry, there's nothing at this address.

+
+
+ + + +
+
+
+ Loading...
- } - - - -

Sorry, there's nothing at this address.

-
-
- +
+ Loading @Status... +
+
+
+ \ No newline at end of file diff --git a/src/MultiShop/Client/Extensions/UIProfileExtensions.cs b/src/MultiShop/Client/Extensions/UIProfileExtensions.cs new file mode 100644 index 0000000..1b8883a --- /dev/null +++ b/src/MultiShop/Client/Extensions/UIProfileExtensions.cs @@ -0,0 +1,22 @@ +using MultiShop.Shared.Models; + +namespace MultiShop.Client.Extensions +{ + public static class applicationProfileExtensions + { + public static string GetButtonCssClass(this ApplicationProfile applicationProfile, string otherClasses = "", bool outline = false) { + if (outline) { + return otherClasses + (applicationProfile.DarkMode ? " btn btn-outline-light" : " btn btn-outline-dark"); + } + return otherClasses + (applicationProfile.DarkMode ? " btn btn-light" : " btn btn-dark"); + } + + public static string GetPageCssClass(this ApplicationProfile applicationProfile, string otherClasses = "") { + return otherClasses + (applicationProfile.DarkMode ? " text-white bg-dark" : " text-dark bg-white"); + } + + public static string GetNavCssClass(this ApplicationProfile applicationProfile, string otherClasses = "") { + return otherClasses + (applicationProfile.DarkMode ? " navbar-dark bg-dark" : " navbar-light bg-light"); + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/App.razor.cs b/src/MultiShop/Client/Module/ShopModuleLoader.cs similarity index 57% rename from src/MultiShop/Client/App.razor.cs rename to src/MultiShop/Client/Module/ShopModuleLoader.cs index 25aae35..21c2342 100644 --- a/src/MultiShop/Client/App.razor.cs +++ b/src/MultiShop/Client/Module/ShopModuleLoader.cs @@ -1,53 +1,48 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.IO; using System.Net.Http; using System.Net.Http.Json; using System.Reflection; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; -using MultiShop.Client.Module; using MultiShop.Shop.Framework; -namespace MultiShop.Client +namespace MultiShop.Client.Module { - public partial class App : IDisposable + public class ShopModuleLoader { - [Inject] - private IHttpClientFactory HttpFactory { get; set; } - - [Inject] - private ILogger Logger {get; set; } + private HttpClient http; + private ILogger logger; + private string[] moduleNames; + private string[] dependencyNames; - private bool modulesLoaded = false; - - private Dictionary shops = new Dictionary(); - protected override async Task OnInitializedAsync() + public ShopModuleLoader(HttpClient http, ILogger logger) { - await base.OnInitializedAsync(); - await DownloadShopModules(); + this.http = http; + this.logger = logger; } - private async Task DownloadShopModules() - { - HttpClient http = HttpFactory.CreateClient("Public-MultiShop.ServerAPI"); + private async Task DownloadAssembliesList() { + moduleNames = await http.GetFromJsonAsync("ShopModule/Modules"); + dependencyNames = await http.GetFromJsonAsync("ShopModule/Dependencies"); + } + + private async Task> DownloadShopModuleAssemblies() { Dictionary assemblyData = new Dictionary(); - string[] moduleNames = await http.GetFromJsonAsync("ShopModule/Modules"); - string[] dependencyNames = await http.GetFromJsonAsync("ShopModule/Dependencies"); Dictionary, string> downloadTasks = new Dictionary, string>(); - Logger.LogInformation("Beginning to download shop modules..."); + logger.LogInformation("Beginning to download shop modules..."); foreach (string moduleName in moduleNames) { - Logger.LogDebug($"Downloading shop: {moduleName}"); + logger.LogDebug($"Downloading shop: {moduleName}"); downloadTasks.Add(http.GetByteArrayAsync("shopModule/Modules/" + moduleName), moduleName); } - Logger.LogInformation("Beginning to download shop module dependencies..."); + logger.LogInformation("Beginning to download shop module dependencies..."); foreach (string depName in dependencyNames) { - Logger.LogDebug($"Downloading shop module dependency: {depName}"); + logger.LogDebug($"Downloading shop module dependency: {depName}"); downloadTasks.Add(http.GetByteArrayAsync("ShopModule/Dependencies/" + depName), depName); } @@ -55,16 +50,26 @@ namespace MultiShop.Client { Task downloadTask = await Task.WhenAny(downloadTasks.Keys); assemblyData.Add(downloadTasks[downloadTask], await downloadTask); - Logger.LogDebug($"Shop module \"{downloadTasks[downloadTask]}\" completed downloading."); + logger.LogDebug($"Shop module \"{downloadTasks[downloadTask]}\" completed downloading."); downloadTasks.Remove(downloadTask); } - Logger.LogInformation($"Downloaded {assemblyData.Count} assemblies in total."); + logger.LogInformation($"Downloaded {assemblyData.Count} assemblies in total."); + return assemblyData; + } + + public async Task> GetShops() + { + await DownloadAssembliesList(); + + Dictionary shops = new Dictionary(); + + IReadOnlyDictionary assemblyData = await DownloadShopModuleAssemblies(); ShopModuleLoadContext context = new ShopModuleLoadContext(assemblyData); - Logger.LogInformation("Beginning to load shop modules."); + logger.LogInformation("Beginning to load shop modules."); foreach (string moduleName in moduleNames) { - Logger.LogDebug($"Attempting to load shop module: \"{moduleName}\""); + logger.LogDebug($"Attempting to load shop module: \"{moduleName}\""); Assembly moduleAssembly = context.LoadFromAssemblyName(new AssemblyName(moduleName)); bool shopLoaded = false; foreach (Type type in moduleAssembly.GetTypes()) @@ -75,32 +80,25 @@ namespace MultiShop.Client shopLoaded = true; shop.Initialize(); shops.Add(shop.ShopName, shop); - Logger.LogDebug($"Added shop: {shop.ShopName}"); + logger.LogDebug($"Added shop: {shop.ShopName}"); } } } if (!shopLoaded) { - Logger.LogWarning($"Module \"{moduleName}\" was reported to be a shop module, but did not contain a shop interface. Please report this to the site administrator."); + logger.LogWarning($"Module \"{moduleName}\" was reported to be a shop module, but did not contain a shop interface. Please report this to the site administrator."); } } - Logger.LogInformation($"Shop module loading complete. Loaded a total of {shops.Count} shops."); - modulesLoaded = true; + logger.LogInformation($"Shop module loading complete. Loaded a total of {shops.Count} shops."); foreach (string assemblyName in context.UseCounter.Keys) { int usage = context.UseCounter[assemblyName]; - Logger.LogDebug($"\"{assemblyName}\" was used {usage} times."); + logger.LogDebug($"\"{assemblyName}\" was used {usage} times."); if (usage <= 0) { - Logger.LogWarning($"\"{assemblyName}\" was not used. Please report this to the site administrator."); + logger.LogWarning($"\"{assemblyName}\" was not used. Please report this to the site administrator."); } } - } - - public void Dispose() - { - foreach (string name in shops.Keys) - { - shops[name].Dispose(); - } + + return shops; } } } \ No newline at end of file diff --git a/src/MultiShop/Client/Pages/Authentication.razor b/src/MultiShop/Client/Pages/Authentication.razor index d201654..8031540 100644 --- a/src/MultiShop/Client/Pages/Authentication.razor +++ b/src/MultiShop/Client/Pages/Authentication.razor @@ -5,7 +5,7 @@ -
+
Loading... @@ -17,7 +17,7 @@
-
+
Loading... @@ -28,6 +28,18 @@
+ +
+
+
+ Loading... +
+
+
+ Hang on just a sec. We're logging you in! +
+
+
@code{ diff --git a/src/MultiShop/Client/Pages/Configuration.razor b/src/MultiShop/Client/Pages/Configuration.razor new file mode 100644 index 0000000..e1f82ee --- /dev/null +++ b/src/MultiShop/Client/Pages/Configuration.razor @@ -0,0 +1,55 @@ +@using MultiShop.Client.Extensions +@page "/configure" + +
+ +
+ @switch (currentSection) + { + case Section.Opening: +

Configuration

+ For all your control-asserting needs. +

You can change how the app looks and operates. These changes will actually be saved to your account if you're logged in so your changes will be with you across different devices! Otherwise, we'll just stash them in cookies. Also know for your convenience, each option is followed by a short description of what changing that option will do. Get started by selecting a section!

+ break; + case Section.UI: +

UI

+
+
+ + +
+

Changes the UI to a dark theme. Pretty self-explanatory.

+
+ break; + case Section.Search: +

Search

+
+
+ + +
+

We will store results from commonly searched queries to reproduce repeated searches faster. to make sure prices are relevant, queries older than a few minutes will be removed.

+
+
+ + +
+ break; + } +
+
+ diff --git a/src/MultiShop/Client/Pages/Configuration.razor.cs b/src/MultiShop/Client/Pages/Configuration.razor.cs new file mode 100644 index 0000000..5f630ab --- /dev/null +++ b/src/MultiShop/Client/Pages/Configuration.razor.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Logging; +using MultiShop.Client.Services; +using MultiShop.Shared.Models; + +namespace MultiShop.Client.Pages +{ + public partial class Configuration : IAsyncDisposable + { + [Inject] + private ILogger Logger { get; set; } + + [Inject] + private LayoutStateChangeNotifier LayoutStateChangeNotifier { get; set; } + + [Inject] + private HttpClient Http { get; set; } + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } + + [CascadingParameter(Name = "ApplicationProfile")] + private ApplicationProfile ApplicationProfile { get; set; } + + private bool collapseNavMenu; + private string NavMenuCssClass => (collapseNavMenu ? "collapse" : ""); + + private enum Section + { + Opening, UI, Search + } + + private Section currentSection = Section.Opening; + + private Dictionary sectionNames = new Dictionary() { + {Section.Opening, "Info"}, + {Section.UI, "UI"}, + {Section.Search, "Search"} + }; + + private List
sectionOrder = new List
() { + Section.Opening, + Section.UI, + Section.Search + }; + + private string GetNavItemCssClass(Section section) + { + return "nav-item" + ((section == currentSection) ? " active" : null); + } + + public async ValueTask DisposeAsync() + { + AuthenticationState authenticationState = await AuthenticationStateTask; + if (authenticationState.User.Identity.IsAuthenticated) { + Logger.LogDebug($"User is authenticated. Attempting to save configuration to server."); + await Http.PutAsJsonAsync("Profile/Application", ApplicationProfile); + } + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Pages/Configuration.razor.css b/src/MultiShop/Client/Pages/Configuration.razor.css new file mode 100644 index 0000000..fece144 --- /dev/null +++ b/src/MultiShop/Client/Pages/Configuration.razor.css @@ -0,0 +1,48 @@ +nav { + position: static; + padding: 0.5rem; + height: fit-content; +} + +nav .nav-toggler { + display: none; + border-style: none; + padding: 0px; + background-color: transparent; +} + +.collapse { + display: block; +} + +.navbar-light .nav-item.active .btn { + color: black; +} + +.nav .nav-item .btn { + color: gray; +} + +.navbar-dark .nav-item.active .btn { + color: white; +} + +@media (max-width: 50rem) { + nav .nav-toggler { + display: block; + } + + nav { + position: fixed; + padding: 1rem; + bottom: 1rem; + left: 1rem; + border-radius: 12px; + } + + .collapse { + display: none; + } + + +} \ No newline at end of file diff --git a/src/MultiShop/Client/Pages/Index.razor b/src/MultiShop/Client/Pages/Index.razor index 032f142..48640f6 100644 --- a/src/MultiShop/Client/Pages/Index.razor +++ b/src/MultiShop/Client/Pages/Index.razor @@ -1,4 +1,14 @@ @page "/" -@* TODO: Add main page content.*@ -

Welcome to MultiShop!

\ No newline at end of file +
+

Welcome to MultiShop!

+
+
+

What is MultiShop?

+

MultiShop is an app that allows users to easily check multiple online retailers, primarily for electronic components. On request, the app searches, aggregates and sorts all the most important information about the products it finds while searching through sites for you! MultiShop is intended to be a small light-weight app the runs on the clients computer. Therefore, all actions and processes are actually reliant on your computer. Additionally, MultiShop does not crawl sites and save their products like a typical search engine. To provide the latest prices, all searches are performed (near) live on command.

+
+ +
+

How does it work?

+

MultiShop is a simple program that takes care of looking through product listings for you. That's all. As all tasks are executed locally on your computer MultiShop is like any other program, except that it is transient, and automatically loaded when you visit this website. The shop has a variety of modules meant to search different shops by essentially querying the shop as the user would normally, and then scanning the results.

+
diff --git a/src/MultiShop/Client/Pages/Search.razor.cs b/src/MultiShop/Client/Pages/Search.razor.cs index 0fe2b72..da5a9da 100644 --- a/src/MultiShop/Client/Pages/Search.razor.cs +++ b/src/MultiShop/Client/Pages/Search.razor.cs @@ -6,9 +6,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Logging; -using MultiShop.Client.Shared; using MultiShop.Client.Extensions; using MultiShop.Client.Listing; +using MultiShop.Client.Services; +using MultiShop.Client.Shared; using MultiShop.Shared.Models; using MultiShop.Shop.Framework; @@ -16,6 +17,9 @@ namespace MultiShop.Client.Pages { public partial class Search : IAsyncDisposable { + [Inject] + private LayoutStateChangeNotifier LayoutStateChangeNotifier { get; set; } + [Inject] private ILogger Logger { get; set; } @@ -23,10 +27,10 @@ namespace MultiShop.Client.Pages private HttpClient Http { get; set; } [CascadingParameter] - Task AuthenticationStateTask { get; set; } + private Task AuthenticationStateTask { get; set; } [CascadingParameter(Name = "Shops")] - public Dictionary Shops { get; set; } + public IReadOnlyDictionary Shops { get; set; } [Parameter] public string Query { get; set; } @@ -43,30 +47,45 @@ namespace MultiShop.Client.Pages private List listings = new List(); private int resultsChecked = 0; + protected override void OnInitialized() + { + base.OnInitialized(); + LayoutStateChangeNotifier.Notify += UpdateState; + } + protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); AuthenticationState authState = await AuthenticationStateTask; - - if (authState.User.Identity.IsAuthenticated) { + + if (authState.User.Identity.IsAuthenticated) + { Logger.LogDebug($"User \"{authState.User.Identity.Name}\" is authenticated. Checking for saved profiles."); HttpResponseMessage searchProfileResponse = await Http.GetAsync("Profile/Search"); - if (searchProfileResponse.IsSuccessStatusCode) { + if (searchProfileResponse.IsSuccessStatusCode) + { activeSearchProfile = await searchProfileResponse.Content.ReadFromJsonAsync(); - } else { + } + else + { Logger.LogWarning("Could not load search profile from server. Using default."); activeSearchProfile = new SearchProfile(); } HttpResponseMessage resultsProfileResponse = await Http.GetAsync("Profile/Results"); - if (resultsProfileResponse.IsSuccessStatusCode) { + if (resultsProfileResponse.IsSuccessStatusCode) + { activeResultsProfile = await resultsProfileResponse.Content.ReadFromJsonAsync(); - } else { + } + else + { Logger.LogWarning("Could not load results profile from server. Using default."); activeResultsProfile = new ResultsProfile(); } - } else { + } + else + { activeSearchProfile = new SearchProfile(); activeResultsProfile = new ResultsProfile(); } @@ -82,9 +101,11 @@ namespace MultiShop.Client.Pages } } - protected override async Task OnAfterRenderAsync(bool firstRender) { + protected override async Task OnAfterRenderAsync(bool firstRender) + { await base.OnAfterRenderAsync(firstRender); - if (firstRender) { + if (firstRender) + { searchBar.Query = Query; searchBar.Searching = true; await PerformSearch(Query); @@ -247,13 +268,21 @@ namespace MultiShop.Client.Pages StateHasChanged(); } + private async Task UpdateState() { + await InvokeAsync(() => { + StateHasChanged(); + }); + } + public async ValueTask DisposeAsync() { AuthenticationState authState = await AuthenticationStateTask; - if (authState.User.Identity.IsAuthenticated) { + if (authState.User.Identity.IsAuthenticated) + { await Http.PutAsJsonAsync("Profile/Search", activeSearchProfile); await Http.PutAsJsonAsync("Profile/Results", activeResultsProfile); } + LayoutStateChangeNotifier.Notify -= UpdateState; } public class Status diff --git a/src/MultiShop/Client/Program.cs b/src/MultiShop/Client/Program.cs index f4fea17..062f231 100644 --- a/src/MultiShop/Client/Program.cs +++ b/src/MultiShop/Client/Program.cs @@ -8,6 +8,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System.Net.Http.Json; using Microsoft.Extensions.Logging; +using MultiShop.Client.Module; +using MultiShop.Shop.Framework; +using MultiShop.Client.Services; namespace MultiShop.Client { @@ -15,7 +18,9 @@ namespace MultiShop.Client { public static async Task Main(string[] args) { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); builder.RootComponents.Add("#app"); @@ -26,15 +31,13 @@ namespace MultiShop.Client Action configureClient = client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); builder.Services.AddHttpClient("Public-MultiShop.ServerAPI", configureClient); - IReadOnlyDictionary webApiConfig = null; using (HttpClient client = new HttpClient()) { - configureClient.Invoke(client); - webApiConfig = await client.GetFromJsonAsync>("PublicApiSettings"); + configureClient.Invoke(client); + builder.Configuration.AddInMemoryCollection(await client.GetFromJsonAsync>("PublicApiSettings")); } - builder.Configuration.AddInMemoryCollection(webApiConfig); - + builder.Services.AddSingleton(); builder.Services.AddApiAuthorization(); diff --git a/src/MultiShop/Client/Services/LayoutStateChangeNotifier.cs b/src/MultiShop/Client/Services/LayoutStateChangeNotifier.cs new file mode 100644 index 0000000..fe0456b --- /dev/null +++ b/src/MultiShop/Client/Services/LayoutStateChangeNotifier.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace MultiShop.Client.Services +{ + public class LayoutStateChangeNotifier + { + public event Func Notify; + public async Task LayoutHasChanged() { + await Notify?.Invoke(); + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Shared/AuthenticationDisplay.razor b/src/MultiShop/Client/Shared/AuthenticationDisplay.razor new file mode 100644 index 0000000..805f1d7 --- /dev/null +++ b/src/MultiShop/Client/Shared/AuthenticationDisplay.razor @@ -0,0 +1,27 @@ +@using Microsoft.Extensions.Configuration +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject NavigationManager Navigation +@inject SignOutSessionStateManager SignOutManager +@inject IConfiguration Configuration + + + + Hello, @auth.User.Identity.Name! + + + + @if (Configuration["IdentityServer:Registration"].Equals("enabled")) + { + Register + } + Log in + + + +@code { + private async Task BeginSignOut(MouseEventArgs args) + { + await SignOutManager.SetSignOutState(); + Navigation.NavigateTo("authentication/logout"); + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Shared/CascadingDependencies.razor b/src/MultiShop/Client/Shared/CascadingDependencies.razor new file mode 100644 index 0000000..27f77a6 --- /dev/null +++ b/src/MultiShop/Client/Shared/CascadingDependencies.razor @@ -0,0 +1,14 @@ +@using Microsoft.Extensions.Logging +@using Module +@using MultiShop.Shared.Models + +@if (loadingDisplay == null) +{ + + + @Content + + +} else { + @LoadingContent(loadingDisplay) +} \ No newline at end of file diff --git a/src/MultiShop/Client/Shared/CascadingDependencies.razor.cs b/src/MultiShop/Client/Shared/CascadingDependencies.razor.cs new file mode 100644 index 0000000..7be6426 --- /dev/null +++ b/src/MultiShop/Client/Shared/CascadingDependencies.razor.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Logging; +using MultiShop.Client.Module; +using MultiShop.Shared.Models; +using MultiShop.Shop.Framework; + +namespace MultiShop.Client.Shared +{ + public partial class CascadingDependencies : IDisposable + { + [Inject] + private ILogger Logger { get; set; } + + [Inject] + private IHttpClientFactory HttpClientFactory { get; set; } + + [CascadingParameter] + private Task AuthenticationStateTask { get; set; } + + [Parameter] + public RenderFragment LoadingContent { get; set; } + + [Parameter] + public RenderFragment Content { get; set; } + + private bool disposedValue; + + private string loadingDisplay; + + private IReadOnlyDictionary shops; + + private ApplicationProfile applicationProfile; + + protected override async Task OnInitializedAsync() + { + loadingDisplay = ""; + await base.OnInitializedAsync(); + await DownloadShops(HttpClientFactory.CreateClient("Public-MultiShop.ServerAPI")); + await DownloadApplicationProfile(HttpClientFactory.CreateClient("MultiShop.ServerAPI")); + loadingDisplay = null; + } + + private async Task DownloadShops(HttpClient http) + { + loadingDisplay = "shops"; + ShopModuleLoader loader = new ShopModuleLoader(http, Logger); + shops = await loader.GetShops(); + } + + private async Task DownloadApplicationProfile(HttpClient http) + { + loadingDisplay = "profile"; + AuthenticationState authState = await AuthenticationStateTask; + if (authState.User.Identity.IsAuthenticated) + { + Logger.LogDebug($"User is logged in. Attempting to fetch application profile."); + HttpResponseMessage response = await http.GetAsync("Profile/Application"); + if (response.IsSuccessStatusCode) + { + applicationProfile = await response.Content.ReadFromJsonAsync(); + } + } + if (applicationProfile == null) applicationProfile = new ApplicationProfile(); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + foreach (string shopName in shops.Keys) + { + shops[shopName].Dispose(); + } + } + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Shared/HorizontalNavMenuTemplate.razor b/src/MultiShop/Client/Shared/HorizontalNavMenuTemplate.razor new file mode 100644 index 0000000..fe67739 --- /dev/null +++ b/src/MultiShop/Client/Shared/HorizontalNavMenuTemplate.razor @@ -0,0 +1,85 @@ +@using MultiShop.Client.Extensions +@using MultiShop.Shared.Models +@using MultiShop.Client.Services +@implements IDisposable +@inject LayoutStateChangeNotifier LayoutStateChangeNotifier + + + +@code { + [CascadingParameter(Name = "ApplicationProfile")] + private ApplicationProfile ApplicationProfile { get; set; } + + private bool collapseNavMenu = true; + + private string NavMenuCssClass => (collapseNavMenu ? "collapse " : " ") + "navbar-collapse"; + + [Parameter] + public IList Items { get; set; } + + [Parameter] + public RenderFragment BrandContent { get; set; } + + [Parameter] + public RenderFragment ItemTemplate { get; set; } + + [Parameter] + public RenderFragment LatterContent { get; set; } + private bool disposed; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } + + protected override void OnInitialized() + { + base.OnInitialized(); + LayoutStateChangeNotifier.Notify += UpdateState; + } + + private async Task UpdateState() { + await InvokeAsync(() => { + StateHasChanged(); + }); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + } + LayoutStateChangeNotifier.Notify -= UpdateState; + disposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/MultiShop/Client/Shared/HorizontalSiteNav.razor b/src/MultiShop/Client/Shared/HorizontalSiteNav.razor new file mode 100644 index 0000000..62a7fa6 --- /dev/null +++ b/src/MultiShop/Client/Shared/HorizontalSiteNav.razor @@ -0,0 +1,42 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using Microsoft.Extensions.Configuration + +@inject NavigationManager Navigation +@inject SignOutSessionStateManager SignOutManager +@inject IConfiguration Configuration + + + + + MultiShop + + + @PlaceNames[place] + + + + + + +@code { + private bool collapseNavMenu = true; + + private string NavMenuCssClass => (collapseNavMenu ? "collapse " : " ") + "navbar-collapse"; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } + + private Dictionary PlaceNames = new Dictionary() { + {"","Home"}, + {"search", "Search"}, + {"configure", "Options"} + }; + + private List PlaceOrder = new List() { + "", + "search", + "configure" + }; +} diff --git a/src/MultiShop/Client/Shared/MainLayout.razor b/src/MultiShop/Client/Shared/MainLayout.razor index f7def2c..e7c668b 100644 --- a/src/MultiShop/Client/Shared/MainLayout.razor +++ b/src/MultiShop/Client/Shared/MainLayout.razor @@ -1,10 +1,51 @@ +@using MultiShop.Client.Extensions +@using MultiShop.Shared.Models +@using MultiShop.Client.Services @inherits LayoutComponentBase +@implements IDisposable +@inject LayoutStateChangeNotifier LayoutStateChangeNotifier -
- -
-
- @Body -
+
+ +
+ @Body
-
\ No newline at end of file +
+ +@code { + [CascadingParameter(Name = "ApplicationProfile")] + private ApplicationProfile ApplicationProfile { get; set; } + private bool disposed; + + protected override void OnInitialized() + { + base.OnInitialized(); + LayoutStateChangeNotifier.Notify += UpdateState; + } + + private async Task UpdateState() + { + await InvokeAsync(() => + { + StateHasChanged(); + }); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + } + LayoutStateChangeNotifier.Notify -= UpdateState; + disposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/MultiShop/Client/Shared/MainLayout.razor.css b/src/MultiShop/Client/Shared/MainLayout.razor.css index b2332aa..b964f02 100644 --- a/src/MultiShop/Client/Shared/MainLayout.razor.css +++ b/src/MultiShop/Client/Shared/MainLayout.razor.css @@ -3,18 +3,3 @@ display: flex; flex-direction: column; } - -.main { - flex: 1; -} - - -@media (max-width: 640.98px) { - .top-row:not(.auth) { - display: none; - } - - .top-row.auth { - justify-content: space-between; - } -} \ No newline at end of file diff --git a/src/MultiShop/Client/Shared/NavMenu.razor b/src/MultiShop/Client/Shared/NavMenu.razor deleted file mode 100644 index 6e3514c..0000000 --- a/src/MultiShop/Client/Shared/NavMenu.razor +++ /dev/null @@ -1,74 +0,0 @@ -@using Microsoft.AspNetCore.Components.Authorization -@using Microsoft.AspNetCore.Components.WebAssembly.Authentication -@using Microsoft.Extensions.Configuration - -@inject NavigationManager Navigation -@inject SignOutSessionStateManager SignOutManager -@inject IConfiguration Configuration - - - - -@code { - private bool collapseNavMenu = true; - - private string NavMenuCssClass => (collapseNavMenu ? "collapse " : " ") + "navbar-collapse"; - - private void ToggleNavMenu() - { - collapseNavMenu = !collapseNavMenu; - } - - private async Task BeginSignOut(MouseEventArgs args) - { - await SignOutManager.SetSignOutState(); - Navigation.NavigateTo("authentication/logout"); - } - -} diff --git a/src/MultiShop/Client/wwwroot/css/app.css b/src/MultiShop/Client/wwwroot/css/app.css index 3c5a10e..5240c8b 100644 --- a/src/MultiShop/Client/wwwroot/css/app.css +++ b/src/MultiShop/Client/wwwroot/css/app.css @@ -4,8 +4,30 @@ html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } +#app { + min-height: 100vh; +} + +.page { + min-height: inherit; +} + +.bg-dark { + background-color: #262626 !important; +} + +nav.bg-dark { + background-color: #1A1A1A !important; +} + .content { padding-top: 1.5rem; + padding-right: 2rem; + padding-left: 2rem; +} + +.bg-dark .card { + background-color: #1F1F1F; } #blazor-error-ui { diff --git a/src/MultiShop/Client/wwwroot/100x100.png b/src/MultiShop/Client/wwwroot/images/100x100.png similarity index 100% rename from src/MultiShop/Client/wwwroot/100x100.png rename to src/MultiShop/Client/wwwroot/images/100x100.png diff --git a/src/MultiShop/Client/wwwroot/index.html b/src/MultiShop/Client/wwwroot/index.html index ae3ab62..162d9ae 100644 --- a/src/MultiShop/Client/wwwroot/index.html +++ b/src/MultiShop/Client/wwwroot/index.html @@ -14,12 +14,12 @@
-
-
+
+
Loading...
-
+
Loading...
diff --git a/src/MultiShop/Server/Controllers/ProfileController.cs b/src/MultiShop/Server/Controllers/ProfileController.cs index 58b2916..8d9cc14 100644 --- a/src/MultiShop/Server/Controllers/ProfileController.cs +++ b/src/MultiShop/Server/Controllers/ProfileController.cs @@ -1,7 +1,10 @@ +using System.Text.Json; using System.Threading.Tasks; +using Castle.Core.Logging; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using MultiShop.Server.Data; using MultiShop.Server.Models; using MultiShop.Shared.Models; @@ -14,12 +17,14 @@ namespace MultiShop.Server.Controllers [Route("[controller]")] public class ProfileController : ControllerBase { + private ILogger logger; private UserManager userManager; private ApplicationDbContext dbContext; - public ProfileController(UserManager userManager, ApplicationDbContext dbContext) + public ProfileController(UserManager userManager, ApplicationDbContext dbContext, ILogger logger) { this.userManager = userManager; this.dbContext = dbContext; + this.logger = logger; } [HttpGet] @@ -36,6 +41,14 @@ namespace MultiShop.Server.Controllers return Ok(userModel.ResultsProfile); } + [HttpGet] + [Route("Application")] + public async Task GetApplicationProfile() { + ApplicationUser userModel = await userManager.GetUserAsync(User); + logger.LogInformation(JsonSerializer.Serialize(userModel.ApplicationProfile)); + return Ok(userModel.ApplicationProfile); + } + [HttpPut] [Route("Search")] public async Task PutSearchProfile(SearchProfile searchProfile) { @@ -52,12 +65,25 @@ namespace MultiShop.Server.Controllers [Route("Results")] public async Task PutResultsProfile(ResultsProfile resultsProfile) { ApplicationUser userModel = await userManager.GetUserAsync(User); - if (userModel.ResultsProfile.Id != resultsProfile.Id) { + if (userModel.ResultsProfile.Id != resultsProfile.Id || userModel.Id != resultsProfile.ApplicationUserId) { return BadRequest(); } dbContext.Entry(userModel.ResultsProfile).CurrentValues.SetValues(resultsProfile); await userManager.UpdateAsync(userModel); return NoContent(); } + + [HttpPut] + [Route("Application")] + public async Task PutApplicationProfile(ApplicationProfile applicationProfile) { + ApplicationUser userModel = await userManager.GetUserAsync(User); + logger.LogInformation(JsonSerializer.Serialize(applicationProfile)); + if (userModel.ApplicationProfile.Id != applicationProfile.Id || userModel.Id != applicationProfile.ApplicationUserId) { + return BadRequest(); + } + dbContext.Entry(userModel.ApplicationProfile).CurrentValues.SetValues(applicationProfile); + await userManager.UpdateAsync(userModel); + return NoContent(); + } } } \ No newline at end of file diff --git a/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.Designer.cs b/src/MultiShop/Server/Data/Migrations/20210531175621_InitialCreate.Designer.cs similarity index 92% rename from src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.Designer.cs rename to src/MultiShop/Server/Data/Migrations/20210531175621_InitialCreate.Designer.cs index a9b3f2c..d26bdb7 100644 --- a/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.Designer.cs +++ b/src/MultiShop/Server/Data/Migrations/20210531175621_InitialCreate.Designer.cs @@ -9,7 +9,7 @@ using MultiShop.Server.Data; namespace MultiShop.Server.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20210525224658_InitialCreate")] + [Migration("20210531175621_InitialCreate")] partial class InitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -317,6 +317,32 @@ namespace MultiShop.Server.Data.Migrations b.ToTable("AspNetUsers"); }); + modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("CacheCommonSearches") + .HasColumnType("INTEGER"); + + b.Property("DarkMode") + .HasColumnType("INTEGER"); + + b.Property("EnableSearchHistory") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("ApplicationProfile"); + }); + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => { b.Property("Id") @@ -452,6 +478,13 @@ namespace MultiShop.Server.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b => + { + b.HasOne("MultiShop.Server.Models.ApplicationUser", null) + .WithOne("ApplicationProfile") + .HasForeignKey("MultiShop.Shared.Models.ApplicationProfile", "ApplicationUserId"); + }); + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => { b.HasOne("MultiShop.Server.Models.ApplicationUser", null) @@ -468,6 +501,9 @@ namespace MultiShop.Server.Data.Migrations modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => { + b.Navigation("ApplicationProfile") + .IsRequired(); + b.Navigation("ResultsProfile") .IsRequired(); diff --git a/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.cs b/src/MultiShop/Server/Data/Migrations/20210531175621_InitialCreate.cs similarity index 92% rename from src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.cs rename to src/MultiShop/Server/Data/Migrations/20210531175621_InitialCreate.cs index 803a8dc..948ce64 100644 --- a/src/MultiShop/Server/Data/Migrations/20210525224658_InitialCreate.cs +++ b/src/MultiShop/Server/Data/Migrations/20210531175621_InitialCreate.cs @@ -106,6 +106,28 @@ namespace MultiShop.Server.Data.Migrations onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "ApplicationProfile", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ApplicationUserId = table.Column(type: "TEXT", nullable: true), + DarkMode = table.Column(type: "INTEGER", nullable: false), + CacheCommonSearches = table.Column(type: "INTEGER", nullable: false), + EnableSearchHistory = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ApplicationProfile", x => x.Id); + table.ForeignKey( + name: "FK_ApplicationProfile_AspNetUsers_ApplicationUserId", + column: x => x.ApplicationUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + migrationBuilder.CreateTable( name: "AspNetUserClaims", columns: table => new @@ -245,6 +267,12 @@ namespace MultiShop.Server.Data.Migrations onDelete: ReferentialAction.Restrict); }); + migrationBuilder.CreateIndex( + name: "IX_ApplicationProfile_ApplicationUserId", + table: "ApplicationProfile", + column: "ApplicationUserId", + unique: true); + migrationBuilder.CreateIndex( name: "IX_AspNetRoleClaims_RoleId", table: "AspNetRoleClaims", @@ -323,6 +351,9 @@ namespace MultiShop.Server.Data.Migrations protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "ApplicationProfile"); + migrationBuilder.DropTable( name: "AspNetRoleClaims"); diff --git a/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 6284e00..40cd8ca 100644 --- a/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/MultiShop/Server/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -315,6 +315,32 @@ namespace MultiShop.Server.Data.Migrations b.ToTable("AspNetUsers"); }); + modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationUserId") + .HasColumnType("TEXT"); + + b.Property("CacheCommonSearches") + .HasColumnType("INTEGER"); + + b.Property("DarkMode") + .HasColumnType("INTEGER"); + + b.Property("EnableSearchHistory") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId") + .IsUnique(); + + b.ToTable("ApplicationProfile"); + }); + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => { b.Property("Id") @@ -450,6 +476,13 @@ namespace MultiShop.Server.Data.Migrations .IsRequired(); }); + modelBuilder.Entity("MultiShop.Shared.Models.ApplicationProfile", b => + { + b.HasOne("MultiShop.Server.Models.ApplicationUser", null) + .WithOne("ApplicationProfile") + .HasForeignKey("MultiShop.Shared.Models.ApplicationProfile", "ApplicationUserId"); + }); + modelBuilder.Entity("MultiShop.Shared.Models.ResultsProfile", b => { b.HasOne("MultiShop.Server.Models.ApplicationUser", null) @@ -466,6 +499,9 @@ namespace MultiShop.Server.Data.Migrations modelBuilder.Entity("MultiShop.Server.Models.ApplicationUser", b => { + b.Navigation("ApplicationProfile") + .IsRequired(); + b.Navigation("ResultsProfile") .IsRequired(); diff --git a/src/MultiShop/Server/Models/ApplicationUser.cs b/src/MultiShop/Server/Models/ApplicationUser.cs index 79f3ff3..d5adf4f 100644 --- a/src/MultiShop/Server/Models/ApplicationUser.cs +++ b/src/MultiShop/Server/Models/ApplicationUser.cs @@ -11,5 +11,8 @@ namespace MultiShop.Server.Models [Required] public virtual ResultsProfile ResultsProfile { get; private set; } = new ResultsProfile(); + + [Required] + public virtual ApplicationProfile ApplicationProfile {get; private set; } = new ApplicationProfile(); } } diff --git a/src/MultiShop/Shared/Models/ApplicationProfile.cs b/src/MultiShop/Shared/Models/ApplicationProfile.cs new file mode 100644 index 0000000..205333e --- /dev/null +++ b/src/MultiShop/Shared/Models/ApplicationProfile.cs @@ -0,0 +1,15 @@ +namespace MultiShop.Shared.Models +{ + public class ApplicationProfile + { + public int Id { get; set; } + + public string ApplicationUserId { get; set; } + + public bool DarkMode { get; set; } + + public bool CacheCommonSearches { get; set; } = true; + + public bool EnableSearchHistory { get; set; } = true; + } +} \ No newline at end of file diff --git a/src/MultiShop/Shared/Models/SearchProfile.cs b/src/MultiShop/Shared/Models/SearchProfile.cs index df8f855..f8d085c 100644 --- a/src/MultiShop/Shared/Models/SearchProfile.cs +++ b/src/MultiShop/Shared/Models/SearchProfile.cs @@ -48,7 +48,7 @@ namespace MultiShop.Shared.Models if (EnableMaxShippingFee) _maxShippingFee = value; } } - public bool KeepUnknownShipping { get; set; } + public bool KeepUnknownShipping { get; set; } = true; [Required] public ShopToggler ShopStates { get; set; } = new ShopToggler(); @@ -115,5 +115,11 @@ namespace MultiShop.Shared.Models { return Id; } + + public SearchProfile DeepCopy() { + SearchProfile profile = (SearchProfile)MemberwiseClone(); + profile.ShopStates = ShopStates.Clone(); + return profile; + } } } \ No newline at end of file