add changes

This commit is contained in:
AD2025
2025-11-14 02:04:33 +02:00
parent 501de0103f
commit 6f23890407
48 changed files with 10759 additions and 213 deletions

View 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';
}
}