commit ec80db6d09e39a0dd86dc41ba005c3c792a48172 Author: henrik Date: Wed Nov 13 17:37:32 2024 +0100 feat: Intiial working text share diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.Pushy/.idea/.gitignore b/.idea/.idea.Pushy/.idea/.gitignore new file mode 100644 index 0000000..5f5bfc6 --- /dev/null +++ b/.idea/.idea.Pushy/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.Pushy.iml +/modules.xml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.Pushy/.idea/encodings.xml b/.idea/.idea.Pushy/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.Pushy/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Pushy/.idea/indexLayout.xml b/.idea/.idea.Pushy/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.Pushy/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Pushy/.idea/vcs.xml b/.idea/.idea.Pushy/.idea/vcs.xml new file mode 100644 index 0000000..4c6280e --- /dev/null +++ b/.idea/.idea.Pushy/.idea/vcs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Pushy.sln b/Pushy.sln new file mode 100644 index 0000000..d241b50 --- /dev/null +++ b/Pushy.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pushy", "Pushy\Pushy\Pushy.csproj", "{1156C75D-F4F0-42CA-A007-724AC6CEC123}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pushy.Client", "Pushy\Pushy.Client\Pushy.Client.csproj", "{E7FD5748-F854-48A7-ABE0-4E5C66B3671D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1156C75D-F4F0-42CA-A007-724AC6CEC123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1156C75D-F4F0-42CA-A007-724AC6CEC123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1156C75D-F4F0-42CA-A007-724AC6CEC123}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1156C75D-F4F0-42CA-A007-724AC6CEC123}.Release|Any CPU.Build.0 = Release|Any CPU + {E7FD5748-F854-48A7-ABE0-4E5C66B3671D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7FD5748-F854-48A7-ABE0-4E5C66B3671D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7FD5748-F854-48A7-ABE0-4E5C66B3671D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7FD5748-F854-48A7-ABE0-4E5C66B3671D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Pushy.sln.DotSettings.user b/Pushy.sln.DotSettings.user new file mode 100644 index 0000000..1280799 --- /dev/null +++ b/Pushy.sln.DotSettings.user @@ -0,0 +1,2 @@ + + ForceIncluded \ No newline at end of file diff --git a/Pushy.slnx b/Pushy.slnx new file mode 100644 index 0000000..5162cfc --- /dev/null +++ b/Pushy.slnx @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Pushy/Pushy.Client/Program.cs b/Pushy/Pushy.Client/Program.cs new file mode 100644 index 0000000..91db88a --- /dev/null +++ b/Pushy/Pushy.Client/Program.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/Pushy/Pushy.Client/Pushy.Client.csproj b/Pushy/Pushy.Client/Pushy.Client.csproj new file mode 100644 index 0000000..99e0ba3 --- /dev/null +++ b/Pushy/Pushy.Client/Pushy.Client.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + true + Default + Linux + 13 + + + + + + + + + .dockerignore + + + + diff --git a/Pushy/Pushy.Client/_Imports.razor b/Pushy/Pushy.Client/_Imports.razor new file mode 100644 index 0000000..28e0312 --- /dev/null +++ b/Pushy/Pushy.Client/_Imports.razor @@ -0,0 +1,9 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Pushy.Client \ No newline at end of file diff --git a/Pushy/Pushy/Components/App.razor b/Pushy/Pushy/Components/App.razor new file mode 100644 index 0000000..81c66db --- /dev/null +++ b/Pushy/Pushy/Components/App.razor @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Pushy/Pushy/Components/Layout/MainLayout.razor b/Pushy/Pushy/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..a7ebcef --- /dev/null +++ b/Pushy/Pushy/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
+ An unhandled error has occurred. + Reload + 🗙 +
\ No newline at end of file diff --git a/Pushy/Pushy/Components/Layout/MainLayout.razor.css b/Pushy/Pushy/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..df8c10f --- /dev/null +++ b/Pushy/Pushy/Components/Layout/MainLayout.razor.css @@ -0,0 +1,18 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/Pushy/Pushy/Components/Pages/DisplayText.razor b/Pushy/Pushy/Components/Pages/DisplayText.razor new file mode 100644 index 0000000..23d4120 --- /dev/null +++ b/Pushy/Pushy/Components/Pages/DisplayText.razor @@ -0,0 +1,30 @@ +@page "/t/{id}" +@using Pushy.Grains + +@if (SharedText is null) +{ +

The shared text did not seem to exist

+} +else +{ + +

@SharedText

+
+} + +@code { + [Parameter] public string Id { get; set; } = null!; + + [Inject] + public IClusterClient ClusterClient { get; set; } = null!; + + public string? SharedText { get; set; } + + /// + protected override async Task OnInitializedAsync() + { + ITextItem item = ClusterClient.GetGrain(Id); + SharedText = await item.GetText(); + } + +} \ No newline at end of file diff --git a/Pushy/Pushy/Components/Pages/Error.razor b/Pushy/Pushy/Components/Pages/Error.razor new file mode 100644 index 0000000..9d7c6be --- /dev/null +++ b/Pushy/Pushy/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; + +} \ No newline at end of file diff --git a/Pushy/Pushy/Components/Pages/Home.razor b/Pushy/Pushy/Components/Pages/Home.razor new file mode 100644 index 0000000..7c2632f --- /dev/null +++ b/Pushy/Pushy/Components/Pages/Home.razor @@ -0,0 +1,51 @@ +@page "/" +@using Pushy.Grains +@inject NavigationManager Nav + +Share Something! + +

What would you like to share?

+ + + + + + +@if (CreatedItems.Count > 0) +{ + +} + +@code { + + [SupplyParameterFromForm] public UploadForm Form { get; set; } = new(); + + [Inject] public ILogger Logger { get; set; } = null!; + + [Inject] public LinkGenerator ItemGenerator { get; set; } = null!; + + [Inject] public IClusterClient ClusterClient { get; set; } = null!; + + public List CreatedItems { get; set; } = new(); + + private async Task SubmitInput() + { + ITextItem item = await ItemGenerator.GenerateTextShare(Form.InputText); + + CreatedItems.Add(item); + + Form = new UploadForm(); + } + + public sealed class UploadForm + { + public string InputText { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Pushy/Pushy/Components/Routes.razor b/Pushy/Pushy/Components/Routes.razor new file mode 100644 index 0000000..6db4332 --- /dev/null +++ b/Pushy/Pushy/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Pushy/Pushy/Components/_Imports.razor b/Pushy/Pushy/Components/_Imports.razor new file mode 100644 index 0000000..bad0b9f --- /dev/null +++ b/Pushy/Pushy/Components/_Imports.razor @@ -0,0 +1,11 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Pushy +@using Pushy.Client +@using Pushy.Components \ No newline at end of file diff --git a/Pushy/Pushy/Dockerfile b/Pushy/Pushy/Dockerfile new file mode 100644 index 0000000..b9190af --- /dev/null +++ b/Pushy/Pushy/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["Pushy/Pushy/Pushy.csproj", "Pushy/Pushy/"] +COPY ["Pushy/Pushy.Client/Pushy.Client.csproj", "Pushy/Pushy.Client/"] +RUN dotnet restore "Pushy/Pushy/Pushy.csproj" +COPY . . +WORKDIR "/src/Pushy/Pushy" +RUN dotnet build "Pushy.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "Pushy.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Pushy.dll"] diff --git a/Pushy/Pushy/Grains/TextItemGrain.cs b/Pushy/Pushy/Grains/TextItemGrain.cs new file mode 100644 index 0000000..9988f85 --- /dev/null +++ b/Pushy/Pushy/Grains/TextItemGrain.cs @@ -0,0 +1,73 @@ +using Orleans.Concurrency; + +namespace Pushy.Grains; + +public sealed class TextItem +{ + public string? Text { get; set; } +} + +public interface ITextItem : IGrainWithStringKey +{ + [Alias("IsAvailable")] + public ValueTask IsAvailable(); + + [Alias("SetText")] + public ValueTask SetText(string text); + + [ReadOnly] + [return: Immutable] + [AlwaysInterleave] + [Alias("GetText")] + ValueTask GetText(); +} + +public sealed class TextItemGrain : Grain, ITextItem, IRemindable +{ + private readonly IPersistentState _state; + private IGrainReminder _reminder; + + public TextItemGrain( + [PersistentState("text")] IPersistentState state) + { + _state = state; + } + + /// + public override async Task OnActivateAsync(CancellationToken cancellationToken) + { + _reminder = await this.RegisterOrUpdateReminder("clear", TimeSpan.FromDays(10), TimeSpan.FromDays(10)); + } + + /// + public ValueTask IsAvailable() + { + return ValueTask.FromResult(_state.State.Text is null); + } + + /// + public async ValueTask SetText(string text) + { + if(_state.State.Text is not null) + throw new InvalidOperationException("Text is already set"); + + _state.State.Text = text; + await _state.WriteStateAsync(); + } + + /// + public ValueTask GetText() + { + return ValueTask.FromResult(_state.State.Text!); + } + + /// + public async Task ReceiveReminder(string reminderName, TickStatus status) + { + if (reminderName == "clear") + { + _state.State.Text = null; + await _state.WriteStateAsync(); + } + } +} \ No newline at end of file diff --git a/Pushy/Pushy/LinkGenerator.cs b/Pushy/Pushy/LinkGenerator.cs new file mode 100644 index 0000000..600bc1e --- /dev/null +++ b/Pushy/Pushy/LinkGenerator.cs @@ -0,0 +1,32 @@ +using System.Security.Cryptography; +using System.Text; +using Pushy.Grains; + +namespace Pushy; + +public sealed class LinkGenerator +{ + private readonly ILogger _logger; + private readonly IClusterClient _clusterClient; + + public LinkGenerator( + IClusterClient clusterClient, + ILogger logger) + { + _clusterClient = clusterClient; + _logger = logger; + } + + public async Task GenerateTextShare(string text) + { + string item = $"{Guid.CreateVersion7():N}"[6..]; + var sharedGrain = _clusterClient.GetGrain(item); + if (await sharedGrain.IsAvailable()) + { + await sharedGrain.SetText(text); + return sharedGrain; + } + + return await GenerateTextShare(text); + } +} \ No newline at end of file diff --git a/Pushy/Pushy/Program.cs b/Pushy/Pushy/Program.cs new file mode 100644 index 0000000..4d186f4 --- /dev/null +++ b/Pushy/Pushy/Program.cs @@ -0,0 +1,54 @@ +using Pushy.Components; +using StackExchange.Redis; +using LinkGenerator = Pushy.LinkGenerator; + +var builder = WebApplication.CreateBuilder(args); + +builder.UseOrleans(silo => +{ + silo.UseLocalhostClustering(); + silo.UseRedisReminderService(conf => + { + conf.ConfigurationOptions = ConfigurationOptions.Parse(builder.Configuration.GetConnectionString("Valkey")); + }); + + silo.AddRedisGrainStorageAsDefault(options => + { + options.ConfigurationOptions = + ConfigurationOptions.Parse(builder.Configuration.GetConnectionString("Valkey")); + }); + + silo.AddActivityPropagation(); +}); + +builder.Services.AddScoped(); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents() + .AddInteractiveWebAssemblyComponents(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); +app.UseAntiforgery(); + +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Pushy.Client._Imports).Assembly); + +app.Run(); \ No newline at end of file diff --git a/Pushy/Pushy/Properties/launchSettings.json b/Pushy/Pushy/Properties/launchSettings.json new file mode 100644 index 0000000..7333118 --- /dev/null +++ b/Pushy/Pushy/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:12247", + "sslPort": 44346 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5106", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7105;http://localhost:5106", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/Pushy/Pushy/Pushy.csproj b/Pushy/Pushy/Pushy.csproj new file mode 100644 index 0000000..c277bb0 --- /dev/null +++ b/Pushy/Pushy/Pushy.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + Linux + 13 + + + + + + + + + + + + + + .dockerignore + + + + diff --git a/Pushy/Pushy/appsettings.Development.json b/Pushy/Pushy/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Pushy/Pushy/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Pushy/Pushy/appsettings.json b/Pushy/Pushy/appsettings.json new file mode 100644 index 0000000..9dc3af0 --- /dev/null +++ b/Pushy/Pushy/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "Valkey": "192.168.1.95:6379" + } +} diff --git a/Pushy/Pushy/wwwroot/app.css b/Pushy/Pushy/wwwroot/app.css new file mode 100644 index 0000000..e398853 --- /dev/null +++ b/Pushy/Pushy/wwwroot/app.css @@ -0,0 +1,29 @@ +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} diff --git a/global.json b/global.json new file mode 100644 index 0000000..93681ff --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file