feat(ui): implement premium gamified Concepts Map dashboard, unify design tokens, and enforce scoped CSS #54

Merged
mjasin merged 4 commits from feature/concepts-map-dashboard into develop 2026-05-26 17:46:56 +00:00
9 changed files with 135 additions and 12 deletions
Showing only changes of commit 44c4ad0903 - Show all commits
@@ -0,0 +1,9 @@
using FluentResults;
using NexusReader.Application.Queries.Concepts;
namespace NexusReader.Application.Abstractions.Services;
public interface IConceptsMapService
{
Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId);
}
@@ -103,7 +103,7 @@
_isInteractive = true;
if (ViewModel != null)
{
await Coordinator.ProcessFullPageAsync(GetFullPageContent());
await Coordinator.ProcessFullPageAsync(GetFullPageContent(), ebookId: ViewModel.EbookId);
}
}
@@ -246,7 +246,7 @@
if (_isInteractive)
{
await Coordinator.ProcessFullPageAsync(GetFullPageContent());
await Coordinator.ProcessFullPageAsync(GetFullPageContent(), ebookId: ViewModel.EbookId);
}
}
else
@@ -7,7 +7,8 @@
@using NexusReader.Application.Queries.Graph
@using NexusReader.Application.Queries.Concepts
@using System.Net.Http.Json
@inject HttpClient Http
@using NexusReader.Application.Abstractions.Services
@inject IConceptsMapService ConceptsMapService
@inject NavigationManager NavigationManager
@inject IIdentityService IdentityService
@inject ISyncService SyncService
@@ -203,11 +204,11 @@
if (BookId.HasValue && BookId.Value != Guid.Empty)
{
var result = await Http.GetFromJsonAsync<BookConceptsMapResultDto>($"api/book/{BookId}/concepts-map");
if (result != null)
var result = await ConceptsMapService.GetConceptsMapAsync(BookId.Value);
if (result.IsSuccess)
{
Nodes = result.Nodes;
LastReadBlockId = result.LastReadBlockId;
Nodes = result.Value.Nodes;
LastReadBlockId = result.Value.LastReadBlockId;
if (Nodes.Any())
{
@@ -216,7 +217,7 @@
}
else
{
_errorMessage = "Brak odpowiedzi od serwera.";
_errorMessage = result.Errors.FirstOrDefault()?.Message ?? "Brak odpowiedzi od serwera.";
}
}
}
@@ -75,7 +75,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
}
}
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global", Guid? ebookId = null)
{
if (string.IsNullOrWhiteSpace(fullContent)) return;
@@ -87,7 +87,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
try
{
var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId);
var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId);
if (result.IsSuccess)
{
var packet = result.Value;
+1
View File
@@ -36,6 +36,7 @@ builder.Services.AddCascadingAuthenticationState();
// AI & Content Services
builder.Services.AddScoped<IKnowledgeService, WasmKnowledgeService>();
builder.Services.AddScoped<IConceptsMapService, WasmConceptsMapService>();
builder.Services.AddTransient<NexusReader.Web.Client.Handlers.AuthenticationHeaderHandler>();
builder.Services.AddHttpClient("NexusAPI", client =>
@@ -0,0 +1,36 @@
using System.Net.Http.Json;
using FluentResults;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Queries.Concepts;
namespace NexusReader.Web.Client.Services;
public class WasmConceptsMapService : IConceptsMapService
{
private readonly HttpClient _httpClient;
public WasmConceptsMapService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId)
{
try
{
var response = await _httpClient.GetAsync($"api/book/{bookId}/concepts-map");
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<BookConceptsMapResultDto>();
return result != null ? Result.Ok(result) : Result.Fail<BookConceptsMapResultDto>("Błąd deserializacji mapy pojęć.");
}
var errorContent = await response.Content.ReadAsStringAsync();
return Result.Fail<BookConceptsMapResultDto>($"Błąd serwera ({response.StatusCode}): {errorContent}");
}
catch (Exception ex)
{
return Result.Fail<BookConceptsMapResultDto>(new Error("Błąd sieci przy pobieraniu mapy pojęć.").CausedBy(ex));
}
}
}
+6
View File
@@ -74,6 +74,7 @@ builder.Services.AddHttpClient("NexusAPI", (sp, client) =>
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
builder.Services.AddScoped<IIdentityService, NexusReader.Web.Services.ServerIdentityService>();
builder.Services.AddScoped<IConceptsMapService, NexusReader.Web.Services.ServerConceptsMapService>();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddApplication();
@@ -87,6 +88,11 @@ builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
// Authorization Policies
builder.Services.AddScoped<IAuthorizationHandler, TokenLimitHandler>();
builder.Services.AddAuthorizationBuilder()
.SetDefaultPolicy(new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(
IdentityConstants.ApplicationScheme,
IdentityConstants.BearerScheme)
.RequireAuthenticatedUser()
.Build())
.AddPolicy("ProUser", policy => policy.RequireClaim("Plan", SubscriptionPlan.ProName, SubscriptionPlan.EnterpriseName))
.AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement()));
@@ -0,0 +1,59 @@
using System.Security.Claims;
using FluentResults;
using MediatR;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Queries.Concepts;
namespace NexusReader.Web.Services;
public class ServerConceptsMapService : IConceptsMapService
{
private readonly IMediator _mediator;
private readonly AuthenticationStateProvider _authStateProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
public ServerConceptsMapService(
IMediator mediator,
AuthenticationStateProvider authStateProvider,
IHttpContextAccessor httpContextAccessor)
{
_mediator = mediator;
_authStateProvider = authStateProvider;
_httpContextAccessor = httpContextAccessor;
}
public async Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId)
{
try
{
var authState = await _authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user == null || !user.Identity?.IsAuthenticated == true)
{
user = _httpContextAccessor.HttpContext?.User;
}
if (user == null || !user.Identity?.IsAuthenticated == true)
{
return Result.Fail<BookConceptsMapResultDto>("Użytkownik nie jest uwierzytelniony.");
}
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
var tenantId = user.FindFirstValue("TenantId") ?? "global";
if (string.IsNullOrEmpty(userId))
{
return Result.Fail<BookConceptsMapResultDto>("Nie znaleziono identyfikatora użytkownika.");
}
return await _mediator.Send(new GetBookConceptsMapQuery(bookId, userId, tenantId));
}
catch (Exception ex)
{
return Result.Fail<BookConceptsMapResultDto>(new Error("Błąd pobierania mapy pojęć na serwerze.").CausedBy(ex));
}
}
}
@@ -9,6 +9,7 @@ using NexusReader.Application.Queries.User;
using MediatR;
using NexusReader.Application.Constants;
using NexusReader.Application.Abstractions.Services;
using Microsoft.AspNetCore.Components.Authorization;
namespace NexusReader.Web.Services;
@@ -19,6 +20,7 @@ public class ServerIdentityService : IIdentityService
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMediator _mediator;
private readonly INativeStorageService _storageService;
private readonly AuthenticationStateProvider _authStateProvider;
public event Func<Task>? OnStateInvalidated;
@@ -27,13 +29,15 @@ public class ServerIdentityService : IIdentityService
SignInManager<NexusUser> signInManager,
IHttpContextAccessor httpContextAccessor,
IMediator mediator,
INativeStorageService storageService)
INativeStorageService storageService,
AuthenticationStateProvider authStateProvider)
{
_userManager = userManager;
_signInManager = signInManager;
_httpContextAccessor = httpContextAccessor;
_mediator = mediator;
_storageService = storageService;
_authStateProvider = authStateProvider;
}
public async Task<Result> LoginAsync(string email, string password, bool rememberMe = false)
@@ -107,7 +111,14 @@ public class ServerIdentityService : IIdentityService
public async Task<Result<UserProfileDto>> GetProfileAsync()
{
var user = _httpContextAccessor.HttpContext?.User;
var authState = await _authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user == null || !user.Identity?.IsAuthenticated == true)
{
user = _httpContextAccessor.HttpContext?.User;
}
if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated.");
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);