diff --git a/BeauFindlay/BeauFindlay.sln b/BeauFindlay/BeauFindlay.sln index 0e7661a..ce28891 100644 --- a/BeauFindlay/BeauFindlay.sln +++ b/BeauFindlay/BeauFindlay.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BeauFindlay.Client", "src\B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeauFindlay.Api", "src\BeauFindlay.Api\BeauFindlay.Api.csproj", "{D2F248BF-8487-4CE7-B6AA-D558587A52DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeauFindlay.Shared", "src\BeauFindlay.Shared\BeauFindlay.Shared.csproj", "{0A17E6ED-1B40-4FAE-94D5-1255C3569F6E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,6 +25,10 @@ Global {D2F248BF-8487-4CE7-B6AA-D558587A52DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2F248BF-8487-4CE7-B6AA-D558587A52DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2F248BF-8487-4CE7-B6AA-D558587A52DA}.Release|Any CPU.Build.0 = Release|Any CPU + {0A17E6ED-1B40-4FAE-94D5-1255C3569F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A17E6ED-1B40-4FAE-94D5-1255C3569F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A17E6ED-1B40-4FAE-94D5-1255C3569F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A17E6ED-1B40-4FAE-94D5-1255C3569F6E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -30,6 +36,7 @@ Global GlobalSection(NestedProjects) = preSolution {979CCAA2-5F1B-457B-9536-02F94BC98F9F} = {3407557D-A21B-4F48-930C-6FDCE961ED2A} {D2F248BF-8487-4CE7-B6AA-D558587A52DA} = {3407557D-A21B-4F48-930C-6FDCE961ED2A} + {0A17E6ED-1B40-4FAE-94D5-1255C3569F6E} = {3407557D-A21B-4F48-930C-6FDCE961ED2A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6D05FF39-4D7B-4F0D-8136-E48F71106C3A} diff --git a/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/ICommand.cs b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/ICommand.cs new file mode 100644 index 0000000..044f012 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/ICommand.cs @@ -0,0 +1,16 @@ +using BeauFindlay.Shared.Abstractions; +using MediatR; + +namespace BeauFindlay.Api.Abstractions.Messaging; + +public interface ICommand : IRequest, IBaseCommand +{ +} + +public interface ICommand : IRequest>, IBaseCommand +{ +} + +public interface IBaseCommand +{ +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/ICommandHandler.cs b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/ICommandHandler.cs new file mode 100644 index 0000000..ac60f74 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/ICommandHandler.cs @@ -0,0 +1,14 @@ +using BeauFindlay.Shared.Abstractions; +using MediatR; + +namespace BeauFindlay.Api.Abstractions.Messaging; + +public interface ICommandHandler : IRequestHandler + where TCommand : ICommand +{ +} + +public interface ICommandHandler : IRequestHandler> + where TCommand : ICommand +{ +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IDomainEvent.cs b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IDomainEvent.cs new file mode 100644 index 0000000..e2dc074 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IDomainEvent.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace BeauFindlay.Api.Abstractions.Messaging; + +public interface IDomainEvent : INotification +{ +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IQuery.cs b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IQuery.cs new file mode 100644 index 0000000..5c15bbc --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IQuery.cs @@ -0,0 +1,8 @@ +using BeauFindlay.Shared.Abstractions; +using MediatR; + +namespace BeauFindlay.Api.Abstractions.Messaging; + +public interface IQuery : IRequest> +{ +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IQueryHandler.cs b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IQueryHandler.cs new file mode 100644 index 0000000..384b1bf --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Abstractions/Messaging/IQueryHandler.cs @@ -0,0 +1,9 @@ +using BeauFindlay.Shared.Abstractions; +using MediatR; + +namespace BeauFindlay.Api.Abstractions.Messaging; + +public interface IQueryHandler : IRequestHandler> + where TQuery : IQuery +{ +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/BeauFindlay.Api.csproj b/BeauFindlay/src/BeauFindlay.Api/BeauFindlay.Api.csproj index 9da3f88..c2f7ea6 100644 --- a/BeauFindlay/src/BeauFindlay.Api/BeauFindlay.Api.csproj +++ b/BeauFindlay/src/BeauFindlay.Api/BeauFindlay.Api.csproj @@ -7,22 +7,29 @@ enable + + + + PreserveNewest - PreserveNewest + Always Never - + + + + \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Extensions/ServiceCollectionExtensions.cs b/BeauFindlay/src/BeauFindlay.Api/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..0768bbc --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,45 @@ +using BeauFindlay.Api.Features.Contact; +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SendGrid.Extensions.DependencyInjection; + +namespace BeauFindlay.Api.Extensions; + +public static class ServiceCollectionExtensions +{ + public static void AddApplicationServices(this IServiceCollection services) + { + var assembly = typeof(ServiceCollectionExtensions).Assembly; + + services.AddMediatR(config => { config.RegisterServicesFromAssembly(assembly); }); + + services.AddValidatorsFromAssembly(assembly, includeInternalTypes: true); + + services.AddEmailService(); + + services.AddRecaptchaService(); + } + + private static void AddEmailService(this IServiceCollection services) + { + var apiKey = Environment.GetEnvironmentVariable("SendGridApiKey") + ?? throw new ArgumentException("SendGrid API key cannot be null"); + + services.AddSendGrid(config => config.ApiKey = apiKey); + + services.AddScoped(); + } + + private static void AddRecaptchaService(this IServiceCollection services) + { + var apiKey = Environment.GetEnvironmentVariable("RecaptchaApiKey") + ?? throw new ArgumentException("Google Recaptcha API key cannot be null"); + + var settings = new RecaptchaSettings(apiKey); + + services.AddSingleton(settings); + + services.AddHttpClient(); + } +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Features/Contact/IRecaptchaService.cs b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/IRecaptchaService.cs new file mode 100644 index 0000000..8f9cd12 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/IRecaptchaService.cs @@ -0,0 +1,9 @@ +using BeauFindlay.Shared.Abstractions; + +namespace BeauFindlay.Api.Features.Contact; + +internal interface IRecaptchaService +{ + Task ValidateResponseAsync(string recaptchaResponse, + CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Features/Contact/ISendGridService.cs b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/ISendGridService.cs new file mode 100644 index 0000000..b0f5b04 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/ISendGridService.cs @@ -0,0 +1,9 @@ +using BeauFindlay.Shared.Abstractions; + +namespace BeauFindlay.Api.Features.Contact; + +internal interface ISendGridService +{ + Task SendEmailAsync(string from, string to, string subject, string plainTextContent, + string htmlContent); +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Features/Contact/RecaptchaService.cs b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/RecaptchaService.cs new file mode 100644 index 0000000..990d748 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/RecaptchaService.cs @@ -0,0 +1,93 @@ +using BeauFindlay.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace BeauFindlay.Api.Features.Contact; + +internal sealed class RecaptchaService( + HttpClient httpClient, + ILogger logger, + RecaptchaSettings settings) + : IRecaptchaService +{ + public async Task ValidateResponseAsync(string recaptchaResponse, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(recaptchaResponse)) + { + logger.LogWarning("Recaptcha response is null."); + + return Result.Failure(RecaptchaErrors.ResponseNull); + } + + var response = await httpClient.PostAsync( + $"https://www.google.com/recaptcha/api/siteverify?secret={settings.ApiKey}&response={recaptchaResponse}", + null, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + logger.LogError( + "Google Recaptcha API validation request failed. Code: {StatusCode}", + response.StatusCode); + + return Result.Failure(RecaptchaErrors.ApiRequestFailed); + } + + var responseString = await response.Content.ReadAsStringAsync(cancellationToken); + var recaptchaResult = JsonConvert.DeserializeObject(responseString); + + if (recaptchaResult is null) + { + logger.LogError("Unable to deserialize Recaptcha result."); + + return Result.Failure(RecaptchaErrors.ResponseSerializationFailed); + } + + if (!recaptchaResult.Success) + { + logger.LogWarning( + "Google Recaptcha validation failed. Errors: {Errors}", + recaptchaResult.ErrorCodes); + + return Result.Failure(RecaptchaErrors.ValidationFailed); + } + + logger.LogInformation("Recaptcha validation passed."); + + return Result.Success(); + } + + private class RecaptchaVerificationResult + { + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("challenge_ts")] + public DateTime ChallengeTs { get; set; } + + [JsonProperty("hostname")] + public string Hostname { get; set; } = string.Empty; + + [JsonProperty("error-codes")] + public List ErrorCodes { get; set; } = []; + } +} + +public static class RecaptchaErrors +{ + public static readonly Error ResponseNull = new( + "Recaptcha.ResponseNull", + "Recaptcha response is null."); + + public static readonly Error ValidationFailed = new( + "Recaptcha.ValidationFailed", + "Recaptcha validation failed."); + + public static readonly Error ResponseSerializationFailed = new( + "Recaptcha.ResponseSerializationFailed", + "Unable to deserialize Recaptcha result."); + + public static readonly Error ApiRequestFailed = new( + "Recaptcha.ApiRequestFailed", + "Recaptcha API validation request failed."); +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Features/Contact/RecaptchaSettings.cs b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/RecaptchaSettings.cs new file mode 100644 index 0000000..5f9be6b --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/RecaptchaSettings.cs @@ -0,0 +1,3 @@ +namespace BeauFindlay.Api.Features.Contact; + +internal sealed record RecaptchaSettings(string ApiKey); \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendContactEmail.cs b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendContactEmail.cs new file mode 100644 index 0000000..aca6732 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendContactEmail.cs @@ -0,0 +1,71 @@ +using BeauFindlay.Api.Abstractions.Messaging; +using BeauFindlay.Shared.Abstractions; +using FluentValidation; +using Microsoft.Extensions.Logging; + +namespace BeauFindlay.Api.Features.Contact; + +public sealed record SendContactEmailCommand(string Name, string FromEmail, string Message, string RecaptchaResponse) + : ICommand; + +internal sealed class SendContactEmailCommandValidator : AbstractValidator +{ + public SendContactEmailCommandValidator(IRecaptchaService recaptchaService) + { + RuleFor(c => c.Name) + .NotEmpty() + .MaximumLength(50); + + RuleFor(c => c.FromEmail) + .NotEmpty() + .EmailAddress(); + + RuleFor(c => c.Message) + .NotEmpty() + .MaximumLength(500); + + RuleFor(c => c.RecaptchaResponse) + .NotEmpty() + .MustAsync(async (response, cancellation) => + { + var validationResult = await recaptchaService.ValidateResponseAsync(response, cancellation); + + return validationResult.IsSuccess; + }); + } +} + +internal sealed class SendContactEmailCommandHandler( + ISendGridService sendGridService, + IValidator validator, + ILogger logger) + : ICommandHandler +{ + private const string EmailSubjectBase = "New website enquiry"; + private const string MyEmail = "me@beaufindlay.com"; + + public async Task Handle(SendContactEmailCommand request, CancellationToken cancellationToken) + { + var validationResult = await validator.ValidateAsync(request, cancellationToken); + + if (!validationResult.IsValid) + { + logger.LogError("Command validation failed. Errors: {ValidationErrors}", validationResult.ToString()); + + return Result.Failure(new Error("ValidationFailed", "Command validation failed")); + } + + var subject = $"{EmailSubjectBase} - {request.FromEmail}"; + var message = $"From: {request.FromEmail}. Message: {request.Message}"; + var htmlMessage = $"

From: {request.FromEmail}

Message:
{request.Message}

"; + + var emailResult = await sendGridService.SendEmailAsync( + MyEmail, + MyEmail, + subject, + message, + htmlMessage); + + return emailResult.IsFailure ? Result.Failure(emailResult.Error) : Result.Success(); + } +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendContactEmailFunction.cs b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendContactEmailFunction.cs new file mode 100644 index 0000000..bf2a881 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendContactEmailFunction.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Net; +using System.Security.Cryptography; +using BeauFindlay.Shared.Contracts; +using MediatR; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace BeauFindlay.Api.Features.Contact; + +public class SendContactEmailFunction(ILoggerFactory loggerFactory, ISender sender) +{ + private readonly ILogger logger = loggerFactory.CreateLogger(); + + [Function(nameof(SendContactEmailFunction))] + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "post", Route = "send-contact-email")] HttpRequestData req, + FunctionContext executionContext, CancellationToken cancellationToken) + { + logger.LogInformation($"{nameof(SendContactEmailFunction)} function received a request."); + + var requestBody = await new StreamReader(req.Body).ReadToEndAsync(cancellationToken); + var request = JsonConvert.DeserializeObject(requestBody); + + HttpResponseData response; + + try + { + if (request == null) + { + throw new ApplicationException("Unable to deserialize response."); + } + + var emailCommand = new SendContactEmailCommand( + request.Name, + request.FromEmail, + request.Message, + request.RecaptchaResponse); + + var sendEmailResult = await sender.Send(emailCommand, cancellationToken); + + if (sendEmailResult.IsFailure) + { + logger.LogError("Send email command failed. Error: {Error}", sendEmailResult.Error.Message); + + response = req.CreateResponse(HttpStatusCode.BadRequest); + + var error = new ErrorResponse + { + Code = (int)HttpStatusCode.BadRequest, + Message = sendEmailResult.Error.Message + }; + + await response.WriteAsJsonAsync(error, cancellationToken); + } + else + { + response = req.CreateResponse(HttpStatusCode.OK); + + await response.WriteAsJsonAsync("", cancellationToken: cancellationToken); + } + } + catch (Exception e) + { + logger.LogError(e, "Exception occured. Error: '{Message}'", e.Message); + + response = req.CreateResponse(HttpStatusCode.InternalServerError); + + await response.WriteAsJsonAsync(ErrorResponse.Generic, cancellationToken); + } + + return response; + } +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendGridService.cs b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendGridService.cs new file mode 100644 index 0000000..ca493fc --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Api/Features/Contact/SendGridService.cs @@ -0,0 +1,38 @@ +using BeauFindlay.Shared.Abstractions; +using Microsoft.Extensions.Logging; +using SendGrid; +using SendGrid.Helpers.Mail; + +namespace BeauFindlay.Api.Features.Contact; + +internal sealed class SendGridService(ISendGridClient sendGridClient, ILogger logger) + : ISendGridService +{ + public async Task SendEmailAsync(string from, string to, string subject, string plainTextContent, + string htmlContent) + { + ArgumentException.ThrowIfNullOrWhiteSpace(from, nameof(from)); + ArgumentException.ThrowIfNullOrWhiteSpace(to, nameof(to)); + ArgumentException.ThrowIfNullOrWhiteSpace(subject, nameof(subject)); + ArgumentException.ThrowIfNullOrWhiteSpace(plainTextContent, nameof(plainTextContent)); + ArgumentException.ThrowIfNullOrWhiteSpace(htmlContent, nameof(htmlContent)); + + var fromEmail = new EmailAddress(from); + var toEmail = new EmailAddress(to); + + var message = MailHelper.CreateSingleEmail(fromEmail, toEmail, subject, plainTextContent, htmlContent); + + var response = await sendGridClient.SendEmailAsync(message); + + if (response is not { IsSuccessStatusCode: true }) + { + logger.LogError("Failed to send email. Status code: '{StatusCode}'", response?.StatusCode); + + return Result.Failure(new Error("Email.SendFailed", "Failed to send email.")); + } + + logger.LogInformation("Email sent successfully."); + + return Result.Success(); + } +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Api/Program.cs b/BeauFindlay/src/BeauFindlay.Api/Program.cs index 9de3b3e..3103e93 100644 --- a/BeauFindlay/src/BeauFindlay.Api/Program.cs +++ b/BeauFindlay/src/BeauFindlay.Api/Program.cs @@ -1,3 +1,4 @@ +using BeauFindlay.Api.Extensions; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -8,6 +9,7 @@ var host = new HostBuilder() { services.AddApplicationInsightsTelemetryWorkerService(); services.ConfigureFunctionsApplicationInsights(); + services.AddApplicationServices(); }) .Build(); diff --git a/BeauFindlay/src/BeauFindlay.Client/BeauFindlay.Client.csproj b/BeauFindlay/src/BeauFindlay.Client/BeauFindlay.Client.csproj index e8c6c7a..4d4cdc2 100644 --- a/BeauFindlay/src/BeauFindlay.Client/BeauFindlay.Client.csproj +++ b/BeauFindlay/src/BeauFindlay.Client/BeauFindlay.Client.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/BeauFindlay/src/BeauFindlay.Client/Components/Alert/Alert.razor b/BeauFindlay/src/BeauFindlay.Client/Components/Alert/Alert.razor new file mode 100644 index 0000000..bcc3a34 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Client/Components/Alert/Alert.razor @@ -0,0 +1,46 @@ +
+
+
+ @if (Type == AlertType.Success) + { + + } + else + { + + } + +
+
+

+ @Title +

+
+

@ChildContent

+
+
+
+
+ +@code { + + [Parameter] + public string Title { get; set; } = string.Empty; + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public AlertType Type { get; set; } + +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/Components/Alert/AlertType.cs b/BeauFindlay/src/BeauFindlay.Client/Components/Alert/AlertType.cs new file mode 100644 index 0000000..133534f --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Client/Components/Alert/AlertType.cs @@ -0,0 +1,7 @@ +namespace BeauFindlay.Client.Components.Alert; + +public enum AlertType +{ + Success, + Error +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/Components/Button/Button.razor b/BeauFindlay/src/BeauFindlay.Client/Components/Button/Button.razor new file mode 100644 index 0000000..f413027 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Client/Components/Button/Button.razor @@ -0,0 +1,24 @@ + + +@code { + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Type { get; set; } = "button"; + + [Parameter] + public bool IsLoading { get; set; } + +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/Components/LoadingSpinner/LoadingSpinner.razor b/BeauFindlay/src/BeauFindlay.Client/Components/LoadingSpinner/LoadingSpinner.razor new file mode 100644 index 0000000..deeae40 --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Client/Components/LoadingSpinner/LoadingSpinner.razor @@ -0,0 +1,39 @@ +
+ + + Loading... +
+ +@code { + + [Parameter] + public LoadingSpinnerSize Size { get; set; } + + private string sizeCss = "w-8 h-8"; + + protected override void OnParametersSet() + { + SetSpinnerSize(); + } + + private void SetSpinnerSize() + { + sizeCss = Size switch + { + LoadingSpinnerSize.Small => "w-6 h-6", + LoadingSpinnerSize.Medium => "w-8 h-8", + LoadingSpinnerSize.Large => "w-12 h-12", + _ => sizeCss + }; + } + +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/Components/LoadingSpinner/LoadingSpinnerSize.cs b/BeauFindlay/src/BeauFindlay.Client/Components/LoadingSpinner/LoadingSpinnerSize.cs new file mode 100644 index 0000000..360a53f --- /dev/null +++ b/BeauFindlay/src/BeauFindlay.Client/Components/LoadingSpinner/LoadingSpinnerSize.cs @@ -0,0 +1,8 @@ +namespace BeauFindlay.Client.Components.LoadingSpinner; + +public enum LoadingSpinnerSize +{ + Small, + Medium, + Large +} \ No newline at end of file diff --git a/BeauFindlay/src/BeauFindlay.Client/Layout/Footer.razor b/BeauFindlay/src/BeauFindlay.Client/Layout/Footer.razor index 906da2b..da00afd 100644 --- a/BeauFindlay/src/BeauFindlay.Client/Layout/Footer.razor +++ b/BeauFindlay/src/BeauFindlay.Client/Layout/Footer.razor @@ -1,5 +1,5 @@