diff --git a/samples/MartenIdentity.Aspire.Web/Components/App.razor b/samples/MartenIdentity.Aspire.Web/Components/App.razor index e7d47f3..4899015 100644 --- a/samples/MartenIdentity.Aspire.Web/Components/App.razor +++ b/samples/MartenIdentity.Aspire.Web/Components/App.razor @@ -6,14 +6,30 @@ + + + + + + - + + + + + - + @* Global interactive server render mode. The RCL's Login/LoginForm pages don't + self-declare a render mode but require interactivity (the SignInHandoff cookie + handoff runs in OnAfterRenderAsync, and Radzen inputs like "Remember me" only + bind interactively). Pages in the RCL that redeclare @rendermode InteractiveServer + coexist fine with this global mode. *@ +
An unhandled error has occurred. diff --git a/samples/MartenIdentity.Aspire.Web/Components/Layout/MainLayout.razor b/samples/MartenIdentity.Aspire.Web/Components/Layout/MainLayout.razor index 76059b4..d34bec7 100644 --- a/samples/MartenIdentity.Aspire.Web/Components/Layout/MainLayout.razor +++ b/samples/MartenIdentity.Aspire.Web/Components/Layout/MainLayout.razor @@ -1,5 +1,8 @@ @inherits LayoutComponentBase +@inject IConfiguration Configuration +@inject NavigationManager NavigationManager + @* Radzen overlay hosts. They live in an interactive island so DialogService / NotificationService calls from the (also interactive) admin & manage pages work. *@ @@ -7,43 +10,161 @@ - - - - -
- - - - - - - - - @context.User.Identity?.Name - -
- - - - -
- - - - -
-
-
- -
+
+
+ + +
+
+
+ + @Breadcrumb() +
+
+ + + +
+ @Initials(context.User.Identity?.Name) + @context.User.Identity?.Name +
+
+ + + + +
+ + + + +
+
+
+ +
@Body
- - +
+
+ +@code { + // Configured application name (falls back to a sensible default). + private string AppName => Configuration["AppName"] ?? "Marten Identity"; + + private string AppInitial => string.IsNullOrEmpty(AppName) ? "?" : AppName[..1].ToUpperInvariant(); + + // Section / page breadcrumb shown in the topbar, derived from the route. + private static readonly Dictionary Breadcrumbs = new(StringComparer.OrdinalIgnoreCase) + { + [""] = "Home", + ["account/manage/profile"] = "Account / Profile", + ["account/manage/email"] = "Account / Email", + ["account/manage/changepassword"] = "Account / Password", + ["account/manage/setpassword"] = "Account / Password", + ["account/manage/twofactorauthentication"] = "Account / Two-factor authentication", + ["account/manage/enableauthenticator"] = "Account / Two-factor authentication", + ["account/manage/generaterecoverycodes"] = "Account / Two-factor authentication", + ["account/manage/disable2fa"] = "Account / Two-factor authentication", + ["account/manage/passkeys"] = "Account / Passkeys", + ["account/manage/personaldata"] = "Account / Personal data", + ["account/manage/deletepersonaldata"] = "Account / Personal data", + ["administration/users"] = "Administration / Users", + ["administration/roles"] = "Administration / Roles", + ["administration/usercleanup"] = "Administration / User cleanup", + }; + + private string Breadcrumb() + { + var path = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + var query = path.IndexOf('?'); + if (query >= 0) + { + path = path[..query]; + } + path = path.TrimEnd('/'); + + if (Breadcrumbs.TryGetValue(path, out var crumb)) + { + return crumb; + } + + var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); + if (segments.Length == 0) + { + return "Home"; + } + + var page = Humanize(segments[^1]); + return segments[0].ToLowerInvariant() switch + { + "account" => $"Account / {page}", + "administration" => $"Administration / {page}", + _ => page, + }; + } + + // "PersonalData" -> "Personal data" for routes not in the lookup. + private static string Humanize(string segment) + { + var spaced = System.Text.RegularExpressions.Regex.Replace(segment, "(?<=[a-z0-9])(?=[A-Z])", " "); + return spaced.Length == 0 ? spaced : char.ToUpperInvariant(spaced[0]) + spaced[1..].ToLowerInvariant(); + } + + // Two-letter avatar initials derived from the account name / email local part. + private static string Initials(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return "?"; + } + + var at = name.IndexOf('@'); + var local = at > 0 ? name[..at] : name; + var parts = local.Split(['.', '-', '_', ' '], StringSplitOptions.RemoveEmptyEntries); + + return parts.Length switch + { + 0 => "?", + 1 => parts[0][..Math.Min(2, parts[0].Length)].ToUpperInvariant(), + _ => $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant(), + }; + } +} diff --git a/samples/MartenIdentity.Aspire.Web/Components/Layout/ThemeToggle.razor b/samples/MartenIdentity.Aspire.Web/Components/Layout/ThemeToggle.razor new file mode 100644 index 0000000..61d6545 --- /dev/null +++ b/samples/MartenIdentity.Aspire.Web/Components/Layout/ThemeToggle.razor @@ -0,0 +1,62 @@ +@* Light / auto / dark theme switch. Interactive so button clicks reach the + browser-side appFoundationTheme helper that owns data-theme + localStorage. *@ +@rendermode InteractiveServer +@using Microsoft.JSInterop +@inject IJSRuntime JS + +
+ @foreach (var mode in Modes) + { + + } +
+ +@code { + private string _current = "auto"; + + private sealed record Mode(string Key, string Svg, string Title); + + private const string Sun = + ""; + + private const string Monitor = + ""; + + private const string Moon = + ""; + + private static readonly Mode[] Modes = + [ + new("light", Sun, "Light"), + new("auto", Monitor, "Match system"), + new("dark", Moon, "Dark"), + ]; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + { + return; + } + + try + { + _current = await JS.InvokeAsync("appFoundationTheme.get"); + StateHasChanged(); + } + catch (JSException) + { + // Script not ready yet (e.g. during prerender) — keep the default. + } + } + + private async Task SetAsync(string mode) => + _current = await JS.InvokeAsync("appFoundationTheme.set", mode); +} diff --git a/samples/MartenIdentity.Aspire.Web/Components/Pages/Home.razor b/samples/MartenIdentity.Aspire.Web/Components/Pages/Home.razor index a6e4a5b..bbc0c41 100644 --- a/samples/MartenIdentity.Aspire.Web/Components/Pages/Home.razor +++ b/samples/MartenIdentity.Aspire.Web/Components/Pages/Home.razor @@ -1,4 +1,7 @@ @page "/" +@* The library's cookie-login middleware redirects here (DefaultRedirect = "/dashboard") + after a successful sign-in when no explicit ReturnUrl was supplied. *@ +@page "/dashboard" Marten Identity Sample diff --git a/samples/MartenIdentity.Aspire.Web/Program.cs b/samples/MartenIdentity.Aspire.Web/Program.cs index e421e75..39c19f9 100644 --- a/samples/MartenIdentity.Aspire.Web/Program.cs +++ b/samples/MartenIdentity.Aspire.Web/Program.cs @@ -1,6 +1,7 @@ using AndreGoepel.Marten.Identity; using AndreGoepel.Marten.Identity.Blazor; using AndreGoepel.Marten.Identity.Blazor.Components.Account; +using AndreGoepel.Marten.Identity.Blazor.Features; using AndreGoepel.Marten.Identity.Users; using JasperFx; using Marten; diff --git a/samples/MartenIdentity.Aspire.Web/appsettings.json b/samples/MartenIdentity.Aspire.Web/appsettings.json index 10f68b8..0d2e837 100644 --- a/samples/MartenIdentity.Aspire.Web/appsettings.json +++ b/samples/MartenIdentity.Aspire.Web/appsettings.json @@ -1,4 +1,5 @@ { + "AppName": "Marten Identity", "Logging": { "LogLevel": { "Default": "Information", diff --git a/samples/MartenIdentity.Aspire.Web/wwwroot/app.css b/samples/MartenIdentity.Aspire.Web/wwwroot/app.css index ee651a0..3777f54 100644 --- a/samples/MartenIdentity.Aspire.Web/wwwroot/app.css +++ b/samples/MartenIdentity.Aspire.Web/wwwroot/app.css @@ -1,15 +1,6 @@ html, body { margin: 0; - font-family: var(--rz-text-font-family, "Roboto", sans-serif); -} - -.rz-header { - background: var(--rz-primary, #3f51b5); -} - -.rz-header a, -.rz-header .rz-text { - color: #fff; + font-family: var(--rz-text-font-family, "Manrope", sans-serif); } #blazor-error-ui { diff --git a/src/AndreGoepel.Marten.Identity.Blazor/Components/Account/Pages/ForgotPassword.razor b/src/AndreGoepel.Marten.Identity.Blazor/Components/Account/Pages/ForgotPassword.razor index eb553f2..a8105c6 100644 --- a/src/AndreGoepel.Marten.Identity.Blazor/Components/Account/Pages/ForgotPassword.razor +++ b/src/AndreGoepel.Marten.Identity.Blazor/Components/Account/Pages/ForgotPassword.razor @@ -11,27 +11,26 @@ -We'll send a password reset link. - -Forgot your password? + - - - - - - - + + + + + -