add changes
This commit is contained in:
378
frontend/src/app/features/quiz/quiz-question/quiz-question.ts
Normal file
378
frontend/src/app/features/quiz/quiz-question/quiz-question.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { Component, OnInit, OnDestroy, inject, signal, computed, effect } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { Subject, takeUntil, interval } from 'rxjs';
|
||||
import { QuizService } from '../../../core/services/quiz.service';
|
||||
import { StorageService } from '../../../core/services/storage.service';
|
||||
import { QuizAnswerSubmission, QuizAnswerResponse } from '../../../core/models/quiz.model';
|
||||
import { Question } from '../../../core/models/question.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-quiz-question',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
MatCardModule,
|
||||
MatRadioModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatProgressBarModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatChipsModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatDividerModule
|
||||
],
|
||||
templateUrl: './quiz-question.html',
|
||||
styleUrls: ['./quiz-question.scss']
|
||||
})
|
||||
export class QuizQuestionComponent implements OnInit, OnDestroy {
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly quizService = inject(QuizService);
|
||||
private readonly storageService = inject(StorageService);
|
||||
private readonly destroy$ = new Subject<void>();
|
||||
|
||||
// Session ID from route
|
||||
sessionId: string = '';
|
||||
|
||||
// Form
|
||||
answerForm!: FormGroup;
|
||||
|
||||
// State signals
|
||||
readonly activeSession = this.quizService.activeSession;
|
||||
readonly isSubmittingAnswer = this.quizService.isSubmittingAnswer;
|
||||
readonly questions = this.quizService.questions;
|
||||
|
||||
// Current question state
|
||||
readonly currentQuestionIndex = computed(() => this.activeSession()?.currentQuestionIndex ?? 0);
|
||||
readonly totalQuestions = computed(() => this.activeSession()?.totalQuestions ?? 0);
|
||||
readonly currentQuestion = signal<Question | null>(null);
|
||||
|
||||
// Answer feedback state
|
||||
readonly answerSubmitted = signal<boolean>(false);
|
||||
readonly answerResult = signal<QuizAnswerResponse | null>(null);
|
||||
readonly showExplanation = signal<boolean>(false);
|
||||
|
||||
// Timer state (for timed quizzes)
|
||||
readonly timeRemaining = signal<number>(0); // in seconds
|
||||
readonly timerRunning = signal<boolean>(false);
|
||||
|
||||
// Progress
|
||||
readonly progress = computed(() => {
|
||||
const total = this.totalQuestions();
|
||||
const current = this.currentQuestionIndex();
|
||||
return total > 0 ? (current / total) * 100 : 0;
|
||||
});
|
||||
|
||||
readonly currentScore = computed(() => this.activeSession()?.score ?? 0);
|
||||
readonly correctAnswers = computed(() => this.activeSession()?.correctAnswers ?? 0);
|
||||
|
||||
// Computed values
|
||||
readonly isLastQuestion = computed(() => {
|
||||
return this.currentQuestionIndex() >= this.totalQuestions() - 1;
|
||||
});
|
||||
|
||||
readonly canSubmitAnswer = computed(() => {
|
||||
return this.answerForm?.valid && !this.answerSubmitted() && !this.isSubmittingAnswer();
|
||||
});
|
||||
|
||||
readonly questionTypeLabel = computed(() => {
|
||||
const type = this.currentQuestion()?.questionType;
|
||||
switch (type) {
|
||||
case 'multiple_choice': return 'Multiple Choice';
|
||||
case 'true_false': return 'True/False';
|
||||
case 'written': return 'Written Answer';
|
||||
default: return '';
|
||||
}
|
||||
});
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sessionId = this.route.snapshot.params['sessionId'];
|
||||
|
||||
if (!this.sessionId) {
|
||||
this.router.navigate(['/quiz/setup']);
|
||||
return;
|
||||
}
|
||||
|
||||
this.initForm();
|
||||
this.loadQuizSession();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.timerRunning.set(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize answer form
|
||||
*/
|
||||
private initForm(): void {
|
||||
this.answerForm = this.fb.group({
|
||||
answer: ['', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load quiz session and questions
|
||||
*/
|
||||
private loadQuizSession(): void {
|
||||
// Check if we have an active session with questions already loaded
|
||||
const activeSession = this.activeSession();
|
||||
const questions = this.questions();
|
||||
|
||||
if (activeSession && activeSession.id === this.sessionId && questions.length > 0) {
|
||||
// Session and questions already loaded from quiz start
|
||||
if (activeSession.status === 'completed') {
|
||||
this.router.navigate(['/quiz', this.sessionId, 'results']);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadCurrentQuestion();
|
||||
|
||||
// Start timer for timed quizzes
|
||||
if (activeSession.quizType === 'timed' && activeSession.timeSpent) {
|
||||
const timeLimit = this.calculateTimeLimit(activeSession.totalQuestions);
|
||||
const remaining = (timeLimit * 60) - (activeSession.timeSpent || 0);
|
||||
if (remaining > 0) {
|
||||
this.startTimer(remaining);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try to restore session from server
|
||||
this.restoreSessionFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore session from server if page was refreshed
|
||||
*/
|
||||
private restoreSessionFromServer(): void {
|
||||
this.quizService.restoreSession(this.sessionId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: ({ session, hasQuestions }) => {
|
||||
if (session.status === 'completed') {
|
||||
this.router.navigate(['/quiz', this.sessionId, 'results']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasQuestions) {
|
||||
// Questions need to be fetched separately
|
||||
// For now, redirect to setup as we can't continue without questions
|
||||
// In a production app, you would fetch questions from the backend
|
||||
console.warn('Session restored but questions not available');
|
||||
this.router.navigate(['/quiz/setup']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Session restored successfully
|
||||
this.loadCurrentQuestion();
|
||||
|
||||
// Start timer for timed quizzes
|
||||
if (session.quizType === 'timed' && session.timeSpent) {
|
||||
const timeLimit = this.calculateTimeLimit(session.totalQuestions);
|
||||
const remaining = (timeLimit * 60) - (session.timeSpent || 0);
|
||||
if (remaining > 0) {
|
||||
this.startTimer(remaining);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// Session not found or error occurred
|
||||
this.router.navigate(['/quiz/setup']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate time limit for timed quiz
|
||||
*/
|
||||
private calculateTimeLimit(questionCount: number): number {
|
||||
return questionCount * 1.5; // 1.5 minutes per question
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current question based on session index
|
||||
*/
|
||||
private loadCurrentQuestion(): void {
|
||||
const index = this.currentQuestionIndex();
|
||||
const allQuestions = this.questions();
|
||||
|
||||
if (index < allQuestions.length) {
|
||||
this.currentQuestion.set(allQuestions[index]);
|
||||
this.answerSubmitted.set(false);
|
||||
this.answerResult.set(null);
|
||||
this.showExplanation.set(false);
|
||||
this.answerForm.reset();
|
||||
this.answerForm.enable();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start timer for timed quiz
|
||||
*/
|
||||
private startTimer(initialTime: number): void {
|
||||
this.timeRemaining.set(initialTime);
|
||||
this.timerRunning.set(true);
|
||||
|
||||
interval(1000)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
const current = this.timeRemaining();
|
||||
if (current > 0 && this.timerRunning()) {
|
||||
this.timeRemaining.set(current - 1);
|
||||
} else if (current === 0) {
|
||||
this.timerRunning.set(false);
|
||||
this.autoCompleteQuiz();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-complete quiz when timer expires
|
||||
*/
|
||||
private autoCompleteQuiz(): void {
|
||||
this.quizService.completeQuiz(this.sessionId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit answer
|
||||
*/
|
||||
submitAnswer(): void {
|
||||
if (!this.canSubmitAnswer()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const question = this.currentQuestion();
|
||||
if (!question) return;
|
||||
|
||||
const answer = this.answerForm.get('answer')?.value;
|
||||
|
||||
const submission: QuizAnswerSubmission = {
|
||||
questionId: question.id,
|
||||
answer: answer,
|
||||
quizSessionId: this.sessionId
|
||||
};
|
||||
|
||||
this.quizService.submitAnswer(submission)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.answerSubmitted.set(true);
|
||||
this.answerResult.set(response);
|
||||
this.showExplanation.set(true);
|
||||
this.answerForm.disable();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Failed to submit answer:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to next question
|
||||
*/
|
||||
nextQuestion(): void {
|
||||
if (this.isLastQuestion()) {
|
||||
// Complete quiz
|
||||
this.completeQuiz();
|
||||
} else {
|
||||
// Load next question
|
||||
this.loadCurrentQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete quiz
|
||||
*/
|
||||
completeQuiz(): void {
|
||||
this.timerRunning.set(false);
|
||||
this.quizService.completeQuiz(this.sessionId)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feedback icon based on answer correctness
|
||||
*/
|
||||
getFeedbackIcon(): string {
|
||||
const result = this.answerResult();
|
||||
return result?.isCorrect ? 'check_circle' : 'cancel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feedback color
|
||||
*/
|
||||
getFeedbackColor(): string {
|
||||
const result = this.answerResult();
|
||||
return result?.isCorrect ? '#4CAF50' : '#f44336';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feedback message
|
||||
*/
|
||||
getFeedbackMessage(): string {
|
||||
const result = this.answerResult();
|
||||
if (!result) return '';
|
||||
return result.isCorrect ? 'Correct!' : 'Incorrect';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time remaining (MM:SS)
|
||||
*/
|
||||
formatTime(seconds: number): string {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get difficulty color
|
||||
*/
|
||||
getDifficultyColor(difficulty: string): string {
|
||||
switch (difficulty) {
|
||||
case 'easy': return '#4CAF50';
|
||||
case 'medium': return '#FF9800';
|
||||
case 'hard': return '#f44336';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if answer is multiple choice
|
||||
*/
|
||||
isMultipleChoice(): boolean {
|
||||
return this.currentQuestion()?.questionType === 'multiple_choice';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if answer is true/false
|
||||
*/
|
||||
isTrueFalse(): boolean {
|
||||
return this.currentQuestion()?.questionType === 'true_false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if answer is written
|
||||
*/
|
||||
isWritten(): boolean {
|
||||
return this.currentQuestion()?.questionType === 'written';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user