Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7882b04
refactor: remove unused quiz and recommendation service functions
NaderMohamed325 Aug 13, 2025
0fb3a0b
fix(pnpm-lock): update dependency versions and add axios
NaderMohamed325 Aug 13, 2025
684fdc7
fix(package.json): ensure axios dependency is included
NaderMohamed325 Aug 13, 2025
6ba0573
feat(prisma): add difficulty and attempt tracking to Topic model; set…
NaderMohamed325 Aug 13, 2025
6b4575f
feat(migration): add totalQuestions to DailyQuiz and attempted, diffi…
NaderMohamed325 Aug 13, 2025
2c972bb
chore: update app.ts for improved error handling and middleware confi…
NaderMohamed325 Aug 13, 2025
34d2001
feat(calendar): implement getQuizSubmissionCalendar to track user qui…
NaderMohamed325 Aug 13, 2025
37f6029
feat(quiz): implement getQuiz and submitQuiz endpoints for quiz manag…
NaderMohamed325 Aug 13, 2025
6dd9535
feat(quizzes): implement calendar endpoint for quiz submission tracking
NaderMohamed325 Aug 13, 2025
6a576b9
feat(quizzes): update submitDailyQuizBodySchema to require choiceInde…
NaderMohamed325 Aug 13, 2025
1eeb191
feat(ai): add fetchAiRecommendation function for AI-based quiz recomm…
NaderMohamed325 Aug 13, 2025
0437764
feat(daily-quiz): implement daily quiz service with CRUD operations
NaderMohamed325 Aug 13, 2025
1f6b5e2
feat(questions): add fetchQuestionsByRecommendation function to retri…
NaderMohamed325 Aug 13, 2025
81440cd
feat(quiz-data): implement buildUserQuizData function to aggregate us…
NaderMohamed325 Aug 13, 2025
88371c7
feat(quiz-submission): add gradeAnswers function to evaluate quiz sub…
NaderMohamed325 Aug 13, 2025
08b00d9
implement the user-topic completion feature and add the essential fun…
Aug 16, 2025
790354d
feat(api): implement notes feature
Elshahaby Aug 16, 2025
f6ded3d
fix(api): correct linting and formatting in note schemas
Elshahaby Aug 16, 2025
d685096
replace the completed course array with set
Aug 16, 2025
7335942
replace the forEach with for of
Aug 16, 2025
a60b30f
feat(daily-quiz): add quizDate field and update unique constraint for…
NaderMohamed325 Aug 19, 2025
498b256
feat(calendar): implement getQuizSubmissionCalendar function to retri…
NaderMohamed325 Aug 19, 2025
4960098
feat(calendar): remove getQuizSubmissionCalendar function and associa…
NaderMohamed325 Aug 19, 2025
3f81264
fix(quizzes): correct import path for getQuizSubmissionCalendar function
NaderMohamed325 Aug 19, 2025
309a7e2
chore(ai.service): no code changes made
NaderMohamed325 Aug 19, 2025
ffd1bbe
fix(quiz-submission): replace hardcoded value with constant for inval…
NaderMohamed325 Aug 19, 2025
fc0686b
feat(migration): add quizDate column and unique constraint to DailyQu…
NaderMohamed325 Aug 19, 2025
1dd8f91
feat(api): implement notes feature (#7)
saifsweelam Aug 19, 2025
3597b22
Merge branch 'dev' into main
saifsweelam Aug 19, 2025
180417a
implement course-completion feature (#10)
saifsweelam Aug 19, 2025
4e602d5
Merge branch 'dev' into main
saifsweelam Aug 19, 2025
4da4cbc
Refactor quiz management and enhance tracking features (#11)
saifsweelam Aug 19, 2025
2992d22
Refactor API structure and add Postman collection
saifsweelam Aug 30, 2025
28e91c0
apply sercvices
fouadhassan74 Sep 1, 2025
a776b6a
add modules
boshraemad Sep 2, 2025
be51752
import interfaces in hooks
boshraemad Sep 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,071 changes: 1,071 additions & 0 deletions OpenLearnPlatform-API.postman_collection.json

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions apps/api/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import globals from "globals";
import jsPlugin from "@eslint/js";
import tsPlugin from "typescript-eslint";
import unicornPlugin from 'eslint-plugin-unicorn';
import prettierConfig from 'eslint-config-prettier';
import prettierPluginRecommended from 'eslint-plugin-prettier/recommended';
import sonarjs from 'eslint-plugin-sonarjs';

import unicornPlugin from "eslint-plugin-unicorn";
// import prettierConfig from "eslint-config-prettier";
// import prettierPluginRecommended from "eslint-plugin-prettier/recommended";
import sonarjs from "eslint-plugin-sonarjs";

/** @type {import('eslint').Linter.Config[]} */
export default [
{
ignores: ["**/seed/**", "**/generated/**"]
ignores: ["**/seed/**", "**/generated/**"],
},
jsPlugin.configs.recommended,
unicornPlugin.configs['recommended'],
prettierPluginRecommended,
prettierConfig,
unicornPlugin.configs["recommended"],
// prettierPluginRecommended,
// prettierConfig,
sonarjs.configs.recommended,
...tsPlugin.configs.recommended,
{
Expand All @@ -29,9 +28,10 @@ export default [
"sonarjs/no-hardcoded-passwords": "off",
"sonarjs/cors": "off",
"@typescript-eslint/no-namespace": "off",
"unicorn/no-useless-undefined": "off",
},
languageOptions: {
globals: globals.node,
}
}
},
},
];
3 changes: 2 additions & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@
"@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",
"express": "^4.21.2",
"express-async-errors": "^3.1.1",
"zod": "^3.24.3"
"zod": "^4.0.17"
},
"devDependencies": {
"@better-auth/cli": "^1.2.12",
Expand Down
Empty file added apps/api/pnpm
Empty file.
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "Note" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"userId" TEXT NOT NULL,
"topicId" TEXT NOT NULL,
"courseId" TEXT NOT NULL,

CONSTRAINT "Note_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "Note" ADD CONSTRAINT "Note_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Note" ADD CONSTRAINT "Note_topicId_fkey" FOREIGN KEY ("topicId") REFERENCES "Topic"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Note" ADD CONSTRAINT "Note_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Warnings:

- You are about to drop the column `attempted` on the `Topic` table. All the data in the column will be lost.
- You are about to drop the column `difficulty` on the `Topic` table. All the data in the column will be lost.
- You are about to drop the column `solved` on the `Topic` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "Topic" DROP COLUMN "attempted",
DROP COLUMN "difficulty",
DROP COLUMN "solved";
27 changes: 27 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ model Course {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
topics Topic[]
notes Note[]
}

model Topic {
Expand All @@ -66,6 +67,26 @@ model Topic {
userCompletions UserCompletion[]
questions Question[]
userPerformances UserTopicPerformance[]

notes Note[]
}

model Note {
id String @id @default(cuid())
title String
content String @db.Text

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

// --- Relations ---
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
topicId String
topic Topic @relation(fields: [topicId], references: [id], onDelete: Cascade)
courseId String
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)

}

model UserCompletion {
Expand Down Expand Up @@ -134,6 +155,10 @@ model DailyQuiz {
createdAt DateTime @default(now())
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, quizDate])
}

model User {
Expand All @@ -158,6 +183,7 @@ model User {
trackId String?
joinedTrack Track? @relation(fields: [trackId], references: [id], onDelete: Cascade, name: "joinedTrack")
userCompletions UserCompletion[]
notes Note[]

// Instructors
createdTracks Track[]
Expand Down Expand Up @@ -226,3 +252,4 @@ model Jwks {

@@map("jwks")
}

9 changes: 8 additions & 1 deletion apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ import express from "express";
import { auth } from "./lib/auth.js";
import { toNodeHandler } from "better-auth/node";
import { admin, adminRouter } from "./lib/admin.js";
import { router, ROUTES_PREFIX } from "./router.js";
import { addEnhancedSendMethod } from "./middlewares/enhanced-send.js";

const app = express();

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}`);

app.use(express.json());
app.use(addEnhancedSendMethod);

app.use(ROUTES_PREFIX, router);

export default app;
12 changes: 12 additions & 0 deletions apps/api/src/errors/already-submitted-quiz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import BaseError from "./base.js";

export default class AlreadySubmittedQuiz extends BaseError<undefined> {
constructor() {
super(
"Quiz has already been submitted",
409,
undefined,
"You will be able to submit a new task by tomorrow",
);
}
}
12 changes: 12 additions & 0 deletions apps/api/src/errors/not-enough-topics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import BaseError from "./base.js";

export default class NotEnoughTopics extends BaseError<undefined> {
constructor() {
super(
"Not enough topics completed",
422,
undefined,
"Start to complete more topics to be able to perform this action",
);
}
}
12 changes: 12 additions & 0 deletions apps/api/src/errors/not-found.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import BaseError from "./base.js";

export class NotFoundError extends BaseError<undefined> {
constructor(hint?: string) {
super(
"Not Found: Ensure the requested resource exists.",
404,
undefined,
hint,
);
}
}
11 changes: 11 additions & 0 deletions apps/api/src/helpers/dates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const getMonthInterval = (month: number, year: number) => {
const start = new Date(year, month, 1);
const end = new Date(year, month + 1, 1);
return { start, end };
};

export const getStartOfDay = (d: Date) =>
new Date(d.getFullYear(), d.getMonth(), d.getDate());

export const getNextDay = (d: Date) =>
new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1);
7 changes: 6 additions & 1 deletion apps/api/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ declare global {
}
}

export const requireAuth: RequestHandler = async (req, res, next) => {
export const requireAuth: RequestHandler<
unknown,
unknown,
unknown,
unknown
> = async (req, res, next) => {
const data = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
Expand Down
28 changes: 28 additions & 0 deletions apps/api/src/middlewares/enhanced-send.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { RequestHandler } from "express";
import { Pagination } from "../schemas/pagination.js";

declare global {
namespace Express {
export interface Response {
enhancedSend: (
statusCode: number,
data: unknown,
pagination?: Pagination,
) => Response;
}
}
}

export const addEnhancedSendMethod: RequestHandler = (req, res, next) => {
res.enhancedSend = (statusCode, data, pagination) => {
const success = statusCode < 400;
return res.status(statusCode).json({
success,
statusCode,
timestamp: new Date(),
data,
pagination,
});
};
next();
};
14 changes: 14 additions & 0 deletions apps/api/src/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Router } from "express";
import { notesRouter } from "./routes/notes.js";
import { tracksRouter } from "./routes/tracks.js";
import { coursesRouter } from "./routes/courses.js";
import { topicsRouter } from "./routes/topics.js";

export const ROUTES_PREFIX = "/api";

export const router = Router();

router.use("/notes", notesRouter);
router.use("/tracks", tracksRouter);
router.use("/courses", coursesRouter);
router.use("/topics", topicsRouter);
54 changes: 54 additions & 0 deletions apps/api/src/routes/courses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import exprees from "express";
const router = exprees.Router();
import { requireAuth } from "../middlewares/auth.js";
import { validate } from "../middlewares/validate.js";
import {
getCourseParamsSchema,
getCourseQuerySchema,
} from "../schemas/courses.js";
import * as Service from "../services/courses.js";
import * as topicService from "../services/topics.js";

router.get(
"/:courseId",
requireAuth,
validate({ params: getCourseParamsSchema }),
async (req, res) => {
const course = await Service.getCourse(req.params.courseId);

const totalTopics = await Service.getToltalTopics(req.params.courseId);

const completedTopics = await Service.getCompletedTopics(
req.user!.id,
req.params.courseId,
);
const completedPercentage =
(completedTopics.length / totalTopics.length) * 100;

res.enhancedSend(200, {
...course,
topics: totalTopics,
completedPercentage,
});
},
);

router.get(
"/:courseId/topics",
requireAuth,
validate({ params: getCourseParamsSchema, query: getCourseQuerySchema }),
async (req, res) => {
const { completed } = req.query;

const topics = await topicService.getToltalTopics(
req.params.courseId,
completed
? { isCompleted: completed === "true", userId: req.user!.id }
: undefined,
);

res.enhancedSend(200, topics);
},
);

export { router as coursesRouter };
Loading