diff --git a/aspire/Money.Aspire.AppHost/Money.Aspire.AppHost.csproj b/aspire/Money.Aspire.AppHost/Money.Aspire.AppHost.csproj deleted file mode 100644 index ff27a8da..00000000 --- a/aspire/Money.Aspire.AppHost/Money.Aspire.AppHost.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - Exe - net8.0 - enable - enable - true - 4bbf0756-e397-4214-94fb-7463f0b6079d - - - - - - - - - - - - - diff --git a/aspire/Money.Aspire.AppHost/Program.cs b/aspire/Money.Aspire.AppHost/Program.cs deleted file mode 100644 index 3ddcf6a3..00000000 --- a/aspire/Money.Aspire.AppHost/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Projects; - -var builder = DistributedApplication.CreateBuilder(args); - -var apiProject = builder.AddProject("api", launchProfileName: "aspire") - .WithExternalHttpEndpoints() - .WithEnvironment("AUTO_MIGRATE", "true") - .WithReplicas(1); - -if (Environment.GetEnvironmentVariable("ASPIRE_POSTGRES") == "true") -{ - var postgres = builder.AddPostgres("money") - .WithDataVolume(isReadOnly: false); - var postgresdb = postgres.AddDatabase("ApplicationDbContext"); - apiProject.WithReference(postgresdb); -} - -var webProject = builder.AddProject("web", launchProfileName: "aspire") - .WithExternalHttpEndpoints() - .WithReference(apiProject); - - -var webEndpoint = webProject.GetEndpoint("https"); -apiProject.WithEnvironment("CORS_ORIGIN", webEndpoint); - -builder.Build().Run(); diff --git a/aspire/Money.Aspire.AppHost/Properties/launchSettings.json b/aspire/Money.Aspire.AppHost/Properties/launchSettings.json deleted file mode 100644 index 4ae40a12..00000000 --- a/aspire/Money.Aspire.AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "https://localhost:17020;http://localhost:15141", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21263", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22276", - "ASPIRE_POSTGRES": "true" - } - }, - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "applicationUrl": "http://localhost:15141", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development", - "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19042", - "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20194", - "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" - } - } - } -} diff --git a/aspire/Money.Aspire.AppHost/appsettings.Development.json b/aspire/Money.Aspire.AppHost/appsettings.Development.json deleted file mode 100644 index 0c208ae9..00000000 --- a/aspire/Money.Aspire.AppHost/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/aspire/Money.Aspire.AppHost/appsettings.json b/aspire/Money.Aspire.AppHost/appsettings.json deleted file mode 100644 index 31c092aa..00000000 --- a/aspire/Money.Aspire.AppHost/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning", - "Aspire.Hosting.Dcp": "Warning" - } - } -} diff --git a/aspire/Money.Aspire.sln b/aspire/Money.Aspire.sln deleted file mode 100644 index e52080b0..00000000 --- a/aspire/Money.Aspire.sln +++ /dev/null @@ -1,92 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35208.52 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Aspire.AppHost", "Money.Aspire.AppHost\Money.Aspire.AppHost.csproj", "{30243B3E-DBF0-41DF-B925-3B410EF628B7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "backend", "backend", "{FD4B84D1-AFC5-4F55-8832-64883493A327}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Api", "..\backend\Money.Api\Money.Api.csproj", "{987601E9-6F6D-4E28-AE3E-98A778E13340}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.ApiClient", "..\backend\Money.ApiClient\Money.ApiClient.csproj", "{91A42589-C744-416B-819C-A7B6F386C02F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Business", "..\backend\Money.Business\Money.Business.csproj", "{1E852510-1C09-4433-8368-A8786D0DBF45}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Common", "..\backend\Money.Common\Money.Common.csproj", "{6879F54B-2C2F-4ABB-9CC8-D796A7E1122D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Data", "..\backend\Money.Data\Money.Data.csproj", "{75522595-A18A-4CB0-8DA7-156B87757977}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "frontend", "frontend", "{100AA6E7-8C1F-4EBF-833D-50B3BD007613}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Web", "..\frontend\Money.Web\Money.Web.csproj", "{3E15DECC-CD05-48BC-ADEC-25C17E437AE7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AFB3481F-13DA-46CE-ABD6-4CB1A7697FF6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.CoreLib", "..\libs\Money.CoreLib\Money.CoreLib.csproj", "{9658CB7E-8D15-4596-A4D9-A4109C463C67}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.WebAssembly.CoreLib", "..\libs\Money.WebAssembly.CoreLib\Money.WebAssembly.CoreLib.csproj", "{55CB1FA4-0985-4458-8D9B-6FA303BFB606}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Api.Tests", "..\backend\Money.Api.Tests\Money.Api.Tests.csproj", "{0370C976-1E5B-4DE4-A65D-31B42169F69A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {30243B3E-DBF0-41DF-B925-3B410EF628B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {30243B3E-DBF0-41DF-B925-3B410EF628B7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {30243B3E-DBF0-41DF-B925-3B410EF628B7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {30243B3E-DBF0-41DF-B925-3B410EF628B7}.Release|Any CPU.Build.0 = Release|Any CPU - {987601E9-6F6D-4E28-AE3E-98A778E13340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {987601E9-6F6D-4E28-AE3E-98A778E13340}.Debug|Any CPU.Build.0 = Debug|Any CPU - {987601E9-6F6D-4E28-AE3E-98A778E13340}.Release|Any CPU.ActiveCfg = Release|Any CPU - {987601E9-6F6D-4E28-AE3E-98A778E13340}.Release|Any CPU.Build.0 = Release|Any CPU - {91A42589-C744-416B-819C-A7B6F386C02F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91A42589-C744-416B-819C-A7B6F386C02F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91A42589-C744-416B-819C-A7B6F386C02F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91A42589-C744-416B-819C-A7B6F386C02F}.Release|Any CPU.Build.0 = Release|Any CPU - {1E852510-1C09-4433-8368-A8786D0DBF45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E852510-1C09-4433-8368-A8786D0DBF45}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E852510-1C09-4433-8368-A8786D0DBF45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E852510-1C09-4433-8368-A8786D0DBF45}.Release|Any CPU.Build.0 = Release|Any CPU - {6879F54B-2C2F-4ABB-9CC8-D796A7E1122D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6879F54B-2C2F-4ABB-9CC8-D796A7E1122D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6879F54B-2C2F-4ABB-9CC8-D796A7E1122D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6879F54B-2C2F-4ABB-9CC8-D796A7E1122D}.Release|Any CPU.Build.0 = Release|Any CPU - {75522595-A18A-4CB0-8DA7-156B87757977}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {75522595-A18A-4CB0-8DA7-156B87757977}.Debug|Any CPU.Build.0 = Debug|Any CPU - {75522595-A18A-4CB0-8DA7-156B87757977}.Release|Any CPU.ActiveCfg = Release|Any CPU - {75522595-A18A-4CB0-8DA7-156B87757977}.Release|Any CPU.Build.0 = Release|Any CPU - {3E15DECC-CD05-48BC-ADEC-25C17E437AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E15DECC-CD05-48BC-ADEC-25C17E437AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E15DECC-CD05-48BC-ADEC-25C17E437AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E15DECC-CD05-48BC-ADEC-25C17E437AE7}.Release|Any CPU.Build.0 = Release|Any CPU - {9658CB7E-8D15-4596-A4D9-A4109C463C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9658CB7E-8D15-4596-A4D9-A4109C463C67}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9658CB7E-8D15-4596-A4D9-A4109C463C67}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9658CB7E-8D15-4596-A4D9-A4109C463C67}.Release|Any CPU.Build.0 = Release|Any CPU - {55CB1FA4-0985-4458-8D9B-6FA303BFB606}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {55CB1FA4-0985-4458-8D9B-6FA303BFB606}.Debug|Any CPU.Build.0 = Debug|Any CPU - {55CB1FA4-0985-4458-8D9B-6FA303BFB606}.Release|Any CPU.ActiveCfg = Release|Any CPU - {55CB1FA4-0985-4458-8D9B-6FA303BFB606}.Release|Any CPU.Build.0 = Release|Any CPU - {0370C976-1E5B-4DE4-A65D-31B42169F69A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0370C976-1E5B-4DE4-A65D-31B42169F69A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0370C976-1E5B-4DE4-A65D-31B42169F69A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0370C976-1E5B-4DE4-A65D-31B42169F69A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {987601E9-6F6D-4E28-AE3E-98A778E13340} = {FD4B84D1-AFC5-4F55-8832-64883493A327} - {1E852510-1C09-4433-8368-A8786D0DBF45} = {FD4B84D1-AFC5-4F55-8832-64883493A327} - {6879F54B-2C2F-4ABB-9CC8-D796A7E1122D} = {FD4B84D1-AFC5-4F55-8832-64883493A327} - {75522595-A18A-4CB0-8DA7-156B87757977} = {FD4B84D1-AFC5-4F55-8832-64883493A327} - {3E15DECC-CD05-48BC-ADEC-25C17E437AE7} = {100AA6E7-8C1F-4EBF-833D-50B3BD007613} - {0370C976-1E5B-4DE4-A65D-31B42169F69A} = {AFB3481F-13DA-46CE-ABD6-4CB1A7697FF6} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {760EC233-46EC-4056-8F5D-EBF4C02653BB} - EndGlobalSection -EndGlobal diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index 024c59c7..dcfcd6ed 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -1,43 +1,45 @@  - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/Money.Api/Controllers/ExternalAuthController.cs b/backend/Money.Api/Controllers/ExternalAuthController.cs index c8bc647d..2f58482c 100644 --- a/backend/Money.Api/Controllers/ExternalAuthController.cs +++ b/backend/Money.Api/Controllers/ExternalAuthController.cs @@ -68,10 +68,16 @@ public async Task Callback() var userId = principal.FindFirst(OpenIddictConstants.Claims.Subject)?.Value ?? principal.FindFirst(ClaimTypes.NameIdentifier)?.Value; - // TODO: Не работает - var email = principal.FindFirst(ClaimTypes.Email)!.Value; + var email = principal.FindFirst(ClaimTypes.Email)?.Value + ?? principal.FindFirst(OpenIddictConstants.Claims.Email)?.Value; + + if (string.IsNullOrWhiteSpace(email)) + { + return BadRequest("Email обязателен для внешней аутентификации."); + } + var name = principal.FindFirst(ClaimTypes.Name)?.Value; - var userNameCandidate = name?? BuildValidUserName(email, userId); + var userNameCandidate = name ?? BuildValidUserName(email, userId); var providerName = result.Properties?.GetString(OpenIddictClientAspNetCoreConstants.Properties.ProviderName) ?? "GitHub"; @@ -96,7 +102,7 @@ public async Task Callback() { UserName = uniqueUserName, Email = email, - EmailConfirmed = email != null + EmailConfirmed = true, }; var createResult = await userManager.CreateAsync(user); diff --git a/backend/Money.Api/Definitions/CoreLibDefinition.cs b/backend/Money.Api/Definitions/CoreLibDefinition.cs new file mode 100644 index 00000000..06316aa2 --- /dev/null +++ b/backend/Money.Api/Definitions/CoreLibDefinition.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Money.Api.Definitions; + +public class CoreLibDefinition : AppDefinition +{ + public override int ApplicationOrderIndex => -1; + + public override void ConfigureServices(WebApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + } + + public override void ConfigureApplication(WebApplication app) + { + if (!app.Environment.IsDevelopment()) + { + return; + } + + app.MapHealthChecks("/health"); + + app.MapHealthChecks("/alive", new() + { + Predicate = r => r.Tags.Contains("live"), + }); + } +} diff --git a/backend/Money.Api/Money.Api.csproj b/backend/Money.Api/Money.Api.csproj index 657bc714..501da31c 100644 --- a/backend/Money.Api/Money.Api.csproj +++ b/backend/Money.Api/Money.Api.csproj @@ -10,6 +10,8 @@ + + @@ -35,7 +37,6 @@ - diff --git a/backend/Money.Api/Program.cs b/backend/Money.Api/Program.cs index 85eaaa6f..52f0ea79 100644 --- a/backend/Money.Api/Program.cs +++ b/backend/Money.Api/Program.cs @@ -1,5 +1,4 @@ #pragma warning disable S2139 -using Money.CoreLib; using NLog; using NLog.Web; using System.Globalization; @@ -16,13 +15,13 @@ builder.Logging.ClearProviders(); builder.Host.UseNLog(); - builder.AddServiceDefaults(); + builder.AddDefinitions(typeof(Program)); var app = builder.Build(); app.UseDefinitions(); - app.MapDefaultEndpoints(); + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); var culture = new CultureInfo("ru-RU"); diff --git a/backend/Money.sln b/backend/Money.sln index 8462502e..e11c359a 100644 --- a/backend/Money.sln +++ b/backend/Money.sln @@ -17,8 +17,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.ApiClient", "Money.Ap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Business", "Money.Business\Money.Business.csproj", "{337534D0-110A-48F0-8CF8-3A7E6DDCA9C3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.CoreLib", "..\libs\Money.CoreLib\Money.CoreLib.csproj", "{24F8ED76-3DDA-424F-84BD-AE1D2DC9EAB5}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{61BD4C25-7142-4C55-93E8-FAD707C08E68}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig @@ -57,10 +55,6 @@ Global {337534D0-110A-48F0-8CF8-3A7E6DDCA9C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {337534D0-110A-48F0-8CF8-3A7E6DDCA9C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {337534D0-110A-48F0-8CF8-3A7E6DDCA9C3}.Release|Any CPU.Build.0 = Release|Any CPU - {24F8ED76-3DDA-424F-84BD-AE1D2DC9EAB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {24F8ED76-3DDA-424F-84BD-AE1D2DC9EAB5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {24F8ED76-3DDA-424F-84BD-AE1D2DC9EAB5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {24F8ED76-3DDA-424F-84BD-AE1D2DC9EAB5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/frontend/Directory.Packages.props b/frontend/Directory.Packages.props index c1599807..f3d9a665 100644 --- a/frontend/Directory.Packages.props +++ b/frontend/Directory.Packages.props @@ -1,20 +1,22 @@  - - true - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/Money.Web.sln b/frontend/Money.Web.sln index 5e1c1504..4baa3be8 100644 --- a/frontend/Money.Web.sln +++ b/frontend/Money.Web.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.Web", "Money.Web\Mone EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.ApiClient", "..\backend\Money.ApiClient\Money.ApiClient.csproj", "{76708031-7C46-4D1D-A086-8F1AFC337311}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Money.WebAssembly.CoreLib", "..\libs\Money.WebAssembly.CoreLib\Money.WebAssembly.CoreLib.csproj", "{0AB45AF6-7602-485D-979C-1BB8CA8101F4}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{92F13889-DBE1-48C1-A638-18DA6D14423E}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig @@ -39,10 +37,6 @@ Global {76708031-7C46-4D1D-A086-8F1AFC337311}.Debug|Any CPU.Build.0 = Debug|Any CPU {76708031-7C46-4D1D-A086-8F1AFC337311}.Release|Any CPU.ActiveCfg = Release|Any CPU {76708031-7C46-4D1D-A086-8F1AFC337311}.Release|Any CPU.Build.0 = Release|Any CPU - {0AB45AF6-7602-485D-979C-1BB8CA8101F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0AB45AF6-7602-485D-979C-1BB8CA8101F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0AB45AF6-7602-485D-979C-1BB8CA8101F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0AB45AF6-7602-485D-979C-1BB8CA8101F4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/frontend/Money.Web/Components/SmartDatePicker.razor b/frontend/Money.Web/Components/SmartDatePicker.razor index eeced3a3..5609fe19 100644 --- a/frontend/Money.Web/Components/SmartDatePicker.razor +++ b/frontend/Money.Web/Components/SmartDatePicker.razor @@ -1,32 +1,35 @@ @if (_isDatePickerVisible) {
- + Mask="@(new DateMask("dd.MM.yyyy"))" + PickerClosed="OnPickerClosedAsync" /> + Style="position: absolute; right: 24px; top: 65%; transform: translateY(-50%); z-index: 1000;" + Title="Текстовый ввод" /> + @* TODO: По идее Style и сама кнопка не нужна, т. к. всегда при закрытии переключится, но на всякий случай *@
} else { - } diff --git a/frontend/Money.Web/Components/SmartDatePicker.razor.cs b/frontend/Money.Web/Components/SmartDatePicker.razor.cs index b9caa2a3..a53db8d3 100644 --- a/frontend/Money.Web/Components/SmartDatePicker.razor.cs +++ b/frontend/Money.Web/Components/SmartDatePicker.razor.cs @@ -6,8 +6,9 @@ namespace Money.Web.Components; public partial class SmartDatePicker : ComponentBase { - private bool _isDatePickerVisible = true; + private bool _isDatePickerVisible; private string? _dateText = string.Empty; + private MudDatePicker? _datePicker; [Parameter] public DateTime? Date { get; set; } @@ -34,12 +35,11 @@ public partial class SmartDatePicker : ComponentBase return null; } - protected override async Task OnInitializedAsync() + protected override Task OnInitializedAsync() { - if (GetInitialDate != null) - { - await UpdateDateAsync(GetInitialDate.Invoke()); - } + return GetInitialDate != null + ? UpdateDateAsync(GetInitialDate.Invoke()) + : Task.CompletedTask; } private async Task TryUpdateDateAsync() @@ -68,27 +68,49 @@ private async Task TryUpdateDateAsync() return true; } - private async Task UpdateDateAsync(DateTime? date) + private Task UpdateDateAsync(DateTime? date) { Date = date; _dateText = date?.ToString("dd.MM.yyyy", CultureInfo.CurrentCulture); - await DateChanged.InvokeAsync(Date); + return DateChanged.InvokeAsync(Date); } - private async Task ToggleDateFieldAsync() + private async Task OpenDatePickerAsync() { - if (_isDatePickerVisible) + if (!string.IsNullOrWhiteSpace(_dateText)) { - _dateText = Date?.ToString("dd.MM.yyyy", CultureInfo.CurrentCulture); + await TryUpdateDateAsync(); } - else if (await TryUpdateDateAsync() == false) + + _isDatePickerVisible = true; + StateHasChanged(); + + await Task.Delay(10); + if (_datePicker != null) { - return; + await _datePicker.OpenAsync(); } + } - _isDatePickerVisible = !_isDatePickerVisible; - await Task.Delay(10); + private async Task OnPickerClosedAsync() + { + _dateText = Date?.ToString("dd.MM.yyyy", CultureInfo.CurrentCulture); + await DateChanged.InvokeAsync(Date); + _isDatePickerVisible = false; + StateHasChanged(); + } + + private Task SwitchToTextInputAsync() + { + _dateText = Date?.ToString("dd.MM.yyyy", CultureInfo.CurrentCulture); + _isDatePickerVisible = false; StateHasChanged(); + return Task.CompletedTask; + } + + private Task OnTextFieldBlurAsync() + { + return TryUpdateDateAsync(); } private DateTime? ParseDate() diff --git a/frontend/Money.Web/Components/SmartPlace.razor b/frontend/Money.Web/Components/SmartPlace.razor index ba1c0f45..a0fd626d 100644 --- a/frontend/Money.Web/Components/SmartPlace.razor +++ b/frontend/Money.Web/Components/SmartPlace.razor @@ -1,21 +1,45 @@ - +
+ + + @if (_showSuggestions && _suggestions.Any()) + { + + + @for (var i = 0; i < _suggestions.Count; i++) + { + var index = i; + var suggestion = _suggestions[i]; + var isSelected = index == _selectedIndex; + var itemStyle = isSelected ? "background-color: var(--mud-palette-action-default-hover);" : string.Empty; + + @suggestion + + } + + + } + + @if (_isLoading) + { + + } +
diff --git a/frontend/Money.Web/Components/SmartPlace.razor.cs b/frontend/Money.Web/Components/SmartPlace.razor.cs index 144b49b3..0a3a19c5 100644 --- a/frontend/Money.Web/Components/SmartPlace.razor.cs +++ b/frontend/Money.Web/Components/SmartPlace.razor.cs @@ -1,13 +1,20 @@ -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using System.Linq.Expressions; +using Timer = System.Timers.Timer; namespace Money.Web.Components; -public partial class SmartPlace(PlaceService placeService) +public sealed partial class SmartPlace(PlaceService placeService) : IDisposable { private string? _currentText; - private bool _isDebouncing; + private List _suggestions = []; + private bool _showSuggestions; + private bool _isLoading; + private int _selectedIndex = -1; + private bool _isFocused; + private CancellationTokenSource? _searchCancellationTokenSource; + private Timer? _debounceTimer; /// /// @@ -39,36 +46,175 @@ public partial class SmartPlace(PlaceService placeService) [Parameter] public int DebounceInterval { get; set; } = 300; + public void Dispose() + { + _searchCancellationTokenSource?.Dispose(); + _debounceTimer?.Dispose(); + GC.SuppressFinalize(this); + } + protected override void OnParametersSet() { _currentText = Value; } - private async Task> SearchPlaceAsync(string? value, CancellationToken token) + private Task OnFocusAsync(FocusEventArgs focusEventArgs) { - var places = await placeService.SearchPlace(value, token); - _isDebouncing = false; - return places; + _ = focusEventArgs; + _isFocused = true; + _selectedIndex = -1; + + _debounceTimer?.Stop(); + _debounceTimer?.Dispose(); + _debounceTimer = null; + + return LoadSuggestionsAsync(); } - private Task OnValueChanged(string? value) + private Task OnKeyDownAsync(KeyboardEventArgs args) { - return ValueChanged.InvokeAsync(_isDebouncing ? _currentText : value); + if (args.Key == "ArrowDown") + { + if (!_showSuggestions || _suggestions.Count <= 0) + { + return Task.CompletedTask; + } + + _selectedIndex = Math.Min(_selectedIndex + 1, _suggestions.Count - 1); + StateHasChanged(); + } + else if (args.Key == "ArrowUp") + { + if (!_showSuggestions || _suggestions.Count <= 0) + { + return Task.CompletedTask; + } + + _selectedIndex = Math.Max(_selectedIndex - 1, -1); + StateHasChanged(); + } + else if (args.Key == "Enter") + { + if (_showSuggestions && _selectedIndex >= 0 && _selectedIndex < _suggestions.Count) + { + return SelectSuggestionAsync(_suggestions[_selectedIndex]); + } + + return AcceptCurrentTextAsync(); + } + else if (args.Key == "Escape") + { + _showSuggestions = false; + _selectedIndex = -1; + StateHasChanged(); + } + else if (args.Key == "Tab") + { + if (!_showSuggestions || _suggestions.Count <= 0 || _selectedIndex < 0) + { + return AcceptCurrentTextAsync(); + } + + return SelectSuggestionAsync(_suggestions[_selectedIndex]); + } + + return Task.CompletedTask; } - private void OnTextChanged(string text) + private Task OnTextChangedAsync(string? value) { - _isDebouncing = true; - _currentText = text; + _currentText = value; + _selectedIndex = -1; + StartSearchDebounced(); + return Task.CompletedTask; } - private Task OnKeyDown(KeyboardEventArgs args) + private void StartSearchDebounced() { - if (args.Key == "Tab" && !string.IsNullOrWhiteSpace(_currentText)) + if (!_isFocused) { - return ValueChanged.InvokeAsync(_currentText); + return; } - return Task.CompletedTask; + _debounceTimer?.Stop(); + _debounceTimer?.Dispose(); + + _debounceTimer = new(DebounceInterval); + _debounceTimer.Elapsed += async (_, _) => + { + await InvokeAsync(LoadSuggestionsAsync); + }; + + _debounceTimer.AutoReset = false; + _debounceTimer.Start(); + } + + private async Task LoadSuggestionsAsync() + { + if (_searchCancellationTokenSource != null) + { + await _searchCancellationTokenSource.CancelAsync(); + _searchCancellationTokenSource.Dispose(); + } + + _searchCancellationTokenSource = new(); + + _isLoading = true; + _selectedIndex = -1; + StateHasChanged(); + + try + { + var searchValue = _currentText; + var places = await placeService.SearchPlace(searchValue, _searchCancellationTokenSource.Token); + _suggestions = [.. places]; + _showSuggestions = _isFocused && _suggestions.Count > 0; + } + catch (OperationCanceledException) + { + _showSuggestions = false; + _selectedIndex = -1; + } + catch (Exception) + { + _suggestions.Clear(); + _showSuggestions = false; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private async Task SelectSuggestionAsync(string suggestion) + { + _currentText = suggestion; + _showSuggestions = false; + _selectedIndex = -1; + await ValueChanged.InvokeAsync(_currentText); + StateHasChanged(); + } + + private async Task AcceptCurrentTextAsync() + { + _showSuggestions = false; + _selectedIndex = -1; + await ValueChanged.InvokeAsync(_currentText); + StateHasChanged(); + } + + private async Task OnBlurAsync() + { + _isFocused = false; + _debounceTimer?.Stop(); + _debounceTimer?.Dispose(); + _debounceTimer = null; + + await Task.Delay(200); + _showSuggestions = false; + _selectedIndex = -1; + await ValueChanged.InvokeAsync(_currentText); + StateHasChanged(); } } diff --git a/frontend/Money.Web/Layout/MainLayout.razor.cs b/frontend/Money.Web/Layout/MainLayout.razor.cs index 1167f3ff..5cdf39fe 100644 --- a/frontend/Money.Web/Layout/MainLayout.razor.cs +++ b/frontend/Money.Web/Layout/MainLayout.razor.cs @@ -1,4 +1,4 @@ -using Blazored.LocalStorage; +using Blazored.LocalStorage; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -38,8 +38,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender) return; } - _appSettings.IsDarkModeSystem = await _mudThemeProvider.GetSystemPreference(); - await _mudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged); + _appSettings.IsDarkModeSystem = await _mudThemeProvider.GetSystemDarkModeAsync(); + await _mudThemeProvider.WatchSystemDarkModeAsync(OnSystemPreferenceChanged); _darkModeToggle.UpdateState(); StateHasChanged(); diff --git a/frontend/Money.Web/Money.Web.csproj b/frontend/Money.Web/Money.Web.csproj index a6bac9b4..82c18644 100644 --- a/frontend/Money.Web/Money.Web.csproj +++ b/frontend/Money.Web/Money.Web.csproj @@ -24,6 +24,8 @@ + + @@ -31,7 +33,6 @@ - diff --git a/frontend/Money.Web/Pages/Account/Login.razor b/frontend/Money.Web/Pages/Account/Login.razor index d2ff6ca7..385f3828 100644 --- a/frontend/Money.Web/Pages/Account/Login.razor +++ b/frontend/Money.Web/Pages/Account/Login.razor @@ -57,20 +57,36 @@ - - Войти через bob217.Auth - - - Войти через GitHub - - + + Вход через внешние сервисы + + Используйте один из провайдеров ниже. Для входа требуется доступ к вашему email. + + + + + Войти через bob217.Auth + + + + Войти через GitHub + + + + + + + diff --git a/frontend/Money.Web/Program.cs b/frontend/Money.Web/Program.cs index 5afd6890..3fe93d1e 100644 --- a/frontend/Money.Web/Program.cs +++ b/frontend/Money.Web/Program.cs @@ -1,9 +1,9 @@ using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.Extensions.ServiceDiscovery; using Money.ApiClient; using Money.Web.Services.Authentication; -using Money.WebAssembly.CoreLib; using MudBlazor.Services; using MudBlazor.Translations; using NCalc.DependencyInjection; @@ -12,7 +12,20 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args); var apiUri = new Uri($"https://{builder.Configuration["Services:api:https:0"]}"); -builder.AddServiceDefaults(); +builder.Services.AddServiceDiscovery(); + +builder.Services.ConfigureHttpClientDefaults(http => +{ + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); +}); + +builder.Services.Configure(static options => +{ + options.SectionName = "Services"; + options.ShouldApplyHostNameMetadata = static _ => true; +}); + builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); diff --git a/frontend/Money.Web/Services/PlaceService.cs b/frontend/Money.Web/Services/PlaceService.cs index c8ce579a..214a2563 100644 --- a/frontend/Money.Web/Services/PlaceService.cs +++ b/frontend/Money.Web/Services/PlaceService.cs @@ -13,7 +13,7 @@ public async Task> SearchPlace(string? value, CancellationTo if (Cache.TryGetValue(value, out var cachedResults)) { - return EnsureValueInList(cachedResults, value); + return cachedResults; } var diff = value.Length - _lastSearchedValue.Length; @@ -23,14 +23,14 @@ public async Task> SearchPlace(string? value, CancellationTo && Cache.TryGetValue(_lastSearchedValue, out var cachedPlaces) && cachedPlaces.Length == 0) { - return [value]; + return []; } var response = await moneyClient.Operations.GetPlaces(0, 10, value, token); if (response.Content == null) { - return [value]; + return []; } var places = response.Content; @@ -38,18 +38,6 @@ public async Task> SearchPlace(string? value, CancellationTo Cache[value] = places; _lastSearchedValue = value; - return EnsureValueInList(places, value); - } - - private static List EnsureValueInList(IEnumerable list, string value) - { - var newList = list.ToList(); - - if (string.IsNullOrWhiteSpace(value) == false && newList.IndexOf(value) == -1) - { - newList.Insert(0, value); - } - - return newList; + return places; } } diff --git a/libs/Money.CoreLib/HostApplicationBuilderExtensions.cs b/libs/Money.CoreLib/HostApplicationBuilderExtensions.cs deleted file mode 100644 index bd36d1b1..00000000 --- a/libs/Money.CoreLib/HostApplicationBuilderExtensions.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using OpenTelemetry; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; - -namespace Money.CoreLib; - -public static class HostApplicationBuilderExtensions -{ - public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) - { - builder.ConfigureOpenTelemetry(); - - builder.AddDefaultHealthChecks(); - - builder.Services.AddServiceDiscovery(); - - builder.Services.ConfigureHttpClientDefaults(http => - { - http.AddStandardResilienceHandler(); - http.AddServiceDiscovery(); - }); - - return builder; - } - - public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) - { - builder.Logging.AddOpenTelemetryDefaults(); - - builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics.AddAspNetCoreInstrumentation() - .AddHttpClientInstrumentation() - .AddRuntimeInstrumentation(); - }) - .WithTracing(tracing => - { - tracing.AddAspNetCoreInstrumentation() - .AddEntityFrameworkCoreInstrumentation(p => - { - p.SetDbStatementForText = true; - - p.EnrichWithIDbCommand = (activity, command) => - { - var stateDisplayName = $"{command.CommandType} main"; - activity.DisplayName = stateDisplayName; - activity.SetTag("db.name", stateDisplayName); - }; - }) - .AddHttpClientInstrumentation(); - }); - - builder.AddOpenTelemetryExporters(); - - return builder; - } - - public static ILoggingBuilder AddOpenTelemetryDefaults(this ILoggingBuilder logging) - { - logging.AddOpenTelemetry(options => - { - options.IncludeFormattedMessage = true; - options.IncludeScopes = true; - }); - - return logging; - } - - public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) - { - builder.Services.AddHealthChecks() - .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); - - return builder; - } - - public static WebApplication MapDefaultEndpoints(this WebApplication app) - { - if (app.Environment.IsDevelopment()) - { - app.MapHealthChecks("/health"); - - app.MapHealthChecks("/alive", new() - { - Predicate = r => r.Tags.Contains("live"), - }); - } - - return app; - } - - private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) - { - var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); - - if (useOtlpExporter) - { - builder.Services.AddOpenTelemetry().UseOtlpExporter(); - } - - return builder; - } -} diff --git a/libs/Money.CoreLib/Money.CoreLib.csproj b/libs/Money.CoreLib/Money.CoreLib.csproj deleted file mode 100644 index f890afba..00000000 --- a/libs/Money.CoreLib/Money.CoreLib.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - diff --git a/libs/Money.WebAssembly.CoreLib/Money.WebAssembly.CoreLib.csproj b/libs/Money.WebAssembly.CoreLib/Money.WebAssembly.CoreLib.csproj deleted file mode 100644 index b677bdac..00000000 --- a/libs/Money.WebAssembly.CoreLib/Money.WebAssembly.CoreLib.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - - - diff --git a/libs/Money.WebAssembly.CoreLib/Properties/launchSettings.json b/libs/Money.WebAssembly.CoreLib/Properties/launchSettings.json deleted file mode 100644 index a1b9866f..00000000 --- a/libs/Money.WebAssembly.CoreLib/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "Money.WebAssembly.CoreLib": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:60354;http://localhost:60355" - } - } -} \ No newline at end of file diff --git a/libs/Money.WebAssembly.CoreLib/WebAssemblyHostBuilderExtensions.cs b/libs/Money.WebAssembly.CoreLib/WebAssemblyHostBuilderExtensions.cs deleted file mode 100644 index 1e88e2d9..00000000 --- a/libs/Money.WebAssembly.CoreLib/WebAssemblyHostBuilderExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.ServiceDiscovery; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; - -namespace Money.WebAssembly.CoreLib; - -public static class WebAssemblyHostBuilderExtensions -{ - public static WebAssemblyHostBuilder AddServiceDefaults(this WebAssemblyHostBuilder builder) - { - builder.ConfigureOpenTelemetry(); - builder.Services.AddServiceDiscovery(); - - builder.Services.ConfigureHttpClientDefaults(http => - { - http.AddStandardResilienceHandler(); - http.AddServiceDiscovery(); - }); - - builder.Services.Configure(static options => - { - options.SectionName = "Services"; - options.ShouldApplyHostNameMetadata = static _ => true; - }); - - return builder; - } - - public static WebAssemblyHostBuilder ConfigureOpenTelemetry(this WebAssemblyHostBuilder builder) - { - builder.Services.AddOpenTelemetry() - .WithMetrics(metrics => - { - metrics.AddHttpClientInstrumentation(); - }) - .WithTracing(tracing => - { - tracing.AddHttpClientInstrumentation(); - }); - - return builder; - } -}