From 5fdc89dbf305ad0aeb5978c4740224337c471020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Sun, 10 May 2026 09:28:40 +0200 Subject: [PATCH 01/11] feat(ui): implement hub navigation, profile dashboard and fix auth sync loop - Added MainHubLayout with glassmorphism sidebar - Implemented Profile dashboard with learn metrics - Added request deduplication and caching to IdentityService - Fixed infinite redirect loop on /profile page - Added dashboard navigation from reader - Closes #26, Closes #27 --- .../DTOs/User/UserProfileDto.cs | 1 + src/NexusReader.Maui/Main.razor | 4 +- .../Components/Atoms/NexusIcon.razor | 39 +- .../Molecules/IntelligenceToolbar.razor | 8 +- .../Layout/MainHubLayout.razor | 106 ++++ .../Layout/MainHubLayout.razor.css | 193 ++++++++ .../{MainLayout.razor => ReaderLayout.razor} | 46 +- ...ayout.razor.css => ReaderLayout.razor.css} | 0 .../Pages/Account/Login.razor | 15 +- .../Pages/Account/Profile.razor | 134 ++--- .../Pages/Account/Profile.razor.css | 459 +++++++++++------- .../Pages/Dashboard.razor | 112 +++++ .../Pages/Dashboard.razor.css | 358 ++++++++++++++ src/NexusReader.UI.Shared/Pages/Home.razor | 3 +- src/NexusReader.UI.Shared/Pages/Library.razor | 17 + .../Pages/NotFound.razor | 2 +- .../Pages/Settings.razor | 17 + src/NexusReader.UI.Shared/Routes.razor | 2 +- .../Services/IdentityService.cs | 142 ++++-- .../NexusAuthenticationStateProvider.cs | 62 ++- .../Handlers/AuthenticationHeaderHandler.cs | 4 + src/NexusReader.Web.New/Program.cs | 21 +- 22 files changed, 1402 insertions(+), 343 deletions(-) create mode 100644 src/NexusReader.UI.Shared/Layout/MainHubLayout.razor create mode 100644 src/NexusReader.UI.Shared/Layout/MainHubLayout.razor.css rename src/NexusReader.UI.Shared/Layout/{MainLayout.razor => ReaderLayout.razor} (80%) rename src/NexusReader.UI.Shared/Layout/{MainLayout.razor.css => ReaderLayout.razor.css} (100%) create mode 100644 src/NexusReader.UI.Shared/Pages/Dashboard.razor create mode 100644 src/NexusReader.UI.Shared/Pages/Dashboard.razor.css create mode 100644 src/NexusReader.UI.Shared/Pages/Library.razor create mode 100644 src/NexusReader.UI.Shared/Pages/Settings.razor diff --git a/src/NexusReader.Application/DTOs/User/UserProfileDto.cs b/src/NexusReader.Application/DTOs/User/UserProfileDto.cs index 8bbe4f2..3784299 100644 --- a/src/NexusReader.Application/DTOs/User/UserProfileDto.cs +++ b/src/NexusReader.Application/DTOs/User/UserProfileDto.cs @@ -4,6 +4,7 @@ public record UserProfileDto { public string Email { get; init; } = string.Empty; public int AITokensUsed { get; init; } + public Guid TenantId { get; init; } /// /// Relational data for the current subscription plan. diff --git a/src/NexusReader.Maui/Main.razor b/src/NexusReader.Maui/Main.razor index 89cac43..29c775f 100644 --- a/src/NexusReader.Maui/Main.razor +++ b/src/NexusReader.Maui/Main.razor @@ -3,7 +3,7 @@ - + @@ -11,7 +11,7 @@ - +

Sorry, there's nothing at this address.

diff --git a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor index 3d73d8f..5bc025a 100644 --- a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor +++ b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor @@ -1,6 +1,27 @@ @switch (Name.ToLowerInvariant()) { + case "home": + + + break; + case "map": + + + + break; + case "share-2": + + + + + + break; + case "help-circle": + + + + break; case "robot": break; @@ -16,8 +37,24 @@ case "message-square": break; + case "diamond": + + break; + case "layout": + + + + break; + case "book-open": + + + break; + case "user": + + + break; case "settings": - + break; case "bookmark": diff --git a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor index 9e4e831..6561265 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor @@ -7,8 +7,8 @@
- - +
+ + + + +
+
+ @Body +
+
+ + +@code { + [Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!; + [Inject] private IIdentityService IdentityService { get; set; } = default!; + [Inject] private NavigationManager NavigationManager { get; set; } = default!; + + private bool _isSyncing = false; + + protected override async Task OnInitializedAsync() + { + if (_isSyncing) return; + + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + if (!authState.User.Identity?.IsAuthenticated ?? true) + { + _isSyncing = true; + // Try to sync with server cookie + await IdentityService.GetProfileAsync(); + } + } + + private async Task HandleLogout() + { + await IdentityService.LogoutAsync(); + NavigationManager.NavigateTo("/", true); + } +} diff --git a/src/NexusReader.UI.Shared/Layout/MainHubLayout.razor.css b/src/NexusReader.UI.Shared/Layout/MainHubLayout.razor.css new file mode 100644 index 0000000..6a9b48c --- /dev/null +++ b/src/NexusReader.UI.Shared/Layout/MainHubLayout.razor.css @@ -0,0 +1,193 @@ +.hub-container { + display: flex; + width: 100vw; + height: 100vh; + background: #121212; + color: #e0e0e0; + overflow: hidden; +} + +::deep .hub-sidebar { + width: 260px; + height: 100%; + background: #161616; + border-right: 1px solid rgba(255, 255, 255, 0.05); + display: flex; + flex-direction: column; + z-index: 100; + flex-shrink: 0; +} + +::deep .sidebar-header { + padding: 2.5rem 1.5rem; +} + +::deep .logo { + display: flex; + align-items: center; + gap: 0.75rem; +} + +::deep .logo-icon { + color: var(--nexus-neon); + filter: drop-shadow(0 0 10px rgba(0, 255, 153, 0.4)); +} + +::deep .logo-text { + font-family: var(--nexus-font-serif); + font-size: 1.5rem; + font-weight: 700; + color: #ffffff; + letter-spacing: -0.01em; +} + +::deep .sidebar-nav { + flex: 1; + padding: 0; + display: flex; + flex-direction: column; +} + +::deep .nav-item { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem 1.5rem; + color: #A0A0A0; + text-decoration: none; + transition: all 0.2s ease; + border-left: 3px solid transparent; + font-family: var(--nexus-font-sans); + font-size: 0.9rem; + font-weight: 500; +} + +::deep .nav-item:hover { + background: rgba(255, 255, 255, 0.02); + color: #ffffff; +} + +::deep .nav-item.active { + color: #ffffff; + background: rgba(0, 255, 153, 0.03); + border-left: 3px solid var(--nexus-neon); +} + +::deep .nav-item.active .nav-icon { + color: var(--nexus-neon); +} + +::deep .nav-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + opacity: 0.7; + transition: opacity 0.2s; +} + +::deep .nav-item:hover .nav-icon, +::deep .nav-item.active .nav-icon { + opacity: 1; +} + +::deep .sidebar-footer { + padding: 1.25rem 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; + justify-content: space-between; +} + +::deep .user-brief { + display: flex; + align-items: center; + gap: 0.75rem; + overflow: hidden; +} + +::deep .user-avatar { + width: 32px; + height: 32px; + background: #222; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: 600; + color: #A0A0A0; + flex-shrink: 0; +} + +::deep .user-details { + display: flex; + flex-direction: column; + overflow: hidden; +} + +::deep .user-name { + font-size: 0.85rem; + font-weight: 500; + color: #A0A0A0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +::deep .logout-btn { + background: transparent; + border: none; + color: #666; + cursor: pointer; + padding: 0.4rem; + border-radius: 6px; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +::deep .logout-btn:hover { + background: rgba(255, 255, 255, 0.05); + color: #ffffff; +} + +.hub-main { + flex: 1; + height: 100%; + overflow-y: auto; + background: radial-gradient(circle at center, #1a1a1a 0%, #121212 100%); +} + +.hub-content { + padding: 2.5rem; + min-height: 100%; +} + +::deep .hub-loading { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.5rem; +} + +::deep .nexus-loader { + width: 32px; + height: 32px; + border: 2px solid rgba(0, 255, 153, 0.1); + border-top-color: var(--nexus-neon); + border-radius: 50%; + animation: spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite; + filter: drop-shadow(0 0 5px var(--nexus-neon)); +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + + diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor similarity index 80% rename from src/NexusReader.UI.Shared/Layout/MainLayout.razor rename to src/NexusReader.UI.Shared/Layout/ReaderLayout.razor index 5b3bd3a..0c2f55c 100644 --- a/src/NexusReader.UI.Shared/Layout/MainLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor @@ -10,19 +10,23 @@ @inject IJSRuntime JS @inject IIdentityService IdentityService @inject NavigationManager NavigationManager -@inject Microsoft.Extensions.Logging.ILogger Logger +@inject Microsoft.Extensions.Logging.ILogger Logger @implements IDisposable - - -
-
-
- @Body -
+
+
+
+ @Body +
+ + -
+ + +
+ +
@@ -34,9 +38,6 @@ Class="@($"neon-glow {(QuizService.HasNewQuiz ? "quiz-available" : "")}")" /> Asystent AI
- - -
@@ -49,18 +50,15 @@
- -
- -
-
-
Weryfikacja...
-
-
- - @Body - -
+ + +
+
+
Weryfikacja...
+
+
+ +
An unhandled error has occurred. diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css similarity index 100% rename from src/NexusReader.UI.Shared/Layout/MainLayout.razor.css rename to src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css diff --git a/src/NexusReader.UI.Shared/Pages/Account/Login.razor b/src/NexusReader.UI.Shared/Pages/Account/Login.razor index 5d54751..60ea128 100644 --- a/src/NexusReader.UI.Shared/Pages/Account/Login.razor +++ b/src/NexusReader.UI.Shared/Pages/Account/Login.razor @@ -9,7 +9,7 @@