@@ -16,6 +18,8 @@
@code {
+ [Parameter] public Guid? BookId { get; set; }
+
private ReaderCanvas? readerCanvas;
private string? _activeQuizBlockId;
@@ -28,14 +32,31 @@
QuizState.OnQuizRequested += HandleQuizRequestedAsync;
FocusMode.OnFocusModeChanged += HandleUpdate;
await FocusMode.InitializeAsync();
+ }
- // Handle deep-linking to a specific chapter
+ protected override async Task OnParametersSetAsync()
+ {
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
+ int chapterIndex = 0;
if (Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query).TryGetValue("chapter", out var chapterValue))
{
- if (int.TryParse(chapterValue, out var chapterIndex))
+ int.TryParse(chapterValue, out chapterIndex);
+ }
+
+ if (BookId.HasValue && BookId.Value != Guid.Empty)
+ {
+ if (NavService.CurrentEbookId != BookId.Value || NavService.CurrentChapterIndex != chapterIndex)
{
- await NavService.GoToChapter(chapterIndex);
+ NavService.SetBook(BookId.Value, chapterIndex);
+ }
+ }
+ else if (NavService.CurrentEbookId == Guid.Empty)
+ {
+ // If no BookId in URL and no book currently selected, try to load last read book
+ var profileResult = await IdentityService.GetProfileAsync();
+ if (profileResult.IsSuccess && profileResult.Value.LastReadBook != null)
+ {
+ NavService.SetBook(profileResult.Value.LastReadBook.Id, chapterIndex > 0 ? chapterIndex : profileResult.Value.LastReadBook.LastChapterIndex);
}
}
}
diff --git a/src/NexusReader.UI.Shared/Pages/Library.razor b/src/NexusReader.UI.Shared/Pages/Library.razor
index 0d771cc..bea0640 100644
--- a/src/NexusReader.UI.Shared/Pages/Library.razor
+++ b/src/NexusReader.UI.Shared/Pages/Library.razor
@@ -1,23 +1,108 @@
@page "/library"
@attribute [Authorize]
@using NexusReader.UI.Shared.Components.Organisms
+@using NexusReader.Application.DTOs.User
+@using NexusReader.UI.Shared.Services
+@using System.Net.Http.Json
+@inject HttpClient Http
+@inject IReaderNavigationService ReaderNavigation
-
+
-
-
-
Twoja kolekcja książek i dokumentów pojawi się tutaj wkrótce.
-
+
+ @if (_isLoading)
+ {
+
+
+
+
Wczytywanie biblioteki...
+
+
+
+ @for (int i = 0; i < 3; i++)
+ {
+
+ }
+
+
+ }
+ else if (_books == null || !_books.Any())
+ {
+
+
+
Pusta biblioteka
+
Nie masz jeszcze żadnych książek w swojej kolekcji.
+
+
+ _isModalOpen = true">
+ Prześlij pierwszą książkę
+
+
+
+ Skontaktuj się z administratorem, aby dodać książki do swojego konta.
+
+
+
+ }
+ else
+ {
+
+ @foreach (var book in _books)
+ {
+
OpenBook(book.Id)">
+
+
+
+ Czytaj teraz
+
+
+
+
@book.Title
+
@book.Author.Name
+
+ @if (book.Progress > 0)
+ {
+
+
+
Postęp: @(book.Progress.ToString("F0"))% (@book.LastChapter)
+
+ }
+ else
+ {
+
Nowa
+ }
+
+
+ }
+
+ }
@@ -26,35 +111,440 @@
padding: 3rem 2rem;
max-width: 1200px;
margin: 0 auto;
+ animation: fadeIn 0.6s ease-out;
}
.library-header {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 2.5rem;
+ margin-bottom: 3rem;
+ flex-wrap: wrap;
+ gap: 1.5rem;
}
- h1 {
- font-family: var(--nexus-font-serif);
- font-size: 2.5rem;
+ .header-title-section h1 {
+ font-family: var(--nexus-font-serif, 'Outfit', 'Georgia', serif);
+ font-size: 2.8rem;
+ font-weight: 700;
+ margin: 0 0 0.5rem 0;
+ background: linear-gradient(135deg, var(--nexus-text, #ffffff) 0%, rgba(var(--nexus-text-rgb, 255, 255, 255), 0.7) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ letter-spacing: -0.5px;
+ }
+
+ .header-title-section .subtitle {
+ font-size: 1rem;
+ color: rgba(255, 255, 255, 0.6);
margin: 0;
- color: var(--nexus-text);
}
- .library-content {
- min-height: 400px;
+ .add-book-trigger {
+ background: linear-gradient(135deg, var(--nexus-primary, #6366f1) 0%, var(--nexus-primary-hover, #4f46e5) 100%) !important;
+ border: none !important;
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4) !important;
+ font-weight: 600 !important;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
+ }
+
+ .add-book-trigger:hover {
+ transform: translateY(-2px) !important;
+ box-shadow: 0 8px 20px rgba(99, 102, 241, 0.6) !important;
+ }
+
+ .btn-icon {
+ margin-right: 0.5rem;
+ font-weight: bold;
+ }
+
+ /* Books Grid */
+ .books-grid, .loading-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 2rem;
+ }
+
+ .book-card {
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+ border-radius: var(--nexus-radius-lg, 16px);
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+ }
+
+ .book-card::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: radial-gradient(800px circle at var(--x, 0) var(--y, 0), rgba(255, 255, 255, 0.06), transparent 40%);
+ opacity: 0;
+ transition: opacity 0.5s;
+ pointer-events: none;
+ }
+
+ .book-card:hover {
+ transform: translateY(-8px) scale(1.02);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 2px rgba(255, 255, 255, 0.1) inset;
+ border-color: rgba(255, 255, 255, 0.2);
+ }
+
+ .book-card:hover::before {
+ opacity: 1;
+ }
+
+ .book-cover-container {
+ position: relative;
+ height: 380px;
+ background: rgba(0, 0, 0, 0.2);
+ overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
- .empty-state {
+ .book-cover {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ .book-card:hover .book-cover {
+ transform: scale(1.08);
+ }
+
+ .cover-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(15, 23, 42, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ backdrop-filter: blur(4px);
+ }
+
+ .book-card:hover .cover-overlay {
+ opacity: 1;
+ }
+
+ .read-action {
+ color: #ffffff;
+ font-weight: 600;
+ font-size: 1.1rem;
+ padding: 0.75rem 1.5rem;
+ border: 2px solid #ffffff;
+ border-radius: 30px;
+ transform: translateY(10px);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ .book-card:hover .read-action {
+ transform: translateY(0);
+ background: #ffffff;
+ color: #0f172a;
+ }
+
+ .book-details {
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ background: rgba(15, 23, 42, 0.3);
+ }
+
+ .book-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin: 0 0 0.5rem 0;
+ color: var(--nexus-text, #ffffff);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-family: var(--nexus-font-sans, 'Inter', sans-serif);
+ }
+
+ .book-author {
+ font-size: 0.9rem;
+ color: rgba(255, 255, 255, 0.5);
+ margin: 0 0 1rem 0;
+ }
+
+ .new-badge {
+ align-self: flex-start;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--nexus-primary, #6366f1);
+ background: rgba(99, 102, 241, 0.15);
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ border: 1px solid rgba(99, 102, 241, 0.3);
+ }
+
+ /* Book Progress Bar */
+ .book-progress-section {
+ margin-top: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .progress-bar {
+ height: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 3px;
+ overflow: hidden;
+ }
+
+ .progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, var(--nexus-primary, #6366f1) 0%, #a855f7 100%);
+ border-radius: 3px;
+ }
+
+ .progress-text {
+ font-size: 0.8rem;
+ color: rgba(255, 255, 255, 0.4);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ /* Empty State */
+ .empty-state-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 5rem 2rem;
text-align: center;
- opacity: 0.6;
+ border-radius: var(--nexus-radius-lg, 16px);
+ }
+
+ .empty-icon-pulse {
+ margin-bottom: 2rem;
+ color: rgba(255, 255, 255, 0.2);
+ animation: pulse 3s infinite alternate;
+ }
+
+ .empty-state-container h3 {
+ font-family: var(--nexus-font-serif);
+ font-size: 1.8rem;
+ margin: 0 0 0.5rem 0;
+ color: var(--nexus-text);
+ }
+
+ .empty-state-container p {
+ color: rgba(255, 255, 255, 0.5);
+ max-width: 400px;
+ margin: 0 0 2rem 0;
+ }
+
+ .btn-nexus.primary {
+ background: linear-gradient(135deg, var(--nexus-primary, #6366f1) 0%, var(--nexus-primary-hover, #4f46e5) 100%);
+ color: #ffffff;
+ border: none;
+ padding: 0.75rem 2rem;
+ border-radius: 30px;
+ font-weight: 600;
+ cursor: pointer;
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.4);
+ transition: all 0.3s ease;
+ }
+
+ .btn-nexus.primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px rgba(99, 102, 241, 0.6);
+ }
+
+ .restricted-info {
+ font-size: 0.85rem;
+ font-style: italic;
+ color: rgba(255, 255, 255, 0.35) !important;
+ }
+
+ /* Skeleton Loading */
+ .skeleton-card {
+ border-radius: var(--nexus-radius-lg, 16px);
+ overflow: hidden;
+ height: 480px;
+ }
+
+ .skeleton-cover {
+ height: 380px;
+ background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.03) 75%);
+ background-size: 200% 100%;
+ animation: loading 1.5s infinite;
+ }
+
+ .skeleton-details {
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ }
+
+ .skeleton-line {
+ background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.03) 75%);
+ background-size: 200% 100%;
+ animation: loading 1.5s infinite;
+ border-radius: 4px;
+ }
+
+ .skeleton-line.title {
+ height: 20px;
+ width: 80%;
+ }
+
+ .skeleton-line.author {
+ height: 14px;
+ width: 50%;
+ }
+
+ .skeleton-line.progress {
+ height: 8px;
+ width: 100%;
+ margin-top: auto;
+ }
+
+ .library-loading-container {
+ position: relative;
+ width: 100%;
+ }
+
+ .library-loading-container .loader-card {
+ position: absolute;
+ top: 180px;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ gap: 1.25rem;
+ padding: 1.25rem 2.25rem;
+ border-radius: 40px;
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4), 0 0 1px rgba(255, 255, 255, 0.15) inset;
+ background: rgba(15, 23, 42, 0.75);
+ backdrop-filter: blur(16px);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+ }
+
+ .spinner-glow {
+ width: 60px;
+ height: 60px;
+ border: 3px solid rgba(0, 255, 153, 0.1);
+ border-radius: 50%;
+ border-top-color: var(--nexus-neon, #00ff99);
+ animation: spin 1s cubic-bezier(0.55, 0.055, 0.675, 0.19) infinite;
+ box-shadow: 0 0 15px rgba(0, 255, 153, 0.2);
+ }
+
+ .spinner-glow.small {
+ width: 28px;
+ height: 28px;
+ border-width: 2px;
+ }
+
+ .loader-text {
+ font-family: var(--nexus-font-sans, 'Inter', sans-serif);
+ font-weight: 500;
+ color: #ffffff;
+ font-size: 0.95rem;
+ letter-spacing: 0.2px;
+ }
+
+ /* Skeleton Loading enhancements */
+ .skeleton-card {
+ background: rgba(255, 255, 255, 0.02) !important;
+ border: 1px solid rgba(255, 255, 255, 0.05) !important;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important;
+ opacity: 0.5;
+ }
+
+ .skeleton-cover {
+ background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.12) 50%, rgba(255,255,255,0.04) 75%) !important;
+ }
+
+ .skeleton-line {
+ background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.12) 50%, rgba(255,255,255,0.04) 75%) !important;
+ }
+
+ /* Animations */
+ @@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(15px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+
+ @@keyframes pulse {
+ 0% { transform: scale(0.95); opacity: 0.6; }
+ 100% { transform: scale(1.05); opacity: 0.9; }
+ }
+
+ @@keyframes loading {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+ }
+
+ @@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ @@keyframes scaleIn {
+ from { transform: translate(-50%, -50%) scale(0.9); opacity: 0; }
+ to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
@code {
private bool _isModalOpen;
+ private bool _isLoading = true;
+ private List
? _books;
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadBooksAsync();
+ }
+
+ private async Task LoadBooksAsync()
+ {
+ _isLoading = true;
+ StateHasChanged();
+
+ try
+ {
+ _books = await Http.GetFromJsonAsync>("api/library/books");
+ _isLoading = false;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[Library] Failed to load books: {ex.Message}");
+ if (OperatingSystem.IsBrowser())
+ {
+ _isLoading = false;
+ }
+ }
+ finally
+ {
+ StateHasChanged();
+ }
+ }
+
+ private async Task RefreshLibrary()
+ {
+ // Refresh when modal closes or when a book is successfully ingested
+ await LoadBooksAsync();
+ }
+
+ private void OpenBook(Guid bookId)
+ {
+ ReaderNavigation.NavigateToBook(bookId);
+ }
}
diff --git a/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs b/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs
index 897cc14..59482ed 100644
--- a/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs
+++ b/src/NexusReader.UI.Shared/Services/IReaderNavigationService.cs
@@ -18,4 +18,9 @@ public interface IReaderNavigationService
/// Navigates to the reader for a specific book and records the current ebook ID.
///
void NavigateToBook(Guid bookId);
+
+ ///
+ /// Sets the active book context (ID and optional chapter) without triggering browser routing.
+ ///
+ void SetBook(Guid bookId, int chapterIndex = 0);
}
diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
index 858512c..7121ba5 100644
--- a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
+++ b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
@@ -43,8 +43,34 @@ public sealed partial class KnowledgeCoordinator : IDisposable
private async Task HandleNodeSelected(string nodeId)
{
- await _interactionService.RequestScrollToBlock(nodeId);
- await _interactionService.RequestHighlightBlock(nodeId);
+ string? targetBlockId = nodeId;
+
+ var graph = _graphService.CurrentGraphData;
+ if (graph != null)
+ {
+ var selectedNode = graph.Nodes.FirstOrDefault(n => n.Id == nodeId);
+ if (selectedNode != null && selectedNode.Group == "concept")
+ {
+ // Look for connected block nodes (group: "current") in the links
+ var connectedLinks = graph.Links.Where(l => l.Source == nodeId || l.Target == nodeId).ToList();
+ foreach (var link in connectedLinks)
+ {
+ var otherId = link.Source == nodeId ? link.Target : link.Source;
+ var otherNode = graph.Nodes.FirstOrDefault(n => n.Id == otherId);
+ if (otherNode != null && otherNode.Group == "current")
+ {
+ targetBlockId = otherId;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!string.IsNullOrEmpty(targetBlockId))
+ {
+ await _interactionService.RequestScrollToBlock(targetBlockId);
+ await _interactionService.RequestHighlightBlock(targetBlockId);
+ }
}
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
diff --git a/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs b/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs
index 2f52a4b..4a64a23 100644
--- a/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs
+++ b/src/NexusReader.UI.Shared/Services/ReaderNavigationService.cs
@@ -62,6 +62,12 @@ public class ReaderNavigationService : IReaderNavigationService
_navigationManager.NavigateTo($"/reader/{bookId}");
}
+ public void SetBook(Guid bookId, int chapterIndex = 0)
+ {
+ CurrentEbookId = bookId;
+ CurrentChapterIndex = chapterIndex;
+ }
+
private async Task NotifyNavigationChangedAsync()
{
var handlers = OnNavigationChanged?.GetInvocationList();
diff --git a/src/NexusReader.UI.Shared/wwwroot/js/auth.js b/src/NexusReader.UI.Shared/wwwroot/js/auth.js
new file mode 100644
index 0000000..35165cc
--- /dev/null
+++ b/src/NexusReader.UI.Shared/wwwroot/js/auth.js
@@ -0,0 +1,17 @@
+window.nexusAuth = {
+ submitLoginForm: function (formId, email, password, rememberMe) {
+ var form = document.getElementById(formId);
+ if (!form) return false;
+
+ var emailInput = form.querySelector('input[name="email"]');
+ var passwordInput = form.querySelector('input[name="password"]');
+ var rememberMeInput = form.querySelector('input[name="rememberMe"]');
+
+ if (emailInput) emailInput.value = email;
+ if (passwordInput) passwordInput.value = password;
+ if (rememberMeInput) rememberMeInput.value = rememberMe ? "true" : "false";
+
+ form.submit();
+ return true;
+ }
+};
diff --git a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js
index 968e052..f83f487 100644
--- a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js
+++ b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js
@@ -386,12 +386,19 @@ export function zoomToFit() {
export function clear() {
if (!rootGroup) return;
- rootGroup.select(".links-layer").selectAll("path").remove();
- rootGroup.select(".nodes-layer").selectAll("g.node-group").remove();
- if (badge) badge.style("display", "none");
- if (simulation) {
- simulation.nodes([]);
- simulation.force("link").links([]);
- simulation.stop();
+ try {
+ rootGroup.select(".links-layer").selectAll("path").remove();
+ rootGroup.select(".nodes-layer").selectAll("g.node-group").remove();
+ if (badge) badge.style("display", "none");
+ if (simulation) {
+ simulation.stop();
+ const linkForce = simulation.force("link");
+ if (linkForce) {
+ linkForce.links([]);
+ }
+ simulation.nodes([]);
+ }
+ } catch (e) {
+ console.warn("Failed to clear force simulation safely:", e);
}
}
diff --git a/src/NexusReader.Web/Components/App.razor b/src/NexusReader.Web/Components/App.razor
index 266dea5..4d84118 100644
--- a/src/NexusReader.Web/Components/App.razor
+++ b/src/NexusReader.Web/Components/App.razor
@@ -42,6 +42,7 @@
setTimeout(hidePreloader, 3000);
})();
+