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,147 @@
@page "/"
@using Microsoft.AspNetCore.Authorization
@using NexusReader.UI.Shared.Components.Atoms
@using NexusReader.UI.Shared.Services
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@attribute [Authorize]
<PageTitle>Dashboard | Nexus Reader</PageTitle>
<div class="dashboard-container">
<!-- Top Profile Section -->
<header class="profile-header">
<div class="header-grid-bg"></div>
<div class="profile-visual">
<div class="avatar-wrapper">
<img src="https://api.dicebear.com/7.x/bottts/svg?seed=Nexus" alt="Profile" class="profile-img" />
<div class="avatar-glow"></div>
</div>
<h1 class="username">[User_Explorer1988]</h1>
<div class="status-pills">
<div class="status-pill">
<span class="pill-label">Books Read:</span>
<span class="pill-value">12</span>
</div>
<div class="status-pill">
<span class="pill-label">Concepts Mapped:</span>
<span class="pill-value">450</span>
</div>
<div class="status-pill">
<span class="pill-label">Quiz Mastery:</span>
<span class="pill-value">88%</span>
</div>
</div>
</div>
</header>
<!-- Main Content Area -->
<main class="dashboard-content">
<h2 class="section-title">Witaj, @(_profile?.Email.Split('@')[0] ?? "Użytkowniku")</h2>
<div class="main-grid">
<!-- Current Reading Card -->
<section class="reading-card glass-panel">
@if (_profile?.LastReadBook != null)
{
<div class="card-header">
<h3>Ostatnio czytane: @_profile.LastReadBook.Title</h3>
</div>
<div class="card-body">
<div class="reading-thumb">
<img src="@(_profile.LastReadBook.CoverUrl ?? "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/402px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg")" alt="Current Book" />
</div>
<div class="reading-info">
<div class="progress-section">
<span class="chapter-label">@_profile.LastReadBook.LastChapter</span>
<div class="progress-container">
<div class="progress-bar" style="width: @(_profile.LastReadBook.Progress.ToString("F0", System.Globalization.CultureInfo.InvariantCulture))%">
<div class="progress-bubble">@(_profile.LastReadBook.Progress.ToString("F1"))%</div>
</div>
</div>
<span class="progress-detail">Postęp: @(_profile.LastReadBook.Progress.ToString("F2"))% - @_profile.LastReadBook.Author.Name</span>
</div>
<p class="reading-desc">
Kontynuuj odkrywanie wiedzy w książce "@_profile.LastReadBook.Title".
Twój cyfrowy asystent Nexus jest gotowy do analizy kolejnych rozdziałów i generowania interaktywnych map myśli.
</p>
<div class="card-actions">
<button class="btn-nexus primary" @onclick='() => NavigationManager.NavigateTo($"/reader?chapter={_profile.LastReadBook.LastChapterIndex}")'>Kontynuuj Czytanie</button>
<button class="btn-nexus secondary" @onclick='() => NavigationManager.NavigateTo("/library")'>Moja Biblioteka</button>
</div>
</div>
</div>
}
else
{
<div class="card-header">
<h3>Brak aktywnych lektur</h3>
</div>
<div class="card-body empty-state">
<div class="empty-icon">
<NexusIcon Name="book-open" Size="64" />
</div>
<div class="reading-info">
<p class="reading-desc">
Nie czytasz obecnie żadnej książki. Przejdź do biblioteki, aby przesłać swój pierwszy plik EPUB i rozpocząć przygodę z Nexus Reader.
</p>
<div class="card-actions">
<button class="btn-nexus primary" @onclick='() => NavigationManager.NavigateTo("/library")'>Przejdź do Biblioteki</button>
</div>
</div>
</div>
}
</section>
<div class="secondary-grid">
<!-- Knowledge Integration -->
<section class="integration-card glass-panel">
<div class="panel-header">
<h4>Knowledge Integration Progress</h4>
<NexusIcon Name="arrow-right" Size="16" />
</div>
<div class="graph-placeholder">
<div class="graph-node central"></div>
<div class="graph-node satellite" style="--angle: 0deg; --dist: 60px;"></div>
<div class="graph-node satellite" style="--angle: 120deg; --dist: 50px;"></div>
<div class="graph-node satellite" style="--angle: 240deg; --dist: 70px;"></div>
<div class="active-node-label">TU JESTEŚ</div>
</div>
</section>
<!-- Quiz Summary -->
<section class="quiz-card glass-panel">
<div class="panel-header">
<h4>Quiz Summary: Key Thinkers</h4>
<NexusIcon Name="arrow-right" Size="16" />
</div>
<div class="quiz-preview">
<p class="question">Który artysta namalował 'Ostatnią Wieczerzę'?</p>
<div class="quiz-options">
<div class="quiz-option active">
<span class="option-letter">A)</span> Michal Anioł
</div>
<div class="quiz-option">
<span class="option-letter">B)</span> Leonardo da Vinci
</div>
</div>
</div>
</section>
</div>
</div>
</main>
</div>
@code {
private UserProfile? _profile;
protected override async Task OnInitializedAsync()
{
var result = await IdentityService.GetProfileAsync();
if (result.IsSuccess)
{
_profile = result.Value;
}
}
}