From 7882b0493af551b30a3334cd762a1b4a72698e52 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:52:50 +0300 Subject: [PATCH 01/22] refactor: remove unused quiz and recommendation service functions --- apps/api/src/services/quizzes.ts | 3 --- apps/api/src/services/recommendations.ts | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 apps/api/src/services/quizzes.ts delete mode 100644 apps/api/src/services/recommendations.ts diff --git a/apps/api/src/services/quizzes.ts b/apps/api/src/services/quizzes.ts deleted file mode 100644 index 2288cc0..0000000 --- a/apps/api/src/services/quizzes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const generateNewQuiz = () => { - console.log("Generating a new quiz..."); -}; diff --git a/apps/api/src/services/recommendations.ts b/apps/api/src/services/recommendations.ts deleted file mode 100644 index 63a8bd7..0000000 --- a/apps/api/src/services/recommendations.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const getRecommendations = () => { - console.log("Getting recommendations..."); -}; From 0fb3a0b0179beb1b0552bbb78e273418d9b17ecd Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:52:58 +0300 Subject: [PATCH 02/22] fix(pnpm-lock): update dependency versions and add axios --- pnpm-lock.yaml | 71 +++++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d1a243..86d32e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: dependencies: '@adminjs/express': specifier: ^6.1.1 - version: 6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1) + version: 6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1) '@prisma/client': specifier: 6.11.1 version: 6.11.1(prisma@6.12.0(typescript@5.8.2))(typescript@5.8.2) @@ -34,7 +34,10 @@ importers: version: 2.1.13(@tiptap/core@2.1.13(@tiptap/pm@2.1.13))(@tiptap/pm@2.1.13) adminjs: specifier: ^7.8.17 - version: 7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8) + version: 7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8) + axios: + specifier: ^1.11.0 + version: 1.11.0 better-auth: specifier: ^1.2.12 version: 1.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -173,7 +176,7 @@ importers: version: 9.32.0 '@tailwindcss/vite': specifier: ^4.1.5 - version: 4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -194,7 +197,7 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.6) @@ -233,16 +236,16 @@ importers: version: 8.38.0(eslint@9.32.0(jiti@2.5.1))(typescript@5.7.3) vite: specifier: ^6.3.1 - version: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + version: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + version: 5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) vitest: specifier: ^3.1.2 - version: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3) + version: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) packages: @@ -5688,9 +5691,9 @@ snapshots: - react-is - supports-color - '@adminjs/express@6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1)': + '@adminjs/express@6.1.1(adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8))(express-formidable@1.2.0)(express-session@1.18.2)(express@4.21.2)(tslib@2.8.1)': dependencies: - adminjs: 7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8) + adminjs: 7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8) express: 4.21.2 express-formidable: 1.2.0 express-session: 1.18.2 @@ -6963,7 +6966,7 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@hello-pangea/dnd@16.6.0(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@hello-pangea/dnd@16.6.0(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.28.2 css-box-model: 1.2.1 @@ -6971,7 +6974,7 @@ snapshots: raf-schd: 4.0.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-redux: 8.1.3(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) + react-redux: 8.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) redux: 4.2.1 use-memo-one: 1.1.3(react@18.3.1) transitivePeerDependencies: @@ -7736,12 +7739,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3))': + '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2))': dependencies: '@tailwindcss/node': 4.1.11 '@tailwindcss/oxide': 4.1.11 tailwindcss: 4.1.11 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) '@tanstack/query-core@5.83.0': {} @@ -8282,7 +8285,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3))': + '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2))': dependencies: '@babel/core': 7.28.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) @@ -8290,7 +8293,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - supports-color @@ -8302,13 +8305,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -8349,7 +8352,7 @@ snapshots: acorn@8.15.0: {} - adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react@19.1.8): + adminjs@7.8.17(@types/babel__core@7.20.5)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8): dependencies: '@adminjs/design-system': 4.1.1(@babel/core@7.28.0)(@types/react@19.1.8)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1) '@babel/core': 7.28.0 @@ -8360,7 +8363,7 @@ snapshots: '@babel/preset-react': 7.27.1(@babel/core@7.28.0) '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) '@babel/register': 7.27.1(@babel/core@7.28.0) - '@hello-pangea/dnd': 16.6.0(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@hello-pangea/dnd': 16.6.0(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@redux-devtools/extension': 3.3.0(redux@4.2.1) '@rollup/plugin-babel': 6.0.4(@babel/core@7.28.0)(@types/babel__core@7.20.5)(rollup@4.40.2) '@rollup/plugin-commonjs': 25.0.8(rollup@4.40.2) @@ -8383,7 +8386,7 @@ snapshots: react-feather: 2.0.10(react@18.3.1) react-i18next: 12.3.1(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-is: 18.3.1 - react-redux: 8.1.3(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) + react-redux: 8.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1) react-router: 6.30.1(react@18.3.1) react-router-dom: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) redux: 4.2.1 @@ -10601,7 +10604,7 @@ snapshots: react-fast-compare: 3.2.2 warning: 4.0.3 - react-redux@8.1.3(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1): + react-redux@8.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1): dependencies: '@babel/runtime': 7.28.2 '@types/hoist-non-react-statics': 3.3.7(@types/react@19.1.8) @@ -10612,6 +10615,7 @@ snapshots: use-sync-external-store: 1.5.0(react@18.3.1) optionalDependencies: '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) react-dom: 18.3.1(react@18.3.1) redux: 4.2.1 @@ -11424,13 +11428,13 @@ snapshots: vary@1.1.2: {} - vite-node@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3): + vite-node@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@5.5.0) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - '@types/node' - jiti @@ -11445,29 +11449,29 @@ snapshots: - tsx - yaml - vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)): + vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.40.2) '@svgr/core': 8.1.0(typescript@5.7.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.7.3)) - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - rollup - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.7.3) optionalDependencies: - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) transitivePeerDependencies: - supports-color - typescript - vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3): + vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2): dependencies: esbuild: 0.25.4 fdir: 6.4.6(picomatch@4.0.3) @@ -11481,12 +11485,13 @@ snapshots: jiti: 2.5.1 lightningcss: 1.30.1 tsx: 4.20.3 + yaml: 1.10.2 - vitest@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3): + vitest@3.2.4(@types/node@22.16.5)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -11504,8 +11509,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) - vite-node: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3) + vite: 6.3.5(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) + vite-node: 3.2.4(@types/node@22.16.5)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@1.10.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.16.5 From 684fdc7342dfb1908b967d673e214c2cad8cf96c Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:53:02 +0300 Subject: [PATCH 03/22] fix(package.json): ensure axios dependency is included --- apps/api/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/package.json b/apps/api/package.json index 8aeb33f..9319bb7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -19,6 +19,7 @@ "@prisma/client": "6.11.1", "@tiptap/extension-horizontal-rule": "2.1.13", "adminjs": "^7.8.17", + "axios": "^1.11.0", "better-auth": "^1.2.12", "cors": "^2.8.5", "dotenv": "^17.2.1", From 6ba0573ef174138aa71fc6e43ea1f98c888eb8c9 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:53:24 +0300 Subject: [PATCH 04/22] feat(prisma): add difficulty and attempt tracking to Topic model; set default values for DailyQuiz --- apps/api/prisma/schema.prisma | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 9f7ec5e..b83210a 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -66,6 +66,10 @@ model Topic { userCompletions UserCompletion[] questions Question[] userPerformances UserTopicPerformance[] + + difficulty QuestionDifficulty @default(easy) + attempted Int @default(0) + solved Int @default(0) } model UserCompletion { @@ -134,6 +138,9 @@ model DailyQuiz { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt submittedAt DateTime? + totalQuestions Int @default(10) + + @@unique([userId, createdAt]) } model User { @@ -226,3 +233,4 @@ model Jwks { @@map("jwks") } + From 6b4575f0c141ce34d455adc7e42266269813eeed Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:53:33 +0300 Subject: [PATCH 05/22] feat(migration): add totalQuestions to DailyQuiz and attempted, difficulty, solved to Topic; create unique index on DailyQuiz --- .../migration.sql | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 apps/api/prisma/migrations/20250813194643_adding_unique_id_quiz/migration.sql diff --git a/apps/api/prisma/migrations/20250813194643_adding_unique_id_quiz/migration.sql b/apps/api/prisma/migrations/20250813194643_adding_unique_id_quiz/migration.sql new file mode 100644 index 0000000..4c2d110 --- /dev/null +++ b/apps/api/prisma/migrations/20250813194643_adding_unique_id_quiz/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId,createdAt]` on the table `DailyQuiz` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "DailyQuiz" ADD COLUMN "totalQuestions" INTEGER NOT NULL DEFAULT 10; + +-- AlterTable +ALTER TABLE "Topic" ADD COLUMN "attempted" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "difficulty" "QuestionDifficulty" NOT NULL DEFAULT 'easy', +ADD COLUMN "solved" INTEGER NOT NULL DEFAULT 0; + +-- CreateIndex +CREATE UNIQUE INDEX "DailyQuiz_userId_createdAt_key" ON "DailyQuiz"("userId", "createdAt"); From 2c972bbc92031cb2523b3826417e4c169ada899c Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:53:38 +0300 Subject: [PATCH 06/22] chore: update app.ts for improved error handling and middleware configuration --- apps/api/src/app.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index be0dc45..6344954 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -3,13 +3,44 @@ import express from "express"; import { auth } from "./lib/auth.js"; import { toNodeHandler } from "better-auth/node"; import { admin, adminRouter } from "./lib/admin.js"; +import { quizzesRouter } from "./routes/quizzes.js"; const app = express(); +// Basic middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + app.disable("x-powered-by"); app.all("/api/auth/*", toNodeHandler(auth)); app.use(admin.options.rootPath, adminRouter); console.log(`AdminJS is running under ${admin.options.rootPath}`); +// API routes +app.use("/api/quizzes", quizzesRouter); + +// Simple health route +app.get("/api/health", (_req: express.Request, res: express.Response): void => { + res.json({ status: "ok" }); +}); + +// Error handler +// eslint-disable-next-line @typescript-eslint/no-unused-vars +app.use((( + err: any, + _req: express.Request, + res: express.Response, + _next: express.NextFunction +) => { + console.error("[Error]", err); + if (err?.message === "Unauthorized") { + res.status(401).json({ status: "fail", message: "Unauthorized" }); + return; + } + res + .status(500) + .json({ status: "error", message: err?.message || "Server error" }); +}) as express.ErrorRequestHandler); + export default app; From 34d20013f7ebe8695ec333f7944e0f27cc664622 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:53:45 +0300 Subject: [PATCH 07/22] feat(calendar): implement getQuizSubmissionCalendar to track user quiz submissions by date --- .../api/src/controller/calender.controller.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 apps/api/src/controller/calender.controller.ts diff --git a/apps/api/src/controller/calender.controller.ts b/apps/api/src/controller/calender.controller.ts new file mode 100644 index 0000000..d7b2d15 --- /dev/null +++ b/apps/api/src/controller/calender.controller.ts @@ -0,0 +1,84 @@ +import { Request, Response } from "express"; +import type { Session, User } from "better-auth"; +import { prisma } from "../lib/prisma.js"; + +declare global { + // Ensure session typing just like other controllers + namespace Express { + interface Request { + session?: Session; + user?: User; + } + } +} + +interface ApiResponse { + status: string; + message?: string; + data?: T; +} +const send = (res: Response, code: number, body: ApiResponse) => + res.status(code).json(body); + +// Utility to get month boundaries in local time (00:00:00.000 inclusive to next month start exclusive) +const monthRange = (year: number, month0: number) => { + const start = new Date(year, month0, 1, 0, 0, 0, 0); + const end = new Date(year, month0 + 1, 1, 0, 0, 0, 0); + return { start, end }; +}; + +/** + * Returns an array of booleans (index 0 = day 1) for the requested month indicating + * which days the user submitted a quiz (DailyQuiz.submittedAt not null). + * Query params: year=YYYY, month=1-12 (defaults to current year/month if omitted) + */ +export const getQuizSubmissionCalendar = async ( + req: Request, + res: Response +): Promise => { + if (!req.session) { + send(res, 401, { status: "fail", message: "Unauthorized" }); + return; + } + const userId = req.session.userId; + + // Parse month/year with fallbacks + const now = new Date(); + const year = Number(req.query.year) || now.getFullYear(); + const monthParam = Number(req.query.month); // 1-12 + const month0 = + monthParam && monthParam >= 1 && monthParam <= 12 ? + monthParam - 1 + : now.getMonth(); + + try { + const { start, end } = monthRange(year, month0); + const daysInMonth = new Date(year, month0 + 1, 0).getDate(); + const days: boolean[] = Array(daysInMonth).fill(false); + + const submissions = await prisma.dailyQuiz.findMany({ + where: { + userId, + submittedAt: { not: null, gte: start, lt: end }, + }, + select: { submittedAt: true }, + }); + + for (const s of submissions) { + if (!s.submittedAt) continue; + const day = s.submittedAt.getDate(); // 1-based + if (day >= 1 && day <= daysInMonth) days[day - 1] = true; + } + + send(res, 200, { + status: "success", + data: { year, month: month0 + 1, days }, + }); + } catch (err) { + console.error("[getQuizSubmissionCalendar] error", err); + send(res, 500, { + status: "error", + message: "Failed to fetch calendar", + }); + } +}; From 37f6029be1717bd91208ccacae02d55b617a6c3d Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:53:52 +0300 Subject: [PATCH 08/22] feat(quiz): implement getQuiz and submitQuiz endpoints for quiz management --- apps/api/src/controller/quiz.controller.ts | 130 +++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 apps/api/src/controller/quiz.controller.ts diff --git a/apps/api/src/controller/quiz.controller.ts b/apps/api/src/controller/quiz.controller.ts new file mode 100644 index 0000000..df66ce1 --- /dev/null +++ b/apps/api/src/controller/quiz.controller.ts @@ -0,0 +1,130 @@ +import { Request, Response } from "express"; +import type { Session, User } from "better-auth"; +import { fetchAiRecommendation } from "../services/ai.service.js"; +import { buildUserQuizData } from "../services/quiz-data.service.js"; +import { fetchQuestionsByRecommendation } from "../services/question.service.js"; +import { + saveOrUpdateDailyQuiz, + findTodayDailyQuiz, +} from "../services/daily-quiz.service.js"; +import { + gradeAnswers, + SubmittedAnswerInput, +} from "../services/quiz-submission.service.js"; +import { updateDailyQuizScoreByUserToday } from "../services/daily-quiz.service.js"; + +declare global { + namespace Express { + interface Request { + session?: Session; + user?: User; + } + } +} + +// Utility helpers ------------------------------------------------------------- +const startOfDay = (d: Date) => + new Date(d.getFullYear(), d.getMonth(), d.getDate()); + +interface ApiResponse { + status: string; + message?: string; + data?: T; +} +const send = (res: Response, code: number, body: ApiResponse) => + res.status(code).json(body); + +export const getQuiz = async (req: Request, res: Response): Promise => { + if (!req.session) { + send(res, 401, { status: "fail", message: "Unauthorized" }); + return; + } + const userId = req.session.userId; + const today = new Date(); + + try { + // Fetch existing quiz (by date range) if any + const existingQuiz = await findTodayDailyQuiz(userId, today); + const totalQuestions = existingQuiz?.totalQuestions || 10; + + // Build data for AI + const quizData = await buildUserQuizData(userId, totalQuestions); + if (!quizData) { + send(res, 404, { + status: "fail", + message: "User hasn't completed any topics yet", + }); + return; + } + + const aiRecommendation = await fetchAiRecommendation(quizData); + const questions = await fetchQuestionsByRecommendation( + aiRecommendation, + totalQuestions + ); + + // Persist / update quiz record (schema only stores counts currently) + const savedQuiz = await saveOrUpdateDailyQuiz( + userId, + startOfDay(today), + totalQuestions + ); + + send(res, 200, { + status: "success", + data: { quiz: savedQuiz, questions, aiRecommendation }, + }); + } catch (err) { + console.error("[getQuiz] error", err); + send(res, 500, { + status: "error", + message: "Failed to generate quiz", + }); + } +}; + +export const submitQuiz = async ( + req: Request, + res: Response +): Promise => { + if (!req.session) { + send(res, 401, { status: "fail", message: "Unauthorized" }); + return; + } + const userId = req.session.userId; + const today = new Date(); + + try { + const { answers } = req.body as { answers: SubmittedAnswerInput[] }; + if (!Array.isArray(answers) || answers.length === 0) { + send(res, 400, { + status: "fail", + message: "answers array required", + }); + return; + } + + const grading = await gradeAnswers(answers); + await updateDailyQuizScoreByUserToday( + userId, + today, + grading.scorePercentage + ); + + send(res, 200, { + status: "success", + data: { + score: grading.scorePercentage, + correctCount: grading.correctCount, + total: grading.total, + answers: grading.graded, + }, + }); + } catch (err) { + console.error("[submitQuiz] error", err); + send(res, 500, { + status: "error", + message: "Failed to submit quiz", + }); + } +}; From 6dd9535ab9da09f19bddb870edf6a09e897aec17 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:53:59 +0300 Subject: [PATCH 09/22] feat(quizzes): implement calendar endpoint for quiz submission tracking --- apps/api/src/routes/quizzes.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/apps/api/src/routes/quizzes.ts b/apps/api/src/routes/quizzes.ts index 06dbcac..44c0f5c 100644 --- a/apps/api/src/routes/quizzes.ts +++ b/apps/api/src/routes/quizzes.ts @@ -1,23 +1,29 @@ import { Router } from "express"; import { validate } from "../middlewares/validate.js"; +import { requireAuth } from "../middlewares/auth.js"; import { submitDailyQuizBodySchema } from "../schemas/quizzes.js"; +import { getQuiz, submitQuiz } from "../controller/quiz.controller.js"; +import { getQuizSubmissionCalendar } from "../controller/calender.controller.js"; const router = Router(); -router.get("/monthly-stats", (req, res) => { - res.send(req.url); -}); +// Calendar of submissions (month view) +router.get( + "/calendar", + requireAuth, + // optional validation for query could be added later + getQuizSubmissionCalendar +); -router.get("/daily", (req, res) => { - res.send(req.url); -}); +// Fetch / (re)generate today's quiz for the user +router.get("/daily", requireAuth, getQuiz); +// Submit answers for today's quiz router.post( "/daily", + requireAuth, validate({ body: submitDailyQuizBodySchema }), - (req, res) => { - res.send(req.url); - }, + submitQuiz ); export { router as quizzesRouter }; From 6a576b97e8c8f45a7113a74d0d396f18181839f8 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:54:05 +0300 Subject: [PATCH 10/22] feat(quizzes): update submitDailyQuizBodySchema to require choiceIndex and remove optional answer field --- apps/api/src/schemas/quizzes.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/api/src/schemas/quizzes.ts b/apps/api/src/schemas/quizzes.ts index 1e6e4e9..226288a 100644 --- a/apps/api/src/schemas/quizzes.ts +++ b/apps/api/src/schemas/quizzes.ts @@ -1,10 +1,12 @@ import z from "zod"; export const submitDailyQuizBodySchema = z.object({ - answers: z.array( - z.object({ - questionId: z.string().uuid(), - answer: z.string().optional(), - }), - ), + answers: z + .array( + z.object({ + questionId: z.string().uuid(), + choiceIndex: z.number().int().min(0), + }) + ) + .min(1), }); From 1eeb191bf470a64764ff8653a63bfcb79de66359 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:54:09 +0300 Subject: [PATCH 11/22] feat(ai): add fetchAiRecommendation function for AI-based quiz recommendations --- apps/api/src/services/ai.service.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/api/src/services/ai.service.ts diff --git a/apps/api/src/services/ai.service.ts b/apps/api/src/services/ai.service.ts new file mode 100644 index 0000000..12848a9 --- /dev/null +++ b/apps/api/src/services/ai.service.ts @@ -0,0 +1,14 @@ +import axios from "axios"; + +export const fetchAiRecommendation = async (quizData: any) => { + try { + const response = await axios.post( + "http://localhost:5000/api/data", + quizData + ); + return response.data; + } catch (err) { + console.error("AI Recommendation error:", err); + throw new Error("Failed to get AI recommendation"); + } +}; From 04377644af417fa13eb17e99255ee9f1ee507a4a Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:54:14 +0300 Subject: [PATCH 12/22] feat(daily-quiz): implement daily quiz service with CRUD operations --- apps/api/src/services/daily-quiz.service.ts | 67 +++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 apps/api/src/services/daily-quiz.service.ts diff --git a/apps/api/src/services/daily-quiz.service.ts b/apps/api/src/services/daily-quiz.service.ts new file mode 100644 index 0000000..c24acf7 --- /dev/null +++ b/apps/api/src/services/daily-quiz.service.ts @@ -0,0 +1,67 @@ +// Local prisma client +import { prisma } from "../lib/prisma.js"; + +const startOfDay = (d: Date) => + new Date(d.getFullYear(), d.getMonth(), d.getDate()); +const nextDay = (d: Date) => + new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1); + +export const findTodayDailyQuiz = async ( + userId: string, + refDate = new Date() +) => { + return prisma.dailyQuiz.findFirst({ + where: { + userId, + createdAt: { gte: startOfDay(refDate), lt: nextDay(refDate) }, + }, + orderBy: { createdAt: "desc" }, + }); +}; + +export const createDailyQuiz = async ( + userId: string, + totalQuestions: number +) => { + return prisma.dailyQuiz.create({ + data: { userId, totalQuestions, score: 0 }, + }); +}; + +export const saveOrUpdateDailyQuiz = async ( + userId: string, + refDate: Date, + totalQuestions: number +) => { + const existing = await findTodayDailyQuiz(userId, refDate); + if (existing) { + if (existing.totalQuestions !== totalQuestions) { + return prisma.dailyQuiz.update({ + where: { id: existing.id }, + data: { totalQuestions }, + }); + } + return existing; + } + return createDailyQuiz(userId, totalQuestions); +}; + +export const submitDailyQuiz = async (quizId: string, score: number) => { + return prisma.dailyQuiz.update({ + where: { id: quizId }, + data: { score, submittedAt: new Date() }, + }); +}; + +export const updateDailyQuizScoreByUserToday = async ( + userId: string, + refDate: Date, + score: number +) => { + const quiz = await findTodayDailyQuiz(userId, refDate); + if (!quiz) return null; + return prisma.dailyQuiz.update({ + where: { id: quiz.id }, + data: { score, submittedAt: new Date() }, + }); +}; From 1f6b5e2066061130cbc9d4a1644c5fb4bfbd8f44 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:54:18 +0300 Subject: [PATCH 13/22] feat(questions): add fetchQuestionsByRecommendation function to retrieve questions based on AI recommendations --- apps/api/src/services/question.service.ts | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 apps/api/src/services/question.service.ts diff --git a/apps/api/src/services/question.service.ts b/apps/api/src/services/question.service.ts new file mode 100644 index 0000000..db3f88b --- /dev/null +++ b/apps/api/src/services/question.service.ts @@ -0,0 +1,25 @@ +import { prisma } from "../lib/prisma.js"; + +export const fetchQuestionsByRecommendation = async ( + aiRecommendation: any, + totalQuestions: number +) => { + const levelsToFetch = aiRecommendation.topics.flatMap((topic: any) => + topic.recommendations.map((rec: any) => rec.level) + ); + + return prisma.question.findMany({ + where: { + topics: { + some: { + course: { + level: { + title: { in: levelsToFetch.map((lvl: number) => `Level ${lvl}`) }, + }, + }, + }, + }, + }, + take: totalQuestions, + }); +}; From 81440cda306ac1960ae3d25d4a186bdd01b79141 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:54:21 +0300 Subject: [PATCH 14/22] feat(quiz-data): implement buildUserQuizData function to aggregate user quiz topics and progress --- apps/api/src/services/quiz-data.service.ts | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 apps/api/src/services/quiz-data.service.ts diff --git a/apps/api/src/services/quiz-data.service.ts b/apps/api/src/services/quiz-data.service.ts new file mode 100644 index 0000000..a29e4a1 --- /dev/null +++ b/apps/api/src/services/quiz-data.service.ts @@ -0,0 +1,97 @@ +import { prisma } from "../lib/prisma.js"; + +export const buildUserQuizData = async ( + userId: string, + totalQuestions: number +) => { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + userCompletions: { + include: { + topic: { + include: { + course: { include: { level: true } }, + questions: true, + }, + }, + }, + }, + }, + }); + + const userCompletions = user?.userCompletions; + if (!userCompletions || userCompletions.length === 0) { + return null; + } + + const topicMap = new Map< + string, + { topic: string; available: { level: number; count: number }[] } + >(); + const progressMap = new Map< + string, + { + topic: string; + progressByLevel: { level: number; solved: number; attempted: number }[]; + } + >(); + + for (const completion of userCompletions) { + const topic = completion.topic; + const course = topic.course; + const levelNumber = parseInt(course.level.title.match(/\d+/)?.[0] || "0"); + + // Build userTopics + if (!topicMap.has(topic.title)) { + topicMap.set(topic.title, { + topic: topic.title, + available: [{ level: levelNumber, count: topic.questions.length }], + }); + } else { + const available = topicMap.get(topic.title)!.available; + const levelEntry = available.find((a) => a.level === levelNumber); + if (levelEntry) { + levelEntry.count += topic.questions.length; + } else { + available.push({ level: levelNumber, count: topic.questions.length }); + } + } + + // Build userProgress + if (!progressMap.has(topic.title)) { + progressMap.set(topic.title, { + topic: topic.title, + progressByLevel: [ + { + level: levelNumber, + solved: topic.solved, + attempted: topic.attempted, + }, + ], + }); + } else { + const progressByLevel = progressMap.get(topic.title)!.progressByLevel; + const progressEntry = progressByLevel.find( + (p) => p.level === levelNumber + ); + if (progressEntry) { + progressEntry.solved += topic.solved; + progressEntry.attempted += topic.attempted; + } else { + progressByLevel.push({ + level: levelNumber, + solved: topic.solved, + attempted: topic.attempted, + }); + } + } + } + + return { + userId, + totalQuestions, + userTopics: Array.from(topicMap.values()), + userProgress: Array.from(progressMap.values()), + }; +}; From 88371c7d221d5cf7aaea786a7c18550d6931fca9 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Wed, 13 Aug 2025 22:54:25 +0300 Subject: [PATCH 15/22] feat(quiz-submission): add gradeAnswers function to evaluate quiz submissions and calculate scores --- .../src/services/quiz-submission.service.ts | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 apps/api/src/services/quiz-submission.service.ts diff --git a/apps/api/src/services/quiz-submission.service.ts b/apps/api/src/services/quiz-submission.service.ts new file mode 100644 index 0000000..fab58a5 --- /dev/null +++ b/apps/api/src/services/quiz-submission.service.ts @@ -0,0 +1,75 @@ +import { prisma } from "../lib/prisma.js"; + +export interface SubmittedAnswerInput { + questionId: string; + choiceIndex: number; +} + +export interface GradedAnswerResult { + questionId: string; + userChoiceIndex: number | null; + correctOptionIndex: number; + isCorrect: boolean; +} + +export interface GradeQuizResult { + graded: GradedAnswerResult[]; + correctCount: number; + total: number; + scorePercentage: number; // 0-100 +} + +/** + * Grades a batch of answers. Missing or invalid questions are ignored but still counted toward total if they existed in input. + */ +export const gradeAnswers = async ( + answers: SubmittedAnswerInput[] +): Promise => { + // Dedupe by questionId (keep last answer provided by user) + const map = new Map(); + for (const a of answers) { + if (a && a.questionId) map.set(a.questionId, a); + } + const uniqueAnswers = Array.from(map.values()); + if (uniqueAnswers.length === 0) { + return { graded: [], correctCount: 0, total: 0, scorePercentage: 0 }; + } + + const questionIds = uniqueAnswers.map((a) => a.questionId); + const questions = await prisma.question.findMany({ + where: { id: { in: questionIds } }, + select: { id: true, correctOptionIndex: true }, + }); + const questionMap = new Map< + string, + { id: string; correctOptionIndex: number } + >( + questions.map((q: { id: string; correctOptionIndex: number }) => [q.id, q]) + ); + + const graded: GradedAnswerResult[] = uniqueAnswers.map((a) => { + const q = questionMap.get(a.questionId); + if (!q) { + return { + questionId: a.questionId, + userChoiceIndex: a.choiceIndex ?? null, + correctOptionIndex: -1, + isCorrect: false, + }; + } + const isCorrect = a.choiceIndex === q.correctOptionIndex; + return { + questionId: q.id, + userChoiceIndex: a.choiceIndex ?? null, + correctOptionIndex: q.correctOptionIndex, + isCorrect, + }; + }); + + const correctCount = graded.filter((g) => g.isCorrect).length; + const total = graded.length; + const scorePercentage = + total === 0 ? 0 : +((correctCount / total) * 100).toFixed(2); + + return { graded, correctCount, total, scorePercentage }; +}; From a60b30f428a5f35f0ea3bcc4be8838df5c1c8b4b Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Tue, 19 Aug 2025 22:46:08 +0300 Subject: [PATCH 16/22] feat(daily-quiz): add quizDate field and update unique constraint for DailyQuiz model --- apps/api/prisma/schema.prisma | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index b83210a..2454dea 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -139,8 +139,9 @@ model DailyQuiz { updatedAt DateTime @updatedAt submittedAt DateTime? totalQuestions Int @default(10) + quizDate DateTime // Should be set to the date (midnight) of the quiz, without time component - @@unique([userId, createdAt]) + @@unique([userId, quizDate]) } model User { From 498b256403f8ea2d0d0f0c168c7d05854b14677f Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Tue, 19 Aug 2025 22:46:13 +0300 Subject: [PATCH 17/22] feat(calendar): implement getQuizSubmissionCalendar function to retrieve quiz submission days for a given month --- .../api/src/controller/calendar.controller.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 apps/api/src/controller/calendar.controller.ts diff --git a/apps/api/src/controller/calendar.controller.ts b/apps/api/src/controller/calendar.controller.ts new file mode 100644 index 0000000..d7b2d15 --- /dev/null +++ b/apps/api/src/controller/calendar.controller.ts @@ -0,0 +1,84 @@ +import { Request, Response } from "express"; +import type { Session, User } from "better-auth"; +import { prisma } from "../lib/prisma.js"; + +declare global { + // Ensure session typing just like other controllers + namespace Express { + interface Request { + session?: Session; + user?: User; + } + } +} + +interface ApiResponse { + status: string; + message?: string; + data?: T; +} +const send = (res: Response, code: number, body: ApiResponse) => + res.status(code).json(body); + +// Utility to get month boundaries in local time (00:00:00.000 inclusive to next month start exclusive) +const monthRange = (year: number, month0: number) => { + const start = new Date(year, month0, 1, 0, 0, 0, 0); + const end = new Date(year, month0 + 1, 1, 0, 0, 0, 0); + return { start, end }; +}; + +/** + * Returns an array of booleans (index 0 = day 1) for the requested month indicating + * which days the user submitted a quiz (DailyQuiz.submittedAt not null). + * Query params: year=YYYY, month=1-12 (defaults to current year/month if omitted) + */ +export const getQuizSubmissionCalendar = async ( + req: Request, + res: Response +): Promise => { + if (!req.session) { + send(res, 401, { status: "fail", message: "Unauthorized" }); + return; + } + const userId = req.session.userId; + + // Parse month/year with fallbacks + const now = new Date(); + const year = Number(req.query.year) || now.getFullYear(); + const monthParam = Number(req.query.month); // 1-12 + const month0 = + monthParam && monthParam >= 1 && monthParam <= 12 ? + monthParam - 1 + : now.getMonth(); + + try { + const { start, end } = monthRange(year, month0); + const daysInMonth = new Date(year, month0 + 1, 0).getDate(); + const days: boolean[] = Array(daysInMonth).fill(false); + + const submissions = await prisma.dailyQuiz.findMany({ + where: { + userId, + submittedAt: { not: null, gte: start, lt: end }, + }, + select: { submittedAt: true }, + }); + + for (const s of submissions) { + if (!s.submittedAt) continue; + const day = s.submittedAt.getDate(); // 1-based + if (day >= 1 && day <= daysInMonth) days[day - 1] = true; + } + + send(res, 200, { + status: "success", + data: { year, month: month0 + 1, days }, + }); + } catch (err) { + console.error("[getQuizSubmissionCalendar] error", err); + send(res, 500, { + status: "error", + message: "Failed to fetch calendar", + }); + } +}; From 4960098e21f68f4895cf44f17d2be73e6ae5672e Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Tue, 19 Aug 2025 22:46:18 +0300 Subject: [PATCH 18/22] feat(calendar): remove getQuizSubmissionCalendar function and associated code --- .../api/src/controller/calender.controller.ts | 84 ------------------- 1 file changed, 84 deletions(-) delete mode 100644 apps/api/src/controller/calender.controller.ts diff --git a/apps/api/src/controller/calender.controller.ts b/apps/api/src/controller/calender.controller.ts deleted file mode 100644 index d7b2d15..0000000 --- a/apps/api/src/controller/calender.controller.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Request, Response } from "express"; -import type { Session, User } from "better-auth"; -import { prisma } from "../lib/prisma.js"; - -declare global { - // Ensure session typing just like other controllers - namespace Express { - interface Request { - session?: Session; - user?: User; - } - } -} - -interface ApiResponse { - status: string; - message?: string; - data?: T; -} -const send = (res: Response, code: number, body: ApiResponse) => - res.status(code).json(body); - -// Utility to get month boundaries in local time (00:00:00.000 inclusive to next month start exclusive) -const monthRange = (year: number, month0: number) => { - const start = new Date(year, month0, 1, 0, 0, 0, 0); - const end = new Date(year, month0 + 1, 1, 0, 0, 0, 0); - return { start, end }; -}; - -/** - * Returns an array of booleans (index 0 = day 1) for the requested month indicating - * which days the user submitted a quiz (DailyQuiz.submittedAt not null). - * Query params: year=YYYY, month=1-12 (defaults to current year/month if omitted) - */ -export const getQuizSubmissionCalendar = async ( - req: Request, - res: Response -): Promise => { - if (!req.session) { - send(res, 401, { status: "fail", message: "Unauthorized" }); - return; - } - const userId = req.session.userId; - - // Parse month/year with fallbacks - const now = new Date(); - const year = Number(req.query.year) || now.getFullYear(); - const monthParam = Number(req.query.month); // 1-12 - const month0 = - monthParam && monthParam >= 1 && monthParam <= 12 ? - monthParam - 1 - : now.getMonth(); - - try { - const { start, end } = monthRange(year, month0); - const daysInMonth = new Date(year, month0 + 1, 0).getDate(); - const days: boolean[] = Array(daysInMonth).fill(false); - - const submissions = await prisma.dailyQuiz.findMany({ - where: { - userId, - submittedAt: { not: null, gte: start, lt: end }, - }, - select: { submittedAt: true }, - }); - - for (const s of submissions) { - if (!s.submittedAt) continue; - const day = s.submittedAt.getDate(); // 1-based - if (day >= 1 && day <= daysInMonth) days[day - 1] = true; - } - - send(res, 200, { - status: "success", - data: { year, month: month0 + 1, days }, - }); - } catch (err) { - console.error("[getQuizSubmissionCalendar] error", err); - send(res, 500, { - status: "error", - message: "Failed to fetch calendar", - }); - } -}; From 3f812642e76b8f234ac1bdc502687a7c6d921ea9 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Tue, 19 Aug 2025 22:46:24 +0300 Subject: [PATCH 19/22] fix(quizzes): correct import path for getQuizSubmissionCalendar function --- apps/api/src/routes/quizzes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/routes/quizzes.ts b/apps/api/src/routes/quizzes.ts index 44c0f5c..986ae87 100644 --- a/apps/api/src/routes/quizzes.ts +++ b/apps/api/src/routes/quizzes.ts @@ -3,7 +3,7 @@ import { validate } from "../middlewares/validate.js"; import { requireAuth } from "../middlewares/auth.js"; import { submitDailyQuizBodySchema } from "../schemas/quizzes.js"; import { getQuiz, submitQuiz } from "../controller/quiz.controller.js"; -import { getQuizSubmissionCalendar } from "../controller/calender.controller.js"; +import { getQuizSubmissionCalendar } from "../controller/calendar.controller.js"; const router = Router(); From 309a7e2395108e9a4c1a78fd593ee1e0be3453e9 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Tue, 19 Aug 2025 22:46:31 +0300 Subject: [PATCH 20/22] chore(ai.service): no code changes made --- apps/api/src/services/ai.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/ai.service.ts b/apps/api/src/services/ai.service.ts index 12848a9..f37d205 100644 --- a/apps/api/src/services/ai.service.ts +++ b/apps/api/src/services/ai.service.ts @@ -2,8 +2,9 @@ import axios from "axios"; export const fetchAiRecommendation = async (quizData: any) => { try { + const aiApiUrl = process.env.AI_API_URL || "http://localhost:5000/api/data"; const response = await axios.post( - "http://localhost:5000/api/data", + aiApiUrl, quizData ); return response.data; From ffd1bbe9eef09d31d3f775322e8ba662d42bd9d1 Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Tue, 19 Aug 2025 22:46:45 +0300 Subject: [PATCH 21/22] fix(quiz-submission): replace hardcoded value with constant for invalid question index --- apps/api/src/services/quiz-submission.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/api/src/services/quiz-submission.service.ts b/apps/api/src/services/quiz-submission.service.ts index fab58a5..f957198 100644 --- a/apps/api/src/services/quiz-submission.service.ts +++ b/apps/api/src/services/quiz-submission.service.ts @@ -19,6 +19,8 @@ export interface GradeQuizResult { scorePercentage: number; // 0-100 } +const INVALID_QUESTION_INDEX = -1; + /** * Grades a batch of answers. Missing or invalid questions are ignored but still counted toward total if they existed in input. */ @@ -53,7 +55,7 @@ export const gradeAnswers = async ( return { questionId: a.questionId, userChoiceIndex: a.choiceIndex ?? null, - correctOptionIndex: -1, + correctOptionIndex: INVALID_QUESTION_INDEX, isCorrect: false, }; } From fc0686b2e1724226ae73e16c14498cbadeea909c Mon Sep 17 00:00:00 2001 From: NaderMohamed325 Date: Tue, 19 Aug 2025 22:47:41 +0300 Subject: [PATCH 22/22] feat(migration): add quizDate column and unique constraint to DailyQuiz table --- .../20250819194732_adding_quiz_date/migration.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 apps/api/prisma/migrations/20250819194732_adding_quiz_date/migration.sql diff --git a/apps/api/prisma/migrations/20250819194732_adding_quiz_date/migration.sql b/apps/api/prisma/migrations/20250819194732_adding_quiz_date/migration.sql new file mode 100644 index 0000000..0171224 --- /dev/null +++ b/apps/api/prisma/migrations/20250819194732_adding_quiz_date/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId,quizDate]` on the table `DailyQuiz` will be added. If there are existing duplicate values, this will fail. + - Added the required column `quizDate` to the `DailyQuiz` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropIndex +DROP INDEX "DailyQuiz_userId_createdAt_key"; + +-- AlterTable +ALTER TABLE "DailyQuiz" ADD COLUMN "quizDate" TIMESTAMP(3) NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "DailyQuiz_userId_quizDate_key" ON "DailyQuiz"("userId", "quizDate");