add changes
This commit is contained in:
@@ -3,12 +3,12 @@ import { Question } from './question.model';
|
||||
|
||||
export interface QuizSessionHistory {
|
||||
|
||||
"time": {
|
||||
"spent": number,
|
||||
"limit": number | null,
|
||||
"percentage": number
|
||||
time: {
|
||||
spent: number,
|
||||
limit: number | null,
|
||||
percentage: number
|
||||
},
|
||||
"createdAt": "2025-12-19T18:49:58.000Z"
|
||||
createdAt: string;
|
||||
id: string;
|
||||
|
||||
category?: {
|
||||
@@ -111,7 +111,7 @@ export interface QuizStartResponse {
|
||||
*/
|
||||
export interface QuizAnswerSubmission {
|
||||
questionId: string;
|
||||
answer: string | string[];
|
||||
userAnswer: string | string[];
|
||||
quizSessionId: string;
|
||||
timeSpent?: number;
|
||||
}
|
||||
@@ -146,18 +146,122 @@ export interface QuizResults {
|
||||
questions: QuizQuestionResult[];
|
||||
}
|
||||
|
||||
|
||||
// Response from /complete endpoint - questions are statistics
|
||||
export interface CompletedQuizResult {
|
||||
sessionId: string;
|
||||
status: string;
|
||||
category: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
icon: string;
|
||||
color: string
|
||||
},
|
||||
quizType: string
|
||||
difficulty: string
|
||||
score: {
|
||||
earned: number,
|
||||
total: number,
|
||||
percentage: number
|
||||
},
|
||||
questions: {
|
||||
total: number,
|
||||
answered: number,
|
||||
correct: number,
|
||||
incorrect: number,
|
||||
unanswered: number
|
||||
},
|
||||
accuracy: number,
|
||||
isPassed: boolean,
|
||||
time: {
|
||||
started: string,
|
||||
completed: string,
|
||||
taken: number,
|
||||
limit: number,
|
||||
isTimeout: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface CompletedQuizResponse {
|
||||
success: boolean
|
||||
data: CompletedQuizResult
|
||||
}
|
||||
|
||||
// Response from /review endpoint - questions are detailed array
|
||||
export interface QuizReviewResult {
|
||||
session: {
|
||||
id: string;
|
||||
status: string;
|
||||
quizType: string;
|
||||
difficulty: string;
|
||||
category: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
};
|
||||
startedAt: string;
|
||||
completedAt: string;
|
||||
timeSpent: number;
|
||||
};
|
||||
summary: {
|
||||
score: {
|
||||
earned: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
};
|
||||
questions: {
|
||||
total: number;
|
||||
answered: number;
|
||||
correct: number;
|
||||
incorrect: number;
|
||||
unanswered: number;
|
||||
};
|
||||
accuracy: number;
|
||||
isPassed: boolean;
|
||||
timeStatistics: {
|
||||
totalTime: number;
|
||||
averageTimePerQuestion: number;
|
||||
timeLimit: number | null;
|
||||
wasTimedOut: boolean;
|
||||
};
|
||||
};
|
||||
questions: QuizQuestionResult[];
|
||||
}
|
||||
|
||||
export interface QuizReviewResponse {
|
||||
success: boolean;
|
||||
data: QuizReviewResult;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quiz Question Result
|
||||
*/
|
||||
export interface QuizQuestionResult {
|
||||
questionId: string;
|
||||
id: string;
|
||||
questionText: string;
|
||||
questionType: string;
|
||||
userAnswer: string | string[];
|
||||
correctAnswer: string | string[];
|
||||
isCorrect: boolean;
|
||||
explanation: string;
|
||||
options: any;
|
||||
difficulty: string;
|
||||
points: number;
|
||||
explanation: string;
|
||||
tags: string[];
|
||||
order: number;
|
||||
correctAnswer: string | string[];
|
||||
userAnswer: string | string[] | null;
|
||||
isCorrect: boolean | null;
|
||||
resultStatus: 'correct' | 'incorrect' | 'unanswered';
|
||||
pointsEarned: number;
|
||||
pointsPossible: number;
|
||||
timeTaken: number | null;
|
||||
answeredAt: string | null;
|
||||
showExplanation: boolean;
|
||||
wasAnswered: boolean;
|
||||
// Legacy support
|
||||
questionId?: string;
|
||||
timeSpent?: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
QuizAnswerSubmission,
|
||||
QuizAnswerResponse,
|
||||
QuizResults,
|
||||
QuizStartFormRequest
|
||||
QuizStartFormRequest,
|
||||
CompletedQuizResult,
|
||||
CompletedQuizResponse,
|
||||
QuizReviewResult,
|
||||
QuizReviewResponse
|
||||
} from '../models/quiz.model';
|
||||
import { ToastService } from './toast.service';
|
||||
import { StorageService } from './storage.service';
|
||||
@@ -37,7 +41,7 @@ export class QuizService {
|
||||
readonly questions = this._questions.asReadonly();
|
||||
|
||||
// Quiz results state
|
||||
private readonly _quizResults = signal<QuizResults | null>(null);
|
||||
private readonly _quizResults = signal<CompletedQuizResult | QuizReviewResult | QuizResults | null>(null);
|
||||
readonly quizResults = this._quizResults.asReadonly();
|
||||
|
||||
// Loading states
|
||||
@@ -131,7 +135,22 @@ export class QuizService {
|
||||
submitAnswer(submission: QuizAnswerSubmission): Observable<QuizAnswerResponse> {
|
||||
this._isSubmittingAnswer.set(true);
|
||||
|
||||
return this.http.post<QuizAnswerResponse>(`${this.apiUrl}/submit`, submission).pipe(
|
||||
return this.http.post<any>(`${this.apiUrl}/submit`, submission).pipe(
|
||||
map(response => {
|
||||
// Backend returns: { success, data: { isCorrect, pointsEarned, feedback: { explanation, correctAnswer }, sessionProgress: { currentScore } } }
|
||||
// Frontend expects: { success, isCorrect, correctAnswer, explanation, points, score }
|
||||
const backendData = response.data;
|
||||
const mappedResponse: QuizAnswerResponse = {
|
||||
success: response.success,
|
||||
isCorrect: backendData.isCorrect,
|
||||
correctAnswer: backendData.feedback?.correctAnswer || '',
|
||||
explanation: backendData.feedback?.explanation || '',
|
||||
points: backendData.pointsEarned || 0,
|
||||
score: backendData.sessionProgress?.currentScore || 0,
|
||||
message: response.message
|
||||
};
|
||||
return mappedResponse;
|
||||
}),
|
||||
tap(response => {
|
||||
if (response.success) {
|
||||
// Update session state
|
||||
@@ -163,13 +182,13 @@ export class QuizService {
|
||||
/**
|
||||
* Complete the quiz session
|
||||
*/
|
||||
completeQuiz(sessionId: string): Observable<QuizResults> {
|
||||
completeQuiz(sessionId: string): Observable<CompletedQuizResponse> {
|
||||
this._isCompletingQuiz.set(true);
|
||||
|
||||
return this.http.post<QuizResults>(`${this.apiUrl}/complete`, { sessionId }).pipe(
|
||||
return this.http.post<CompletedQuizResponse>(`${this.apiUrl}/complete`, { sessionId }).pipe(
|
||||
tap(results => {
|
||||
if (results.success) {
|
||||
this._quizResults.set(results);
|
||||
this._quizResults.set(results.data);
|
||||
|
||||
// Update session status
|
||||
const currentSession = this._activeSession();
|
||||
@@ -220,11 +239,11 @@ export class QuizService {
|
||||
/**
|
||||
* Get quiz review data
|
||||
*/
|
||||
reviewQuiz(sessionId: string): Observable<QuizResults> {
|
||||
return this.http.get<QuizResults>(`${this.apiUrl}/review/${sessionId}`).pipe(
|
||||
reviewQuiz(sessionId: string): Observable<QuizReviewResponse> {
|
||||
return this.http.get<QuizReviewResponse>(`${this.apiUrl}/review/${sessionId}`).pipe(
|
||||
tap(results => {
|
||||
if (results.success) {
|
||||
this._quizResults.set(results);
|
||||
this._quizResults.set(results.data);
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
|
||||
@@ -102,10 +102,10 @@
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@if (!answerResult()?.isCorrect) {
|
||||
@if (answerResult() && !answerResult()!.isCorrect) {
|
||||
<div class="correct-answer">
|
||||
<strong>Correct Answer:</strong>
|
||||
<p>{{ answerResult()?.correctAnswer }}</p>
|
||||
<p>{{ answerResult()!.correctAnswer }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -123,9 +123,6 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
{{this.answerForm?.valid }}
|
||||
{{ !this.answerSubmitted()}}
|
||||
{{ !this.isSubmittingAnswer()}}
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
@if (!answerSubmitted()) {
|
||||
|
||||
@@ -269,7 +269,7 @@ export class QuizQuestionComponent implements OnInit, OnDestroy {
|
||||
|
||||
const submission: QuizAnswerSubmission = {
|
||||
questionId: question.id,
|
||||
answer: answer,
|
||||
userAnswer: answer,
|
||||
quizSessionId: this.sessionId
|
||||
};
|
||||
|
||||
|
||||
@@ -1,243 +1,208 @@
|
||||
<div class="quiz-review-container">
|
||||
<!-- Loading State -->
|
||||
@if (isLoading()) {
|
||||
<div class="loading-container">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
<p>Loading review...</p>
|
||||
</div>
|
||||
<div class="loading-container">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
<p>Loading review...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Review Content -->
|
||||
@if (!isLoading() && results()) {
|
||||
<div class="review-content">
|
||||
<!-- Header Section -->
|
||||
<div class="review-header">
|
||||
<button mat-icon-button (click)="backToResults()" class="back-btn">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<div class="header-content">
|
||||
<h1 class="review-title">Quiz Review</h1>
|
||||
<p class="review-subtitle">Review your answers and learn from mistakes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="summary-cards">
|
||||
<mat-card class="summary-card">
|
||||
<mat-card-content>
|
||||
<div class="card-icon total">
|
||||
<mat-icon>quiz</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ allQuestions().length }}</div>
|
||||
<div class="card-label">Total Questions</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="summary-card correct">
|
||||
<mat-card-content>
|
||||
<div class="card-icon success">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ correctCount() }}</div>
|
||||
<div class="card-label">Correct</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="summary-card incorrect">
|
||||
<mat-card-content>
|
||||
<div class="card-icon error">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ incorrectCount() }}</div>
|
||||
<div class="card-label">Incorrect</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="summary-card score">
|
||||
<mat-card-content>
|
||||
<div class="card-icon primary">
|
||||
<mat-icon>emoji_events</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ results()!.percentage }}%</div>
|
||||
<div class="card-label">Score</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Filter Tabs -->
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
mat-stroked-button
|
||||
[class.active]="filterType() === 'all'"
|
||||
(click)="setFilter('all')"
|
||||
>
|
||||
<mat-icon>list</mat-icon>
|
||||
All Questions ({{ allQuestions().length }})
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
[class.active]="filterType() === 'correct'"
|
||||
(click)="setFilter('correct')"
|
||||
class="correct-tab"
|
||||
>
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Correct ({{ correctCount() }})
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
[class.active]="filterType() === 'incorrect'"
|
||||
(click)="setFilter('incorrect')"
|
||||
class="incorrect-tab"
|
||||
>
|
||||
<mat-icon>cancel</mat-icon>
|
||||
Incorrect ({{ incorrectCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Questions List -->
|
||||
<div class="questions-list">
|
||||
@for (question of paginatedQuestions(); track question.questionId; let i = $index) {
|
||||
<mat-card class="question-card" [class.incorrect]="!question.isCorrect">
|
||||
<mat-card-header>
|
||||
<div class="question-header-content">
|
||||
<div class="question-number-badge" [class.correct]="question.isCorrect">
|
||||
<span class="number">{{ (pageIndex() * pageSize()) + i + 1 }}</span>
|
||||
<mat-icon class="status-icon">
|
||||
{{ question.isCorrect ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
</div>
|
||||
|
||||
<div class="question-meta">
|
||||
<mat-chip class="type-chip">
|
||||
{{ getQuestionTypeText(question.questionType) }}
|
||||
</mat-chip>
|
||||
<span class="points-badge">{{ question.points }} pts</span>
|
||||
</div>
|
||||
|
||||
@if (isAuthenticated()) {
|
||||
<button
|
||||
mat-icon-button
|
||||
class="bookmark-btn"
|
||||
[class.bookmarked]="isBookmarked(question.questionId)"
|
||||
(click)="toggleBookmark(question.questionId)"
|
||||
[matTooltip]="isBookmarked(question.questionId) ? 'Remove bookmark' : 'Bookmark question'"
|
||||
>
|
||||
<mat-icon>
|
||||
{{ isBookmarked(question.questionId) ? 'bookmark' : 'bookmark_border' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<div class="question-text">{{ question.questionText }}</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="answer-section">
|
||||
<div class="answer-row">
|
||||
<div class="answer-label">Your Answer:</div>
|
||||
<div class="answer-value" [class.incorrect]="!question.isCorrect">
|
||||
{{ formatAnswer(question.userAnswer) || 'Not answered' }}
|
||||
@if (!question.isCorrect) {
|
||||
<mat-icon class="answer-icon error">close</mat-icon>
|
||||
} @else {
|
||||
<mat-icon class="answer-icon success">check</mat-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!question.isCorrect) {
|
||||
<div class="answer-row correct-answer">
|
||||
<div class="answer-label">Correct Answer:</div>
|
||||
<div class="answer-value correct">
|
||||
{{ formatAnswer(question.correctAnswer) }}
|
||||
<mat-icon class="answer-icon success">check</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (question.explanation) {
|
||||
<div class="explanation-section">
|
||||
<div class="explanation-header">
|
||||
<mat-icon>lightbulb</mat-icon>
|
||||
<span>Explanation</span>
|
||||
</div>
|
||||
<p class="explanation-text">{{ question.explanation }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (question.timeSpent) {
|
||||
<div class="time-spent">
|
||||
<mat-icon>schedule</mat-icon>
|
||||
<span>Time spent: {{ question.timeSpent }}s</span>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
|
||||
@if (paginatedQuestions().length === 0) {
|
||||
<div class="empty-state">
|
||||
<mat-icon>info</mat-icon>
|
||||
<p>No questions match the selected filter</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalQuestions() > pageSize()) {
|
||||
<mat-paginator
|
||||
[length]="totalQuestions()"
|
||||
[pageSize]="pageSize()"
|
||||
[pageIndex]="pageIndex()"
|
||||
[pageSizeOptions]="[5, 10, 20, 50]"
|
||||
(page)="onPageChange($event)"
|
||||
showFirstLastButtons
|
||||
>
|
||||
</mat-paginator>
|
||||
}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="backToResults()"
|
||||
class="action-btn"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
Back to Results
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="retakeQuiz()"
|
||||
class="action-btn"
|
||||
>
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Retake Quiz
|
||||
</button>
|
||||
|
||||
<button
|
||||
mat-raised-button
|
||||
(click)="goToDashboard()"
|
||||
class="action-btn"
|
||||
>
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
Dashboard
|
||||
</button>
|
||||
<div class="review-content">
|
||||
<!-- Header Section -->
|
||||
<div class="review-header">
|
||||
<button mat-icon-button (click)="backToResults()" class="back-btn">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<div class="header-content">
|
||||
<h1 class="review-title">Quiz Review</h1>
|
||||
<p class="review-subtitle">Review your answers and learn from mistakes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="summary-cards">
|
||||
<mat-card class="summary-card">
|
||||
<mat-card-content>
|
||||
<div class="card-icon total">
|
||||
<mat-icon>quiz</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ allQuestions().length }}</div>
|
||||
<div class="card-label">Total Questions</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="summary-card correct">
|
||||
<mat-card-content>
|
||||
<div class="card-icon success">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ correctCount() }}</div>
|
||||
<div class="card-label">Correct</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="summary-card incorrect">
|
||||
<mat-card-content>
|
||||
<div class="card-icon error">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ incorrectCount() }}</div>
|
||||
<div class="card-label">Incorrect</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card class="summary-card score">
|
||||
<mat-card-content>
|
||||
<div class="card-icon primary">
|
||||
<mat-icon>emoji_events</mat-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-value">{{ scorePercentage() }}%</div>
|
||||
<div class="card-label">Score</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<!-- Filter Tabs -->
|
||||
<div class="filter-tabs">
|
||||
<button mat-stroked-button [class.active]="filterType() === 'all'" (click)="setFilter('all')">
|
||||
<mat-icon>list</mat-icon>
|
||||
All Questions ({{ allQuestions().length }})
|
||||
</button>
|
||||
<button mat-stroked-button [class.active]="filterType() === 'correct'" (click)="setFilter('correct')"
|
||||
class="correct-tab">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Correct ({{ correctCount() }})
|
||||
</button>
|
||||
<button mat-stroked-button [class.active]="filterType() === 'incorrect'" (click)="setFilter('incorrect')"
|
||||
class="incorrect-tab">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
Incorrect ({{ incorrectCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Questions List -->
|
||||
<div class="questions-list">
|
||||
@for (question of paginatedQuestions(); track question.id; let i = $index) {
|
||||
<mat-card class="question-card" [class.incorrect]="!question.isCorrect">
|
||||
<mat-card-header>
|
||||
<div class="question-header-content">
|
||||
<div class="question-number-badge" [class.correct]="question.isCorrect">
|
||||
<span class="number">{{ (pageIndex() * pageSize()) + i + 1 }}</span>
|
||||
<mat-icon class="status-icon">
|
||||
{{ question.isCorrect ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
</div>
|
||||
|
||||
<div class="question-meta">
|
||||
<mat-chip class="type-chip">
|
||||
{{ getQuestionTypeText(question.questionType) }}
|
||||
</mat-chip>
|
||||
<span class="points-badge">{{ question.points }} pts</span>
|
||||
</div>
|
||||
|
||||
@if (isAuthenticated()) {
|
||||
<button mat-icon-button class="bookmark-btn" [class.bookmarked]="isBookmarked(question.id)"
|
||||
(click)="toggleBookmark(question.id)"
|
||||
[matTooltip]="isBookmarked(question.id) ? 'Remove bookmark' : 'Bookmark question'">
|
||||
<mat-icon>
|
||||
{{ isBookmarked(question.id) ? 'bookmark' : 'bookmark_border' }}
|
||||
</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<div class="question-text">{{ question.questionText }}</div>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<div class="answer-section">
|
||||
<div class="answer-row">
|
||||
<div class="answer-label">Your Answer:</div>
|
||||
<div class="answer-value" [class.incorrect]="!question.isCorrect">
|
||||
{{ formatAnswer(question.userAnswer ?? '') || 'Not answered' }}
|
||||
@if (!question.isCorrect) {
|
||||
<mat-icon class="answer-icon error">close</mat-icon>
|
||||
} @else {
|
||||
<mat-icon class="answer-icon success">check</mat-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!question.isCorrect) {
|
||||
<div class="answer-row correct-answer">
|
||||
<div class="answer-label">Correct Answer:</div>
|
||||
<div class="answer-value correct">
|
||||
{{ formatAnswer(question.correctAnswer) }}
|
||||
<mat-icon class="answer-icon success">check</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (question.explanation) {
|
||||
<div class="explanation-section">
|
||||
<div class="explanation-header">
|
||||
<mat-icon>lightbulb</mat-icon>
|
||||
<span>Explanation</span>
|
||||
</div>
|
||||
<p class="explanation-text">{{ question.explanation }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (question.timeSpent) {
|
||||
<div class="time-spent">
|
||||
<mat-icon>schedule</mat-icon>
|
||||
<span>Time spent: {{ question.timeSpent }}s</span>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
|
||||
@if (paginatedQuestions().length === 0) {
|
||||
<div class="empty-state">
|
||||
<mat-icon>info</mat-icon>
|
||||
<p>No questions match the selected filter</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
@if (totalQuestions() > pageSize()) {
|
||||
<mat-paginator [length]="totalQuestions()" [pageSize]="pageSize()" [pageIndex]="pageIndex()"
|
||||
[pageSizeOptions]="[5, 10, 20, 50]" (page)="onPageChange($event)" showFirstLastButtons>
|
||||
</mat-paginator>
|
||||
}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button mat-raised-button (click)="backToResults()" class="action-btn">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
Back to Results
|
||||
</button>
|
||||
|
||||
<button mat-raised-button color="primary" (click)="retakeQuiz()" class="action-btn">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Retake Quiz
|
||||
</button>
|
||||
|
||||
<button mat-raised-button (click)="goToDashboard()" class="action-btn">
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -12,7 +12,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { QuizService } from '../../../core/services/quiz.service';
|
||||
import { StorageService } from '../../../core/services/storage.service';
|
||||
import { QuizResults, QuizQuestionResult } from '../../../core/models/quiz.model';
|
||||
import { QuizReviewResult, QuizQuestionResult } from '../../../core/models/quiz.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-quiz-review',
|
||||
@@ -49,8 +49,15 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
|
||||
// Filter
|
||||
readonly filterType = signal<'all' | 'correct' | 'incorrect'>('all');
|
||||
|
||||
// Computed values
|
||||
readonly allQuestions = computed(() => this.results()?.questions ?? []);
|
||||
// Computed values - handle QuizReviewResult structure
|
||||
readonly allQuestions = computed(() => {
|
||||
const res = this.results();
|
||||
// Check if it's QuizReviewResult (has questions array)
|
||||
if (res && 'questions' in res && Array.isArray(res.questions)) {
|
||||
return res.questions;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
readonly filteredQuestions = computed(() => {
|
||||
const questions = this.allQuestions();
|
||||
@@ -80,6 +87,22 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
|
||||
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(() => {
|
||||
const res = this.results();
|
||||
if (res && 'session' in res) {
|
||||
return res.session;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
readonly isAuthenticated = computed(() =>
|
||||
this.storageService.isAuthenticated()
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user