Skip to content
Closed
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
3 changes: 2 additions & 1 deletion apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ 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";
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("/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";
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;

res.status(200).json({
course,
totalTopics,
persentage,
});
},
);
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;
} 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();
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();
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) => {
return await prisma.topic.findMany({
where: {
courseId,
},
select: {
title: true,
},
orderBy: {
order: "asc",
},
});
};

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({
where: {
userId_topicId: { userId, topicId },
},
});
};

export const getToltalTopics = async (courseId: string) => {
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;
};
Loading