diff --git a/backend/routes/quiz.routes.js b/backend/routes/quiz.routes.js index 80d8eb0..fbce1dc 100644 --- a/backend/routes/quiz.routes.js +++ b/backend/routes/quiz.routes.js @@ -30,6 +30,8 @@ const authenticateUserOrGuest = async (req, res, next) => { // Try to verify guest token const guestToken = req.headers['x-guest-token']; + console.log(guestToken); + if (guestToken) { try { await new Promise((resolve, reject) => { diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts index f47c83d..f319656 100644 --- a/frontend/src/app/app.ts +++ b/frontend/src/app/app.ts @@ -35,26 +35,26 @@ export class App implements OnInit { private toastService = inject(ToastService); private router = inject(Router); protected title = 'Interview Quiz Application'; - + // Signal for mobile sidebar state isSidebarOpen = signal(false); - + // Signal for app initialization state isInitializing = signal(true); - + // Signal for navigation loading state isNavigating = signal(false); - + // Computed signal to check if user is guest isGuest = computed(() => { return this.guestService.guestState().isGuest && !this.authService.authState().isAuthenticated; }); - + ngOnInit(): void { this.initializeApp(); this.setupNavigationListener(); } - + /** * Setup navigation event listener for progress bar */ @@ -71,24 +71,24 @@ export class App implements OnInit { } }); } - + /** * Initialize application and verify token */ private initializeApp(): void { const token = this.authService.authState().isAuthenticated; - + // If no token, skip verification if (!token) { this.isInitializing.set(false); return; } - + // Verify token on app load this.authService.verifyToken().subscribe({ next: (response) => { this.isInitializing.set(false); - if (!response.valid) { + if (!response.success) { this.toastService.warning('Session expired. Please login again.'); this.router.navigate(['/login']); } @@ -100,14 +100,14 @@ export class App implements OnInit { } }); } - + /** * Toggle mobile sidebar */ toggleSidebar(): void { this.isSidebarOpen.update(value => !value); } - + /** * Close sidebar (for mobile) */ diff --git a/frontend/src/app/core/models/category.model.ts b/frontend/src/app/core/models/category.model.ts index e7cfd0f..5ff4935 100644 --- a/frontend/src/app/core/models/category.model.ts +++ b/frontend/src/app/core/models/category.model.ts @@ -61,7 +61,7 @@ export interface QuestionPreview { /** * Question Types */ -export type QuestionType = 'multiple_choice' | 'true_false' | 'written'; +export type QuestionType = 'multiple' | 'trueFalse' | 'written'; /** * Difficulty Levels diff --git a/frontend/src/app/core/models/dashboard.model.ts b/frontend/src/app/core/models/dashboard.model.ts index 2e2f55c..fc0ce66 100644 --- a/frontend/src/app/core/models/dashboard.model.ts +++ b/frontend/src/app/core/models/dashboard.model.ts @@ -1,5 +1,5 @@ import { User } from './user.model'; -import { QuizSession } from './quiz.model'; +import { QuizSession, QuizSessionHistory } from './quiz.model'; /** * User Dashboard Response @@ -33,8 +33,20 @@ export interface CategoryPerformance { */ export interface QuizHistoryResponse { success: boolean; - sessions: QuizSession[]; - pagination: PaginationInfo; + data: { + sessions: QuizSessionHistory[]; + pagination: PaginationInfo; + filters: { + "category": null, + "status": null, + "startDate": null, + "endDate": null + } + "sorting": { + "sortBy": string + "sortOrder": string + } + }; } /** diff --git a/frontend/src/app/core/models/question.model.ts b/frontend/src/app/core/models/question.model.ts index 33e1ea1..9baeac2 100644 --- a/frontend/src/app/core/models/question.model.ts +++ b/frontend/src/app/core/models/question.model.ts @@ -19,7 +19,7 @@ export interface Question { color?: string; guestAccessible?: boolean; }; - options?: string[]; // For multiple choice + options?: string[] | { id: string; text: string }[]; // For multiple choice correctAnswer: string | string[]; explanation: string; points: number; diff --git a/frontend/src/app/core/models/quiz.model.ts b/frontend/src/app/core/models/quiz.model.ts index df166b3..be1aa03 100644 --- a/frontend/src/app/core/models/quiz.model.ts +++ b/frontend/src/app/core/models/quiz.model.ts @@ -1,5 +1,42 @@ +import { Category } from './category.model'; import { Question } from './question.model'; +export interface QuizSessionHistory { + + "time": { + "spent": number, + "limit": number | null, + "percentage": number + }, + "createdAt": "2025-12-19T18:49:58.000Z" + id: string; + + category?: { + id: string; + name: string; + slug: string; + icon: string; + color: string; + }; + quizType: QuizType; + difficulty: string; + questions: { + answered: number, + total: number, + correct: number, + accuracy: number + }; + score: { + earned: number + total: number + percentage: number + }; + + status: QuizStatus; + startedAt: string; + completedAt?: string; + isPassed?: boolean; +} /** * Quiz Session Interface * Represents an active or completed quiz session @@ -40,6 +77,16 @@ export type QuizStatus = 'in_progress' | 'completed' | 'abandoned'; * Quiz Start Request */ export interface QuizStartRequest { + success: true; + data: { + categoryId: string; + questionCount: number; + difficulty?: string; // 'easy', 'medium', 'hard', 'mixed' + quizType?: QuizType; + }; +} +export interface QuizStartFormRequest { + categoryId: string; questionCount: number; difficulty?: string; // 'easy', 'medium', 'hard', 'mixed' @@ -51,10 +98,12 @@ export interface QuizStartRequest { */ export interface QuizStartResponse { success: boolean; - sessionId: string; - questions: Question[]; - totalQuestions: number; - message?: string; + data: { + sessionId: string; + questions: Question[]; + totalQuestions: number; + message?: string; + }; } /** diff --git a/frontend/src/app/core/services/auth.service.ts b/frontend/src/app/core/services/auth.service.ts index c858b38..4e025b4 100644 --- a/frontend/src/app/core/services/auth.service.ts +++ b/frontend/src/app/core/services/auth.service.ts @@ -5,12 +5,12 @@ import { Observable, throwError, tap, catchError } from 'rxjs'; import { environment } from '../../../environments/environment.development'; import { StorageService } from './storage.service'; import { ToastService } from './toast.service'; -import { - User, - UserRegistration, - UserLogin, +import { + User, + UserRegistration, + UserLogin, AuthResponse, - AuthState + AuthState } from '../models/user.model'; @Injectable({ @@ -21,9 +21,9 @@ export class AuthService { private storageService = inject(StorageService); private toastService = inject(ToastService); private router = inject(Router); - + private readonly API_URL = `${environment.apiUrl}/auth`; - + // Auth state signal private authStateSignal = signal({ user: this.storageService.getUserData(), @@ -31,10 +31,10 @@ export class AuthService { isLoading: false, error: null }); - + // Public readonly auth state public readonly authState = this.authStateSignal.asReadonly(); - + /** * Register a new user account * Handles guest-to-user conversion if guestSessionId provided @@ -46,34 +46,34 @@ export class AuthService { guestSessionId?: string ): Observable { this.setLoading(true); - + const registrationData: UserRegistration = { username, email, password, guestSessionId }; - + return this.http.post(`${this.API_URL}/register`, registrationData).pipe( tap((response) => { // Store token and user data this.storageService.setToken(response.data.token, true); // Remember me by default this.storageService.setUserData(response.data.user); - + // Clear guest token if converting if (guestSessionId) { this.storageService.clearGuestToken(); } - + // Update auth state this.updateAuthState(response.data.user, null); - + // Show success message const message = response.migratedStats ? `Welcome ${response.data.user.username}! Your guest progress has been saved.` : `Welcome ${response.data.user.username}! Your account has been created.`; this.toastService.success(message); - + // Auto-login: redirect to categories this.router.navigate(['/categories']); }), @@ -83,32 +83,32 @@ export class AuthService { }) ); } - + /** * Login user */ login(email: string, password: string, rememberMe: boolean = false, redirectUrl: string = '/categories'): Observable { this.setLoading(true); - + const loginData: UserLogin = { email, password }; - + return this.http.post(`${this.API_URL}/login`, loginData).pipe( tap((response) => { // Store token and user data console.log(response.data.user); - + this.storageService.setToken(response.data.token, rememberMe); this.storageService.setUserData(response.data.user); - + // Clear guest token this.storageService.clearGuestToken(); - + // Update auth state this.updateAuthState(response.data.user, null); - + // Show success message this.toastService.success(`Welcome back, ${response.data.user.username}!`); - + // Redirect to requested URL this.router.navigate([redirectUrl]); }), @@ -118,18 +118,18 @@ export class AuthService { }) ); } - + /** * Logout user */ logout(): Observable { this.setLoading(true); - + return this.http.post(`${this.API_URL}/logout`, {}).pipe( tap(() => { // Clear all auth data this.storageService.clearAll(); - + // Reset auth state this.authStateSignal.set({ user: null, @@ -137,10 +137,10 @@ export class AuthService { isLoading: false, error: null }); - + // Show success message this.toastService.success('You have been logged out successfully.'); - + // Redirect to login this.router.navigate(['/login']); }), @@ -158,13 +158,13 @@ export class AuthService { }) ); } - + /** * Verify JWT token validity */ - verifyToken(): Observable<{ valid: boolean; user?: User }> { + verifyToken(): Observable<{ success: boolean; data: { user?: User }, message: string }> { const token = this.storageService.getToken(); - + if (!token) { this.authStateSignal.update(state => ({ ...state, @@ -173,15 +173,15 @@ export class AuthService { })); return throwError(() => new Error('No token found')); } - + this.setLoading(true); - - return this.http.get<{ valid: boolean; user?: User }>(`${this.API_URL}/verify`).pipe( + + return this.http.get<{ success: boolean; data: { user?: User }, message: string }>(`${this.API_URL}/verify`).pipe( tap((response) => { - if (response.valid && response.user) { + if (response.success && response.data.user) { // Update user data - this.storageService.setUserData(response.user); - this.updateAuthState(response.user, null); + this.storageService.setUserData(response.data.user); + this.updateAuthState(response.data.user, null); } else { // Token invalid, clear auth this.clearAuth(); @@ -194,7 +194,7 @@ export class AuthService { }) ); } - + /** * Clear authentication data */ @@ -208,7 +208,7 @@ export class AuthService { error: null }); } - + /** * Update auth state signal */ @@ -220,20 +220,20 @@ export class AuthService { error }); } - + /** * Set loading state */ private setLoading(isLoading: boolean): void { this.authStateSignal.update(state => ({ ...state, isLoading })); } - + /** * Handle authentication errors */ private handleAuthError(error: HttpErrorResponse): void { let errorMessage = 'An error occurred. Please try again.'; - + if (error.status === 400) { errorMessage = 'Invalid input. Please check your information.'; } else if (error.status === 401) { @@ -245,25 +245,25 @@ export class AuthService { } else if (error.status === 0) { errorMessage = 'Unable to connect to server. Please check your internet connection.'; } - + this.updateAuthState(null, errorMessage); this.toastService.error(errorMessage); } - + /** * Get current user */ getCurrentUser(): User | null { return this.authStateSignal().user; } - + /** * Check if user is authenticated */ isAuthenticated(): boolean { return this.authStateSignal().isAuthenticated; } - + /** * Check if user is admin */ diff --git a/frontend/src/app/core/services/guest.service.ts b/frontend/src/app/core/services/guest.service.ts index 2b6d0a0..647b8e0 100644 --- a/frontend/src/app/core/services/guest.service.ts +++ b/frontend/src/app/core/services/guest.service.ts @@ -15,13 +15,13 @@ export class GuestService { private storageService = inject(StorageService); private toastService = inject(ToastService); private router = inject(Router); - + private readonly API_URL = `${environment.apiUrl}/guest`; private readonly GUEST_TOKEN_KEY = 'guest_token'; private readonly GUEST_ID_KEY = 'guest_id'; private readonly DEVICE_ID_KEY = 'device_id'; private readonly SESSION_EXPIRY_HOURS = 24; - + // Guest state signal private guestStateSignal = signal({ session: null, @@ -30,34 +30,34 @@ export class GuestService { error: null, quizLimit: null }); - + // Public readonly guest state public readonly guestState = this.guestStateSignal.asReadonly(); - + /** * Start a new guest session * Generates device ID and creates session on backend */ - startSession(): Observable { + startSession(): Observable<{ success: boolean, message: string, data: GuestSession }> { this.setLoading(true); - + const deviceId = this.getOrCreateDeviceId(); - - return this.http.post(`${this.API_URL}/start-session`, { deviceId }).pipe( - tap((session: GuestSession) => { + + return this.http.post<{ success: boolean, message: string, data: GuestSession }>(`${this.API_URL}/start-session`, { deviceId }).pipe( + tap((session: { success: boolean, message: string, data: GuestSession }) => { // Store guest session data - this.storageService.setItem(this.GUEST_TOKEN_KEY, session.sessionToken); - this.storageService.setItem(this.GUEST_ID_KEY, session.guestId); - + this.storageService.setItem(this.GUEST_ID_KEY, session.data.guestId); + this.storageService.setGuestToken(session.data.sessionToken); + // Update guest state this.guestStateSignal.update(state => ({ ...state, - session, + session: session.data, isGuest: true, isLoading: false, error: null })); - + this.toastService.success('Welcome! You\'re browsing as a guest.'); }), catchError((error: HttpErrorResponse) => { @@ -67,13 +67,13 @@ export class GuestService { }) ); } - + /** * Get guest session details */ getSession(guestId: string): Observable { this.setLoading(true); - + return this.http.get(`${this.API_URL}/session/${guestId}`).pipe( tap((session: GuestSession) => { this.guestStateSignal.update(state => ({ @@ -95,13 +95,13 @@ export class GuestService { }) ); } - + /** * Get remaining quiz attempts for guest */ getQuizLimit(): Observable { this.setLoading(true); - + return this.http.get(`${this.API_URL}/quiz-limit`).pipe( tap((limit: GuestLimit) => { this.guestStateSignal.update(state => ({ @@ -117,14 +117,14 @@ export class GuestService { }) ); } - + /** * Convert guest session to registered user * Called during registration process */ convertToUser(guestSessionId: string, userData: any): Observable { this.setLoading(true); - + return this.http.post(`${this.API_URL}/convert`, { guestSessionId, ...userData @@ -140,23 +140,23 @@ export class GuestService { }) ); } - + /** * Generate or retrieve device ID * Used for fingerprinting guest sessions */ private getOrCreateDeviceId(): string { let deviceId = this.storageService.getItem(this.DEVICE_ID_KEY); - + if (!deviceId) { // Generate UUID v4 deviceId = this.generateUUID(); this.storageService.setItem(this.DEVICE_ID_KEY, deviceId); } - + return deviceId; } - + /** * Generate UUID v4 */ @@ -167,7 +167,7 @@ export class GuestService { return v.toString(16); }); } - + /** * Check if user has an active guest session */ @@ -176,42 +176,42 @@ export class GuestService { const guestId = this.storageService.getItem(this.GUEST_ID_KEY); return !!(token && guestId); } - + /** * Get stored guest token */ getGuestToken(): string | null { return this.storageService.getItem(this.GUEST_TOKEN_KEY); } - + /** * Get stored guest ID */ getGuestId(): string | null { return this.storageService.getItem(this.GUEST_ID_KEY); } - + /** * Check if session is expired (24 hours) */ isSessionExpired(): boolean { const session = this.guestState().session; if (!session) return true; - + const createdAt = new Date(session.createdAt); const now = new Date(); const hoursDiff = (now.getTime() - createdAt.getTime()) / (1000 * 60 * 60); - + return hoursDiff >= this.SESSION_EXPIRY_HOURS; } - + /** * Clear guest session data */ clearGuestSession(): void { this.storageService.removeItem(this.GUEST_TOKEN_KEY); this.storageService.removeItem(this.GUEST_ID_KEY); - + this.guestStateSignal.update(state => ({ ...state, session: null, @@ -221,14 +221,14 @@ export class GuestService { quizLimit: null })); } - + /** * Set loading state */ private setLoading(isLoading: boolean): void { this.guestStateSignal.update(state => ({ ...state, isLoading })); } - + /** * Set error state */ @@ -239,7 +239,7 @@ export class GuestService { error })); } - + /** * Check if guest has reached quiz limit */ @@ -248,24 +248,24 @@ export class GuestService { if (!limit) return false; return limit.quizzesRemaining <= 0; } - + /** * Get time remaining until session expires */ getTimeRemaining(): string { const session = this.guestState().session; if (!session) return '0h 0m'; - + const createdAt = new Date(session.createdAt); const expiryTime = new Date(createdAt.getTime() + (this.SESSION_EXPIRY_HOURS * 60 * 60 * 1000)); const now = new Date(); const diff = expiryTime.getTime() - now.getTime(); - + if (diff <= 0) return '0h 0m'; - + const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - + return `${hours}h ${minutes}m`; } } diff --git a/frontend/src/app/core/services/quiz.service.ts b/frontend/src/app/core/services/quiz.service.ts index 7037b19..33391b5 100644 --- a/frontend/src/app/core/services/quiz.service.ts +++ b/frontend/src/app/core/services/quiz.service.ts @@ -3,13 +3,14 @@ import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; import { Observable, tap, catchError, throwError, map } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { - QuizSession, - QuizStartRequest, +import { + QuizSession, + QuizStartRequest, QuizStartResponse, QuizAnswerSubmission, QuizAnswerResponse, - QuizResults + QuizResults, + QuizStartFormRequest } from '../models/quiz.model'; import { ToastService } from './toast.service'; import { StorageService } from './storage.service'; @@ -62,7 +63,7 @@ export class QuizService { /** * Start a new quiz session */ - startQuiz(request: QuizStartRequest): Observable { + startQuiz(request: QuizStartFormRequest): Observable { // Validate category accessibility if (!this.canAccessCategory(request.categoryId)) { this.toastService.error('You do not have access to this category'); @@ -87,13 +88,13 @@ export class QuizService { if (response.success) { // Store session data const session: QuizSession = { - id: response.sessionId, + id: response.data.sessionId, userId: this.storageService.getUserData()?.id, guestSessionId: this.guestService.guestState().session?.guestId, categoryId: request.categoryId, quizType: request.quizType || 'practice', difficulty: request.difficulty || 'mixed', - totalQuestions: response.totalQuestions, + totalQuestions: response.data.totalQuestions, currentQuestionIndex: 0, score: 0, correctAnswers: 0, @@ -104,15 +105,15 @@ export class QuizService { }; this._activeSession.set(session); - + // Store questions from response - if (response.questions) { - this._questions.set(response.questions); + if (response.data.questions) { + this._questions.set(response.data.questions); } - + // Store session ID for restoration - this.storeSessionId(response.sessionId); - + this.storeSessionId(response.data.sessionId); + this.toastService.success('Quiz started successfully!'); } }), @@ -139,11 +140,11 @@ export class QuizService { const updated: QuizSession = { ...currentSession, score: response.score, - correctAnswers: response.isCorrect - ? currentSession.correctAnswers + 1 + correctAnswers: response.isCorrect + ? currentSession.correctAnswers + 1 : currentSession.correctAnswers, - incorrectAnswers: !response.isCorrect - ? currentSession.incorrectAnswers + 1 + incorrectAnswers: !response.isCorrect + ? currentSession.incorrectAnswers + 1 : currentSession.incorrectAnswers, currentQuestionIndex: currentSession.currentQuestionIndex + 1 }; @@ -169,7 +170,7 @@ export class QuizService { tap(results => { if (results.success) { this._quizResults.set(results); - + // Update session status const currentSession = this._activeSession(); if (currentSession) { @@ -277,10 +278,10 @@ export class QuizService { tap(session => { // Store session ID in localStorage for future restoration localStorage.setItem('activeQuizSessionId', sessionId); - + // Check if we have questions stored const hasQuestions = this._questions().length > 0; - + if (!hasQuestions) { // Questions need to be fetched separately if not in memory // For now, we'll navigate to the quiz page which will handle loading diff --git a/frontend/src/app/core/services/storage.service.ts b/frontend/src/app/core/services/storage.service.ts index bf52fec..eefbd02 100644 --- a/frontend/src/app/core/services/storage.service.ts +++ b/frontend/src/app/core/services/storage.service.ts @@ -14,25 +14,21 @@ export class StorageService { private readonly THEME_KEY = 'app_theme'; private readonly REMEMBER_ME_KEY = 'remember_me'; - constructor() {} + constructor() { } /** * Get item from storage (checks localStorage first, then sessionStorage) */ - getItem(key: string): string | null { - return localStorage.getItem(key) || sessionStorage.getItem(key); + getItem(key: string): string | null { + return localStorage.getItem(key); } /** * Set item in storage * Uses localStorage if rememberMe is true, otherwise sessionStorage */ - setItem(key: string, value: string, persistent: boolean = true): void { - if (persistent) { - localStorage.setItem(key, value); - } else { - sessionStorage.setItem(key, value); - } + setItem(key: string, value: string, persistent: boolean = true): void { + localStorage.setItem(key, value); } // Auth Token Methods @@ -55,7 +51,7 @@ export class StorageService { } setGuestToken(token: string): void { - this.setItem(this.GUEST_TOKEN_KEY, token, true); + this.setItem(this.GUEST_TOKEN_KEY, token); } clearGuestToken(): void { @@ -118,6 +114,5 @@ export class StorageService { // Remove a specific item from storage removeItem(key: string): void { localStorage.removeItem(key); - sessionStorage.removeItem(key); } } diff --git a/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.html b/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.html index 82d2f42..684dc04 100644 --- a/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.html +++ b/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.html @@ -2,60 +2,56 @@
@if (isEditMode()) { -

- edit - Edit Question -

-

Update the details below to modify the quiz question

- @if (questionId()) { -

Question ID: {{ questionId() }}

- } +

+ edit + Edit Question +

+

Update the details below to modify the quiz question

+ @if (questionId()) { +

Question ID: {{ questionId() }}

+ } } @else { -

- add_circle - Create New Question -

-

Fill in the details below to create a new quiz question

+

+ add_circle + Create New Question +

+

Fill in the details below to create a new quiz question

}
@if (isLoadingQuestion()) { - - -
- hourglass_empty -

Loading question data...

-
-
-
+ + +
+ hourglass_empty +

Loading question data...

+
+
+
} @else { - - - -
- + + + + + @if (getFormError()) { -
- error - {{ getFormError() }} -
+
+ error + {{ getFormError() }} +
} Question Text - Minimum 10 characters @if (getErrorMessage('questionText')) { - {{ getErrorMessage('questionText') }} + {{ getErrorMessage('questionText') }} } @@ -65,13 +61,13 @@ Question Type @for (type of questionTypes; track type.value) { - - {{ type.label }} - + + {{ type.label }} + } @if (getErrorMessage('questionType')) { - {{ getErrorMessage('questionType') }} + {{ getErrorMessage('questionType') }} } @@ -79,17 +75,17 @@ Category @if (isLoadingCategories()) { - Loading categories... + Loading categories... } @else { - @for (category of categories(); track category.id) { - - {{ category.name }} - - } + @for (category of categories(); track category.id) { + + {{ category.name }} + + } } @if (getErrorMessage('categoryId')) { - {{ getErrorMessage('categoryId') }} + {{ getErrorMessage('categoryId') }} }
@@ -100,29 +96,22 @@ Difficulty @for (level of difficultyLevels; track level.value) { - - {{ level.label }} - + + {{ level.label }} + } @if (getErrorMessage('difficulty')) { - {{ getErrorMessage('difficulty') }} + {{ getErrorMessage('difficulty') }} } Points - + Between 1 and 100 @if (getErrorMessage('points')) { - {{ getErrorMessage('points') }} + {{ getErrorMessage('points') }} } @@ -131,109 +120,93 @@ @if (showOptions()) { -
-

- list - Answer Options -

+
+

+ list + Answer Options +

-
- @for (option of optionsArray.controls; track $index) { -
- Option {{ $index + 1 }} - - - - @if (optionsArray.length > 2) { - - } -
+
+ @for (option of optionsArray.controls; track $index) { +
+ Option {{ $index + 1 }} + + + + @if (optionsArray.length > 2) { + }
- - @if (optionsArray.length < 10) { - }
- + @if (optionsArray.length < 10) { + } +
- -
-

- check_circle - Correct Answer -

- - Select Correct Answer - - @for (optionText of getOptionTexts(); track $index) { - - {{ optionText }} - - } - - @if (getErrorMessage('correctAnswer')) { - {{ getErrorMessage('correctAnswer') }} + + + +
+

+ check_circle + Correct Answer +

+ + Select Correct Answer + + @for (optionText of getOptionTexts(); track $index) { + + {{ optionText }} + } - -
+ + @if (getErrorMessage('correctAnswer')) { + {{ getErrorMessage('correctAnswer') }} + } +
+
} @if (showTrueFalse()) { -
-

- check_circle - Correct Answer -

- - True - False - -
+
+

+ check_circle + Correct Answer +

+ + True + False + +
} @if (selectedQuestionType() === 'written') { -
-

- edit - Sample Correct Answer -

- - Expected Answer - - This is a reference answer for grading - @if (getErrorMessage('correctAnswer')) { - {{ getErrorMessage('correctAnswer') }} - } - -
+ This is a reference answer for grading + @if (getErrorMessage('correctAnswer')) { + {{ getErrorMessage('correctAnswer') }} + } + +
} @@ -241,16 +214,12 @@ Explanation - Minimum 10 characters @if (getErrorMessage('explanation')) { - {{ getErrorMessage('explanation') }} + {{ getErrorMessage('explanation') }} } @@ -264,19 +233,16 @@ Add Tags @for (tag of tagsArray; track tag) { - - {{ tag }} - - + + {{ tag }} + + } - + Press Enter or comma to add tags
@@ -293,29 +259,23 @@
- -
@@ -347,7 +307,8 @@ {{ questionForm.get('questionType')?.value | titlecase }} - + {{ questionForm.get('difficulty')?.value | titlecase }} @@ -357,56 +318,59 @@ @if (showOptions() && getOptionTexts().length > 0) { -
-
Options:
-
- @for (optionText of getOptionTexts(); track $index) { -
- {{ questionForm.get('correctAnswer')?.value === optionText ? 'check_circle' : 'radio_button_unchecked' }} - {{ optionText }} -
- } +
+
Options:
+
+ @for (optionText of getOptionTexts(); track $index) { +
+ {{ questionForm.get('correctAnswer')?.value === optionText ? 'check_circle' : + 'radio_button_unchecked' }} + {{ optionText }}
+ }
+
} @if (showTrueFalse()) { -
-
Options:
-
-
- {{ questionForm.get('correctAnswer')?.value === 'true' ? 'check_circle' : 'radio_button_unchecked' }} - True -
-
- {{ questionForm.get('correctAnswer')?.value === 'false' ? 'check_circle' : 'radio_button_unchecked' }} - False -
+
+
Options:
+
+
+ {{ questionForm.get('correctAnswer')?.value === 'true' ? 'check_circle' : + 'radio_button_unchecked' }} + True +
+
+ {{ questionForm.get('correctAnswer')?.value === 'false' ? 'check_circle' : + 'radio_button_unchecked' }} + False
+
} @if (questionForm.get('explanation')?.value) { -
-
Explanation:
-
- {{ questionForm.get('explanation')?.value }} -
+
+
Explanation:
+
+ {{ questionForm.get('explanation')?.value }}
+
} @if (tagsArray.length > 0) { -
-
Tags:
-
- @for (tag of tagsArray; track tag) { - {{ tag }} - } -
+
+
Tags:
+
+ @for (tag of tagsArray; track tag) { + {{ tag }} + }
+
} @@ -414,12 +378,12 @@
Access:
@if (questionForm.get('isPublic')?.value) { - Public + Public } @else { - Private + Private } @if (questionForm.get('isGuestAccessible')?.value) { - Guest Accessible + Guest Accessible }
@@ -427,4 +391,4 @@
-
+
\ No newline at end of file diff --git a/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.ts b/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.ts index ff3abdf..5b54566 100644 --- a/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.ts +++ b/frontend/src/app/features/admin/admin-question-form/admin-question-form.component.ts @@ -102,12 +102,12 @@ export class AdminQuestionFormComponent implements OnInit { readonly showOptions = computed(() => { const type = this.selectedQuestionType(); - return type === 'multiple_choice'; + return type === 'multiple'; }); readonly showTrueFalse = computed(() => { const type = this.selectedQuestionType(); - return type === 'true_false'; + return type === 'trueFalse'; }); readonly isFormValid = computed(() => { @@ -147,17 +147,17 @@ export class AdminQuestionFormComponent implements OnInit { this.isLoadingQuestion.set(true); this.adminService.getQuestion(id).subscribe({ - next: (response) => { - this.isLoadingQuestion.set(false); - this.populateForm(response.data); - }, - error: (error) => { - this.isLoadingQuestion.set(false); - console.error('Error loading question:', error); - // Redirect back if question not found - this.router.navigate(['/admin/questions']); - } - }); + next: (response) => { + this.isLoadingQuestion.set(false); + this.populateForm(response.data); + }, + error: (error) => { + this.isLoadingQuestion.set(false); + console.error('Error loading question:', error); + // Redirect back if question not found + this.router.navigate(['/admin/questions']); + } + }); } /** @@ -182,8 +182,8 @@ export class AdminQuestionFormComponent implements OnInit { }); // Populate options for multiple choice - if (question.questionType === 'multiple_choice' && question.options) { - question.options.forEach((option: string) => { + if (question.questionType === 'multiple' && question.options) { + question.options.forEach((option: string | { text: string, id: string }) => { this.optionsArray.push(this.createOption(option)); }); } @@ -222,7 +222,7 @@ export class AdminQuestionFormComponent implements OnInit { /** * Create option form control */ - private createOption(value: string = ''): FormGroup { + private createOption(value: string | { text: string, id: string } = ''): FormGroup { return this.fb.group({ text: [value, Validators.required] }); @@ -247,14 +247,14 @@ export class AdminQuestionFormComponent implements OnInit { */ private onQuestionTypeChange(type: QuestionType): void { const correctAnswerControl = this.questionForm.get('correctAnswer'); - - if (type === 'multiple_choice') { + + if (type === 'multiple') { // Ensure at least 2 options while (this.optionsArray.length < 2) { this.addOption(); } correctAnswerControl?.setValidators([Validators.required]); - } else if (type === 'true_false') { + } else if (type === 'trueFalse') { // Clear options for True/False this.optionsArray.clear(); correctAnswerControl?.setValidators([Validators.required]); @@ -287,7 +287,7 @@ export class AdminQuestionFormComponent implements OnInit { removeOption(index: number): void { if (this.optionsArray.length > 2) { this.optionsArray.removeAt(index); - + // Clear correct answer if it matches the removed option const correctAnswer = this.questionForm.get('correctAnswer')?.value; const removedOption = this.optionsArray.at(index)?.get('text')?.value; @@ -336,7 +336,7 @@ export class AdminQuestionFormComponent implements OnInit { if (questionType === 'multiple_choice' && correctAnswer && options) { const optionTexts = options.controls.map(opt => opt.get('text')?.value); const isValid = optionTexts.includes(correctAnswer); - + if (!isValid) { return { correctAnswerMismatch: true }; } @@ -388,15 +388,15 @@ export class AdminQuestionFormComponent implements OnInit { : this.adminService.createQuestion(questionData); serviceCall.subscribe({ - next: (response) => { - this.isSubmitting.set(false); - this.router.navigate(['/admin/questions']); - }, - error: (error) => { - this.isSubmitting.set(false); - console.error(`Error ${this.isEditMode() ? 'updating' : 'creating'} question:`, error); - } - }); + next: (response) => { + this.isSubmitting.set(false); + this.router.navigate(['/admin/questions']); + }, + error: (error) => { + this.isSubmitting.set(false); + console.error(`Error ${this.isEditMode() ? 'updating' : 'creating'} question:`, error); + } + }); } /** @@ -425,7 +425,7 @@ export class AdminQuestionFormComponent implements OnInit { */ getErrorMessage(fieldName: string): string { const control = this.questionForm.get(fieldName); - + if (!control || !control.errors || !control.touched) { return ''; } diff --git a/frontend/src/app/features/auth/login/login.ts b/frontend/src/app/features/auth/login/login.ts index 238ebe7..052d45b 100644 --- a/frontend/src/app/features/auth/login/login.ts +++ b/frontend/src/app/features/auth/login/login.ts @@ -13,6 +13,7 @@ import { MatDividerModule } from '@angular/material/divider'; import { AuthService } from '../../../core/services/auth.service'; import { GuestService } from '../../../core/services/guest.service'; import { Subject, takeUntil } from 'rxjs'; +import { StorageService } from '../../../core/services'; @Component({ selector: 'app-login', @@ -36,19 +37,20 @@ export class LoginComponent implements OnDestroy { private fb = inject(FormBuilder); private authService = inject(AuthService); private guestService = inject(GuestService); + private storageService = inject(StorageService); private router = inject(Router); private route = inject(ActivatedRoute); private destroy$ = new Subject(); - + // Signals isSubmitting = signal(false); hidePassword = signal(true); returnUrl = signal('/categories'); isStartingGuestSession = signal(false); - + // Form loginForm: FormGroup; - + constructor() { // Initialize form this.loginForm = this.fb.group({ @@ -56,27 +58,27 @@ export class LoginComponent implements OnDestroy { password: ['', [Validators.required, Validators.minLength(8)]], rememberMe: [false] }); - + // Get return URL from query params this.route.queryParams .pipe(takeUntil(this.destroy$)) .subscribe(params => { this.returnUrl.set(params['returnUrl'] || '/categories'); }); - + // Redirect if already authenticated if (this.authService.isAuthenticated()) { this.router.navigate(['/categories']); } } - + /** * Toggle password visibility */ togglePasswordVisibility(): void { this.hidePassword.update(val => !val); } - + /** * Submit login form */ @@ -85,11 +87,11 @@ export class LoginComponent implements OnDestroy { this.loginForm.markAllAsTouched(); return; } - + this.isSubmitting.set(true); - + const { email, password, rememberMe } = this.loginForm.value; - + this.authService.login(email, password, rememberMe, this.returnUrl()) .pipe(takeUntil(this.destroy$)) .subscribe({ @@ -102,33 +104,33 @@ export class LoginComponent implements OnDestroy { } }); } - + /** * Get form control error message */ getErrorMessage(controlName: string): string { const control = this.loginForm.get(controlName); - + if (!control || !control.touched) { return ''; } - + if (control.hasError('required')) { return `${this.getFieldLabel(controlName)} is required`; } - + if (control.hasError('email')) { return 'Please enter a valid email address'; } - + if (control.hasError('minlength')) { const minLength = control.getError('minlength').requiredLength; return `Must be at least ${minLength} characters`; } - + return ''; } - + /** * Get field label */ @@ -139,7 +141,7 @@ export class LoginComponent implements OnDestroy { }; return labels[controlName] || controlName; } - + /** * Start guest session */ @@ -148,7 +150,7 @@ export class LoginComponent implements OnDestroy { this.guestService.startSession() .pipe(takeUntil(this.destroy$)) .subscribe({ - next: () => { + next: (res: {}) => { this.isStartingGuestSession.set(false); this.router.navigate(['/guest-welcome']); }, diff --git a/frontend/src/app/features/history/quiz-history.component.html b/frontend/src/app/features/history/quiz-history.component.html index 1dd9c91..5dd70e3 100644 --- a/frontend/src/app/features/history/quiz-history.component.html +++ b/frontend/src/app/features/history/quiz-history.component.html @@ -17,7 +17,7 @@
- +
@@ -32,7 +32,7 @@ Export CSV
- + @@ -46,7 +46,7 @@ - + Sort By @@ -54,14 +54,14 @@ Score (Highest First) - +
- +
quiz @@ -72,11 +72,11 @@ Start a Quiz
- + - + @@ -84,74 +84,66 @@ {{ formatDate(session.completedAt || session.startedAt) }} - + - + - + - + - + - +
Date Category - {{ session.categoryName || 'Unknown' }} + {{ session.category.name || 'Unknown' }} Score - {{ session.score }}/{{ session.totalQuestions }} - ({{ ((session.score / session.totalQuestions) * 100).toFixed(0) }}%) + {{ session.score.earned }}/{{ session.questions.total }} + ({{ ((session.score.earned / session.questions.total) * 100).toFixed(0) }}%) Time Spent - {{ formatDuration(session.timeSpent) }} + {{ formatDuration(session.time.spent) }} Status - {{ session.status === 'in_progress' ? 'In Progress' : - session.status === 'completed' ? 'Completed' : - 'Abandoned' }} + {{ session.status === 'in_progress' ? 'In Progress' : + session.status === 'completed' ? 'Completed' : + 'Abandoned' }} Actions - -
- +
@@ -159,15 +151,15 @@
quiz - {{ session.categoryName || 'Unknown' }} + {{ session.category?.name || 'Unknown' }}
- {{ session.status === 'in_progress' ? 'In Progress' : - session.status === 'completed' ? 'Completed' : - 'Abandoned' }} + {{ session.status === 'in_progress' ? 'In Progress' : + session.status === 'completed' ? 'Completed' : + 'Abandoned' }}
- +
calendar_today @@ -175,17 +167,17 @@
timer - {{ formatDuration(session.timeSpent) }} + {{ formatDuration(session.time.spent) }}
Score: - - {{ session.score }}/{{ session.totalQuestions }} - ({{ ((session.score / session.totalQuestions) * 100).toFixed(0) }}%) + + {{ session.score.earned }}/{{ session.questions.total }} + ({{ ((session.score.earned / session.questions.total) * 100).toFixed(0) }}%)
- +
- + - + -
+
\ No newline at end of file diff --git a/frontend/src/app/features/history/quiz-history.component.ts b/frontend/src/app/features/history/quiz-history.component.ts index d1e2420..068316e 100644 --- a/frontend/src/app/features/history/quiz-history.component.ts +++ b/frontend/src/app/features/history/quiz-history.component.ts @@ -15,7 +15,7 @@ import { UserService } from '../../core/services/user.service'; import { AuthService } from '../../core/services/auth.service'; import { CategoryService } from '../../core/services/category.service'; import { QuizHistoryResponse, PaginationInfo } from '../../core/models/dashboard.model'; -import { QuizSession } from '../../core/models/quiz.model'; +import { QuizSession, QuizSessionHistory } from '../../core/models/quiz.model'; import { Category } from '../../core/models/category.model'; @Component({ @@ -44,32 +44,32 @@ export class QuizHistoryComponent implements OnInit { private categoryService = inject(CategoryService); private router = inject(Router); private route = inject(ActivatedRoute); - + // Signals isLoading = signal(true); - history = signal([]); + history = signal([]); pagination = signal(null); categories = signal([]); error = signal(null); - + // Filter and sort state currentPage = signal(1); pageSize = signal(10); selectedCategory = signal(null); sortBy = signal<'date' | 'score'>('date'); - + // Table columns displayedColumns: string[] = ['date', 'category', 'score', 'time', 'status', 'actions']; - + // Computed values isEmpty = computed(() => this.history().length === 0 && !this.isLoading()); totalItems = computed(() => this.pagination()?.totalItems || 0); - + ngOnInit(): void { this.loadCategories(); this.loadHistoryFromRoute(); } - + /** * Load categories for filter */ @@ -83,7 +83,7 @@ export class QuizHistoryComponent implements OnInit { } }); } - + /** * Load history based on route query params */ @@ -93,31 +93,31 @@ export class QuizHistoryComponent implements OnInit { const limit = params['limit'] ? parseInt(params['limit'], 10) : 10; const category = params['category'] || null; const sortBy = params['sortBy'] || 'date'; - + this.currentPage.set(page); this.pageSize.set(limit); this.selectedCategory.set(category); this.sortBy.set(sortBy); - + this.loadHistory(); }); } - + /** * Load quiz history */ loadHistory(): void { const state: any = (this.authService as any).authState(); const user = state?.user; - + if (!user || !user.id) { this.router.navigate(['/login']); return; } - + this.isLoading.set(true); this.error.set(null); - + (this.userService as any).getHistory( user.id, this.currentPage(), @@ -126,8 +126,8 @@ export class QuizHistoryComponent implements OnInit { this.sortBy() ).subscribe({ next: (response: QuizHistoryResponse) => { - this.history.set(response.sessions || []); - this.pagination.set(response.pagination); + this.history.set(response.data.sessions || []); + this.pagination.set(response.data.pagination); this.isLoading.set(false); }, error: (err: any) => { @@ -137,7 +137,7 @@ export class QuizHistoryComponent implements OnInit { } }); } - + /** * Handle page change */ @@ -146,7 +146,7 @@ export class QuizHistoryComponent implements OnInit { this.pageSize.set(event.pageSize); this.updateUrlAndLoad(); } - + /** * Handle category filter change */ @@ -155,7 +155,7 @@ export class QuizHistoryComponent implements OnInit { this.currentPage.set(1); // Reset to first page this.updateUrlAndLoad(); } - + /** * Handle sort change */ @@ -164,7 +164,7 @@ export class QuizHistoryComponent implements OnInit { this.currentPage.set(1); // Reset to first page this.updateUrlAndLoad(); } - + /** * Update URL with query params and reload data */ @@ -174,18 +174,18 @@ export class QuizHistoryComponent implements OnInit { limit: this.pageSize(), sortBy: this.sortBy() }; - + if (this.selectedCategory()) { queryParams.category = this.selectedCategory(); } - + this.router.navigate([], { relativeTo: this.route, queryParams, queryParamsHandling: 'merge' }); } - + /** * View quiz results */ @@ -194,7 +194,7 @@ export class QuizHistoryComponent implements OnInit { this.router.navigate(['/quiz', sessionId, 'results']); } } - + /** * Review quiz */ @@ -203,13 +203,13 @@ export class QuizHistoryComponent implements OnInit { this.router.navigate(['/quiz', sessionId, 'review']); } } - + /** * Format date */ formatDate(dateString: string | undefined): string { if (!dateString) return 'N/A'; - + const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', @@ -219,23 +219,23 @@ export class QuizHistoryComponent implements OnInit { minute: '2-digit' }); } - + /** * Format duration */ formatDuration(seconds: number | undefined): string { if (!seconds) return '0s'; - + const minutes = Math.floor(seconds / 60); const secs = seconds % 60; - + if (minutes === 0) { return `${secs}s`; } - + return `${minutes}m ${secs}s`; } - + /** * Get score color */ @@ -245,7 +245,7 @@ export class QuizHistoryComponent implements OnInit { if (percentage >= 60) return 'warning'; return 'error'; } - + /** * Get status badge class */ @@ -261,7 +261,7 @@ export class QuizHistoryComponent implements OnInit { return ''; } } - + /** * Export to CSV */ @@ -269,32 +269,32 @@ export class QuizHistoryComponent implements OnInit { if (this.history().length === 0) { return; } - + // Create CSV header const headers = ['Date', 'Category', 'Score', 'Total Questions', 'Percentage', 'Time Spent', 'Status']; const csvRows = [headers.join(',')]; - + // Add data rows this.history().forEach(session => { - const percentage = ((session.score / session.totalQuestions) * 100).toFixed(2); + const percentage = ((session.score.earned / session.questions.total) * 100).toFixed(2); const row = [ this.formatDate(session.completedAt || session.startedAt), - session.categoryName || 'Unknown', - session.score.toString(), - session.totalQuestions.toString(), + session.category?.name || 'Unknown', + session.score.earned.toString(), + session.questions.total.toString(), `${percentage}%`, - this.formatDuration(session.timeSpent), + this.formatDuration(session.time.spent), session.status ]; csvRows.push(row.join(',')); }); - + // Create blob and download const csvContent = csvRows.join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); - + link.setAttribute('href', url); link.setAttribute('download', `quiz-history-${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = 'hidden'; @@ -302,7 +302,7 @@ export class QuizHistoryComponent implements OnInit { link.click(); document.body.removeChild(link); } - + /** * Refresh history */ diff --git a/frontend/src/app/features/quiz/quiz-question/quiz-question.html b/frontend/src/app/features/quiz/quiz-question/quiz-question.html index 1d62d37..5932abf 100644 --- a/frontend/src/app/features/quiz/quiz-question/quiz-question.html +++ b/frontend/src/app/features/quiz/quiz-question/quiz-question.html @@ -6,187 +6,164 @@ Question {{ currentQuestionIndex() + 1 }} of {{ totalQuestions() }} @if (activeSession()?.quizType === 'timed') { -
- timer - {{ formatTime(timeRemaining()) }} -
+
+ timer + {{ formatTime(timeRemaining()) }} +
}
stars Score: {{ currentScore() }}
- +
@if (currentQuestion(); as question) { - - -
-
- {{ questionTypeLabel() }} - - {{ question.difficulty | titlecase }} - - {{ question.points }} points -
+ + +
+
+ {{ questionTypeLabel() }} + + {{ question.difficulty | titlecase }} + + {{ question.points }} points
- +
+
- + - - -
-

{{ question.questionText }}

+ + +
+

{{ question.questionText }}

+
+ + + + + + @if (question.questionType === 'multiple' && question.options) { + + @for (option of question.options; track option) { + + {{ typeof option === 'string' ? option : option.text }} + + } + + } + + + @if (isTrueFalse()) { +
+ +
+ } - - - - - @if (isMultipleChoice() && question.options) { - - @for (option of question.options; track option) { - - {{ option }} - - } - - } - - - @if (isTrueFalse()) { -
- - -
- } - - - @if (isWritten()) { - - Your Answer - - Be as detailed as possible - - } + Be as detailed as possible + + } - - @if (answerSubmitted() && answerResult()) { -
- - - @if (!answerResult()?.isCorrect) { -
- Correct Answer: -

{{ answerResult()?.correctAnswer }}

-
- } - - @if (answerResult()?.explanation) { -
- Explanation: -

{{ answerResult()?.explanation }}

-
- } - -
- stars - Points earned: {{ answerResult()?.points }} -
-
- } - - -
- @if (!answerSubmitted()) { - - } @else { - - } + + @if (answerSubmitted() && answerResult()) { +
+ - - + + @if (!answerResult()?.isCorrect) { +
+ Correct Answer: +

{{ answerResult()?.correctAnswer }}

+
+ } + + @if (answerResult()?.explanation) { +
+ Explanation: +

{{ answerResult()?.explanation }}

+
+ } + +
+ stars + Points earned: {{ answerResult()?.points }} +
+
+ } + + {{this.answerForm?.valid }} + {{ !this.answerSubmitted()}} + {{ !this.isSubmittingAnswer()}} + +
+ @if (!answerSubmitted()) { + + } @else { + + } +
+ + } @else { - - - -

Loading question...

-
+ + + +

Loading question...

+
} @@ -208,4 +185,4 @@
-
+ \ No newline at end of file diff --git a/frontend/src/app/features/quiz/quiz-question/quiz-question.ts b/frontend/src/app/features/quiz/quiz-question/quiz-question.ts index 985bc86..ade4cb4 100644 --- a/frontend/src/app/features/quiz/quiz-question/quiz-question.ts +++ b/frontend/src/app/features/quiz/quiz-question/quiz-question.ts @@ -56,21 +56,21 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { 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(); @@ -93,8 +93,8 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { readonly questionTypeLabel = computed(() => { const type = this.currentQuestion()?.questionType; switch (type) { - case 'multiple_choice': return 'Multiple Choice'; - case 'true_false': return 'True/False'; + case 'multiple': return 'Multiple Choice'; + case 'trueFalse': return 'True/False'; case 'written': return 'Written Answer'; default: return ''; } @@ -102,7 +102,7 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { ngOnInit(): void { this.sessionId = this.route.snapshot.params['sessionId']; - + if (!this.sessionId) { this.router.navigate(['/quiz/setup']); return; @@ -110,6 +110,8 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { this.initForm(); this.loadQuizSession(); + console.log(this.questions()); + } ngOnDestroy(): void { @@ -134,16 +136,16 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { // 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); @@ -212,7 +214,7 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { private loadCurrentQuestion(): void { const index = this.currentQuestionIndex(); const allQuestions = this.questions(); - + if (index < allQuestions.length) { this.currentQuestion.set(allQuestions[index]); this.answerSubmitted.set(false); @@ -264,7 +266,7 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { if (!question) return; const answer = this.answerForm.get('answer')?.value; - + const submission: QuizAnswerSubmission = { questionId: question.id, answer: answer, @@ -359,14 +361,14 @@ export class QuizQuestionComponent implements OnInit, OnDestroy { * Check if answer is multiple choice */ isMultipleChoice(): boolean { - return this.currentQuestion()?.questionType === 'multiple_choice'; + return this.currentQuestion()?.questionType === 'multiple'; } /** * Check if answer is true/false */ isTrueFalse(): boolean { - return this.currentQuestion()?.questionType === 'true_false'; + return this.currentQuestion()?.questionType === 'trueFalse'; } /** diff --git a/frontend/src/app/features/quiz/quiz-setup/quiz-setup.html b/frontend/src/app/features/quiz/quiz-setup/quiz-setup.html index 06433b6..b84a04e 100644 --- a/frontend/src/app/features/quiz/quiz-setup/quiz-setup.html +++ b/frontend/src/app/features/quiz/quiz-setup/quiz-setup.html @@ -13,191 +13,176 @@ @if (showGuestWarning()) { -
- warning -
-

Limited Quizzes Remaining

-

You have {{ remainingQuizzes() }} quiz(es) left as a guest.

- -
+
+ warning +
+

Limited Quizzes Remaining

+

You have {{ remainingQuizzes() }} quiz(es) left as a guest.

+
+
} @if (isLoadingCategories()) { -
- -

Loading categories...

-
+
+ +

Loading categories...

+
} @if (!isLoadingCategories()) { -
- - -
-

- category - Select Category -

- - Choose a category - - @for (category of getAvailableCategories(); track category.id) { - -
- @if (category.icon) { - {{ category.icon }} - } - {{ category.name }} - ({{ category.questionCount }} questions) -
-
- } -
- @if (setupForm.get('categoryId')?.hasError('required') && setupForm.get('categoryId')?.touched) { - Please select a category - } -
+ - @if (selectedCategory()) { -
- {{ selectedCategory()?.icon }} -
-

{{ selectedCategory()?.name }}

-

{{ selectedCategory()?.description }}

+ +
+

+ category + Select Category +

+ + Choose a category + + @for (category of getAvailableCategories(); track category.id) { + +
+ @if (category.icon) { + {{ category.icon }} + } + {{ category.name }} + ({{ category.questionCount }} questions)
-
+ + } + + @if (setupForm.get('categoryId')?.hasError('required') && setupForm.get('categoryId')?.touched) { + Please select a category + } + + + @if (selectedCategory()) { +
+ {{ selectedCategory()?.icon }} +
+

{{ selectedCategory()?.name }}

+

{{ selectedCategory()?.description }}

+
+
+ } +
+ + +
+

+ format_list_numbered + Number of Questions +

+
+ @for (count of questionCountOptions; track count) { + }
+

Selected: {{ setupForm.get('questionCount')?.value }} questions

+
- -
-

- format_list_numbered - Number of Questions -

-
- @for (count of questionCountOptions; track count) { - - } -
-

Selected: {{ setupForm.get('questionCount')?.value }} questions

+ +
+

+ tune + Difficulty Level +

+
+ @for (difficulty of difficultyOptions; track difficulty.value) { + + }
+
- -
-

- tune - Difficulty Level -

-
- @for (difficulty of difficultyOptions; track difficulty.value) { - - } -
-
- - -
-

- mode - Quiz Mode -

-
- @for (type of quizTypeOptions; track type.value) { - - {{ type.icon }} -

{{ type.label }}

-

{{ type.description }}

-
- } -
-
- - -
- -

- info - Quiz Summary -

-
-
- Category: - {{ selectedCategory()?.name || 'Not selected' }} -
-
- Questions: - {{ setupForm.get('questionCount')?.value }} -
-
- Difficulty: - - {{ getSelectedDifficultyLabel() }} - -
-
- Mode: - - {{ getSelectedQuizTypeLabel() }} - -
-
- Estimated Time: - ~{{ estimatedTime() }} minutes -
-
+ +
+

+ mode + Quiz Mode +

+
+ @for (type of quizTypeOptions; track type.value) { + + {{ type.icon }} +

{{ type.label }}

+

{{ type.description }}

+ }
+
- -
- - -
- + +
+ +

+ info + Quiz Summary +

+
+
+ Category: + {{ selectedCategory()?.name || 'Not selected' }} +
+
+ Questions: + {{ setupForm.get('questionCount')?.value }} +
+
+ Difficulty: + + {{ getSelectedDifficultyLabel() }} + +
+
+ Mode: + + {{ getSelectedQuizTypeLabel() }} + +
+
+ Estimated Time: + ~{{ estimatedTime() }} minutes +
+
+
+
+ + +
+ + +
+ }
-
+
\ No newline at end of file diff --git a/frontend/src/app/features/quiz/quiz-setup/quiz-setup.ts b/frontend/src/app/features/quiz/quiz-setup/quiz-setup.ts index a020082..6ab8bbe 100644 --- a/frontend/src/app/features/quiz/quiz-setup/quiz-setup.ts +++ b/frontend/src/app/features/quiz/quiz-setup/quiz-setup.ts @@ -17,7 +17,7 @@ import { CategoryService } from '../../../core/services/category.service'; import { GuestService } from '../../../core/services/guest.service'; import { StorageService } from '../../../core/services/storage.service'; import { Category } from '../../../core/models/category.model'; -import { QuizStartRequest } from '../../../core/models/quiz.model'; +import { QuizStartFormRequest, QuizStartRequest } from '../../../core/models/quiz.model'; @Component({ selector: 'app-quiz-setup', @@ -56,7 +56,7 @@ export class QuizSetupComponent implements OnInit, OnDestroy { readonly categories = this.categoryService.categories; readonly isLoadingCategories = this.categoryService.isLoading; readonly isStartingQuiz = this.quizService.isStartingQuiz; - + // Guest limit readonly isGuest = computed(() => !this.storageService.isAuthenticated()); readonly guestState = this.guestService.guestState; @@ -68,7 +68,7 @@ export class QuizSetupComponent implements OnInit, OnDestroy { // Question count options readonly questionCountOptions = [5, 10, 15, 20]; - + // Difficulty options readonly difficultyOptions = [ { value: 'easy', label: 'Easy', icon: 'sentiment_satisfied', color: '#4CAF50' }, @@ -79,15 +79,15 @@ export class QuizSetupComponent implements OnInit, OnDestroy { // Quiz type options readonly quizTypeOptions = [ - { - value: 'practice', - label: 'Practice Mode', + { + value: 'practice', + label: 'Practice Mode', icon: 'school', description: 'No time limit, learn at your own pace' }, - { - value: 'timed', - label: 'Timed Mode', + { + value: 'timed', + label: 'Timed Mode', icon: 'timer', description: 'Challenge yourself with time constraints' } @@ -125,7 +125,7 @@ export class QuizSetupComponent implements OnInit, OnDestroy { */ private initForm(): void { this.setupForm = this.fb.group({ - categoryId: ['', Validators.required], + categoryId: [null, Validators.required], questionCount: [10, [Validators.required, Validators.min(5), Validators.max(20)]], difficulty: ['mixed', Validators.required], quizType: ['practice', Validators.required] @@ -160,7 +160,7 @@ export class QuizSetupComponent implements OnInit, OnDestroy { } const formValue = this.setupForm.value; - const request: QuizStartRequest = { + const request: QuizStartFormRequest = { categoryId: formValue.categoryId, questionCount: formValue.questionCount, difficulty: formValue.difficulty, @@ -173,7 +173,7 @@ export class QuizSetupComponent implements OnInit, OnDestroy { next: (response) => { if (response.success) { // Navigate to quiz page - this.router.navigate(['/quiz', response.sessionId]); + this.router.navigate(['/quiz', response.data.sessionId]); } }, error: (error) => { @@ -187,12 +187,12 @@ export class QuizSetupComponent implements OnInit, OnDestroy { */ getAvailableCategories(): Category[] { const allCategories = this.categories() || []; - + if (this.isGuest()) { // Filter to show only guest-accessible categories return allCategories.filter(cat => cat.guestAccessible); } - + return allCategories; } diff --git a/frontend/src/app/shared/components/resume-quiz-dialog/resume-quiz-dialog.html b/frontend/src/app/shared/components/resume-quiz-dialog/resume-quiz-dialog.html index f381dbe..8e9888d 100644 --- a/frontend/src/app/shared/components/resume-quiz-dialog/resume-quiz-dialog.html +++ b/frontend/src/app/shared/components/resume-quiz-dialog/resume-quiz-dialog.html @@ -25,11 +25,8 @@
- + {{ progress() }}% Complete
@@ -76,32 +73,23 @@
@if (session().score > 0) { -
- emoji_events - Current Score: {{ session().score }} points -
+
+ emoji_events + Current Score: {{ session().score }} points +
}
- - - + \ No newline at end of file