feat(ui): Hub Navigation, Profile Dashboard and Auth Stability Fixes (#31)

This PR implements the Hub Navigation system and the Profile Dashboard, while resolving critical session synchronization issues.

### Key Changes
- **Hub Navigation**: Introduced `MainHubLayout` with a premium glassmorphism sidebar, providing access to Dashboard, Library, Concepts Map, and Profile.
- **Profile Dashboard**: Implemented a high-fidelity Profile page (#27) with learning metrics, AI token usage tracking, and system rank visualization.
- **Stability Fixes**:
    - Resolved an infinite network loop on the `/profile` page by implementing request deduplication and in-memory caching in `IdentityService`.
    - Added environment-aware guards to prevent illegal JavaScript interop calls during server-side prerendering.
    - Implemented automatic session invalidation on `401 Unauthorized` responses to handle stale authentication states gracefully.
- **Reader Integration**: Added a "Return to Dashboard" option in the reader toolbar (#26).

Closes #26
Closes #27

Reviewed-on: #31
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #31.
This commit is contained in:
2026-05-10 17:36:35 +00:00
committed by Marek Jaisński
parent 34794db209
commit 2e23a032d3
56 changed files with 4292 additions and 481 deletions
@@ -0,0 +1,116 @@
@inherits LayoutComponentBase
@using NexusReader.Application.Abstractions.Services
@using NexusReader.UI.Shared.Services
@using NexusReader.UI.Shared.Components.Molecules
@using NexusReader.UI.Shared.Components.Organisms
@using Microsoft.Extensions.Logging
@inject IPlatformService PlatformService
@inject IFocusModeService FocusMode
@inject IQuizStateService QuizService
@inject IJSRuntime JS
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
@implements IDisposable
<div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "")">
<div class="reader-pane">
<main>
@Body
</main>
<AuthorizeView>
<Authorized>
<ReaderFooter />
</Authorized>
</AuthorizeView>
</div>
<AuthorizeView>
<Authorized>
<div class="resizer" id="sidebar-resizer"></div>
<div class="intelligence-sidebar">
<IntelligenceToolbar />
<div class="intelligence-content">
<div class="intelligence-header">
<div class="ai-title">
<NexusIcon Name="robot" Size="20"
Class="@($"neon-glow {(QuizService.HasNewQuiz ? "quiz-available" : "")}")" />
<span>Asystent AI</span>
</div>
<button class="close-btn">×</button>
</div>
<div class="intelligence-scroll-area">
@if (!_isMobile)
{
<KnowledgeGraph />
}
<KnowledgeCheck />
</div>
</div>
</div>
</Authorized>
<Authorizing>
<div class="app-preloader">
<div class="preloader-spinner"></div>
<div class="preloader-text">Weryfikacja...</div>
</div>
</Authorizing>
</AuthorizeView>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
private string _platformClass = "platform-desktop";
private bool _isMobile = false;
protected override void OnInitialized()
{
FocusMode.OnFocusModeChanged += HandleUpdate;
QuizService.OnQuizUpdated += HandleUpdate;
var context = PlatformService.GetDeviceContext();
if (context.IsSuccess)
{
_isMobile = context.Value.DeviceType switch
{
DeviceType.Phone or DeviceType.Tablet => true,
_ => false
};
_platformClass = _isMobile ? "platform-mobile" : "platform-desktop";
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/layoutResizer.js");
await module.InvokeVoidAsync("initResizer", ".app-container", "#sidebar-resizer", "--sidebar-width");
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to initialize layout resizer JS module.");
}
}
}
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
public void Dispose()
{
FocusMode.OnFocusModeChanged -= HandleUpdate;
QuizService.OnQuizUpdated -= HandleUpdate;
}
}