Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import express from "express";
import { auth } from "./lib/auth.js";
import { toNodeHandler } from "better-auth/node";
import { admin, adminRouter } from "./lib/admin.js";
import api from "./routes/index.js";
import { notesRouter } from "./routes/notes.js";

const app = express();
Expand All @@ -13,5 +14,6 @@ app.use(admin.options.rootPath, adminRouter);
console.log(`AdminJS is running under ${admin.options.rootPath}`);
app.use(express.json());
app.use(notesRouter);
app.use("/api", api);

export default app;
70 changes: 70 additions & 0 deletions apps/api/src/routes/courses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import exprees from "express";
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement has a typo. 'exprees' should be 'express'.

Copilot uses AI. Check for mistakes.
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 persentage = (completedTopics.length / totalTopics.length) * 100;
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division by zero error possible. If totalTopics.length is 0, this will result in NaN. Add a check to handle the case when there are no topics.

Suggested change
const persentage = (completedTopics.length / totalTopics.length) * 100;
const persentage =
totalTopics.length === 0
? 0
: (completedTopics.length / totalTopics.length) * 100;

Copilot uses AI. Check for mistakes.

res.status(200).json({
course,
totalTopics,
persentage,
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name has a typo. 'persentage' should be 'percentage'.

Suggested change
persentage,
const percentage = (completedTopics.length / totalTopics.length) * 100;
res.status(200).json({
course,
totalTopics,
percentage,

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

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

const completedTopics = await topicService.getCompletedTopics(
req.user!.id,
req.params.courseId,
);

let filterdTopics;
if (completed === "true") {
filterdTopics = completedTopics;
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name has a typo. 'filterdTopics' should be 'filteredTopics'.

Suggested change
filterdTopics = completedTopics;
let filteredTopics;
if (completed === "true") {
filteredTopics = completedTopics;

Copilot uses AI. Check for mistakes.
} else if (completed === "false") {
const completedTopicsIds = new Set();
for (const topic of completedTopics) {
completedTopicsIds.add(topic.id);
}
const unCompletedTopics = totalTopics.filter(
(ele) => !completedTopicsIds.has(ele.id),
);
filterdTopics = unCompletedTopics;
} else {
filterdTopics = totalTopics;
}

res.status(200).json({
filterdTopics,
});
},
);

export { router as coursesRouter };
17 changes: 17 additions & 0 deletions apps/api/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import express from "express";
const router = express.Router();
import { tracksRouter } from "./tracks.js";
import { coursesRouter } from "./courses.js";
import { topicsRouter } from "./topics.js";

router.get("/", (req, res) => {
res.status(200).json({
message: "hello from api.",
});
});

router.use("/tracks", tracksRouter);
router.use("/courses", coursesRouter);
router.use("/topics", topicsRouter);

export default router;
40 changes: 40 additions & 0 deletions apps/api/src/routes/topics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import exprees from "express";
const router = exprees.Router();
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement has a typo. 'exprees' should be 'express'.

Suggested change
const router = exprees.Router();
import express from "express";
const router = express.Router();

Copilot uses AI. Check for mistakes.
import { validate } from "../middlewares/validate.js";
import { getTopicParamsSchema } from "../schemas/topics.js";
import * as Service from "../services/topics.js";
import { requireAuth } from "../middlewares/auth.js";

router.get(
"/:topicId",
validate({ params: getTopicParamsSchema }),
async (req, res) => {
const topic = await Service.getTopic(req.params.topicId);

res.status(200).json({
topic,
});
},
);
router.post(
"/:topicId/completion",
requireAuth,
validate({ params: getTopicParamsSchema }),
async (req, res) => {
const topic = await Service.completeTopic(req.user!.id, req.params.topicId);
res.status(201).json({
topic,
});
},
);
router.delete(
"/:topicId/completion",
requireAuth,
validate({ params: getTopicParamsSchema }),
async (req, res) => {
await Service.inCompleteTopic(req.user!.id, req.params.topicId);
res.status(204).json({});
},
);

export { router as topicsRouter };
60 changes: 60 additions & 0 deletions apps/api/src/routes/tracks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import exprees from "express";
const router = exprees.Router();
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import statement has a typo. 'exprees' should be 'express'.

Suggested change
const router = exprees.Router();
import express from "express";
const router = express.Router();

Copilot uses AI. Check for mistakes.
import { Prisma } from "../generated/prisma/client.js";
import { validate } from "../middlewares/validate.js";
import {
getTrackQuerySchema,
getTrackParamsSchema,
} from "../schemas/tracks.js";
import * as Service from "../services/tracks.js";
import { requireAuth } from "../middlewares/auth.js";
import * as courseSrvice from "../services/courses.js";

router.get("/", async (req, res) => {
const tracks = await Service.getAllTracks();

res.status(200).json({
length: tracks.length,
data: tracks,
});
});
router.get(
"/:trackId",
requireAuth,
validate({ params: getTrackParamsSchema, query: getTrackQuerySchema }),
async (req, res) => {
const { levelId } = req.query as { levelId: string };
const track = await Service.getTrack(req.params.trackId);

const courses = await courseSrvice.getCourses(
levelId,
req.params.trackId,
req.user!.id,
);
const progress = courses.map(
(
course: Prisma.CourseGetPayload<{
include: { topics: { select: { userCompletions: true } } };
}>,
) => {
const totalTopics = course.topics.length;
const completedTopics = course.topics.filter(
(topic) => topic.userCompletions.length > 0,
).length;
const percentage = (completedTopics / totalTopics) * 100;

return {
id: course.id,
title: course.title,
percentage,
};
},
);
res.status(200).json({
track,
courses: progress,
});
},
);

export { router as tracksRouter };
9 changes: 9 additions & 0 deletions apps/api/src/schemas/courses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import z from "zod";

export const getCourseParamsSchema = z.object({
courseId: z.string().min(1, "courseId is required"),
});

export const getCourseQuerySchema = z.object({
completed: z.string().optional(),
});
5 changes: 5 additions & 0 deletions apps/api/src/schemas/topics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import z from "zod";

export const getTopicParamsSchema = z.object({
topicId: z.string().min(1, "topicId is required"),
});
8 changes: 8 additions & 0 deletions apps/api/src/schemas/tracks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import z from "zod";

export const getTrackParamsSchema = z.object({
trackId: z.string().min(1, "trackId is required"),
});
export const getTrackQuerySchema = z.object({
levelId: z.string().optional(),
});
61 changes: 61 additions & 0 deletions apps/api/src/services/courses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { prisma } from "../lib/prisma.js";

export const getCourse = async (courseId: string) => {
return await prisma.course.findUnique({
where: {
id: courseId,
},
});
};

export const getToltalTopics = async (courseId: string) => {
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function name has a typo. 'getToltalTopics' should be 'getTotalTopics'.

Suggested change
export const getToltalTopics = async (courseId: string) => {
export const getTotalTopics = async (courseId: string) => {

Copilot uses AI. Check for mistakes.
return await prisma.topic.findMany({
where: {
courseId,
},
select: {
title: true,
},
orderBy: {
order: "asc",
},
});
};

Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate function 'getToltalTopics' exists in both courses.ts and topics.ts services. Consider consolidating this logic to avoid code duplication.

Suggested change

Copilot uses AI. Check for mistakes.
export const getCompletedTopics = async (userId: string, courseId: string) => {
return await prisma.userCompletion.findMany({
where: {
userId,
topic: {
courseId,
},
},
});
};

export const getCourses = async (
levelId: string,
trackId: string,
userId: string,
) => {
return await prisma.course.findMany({
where: {
trackId,
...(levelId && { levelId }),
},
orderBy: {
order: "asc",
},
include: {
topics: {
select: {
userCompletions: {
where: {
userId,
},
},
},
},
},
});
};
54 changes: 54 additions & 0 deletions apps/api/src/services/topics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { prisma } from "../lib/prisma.js";

export const getTopic = async (topicId: string) => {
return prisma.topic.findUnique({
where: {
id: topicId,
},
select: {
title: true,
durationInMinutes: true,
content: true,
},
});
};

export const completeTopic = async (userId: string, topicId: string) => {
return await prisma.userCompletion.create({
data: {
userId,
topicId,
},
});
};
export const inCompleteTopic = async (userId: string, topicId: string) => {
prisma.userCompletion.delete({
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing 'return' or 'await' statement. The delete operation is not being returned or awaited, which could cause issues with error handling and response timing.

Suggested change
prisma.userCompletion.delete({
return await prisma.userCompletion.delete({

Copilot uses AI. Check for mistakes.
where: {
userId_topicId: { userId, topicId },
},
});
};

export const getToltalTopics = async (courseId: string) => {
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function name has a typo. 'getToltalTopics' should be 'getTotalTopics'.

Suggested change
export const getToltalTopics = async (courseId: string) => {
export const getTotalTopics = async (courseId: string) => {

Copilot uses AI. Check for mistakes.
return await prisma.topic.findMany({
where: {
courseId,
},
orderBy: {
order: "asc",
},
});
};
export const getCompletedTopics = async (userId: string, courseId: string) => {
return await prisma.userCompletion.findMany({
where: {
userId,
topic: {
courseId,
},
},
include: {
topic: true,
},
});
};
25 changes: 25 additions & 0 deletions apps/api/src/services/tracks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { prisma } from "../lib/prisma.js";

export const getAllTracks = async () => {
const tracks = await prisma.track.findMany({
select: {
id: true,
title: true,
},
});
return tracks;
};

export const getTrack = async (trackId: string) => {
const track = await prisma.track.findUnique({
where: {
id: trackId,
},
select: {
id: true,
title: true,
description: true,
},
});
return track;
};