add changes

This commit is contained in:
AD2025
2025-12-25 00:24:11 +02:00
parent 079c10e843
commit efb4f69e20
64 changed files with 576 additions and 568 deletions

View File

@@ -7,7 +7,7 @@ const swaggerSpec = require('./config/swagger');
const logger = require('./config/logger'); const logger = require('./config/logger');
const { errorHandler, notFoundHandler } = require('./middleware/errorHandler'); const { errorHandler, notFoundHandler } = require('./middleware/errorHandler');
const { testConnection, getDatabaseStats } = require('./config/db'); const { testConnection, getDatabaseStats } = require('./config/db');
const { validateEnvironment } = require('./validate-env'); const { validateEnvironment } = require('./tests/validate-env');
const { isRedisConnected } = require('./config/redis'); const { isRedisConnected } = require('./config/redis');
// Security middleware // Security middleware

View File

@@ -1,4 +1,4 @@
const { Category } = require('./models'); const { Category } = require('../models');
async function checkCategories() { async function checkCategories() {
const allActive = await Category.findAll({ const allActive = await Category.findAll({

View File

@@ -1,4 +1,4 @@
const { Category } = require('./models'); const { Category } = require('../models');
async function checkCategoryIds() { async function checkCategoryIds() {
try { try {

View File

@@ -1,4 +1,4 @@
const { Question, Category } = require('./models'); const { Question, Category } = require('../models');
async function checkQuestions() { async function checkQuestions() {
try { try {

View File

@@ -1,5 +1,5 @@
// Script to drop categories table // Script to drop categories table
const { sequelize } = require('./models'); const { sequelize } = require('../models');
async function dropCategoriesTable() { async function dropCategoriesTable() {
try { try {

View File

@@ -1,4 +1,4 @@
const { Category } = require('./models'); const { Category } = require('../models');
async function getCategoryMapping() { async function getCategoryMapping() {
try { try {

View File

@@ -1,4 +1,4 @@
const { Question, Category } = require('./models'); const { Question, Category } = require('../models');
async function getQuestionMapping() { async function getQuestionMapping() {
try { try {

View File

@@ -1,5 +1,5 @@
// Category Model Tests // Category Model Tests
const { sequelize, Category } = require('./models'); const { sequelize, Category } = require('../models');
async function runTests() { async function runTests() {
try { try {

View File

@@ -1,5 +1,5 @@
require('dotenv').config(); require('dotenv').config();
const db = require('./models'); const db = require('../models');
async function testDatabaseConnection() { async function testDatabaseConnection() {
console.log('\n🔍 Testing Database Connection...\n'); console.log('\n🔍 Testing Database Connection...\n');

View File

@@ -1,4 +1,4 @@
const { Category } = require('./models'); const { Category } = require('../models');
async function testFindByPk() { async function testFindByPk() {
try { try {

View File

@@ -125,7 +125,7 @@ async function runTests() {
try { try {
// Create a token with fake guest ID // Create a token with fake guest ID
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const config = require('./config/config'); const config = require('../config/config');
const fakeToken = jwt.sign( const fakeToken = jwt.sign(
{ guestId: 'guest_fake_12345' }, { guestId: 'guest_fake_12345' },
config.jwt.secret, config.jwt.secret,

View File

@@ -1,5 +1,5 @@
// GuestSession Model Tests // GuestSession Model Tests
const { sequelize, GuestSession, User } = require('./models'); const { sequelize, GuestSession, User } = require('../models');
async function runTests() { async function runTests() {
try { try {

View File

@@ -1,5 +1,5 @@
const { sequelize } = require('./models'); const { sequelize } = require('../models');
const { User, Category, Question, GuestSession, QuizSession } = require('./models'); const { User, Category, Question, GuestSession, QuizSession } = require('../models');
const { QueryTypes } = require('sequelize'); const { QueryTypes } = require('sequelize');
async function runTests() { async function runTests() {

View File

@@ -13,7 +13,7 @@ async function testLimitReached() {
try { try {
// First, update the guest session to simulate reaching limit // First, update the guest session to simulate reaching limit
const { GuestSession } = require('./models'); const { GuestSession } = require('../models');
console.log('Step 1: Updating guest session to simulate limit reached...'); console.log('Step 1: Updating guest session to simulate limit reached...');
const guestSession = await GuestSession.findOne({ const guestSession = await GuestSession.findOne({

View File

@@ -1,5 +1,5 @@
// Question Model Tests // Question Model Tests
const { sequelize, Question, Category, User } = require('./models'); const { sequelize, Question, Category, User } = require('../models');
async function runTests() { async function runTests() {
try { try {

View File

@@ -1,5 +1,5 @@
const { sequelize } = require('./models'); const { sequelize } = require('../models');
const { User, Category, GuestSession, QuizSession } = require('./models'); const { User, Category, GuestSession, QuizSession } = require('../models');
async function runTests() { async function runTests() {
console.log('🧪 Running QuizSession Model Tests\n'); console.log('🧪 Running QuizSession Model Tests\n');

View File

@@ -1,5 +1,5 @@
require('dotenv').config(); require('dotenv').config();
const db = require('./models'); const db = require('../models');
const { User } = db; const { User } = db;
async function testUserModel() { async function testUserModel() {

View File

@@ -1,5 +1,5 @@
const { Sequelize } = require('sequelize'); const { Sequelize } = require('sequelize');
const config = require('./config/database'); const config = require('../config/database');
const sequelize = new Sequelize( const sequelize = new Sequelize(
config.development.database, config.development.database,

View File

@@ -4,17 +4,92 @@ import { QuizSession, QuizSessionHistory } from './quiz.model';
/** /**
* User Dashboard Response * User Dashboard Response
*/ */
export interface UserDashboard {
export interface UserDataDashboard {
id: string;
username: string;
email: string;
role: string;
profileImage: string | null;
memberSince: string;
}
export interface StatsDashboard {
totalQuizzes: number
quizzesPassed: number
passRate: number
totalQuestionsAnswered: number
correctAnswers: number
overallAccuracy: number
currentStreak: number
longestStreak: number
streakStatus: string;
lastActiveDate: string | null
}
export interface RecentSessionsScoreDashboard {
earned: number
total: number
percentage: number
}
export interface RecentSessionsCategoryDashboard {
id: string
name: string
slug: string
icon: any
color: string
}
export interface RecentSessionsDashboard {
id: string
category: RecentSessionsCategoryDashboard
quizType: string
difficulty: string
status: string
score: RecentSessionsScoreDashboard
isPassed: boolean
questionsAnswered: number
correctAnswers: number
accuracy: number
timeSpent: number
completedAt: string
}
export interface CategoryPerformanceStats {
quizzesTaken: number
quizzesPassed: number
passRate: number
averageScore: number
totalQuestions: number
correctAnswers: number
accuracy: number
}
export interface CategoryPerformanceDashboard {
category: RecentSessionsCategoryDashboard
stats: CategoryPerformanceStats
lastAttempt: string
}
export interface RecentActivityDashboard {
date: string
quizzesCompleted: number
}
export interface UserDashboardResponse {
success: boolean; success: boolean;
totalQuizzes: number; data: UserDashboard
totalQuestionsAnswered: number; }
overallAccuracy: number; export interface UserDashboard {
currentStreak: number; user: UserDataDashboard;
longestStreak: number; stats: StatsDashboard;
averageScore: number; recentSessions: RecentSessionsDashboard[]
recentQuizzes: QuizSession[]; categoryPerformance: CategoryPerformanceDashboard[]
categoryPerformance: CategoryPerformance[]; recentActivity: RecentActivityDashboard[]
achievements?: Achievement[]; // totalQuizzes: number;
// totalQuestionsAnswered: number;
// overallAccuracy: number;
// currentStreak: number;
// longestStreak: number;
// averageScore: number;
// recentQuizzes: QuizSession[];
// categoryPerformance: CategoryPerformance[];
// achievements?: Achievement[];
} }
/** /**
@@ -37,14 +112,14 @@ export interface QuizHistoryResponse {
sessions: QuizSessionHistory[]; sessions: QuizSessionHistory[];
pagination: PaginationInfo; pagination: PaginationInfo;
filters: { filters: {
"category": null, category: null,
"status": null, status: null,
"startDate": null, startDate: null,
"endDate": null endDate: null
} }
"sorting": { sorting: {
"sortBy": string sortBy: string
"sortOrder": string sortOrder: string
} }
}; };
} }

View File

@@ -263,6 +263,7 @@ export interface QuizQuestionResult {
// Legacy support // Legacy support
questionId?: string; questionId?: string;
timeSpent?: number; timeSpent?: number;
} }
/** /**

View File

@@ -14,7 +14,8 @@ import {
CompletedQuizResult, CompletedQuizResult,
CompletedQuizResponse, CompletedQuizResponse,
QuizReviewResult, QuizReviewResult,
QuizReviewResponse QuizReviewResponse,
QuizSessionHistory
} from '../models/quiz.model'; } from '../models/quiz.model';
import { ToastService } from './toast.service'; import { ToastService } from './toast.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
@@ -41,8 +42,13 @@ export class QuizService {
readonly questions = this._questions.asReadonly(); readonly questions = this._questions.asReadonly();
// Quiz results state // Quiz results state
private readonly _quizResults = signal<CompletedQuizResult | QuizReviewResult | QuizResults | null>(null); private readonly _quizResults = signal<QuizReviewResult | null>(null);
private readonly _completedQuiz = signal<CompletedQuizResult | null>(null);
private readonly _sessionHistoryQuiz = signal<QuizSessionHistory | null>(null);
//private readonly _quizResults = signal<CompletedQuizResult | QuizReviewResult | QuizResults | null>(null);
readonly quizResults = this._quizResults.asReadonly(); readonly quizResults = this._quizResults.asReadonly();
readonly sessionQuizHistory = this._sessionHistoryQuiz.asReadonly();
readonly completedQuiz = this._completedQuiz.asReadonly();
// Loading states // Loading states
private readonly _isStartingQuiz = signal<boolean>(false); private readonly _isStartingQuiz = signal<boolean>(false);
@@ -188,7 +194,7 @@ export class QuizService {
return this.http.post<CompletedQuizResponse>(`${this.apiUrl}/complete`, { sessionId }).pipe( return this.http.post<CompletedQuizResponse>(`${this.apiUrl}/complete`, { sessionId }).pipe(
tap(results => { tap(results => {
if (results.success) { if (results.success) {
this._quizResults.set(results.data); this._completedQuiz.set(results.data);
// Update session status // Update session status
const currentSession = this._activeSession(); const currentSession = this._activeSession();

View File

@@ -4,7 +4,7 @@ import { Router } from '@angular/router';
import { catchError, tap, map } from 'rxjs/operators'; import { catchError, tap, map } from 'rxjs/operators';
import { of, Observable } from 'rxjs'; import { of, Observable } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { UserDashboard, QuizHistoryResponse, UserProfileUpdate, UserProfileUpdateResponse } from '../models/dashboard.model'; import { UserDashboard, QuizHistoryResponse, UserProfileUpdate, UserProfileUpdateResponse, UserDashboardResponse } from '../models/dashboard.model';
import { ToastService } from './toast.service'; import { ToastService } from './toast.service';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { StorageService } from './storage.service'; import { StorageService } from './storage.service';
@@ -28,23 +28,23 @@ export class UserService {
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
// Signals // Signals
dashboardState = signal<UserDashboard | null>(null); dashboardState = signal<UserDashboardResponse | null>(null);
historyState = signal<QuizHistoryResponse | null>(null); historyState = signal<QuizHistoryResponse | null>(null);
isLoading = signal<boolean>(false); isLoading = signal<boolean>(false);
error = signal<string | null>(null); error = signal<string | null>(null);
// Cache // Cache
private dashboardCache = new Map<string, CacheEntry<UserDashboard>>(); private dashboardCache = new Map<string, CacheEntry<UserDashboardResponse>>();
// Computed values // Computed values
totalQuizzes = computed(() => this.dashboardState()?.totalQuizzes || 0); totalQuizzes = computed(() => this.dashboardState()?.data.stats.totalQuizzes || 0);
overallAccuracy = computed(() => this.dashboardState()?.overallAccuracy || 0); overallAccuracy = computed(() => this.dashboardState()?.data.stats.overallAccuracy || 0);
currentStreak = computed(() => this.dashboardState()?.currentStreak || 0); currentStreak = computed(() => this.dashboardState()?.data.stats.currentStreak || 0);
/** /**
* Get user dashboard with statistics * Get user dashboard with statistics
*/ */
getDashboard(userId: string, forceRefresh = false): Observable<UserDashboard> { getDashboard(userId: string, forceRefresh = false): Observable<UserDashboardResponse> {
// Check cache if not forcing refresh // Check cache if not forcing refresh
if (!forceRefresh) { if (!forceRefresh) {
const cached = this.dashboardCache.get(userId); const cached = this.dashboardCache.get(userId);
@@ -57,7 +57,7 @@ export class UserService {
this.isLoading.set(true); this.isLoading.set(true);
this.error.set(null); this.error.set(null);
return this.http.get<UserDashboard>(`${this.API_URL}/${userId}/dashboard`).pipe( return this.http.get<UserDashboardResponse>(`${this.API_URL}/${userId}/dashboard`).pipe(
tap(response => { tap(response => {
this.dashboardState.set(response); this.dashboardState.set(response);
// Cache the response // Cache the response
@@ -182,6 +182,6 @@ export class UserService {
*/ */
isDashboardEmpty(): boolean { isDashboardEmpty(): boolean {
const dashboard = this.dashboardState(); const dashboard = this.dashboardState();
return dashboard ? dashboard.totalQuizzes === 0 : true; return dashboard ? dashboard.data.stats.totalQuizzes === 0 : true;
} }
} }

View File

@@ -75,25 +75,22 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div class="category-performance"> <div class="category-performance">
<div <div *ngFor="let category of topCategories()" class="category-bar"
*ngFor="let category of topCategories()" (click)="viewCategory(category.category.id)">
class="category-bar"
(click)="viewCategory(category.categoryId)"
>
<div class="category-info"> <div class="category-info">
<span class="category-name">{{ category.categoryName }}</span> <span class="category-name">{{ category.category.name }}</span>
<span class="category-stats"> <!-- <span class="category-stats">
{{ category.quizzesTaken }} {{ category.quizzesTaken === 1 ? 'quiz' : 'quizzes' }} {{ category.category. }} {{ category.quizzesTaken === 1 ? 'quiz' : 'quizzes' }}
</span> </span> -->
</div> </div>
<div class="progress-bar-container"> <div class="progress-bar-container">
<div <!-- <div
class="progress-bar" class="progress-bar"
[style.width.%]="category.accuracy" [style.width.%]="category.category.accuracy"
[ngClass]="getAccuracyColor(category.accuracy)" [ngClass]="getAccuracyColor(category.category.accuracy)"
></div> ></div> -->
</div> </div>
<span class="accuracy-value">{{ category.accuracy.toFixed(1) }}%</span> <!-- <span class="accuracy-value">{{ category.accuracy.toFixed(1) }}%</span> -->
</div> </div>
</div> </div>
@@ -111,28 +108,19 @@
<mat-icon>history</mat-icon> <mat-icon>history</mat-icon>
Recent Quiz Sessions Recent Quiz Sessions
</mat-card-title> </mat-card-title>
<button <button mat-button color="primary" class="view-all-btn" (click)="viewAllHistory()">
mat-button
color="primary"
class="view-all-btn"
(click)="viewAllHistory()"
>
View All View All
<mat-icon>arrow_forward</mat-icon> <mat-icon>arrow_forward</mat-icon>
</button> </button>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div class="sessions-list"> <div class="sessions-list">
<div <div *ngFor="let session of recentSessions()" class="session-item" (click)="viewQuizResults(session.id)">
*ngFor="let session of recentSessions()"
class="session-item"
(click)="viewQuizResults(session.id)"
>
<div class="session-icon"> <div class="session-icon">
<mat-icon>quiz</mat-icon> <mat-icon>quiz</mat-icon>
</div> </div>
<div class="session-info"> <div class="session-info">
<div class="session-title">{{ session.categoryName || 'Quiz' }}</div> <div class="session-title">{{ session.category.name }}</div>
<div class="session-meta"> <div class="session-meta">
<span class="session-date">{{ formatDate(session.completedAt) }}</span> <span class="session-date">{{ formatDate(session.completedAt) }}</span>
<span class="session-separator"></span> <span class="session-separator"></span>
@@ -144,13 +132,10 @@
</div> </div>
</div> </div>
<div class="session-score"> <div class="session-score">
<span <span class="score-value" [ngClass]="getScoreColor(session.score.earned, session.score.total)">
class="score-value" {{ session.score.total }}/{{ session.questionsAnswered }}
[ngClass]="getScoreColor(session.score, session.totalQuestions)"
>
{{ session.score }}/{{ session.totalQuestions }}
</span> </span>
<span class="score-percentage">{{ ((session.score / session.totalQuestions) * 100).toFixed(0) }}%</span> <span class="score-percentage">{{ session.score.percentage }}%</span>
</div> </div>
<mat-icon class="session-arrow">chevron_right</mat-icon> <mat-icon class="session-arrow">chevron_right</mat-icon>
</div> </div>
@@ -173,17 +158,14 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div class="achievements-grid"> <div class="achievements-grid">
<div <div *ngFor="let achievement of achievements()" class="achievement-item"
*ngFor="let achievement of achievements()" [matTooltip]="achievement.category.name">
class="achievement-item"
[matTooltip]="achievement.description"
>
<div class="achievement-icon"> <div class="achievement-icon">
<mat-icon>{{ achievement.icon }}</mat-icon> <mat-icon>{{ achievement.category.icon }}</mat-icon>
</div> </div>
<div class="achievement-name">{{ achievement.name }}</div> <div class="achievement-name">{{ achievement.category.name }}</div>
<div class="achievement-date" *ngIf="achievement.earnedAt"> <div class="achievement-date" *ngIf="achievement.completedAt">
{{ formatDate(achievement.earnedAt) }} {{ formatDate(achievement.completedAt) }}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { UserDashboard, } from '../../core/models/dashboard.model'; import { UserDashboard, UserDashboardResponse, } from '../../core/models/dashboard.model';
import { UserService } from '../../core/services/user.service'; import { UserService } from '../../core/services/user.service';
import { AuthService } from '../../core/services'; import { AuthService } from '../../core/services';
@@ -49,7 +49,7 @@ export class DashboardComponent implements OnInit {
}); });
isEmpty = computed(() => { isEmpty = computed(() => {
const dash = this.dashboard(); const dash = this.dashboard();
return dash ? dash.totalQuizzes === 0 : true; return dash ? dash.stats.totalQuizzes === 0 : true;
}); });
// Stat cards computed // Stat cards computed
@@ -60,29 +60,29 @@ export class DashboardComponent implements OnInit {
return [ return [
{ {
title: 'Total Quizzes', title: 'Total Quizzes',
value: dash.totalQuizzes, value: dash.stats.totalQuizzes,
icon: 'quiz', icon: 'quiz',
color: 'primary', color: 'primary',
description: 'Quizzes completed' description: 'Quizzes completed'
}, },
{ {
title: 'Overall Accuracy', title: 'Overall Accuracy',
value: `${dash.overallAccuracy.toFixed(1)}%`, value: `${dash.stats.overallAccuracy.toFixed(1)}%`,
icon: 'percent', icon: 'percent',
color: 'success', color: 'success',
description: 'Correct answers' description: 'Correct answers'
}, },
{ {
title: 'Current Streak', title: 'Current Streak',
value: dash.currentStreak, value: dash.stats.currentStreak,
icon: 'local_fire_department', icon: 'local_fire_department',
color: 'warning', color: 'warning',
description: 'Days in a row', description: 'Days in a row',
badge: dash.longestStreak > 0 ? `Best: ${dash.longestStreak}` : undefined badge: dash.stats.longestStreak > 0 ? `Best: ${dash.stats.longestStreak}` : undefined
}, },
{ {
title: 'Questions Answered', title: 'Questions Answered',
value: dash.totalQuestionsAnswered, value: dash.stats.totalQuestionsAnswered,
icon: 'question_answer', icon: 'question_answer',
color: 'accent', color: 'accent',
description: 'Total questions' description: 'Total questions'
@@ -96,22 +96,22 @@ export class DashboardComponent implements OnInit {
if (!dash || !dash.categoryPerformance) return []; if (!dash || !dash.categoryPerformance) return [];
return [...dash.categoryPerformance] return [...dash.categoryPerformance]
.sort((a, b) => b.accuracy - a.accuracy) .sort((a, b) => b.stats.accuracy - a.stats.accuracy)
.slice(0, 5); .slice(0, 5);
}); });
// Recent sessions computed // Recent sessions computed
recentSessions = computed(() => { recentSessions = computed(() => {
const dash = this.dashboard(); const dash = this.dashboard();
if (!dash || !dash.recentQuizzes) return []; if (!dash || !dash.recentSessions) return [];
return dash.recentQuizzes.slice(0, 5); return dash.recentSessions.slice(0, 5);
}); });
// Achievements computed // Achievements computed
achievements = computed(() => { achievements = computed(() => {
const dash = this.dashboard(); const dash = this.dashboard();
return dash?.achievements || []; return dash?.recentSessions || [];
}); });
ngOnInit(): void { ngOnInit(): void {
@@ -134,8 +134,8 @@ export class DashboardComponent implements OnInit {
this.error.set(null); this.error.set(null);
(this.userService as any).getDashboard(user.id).subscribe({ (this.userService as any).getDashboard(user.id).subscribe({
next: (data: UserDashboard) => { next: (res: UserDashboardResponse) => {
this.dashboard.set(data); this.dashboard.set(res.data);
this.isLoading.set(false); this.isLoading.set(false);
}, },
error: (err: any) => { error: (err: any) => {

View File

@@ -45,16 +45,13 @@
<div class="score-circle"> <div class="score-circle">
<svg viewBox="0 0 100 100"> <svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" class="score-bg"></circle> <circle cx="50" cy="50" r="45" class="score-bg"></circle>
<circle <circle cx="50" cy="50" r="45" class="score-progress"
cx="50" [style.stroke-dashoffset]="283 - (283 * scorePercentage() / 100)"></circle>
cy="50"
r="45"
class="score-progress"
[style.stroke-dashoffset]="283 - (283 * scorePercentage() / 100)"
></circle>
</svg> </svg>
<div class="score-text"> <div class="score-text">
<span class="score-number">{{ scorePercentage() }}%</span> @let score = results()!.summary.score.total> 0 ? (results()!.summary.score.earned /
results()!.summary.score.total) * 100 : 0;
<span class="score-number">{{score }}%</span>
<span class="score-label ">Score</span> <span class="score-label ">Score</span>
</div> </div>
</div> </div>
@@ -62,22 +59,22 @@
<div class="score-stat"> <div class="score-stat">
<mat-icon class="stat-icon success">check_circle</mat-icon> <mat-icon class="stat-icon success">check_circle</mat-icon>
<div> <div>
<div class="stat-value">{{ results()!.correctAnswers }}</div> <div class="stat-value">{{ results()!.summary.questions.correct }}</div>
<div class="stat-label">Correct</div> <div class="stat-label">Correct</div>
</div> </div>
</div> </div>
<div class="score-stat"> <div class="score-stat">
<mat-icon class="stat-icon error">cancel</mat-icon> <mat-icon class="stat-icon error">cancel</mat-icon>
<div> <div>
<div class="stat-value">{{ results()!.incorrectAnswers }}</div> <div class="stat-value">{{ results()!.summary.questions.incorrect }}</div>
<div class="stat-label">Incorrect</div> <div class="stat-label">Incorrect</div>
</div> </div>
</div> </div>
@if (results()!.skippedAnswers > 0) { @if (results()!.summary.questions.unanswered > 0) {
<div class="score-stat"> <div class="score-stat">
<mat-icon class="stat-icon warning">remove_circle</mat-icon> <mat-icon class="stat-icon warning">remove_circle</mat-icon>
<div> <div>
<div class="stat-value">{{ results()!.skippedAnswers }}</div> <div class="stat-value">{{ results()!.summary.questions.unanswered }}</div>
<div class="stat-label">Skipped</div> <div class="stat-label">Skipped</div>
</div> </div>
</div> </div>
@@ -90,14 +87,14 @@
<div class="quiz-metadata"> <div class="quiz-metadata">
<div class="metadata-item"> <div class="metadata-item">
<mat-icon>timer</mat-icon> <mat-icon>timer</mat-icon>
<span>Time: {{ formatTime(results()!.timeSpent) }}</span> <span>Time: {{ formatTime(results()!.session.timeSpent) }}</span>
</div> </div>
<div class="metadata-item"> <div class="metadata-item">
<mat-icon>quiz</mat-icon> <mat-icon>quiz</mat-icon>
<span>{{ results()!.totalQuestions }} Questions</span> <span>{{ results()!!.summary.questions.total }} Questions</span>
</div> </div>
<div class="metadata-item"> <div class="metadata-item">
@if (results()!.isPassed) { @if (results()!.summary.isPassed) {
<mat-icon class="success">verified</mat-icon> <mat-icon class="success">verified</mat-icon>
<span class="success">Passed</span> <span class="success">Passed</span>
} @else { } @else {
@@ -119,47 +116,25 @@
<div class="pie-chart"> <div class="pie-chart">
<svg viewBox="0 0 200 200"> <svg viewBox="0 0 200 200">
<!-- Correct answers slice --> <!-- Correct answers slice -->
<circle <circle cx="100" cy="100" r="80" fill="transparent" stroke="#4caf50" stroke-width="40"
cx="100" [style.stroke-dasharray]="chartPercentages().correct * 5.03 + ' 503'" transform="rotate(-90 100 100)">
cy="100" </circle>
r="80"
fill="transparent"
stroke="#4caf50"
stroke-width="40"
[style.stroke-dasharray]="chartPercentages().correct * 5.03 + ' 503'"
transform="rotate(-90 100 100)"
></circle>
<!-- Incorrect answers slice --> <!-- Incorrect answers slice -->
<circle <circle cx="100" cy="100" r="80" fill="transparent" stroke="#f44336" stroke-width="40"
cx="100"
cy="100"
r="80"
fill="transparent"
stroke="#f44336"
stroke-width="40"
[style.stroke-dasharray]="chartPercentages().incorrect * 5.03 + ' 503'" [style.stroke-dasharray]="chartPercentages().incorrect * 5.03 + ' 503'"
[style.stroke-dashoffset]="-chartPercentages().correct * 5.03" [style.stroke-dashoffset]="-chartPercentages().correct * 5.03" transform="rotate(-90 100 100)"></circle>
transform="rotate(-90 100 100)"
></circle>
<!-- Skipped answers slice (if any) --> <!-- Skipped answers slice (if any) -->
@if (chartPercentages().skipped > 0) { @if (chartPercentages().skipped > 0) {
<circle <circle cx="100" cy="100" r="80" fill="transparent" stroke="#ff9800" stroke-width="40"
cx="100"
cy="100"
r="80"
fill="transparent"
stroke="#ff9800"
stroke-width="40"
[style.stroke-dasharray]="chartPercentages().skipped * 5.03 + ' 503'" [style.stroke-dasharray]="chartPercentages().skipped * 5.03 + ' 503'"
[style.stroke-dashoffset]="-(chartPercentages().correct + chartPercentages().incorrect) * 5.03" [style.stroke-dashoffset]="-(chartPercentages().correct + chartPercentages().incorrect) * 5.03"
transform="rotate(-90 100 100)" transform="rotate(-90 100 100)"></circle>
></circle>
} }
</svg> </svg>
<div class="chart-center"> <div class="chart-center">
<span class="chart-total">{{ results()!.totalQuestions }}</span> <span class="chart-total">{{ results()!.summary.questions.total }}</span>
<span class="chart-label">Questions</span> <span class="chart-label">Questions</span>
</div> </div>
</div> </div>
@@ -240,33 +215,19 @@
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="action-buttons"> <div class="action-buttons">
<button <button mat-raised-button color="primary" (click)="retakeQuiz()" class="action-btn">
mat-raised-button
color="primary"
(click)="retakeQuiz()"
class="action-btn"
>
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
Retake Quiz Retake Quiz
</button> </button>
@if (hasIncorrectAnswers()) { @if (hasIncorrectAnswers()) {
<button <button mat-raised-button color="accent" (click)="reviewIncorrect()" class="action-btn">
mat-raised-button
color="accent"
(click)="reviewIncorrect()"
class="action-btn"
>
<mat-icon>rate_review</mat-icon> <mat-icon>rate_review</mat-icon>
Review Incorrect Answers Review Incorrect Answers
</button> </button>
} }
<button <button mat-raised-button (click)="goToDashboard()" class="action-btn">
mat-raised-button
(click)="goToDashboard()"
class="action-btn"
>
<mat-icon>dashboard</mat-icon> <mat-icon>dashboard</mat-icon>
Return to Dashboard Return to Dashboard
</button> </button>
@@ -279,54 +240,37 @@
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<div class="share-buttons"> <div class="share-buttons">
<button <button mat-mini-fab color="primary" (click)="shareResults('twitter')" matTooltip="Share on Twitter"
mat-mini-fab class="share-btn twitter">
color="primary"
(click)="shareResults('twitter')"
matTooltip="Share on Twitter"
class="share-btn twitter"
>
<mat-icon> <mat-icon>
<svg viewBox="0 0 24 24" width="24" height="24"> <svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.70,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" /> <path fill="currentColor"
d="M22.46,6C21.69,6.35 20.86,6.58 20,6.69C20.88,6.16 21.56,5.32 21.88,4.31C21.05,4.81 20.13,5.16 19.16,5.36C18.37,4.5 17.26,4 16,4C13.65,4 11.73,5.92 11.73,8.29C11.73,8.63 11.77,8.96 11.84,9.27C8.28,9.09 5.11,7.38 3,4.79C2.63,5.42 2.42,6.16 2.42,6.94C2.42,8.43 3.17,9.75 4.33,10.5C3.62,10.5 2.96,10.3 2.38,10C2.38,10 2.38,10 2.38,10.03C2.38,12.11 3.86,13.85 5.82,14.24C5.46,14.34 5.08,14.39 4.69,14.39C4.42,14.39 4.15,14.36 3.89,14.31C4.43,16 6,17.26 7.89,17.29C6.43,18.45 4.58,19.13 2.56,19.13C2.22,19.13 1.88,19.11 1.54,19.07C3.44,20.29 5.70,21 8.12,21C16,21 20.33,14.46 20.33,8.79C20.33,8.6 20.33,8.42 20.32,8.23C21.16,7.63 21.88,6.87 22.46,6Z" />
</svg> </svg>
</mat-icon> </mat-icon>
</button> </button>
<button <button mat-mini-fab color="primary" (click)="shareResults('linkedin')" matTooltip="Share on LinkedIn"
mat-mini-fab class="share-btn linkedin">
color="primary"
(click)="shareResults('linkedin')"
matTooltip="Share on LinkedIn"
class="share-btn linkedin"
>
<mat-icon> <mat-icon>
<svg viewBox="0 0 24 24" width="24" height="24"> <svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M19 3A2 2 0 0 1 21 5V19A2 2 0 0 1 19 21H5A2 2 0 0 1 3 19V5A2 2 0 0 1 5 3H19M18.5 18.5V13.2A3.26 3.26 0 0 0 15.24 9.94C14.39 9.94 13.4 10.46 12.92 11.24V10.13H10.13V18.5H12.92V13.57C12.92 12.8 13.54 12.17 14.31 12.17A1.4 1.4 0 0 1 15.71 13.57V18.5H18.5M6.88 8.56A1.68 1.68 0 0 0 8.56 6.88C8.56 5.95 7.81 5.19 6.88 5.19A1.69 1.69 0 0 0 5.19 6.88C5.19 7.81 5.95 8.56 6.88 8.56M8.27 18.5V10.13H5.5V18.5H8.27Z" /> <path fill="currentColor"
d="M19 3A2 2 0 0 1 21 5V19A2 2 0 0 1 19 21H5A2 2 0 0 1 3 19V5A2 2 0 0 1 5 3H19M18.5 18.5V13.2A3.26 3.26 0 0 0 15.24 9.94C14.39 9.94 13.4 10.46 12.92 11.24V10.13H10.13V18.5H12.92V13.57C12.92 12.8 13.54 12.17 14.31 12.17A1.4 1.4 0 0 1 15.71 13.57V18.5H18.5M6.88 8.56A1.68 1.68 0 0 0 8.56 6.88C8.56 5.95 7.81 5.19 6.88 5.19A1.69 1.69 0 0 0 5.19 6.88C5.19 7.81 5.95 8.56 6.88 8.56M8.27 18.5V10.13H5.5V18.5H8.27Z" />
</svg> </svg>
</mat-icon> </mat-icon>
</button> </button>
<button <button mat-mini-fab color="primary" (click)="shareResults('facebook')" matTooltip="Share on Facebook"
mat-mini-fab class="share-btn facebook">
color="primary"
(click)="shareResults('facebook')"
matTooltip="Share on Facebook"
class="share-btn facebook"
>
<mat-icon> <mat-icon>
<svg viewBox="0 0 24 24" width="24" height="24"> <svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M12 2.04C6.5 2.04 2 6.53 2 12.06C2 17.06 5.66 21.21 10.44 21.96V14.96H7.9V12.06H10.44V9.85C10.44 7.34 11.93 5.96 14.22 5.96C15.31 5.96 16.45 6.15 16.45 6.15V8.62H15.19C13.95 8.62 13.56 9.39 13.56 10.18V12.06H16.34L15.89 14.96H13.56V21.96A10 10 0 0 0 22 12.06C22 6.53 17.5 2.04 12 2.04Z" /> <path fill="currentColor"
d="M12 2.04C6.5 2.04 2 6.53 2 12.06C2 17.06 5.66 21.21 10.44 21.96V14.96H7.9V12.06H10.44V9.85C10.44 7.34 11.93 5.96 14.22 5.96C15.31 5.96 16.45 6.15 16.45 6.15V8.62H15.19C13.95 8.62 13.56 9.39 13.56 10.18V12.06H16.34L15.89 14.96H13.56V21.96A10 10 0 0 0 22 12.06C22 6.53 17.5 2.04 12 2.04Z" />
</svg> </svg>
</mat-icon> </mat-icon>
</button> </button>
<button <button mat-mini-fab (click)="copyLink()" matTooltip="Copy Link" class="share-btn copy">
mat-mini-fab
(click)="copyLink()"
matTooltip="Copy Link"
class="share-btn copy"
>
<mat-icon>link</mat-icon> <mat-icon>link</mat-icon>
</button> </button>
</div> </div>

View File

@@ -88,6 +88,7 @@
opacity: 0; opacity: 0;
transform: translateY(20px); transform: translateY(20px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -172,6 +173,7 @@
transform: scale(0); transform: scale(0);
opacity: 0; opacity: 0;
} }
to { to {
transform: scale(1); transform: scale(1);
opacity: 1; opacity: 1;
@@ -252,6 +254,7 @@
} }
.score-label { .score-label {
margin-top: 10px;
display: block; display: block;
font-size: 1rem; font-size: 1rem;
color: var(--text-secondary); color: var(--text-secondary);
@@ -349,6 +352,7 @@
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);

View File

@@ -42,7 +42,9 @@ export class QuizResultsComponent implements OnInit, OnDestroy {
// Computed values // Computed values
readonly scorePercentage = computed(() => { readonly scorePercentage = computed(() => {
const res = this.results(); const res = this.results();
return res?.percentage ?? 0; console.log(res);
return res?.summary.score.percentage ?? 0;
}); });
readonly performanceLevel = computed(() => { readonly performanceLevel = computed(() => {
@@ -79,9 +81,9 @@ export class QuizResultsComponent implements OnInit, OnDestroy {
if (!res) return { correct: 0, incorrect: 0, skipped: 0 }; if (!res) return { correct: 0, incorrect: 0, skipped: 0 };
return { return {
correct: res.correctAnswers, correct: res.summary.questions.correct,
incorrect: res.incorrectAnswers, incorrect: res.summary.questions.incorrect,
skipped: res.skippedAnswers skipped: res.summary.questions.unanswered
}; };
}); });
@@ -245,7 +247,7 @@ export class QuizResultsComponent implements OnInit, OnDestroy {
const results = this.results(); const results = this.results();
if (!results) return; if (!results) return;
const text = `I scored ${results.percentage}% on my quiz! 🎯`; const text = `I scored ${results.summary.score.percentage}% on my quiz! 🎯`;
const url = window.location.href; const url = window.location.href;
let shareUrl = ''; let shareUrl = '';

View File

@@ -65,7 +65,9 @@
<mat-icon>emoji_events</mat-icon> <mat-icon>emoji_events</mat-icon>
</div> </div>
<div class="card-info"> <div class="card-info">
<div class="card-value">{{ scorePercentage() }}%</div> @let score = results()!.summary.score.total> 0 ? (results()!.summary.score.earned /
results()!.summary.score.total) * 100 : 0;
<div class="card-value">{{ score }}%</div>
<div class="card-label">Score</div> <div class="card-label">Score</div>
</div> </div>
</mat-card-content> </mat-card-content>

View File

@@ -87,14 +87,6 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
this.allQuestions().filter(q => !q.isCorrect).length this.allQuestions().filter(q => !q.isCorrect).length
); );
readonly scorePercentage = computed(() => {
const res = this.results();
if (res && 'summary' in res) {
return res.summary.score.percentage;
}
return 0;
});
readonly sessionInfo = computed(() => { readonly sessionInfo = computed(() => {
const res = this.results(); const res = this.results();
if (res && 'session' in res) { if (res && 'session' in res) {