diff --git a/ChessCubing.App/Components/SiteMenu.razor b/ChessCubing.App/Components/SiteMenu.razor
index 90dc54b..d7141be 100644
--- a/ChessCubing.App/Components/SiteMenu.razor
+++ b/ChessCubing.App/Components/SiteMenu.razor
@@ -1,5 +1,7 @@
@using System.Security.Claims
+@implements IAsyncDisposable
@inject NavigationManager Navigation
+@inject IJSRuntime JS
+
+
+
+
+
+
Compte Keycloak
+
@AuthModalTitle
+
+
+
+
+ L'authentification se fait maintenant dans cette fenetre integree, sans quitter la page en cours.
+
+ @if (!string.IsNullOrWhiteSpace(AuthModalSource))
+ {
+
+
+
+ }
+
+
+
@code {
private static readonly string[] HomePaths = ["", "index.html"];
private static readonly string[] ApplicationPaths = ["application", "application.html"];
private static readonly string[] RulesPaths = ["reglement", "reglement.html"];
+ private DotNetObjectReference? _dotNetReference;
+ private bool _listenerRegistered;
+ private string? AuthModalSource;
+ private string AuthModalTitle = "Authentification";
+ private bool ShowAuthModal;
+
private string LoginHref => BuildAuthHref("login", EffectiveReturnUrl);
private string RegisterHref => BuildAuthHref("register", EffectiveReturnUrl);
private string LogoutHref => BuildAuthHref("logout", "/");
@@ -109,4 +139,69 @@
private static string BuildMeta(ClaimsPrincipal user)
=> user.FindFirst("email")?.Value
?? "Session active";
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!firstRender)
+ {
+ return;
+ }
+
+ _dotNetReference = DotNetObjectReference.Create(this);
+ await JS.InvokeVoidAsync("chesscubingAuthModal.registerListener", _dotNetReference);
+ _listenerRegistered = true;
+ }
+
+ private void OpenLoginModal()
+ => OpenAuthModal("Se connecter", LoginHref);
+
+ private void OpenRegisterModal()
+ => OpenAuthModal("Creer un compte", RegisterHref);
+
+ private void OpenAuthModal(string title, string source)
+ {
+ AuthModalTitle = title;
+ AuthModalSource = source;
+ ShowAuthModal = true;
+ }
+
+ private void CloseAuthModal()
+ {
+ ShowAuthModal = false;
+ AuthModalSource = null;
+ }
+
+ [JSInvokable]
+ public Task HandleAuthModalMessage(string status)
+ {
+ if (!string.Equals(status, "login-succeeded", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(status, "logout-succeeded", StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.CompletedTask;
+ }
+
+ CloseAuthModal();
+ StateHasChanged();
+ Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
+ return Task.CompletedTask;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_listenerRegistered)
+ {
+ try
+ {
+ await JS.InvokeVoidAsync("chesscubingAuthModal.unregisterListener");
+ }
+ catch
+ {
+ }
+ }
+
+ _dotNetReference?.Dispose();
+ }
+
+ private static string BoolString(bool value)
+ => value ? "true" : "false";
}
diff --git a/ChessCubing.App/Layout/MainLayout.razor b/ChessCubing.App/Layout/MainLayout.razor
index 200d7c3..03d437f 100644
--- a/ChessCubing.App/Layout/MainLayout.razor
+++ b/ChessCubing.App/Layout/MainLayout.razor
@@ -18,7 +18,8 @@
return string.Equals(currentPath, "chrono", StringComparison.OrdinalIgnoreCase)
|| string.Equals(currentPath, "chrono.html", StringComparison.OrdinalIgnoreCase)
|| string.Equals(currentPath, "cube", StringComparison.OrdinalIgnoreCase)
- || string.Equals(currentPath, "cube.html", StringComparison.OrdinalIgnoreCase);
+ || string.Equals(currentPath, "cube.html", StringComparison.OrdinalIgnoreCase)
+ || currentPath.StartsWith("authentication/", StringComparison.OrdinalIgnoreCase);
}
}
}
diff --git a/ChessCubing.App/Pages/Authentication.razor b/ChessCubing.App/Pages/Authentication.razor
index 978ce4a..1422a05 100644
--- a/ChessCubing.App/Pages/Authentication.razor
+++ b/ChessCubing.App/Pages/Authentication.razor
@@ -1,5 +1,6 @@
@page "/authentication/{action}"
@inject NavigationManager Navigation
+@inject IJSRuntime JS
@@ -45,6 +46,29 @@
Navigation.NavigateToLogin("authentication/login", request);
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!firstRender)
+ {
+ return;
+ }
+
+ var status = Action switch
+ {
+ RemoteAuthenticationActions.LogInCallback => "login-succeeded",
+ RemoteAuthenticationActions.LogOutCallback => "logout-succeeded",
+ _ => null,
+ };
+
+ if (status is null)
+ {
+ return;
+ }
+
+ await Task.Delay(700);
+ await JS.InvokeVoidAsync("chesscubingAuthModal.notifyParent", status);
+ }
+
private static string NormalizeReturnUrl(string? returnUrl)
{
if (string.IsNullOrWhiteSpace(returnUrl))
diff --git a/ChessCubing.App/wwwroot/js/chesscubing-interop.js b/ChessCubing.App/wwwroot/js/chesscubing-interop.js
index bb0abb6..0fceb13 100644
--- a/ChessCubing.App/wwwroot/js/chesscubing-interop.js
+++ b/ChessCubing.App/wwwroot/js/chesscubing-interop.js
@@ -2,6 +2,7 @@
const assetTokenStorageKey = "chesscubing-arena-asset-token";
let viewportStarted = false;
let audioContext = null;
+ let authModalMessageHandler = null;
function syncViewportHeight() {
const visibleHeight = window.visualViewport?.height ?? window.innerHeight;
@@ -198,5 +199,51 @@
},
};
+ window.chesscubingAuthModal = {
+ registerListener(dotNetReference) {
+ if (authModalMessageHandler) {
+ window.removeEventListener("message", authModalMessageHandler);
+ }
+
+ authModalMessageHandler = (event) => {
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+
+ const data = event.data;
+ if (!data || data.source !== "chesscubing-auth-modal" || typeof data.status !== "string") {
+ return;
+ }
+
+ dotNetReference.invokeMethodAsync("HandleAuthModalMessage", data.status).catch(() => undefined);
+ };
+
+ window.addEventListener("message", authModalMessageHandler);
+ },
+
+ unregisterListener() {
+ if (!authModalMessageHandler) {
+ return;
+ }
+
+ window.removeEventListener("message", authModalMessageHandler);
+ authModalMessageHandler = null;
+ },
+
+ notifyParent(status) {
+ if (!window.parent || window.parent === window) {
+ return;
+ }
+
+ window.parent.postMessage(
+ {
+ source: "chesscubing-auth-modal",
+ status,
+ },
+ window.location.origin,
+ );
+ },
+ };
+
startViewport();
})();
diff --git a/README.md b/README.md
index 9ef7c99..d6d84a8 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ Le projet continue a exposer les routes historiques `index.html`, `application.h
L'application embarque maintenant une authentification OpenID Connect basee sur Keycloak.
- toutes les pages du site restent accessibles sans connexion
-- un menu general en haut des pages site et application regroupe la navigation et les actions `Se connecter` / `Creer un compte`
+- un menu general en haut des pages site et application regroupe la navigation et les actions `Se connecter` / `Creer un compte` dans une modal integree
- l'action `Creer un compte` ouvre le parcours d'inscription natif de Keycloak
- les roles Keycloak du realm sont exposes dans l'application
- l'etat du match est isole par utilisateur dans le navigateur grace a une cle de stockage derivee du compte connecte
diff --git a/styles.css b/styles.css
index 5ac3534..7bf5d39 100644
--- a/styles.css
+++ b/styles.css
@@ -1152,6 +1152,34 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after {
font-size: 0.9rem;
}
+.auth-modal-card {
+ width: min(980px, 100%);
+ padding: 1.1rem;
+ border-radius: 28px;
+ border: 1px solid var(--panel-border);
+ background: rgba(14, 16, 21, 0.96);
+}
+
+.auth-modal-copy {
+ margin-bottom: 1rem;
+ color: var(--muted);
+}
+
+.auth-modal-frame-shell {
+ overflow: hidden;
+ border-radius: 22px;
+ border: 1px solid var(--panel-border);
+ background: rgba(10, 12, 17, 0.85);
+}
+
+.auth-modal-frame {
+ display: block;
+ width: 100%;
+ height: min(78dvh, 760px);
+ border: 0;
+ background: #141414;
+}
+
.hero-rules h1 {
margin: 0;
font-size: clamp(2.25rem, 4.8vw, 3.8rem);
@@ -1493,6 +1521,14 @@ body[data-page="cube"] .zone-button.cube-hold-ready::after {
text-align: left;
}
+ .auth-modal-card {
+ padding: 1rem;
+ }
+
+ .auth-modal-frame {
+ height: min(70dvh, 680px);
+ }
+
.setup-form {
grid-template-columns: 1fr;
}