feat(ui/quiz): implement real-time global chapter quiz generation, submit results to database, and display dynamic statistics on dashboard

This commit is contained in:
2026-05-25 10:23:38 +02:00
parent 1c6ee82d01
commit f8d1ceabd3
18 changed files with 1198 additions and 45 deletions
+61 -17
View File
@@ -6,6 +6,8 @@
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@attribute [Authorize]
@implements IDisposable
<PageTitle>Dashboard | Nexus Reader</PageTitle>
@@ -18,20 +20,20 @@
<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>
<h1 class="username">@(string.IsNullOrEmpty(_profile?.DisplayName) ? (_profile?.Email.Split('@')[0] ?? "Użytkownik") : _profile.DisplayName)</h1>
<div class="status-pills">
<div class="status-pill">
<span class="pill-label">Books Read:</span>
<span class="pill-value">12</span>
<span class="pill-label">Książki:</span>
<span class="pill-value">@(_profile?.BooksReadCount ?? 0)</span>
</div>
<div class="status-pill">
<span class="pill-label">Concepts Mapped:</span>
<span class="pill-value">450</span>
<span class="pill-label">Pojęcia:</span>
<span class="pill-value">@(_profile?.ConceptsMappedCount ?? 0)</span>
</div>
<div class="status-pill">
<span class="pill-label">Quiz Mastery:</span>
<span class="pill-value">88%</span>
<span class="pill-label">Średni Wynik:</span>
<span class="pill-value">@(_profile?.AverageQuizScore ?? 0)%</span>
</div>
</div>
</div>
@@ -39,7 +41,7 @@
<!-- Main Content Area -->
<main class="dashboard-content">
<h2 class="section-title">Witaj, @(_profile?.Email.Split('@')[0] ?? "Użytkowniku")</h2>
<h2 class="section-title">Witaj, @(string.IsNullOrEmpty(_profile?.DisplayName) ? (_profile?.Email.Split('@')[0] ?? "Użytkowniku") : _profile.DisplayName)</h2>
<div class="main-grid">
<!-- Current Reading Card -->
@@ -49,7 +51,7 @@
<!-- Knowledge Integration -->
<section class="integration-card glass-panel">
<div class="panel-header">
<h4>Knowledge Integration Progress</h4>
<h4>Integracja Wiedzy</h4>
<NexusIcon Name="arrow-right" Size="16" />
</div>
<div class="graph-placeholder">
@@ -64,19 +66,36 @@
<!-- Quiz Summary -->
<section class="quiz-card glass-panel">
<div class="panel-header">
<h4>Quiz Summary: Key Thinkers</h4>
<h4>Rozwiązane Quizy</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ł
@if (_profile?.RecentQuizzes != null && _profile.RecentQuizzes.Any())
{
<div class="quiz-history-list">
@foreach (var quiz in _profile.RecentQuizzes)
{
<div class="quiz-history-item">
<div class="quiz-item-header">
<span class="quiz-topic">@quiz.Topic</span>
<span class="quiz-score badge @(quiz.Percentage >= 80 ? "badge-success" : quiz.Percentage >= 50 ? "badge-warning" : "badge-danger")">
@quiz.Score / @quiz.TotalQuestions (@((int)quiz.Percentage)%)
</span>
</div>
<div class="quiz-item-meta">
<span class="quiz-date">@quiz.CompletedDate.ToString("g")</span>
</div>
</div>
}
</div>
<div class="quiz-option">
<span class="option-letter">B)</span> Leonardo da Vinci
}
else
{
<div class="empty-quiz-state">
<p class="question">Brak rozwiązanych quizów</p>
<p class="sub-text">Rozwiązuj quizy w trakcie czytania książek, aby śledzić swoje postępy.</p>
</div>
</div>
}
</div>
</section>
</div>
@@ -88,11 +107,36 @@
private UserProfileDto? _profile;
protected override async Task OnInitializedAsync()
{
IdentityService.OnStateInvalidated += HandleStateInvalidatedAsync;
await LoadProfileAsync();
}
private async Task LoadProfileAsync()
{
var result = await IdentityService.GetProfileAsync();
if (result.IsSuccess)
{
_profile = result.Value;
}
else
{
_profile = null;
}
StateHasChanged();
}
private async Task HandleStateInvalidatedAsync()
{
await InvokeAsync(async () =>
{
await LoadProfileAsync();
});
}
public void Dispose()
{
IdentityService.OnStateInvalidated -= HandleStateInvalidatedAsync;
}
}
@@ -404,3 +404,79 @@
grid-template-columns: 1fr;
}
}
/* --- Quiz History Styling --- */
.quiz-history-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.quiz-history-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 12px;
padding: 1rem;
transition: all 0.2s ease;
}
.quiz-history-item:hover {
background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.1);
}
.quiz-item-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.quiz-topic {
font-size: 0.95rem;
font-weight: 500;
color: #ffffff;
}
.quiz-item-meta {
display: flex;
font-size: 0.75rem;
color: #666666;
}
.badge {
padding: 0.25rem 0.5rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-success {
background: rgba(0, 255, 153, 0.1);
color: var(--nexus-neon);
border: 1px solid rgba(0, 255, 153, 0.3);
}
.badge-warning {
background: rgba(255, 170, 0, 0.1);
color: #ffa800;
border: 1px solid rgba(255, 170, 0, 0.3);
}
.badge-danger {
background: rgba(255, 50, 50, 0.1);
color: #ff3232;
border: 1px solid rgba(255, 50, 50, 0.3);
}
.empty-quiz-state {
text-align: center;
padding: 2rem 1rem;
}
.empty-quiz-state .sub-text {
font-size: 0.8rem;
color: #666666;
margin-top: 0.5rem;
}