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:
@@ -35,6 +35,16 @@ public class UpdateReadingProgressCommandHandler : IRequestHandler<UpdateReading
|
||||
user.LastReadPageId = request.PageId;
|
||||
user.LastReadAt = now;
|
||||
|
||||
// Update specific Ebook progress
|
||||
var ebook = await context.Ebooks.FirstOrDefaultAsync(e => e.Id == request.EbookId, cancellationToken);
|
||||
if (ebook != null)
|
||||
{
|
||||
ebook.Progress = request.Progress;
|
||||
ebook.LastChapter = request.ChapterTitle;
|
||||
ebook.LastChapterIndex = request.ChapterIndex;
|
||||
ebook.LastReadDate = now;
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Broadcast to other devices
|
||||
|
||||
@@ -2,6 +2,7 @@ using MediatR;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NexusReader.Application.Commands.Sync;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace NexusReader.Infrastructure.RealTime;
|
||||
|
||||
@@ -15,12 +16,12 @@ public class SyncHub : Hub
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task UpdateProgress(string pageId)
|
||||
public async Task UpdateProgress(string pageId, Guid ebookId, double progress, string? chapterTitle, int chapterIndex)
|
||||
{
|
||||
var userId = Context.UserIdentifier;
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
var userId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (userId != null)
|
||||
{
|
||||
await _mediator.Send(new UpdateReadingProgressCommand(pageId, userId, Context.ConnectionId));
|
||||
await _mediator.Send(new UpdateReadingProgressCommand(pageId, userId, ebookId, progress, chapterTitle, chapterIndex, Context.ConnectionId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,24 @@ using FluentResults;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Application.Queries.Reader;
|
||||
using VersOne.Epub;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NexusReader.Data.Persistence;
|
||||
using NexusReader.Domain.Entities;
|
||||
|
||||
namespace NexusReader.Infrastructure.Services;
|
||||
|
||||
public class EpubService : IEpubService
|
||||
{
|
||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||
private const string EpubPath = "wwwroot/assets/book.epub";
|
||||
private const int WordThreshold = 1000;
|
||||
|
||||
public async Task<Result<ReaderPageViewModel>> GetEpubContentAsync(int chapterIndex)
|
||||
public EpubService(IDbContextFactory<AppDbContext> dbContextFactory)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Result<ReaderPageViewModel>> GetEpubContentAsync(int chapterIndex, string? userId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -100,7 +109,29 @@ public class EpubService : IEpubService
|
||||
blocks.Add(CreateAiTrigger($"trigger-{blockCounter++}"));
|
||||
}
|
||||
|
||||
return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, readingOrder.Count, chapterTitle));
|
||||
// Find the EbookId from DB for this file AND this user
|
||||
using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
var ebook = await context.Ebooks
|
||||
.Where(e => e.FilePath.Contains("book.epub") && (userId == null || e.UserId == userId))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// Auto-provision if not found for this user (convenience for dev)
|
||||
if (ebook == null && !string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var author = await context.Authors.FirstOrDefaultAsync() ?? new Author { Name = "Unknown Author" };
|
||||
ebook = new Ebook
|
||||
{
|
||||
Title = "Lives of the Most Excellent Painters, Sculptors, and Architects",
|
||||
FilePath = "wwwroot/assets/book.epub",
|
||||
UserId = userId,
|
||||
Author = author,
|
||||
TenantId = "global"
|
||||
};
|
||||
context.Ebooks.Add(ebook);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, readingOrder.Count, chapterTitle, ebook?.Id ?? Guid.Empty));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user