From efb4f69e2057a726b1cc8284d90c54c1550e80bb Mon Sep 17 00:00:00 2001 From: AD2025 Date: Thu, 25 Dec 2025 00:24:11 +0200 Subject: [PATCH] add changes --- backend/server.js | 14 +- backend/{ => tests}/check-categories.js | 12 +- backend/{ => tests}/check-category-ids.js | 14 +- backend/{ => tests}/check-questions.js | 8 +- backend/{ => tests}/drop-categories.js | 2 +- backend/{ => tests}/generate-jwt-secret.js | 0 backend/{ => tests}/get-category-mapping.js | 10 +- backend/{ => tests}/get-question-mapping.js | 4 +- .../test-admin-questions-pagination.js | 0 backend/{ => tests}/test-admin-statistics.js | 0 .../{ => tests}/test-admin-update-question.js | 0 backend/{ => tests}/test-auth-endpoints.js | 0 backend/{ => tests}/test-bookmarks.js | 0 backend/{ => tests}/test-category-admin.js | 0 backend/{ => tests}/test-category-details.js | 0 .../{ => tests}/test-category-endpoints.js | 0 backend/{ => tests}/test-category-model.js | 2 +- backend/{ => tests}/test-complete-quiz.js | 0 backend/{ => tests}/test-conversion-quick.js | 0 backend/{ => tests}/test-create-question.js | 0 backend/{ => tests}/test-db-connection.js | 8 +- backend/{ => tests}/test-error-handling.js | 0 backend/{ => tests}/test-find-by-pk.js | 10 +- backend/{ => tests}/test-guest-analytics.js | 0 backend/{ => tests}/test-guest-conversion.js | 0 backend/{ => tests}/test-guest-endpoints.js | 0 backend/{ => tests}/test-guest-quiz-limit.js | 24 +- .../{ => tests}/test-guest-session-model.js | 10 +- backend/{ => tests}/test-guest-settings.js | 0 backend/{ => tests}/test-junction-tables.js | 20 +- backend/{ => tests}/test-limit-reached.js | 6 +- backend/{ => tests}/test-logout-verify.js | 0 backend/{ => tests}/test-performance.js | 0 backend/{ => tests}/test-question-by-id.js | 0 backend/{ => tests}/test-question-model.js | 2 +- backend/{ => tests}/test-question-search.js | 0 .../{ => tests}/test-questions-by-category.js | 0 backend/{ => tests}/test-quiz-history.js | 0 .../{ => tests}/test-quiz-session-model.js | 18 +- backend/{ => tests}/test-review-quiz.js | 0 backend/{ => tests}/test-security.js | 0 backend/{ => tests}/test-session-details.js | 0 backend/{ => tests}/test-simple-category.js | 0 backend/{ => tests}/test-start-quiz.js | 0 backend/{ => tests}/test-submit-answer.js | 0 .../test-update-delete-question.js | 0 backend/{ => tests}/test-update-profile.js | 0 backend/{ => tests}/test-user-bookmarks.js | 0 backend/{ => tests}/test-user-dashboard.js | 0 backend/{ => tests}/test-user-management.js | 0 backend/{ => tests}/test-user-model.js | 2 +- backend/{ => tests}/validate-env.js | 0 backend/{ => tests}/verify-seeded-data.js | 2 +- .../src/app/core/models/dashboard.model.ts | 109 +++- frontend/src/app/core/models/quiz.model.ts | 1 + .../src/app/core/services/quiz.service.ts | 12 +- .../src/app/core/services/user.service.ts | 62 +- .../dashboard/dashboard.component.html | 88 ++- .../features/dashboard/dashboard.component.ts | 94 +-- .../quiz/quiz-results/quiz-results.html | 570 ++++++++---------- .../quiz/quiz-results/quiz-results.scss | 6 +- .../quiz/quiz-results/quiz-results.ts | 22 +- .../quiz/quiz-review/quiz-review.html | 4 +- .../features/quiz/quiz-review/quiz-review.ts | 8 - 64 files changed, 576 insertions(+), 568 deletions(-) rename backend/{ => tests}/check-categories.js (92%) rename backend/{ => tests}/check-category-ids.js (93%) rename backend/{ => tests}/check-questions.js (94%) rename backend/{ => tests}/drop-categories.js (93%) rename backend/{ => tests}/generate-jwt-secret.js (100%) rename backend/{ => tests}/get-category-mapping.js (94%) rename backend/{ => tests}/get-question-mapping.js (95%) rename backend/{ => tests}/test-admin-questions-pagination.js (100%) rename backend/{ => tests}/test-admin-statistics.js (100%) rename backend/{ => tests}/test-admin-update-question.js (100%) rename backend/{ => tests}/test-auth-endpoints.js (100%) rename backend/{ => tests}/test-bookmarks.js (100%) rename backend/{ => tests}/test-category-admin.js (100%) rename backend/{ => tests}/test-category-details.js (100%) rename backend/{ => tests}/test-category-endpoints.js (100%) rename backend/{ => tests}/test-category-model.js (99%) rename backend/{ => tests}/test-complete-quiz.js (100%) rename backend/{ => tests}/test-conversion-quick.js (100%) rename backend/{ => tests}/test-create-question.js (100%) rename backend/{ => tests}/test-db-connection.js (98%) rename backend/{ => tests}/test-error-handling.js (100%) rename backend/{ => tests}/test-find-by-pk.js (93%) rename backend/{ => tests}/test-guest-analytics.js (100%) rename backend/{ => tests}/test-guest-conversion.js (100%) rename backend/{ => tests}/test-guest-endpoints.js (100%) rename backend/{ => tests}/test-guest-quiz-limit.js (96%) rename backend/{ => tests}/test-guest-session-model.js (98%) rename backend/{ => tests}/test-guest-settings.js (100%) rename backend/{ => tests}/test-junction-tables.js (99%) rename backend/{ => tests}/test-limit-reached.js (97%) rename backend/{ => tests}/test-logout-verify.js (100%) rename backend/{ => tests}/test-performance.js (100%) rename backend/{ => tests}/test-question-by-id.js (100%) rename backend/{ => tests}/test-question-model.js (99%) rename backend/{ => tests}/test-question-search.js (100%) rename backend/{ => tests}/test-questions-by-category.js (100%) rename backend/{ => tests}/test-quiz-history.js (100%) rename backend/{ => tests}/test-quiz-session-model.js (98%) rename backend/{ => tests}/test-review-quiz.js (100%) rename backend/{ => tests}/test-security.js (100%) rename backend/{ => tests}/test-session-details.js (100%) rename backend/{ => tests}/test-simple-category.js (100%) rename backend/{ => tests}/test-start-quiz.js (100%) rename backend/{ => tests}/test-submit-answer.js (100%) rename backend/{ => tests}/test-update-delete-question.js (100%) rename backend/{ => tests}/test-update-profile.js (100%) rename backend/{ => tests}/test-user-bookmarks.js (100%) rename backend/{ => tests}/test-user-dashboard.js (100%) rename backend/{ => tests}/test-user-management.js (100%) rename backend/{ => tests}/test-user-model.js (99%) rename backend/{ => tests}/validate-env.js (100%) rename backend/{ => tests}/verify-seeded-data.js (98%) diff --git a/backend/server.js b/backend/server.js index 613c2ce..8e1cf59 100644 --- a/backend/server.js +++ b/backend/server.js @@ -7,7 +7,7 @@ const swaggerSpec = require('./config/swagger'); const logger = require('./config/logger'); const { errorHandler, notFoundHandler } = require('./middleware/errorHandler'); const { testConnection, getDatabaseStats } = require('./config/db'); -const { validateEnvironment } = require('./validate-env'); +const { validateEnvironment } = require('./tests/validate-env'); const { isRedisConnected } = require('./config/redis'); // Security middleware @@ -88,7 +88,7 @@ app.get('/api-docs.json', docsLimiter, (req, res) => { // Health check endpoint app.get('/health', async (req, res) => { const dbStats = await getDatabaseStats(); - + res.status(200).json({ status: 'OK', message: 'Interview Quiz API is running', @@ -133,9 +133,9 @@ app.use(errorHandler); // Start server app.listen(PORT, async () => { logger.info('Server starting up...'); - + const redisStatus = isRedisConnected() ? '✅ Connected' : '⚠️ Not Connected (Optional)'; - + console.log(` ╔════════════════════════════════════════╗ ║ Interview Quiz API - MySQL Edition ║ @@ -149,16 +149,16 @@ app.listen(PORT, async () => { 📝 Logs: backend/logs/ 💾 Cache (Redis): ${redisStatus} `); - + logger.info(`Server started successfully on port ${PORT}`); - + // Test database connection on startup console.log('🔌 Testing database connection...'); const connected = await testConnection(); if (!connected) { console.warn('⚠️ Warning: Database connection failed. Server is running but database operations will fail.'); } - + // Log Redis status if (isRedisConnected()) { console.log('💾 Redis cache connected and ready'); diff --git a/backend/check-categories.js b/backend/tests/check-categories.js similarity index 92% rename from backend/check-categories.js rename to backend/tests/check-categories.js index 44ceabc..fa3649b 100644 --- a/backend/check-categories.js +++ b/backend/tests/check-categories.js @@ -1,25 +1,25 @@ -const { Category } = require('./models'); +const { Category } = require('../models'); async function checkCategories() { const allActive = await Category.findAll({ where: { isActive: true }, order: [['displayOrder', 'ASC']] }); - + console.log(`\nTotal active categories: ${allActive.length}\n`); - + allActive.forEach(cat => { console.log(`${cat.displayOrder}. ${cat.name}`); console.log(` Guest Accessible: ${cat.guestAccessible}`); console.log(` Question Count: ${cat.questionCount}\n`); }); - + const guestOnly = allActive.filter(c => c.guestAccessible); const authOnly = allActive.filter(c => !c.guestAccessible); - + console.log(`Guest-accessible: ${guestOnly.length}`); console.log(`Auth-only: ${authOnly.length}`); - + process.exit(0); } diff --git a/backend/check-category-ids.js b/backend/tests/check-category-ids.js similarity index 93% rename from backend/check-category-ids.js rename to backend/tests/check-category-ids.js index 66453dc..1f3cb1f 100644 --- a/backend/check-category-ids.js +++ b/backend/tests/check-category-ids.js @@ -1,16 +1,16 @@ -const { Category } = require('./models'); +const { Category } = require('../models'); async function checkCategoryIds() { try { console.log('\n=== Checking Category IDs ===\n'); - + const categories = await Category.findAll({ attributes: ['id', 'name', 'isActive', 'guestAccessible'], limit: 10 }); - + console.log(`Found ${categories.length} categories:\n`); - + categories.forEach(cat => { console.log(`ID: ${cat.id} (${typeof cat.id})`); console.log(` Name: ${cat.name}`); @@ -18,16 +18,16 @@ async function checkCategoryIds() { console.log(` guestAccessible: ${cat.guestAccessible}`); console.log(''); }); - + // Try to find one by PK if (categories.length > 0) { const firstId = categories[0].id; console.log(`\nTrying findByPk with ID: ${firstId} (${typeof firstId})\n`); - + const found = await Category.findByPk(firstId); console.log('findByPk result:', found ? found.name : 'NOT FOUND'); } - + process.exit(0); } catch (error) { console.error('Error:', error); diff --git a/backend/check-questions.js b/backend/tests/check-questions.js similarity index 94% rename from backend/check-questions.js rename to backend/tests/check-questions.js index 3d82daf..2152324 100644 --- a/backend/check-questions.js +++ b/backend/tests/check-questions.js @@ -1,4 +1,4 @@ -const { Question, Category } = require('./models'); +const { Question, Category } = require('../models'); async function checkQuestions() { try { @@ -12,9 +12,9 @@ async function checkQuestions() { attributes: ['id', 'questionText', 'categoryId', 'difficulty'], limit: 10 }); - + console.log(`\nTotal active questions: ${questions.length}\n`); - + if (questions.length === 0) { console.log('❌ No questions found in database!'); console.log('\nYou need to run the questions seeder:'); @@ -27,7 +27,7 @@ async function checkQuestions() { console.log(` Category: ${q.category?.name || 'N/A'} | Difficulty: ${q.difficulty}`); }); } - + process.exit(0); } catch (error) { console.error('Error:', error); diff --git a/backend/drop-categories.js b/backend/tests/drop-categories.js similarity index 93% rename from backend/drop-categories.js rename to backend/tests/drop-categories.js index 9581ef3..7a58a76 100644 --- a/backend/drop-categories.js +++ b/backend/tests/drop-categories.js @@ -1,5 +1,5 @@ // Script to drop categories table -const { sequelize } = require('./models'); +const { sequelize } = require('../models'); async function dropCategoriesTable() { try { diff --git a/backend/generate-jwt-secret.js b/backend/tests/generate-jwt-secret.js similarity index 100% rename from backend/generate-jwt-secret.js rename to backend/tests/generate-jwt-secret.js diff --git a/backend/get-category-mapping.js b/backend/tests/get-category-mapping.js similarity index 94% rename from backend/get-category-mapping.js rename to backend/tests/get-category-mapping.js index 7537fb9..0a942eb 100644 --- a/backend/get-category-mapping.js +++ b/backend/tests/get-category-mapping.js @@ -1,4 +1,4 @@ -const { Category } = require('./models'); +const { Category } = require('../models'); async function getCategoryMapping() { try { @@ -7,9 +7,9 @@ async function getCategoryMapping() { attributes: ['id', 'name', 'slug', 'guestAccessible'], order: [['displayOrder', 'ASC']] }); - + console.log('\n=== Category ID Mapping ===\n'); - + const mapping = {}; categories.forEach(cat => { mapping[cat.slug] = { @@ -22,7 +22,7 @@ async function getCategoryMapping() { console.log(` Guest Accessible: ${cat.guestAccessible}`); console.log(''); }); - + // Export for use in tests console.log('\nFor tests, use:'); console.log('const CATEGORY_IDS = {'); @@ -30,7 +30,7 @@ async function getCategoryMapping() { console.log(` ${slug.toUpperCase().replace(/-/g, '_')}: '${mapping[slug].id}',`); }); console.log('};'); - + process.exit(0); } catch (error) { console.error('Error:', error); diff --git a/backend/get-question-mapping.js b/backend/tests/get-question-mapping.js similarity index 95% rename from backend/get-question-mapping.js rename to backend/tests/get-question-mapping.js index 6338277..5776a3b 100644 --- a/backend/get-question-mapping.js +++ b/backend/tests/get-question-mapping.js @@ -1,4 +1,4 @@ -const { Question, Category } = require('./models'); +const { Question, Category } = require('../models'); async function getQuestionMapping() { try { @@ -14,7 +14,7 @@ async function getQuestionMapping() { }); console.log('=== Question ID Mapping ===\n'); - + const mapping = {}; questions.forEach((q, index) => { const key = `QUESTION_${index + 1}`; diff --git a/backend/test-admin-questions-pagination.js b/backend/tests/test-admin-questions-pagination.js similarity index 100% rename from backend/test-admin-questions-pagination.js rename to backend/tests/test-admin-questions-pagination.js diff --git a/backend/test-admin-statistics.js b/backend/tests/test-admin-statistics.js similarity index 100% rename from backend/test-admin-statistics.js rename to backend/tests/test-admin-statistics.js diff --git a/backend/test-admin-update-question.js b/backend/tests/test-admin-update-question.js similarity index 100% rename from backend/test-admin-update-question.js rename to backend/tests/test-admin-update-question.js diff --git a/backend/test-auth-endpoints.js b/backend/tests/test-auth-endpoints.js similarity index 100% rename from backend/test-auth-endpoints.js rename to backend/tests/test-auth-endpoints.js diff --git a/backend/test-bookmarks.js b/backend/tests/test-bookmarks.js similarity index 100% rename from backend/test-bookmarks.js rename to backend/tests/test-bookmarks.js diff --git a/backend/test-category-admin.js b/backend/tests/test-category-admin.js similarity index 100% rename from backend/test-category-admin.js rename to backend/tests/test-category-admin.js diff --git a/backend/test-category-details.js b/backend/tests/test-category-details.js similarity index 100% rename from backend/test-category-details.js rename to backend/tests/test-category-details.js diff --git a/backend/test-category-endpoints.js b/backend/tests/test-category-endpoints.js similarity index 100% rename from backend/test-category-endpoints.js rename to backend/tests/test-category-endpoints.js diff --git a/backend/test-category-model.js b/backend/tests/test-category-model.js similarity index 99% rename from backend/test-category-model.js rename to backend/tests/test-category-model.js index 5e9a05c..e9bc752 100644 --- a/backend/test-category-model.js +++ b/backend/tests/test-category-model.js @@ -1,5 +1,5 @@ // Category Model Tests -const { sequelize, Category } = require('./models'); +const { sequelize, Category } = require('../models'); async function runTests() { try { diff --git a/backend/test-complete-quiz.js b/backend/tests/test-complete-quiz.js similarity index 100% rename from backend/test-complete-quiz.js rename to backend/tests/test-complete-quiz.js diff --git a/backend/test-conversion-quick.js b/backend/tests/test-conversion-quick.js similarity index 100% rename from backend/test-conversion-quick.js rename to backend/tests/test-conversion-quick.js diff --git a/backend/test-create-question.js b/backend/tests/test-create-question.js similarity index 100% rename from backend/test-create-question.js rename to backend/tests/test-create-question.js diff --git a/backend/test-db-connection.js b/backend/tests/test-db-connection.js similarity index 98% rename from backend/test-db-connection.js rename to backend/tests/test-db-connection.js index 3b8284d..f126e25 100644 --- a/backend/test-db-connection.js +++ b/backend/tests/test-db-connection.js @@ -1,9 +1,9 @@ require('dotenv').config(); -const db = require('./models'); +const db = require('../models'); async function testDatabaseConnection() { console.log('\n🔍 Testing Database Connection...\n'); - + console.log('Configuration:'); console.log('- Host:', process.env.DB_HOST); console.log('- Port:', process.env.DB_PORT); @@ -24,10 +24,10 @@ async function testDatabaseConnection() { // Check if database exists const [databases] = await db.sequelize.query('SHOW DATABASES'); const dbExists = databases.some(d => d.Database === process.env.DB_NAME); - + if (dbExists) { console.log(`✅ Database '${process.env.DB_NAME}' exists.\n`); - + // Show tables in database const [tables] = await db.sequelize.query(`SHOW TABLES FROM ${process.env.DB_NAME}`); console.log(`📋 Tables in '${process.env.DB_NAME}':`, tables.length > 0 ? tables.length : 'No tables yet'); diff --git a/backend/test-error-handling.js b/backend/tests/test-error-handling.js similarity index 100% rename from backend/test-error-handling.js rename to backend/tests/test-error-handling.js diff --git a/backend/test-find-by-pk.js b/backend/tests/test-find-by-pk.js similarity index 93% rename from backend/test-find-by-pk.js rename to backend/tests/test-find-by-pk.js index 529d856..d42dcff 100644 --- a/backend/test-find-by-pk.js +++ b/backend/tests/test-find-by-pk.js @@ -1,9 +1,9 @@ -const { Category } = require('./models'); +const { Category } = require('../models'); async function testFindByPk() { try { console.log('\n=== Testing Category.findByPk(1) ===\n'); - + const category = await Category.findByPk(1, { attributes: [ 'id', @@ -18,9 +18,9 @@ async function testFindByPk() { 'isActive' ] }); - + console.log('Result:', JSON.stringify(category, null, 2)); - + if (category) { console.log('\nCategory found:'); console.log(' Name:', category.name); @@ -29,7 +29,7 @@ async function testFindByPk() { } else { console.log('Category not found!'); } - + process.exit(0); } catch (error) { console.error('Error:', error); diff --git a/backend/test-guest-analytics.js b/backend/tests/test-guest-analytics.js similarity index 100% rename from backend/test-guest-analytics.js rename to backend/tests/test-guest-analytics.js diff --git a/backend/test-guest-conversion.js b/backend/tests/test-guest-conversion.js similarity index 100% rename from backend/test-guest-conversion.js rename to backend/tests/test-guest-conversion.js diff --git a/backend/test-guest-endpoints.js b/backend/tests/test-guest-endpoints.js similarity index 100% rename from backend/test-guest-endpoints.js rename to backend/tests/test-guest-endpoints.js diff --git a/backend/test-guest-quiz-limit.js b/backend/tests/test-guest-quiz-limit.js similarity index 96% rename from backend/test-guest-quiz-limit.js rename to backend/tests/test-guest-quiz-limit.js index a7b5a6e..1f53f8c 100644 --- a/backend/test-guest-quiz-limit.js +++ b/backend/tests/test-guest-quiz-limit.js @@ -35,17 +35,17 @@ async function runTests() { const response = await axios.post(`${BASE_URL}/guest/start-session`, { deviceId: `test_device_${Date.now()}` }); - + if (response.status === 201 && response.data.success) { testSession.guestId = response.data.data.guestId; testSession.sessionToken = response.data.data.sessionToken; - printTestResult(1, 'Guest session created', true, + printTestResult(1, 'Guest session created', true, `Guest ID: ${testSession.guestId}\nToken: ${testSession.sessionToken.substring(0, 50)}...`); } else { throw new Error('Failed to create session'); } } catch (error) { - printTestResult(1, 'Guest session creation', false, + printTestResult(1, 'Guest session creation', false, `Error: ${error.response?.data?.message || error.message}`); return; // Can't continue without session } @@ -58,7 +58,7 @@ async function runTests() { 'X-Guest-Token': testSession.sessionToken } }); - + if (response.status === 200 && response.data.success) { const { quizLimit, session } = response.data.data; printTestResult(2, 'Quiz limit check with valid token', true, @@ -79,7 +79,7 @@ async function runTests() { printSection('Test 3: Check quiz limit without token (should fail with 401)'); try { const response = await axios.get(`${BASE_URL}/guest/quiz-limit`); - printTestResult(3, 'No token provided', false, + printTestResult(3, 'No token provided', false, 'Should have returned 401 but got: ' + response.status); } catch (error) { if (error.response?.status === 401) { @@ -125,13 +125,13 @@ async function runTests() { try { // Create a token with fake guest ID const jwt = require('jsonwebtoken'); - const config = require('./config/config'); + const config = require('../config/config'); const fakeToken = jwt.sign( { guestId: 'guest_fake_12345' }, config.jwt.secret, { expiresIn: '24h' } ); - + const response = await axios.get(`${BASE_URL}/guest/quiz-limit`, { headers: { 'X-Guest-Token': fakeToken @@ -157,9 +157,9 @@ async function runTests() { 'X-Guest-Token': testSession.sessionToken } }); - + const { data } = response.data; - const hasCorrectStructure = + const hasCorrectStructure = data.guestId && data.quizLimit && typeof data.quizLimit.maxQuizzes === 'number' && @@ -169,7 +169,7 @@ async function runTests() { data.session && data.session.expiresAt && data.session.timeRemaining; - + if (hasCorrectStructure) { printTestResult(7, 'Response structure verification', true, 'All required fields present with correct types'); @@ -190,10 +190,10 @@ async function runTests() { 'X-Guest-Token': testSession.sessionToken } }); - + const { quizLimit } = response.data.data; const expectedRemaining = quizLimit.maxQuizzes - quizLimit.quizzesAttempted; - + if (quizLimit.quizzesRemaining === expectedRemaining) { printTestResult(8, 'Quiz remaining calculation', true, `Calculation correct: ${quizLimit.maxQuizzes} - ${quizLimit.quizzesAttempted} = ${quizLimit.quizzesRemaining}`); diff --git a/backend/test-guest-session-model.js b/backend/tests/test-guest-session-model.js similarity index 98% rename from backend/test-guest-session-model.js rename to backend/tests/test-guest-session-model.js index 138315e..970a9a4 100644 --- a/backend/test-guest-session-model.js +++ b/backend/tests/test-guest-session-model.js @@ -1,5 +1,5 @@ // GuestSession Model Tests -const { sequelize, GuestSession, User } = require('./models'); +const { sequelize, GuestSession, User } = require('../models'); async function runTests() { try { @@ -119,7 +119,7 @@ async function runTests() { password: 'password123', role: 'user' }); - + await session1.convertToUser(testUser.id); await session1.reload(); console.log('✅ Session converted to user'); @@ -191,12 +191,12 @@ async function runTests() { console.log('\nTest 20: Cleanup expired sessions'); // Create an expired session by creating a valid one then updating it const tempSession = await GuestSession.createSession({ maxQuizzes: 3 }); - await tempSession.update({ + await tempSession.update({ expiresAt: new Date(Date.now() - 1000) // Set to expired - }, { + }, { validate: false // Skip validation }); - + const cleanedCount = await GuestSession.cleanupExpiredSessions(); console.log('✅ Expired sessions cleaned'); console.log(' Sessions deleted:', cleanedCount); diff --git a/backend/test-guest-settings.js b/backend/tests/test-guest-settings.js similarity index 100% rename from backend/test-guest-settings.js rename to backend/tests/test-guest-settings.js diff --git a/backend/test-junction-tables.js b/backend/tests/test-junction-tables.js similarity index 99% rename from backend/test-junction-tables.js rename to backend/tests/test-junction-tables.js index 5ee9c0a..e40abb1 100644 --- a/backend/test-junction-tables.js +++ b/backend/tests/test-junction-tables.js @@ -1,5 +1,5 @@ -const { sequelize } = require('./models'); -const { User, Category, Question, GuestSession, QuizSession } = require('./models'); +const { sequelize } = require('../models'); +const { User, Category, Question, GuestSession, QuizSession } = require('../models'); const { QueryTypes } = require('sequelize'); async function runTests() { @@ -162,7 +162,7 @@ async function runTests() { // Test 10: Test user_achievements junction console.log('\nTest 10: Test user_achievements junction table'); const achievementId = achievements[0].id; - + await sequelize.query( `INSERT INTO user_achievements (id, user_id, achievement_id, notified) VALUES (UUID(), ?, ?, 0)`, @@ -224,9 +224,9 @@ async function runTests() { "SELECT COUNT(*) as count FROM quiz_answers WHERE quiz_session_id = ?", { replacements: [testQuizSession.id], type: QueryTypes.SELECT } ); - + await QuizSession.destroy({ where: { id: testQuizSession.id } }); - + const answersAfter = await sequelize.query( "SELECT COUNT(*) as count FROM quiz_answers WHERE quiz_session_id = ?", { replacements: [testQuizSession.id], type: QueryTypes.SELECT } @@ -243,9 +243,9 @@ async function runTests() { "SELECT COUNT(*) as count FROM user_bookmarks WHERE user_id = ?", { replacements: [testUser.id], type: QueryTypes.SELECT } ); - + await User.destroy({ where: { id: testUser.id } }); - + const bookmarksAfter = await sequelize.query( "SELECT COUNT(*) as count FROM user_bookmarks WHERE user_id = ?", { replacements: [testUser.id], type: QueryTypes.SELECT } @@ -258,7 +258,7 @@ async function runTests() { // Test 16: Verify all indexes exist console.log('\nTest 16: Verify indexes on all tables'); - + const quizAnswersIndexes = await sequelize.query( "SHOW INDEX FROM quiz_answers", { type: QueryTypes.SELECT } @@ -292,7 +292,7 @@ async function runTests() { console.log('\n====================================='); console.log('🧹 Cleaning up test data...'); - + // Clean up remaining test data await sequelize.query("DELETE FROM user_achievements"); await sequelize.query("DELETE FROM achievements"); @@ -303,7 +303,7 @@ async function runTests() { await sequelize.query("DELETE FROM questions"); await sequelize.query("DELETE FROM categories"); await sequelize.query("DELETE FROM users"); - + console.log('✅ Test data deleted'); console.log('\n✅ All Junction Tables Tests Completed!'); diff --git a/backend/test-limit-reached.js b/backend/tests/test-limit-reached.js similarity index 97% rename from backend/test-limit-reached.js rename to backend/tests/test-limit-reached.js index bbea948..7a55a8a 100644 --- a/backend/test-limit-reached.js +++ b/backend/tests/test-limit-reached.js @@ -13,8 +13,8 @@ async function testLimitReached() { try { // First, update the guest session to simulate reaching limit - const { GuestSession } = require('./models'); - + const { GuestSession } = require('../models'); + console.log('Step 1: Updating guest session to simulate limit reached...'); const guestSession = await GuestSession.findOne({ where: { guestId: GUEST_ID } @@ -47,7 +47,7 @@ async function testLimitReached() { console.log(`✅ Has Reached Limit: ${data.quizLimit.hasReachedLimit}`); console.log(`✅ Quizzes Attempted: ${data.quizLimit.quizzesAttempted}`); console.log(`✅ Quizzes Remaining: ${data.quizLimit.quizzesRemaining}`); - + if (data.upgradePrompt) { console.log('\n✅ Upgrade Prompt Present:'); console.log(` Message: ${data.upgradePrompt.message}`); diff --git a/backend/test-logout-verify.js b/backend/tests/test-logout-verify.js similarity index 100% rename from backend/test-logout-verify.js rename to backend/tests/test-logout-verify.js diff --git a/backend/test-performance.js b/backend/tests/test-performance.js similarity index 100% rename from backend/test-performance.js rename to backend/tests/test-performance.js diff --git a/backend/test-question-by-id.js b/backend/tests/test-question-by-id.js similarity index 100% rename from backend/test-question-by-id.js rename to backend/tests/test-question-by-id.js diff --git a/backend/test-question-model.js b/backend/tests/test-question-model.js similarity index 99% rename from backend/test-question-model.js rename to backend/tests/test-question-model.js index a08e1db..f2dfcf5 100644 --- a/backend/test-question-model.js +++ b/backend/tests/test-question-model.js @@ -1,5 +1,5 @@ // Question Model Tests -const { sequelize, Question, Category, User } = require('./models'); +const { sequelize, Question, Category, User } = require('../models'); async function runTests() { try { diff --git a/backend/test-question-search.js b/backend/tests/test-question-search.js similarity index 100% rename from backend/test-question-search.js rename to backend/tests/test-question-search.js diff --git a/backend/test-questions-by-category.js b/backend/tests/test-questions-by-category.js similarity index 100% rename from backend/test-questions-by-category.js rename to backend/tests/test-questions-by-category.js diff --git a/backend/test-quiz-history.js b/backend/tests/test-quiz-history.js similarity index 100% rename from backend/test-quiz-history.js rename to backend/tests/test-quiz-history.js diff --git a/backend/test-quiz-session-model.js b/backend/tests/test-quiz-session-model.js similarity index 98% rename from backend/test-quiz-session-model.js rename to backend/tests/test-quiz-session-model.js index df37986..28b4a2c 100644 --- a/backend/test-quiz-session-model.js +++ b/backend/tests/test-quiz-session-model.js @@ -1,5 +1,5 @@ -const { sequelize } = require('./models'); -const { User, Category, GuestSession, QuizSession } = require('./models'); +const { sequelize } = require('../models'); +const { User, Category, GuestSession, QuizSession } = require('../models'); async function runTests() { console.log('🧪 Running QuizSession Model Tests\n'); @@ -80,8 +80,8 @@ async function runTests() { console.log(' Questions answered:', userQuiz.questionsAnswered); console.log(' Correct answers:', userQuiz.correctAnswers); console.log(' Total points:', userQuiz.totalPoints); - console.log(' Match:', userQuiz.questionsAnswered === beforeAnswers + 1 && - userQuiz.correctAnswers === beforeCorrect + 1 ? '✅' : '❌'); + console.log(' Match:', userQuiz.questionsAnswered === beforeAnswers + 1 && + userQuiz.correctAnswers === beforeCorrect + 1 ? '✅' : '❌'); // Test 5: Record incorrect answer console.log('\nTest 5: Record incorrect answer'); @@ -92,8 +92,8 @@ async function runTests() { console.log('✅ Incorrect answer recorded'); console.log(' Questions answered:', userQuiz.questionsAnswered); console.log(' Correct answers:', userQuiz.correctAnswers); - console.log(' Match:', userQuiz.questionsAnswered === beforeAnswers2 + 1 && - userQuiz.correctAnswers === beforeCorrect2 ? '✅' : '❌'); + console.log(' Match:', userQuiz.questionsAnswered === beforeAnswers2 + 1 && + userQuiz.correctAnswers === beforeCorrect2 ? '✅' : '❌'); // Test 6: Get quiz progress console.log('\nTest 6: Get quiz progress'); @@ -204,7 +204,7 @@ async function runTests() { totalQuestions: 10 }); await activeQuiz.start(); - + const foundActive = await QuizSession.findActiveForUser(testUser.id); console.log('✅ Active session found'); console.log(' Found ID:', foundActive.id); @@ -360,13 +360,13 @@ async function runTests() { console.log('\n====================================='); console.log('🧹 Cleaning up test data...'); - + // Clean up test data await QuizSession.destroy({ where: {} }); await GuestSession.destroy({ where: {} }); await Category.destroy({ where: {} }); await User.destroy({ where: {} }); - + console.log('✅ Test data deleted'); console.log('\n✅ All QuizSession Model Tests Completed!'); diff --git a/backend/test-review-quiz.js b/backend/tests/test-review-quiz.js similarity index 100% rename from backend/test-review-quiz.js rename to backend/tests/test-review-quiz.js diff --git a/backend/test-security.js b/backend/tests/test-security.js similarity index 100% rename from backend/test-security.js rename to backend/tests/test-security.js diff --git a/backend/test-session-details.js b/backend/tests/test-session-details.js similarity index 100% rename from backend/test-session-details.js rename to backend/tests/test-session-details.js diff --git a/backend/test-simple-category.js b/backend/tests/test-simple-category.js similarity index 100% rename from backend/test-simple-category.js rename to backend/tests/test-simple-category.js diff --git a/backend/test-start-quiz.js b/backend/tests/test-start-quiz.js similarity index 100% rename from backend/test-start-quiz.js rename to backend/tests/test-start-quiz.js diff --git a/backend/test-submit-answer.js b/backend/tests/test-submit-answer.js similarity index 100% rename from backend/test-submit-answer.js rename to backend/tests/test-submit-answer.js diff --git a/backend/test-update-delete-question.js b/backend/tests/test-update-delete-question.js similarity index 100% rename from backend/test-update-delete-question.js rename to backend/tests/test-update-delete-question.js diff --git a/backend/test-update-profile.js b/backend/tests/test-update-profile.js similarity index 100% rename from backend/test-update-profile.js rename to backend/tests/test-update-profile.js diff --git a/backend/test-user-bookmarks.js b/backend/tests/test-user-bookmarks.js similarity index 100% rename from backend/test-user-bookmarks.js rename to backend/tests/test-user-bookmarks.js diff --git a/backend/test-user-dashboard.js b/backend/tests/test-user-dashboard.js similarity index 100% rename from backend/test-user-dashboard.js rename to backend/tests/test-user-dashboard.js diff --git a/backend/test-user-management.js b/backend/tests/test-user-management.js similarity index 100% rename from backend/test-user-management.js rename to backend/tests/test-user-management.js diff --git a/backend/test-user-model.js b/backend/tests/test-user-model.js similarity index 99% rename from backend/test-user-model.js rename to backend/tests/test-user-model.js index f6dfbc5..e12813a 100644 --- a/backend/test-user-model.js +++ b/backend/tests/test-user-model.js @@ -1,5 +1,5 @@ require('dotenv').config(); -const db = require('./models'); +const db = require('../models'); const { User } = db; async function testUserModel() { diff --git a/backend/validate-env.js b/backend/tests/validate-env.js similarity index 100% rename from backend/validate-env.js rename to backend/tests/validate-env.js diff --git a/backend/verify-seeded-data.js b/backend/tests/verify-seeded-data.js similarity index 98% rename from backend/verify-seeded-data.js rename to backend/tests/verify-seeded-data.js index af327d4..482f85f 100644 --- a/backend/verify-seeded-data.js +++ b/backend/tests/verify-seeded-data.js @@ -1,5 +1,5 @@ const { Sequelize } = require('sequelize'); -const config = require('./config/database'); +const config = require('../config/database'); const sequelize = new Sequelize( config.development.database, diff --git a/frontend/src/app/core/models/dashboard.model.ts b/frontend/src/app/core/models/dashboard.model.ts index fc0ce66..ede2055 100644 --- a/frontend/src/app/core/models/dashboard.model.ts +++ b/frontend/src/app/core/models/dashboard.model.ts @@ -4,17 +4,92 @@ import { QuizSession, QuizSessionHistory } from './quiz.model'; /** * User Dashboard Response */ -export interface UserDashboard { + +export interface UserDataDashboard { + id: string; + username: string; + email: string; + role: string; + profileImage: string | null; + memberSince: string; +} +export interface StatsDashboard { + totalQuizzes: number + quizzesPassed: number + passRate: number + totalQuestionsAnswered: number + correctAnswers: number + overallAccuracy: number + currentStreak: number + longestStreak: number + streakStatus: string; + lastActiveDate: string | null + +} +export interface RecentSessionsScoreDashboard { + earned: number + total: number + percentage: number +} +export interface RecentSessionsCategoryDashboard { + id: string + name: string + slug: string + icon: any + color: string +} +export interface RecentSessionsDashboard { + id: string + category: RecentSessionsCategoryDashboard + quizType: string + difficulty: string + status: string + score: RecentSessionsScoreDashboard + isPassed: boolean + questionsAnswered: number + correctAnswers: number + accuracy: number + timeSpent: number + completedAt: string +} +export interface CategoryPerformanceStats { + quizzesTaken: number + quizzesPassed: number + passRate: number + averageScore: number + totalQuestions: number + correctAnswers: number + accuracy: number +} +export interface CategoryPerformanceDashboard { + category: RecentSessionsCategoryDashboard + stats: CategoryPerformanceStats + lastAttempt: string +} + +export interface RecentActivityDashboard { + date: string + quizzesCompleted: number +} +export interface UserDashboardResponse { success: boolean; - totalQuizzes: number; - totalQuestionsAnswered: number; - overallAccuracy: number; - currentStreak: number; - longestStreak: number; - averageScore: number; - recentQuizzes: QuizSession[]; - categoryPerformance: CategoryPerformance[]; - achievements?: Achievement[]; + data: UserDashboard +} +export interface UserDashboard { + user: UserDataDashboard; + stats: StatsDashboard; + recentSessions: RecentSessionsDashboard[] + categoryPerformance: CategoryPerformanceDashboard[] + recentActivity: RecentActivityDashboard[] + // totalQuizzes: number; + // totalQuestionsAnswered: number; + // overallAccuracy: number; + // currentStreak: number; + // longestStreak: number; + // averageScore: number; + // recentQuizzes: QuizSession[]; + // categoryPerformance: CategoryPerformance[]; + // achievements?: Achievement[]; } /** @@ -37,14 +112,14 @@ export interface QuizHistoryResponse { sessions: QuizSessionHistory[]; pagination: PaginationInfo; filters: { - "category": null, - "status": null, - "startDate": null, - "endDate": null + category: null, + status: null, + startDate: null, + endDate: null } - "sorting": { - "sortBy": string - "sortOrder": string + sorting: { + sortBy: string + sortOrder: string } }; } diff --git a/frontend/src/app/core/models/quiz.model.ts b/frontend/src/app/core/models/quiz.model.ts index 543f9c1..0ddf96a 100644 --- a/frontend/src/app/core/models/quiz.model.ts +++ b/frontend/src/app/core/models/quiz.model.ts @@ -263,6 +263,7 @@ export interface QuizQuestionResult { // Legacy support questionId?: string; timeSpent?: number; + } /** diff --git a/frontend/src/app/core/services/quiz.service.ts b/frontend/src/app/core/services/quiz.service.ts index 7f8c278..cd824ab 100644 --- a/frontend/src/app/core/services/quiz.service.ts +++ b/frontend/src/app/core/services/quiz.service.ts @@ -14,7 +14,8 @@ import { CompletedQuizResult, CompletedQuizResponse, QuizReviewResult, - QuizReviewResponse + QuizReviewResponse, + QuizSessionHistory } from '../models/quiz.model'; import { ToastService } from './toast.service'; import { StorageService } from './storage.service'; @@ -41,8 +42,13 @@ export class QuizService { readonly questions = this._questions.asReadonly(); // Quiz results state - private readonly _quizResults = signal(null); + private readonly _quizResults = signal(null); + private readonly _completedQuiz = signal(null); + private readonly _sessionHistoryQuiz = signal(null); + //private readonly _quizResults = signal(null); readonly quizResults = this._quizResults.asReadonly(); + readonly sessionQuizHistory = this._sessionHistoryQuiz.asReadonly(); + readonly completedQuiz = this._completedQuiz.asReadonly(); // Loading states private readonly _isStartingQuiz = signal(false); @@ -188,7 +194,7 @@ export class QuizService { return this.http.post(`${this.apiUrl}/complete`, { sessionId }).pipe( tap(results => { if (results.success) { - this._quizResults.set(results.data); + this._completedQuiz.set(results.data); // Update session status const currentSession = this._activeSession(); diff --git a/frontend/src/app/core/services/user.service.ts b/frontend/src/app/core/services/user.service.ts index 5ae79ae..79090b5 100644 --- a/frontend/src/app/core/services/user.service.ts +++ b/frontend/src/app/core/services/user.service.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router'; import { catchError, tap, map } from 'rxjs/operators'; import { of, Observable } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { UserDashboard, QuizHistoryResponse, UserProfileUpdate, UserProfileUpdateResponse } from '../models/dashboard.model'; +import { UserDashboard, QuizHistoryResponse, UserProfileUpdate, UserProfileUpdateResponse, UserDashboardResponse } from '../models/dashboard.model'; import { ToastService } from './toast.service'; import { AuthService } from './auth.service'; import { StorageService } from './storage.service'; @@ -23,28 +23,28 @@ export class UserService { private toastService = inject(ToastService); private authService = inject(AuthService); private storageService = inject(StorageService); - + private readonly API_URL = `${environment.apiUrl}/users`; private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds - + // Signals - dashboardState = signal(null); + dashboardState = signal(null); historyState = signal(null); isLoading = signal(false); error = signal(null); - + // Cache - private dashboardCache = new Map>(); - + private dashboardCache = new Map>(); + // Computed values - totalQuizzes = computed(() => this.dashboardState()?.totalQuizzes || 0); - overallAccuracy = computed(() => this.dashboardState()?.overallAccuracy || 0); - currentStreak = computed(() => this.dashboardState()?.currentStreak || 0); - + totalQuizzes = computed(() => this.dashboardState()?.data.stats.totalQuizzes || 0); + overallAccuracy = computed(() => this.dashboardState()?.data.stats.overallAccuracy || 0); + currentStreak = computed(() => this.dashboardState()?.data.stats.currentStreak || 0); + /** * Get user dashboard with statistics */ - getDashboard(userId: string, forceRefresh = false): Observable { + getDashboard(userId: string, forceRefresh = false): Observable { // Check cache if not forcing refresh if (!forceRefresh) { const cached = this.dashboardCache.get(userId); @@ -53,11 +53,11 @@ export class UserService { return of(cached.data); } } - + this.isLoading.set(true); this.error.set(null); - - return this.http.get(`${this.API_URL}/${userId}/dashboard`).pipe( + + return this.http.get(`${this.API_URL}/${userId}/dashboard`).pipe( tap(response => { this.dashboardState.set(response); // Cache the response @@ -71,19 +71,19 @@ export class UserService { console.error('Error fetching dashboard:', error); this.error.set(error.error?.message || 'Failed to load dashboard'); this.isLoading.set(false); - + if (error.status === 401) { this.toastService.error('Please log in to view your dashboard'); this.router.navigate(['/login']); } else { this.toastService.error('Failed to load dashboard data'); } - + throw error; }) ); } - + /** * Get user quiz history with pagination and filters */ @@ -96,12 +96,12 @@ export class UserService { ): Observable { this.isLoading.set(true); this.error.set(null); - + let params: any = { page, limit, sortBy }; if (category) { params.category = category; } - + return this.http.get(`${this.API_URL}/${userId}/history`, { params }).pipe( tap(response => { this.historyState.set(response); @@ -111,26 +111,26 @@ export class UserService { console.error('Error fetching history:', error); this.error.set(error.error?.message || 'Failed to load quiz history'); this.isLoading.set(false); - + if (error.status === 401) { this.toastService.error('Please log in to view your history'); this.router.navigate(['/login']); } else { this.toastService.error('Failed to load quiz history'); } - + throw error; }) ); } - + /** * Update user profile */ updateProfile(userId: string, data: UserProfileUpdate): Observable { this.isLoading.set(true); this.error.set(null); - + return this.http.put(`${this.API_URL}/${userId}`, data).pipe( tap(response => { // Update auth state with new user data @@ -138,12 +138,12 @@ export class UserService { if (currentUser && response.data?.user) { const updatedUser = { ...currentUser, ...response.data.user }; this.storageService.setUserData(updatedUser); - + // Update auth state by calling a private method reflection // Since updateAuthState is private, we update storage directly // The auth state will sync on next navigation/refresh } - + this.isLoading.set(false); this.toastService.success('Profile updated successfully'); // Invalidate dashboard cache @@ -153,7 +153,7 @@ export class UserService { console.error('Error updating profile:', error); this.error.set(error.error?.message || 'Failed to update profile'); this.isLoading.set(false); - + if (error.status === 401) { this.toastService.error('Please log in to update your profile'); } else if (error.status === 409) { @@ -161,12 +161,12 @@ export class UserService { } else { this.toastService.error('Failed to update profile'); } - + throw error; }) ); } - + /** * Clear cache (useful after logout or data updates) */ @@ -176,12 +176,12 @@ export class UserService { this.historyState.set(null); this.error.set(null); } - + /** * Check if dashboard data is empty (no quizzes taken) */ isDashboardEmpty(): boolean { const dashboard = this.dashboardState(); - return dashboard ? dashboard.totalQuizzes === 0 : true; + return dashboard ? dashboard.data.stats.totalQuizzes === 0 : true; } } diff --git a/frontend/src/app/features/dashboard/dashboard.component.html b/frontend/src/app/features/dashboard/dashboard.component.html index 7cb2a0b..5b872dd 100644 --- a/frontend/src/app/features/dashboard/dashboard.component.html +++ b/frontend/src/app/features/dashboard/dashboard.component.html @@ -17,7 +17,7 @@
- +
@@ -35,7 +35,7 @@
- +
quiz @@ -46,10 +46,10 @@ Take Your First Quiz
- +
- +
@@ -64,7 +64,7 @@
- + @@ -75,35 +75,32 @@
-
+
- {{ category.categoryName }} - - {{ category.quizzesTaken }} {{ category.quizzesTaken === 1 ? 'quiz' : 'quizzes' }} - + {{ category.category.name }} +
-
+ [style.width.%]="category.category.accuracy" + [ngClass]="getAccuracyColor(category.category.accuracy)" + >
-->
- {{ category.accuracy.toFixed(1) }}% +
- +

No category data available yet

- + @@ -111,28 +108,19 @@ history Recent Quiz Sessions -
-
+
quiz
-
{{ session.categoryName || 'Quiz' }}
+
{{ session.category.name }}
{{ formatDate(session.completedAt) }} @@ -144,25 +132,22 @@
- - {{ session.score }}/{{ session.totalQuestions }} + + {{ session.score.total }}/{{ session.questionsAnswered }} - {{ ((session.score / session.totalQuestions) * 100).toFixed(0) }}% + {{ session.score.percentage }}%
chevron_right
- +

No recent quiz sessions

- + @@ -173,28 +158,25 @@
-
+
- {{ achievement.icon }} + {{ achievement.category.icon }}
-
{{ achievement.name }}
-
- {{ formatDate(achievement.earnedAt) }} +
{{ achievement.category.name }}
+
+ {{ formatDate(achievement.completedAt) }}
- +

No achievements earned yet. Keep taking quizzes to unlock badges!

- +
-
+
\ No newline at end of file diff --git a/frontend/src/app/features/dashboard/dashboard.component.ts b/frontend/src/app/features/dashboard/dashboard.component.ts index 2017e56..d9223b8 100644 --- a/frontend/src/app/features/dashboard/dashboard.component.ts +++ b/frontend/src/app/features/dashboard/dashboard.component.ts @@ -7,11 +7,11 @@ import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatChipsModule } from '@angular/material/chips'; import { MatTooltipModule } from '@angular/material/tooltip'; - -import { UserDashboard, } from '../../core/models/dashboard.model'; - import { UserService } from '../../core/services/user.service'; + +import { UserDashboard, UserDashboardResponse, } from '../../core/models/dashboard.model'; +import { UserService } from '../../core/services/user.service'; import { AuthService } from '../../core/services'; - + @Component({ selector: 'app-dashboard', standalone: true, @@ -32,12 +32,12 @@ export class DashboardComponent implements OnInit { private userService = inject(UserService); private authService = inject(AuthService); private router = inject(Router); - + // Signals isLoading = signal(true); dashboard = signal(null); error = signal(null); - + // Computed values username = computed(() => { try { @@ -49,93 +49,93 @@ export class DashboardComponent implements OnInit { }); isEmpty = computed(() => { const dash = this.dashboard(); - return dash ? dash.totalQuizzes === 0 : true; + return dash ? dash.stats.totalQuizzes === 0 : true; }); - + // Stat cards computed statCards = computed(() => { const dash = this.dashboard(); if (!dash) return []; - + return [ { title: 'Total Quizzes', - value: dash.totalQuizzes, + value: dash.stats.totalQuizzes, icon: 'quiz', color: 'primary', description: 'Quizzes completed' }, { title: 'Overall Accuracy', - value: `${dash.overallAccuracy.toFixed(1)}%`, + value: `${dash.stats.overallAccuracy.toFixed(1)}%`, icon: 'percent', color: 'success', description: 'Correct answers' }, { title: 'Current Streak', - value: dash.currentStreak, + value: dash.stats.currentStreak, icon: 'local_fire_department', color: 'warning', description: 'Days in a row', - badge: dash.longestStreak > 0 ? `Best: ${dash.longestStreak}` : undefined + badge: dash.stats.longestStreak > 0 ? `Best: ${dash.stats.longestStreak}` : undefined }, { title: 'Questions Answered', - value: dash.totalQuestionsAnswered, + value: dash.stats.totalQuestionsAnswered, icon: 'question_answer', color: 'accent', description: 'Total questions' } ]; }); - + // Top categories computed topCategories = computed(() => { const dash = this.dashboard(); if (!dash || !dash.categoryPerformance) return []; - + return [...dash.categoryPerformance] - .sort((a, b) => b.accuracy - a.accuracy) + .sort((a, b) => b.stats.accuracy - a.stats.accuracy) .slice(0, 5); }); - + // Recent sessions computed recentSessions = computed(() => { const dash = this.dashboard(); - if (!dash || !dash.recentQuizzes) return []; - - return dash.recentQuizzes.slice(0, 5); + if (!dash || !dash.recentSessions) return []; + + return dash.recentSessions.slice(0, 5); }); - + // Achievements computed achievements = computed(() => { const dash = this.dashboard(); - return dash?.achievements || []; + return dash?.recentSessions || []; }); - + ngOnInit(): void { this.loadDashboard(); } - + /** * Load dashboard data */ loadDashboard(): 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).getDashboard(user.id).subscribe({ - next: (data: UserDashboard) => { - this.dashboard.set(data); + next: (res: UserDashboardResponse) => { + this.dashboard.set(res.data); this.isLoading.set(false); }, error: (err: any) => { @@ -145,14 +145,14 @@ export class DashboardComponent implements OnInit { } }); } - + /** * Navigate to quiz setup */ startNewQuiz(): void { this.router.navigate(['/quiz/setup']); } - + /** * Navigate to category detail */ @@ -161,7 +161,7 @@ export class DashboardComponent implements OnInit { this.router.navigate(['/categories', categoryId]); } } - + /** * Navigate to quiz results */ @@ -170,14 +170,14 @@ export class DashboardComponent implements OnInit { this.router.navigate(['/quiz', sessionId, 'results']); } } - + /** * Navigate to full history */ viewAllHistory(): void { this.router.navigate(['/history']); } - + /** * Get color class for accuracy */ @@ -186,7 +186,7 @@ export class DashboardComponent implements OnInit { if (accuracy >= 60) return 'warning'; return 'error'; } - + /** * Get score display color */ @@ -196,45 +196,45 @@ export class DashboardComponent implements OnInit { if (percentage >= 60) return 'warning'; return 'error'; } - + /** * Format time 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`; } - + /** * Format date for display */ formatDate(dateString: string | undefined): string { if (!dateString) return 'Unknown'; - + const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - + if (diffDays === 0) return 'Today'; if (diffDays === 1) return 'Yesterday'; if (diffDays < 7) return `${diffDays} days ago`; - - return date.toLocaleDateString('en-US', { - month: 'short', + + return date.toLocaleDateString('en-US', { + month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined }); } - + /** * Refresh dashboard data */ diff --git a/frontend/src/app/features/quiz/quiz-results/quiz-results.html b/frontend/src/app/features/quiz/quiz-results/quiz-results.html index eb6bb3a..decf9c5 100644 --- a/frontend/src/app/features/quiz/quiz-results/quiz-results.html +++ b/frontend/src/app/features/quiz/quiz-results/quiz-results.html @@ -1,337 +1,281 @@
@if (isLoading()) { -
- -

Loading results...

-
+
+ +

Loading results...

+
} @if (!isLoading() && results()) { - - @if (showConfetti()) { -
- @for (i of [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]; track i) { -
+ + @if (showConfetti()) { +
+ @for (i of [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]; track i) { +
+ } +
+ } + +
+ +
+
+ @if (performanceLevel() === 'excellent') { + emoji_events + } @else if (performanceLevel() === 'good') { + thumb_up + } @else if (performanceLevel() === 'average') { + trending_up + } @else { + school }
- } +

Quiz Completed!

+

+ {{ performanceMessage() }} +

+
-
- -
-
- @if (performanceLevel() === 'excellent') { - emoji_events - } @else if (performanceLevel() === 'good') { - thumb_up - } @else if (performanceLevel() === 'average') { - trending_up - } @else { - school - } -
-

Quiz Completed!

-

- {{ performanceMessage() }} -

-
- - - - -
-
- - - - -
- {{ scorePercentage() }}% - Score -
-
-
-
- check_circle -
-
{{ results()!.correctAnswers }}
-
Correct
-
-
-
- cancel -
-
{{ results()!.incorrectAnswers }}
-
Incorrect
-
-
- @if (results()!.skippedAnswers > 0) { -
- remove_circle -
-
{{ results()!.skippedAnswers }}
-
Skipped
-
-
- } + + + +
+
+ + + + +
+ @let score = results()!.summary.score.total> 0 ? (results()!.summary.score.earned / + results()!.summary.score.total) * 100 : 0; + {{score }}% + Score
- - - - - - - - - - - Performance Breakdown - - -
-
- - - - - - - - - @if (chartPercentages().skipped > 0) { - - } - -
- {{ results()!.totalQuestions }} - Questions +
+
+ check_circle +
+
{{ results()!.summary.questions.correct }}
+
Correct
- -
-
- - Correct ({{ chartData().correct }}) +
+ cancel +
+
{{ results()!.summary.questions.incorrect }}
+
Incorrect
-
- - Incorrect ({{ chartData().incorrect }}) -
- @if (chartData().skipped > 0) { -
- - Skipped ({{ chartData().skipped }}) -
- }
-
- - - - - - - Question Review - Review all questions and answers - - -
- @for (question of results()!.questions; track question.questionId; let i = $index) { -
-
-
- {{ i + 1 }} - @if (question.isCorrect) { - check_circle - } @else { - cancel - } -
-
- {{ getQuestionTypeText(question.questionType) }} - {{ question.points }} pts -
-
- -
{{ question.questionText }}
- -
-
- Your Answer: - - {{ question.userAnswer || 'Not answered' }} - -
- @if (!question.isCorrect) { -
- Correct Answer: - {{ question.correctAnswer }} -
- } -
- - @if (question.explanation) { -
- info -

{{ question.explanation }}

-
- } + @if (results()!.summary.questions.unanswered > 0) { +
+ remove_circle +
+
{{ results()!.summary.questions.unanswered }}
+
Skipped
+
}
- - +
- -
- + - @if (hasIncorrectAnswers()) { - - } - - -
- - -
+
\ No newline at end of file diff --git a/frontend/src/app/features/quiz/quiz-results/quiz-results.scss b/frontend/src/app/features/quiz/quiz-results/quiz-results.scss index 63f3a34..9edef92 100644 --- a/frontend/src/app/features/quiz/quiz-results/quiz-results.scss +++ b/frontend/src/app/features/quiz/quiz-results/quiz-results.scss @@ -88,6 +88,7 @@ opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); @@ -172,6 +173,7 @@ transform: scale(0); opacity: 0; } + to { transform: scale(1); opacity: 1; @@ -252,6 +254,7 @@ } .score-label { + margin-top: 10px; display: block; font-size: 1rem; color: var(--text-secondary); @@ -349,6 +352,7 @@ opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); @@ -671,4 +675,4 @@ .explanation { background-color: rgba(255, 255, 255, 0.05); } -} +} \ No newline at end of file diff --git a/frontend/src/app/features/quiz/quiz-results/quiz-results.ts b/frontend/src/app/features/quiz/quiz-results/quiz-results.ts index b5a706d..8920d27 100644 --- a/frontend/src/app/features/quiz/quiz-results/quiz-results.ts +++ b/frontend/src/app/features/quiz/quiz-results/quiz-results.ts @@ -42,7 +42,9 @@ export class QuizResultsComponent implements OnInit, OnDestroy { // Computed values readonly scorePercentage = computed(() => { const res = this.results(); - return res?.percentage ?? 0; + console.log(res); + + return res?.summary.score.percentage ?? 0; }); readonly performanceLevel = computed(() => { @@ -77,20 +79,20 @@ export class QuizResultsComponent implements OnInit, OnDestroy { readonly chartData = computed(() => { const res = this.results(); if (!res) return { correct: 0, incorrect: 0, skipped: 0 }; - + return { - correct: res.correctAnswers, - incorrect: res.incorrectAnswers, - skipped: res.skippedAnswers + correct: res.summary.questions.correct, + incorrect: res.summary.questions.incorrect, + skipped: res.summary.questions.unanswered }; }); readonly chartPercentages = computed(() => { const data = this.chartData(); const total = data.correct + data.incorrect + data.skipped; - + if (total === 0) return { correct: 0, incorrect: 0, skipped: 0 }; - + return { correct: Math.round((data.correct / total) * 100), incorrect: Math.round((data.incorrect / total) * 100), @@ -161,10 +163,10 @@ export class QuizResultsComponent implements OnInit, OnDestroy { */ formatTime(seconds: number): string { if (!seconds) return '0s'; - + const minutes = Math.floor(seconds / 60); const secs = seconds % 60; - + if (minutes > 0) { return `${minutes}m ${secs}s`; } @@ -245,7 +247,7 @@ export class QuizResultsComponent implements OnInit, OnDestroy { const results = this.results(); if (!results) return; - const text = `I scored ${results.percentage}% on my quiz! 🎯`; + const text = `I scored ${results.summary.score.percentage}% on my quiz! 🎯`; const url = window.location.href; let shareUrl = ''; diff --git a/frontend/src/app/features/quiz/quiz-review/quiz-review.html b/frontend/src/app/features/quiz/quiz-review/quiz-review.html index 64f6d19..2bf044a 100644 --- a/frontend/src/app/features/quiz/quiz-review/quiz-review.html +++ b/frontend/src/app/features/quiz/quiz-review/quiz-review.html @@ -65,7 +65,9 @@ emoji_events
-
{{ scorePercentage() }}%
+ @let score = results()!.summary.score.total> 0 ? (results()!.summary.score.earned / + results()!.summary.score.total) * 100 : 0; +
{{ score }}%
Score
diff --git a/frontend/src/app/features/quiz/quiz-review/quiz-review.ts b/frontend/src/app/features/quiz/quiz-review/quiz-review.ts index c35e087..0cebf7e 100644 --- a/frontend/src/app/features/quiz/quiz-review/quiz-review.ts +++ b/frontend/src/app/features/quiz/quiz-review/quiz-review.ts @@ -87,14 +87,6 @@ export class QuizReviewComponent implements OnInit, OnDestroy { this.allQuestions().filter(q => !q.isCorrect).length ); - 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) {