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(); // 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(null); // Answer feedback state readonly answerSubmitted = signal(false); readonly answerResult = signal(null); readonly showExplanation = signal(false); // Timer state (for timed quizzes) readonly timeRemaining = signal(0); // in seconds readonly timerRunning = signal(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'; } }