diff --git a/.agent/skills/nexus-ui-engine/SKILL.md b/.agent/skills/nexus-ui-engine/SKILL.md index 120a706..4c5eba8 100644 --- a/.agent/skills/nexus-ui-engine/SKILL.md +++ b/.agent/skills/nexus-ui-engine/SKILL.md @@ -43,4 +43,7 @@ description: Design System & Component rules for Blazor - **Interactive Flow:** - AI Assistant interactions must be non-blocking and smoothly transition using CSS animations. - - Interactive elements must have clear `:hover`, `:active`, and `:focus` states. \ No newline at end of file + - Interactive elements must have clear `:hover`, `:active`, and `:focus` states. + - **Glass Panel Standard:** All primary data panels (`.glass-panel`) must implement the following interaction signature: + - `transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)` + - `:hover` state must include: `transform: translateY(-4px)`, increased background opacity, and a subtle `--nexus-neon` border highlight (e.g., `rgba(0, 255, 153, 0.2)`). \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Layout/MainHubLayout.razor b/src/NexusReader.UI.Shared/Layout/MainHubLayout.razor index 792c199..744c143 100644 --- a/src/NexusReader.UI.Shared/Layout/MainHubLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/MainHubLayout.razor @@ -85,14 +85,6 @@ private bool _isSyncing = false; - protected override void OnAfterRender(bool firstRender) - { - if (firstRender) - { - Console.WriteLine($"[MainHubLayout] Rendered. IsBrowser: {System.OperatingSystem.IsBrowser()}"); - } - } - protected override async Task OnInitializedAsync() { if (_isSyncing) return; diff --git a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor index 0681c6b..511ce76 100644 --- a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor +++ b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor @@ -110,9 +110,7 @@ protected override async Task OnInitializedAsync() { - Console.WriteLine("[Profile] Initializing..."); _profile = await IdentityService.GetProfileAsync(); - Console.WriteLine($"[Profile] Profile loaded: {_profile?.Email ?? "NULL"}"); StateHasChanged(); } diff --git a/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css b/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css index f99fdd8..a495355 100644 --- a/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css +++ b/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css @@ -128,6 +128,14 @@ border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 20px; padding: 1.5rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.glass-panel:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(0, 255, 153, 0.2); + transform: translateY(-4px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); } /* Reading Card */ diff --git a/src/NexusReader.UI.Shared/Services/IdentityService.cs b/src/NexusReader.UI.Shared/Services/IdentityService.cs index 5ca6764..79d71c8 100644 --- a/src/NexusReader.UI.Shared/Services/IdentityService.cs +++ b/src/NexusReader.UI.Shared/Services/IdentityService.cs @@ -121,17 +121,14 @@ public class IdentityService : IIdentityService { if (_cachedProfile != null) { - Console.WriteLine("[IdentityService] Returning cached profile."); return _cachedProfile; } if (_profileTask != null) { - Console.WriteLine("[IdentityService] Awaiting existing profile task..."); return await _profileTask; } - Console.WriteLine("[IdentityService] Starting new profile fetch task..."); _profileTask = GetProfileInternalAsync(); return await _profileTask; } @@ -142,13 +139,11 @@ public class IdentityService : IIdentityService { if (!System.OperatingSystem.IsBrowser()) { - Console.WriteLine("[IdentityService] Skipping profile fetch: Not in browser."); return null; } if (DateTime.UtcNow - _lastFetchAttempt < TimeSpan.FromSeconds(5)) { - Console.WriteLine("[IdentityService] Skipping profile fetch: Throttled."); return null; } @@ -156,23 +151,18 @@ public class IdentityService : IIdentityService try { var response = await _httpClient.GetAsync("identity/profile"); - Console.WriteLine($"[IdentityService] Profile response: {response.StatusCode}"); if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { - Console.WriteLine("[IdentityService] Unauthorized. Logging out..."); await LogoutAsync(); return null; } if (response.IsSuccessStatusCode) { - var content = await response.Content.ReadAsStringAsync(); - Console.WriteLine($"[IdentityService] Raw content: {content}"); var profile = await response.Content.ReadFromJsonAsync(); if (profile != null) { - Console.WriteLine($"[IdentityService] Profile fetched: {profile.Email}"); _cachedProfile = profile; await _storageService.SaveSecureString("nexus_user_email", profile.Email); await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); @@ -183,9 +173,8 @@ public class IdentityService : IIdentityService return null; } - catch (Exception ex) + catch { - Console.WriteLine($"[IdentityService] ERROR: {ex.Message}"); return null; } finally diff --git a/src/NexusReader.Web.New/Program.cs b/src/NexusReader.Web.New/Program.cs index af5ed83..7805acf 100644 --- a/src/NexusReader.Web.New/Program.cs +++ b/src/NexusReader.Web.New/Program.cs @@ -52,7 +52,8 @@ builder.Services.AddHttpClient("NexusAPI", client => }); builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("NexusAPI")); -builder.Services.AddScoped(); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddScoped(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddApplication(); diff --git a/src/NexusReader.Web.New/Services/ServerIdentityService.cs b/src/NexusReader.Web.New/Services/ServerIdentityService.cs new file mode 100644 index 0000000..4834edb --- /dev/null +++ b/src/NexusReader.Web.New/Services/ServerIdentityService.cs @@ -0,0 +1,74 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using NexusReader.Application.DTOs.User; +using NexusReader.Data.Persistence; +using NexusReader.Domain.Entities; +using NexusReader.UI.Shared.Services; + +namespace NexusReader.Web.New.Services; + +public class ServerIdentityService : IIdentityService +{ + private readonly UserManager _userManager; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IDbContextFactory _dbContextFactory; + + public ServerIdentityService( + UserManager userManager, + IHttpContextAccessor httpContextAccessor, + IDbContextFactory dbContextFactory) + { + _userManager = userManager; + _httpContextAccessor = httpContextAccessor; + _dbContextFactory = dbContextFactory; + } + + public Task LoginAsync(string email, string password, bool rememberMe = false) + => throw new NotSupportedException("Use standard Identity endpoints for login on server."); + + public Task LogoutAsync() + => throw new NotSupportedException("Use standard Identity endpoints for logout on server."); + + public Task RegisterAsync(string email, string password) + => throw new NotSupportedException("Use standard Identity endpoints for registration on server."); + + public Task RefreshTokenAsync() => Task.FromResult(true); + + public async Task GetProfileAsync() + { + var user = _httpContextAccessor.HttpContext?.User; + if (user == null || !user.Identity?.IsAuthenticated == true) return null; + + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); + if (userId == null) return null; + + using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + + var profile = await dbContext.Users + .Where(u => u.Id == userId) + .Select(u => new UserProfile( + u.Email ?? string.Empty, + u.AITokensUsed, + u.TenantId != null && u.TenantId.Length == 36 ? new Guid(u.TenantId) : Guid.Empty, + u.SubscriptionPlan != null ? new SubscriptionPlanDto + { + Id = u.SubscriptionPlan.Id, + Name = u.SubscriptionPlan.PlanName, + AITokenLimit = u.SubscriptionPlan.AITokenLimit, + MonthlyPrice = u.SubscriptionPlan.MonthlyPrice + } : new SubscriptionPlanDto(), + u.QuizResults.Any(q => q.TotalQuestions > 0) + ? (int)u.QuizResults.Where(q => q.TotalQuestions > 0).Average(q => (double)q.Score / q.TotalQuestions * 100) + : 0, + u.Ebooks.OrderByDescending(e => e.LastReadDate).Select(e => new LastReadBookDto + { + Id = e.Id, + Title = e.Title + }).FirstOrDefault() + )) + .FirstOrDefaultAsync(); + + return profile; + } +}