add changes
This commit is contained in:
@@ -3,12 +3,12 @@ import { Question } from './question.model';
|
|||||||
|
|
||||||
export interface QuizSessionHistory {
|
export interface QuizSessionHistory {
|
||||||
|
|
||||||
"time": {
|
time: {
|
||||||
"spent": number,
|
spent: number,
|
||||||
"limit": number | null,
|
limit: number | null,
|
||||||
"percentage": number
|
percentage: number
|
||||||
},
|
},
|
||||||
"createdAt": "2025-12-19T18:49:58.000Z"
|
createdAt: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
category?: {
|
category?: {
|
||||||
@@ -111,7 +111,7 @@ export interface QuizStartResponse {
|
|||||||
*/
|
*/
|
||||||
export interface QuizAnswerSubmission {
|
export interface QuizAnswerSubmission {
|
||||||
questionId: string;
|
questionId: string;
|
||||||
answer: string | string[];
|
userAnswer: string | string[];
|
||||||
quizSessionId: string;
|
quizSessionId: string;
|
||||||
timeSpent?: number;
|
timeSpent?: number;
|
||||||
}
|
}
|
||||||
@@ -146,18 +146,122 @@ export interface QuizResults {
|
|||||||
questions: QuizQuestionResult[];
|
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
|
* Quiz Question Result
|
||||||
*/
|
*/
|
||||||
export interface QuizQuestionResult {
|
export interface QuizQuestionResult {
|
||||||
questionId: string;
|
id: string;
|
||||||
questionText: string;
|
questionText: string;
|
||||||
questionType: string;
|
questionType: string;
|
||||||
userAnswer: string | string[];
|
options: any;
|
||||||
correctAnswer: string | string[];
|
difficulty: string;
|
||||||
isCorrect: boolean;
|
|
||||||
explanation: string;
|
|
||||||
points: number;
|
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;
|
timeSpent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
QuizAnswerSubmission,
|
QuizAnswerSubmission,
|
||||||
QuizAnswerResponse,
|
QuizAnswerResponse,
|
||||||
QuizResults,
|
QuizResults,
|
||||||
QuizStartFormRequest
|
QuizStartFormRequest,
|
||||||
|
CompletedQuizResult,
|
||||||
|
CompletedQuizResponse,
|
||||||
|
QuizReviewResult,
|
||||||
|
QuizReviewResponse
|
||||||
} 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';
|
||||||
@@ -37,7 +41,7 @@ export class QuizService {
|
|||||||
readonly questions = this._questions.asReadonly();
|
readonly questions = this._questions.asReadonly();
|
||||||
|
|
||||||
// Quiz results state
|
// Quiz results state
|
||||||
private readonly _quizResults = signal<QuizResults | null>(null);
|
private readonly _quizResults = signal<CompletedQuizResult | QuizReviewResult | QuizResults | null>(null);
|
||||||
readonly quizResults = this._quizResults.asReadonly();
|
readonly quizResults = this._quizResults.asReadonly();
|
||||||
|
|
||||||
// Loading states
|
// Loading states
|
||||||
@@ -131,7 +135,22 @@ export class QuizService {
|
|||||||
submitAnswer(submission: QuizAnswerSubmission): Observable<QuizAnswerResponse> {
|
submitAnswer(submission: QuizAnswerSubmission): Observable<QuizAnswerResponse> {
|
||||||
this._isSubmittingAnswer.set(true);
|
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 => {
|
tap(response => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Update session state
|
// Update session state
|
||||||
@@ -163,13 +182,13 @@ export class QuizService {
|
|||||||
/**
|
/**
|
||||||
* Complete the quiz session
|
* Complete the quiz session
|
||||||
*/
|
*/
|
||||||
completeQuiz(sessionId: string): Observable<QuizResults> {
|
completeQuiz(sessionId: string): Observable<CompletedQuizResponse> {
|
||||||
this._isCompletingQuiz.set(true);
|
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 => {
|
tap(results => {
|
||||||
if (results.success) {
|
if (results.success) {
|
||||||
this._quizResults.set(results);
|
this._quizResults.set(results.data);
|
||||||
|
|
||||||
// Update session status
|
// Update session status
|
||||||
const currentSession = this._activeSession();
|
const currentSession = this._activeSession();
|
||||||
@@ -220,11 +239,11 @@ export class QuizService {
|
|||||||
/**
|
/**
|
||||||
* Get quiz review data
|
* Get quiz review data
|
||||||
*/
|
*/
|
||||||
reviewQuiz(sessionId: string): Observable<QuizResults> {
|
reviewQuiz(sessionId: string): Observable<QuizReviewResponse> {
|
||||||
return this.http.get<QuizResults>(`${this.apiUrl}/review/${sessionId}`).pipe(
|
return this.http.get<QuizReviewResponse>(`${this.apiUrl}/review/${sessionId}`).pipe(
|
||||||
tap(results => {
|
tap(results => {
|
||||||
if (results.success) {
|
if (results.success) {
|
||||||
this._quizResults.set(results);
|
this._quizResults.set(results.data);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
|
|||||||
@@ -102,10 +102,10 @@
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!answerResult()?.isCorrect) {
|
@if (answerResult() && !answerResult()!.isCorrect) {
|
||||||
<div class="correct-answer">
|
<div class="correct-answer">
|
||||||
<strong>Correct Answer:</strong>
|
<strong>Correct Answer:</strong>
|
||||||
<p>{{ answerResult()?.correctAnswer }}</p>
|
<p>{{ answerResult()!.correctAnswer }}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,9 +123,6 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{{this.answerForm?.valid }}
|
|
||||||
{{ !this.answerSubmitted()}}
|
|
||||||
{{ !this.isSubmittingAnswer()}}
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
@if (!answerSubmitted()) {
|
@if (!answerSubmitted()) {
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export class QuizQuestionComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const submission: QuizAnswerSubmission = {
|
const submission: QuizAnswerSubmission = {
|
||||||
questionId: question.id,
|
questionId: question.id,
|
||||||
answer: answer,
|
userAnswer: answer,
|
||||||
quizSessionId: this.sessionId
|
quizSessionId: this.sessionId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,243 +1,208 @@
|
|||||||
<div class="quiz-review-container">
|
<div class="quiz-review-container">
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@if (isLoading()) {
|
@if (isLoading()) {
|
||||||
<div class="loading-container">
|
<div class="loading-container">
|
||||||
<mat-spinner diameter="50"></mat-spinner>
|
<mat-spinner diameter="50"></mat-spinner>
|
||||||
<p>Loading review...</p>
|
<p>Loading review...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Review Content -->
|
<!-- Review Content -->
|
||||||
@if (!isLoading() && results()) {
|
@if (!isLoading() && results()) {
|
||||||
<div class="review-content">
|
<div class="review-content">
|
||||||
<!-- Header Section -->
|
<!-- Header Section -->
|
||||||
<div class="review-header">
|
<div class="review-header">
|
||||||
<button mat-icon-button (click)="backToResults()" class="back-btn">
|
<button mat-icon-button (click)="backToResults()" class="back-btn">
|
||||||
<mat-icon>arrow_back</mat-icon>
|
<mat-icon>arrow_back</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1 class="review-title">Quiz Review</h1>
|
<h1 class="review-title">Quiz Review</h1>
|
||||||
<p class="review-subtitle">Review your answers and learn from mistakes</p>
|
<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>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
@@ -12,7 +12,7 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
|||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
import { QuizService } from '../../../core/services/quiz.service';
|
import { QuizService } from '../../../core/services/quiz.service';
|
||||||
import { StorageService } from '../../../core/services/storage.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({
|
@Component({
|
||||||
selector: 'app-quiz-review',
|
selector: 'app-quiz-review',
|
||||||
@@ -41,25 +41,32 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
|
|||||||
readonly sessionId = signal<string>('');
|
readonly sessionId = signal<string>('');
|
||||||
readonly results = this.quizService.quizResults;
|
readonly results = this.quizService.quizResults;
|
||||||
readonly isLoading = signal<boolean>(true);
|
readonly isLoading = signal<boolean>(true);
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
readonly pageSize = signal<number>(10);
|
readonly pageSize = signal<number>(10);
|
||||||
readonly pageIndex = signal<number>(0);
|
readonly pageIndex = signal<number>(0);
|
||||||
|
|
||||||
// Filter
|
// Filter
|
||||||
readonly filterType = signal<'all' | 'correct' | 'incorrect'>('all');
|
readonly filterType = signal<'all' | 'correct' | 'incorrect'>('all');
|
||||||
|
|
||||||
// Computed values
|
// Computed values - handle QuizReviewResult structure
|
||||||
readonly allQuestions = computed(() => this.results()?.questions ?? []);
|
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(() => {
|
readonly filteredQuestions = computed(() => {
|
||||||
const questions = this.allQuestions();
|
const questions = this.allQuestions();
|
||||||
const filter = this.filterType();
|
const filter = this.filterType();
|
||||||
|
|
||||||
if (filter === 'all') return questions;
|
if (filter === 'all') return questions;
|
||||||
if (filter === 'correct') return questions.filter(q => q.isCorrect);
|
if (filter === 'correct') return questions.filter(q => q.isCorrect);
|
||||||
if (filter === 'incorrect') return questions.filter(q => !q.isCorrect);
|
if (filter === 'incorrect') return questions.filter(q => !q.isCorrect);
|
||||||
|
|
||||||
return questions;
|
return questions;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,15 +79,31 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
readonly totalQuestions = computed(() => this.filteredQuestions().length);
|
readonly totalQuestions = computed(() => this.filteredQuestions().length);
|
||||||
|
|
||||||
readonly correctCount = computed(() =>
|
readonly correctCount = computed(() =>
|
||||||
this.allQuestions().filter(q => q.isCorrect).length
|
this.allQuestions().filter(q => q.isCorrect).length
|
||||||
);
|
);
|
||||||
|
|
||||||
readonly incorrectCount = computed(() =>
|
readonly incorrectCount = computed(() =>
|
||||||
this.allQuestions().filter(q => !q.isCorrect).length
|
this.allQuestions().filter(q => !q.isCorrect).length
|
||||||
);
|
);
|
||||||
|
|
||||||
readonly isAuthenticated = computed(() =>
|
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()
|
this.storageService.isAuthenticated()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,7 +118,7 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
|
|||||||
const id = params['sessionId'];
|
const id = params['sessionId'];
|
||||||
if (id) {
|
if (id) {
|
||||||
this.sessionId.set(id);
|
this.sessionId.set(id);
|
||||||
|
|
||||||
// Check for filter query param
|
// Check for filter query param
|
||||||
this.route.queryParams
|
this.route.queryParams
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
@@ -174,7 +197,7 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
const bookmarks = this.bookmarkedQuestions();
|
const bookmarks = this.bookmarkedQuestions();
|
||||||
const newBookmarks = new Set(bookmarks);
|
const newBookmarks = new Set(bookmarks);
|
||||||
|
|
||||||
if (newBookmarks.has(questionId)) {
|
if (newBookmarks.has(questionId)) {
|
||||||
newBookmarks.delete(questionId);
|
newBookmarks.delete(questionId);
|
||||||
// TODO: Call bookmark service to remove bookmark
|
// TODO: Call bookmark service to remove bookmark
|
||||||
@@ -184,7 +207,7 @@ export class QuizReviewComponent implements OnInit, OnDestroy {
|
|||||||
// TODO: Call bookmark service to add bookmark
|
// TODO: Call bookmark service to add bookmark
|
||||||
console.log('Add bookmark:', questionId);
|
console.log('Add bookmark:', questionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bookmarkedQuestions.set(newBookmarks);
|
this.bookmarkedQuestions.set(newBookmarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user