Register page
This commit is contained in:
		| @@ -7,6 +7,10 @@ | ||||
|     </PropertyGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|         <PackageReference Include="AutoMapper" Version="13.0.1" /> | ||||
|         <PackageReference Include="FluentValidation" Version="11.9.2" /> | ||||
|         <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" Version="3.2.0-rc1.20223.4" /> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.1"/> | ||||
|         <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.1" PrivateAssets="all"/> | ||||
|     </ItemGroup> | ||||
| @@ -15,4 +19,8 @@ | ||||
|       <ProjectReference Include="..\VisaApiClient\VisaApiClient.csproj" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
|     <ItemGroup> | ||||
|       <_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" /> | ||||
|     </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -0,0 +1,39 @@ | ||||
| @using VisaApiClient | ||||
|  | ||||
| <div> | ||||
|     <div > | ||||
|         <label > | ||||
|             Country:<br/> | ||||
|             <InputText class="rounded" @bind-Value="Address.Country"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Address.Country"></ValidationMessage><br/> | ||||
|     </div> | ||||
|  | ||||
|     <div > | ||||
|         <label > | ||||
|             City:<br/> | ||||
|             <InputText class="rounded" @bind-Value="Address.City"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Address.City"></ValidationMessage><br/> | ||||
|     </div> | ||||
|  | ||||
|     <div > | ||||
|         <label > | ||||
|             Street:<br/> | ||||
|             <InputText class="rounded" @bind-Value="Address.Street"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Address.Street"></ValidationMessage><br/> | ||||
|     </div> | ||||
|  | ||||
|     <div > | ||||
|         <label > | ||||
|             Building:<br/> | ||||
|             <InputText class="rounded" @bind-Value="Address.Building"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Address.Building"></ValidationMessage> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @code { | ||||
|     [Parameter, EditorRequired] public AddressModel Address { get; set; } = null!; | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| @using VisaApiClient | ||||
|  | ||||
| <div> | ||||
|     <div > | ||||
|         <label > | ||||
|             Email:<br/> | ||||
|             <InputText class="rounded" @bind-Value="AuthData.Email"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => AuthData.Email"></ValidationMessage><br/> | ||||
|     </div> | ||||
|  | ||||
|     <div > | ||||
|         <label > | ||||
|             Password:<br/> | ||||
|             <InputText class="rounded" @bind-Value="AuthData.Password"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => AuthData.Password"></ValidationMessage> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @code { | ||||
|     [Parameter, EditorRequired] public AuthData AuthData { get; set; } = null!; | ||||
| } | ||||
| @@ -0,0 +1,50 @@ | ||||
| @using System.ComponentModel.DataAnnotations | ||||
| @using System.Linq.Expressions | ||||
| @using System.Reflection | ||||
| @typeparam TItem where TItem : class | ||||
|     @typeparam TMember where TMember : struct, Enum | ||||
|  | ||||
| <InputSelect TValue="TMember"  @bind-Value="selected"> | ||||
|     @foreach (var value in enumValues) | ||||
|     { | ||||
|         <option value="@value.Key">@value.Value</option> | ||||
|     } | ||||
| </InputSelect><br/> | ||||
|  | ||||
| @code { | ||||
|     [Parameter, EditorRequired] public TItem Model { get; set; } = default!; | ||||
|  | ||||
|     [Parameter, EditorRequired] public Expression<Func<TItem, TMember>> EnumProperty { get; set; } = null!; | ||||
|  | ||||
|     private Dictionary<TMember, string> enumValues = new(); | ||||
|     private PropertyInfo modelMemberInfo = null!; | ||||
|     private TMember selected; | ||||
|  | ||||
|     protected override void OnInitialized() | ||||
|     { | ||||
|         var enumMembers = typeof(TMember).GetMembers(); | ||||
|         var modelMemberName = ((MemberExpression)EnumProperty.Body).Member.Name; | ||||
|         modelMemberInfo = typeof(TItem).GetProperty(modelMemberName)!; | ||||
|  | ||||
|         foreach (var value in Enum.GetValues<TMember>()) | ||||
|         { | ||||
|             var member = enumMembers.First(info => info.Name == value.ToString()); | ||||
|             var displayAttribute = (DisplayAttribute?)member | ||||
|                 .GetCustomAttributes(typeof(DisplayAttribute), false) | ||||
|                 .FirstOrDefault(); | ||||
|             var displayName = displayAttribute?.Name ?? value.ToString(); | ||||
|             enumValues.Add(value, displayName); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected override void OnAfterRender(bool firstRender) | ||||
|     { | ||||
|         OnValueChanged(); | ||||
|     } | ||||
|  | ||||
|     private void OnValueChanged() | ||||
|     { | ||||
|         modelMemberInfo.SetValue(Model, selected); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,31 @@ | ||||
| @using VisaApiClient | ||||
|  | ||||
| <div> | ||||
|     <div > | ||||
|         <label> | ||||
|             First name:<br/> | ||||
|             <InputText DisplayName="First name" class="rounded" @bind-Value="Name.FirstName"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Name.FirstName"></ValidationMessage> | ||||
|     </div><br/> | ||||
|  | ||||
|     <div > | ||||
|         <label> | ||||
|             Surname:<br/> | ||||
|             <InputText class="rounded" @bind-Value="Name.Surname"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Name.Surname"></ValidationMessage> | ||||
|     </div><br/> | ||||
|  | ||||
|     <div > | ||||
|         <label> | ||||
|             Patronymic:<br/> | ||||
|             <InputText class="rounded" @bind-Value="Name.Patronymic"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Name.Patronymic"></ValidationMessage> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @code { | ||||
|     [Parameter, EditorRequired] public NameModel Name { get; set; } = null!; | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| @using BlazorWebAssemblyVisaApiClient.Infrastructure.Services | ||||
| @using VisaApiClient | ||||
|  | ||||
| <div> | ||||
|     <div > | ||||
|         <label> | ||||
|             Passport number:<br/> | ||||
|             <InputText DisplayName="Passport number" class="rounded" @bind-Value="Passport.Number"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Passport.Number"></ValidationMessage> | ||||
|     </div><br/> | ||||
|  | ||||
|     <div > | ||||
|         <label> | ||||
|             Issuer:<br/> | ||||
|             <InputText class="rounded" @bind-Value="Passport.Issuer"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Passport.Issuer"></ValidationMessage> | ||||
|     </div><br/> | ||||
|  | ||||
|     <div > | ||||
|         <label> | ||||
|             Issue date:<br/> | ||||
|             <InputDate DisplayName="Issue date" class="rounded" @bind-Value="Passport.IssueDate" max="DateTimeProvider.Now()"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Passport.IssueDate"></ValidationMessage> | ||||
|     </div><br/> | ||||
|  | ||||
|     <div > | ||||
|         <label> | ||||
|             Expiration date:<br/> | ||||
|             <InputDate DisplayName="Expiration date" class="rounded" @bind-Value="Passport.ExpirationDate" min="DateTimeProvider.Now()"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => Passport.ExpirationDate"></ValidationMessage> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @code { | ||||
|     private string now = DateTime.Now.ToString("yyyy-MM-dd"); | ||||
|  | ||||
|     [Parameter, EditorRequired] public PassportModel Passport { get; set; } = null!; | ||||
|  | ||||
|     [Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!; | ||||
|  | ||||
|     protected override void OnInitialized() | ||||
|     { | ||||
|         Passport.IssueDate = DateTime.Now; | ||||
|         Passport.ExpirationDate = DateTime.Now; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| @using BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models | ||||
|  | ||||
| <div> | ||||
|     <div > | ||||
|         <label > | ||||
|             Name:<br/> | ||||
|             <InputText class="rounded" @bind-Value="PlaceOfWork.Name"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => PlaceOfWork.Name"></ValidationMessage><br/> | ||||
|     </div> | ||||
|  | ||||
|     <div > | ||||
|         <label > | ||||
|             Phone number:<br/> | ||||
|             <InputText DisplayName="Phone number" class="rounded" @bind-Value="PlaceOfWork.PhoneNum"/> | ||||
|         </label><br/> | ||||
|         <ValidationMessage For="() => PlaceOfWork.PhoneNum"></ValidationMessage> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @code { | ||||
|     [Parameter, EditorRequired] public PlaceOfWorkModel PlaceOfWork { get; set; } = null!; | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| <h3>CustomValidation</h3> | ||||
|  | ||||
| @code { | ||||
|     private ValidationMessageStore? messageStore; | ||||
|  | ||||
|     [CascadingParameter] | ||||
|     private EditContext? CurrentEditContext { get; set; } | ||||
|  | ||||
|     protected override void OnInitialized() | ||||
|     { | ||||
|         if (CurrentEditContext is null) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 $"{nameof(CustomValidation)} requires a cascading " + | ||||
|                 $"parameter of type {nameof(EditContext)}. " + | ||||
|                 $"For example, you can use {nameof(CustomValidation)} " + | ||||
|                 $"inside an {nameof(EditForm)}."); | ||||
|         } | ||||
|  | ||||
|         messageStore = new(CurrentEditContext); | ||||
|  | ||||
|         CurrentEditContext.OnValidationRequested += (_, _) => | ||||
|             messageStore?.Clear(); | ||||
|         CurrentEditContext.OnFieldChanged += (_, e) => | ||||
|             messageStore?.Clear(e.FieldIdentifier); | ||||
|     } | ||||
|  | ||||
|     public void DisplayErrors(Dictionary<string, List<string>> errors) | ||||
|     { | ||||
|         if (CurrentEditContext is not null) | ||||
|         { | ||||
|             foreach (var err in errors) | ||||
|             { | ||||
|                 messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value); | ||||
|             } | ||||
|  | ||||
|             CurrentEditContext.NotifyValidationStateChanged(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void ClearErrors() | ||||
|     { | ||||
|         messageStore?.Clear(); | ||||
|         CurrentEditContext?.NotifyValidationStateChanged(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models | ||||
| { | ||||
|     /// Model of place of work with attributes required for validation to work | ||||
|     public class PlaceOfWorkModel | ||||
|     { | ||||
|         [Required] | ||||
|         [StringLength(ConfigurationConstraints.PlaceOfWorkNameLength, MinimumLength = 1)] | ||||
|         public string Name { get; set; } = default!; | ||||
|  | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public AddressModel Address { get; set; } = new AddressModel(); | ||||
|  | ||||
|         [Required] | ||||
|         [StringLength(ConfigurationConstraints.PhoneNumberLength, MinimumLength = ConfigurationConstraints.PhoneNumberMinLength)] | ||||
|         public string PhoneNum { get; set; } = default!; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,68 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Converters; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models | ||||
| { | ||||
|     /// Model of request with attributes required for validation to work | ||||
|     public class RegisterApplicantRequestModel | ||||
|     { | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public RegisterRequestModel RegisterRequest { get; set; } = new(); | ||||
|  | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public NameModel ApplicantName { get; set; } = new(); | ||||
|  | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public PassportModel Passport { get; set; } = new(); | ||||
|  | ||||
|         [Required(AllowEmptyStrings = true)] | ||||
|         public DateTimeOffset BirthDate { get; set; } | ||||
|  | ||||
|         [Required] | ||||
|         [StringLength(70, MinimumLength = 1)] | ||||
|         public string CityOfBirth { get; set; } = default!; | ||||
|  | ||||
|         [Required] | ||||
|         [StringLength(70, MinimumLength = 1)] | ||||
|         public string CountryOfBirth { get; set; } = default!; | ||||
|  | ||||
|         [Required] | ||||
|         [StringLength(30, MinimumLength = 1)] | ||||
|         public string Citizenship { get; set; } = default!; | ||||
|  | ||||
|         [Required] | ||||
|         [StringLength(30, MinimumLength = 1)] | ||||
|         public string CitizenshipByBirth { get; set; } = default!; | ||||
|  | ||||
|         [Required(AllowEmptyStrings = true)] | ||||
|         [JsonConverter(typeof(StringEnumConverter))] | ||||
|         public Gender Gender { get; set; } | ||||
|  | ||||
|         [Required(AllowEmptyStrings = true)] | ||||
|         [JsonConverter(typeof(StringEnumConverter))] | ||||
|         public MaritalStatus MaritalStatus { get; set; } | ||||
|  | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public NameModel FatherName { get; set; } = new(); | ||||
|  | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public NameModel MotherName { get; set; } = new(); | ||||
|  | ||||
|         [Required] | ||||
|         [StringLength(50, MinimumLength = 1)] | ||||
|         public string JobTitle { get; set; } = default!; | ||||
|  | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public PlaceOfWorkModel PlaceOfWork { get; set; } = new(); | ||||
|  | ||||
|         public bool IsNonResident { get; set; } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,13 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models | ||||
| { | ||||
|     /// Model of request with attributes required for validation to work | ||||
|     public class RegisterRequestModel | ||||
|     { | ||||
|         [Required] | ||||
|         [ValidateComplexType] | ||||
|         public AuthData AuthData { get; set; } = new AuthData(); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| using FluentValidation; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Validators; | ||||
|  | ||||
| public class NameModelValidator : AbstractValidator<NameModel> | ||||
| { | ||||
|     public NameModelValidator() | ||||
|     { | ||||
|         RuleFor(m => m.FirstName) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("First Name can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.NameLength) | ||||
|             .WithMessage($"First Name length must be less than {ConfigurationConstraints.NameLength}"); | ||||
|  | ||||
|         RuleFor(m => m.Surname) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Surname can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.NameLength) | ||||
|             .WithMessage($"Surname length must be less than {ConfigurationConstraints.NameLength}"); | ||||
|  | ||||
|         RuleFor(m => m.Patronymic) | ||||
|             .MaximumLength(ConfigurationConstraints.NameLength) | ||||
|             .WithMessage($"Patronymic length must be less than {ConfigurationConstraints.NameLength}"); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,35 @@ | ||||
| using BlazorWebAssemblyVisaApiClient.Infrastructure.Services; | ||||
| using FluentValidation; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Validators; | ||||
|  | ||||
| public class PassportModelValidator : AbstractValidator<PassportModel> | ||||
| { | ||||
|     public PassportModelValidator(IDateTimeProvider dateTimeProvider) | ||||
|     { | ||||
|         RuleFor(r => r.Issuer) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Passport issuer can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.IssuerNameLength) | ||||
|             .WithMessage($"Passport issuer length must be less than {ConfigurationConstraints.IssuerNameLength}"); | ||||
|  | ||||
|         RuleFor(r => r.Number) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Passport number can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.PassportNumberLength) | ||||
|             .WithMessage($"Passport number length must be less than {ConfigurationConstraints.PassportNumberLength}"); | ||||
|  | ||||
|         RuleFor(r => r.ExpirationDate) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Passport expiration date can not be empty") | ||||
|             .GreaterThan(dateTimeProvider.Now()) | ||||
|             .WithMessage("Can not approve visa for applicants with expired passport"); | ||||
|  | ||||
|         RuleFor(r => r.IssueDate) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Passport issue date can not be empty") | ||||
|             .LessThanOrEqualTo(dateTimeProvider.Now()) | ||||
|             .WithMessage("Passport issue date must be in past"); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,50 @@ | ||||
| using BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models; | ||||
| using FluentValidation; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Validators; | ||||
|  | ||||
| public class PlaceOfWorkModelValidator : AbstractValidator<PlaceOfWorkModel> | ||||
| { | ||||
|     public PlaceOfWorkModelValidator() | ||||
|     { | ||||
|         RuleFor(p => p.Name) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Place of work name can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.PlaceOfWorkNameLength) | ||||
|             .WithMessage($"Place of work name length must be less than {ConfigurationConstraints.PlaceOfWorkNameLength}"); | ||||
|  | ||||
|         RuleFor(p => p.PhoneNum) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Place of work phone number can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.PhoneNumberLength) | ||||
|             .WithMessage( | ||||
|                 $"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}") | ||||
|             .MinimumLength(ConfigurationConstraints.PhoneNumberMinLength) | ||||
|             .WithMessage( | ||||
|                 $"Phone number length must be in range from {ConfigurationConstraints.PhoneNumberMinLength} to {ConfigurationConstraints.PhoneNumberLength}"); | ||||
|  | ||||
|         RuleFor(p => p.Address.Country) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Country name of place of work can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.CountryNameLength) | ||||
|             .WithMessage($"Country name of place of work length must be less than {ConfigurationConstraints.CountryNameLength}"); | ||||
|  | ||||
|         RuleFor(p => p.Address.City) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("City name of place of work can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.CityNameLength) | ||||
|             .WithMessage($"City name of place of work length must be less than {ConfigurationConstraints.CityNameLength}"); | ||||
|  | ||||
|         RuleFor(p => p.Address.Street) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Street name of place of work can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.StreetNameLength) | ||||
|             .WithMessage($"Street name of place of work length must be less than {ConfigurationConstraints.StreetNameLength}"); | ||||
|  | ||||
|         RuleFor(p => p.Address.Building) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Building of place of work can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.CountryNameLength) | ||||
|             .WithMessage($"Building of place of work length must be less than {ConfigurationConstraints.BuildingNumberLength}"); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,76 @@ | ||||
| using BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models; | ||||
| using BlazorWebAssemblyVisaApiClient.Infrastructure.Services; | ||||
| using FluentValidation; | ||||
| using VisaApiClient; | ||||
| using PlaceOfWorkModel = BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models.PlaceOfWorkModel; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Validators; | ||||
|  | ||||
| public class RegisterApplicantRequestValidator : AbstractValidator<RegisterApplicantRequestModel> | ||||
| { | ||||
|     public RegisterApplicantRequestValidator( | ||||
|         IDateTimeProvider dateTimeProvider, | ||||
|         IValidator<NameModel> nameValidator, | ||||
|         IValidator<RegisterRequestModel> registerRequestModelValidator, | ||||
|         IValidator<PassportModel> passportValidator, | ||||
|         IValidator<PlaceOfWorkModel> placeOfWorkModelValidator) | ||||
|     { | ||||
|         RuleFor(r => r.RegisterRequest) | ||||
|             .SetValidator(registerRequestModelValidator); | ||||
|  | ||||
|         RuleFor(r => r.ApplicantName) | ||||
|             .SetValidator(nameValidator); | ||||
|  | ||||
|         RuleFor(r => r.FatherName) | ||||
|             .SetValidator(nameValidator); | ||||
|  | ||||
|         RuleFor(r => r.MotherName) | ||||
|             .SetValidator(nameValidator); | ||||
|  | ||||
|         RuleFor(r => r.Passport) | ||||
|             .SetValidator(passportValidator); | ||||
|  | ||||
|         RuleFor(r => r.BirthDate) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Birth date can not be empty") | ||||
|             .LessThanOrEqualTo(dateTimeProvider.Now().AddYears(-ConfigurationConstraints.ApplicantMinAge)) | ||||
|             .WithMessage($"Applicant must be older than {ConfigurationConstraints.ApplicantMinAge}"); | ||||
|  | ||||
|         RuleFor(r => r.CountryOfBirth) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Country of birth can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.CountryNameLength) | ||||
|             .WithMessage($"Country of birth name length must be less than {ConfigurationConstraints.CountryNameLength}"); | ||||
|  | ||||
|         RuleFor(r => r.CityOfBirth) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("City of birth can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.CityNameLength) | ||||
|             .WithMessage($"City of birth name length must be less than {ConfigurationConstraints.CityNameLength}"); | ||||
|  | ||||
|         RuleFor(r => r.Citizenship) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Citizenship can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.CitizenshipLength) | ||||
|             .WithMessage($"Citizenship length must be less than {ConfigurationConstraints.CitizenshipLength}"); | ||||
|  | ||||
|         RuleFor(r => r.CitizenshipByBirth) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Citizenship by birth can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.CitizenshipLength) | ||||
|             .WithMessage($"Citizenship by birth length must be less than {ConfigurationConstraints.CitizenshipLength}"); | ||||
|  | ||||
|         RuleFor(r => r.Gender).IsInEnum(); | ||||
|  | ||||
|         RuleFor(r => r.MaritalStatus).IsInEnum(); | ||||
|  | ||||
|         RuleFor(r => r.JobTitle) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Title of job can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.JobTitleLength) | ||||
|             .WithMessage($"Title of job length must be less than {ConfigurationConstraints.JobTitleLength}"); | ||||
|  | ||||
|         RuleFor(r => r.PlaceOfWork) | ||||
|             .SetValidator(placeOfWorkModelValidator); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| using FluentValidation; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Common; | ||||
|  | ||||
| public class AuthDataValidator : AbstractValidator<AuthData> | ||||
| { | ||||
|     public AuthDataValidator() | ||||
|     { | ||||
|         RuleFor(d => d.Email) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Email can not be empty") | ||||
|             .EmailAddress() | ||||
|             .WithMessage("Email must be valid") | ||||
|             .MaximumLength(ConfigurationConstraints.EmailLength) | ||||
|             .WithMessage($"Email length must be less than {ConfigurationConstraints.EmailLength}"); | ||||
|  | ||||
|         RuleFor(d => d.Password) | ||||
|             .NotEmpty() | ||||
|             .WithMessage("Password can not be empty") | ||||
|             .MaximumLength(ConfigurationConstraints.PasswordLength) | ||||
|             .WithMessage($"Password length must be less than {ConfigurationConstraints.PasswordLength}"); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,14 @@ | ||||
| using BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models; | ||||
| using FluentValidation; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation.Common; | ||||
|  | ||||
| public class RegisterRequestModelValidator : AbstractValidator<RegisterRequestModel> | ||||
| { | ||||
|     public RegisterRequestModelValidator(IValidator<AuthData> authDataValidator) | ||||
|     { | ||||
|         RuleFor(r => r.AuthData) | ||||
|             .SetValidator(authDataValidator); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| namespace BlazorWebAssemblyVisaApiClient.FluentValidation; | ||||
|  | ||||
| public static class ConfigurationConstraints | ||||
| { | ||||
|     public const int CityNameLength = 70; | ||||
|     public const int CountryNameLength = 70; | ||||
|     public const int CitizenshipLength = 30; | ||||
|     public const int ReentryPermitNumberLength = 25; | ||||
|     public const int IssuerNameLength = 200; | ||||
|     public const int VisaNameLength = 70; | ||||
|     public const int StreetNameLength = 100; | ||||
|     public const int PlaceOfWorkNameLength = 200; | ||||
|     public const int NameLength = 50; | ||||
|     public const int BuildingNumberLength = 10; | ||||
|     public const int PassportNumberLength = 20; | ||||
|     public const int PhoneNumberLength = 13; | ||||
|     public const int PhoneNumberMinLength = 11; | ||||
|     public const int EmailLength = 254; | ||||
|     public const int PasswordLength = 50; | ||||
|     public const int ApplicantMinAge = 14; | ||||
|     public const int JobTitleLength = 50; | ||||
|     public const int MaxValidDays = 90; | ||||
| } | ||||
| @@ -0,0 +1,18 @@ | ||||
| using AutoMapper; | ||||
| using BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models; | ||||
| using VisaApiClient; | ||||
|  | ||||
| namespace BlazorWebAssemblyVisaApiClient.Infrastructure.AutoMapper.Profiles | ||||
| { | ||||
|     public class RegisterApplicantRequestProfile : Profile | ||||
|     { | ||||
|         public RegisterApplicantRequestProfile() | ||||
|         { | ||||
|             CreateMap<RegisterApplicantRequestModel, RegisterApplicantRequest>(MemberList.Destination); | ||||
|  | ||||
|             CreateMap<RegisterRequestModel, RegisterRequest>(MemberList.Destination); | ||||
|  | ||||
|             CreateMap<FluentValidation.Applicants.Models.PlaceOfWorkModel, VisaApiClient.PlaceOfWorkModel>(MemberList.Destination); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services | ||||
| { | ||||
|     public class DateTimeProvider : IDateTimeProvider | ||||
|     { | ||||
|         public DateTime Now() => DateTime.Now; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| namespace BlazorWebAssemblyVisaApiClient.Infrastructure.Services | ||||
| { | ||||
|     public interface IDateTimeProvider | ||||
|     { | ||||
|         DateTime Now(); | ||||
|     } | ||||
| } | ||||
| @@ -5,11 +5,7 @@ | ||||
|     </div> | ||||
|  | ||||
|     <main> | ||||
|         <div class="top-row px-4"> | ||||
|             <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> | ||||
|         </div> | ||||
|  | ||||
|         <article class="content px-4"> | ||||
|         <article class="content px-4 fullscreen"> | ||||
|             @Body | ||||
|         </article> | ||||
|     </main> | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <div class="top-row ps-3 navbar navbar-dark"> | ||||
|     <div class="container-fluid"> | ||||
|         <a class="navbar-brand" href="">BlazorWebAssemblyVisaApiClient</a> | ||||
|         <a class="navbar-brand" href="">Schengen Visa</a> | ||||
|         <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu"> | ||||
|             <span class="navbar-toggler-icon"></span> | ||||
|         </button> | ||||
| @@ -11,17 +11,7 @@ | ||||
|     <nav class="flex-column"> | ||||
|         <div class="nav-item px-3"> | ||||
|             <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> | ||||
|                 <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home | ||||
|             </NavLink> | ||||
|         </div> | ||||
|         <div class="nav-item px-3"> | ||||
|             <NavLink class="nav-link" href="counter"> | ||||
|                 <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter | ||||
|             </NavLink> | ||||
|         </div> | ||||
|         <div class="nav-item px-3"> | ||||
|             <NavLink class="nav-link" href="weather"> | ||||
|                 <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather | ||||
|                 <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Login | ||||
|             </NavLink> | ||||
|         </div> | ||||
|     </nav> | ||||
|   | ||||
| @@ -1,31 +1,38 @@ | ||||
| @page "/login" | ||||
| @page "/" | ||||
| @using VisaApiClient | ||||
| @using BlazorWebAssemblyVisaApiClient.Components.FormComponents.Applicants | ||||
|  | ||||
| <PageTitle>Authentication</PageTitle> | ||||
|  | ||||
| <EditForm Model="loginData" OnValidSubmit="TryLogin"> | ||||
|     <DataAnnotationsValidator/> | ||||
|     <label >Email: <InputText @bind-Value="loginData.Email"/></label> | ||||
|     <label >Password: <InputText @bind-Value="loginData.Password"/></label> | ||||
|     <input type="submit" value="Login"/> | ||||
| </EditForm> | ||||
| <p>@loginResult</p> | ||||
| <div class="with-centered-content"> | ||||
|     <EditForm class="form" Model="loginData" OnValidSubmit="TryLogin"> | ||||
|         <DataAnnotationsValidator/> | ||||
|  | ||||
|         <AuthDataInput AuthData="loginData"/><br/> | ||||
|  | ||||
|         <input class="btn-outline-primary rounded" type="submit" value="Login"/> | ||||
|         or | ||||
|         <NavLink href="register">Register</NavLink > | ||||
|         <p>@loginResult</p> | ||||
|     </EditForm> | ||||
| </div> | ||||
|  | ||||
| @code | ||||
| { | ||||
|     private AuthData loginData = new(); | ||||
|     private string loginResult = string.Empty; | ||||
|  | ||||
|     [Inject] | ||||
|     private Client Client { get; set; } = null!; | ||||
|     [Inject] private Client Client { get; set; } = null!; | ||||
|  | ||||
|     private async Task TryLogin(EditContext obj) | ||||
|     private async Task TryLogin() | ||||
|     { | ||||
|         loginResult = "Wait..."; | ||||
|         StateHasChanged(); | ||||
|         try | ||||
|         { | ||||
|             var token = await Client.LoginAsync(loginData.Email, loginData.Password); | ||||
|             Client.SetAuthToken(token); | ||||
|             loginResult = "Logged in successfully"; | ||||
|             loginResult = "Logged in successfully."; | ||||
|         } | ||||
|         catch (ApiException<ProblemDetails> e) | ||||
|         { | ||||
|   | ||||
| @@ -1,19 +0,0 @@ | ||||
| @page "/counter" | ||||
|  | ||||
| <PageTitle>Counter</PageTitle> | ||||
|  | ||||
| <h1>Counter</h1> | ||||
|  | ||||
| <p role="status">Current count: @currentCount</p> | ||||
|  | ||||
| <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> | ||||
|  | ||||
| @code { | ||||
|     private int currentCount = 0; | ||||
|  | ||||
|     private void IncrementCount() | ||||
|     { | ||||
|         currentCount++; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,7 +0,0 @@ | ||||
| @page "/" | ||||
|  | ||||
| <PageTitle>Home</PageTitle> | ||||
|  | ||||
| <h1>Hello, world!</h1> | ||||
|  | ||||
| Welcome to your new app. | ||||
| @@ -0,0 +1,218 @@ | ||||
| @page "/register" | ||||
| @using System.Net | ||||
| @using System.Text | ||||
| @using AutoMapper | ||||
| @using VisaApiClient | ||||
| @using BlazorWebAssemblyVisaApiClient.Components.FormComponents.Applicants | ||||
| @using BlazorWebAssemblyVisaApiClient.FluentValidation.Applicants.Models | ||||
| @using BlazorWebAssemblyVisaApiClient.Infrastructure.Services | ||||
| @using global::FluentValidation | ||||
| @using Newtonsoft.Json | ||||
| @using Newtonsoft.Json.Linq | ||||
|  | ||||
| <PageTitle>Registration</PageTitle> | ||||
|  | ||||
| <div class="horizontal-centered-content"> | ||||
|     <h3>Registration data</h3> | ||||
|     <EditForm class="form" Model="requestModel" OnValidSubmit="TryRegisterApplicant"> | ||||
|         <ObjectGraphDataAnnotationsValidator/> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Authentication data</h5> | ||||
|             <AuthDataInput AuthData="requestModel.RegisterRequest.AuthData"/> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Your Fullname</h5> | ||||
|             <NameInput Name="requestModel.ApplicantName"/> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Fullname of your mother</h5> | ||||
|             <NameInput Name="requestModel.MotherName"/> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Fullname of your father</h5> | ||||
|             <NameInput Name="requestModel.FatherName"/> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Your passport</h5> | ||||
|             <PassportInput Passport="requestModel.Passport"/> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Birth data</h5> | ||||
|             <div > | ||||
|                 <label> | ||||
|                     Country of birth:<br/> | ||||
|                     <InputText DisplayName="Country of birth" class="rounded" @bind-Value="requestModel.CountryOfBirth"/> | ||||
|                 </label><br/> | ||||
|                 <ValidationMessage For="() => requestModel.CountryOfBirth"></ValidationMessage><br/> | ||||
|                 <label> | ||||
|                     City of birth:<br/> | ||||
|                     <InputText DisplayName="City of birth" class="rounded" @bind-Value="requestModel.CityOfBirth"/> | ||||
|                 </label><br/> | ||||
|                 <ValidationMessage For="() => requestModel.CityOfBirth"></ValidationMessage><br/> | ||||
|                 <label> | ||||
|                     Birth date:<br/> | ||||
|                     <InputDate DisplayName="Birth date" class="rounded" @bind-Value="requestModel.BirthDate" max="@DateTimeProvider.Now()"/> | ||||
|                 </label><br/> | ||||
|                 <ValidationMessage For="() => requestModel.BirthDate"></ValidationMessage> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Citizenship</h5> | ||||
|             <div > | ||||
|                 <label> | ||||
|                     Citizenship:<br/> | ||||
|                     <InputText class="rounded" @bind-Value="requestModel.Citizenship"/> | ||||
|                 </label><br/> | ||||
|                 <ValidationMessage For="() => requestModel.Citizenship"></ValidationMessage><br/> | ||||
|                 <label> | ||||
|                     Citizenship by birth:<br/> | ||||
|                     <InputText DisplayName="Citizenship by birth" class="rounded" @bind-Value="requestModel.CitizenshipByBirth"/> | ||||
|                 </label><br/> | ||||
|                 <ValidationMessage For="() => requestModel.CitizenshipByBirth"></ValidationMessage> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Address of your place of work</h5> | ||||
|             <div > | ||||
|                 <AddressInput Address="requestModel.PlaceOfWork.Address"/> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Place of work data</h5> | ||||
|             <div > | ||||
|                 <PlaceOfWorkInput PlaceOfWork="requestModel.PlaceOfWork"/><br/> | ||||
|  | ||||
|                 <label> | ||||
|                     Job title:<br/> | ||||
|                     <InputText DisplayName="Job title" class="rounded" @bind-Value="requestModel.JobTitle"/> | ||||
|                 </label><br/> | ||||
|                 <ValidationMessage For="() => requestModel.JobTitle"></ValidationMessage> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-block"> | ||||
|             <h5>Other</h5> | ||||
|  | ||||
|             <div > | ||||
|                 <label> | ||||
|                     Gender: <EnumInputList Model="requestModel" EnumProperty="r => r.Gender"/> | ||||
|                 </label> | ||||
|             </div><br/> | ||||
|  | ||||
|             <div > | ||||
|                 <label> | ||||
|                     Marital status: <EnumInputList Model="requestModel" EnumProperty="r => r.MaritalStatus"/> | ||||
|                 </label> | ||||
|             </div><br/> | ||||
|  | ||||
|             <div > | ||||
|                 <label> | ||||
|                     Non-resident: <InputCheckbox @bind-Value="requestModel.IsNonResident"/> | ||||
|                 </label> | ||||
|             </div> | ||||
|         </div><br/> | ||||
|  | ||||
|         <input type="submit" class="btn-outline-primary" value="Register"/> | ||||
|         <p class="@requestResultClass">@((MarkupString)requestResult)</p> | ||||
|     </EditForm> | ||||
| </div> | ||||
|  | ||||
| @code | ||||
| { | ||||
|     private RegisterApplicantRequestModel requestModel = new(); | ||||
|     private string requestResult = string.Empty; | ||||
|     private string requestResultClass = string.Empty; | ||||
|  | ||||
|     [Inject] public Client Client { get; set; } = null!; | ||||
|  | ||||
|     [Inject] IValidator<RegisterApplicantRequestModel> RegisterApplicantRequestValidator { get; set; } = null!; | ||||
|  | ||||
|     [Inject] IMapper Mapper { get; set; } = null!; | ||||
|  | ||||
|     [Inject] IDateTimeProvider DateTimeProvider { get; set; } = null!; | ||||
|  | ||||
|     protected override void OnInitialized() | ||||
|     { | ||||
|         requestModel.BirthDate = DateTime.Now; | ||||
|     } | ||||
|  | ||||
|     private void SetRequestResultMessage(string message) | ||||
|     { | ||||
|         requestResult = message; | ||||
|         requestResultClass = string.Empty; | ||||
|         StateHasChanged(); | ||||
|     } | ||||
|  | ||||
|     private void SetRequestResultSuccess(string message) | ||||
|     { | ||||
|         requestResult = message; | ||||
|         requestResultClass = "text-success"; | ||||
|         StateHasChanged(); | ||||
|     } | ||||
|  | ||||
|     private void SetRequestResultError(string message) | ||||
|     { | ||||
|         requestResult = message; | ||||
|         requestResultClass = "validation-message"; | ||||
|         StateHasChanged(); | ||||
|     } | ||||
|  | ||||
|     private string ErrorsToString(IEnumerable<string> errors) | ||||
|     { | ||||
|         var stringBuilder = new StringBuilder(); | ||||
|         foreach (var error in errors) | ||||
|         { | ||||
|             stringBuilder.Append($"{error}<br/>"); | ||||
|         } | ||||
|  | ||||
|         return stringBuilder.ToString(); | ||||
|     } | ||||
|  | ||||
|     private async void TryRegisterApplicant() | ||||
|     { | ||||
|         var validationResult = await RegisterApplicantRequestValidator.ValidateAsync(requestModel); | ||||
|         if (!validationResult.IsValid) | ||||
|         { | ||||
|             var errorsString = ErrorsToString(validationResult.Errors.Select(e => e.ErrorMessage)); | ||||
|             SetRequestResultError(errorsString); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         SetRequestResultMessage("Wait..."); | ||||
|  | ||||
|         var request = Mapper.Map<RegisterApplicantRequest>(requestModel); | ||||
|         try | ||||
|         { | ||||
|             await Client.RegisterAsync(request); | ||||
|             SetRequestResultSuccess("Register successful. Now log in."); | ||||
|         } | ||||
|         catch (ApiException<ProblemDetails> e) | ||||
|         { | ||||
|             if (e.StatusCode == (int)HttpStatusCode.BadRequest | ||||
|                 && e.Result.AdditionalProperties.TryGetValue("errors", out var errors)) | ||||
|             { | ||||
|                 var errorsList = ((JArray)errors).ToObject<List<string>>(); | ||||
|                 if (errorsList is null) | ||||
|                 { | ||||
|                     throw new JsonException(); | ||||
|                 } | ||||
|  | ||||
|                 SetRequestResultError(ErrorsToString(errorsList)); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 SetRequestResultError(e.Result.Detail!); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,60 +0,0 @@ | ||||
| @page "/weather" | ||||
| @inject HttpClient Http | ||||
|  | ||||
| <PageTitle>Weather</PageTitle> | ||||
|  | ||||
| <h1>Weather</h1> | ||||
|  | ||||
| <p>This component demonstrates fetching data from the server.</p> | ||||
|  | ||||
| @if (forecasts == null) | ||||
| { | ||||
|     <p> | ||||
|         <em>Loading...</em> | ||||
|     </p> | ||||
| } | ||||
| else | ||||
| { | ||||
|     <table class="table"> | ||||
|         <thead> | ||||
|         <tr> | ||||
|             <th>Date</th> | ||||
|             <th>Temp. (C)</th> | ||||
|             <th>Temp. (F)</th> | ||||
|             <th>Summary</th> | ||||
|         </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|         @foreach (var forecast in forecasts) | ||||
|         { | ||||
|             <tr> | ||||
|                 <td>@forecast.Date.ToShortDateString()</td> | ||||
|                 <td>@forecast.TemperatureC</td> | ||||
|                 <td>@forecast.TemperatureF</td> | ||||
|                 <td>@forecast.Summary</td> | ||||
|             </tr> | ||||
|         } | ||||
|         </tbody> | ||||
|     </table> | ||||
| } | ||||
|  | ||||
| @code { | ||||
|     private WeatherForecast[]? forecasts; | ||||
|  | ||||
|     protected override async Task OnInitializedAsync() | ||||
|     { | ||||
|         forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json"); | ||||
|     } | ||||
|  | ||||
|     public class WeatherForecast | ||||
|     { | ||||
|         public DateOnly Date { get; set; } | ||||
|  | ||||
|         public int TemperatureC { get; set; } | ||||
|  | ||||
|         public string? Summary { get; set; } | ||||
|  | ||||
|         public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,3 +1,6 @@ | ||||
| using System.Reflection; | ||||
| using BlazorWebAssemblyVisaApiClient.Infrastructure.Services; | ||||
| using FluentValidation; | ||||
| using Microsoft.AspNetCore.Components.Web; | ||||
| using Microsoft.AspNetCore.Components.WebAssembly.Hosting; | ||||
| using VisaApiClient; | ||||
| @@ -17,8 +20,12 @@ public class Program | ||||
|  | ||||
|         //todo make pretty | ||||
|         builder.Services.AddScoped(_ => new HttpClient { BaseAddress = new Uri(baseAddress) }); | ||||
|         builder.Services.AddScoped<Client>(sp => | ||||
|             new Client(baseAddress, sp.GetRequiredService<HttpClient>())); | ||||
|         builder.Services.AddScoped<Client>(sp => new Client(baseAddress, sp.GetRequiredService<HttpClient>())); | ||||
|  | ||||
|         builder.Services.AddSingleton<IDateTimeProvider, DateTimeProvider>(); | ||||
|         builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); | ||||
|  | ||||
|         builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); | ||||
|  | ||||
|         await builder.Build().RunAsync(); | ||||
|     } | ||||
|   | ||||
| @@ -98,6 +98,37 @@ a, .btn-link { | ||||
|         content: var(--blazor-load-percentage-text, "Loading"); | ||||
|     } | ||||
|  | ||||
|     .fullscreen { | ||||
|         height: 100vh; | ||||
|     } | ||||
|  | ||||
|     .with-centered-content { | ||||
|         height: 100%; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|     } | ||||
|  | ||||
|     .with-centered-content > * { | ||||
|         margin: auto; | ||||
|     } | ||||
|  | ||||
|     .horizontal-centered-content > * { | ||||
|         margin-left: auto; | ||||
|         margin-right: auto; | ||||
|         text-align: center; | ||||
|     } | ||||
|  | ||||
|     .form { | ||||
|         width: fit-content; | ||||
|         text-align: left; | ||||
|     } | ||||
|  | ||||
|     .form-block { | ||||
|         margin: 10px 20px; | ||||
|         display: inline-block; | ||||
|         vertical-align: top; | ||||
|     } | ||||
|  | ||||
| code { | ||||
|     color: #c02d76; | ||||
| } | ||||
|   | ||||
| @@ -1,27 +0,0 @@ | ||||
| [ | ||||
|   { | ||||
|     "date": "2022-01-06", | ||||
|     "temperatureC": 1, | ||||
|     "summary": "Freezing" | ||||
|   }, | ||||
|   { | ||||
|     "date": "2022-01-07", | ||||
|     "temperatureC": 14, | ||||
|     "summary": "Bracing" | ||||
|   }, | ||||
|   { | ||||
|     "date": "2022-01-08", | ||||
|     "temperatureC": -13, | ||||
|     "summary": "Freezing" | ||||
|   }, | ||||
|   { | ||||
|     "date": "2022-01-09", | ||||
|     "temperatureC": -16, | ||||
|     "summary": "Balmy" | ||||
|   }, | ||||
|   { | ||||
|     "date": "2022-01-10", | ||||
|     "temperatureC": -2, | ||||
|     "summary": "Chilly" | ||||
|   } | ||||
| ] | ||||
		Reference in New Issue
	
	Block a user