From 179d763f3fcd8283bf08e95a39a2180e437639e1 Mon Sep 17 00:00:00 2001 From: charlottenguyen05 Date: Mon, 5 Jan 2026 18:46:36 +0100 Subject: [PATCH 1/3] feat: implement star rating system --- .../components/courses/CourseDetailPage.tsx | 194 ++++++++++++++---- client/src/components/courses/CoursesPage.tsx | 69 ++++--- client/src/components/ui/Star.tsx | 27 +++ client/src/components/ui/StarRating.tsx | 33 +++ client/src/types/index.ts | 2 + client/src/utils/api.ts | 31 +++ docs/API.md | 55 +++++ server/prisma/schema.prisma | 18 +- server/routes/courses.js | 188 ++++++++++++++++- 9 files changed, 541 insertions(+), 76 deletions(-) create mode 100644 client/src/components/ui/Star.tsx create mode 100644 client/src/components/ui/StarRating.tsx diff --git a/client/src/components/courses/CourseDetailPage.tsx b/client/src/components/courses/CourseDetailPage.tsx index db07789..d8031db 100644 --- a/client/src/components/courses/CourseDetailPage.tsx +++ b/client/src/components/courses/CourseDetailPage.tsx @@ -1,7 +1,7 @@ "use client"; import type React from "react"; -import { useState, useEffect } from "react"; +import { useState, useEffect, use } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import { @@ -35,10 +35,14 @@ import { updateChapterProgress, generateCertificateTest, getUserTests, + rateCourse, + getCourseRatings, } from "../../utils/api"; import type { Course } from "../../types"; import { isAuthenticated } from "../../utils/auth"; import TestInstructionsModal from "./TestInstructionsModal"; +import StarRating from "../ui/StarRating"; +import { ToastContainer, ToastType } from "../ui/Toast"; const CourseDetailPage: React.FC = () => { const { id } = useParams<{ id: string }>(); @@ -66,6 +70,16 @@ const CourseDetailPage: React.FC = () => { nextAvailableAt: string; } | null>(null); const [cooldownTimer, setCooldownTimer] = useState(""); + const [rating, setRating] = useState(0); + const [averageRating, setAverageRating] = useState(0); + const [ratingCount, setRatingCount] = useState(0); + const [toasts, setToasts] = useState>([]); useEffect(() => { checkAuth(); @@ -82,6 +96,22 @@ const CourseDetailPage: React.FC = () => { } }, [isAuth, id, course]); + // Fetch user's existing rating when authenticated + useEffect(() => { + if (isAuth === true && id) { + console.log("⭐ Fetching user's existing rating..."); + fetchUserRating(); + } + }, [isAuth, id]); + + // Update rating states when course data is fetched + useEffect(() => { + if (course) { + setAverageRating(course.average_rating || 0); + setRatingCount(course.rating_count || 0); + } + }, [course]); + // Cooldown timer effect useEffect(() => { if (!testCooldown?.isActive) { @@ -120,6 +150,16 @@ const CourseDetailPage: React.FC = () => { return () => clearInterval(interval); }, [testCooldown]); + // Toast helper functions + const addToast = (toast: Omit) => { + const id = Date.now().toString(); + setToasts((prev) => [...prev, { ...toast, id }]); + }; + + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + // Check for navigation state (removed - no longer needed) useEffect(() => { // Navigation state handling removed as we now use separate results page @@ -567,6 +607,59 @@ const CourseDetailPage: React.FC = () => { navigate(`/courses/${course.id}/test/${testId}/results`); }; + const handleRatingSubmit = async (rating: number) => { + if (!course) return; + + // Validate that at least one star is selected + if (rating === 0) { + addToast({ + type: "error", + title: "Please select a rating", + message: "You must select at least one star before submitting.", + }); + return; + } + + try { + await rateCourse(course.id, rating); + setRating(rating); + addToast({ + type: "success", + title: "Thank you for your feedback!", + message: "Your rating has been submitted successfully.", + }); + + // Fetch updated ratings from server to get accurate average + await fetchUserRating(); + } catch (error) { + console.error("Error submitting rating:", error); + addToast({ + type: "error", + title: "Failed to submit rating", + message: "Please try again later.", + }); + } + }; + + const fetchUserRating = async () => { + try { + if (!id) return; + const response = await getCourseRatings(id); + + // Update all rating-related states from server response + setAverageRating(response.averageRating); + setRatingCount(response.totalRatings); + + if (response.hasRated && response.userRating !== null) { + setRating(response.userRating); + console.log("✅ User's existing rating loaded:", response.userRating); + } else { + console.log("â„šī¸ User hasn't rated this course yet"); + } + } catch (error) { + console.error("Error fetching ratings:", error); + } + }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString("en-US", { @@ -689,6 +782,12 @@ const CourseDetailPage: React.FC = () => { +
+ { }} /> + + {averageRating.toFixed(1)} ({ratingCount} ratings) + +
@@ -792,50 +891,64 @@ const CourseDetailPage: React.FC = () => { {/* Course Completion Certificate */} {course.enrollment_data.isCompleted && ( -
-
-
- -
-

- Congratulations! -

-

- You've completed this course -

+
+
+
+
+ +
+

+ Congratulations! +

+

+ You've completed this course +

+
-
- {/* Show cooldown timer if active */} - {testCooldown?.isActive ? ( -
-
-
- Next test available in: -
-
- {cooldownTimer} + {/* Show cooldown timer if active */} + {testCooldown?.isActive ? ( +
+
+
+ Next test available in: +
+
+ {cooldownTimer} +
-
- ) : ( - + )} +
+
+
+ Rate this course: + +
+ - )} +
+ + + )} {/* Test Results Section */} @@ -1368,7 +1481,10 @@ const CourseDetailPage: React.FC = () => { questions={currentTest.questions} /> )} -
+ + {/* Toast Notifications */} + +
); }; diff --git a/client/src/components/courses/CoursesPage.tsx b/client/src/components/courses/CoursesPage.tsx index 6908620..61da9b7 100644 --- a/client/src/components/courses/CoursesPage.tsx +++ b/client/src/components/courses/CoursesPage.tsx @@ -14,6 +14,7 @@ import { Loader2, } from "lucide-react"; import SEO from "../seo/SEO"; +import StarRating from "../ui/StarRating"; import { getCourses, toggleCourseBookmark, @@ -117,12 +118,12 @@ const CoursesPage: React.FC = () => { prevCourses.map((course) => course.id === courseId ? { - ...course, - is_bookmarked: response.bookmarked, - bookmark_count: response.bookmarked - ? course.bookmark_count + 1 - : course.bookmark_count - 1, - } + ...course, + is_bookmarked: response.bookmarked, + bookmark_count: response.bookmarked + ? course.bookmark_count + 1 + : course.bookmark_count - 1, + } : course ) ); @@ -151,9 +152,9 @@ const CoursesPage: React.FC = () => { prevCourses.map((course) => course.id === courseId ? { - ...course, - is_enrolled: !isEnrolled, - } + ...course, + is_enrolled: !isEnrolled, + } : course ) ); @@ -296,12 +297,12 @@ const CoursesPage: React.FC = () => { {filter === "my-courses" ? "You haven't created any courses yet." : filter === "bookmarked" - ? "You haven't bookmarked any courses yet." - : filter === "enrolled" - ? "You haven't enrolled in any courses yet." - : searchTerm - ? "Try adjusting your search terms." - : "Be the first to create a course!"} + ? "You haven't bookmarked any courses yet." + : filter === "enrolled" + ? "You haven't enrolled in any courses yet." + : searchTerm + ? "Try adjusting your search terms." + : "Be the first to create a course!"}

{isAuth && ( {

{course.description}

+ + {/* Rating Display */} +
+ { }} + /> + + {course.average_rating == 0 ? "" : course.average_rating.toFixed(1)} + + + ({course.rating_count}{" "} + {course.rating_count <= 1 + ? "rating" + : "ratings"} + ) + +
{isAuth && ( diff --git a/client/src/components/ui/Star.tsx b/client/src/components/ui/Star.tsx new file mode 100644 index 0000000..8d38f32 --- /dev/null +++ b/client/src/components/ui/Star.tsx @@ -0,0 +1,27 @@ +import { Star as StarIcon } from 'lucide-react' +import { cn } from '../../lib/utils' + +interface StarProps { + filled: boolean + extraClass?: string + readonly: boolean + onMouseEnter?: () => void + onClick?: () => void +} + +const Star = ({ filled, extraClass, readonly, onMouseEnter, onClick }: StarProps) => { + + return ( + + ) +} + +export default Star diff --git a/client/src/components/ui/StarRating.tsx b/client/src/components/ui/StarRating.tsx new file mode 100644 index 0000000..d710501 --- /dev/null +++ b/client/src/components/ui/StarRating.tsx @@ -0,0 +1,33 @@ +import Star from "./Star"; +import { useState } from "react"; + +interface StarRatingProps { + size?: "size-4" | "size-6" | "size-8"; // size-4 = small, size-6 = medium, size-8 = large + readonly: boolean; + rating: number; + onRatingChange: (rating: number) => void; +} + +const StarRating = ({ size = "size-6", readonly, rating, onRatingChange }: StarRatingProps) => { + const [hoverRating, setHoverRating] = useState(0); + + return ( +
!readonly && setHoverRating(0)} + > + {[1, 2, 3, 4, 5].map((star) => ( + !readonly && setHoverRating(star)} + onClick={() => !readonly && onRatingChange(star)} + /> + ))} +
+ ); +}; + +export default StarRating; diff --git a/client/src/types/index.ts b/client/src/types/index.ts index 6feb16d..32a586e 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -43,6 +43,8 @@ export interface Course { chapter_count: number; bookmark_count: number; enrollment_count: number; + average_rating: number; + rating_count: number; is_bookmarked: boolean; is_enrolled: boolean; enrollment_data: CourseEnrollment | null; diff --git a/client/src/utils/api.ts b/client/src/utils/api.ts index c87720e..8800d9d 100644 --- a/client/src/utils/api.ts +++ b/client/src/utils/api.ts @@ -613,6 +613,37 @@ export const deleteCourse = async ( return response.data; }; +export const rateCourse = async ( + courseId: string, + rating: number +): Promise<{ + message: string; + rating: number; +}> => { + const response = await api.post(`/courses/${courseId}/rate`, { rating }); + return response.data; +}; + +export const getCourseRatings = async ( + courseId: string +): Promise<{ + ratings: Array<{ + id: string; + rating: number; + user: { + id: string; + username: string; + }; + }>; + averageRating: number; + totalRatings: number; + userRating: number | null; + hasRated: boolean; +}> => { + const response = await api.get(`/courses/${courseId}/rate`); + return response.data; +}; + // Course enrollment functions export const enrollInCourse = async ( courseId: string diff --git a/docs/API.md b/docs/API.md index f1e9ae4..745c3c3 100644 --- a/docs/API.md +++ b/docs/API.md @@ -208,6 +208,61 @@ PUT /api/courses/:id { "message": "Course updated successfully", "course": { /* updated */ } } ``` +GET /api/courses/:id/rate + +- Authentication: Optional +- Request body: None +- Response (200): + ```json + { + "ratings": [ + { + "id": "rating_id_1", + "rating": 5, + "user": { + "id": "user_id_1", + "username": "john_doe" + } + }, + { + "id": "rating_id_2", + "rating": 4, + "user": { + "id": "user_id_2", + "username": "jane_smith" + } + } + ], + "averageRating": 4.5, + "totalRatings": 2, + "userRating": 5, + "hasRated": true + } + ``` +- Description: Get all ratings for a course with user information. If authenticated, also returns the current user's rating. Returns `userRating: null` and `hasRated: false` if user hasn't rated or is not authenticated. + +POST /api/courses/:id/rate +- Authentication: Yes (Bearer token) +- Request body: + ```json + { "rating": 5 } + ``` +- Response (200): + ```json + { + "message": "Rating submitted successfully", + "rating": 5 + } + ``` + OR (if updating existing rating): + ```json + { + "message": "Rating updated successfully", + "rating": 4 + } + ``` +- Description: Submit or update a rating for a course. The `rating` must be a number between 1 and 5 (inclusive). If the user has already rated the course, their existing rating will be updated. If not, a new rating will be created. Only authenticated users can rate courses. + DELETE /api/courses/:id - Authentication: Yes - Response (200): diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index ac165f1..aff6535 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -42,6 +42,7 @@ model User { sentNotifications Notification[] @relation("NotificationSender") courses Course[] bookmarkedCourses CourseBookmark[] + ratings CourseRating[] enrolledCourses CourseEnrollment[] chapterProgress ChapterProgress[] courseTests CourseTest[] @@ -247,6 +248,7 @@ model Course { bookmarks CourseBookmark[] enrollments CourseEnrollment[] tests CourseTest[] + ratings CourseRating[] @@map("courses") } @@ -450,4 +452,18 @@ model CourseTest { @@unique([courseId, userId, createdAt]) @@map("course_tests") -} \ No newline at end of file +} + +model CourseRating { + id String @id @default(auto()) @map("_id") @db.ObjectId + courseId String @map("course_id") @db.ObjectId + userId String @map("user_id") @db.ObjectId + rating Int // Rating value (1 to 5) + + // Relations + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id]) + + @@unique([courseId, userId]) + @@map("course_ratings") +} diff --git a/server/routes/courses.js b/server/routes/courses.js index 5755858..8b100dc 100644 --- a/server/routes/courses.js +++ b/server/routes/courses.js @@ -276,10 +276,14 @@ router.get("/", optionalAuth, async (req, res) => { select: { id: true }, take: 0, }, + ratings: { + select: { rating: true }, + }, _count: { select: { bookmarks: true, enrollments: true, + ratings: true, }, }, }, @@ -291,17 +295,26 @@ router.get("/", optionalAuth, async (req, res) => { ]); // Transform the response - const transformedCourses = courses.map((course) => ({ - ...course, - author_username: course.author.username, - chapter_count: course.chapters.length, - bookmark_count: course._count.bookmarks, - enrollment_count: course._count.enrollments, - is_bookmarked: userId ? course.bookmarks.length > 0 : false, - is_enrolled: userId ? course.enrollments.length > 0 : false, - created_at: course.createdAt, - updated_at: course.updatedAt, - })); + const transformedCourses = courses.map((course) => { + // Calculate average rating + const averageRating = course.ratings.length > 0 + ? course.ratings.reduce((sum, r) => sum + r.rating, 0) / course.ratings.length + : 0; + + return { + ...course, + author_username: course.author.username, + chapter_count: course.chapters.length, + bookmark_count: course._count.bookmarks, + enrollment_count: course._count.enrollments, + average_rating: parseFloat(averageRating.toFixed(1)), + rating_count: course._count.ratings, + is_bookmarked: userId ? course.bookmarks.length > 0 : false, + is_enrolled: userId ? course.enrollments.length > 0 : false, + created_at: course.createdAt, + updated_at: course.updatedAt, + }; + }); // Debug logging if (transformedCourses.length > 0) { @@ -378,10 +391,14 @@ router.get("/:id", optionalAuth, async (req, res) => { select: { id: true }, take: 0, }, + ratings: { + select: { rating: true }, + }, _count: { select: { bookmarks: true, enrollments: true, + ratings: true, }, }, }, @@ -424,6 +441,11 @@ router.get("/:id", optionalAuth, async (req, res) => { (chapter.progress && chapter.progress[0]?.completedAt) || null, })); + // Calculate average rating + const averageRating = course.ratings.length > 0 + ? course.ratings.reduce((sum, r) => sum + r.rating, 0) / course.ratings.length + : 0; + // Transform the response const courseDetails = { ...course, @@ -431,6 +453,8 @@ router.get("/:id", optionalAuth, async (req, res) => { chapter_count: course.chapters.length, bookmark_count: course._count.bookmarks, enrollment_count: course._count.enrollments, + average_rating: parseFloat(averageRating.toFixed(1)), + rating_count: course._count.ratings, is_bookmarked: userId ? course.bookmarks.length > 0 : false, is_enrolled: userId ? course.enrollments.length > 0 : false, enrollment_data: @@ -712,6 +736,147 @@ router.post("/:id/bookmark", authenticateToken, async (req, res) => { } }); +// Rate a course +// Get all ratings for a course +router.get("/:id/rate", optionalAuth, async (req, res) => { + try { + const { id } = req.params; + const userId = req.user?.id; + + console.log("📊 Fetching ratings for course:", { courseId: id, userId }); + + // Check if course exists + const course = await prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + console.log("❌ Course not found:", id); + return res.status(404).json({ error: "Course not found" }); + } + + // Get all ratings for this course + const ratings = await prisma.courseRating.findMany({ + where: { courseId: id }, + include: { + user: { + select: { + id: true, + username: true, + }, + }, + }, + orderBy: { + id: "desc", + }, + }); + + // Calculate average rating + const totalRatings = ratings.length; + const averageRating = totalRatings > 0 + ? ratings.reduce((sum, r) => sum + r.rating, 0) / totalRatings + : 0; + + // Check if current user has rated + const userRating = userId + ? ratings.find((r) => r.userId === userId) + : null; + + res.json({ + ratings: ratings.map((r) => ({ + id: r.id, + rating: r.rating, + user: r.user, + })), + averageRating, + totalRatings, + userRating: userRating ? userRating.rating : null, + hasRated: !!userRating, + }); + } catch (error) { + console.error("❌ Error fetching ratings:", error); + console.error("Error details:", error.stack); + res + .status(500) + .json({ error: "Failed to fetch ratings", details: error.message }); + } +}); + +router.post("/:id/rate", authenticateToken, async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.id; + const { rating } = req.body; + + console.log("⭐ Rating request:", { courseId: id, userId, rating }); + + // Validate rating value + if (!rating || typeof rating !== "number" || rating < 1 || rating > 5) { + return res.status(400).json({ + error: "Invalid rating. Rating must be a number between 1 and 5" + }); + } + + // Check if course exists + const course = await prisma.course.findUnique({ + where: { id }, + }); + + if (!course) { + console.log("❌ Course not found:", id); + return res.status(404).json({ error: "Course not found" }); + } + + // Check if user has already rated this course + const existingRating = await prisma.courseRating.findUnique({ + where: { + courseId_userId: { + courseId: id, + userId: userId, + }, + }, + }); + + let result; + if (existingRating) { + // Update existing rating + result = await prisma.courseRating.update({ + where: { + courseId_userId: { + courseId: id, + userId: userId, + }, + }, + data: { + rating: rating, + }, + }); + console.log("✅ Rating updated:", result.id); + } else { + // Create new rating + result = await prisma.courseRating.create({ + data: { + courseId: id, + userId: userId, + rating: rating, + }, + }); + console.log("✅ Rating created:", result.id); + } + + res.json({ + message: existingRating ? "Rating updated successfully" : "Rating submitted successfully", + rating: result.rating, + }); + } catch (error) { + console.error("❌ Error rating course:", error); + console.error("Error details:", error.stack); + res + .status(500) + .json({ error: "Failed to rate course", details: error.message }); + } +}); + // Update course router.put("/:id", authenticateToken, async (req, res) => { try { @@ -934,6 +1099,7 @@ router.delete("/:id/enroll", authenticateToken, async (req, res) => { } }); + // Mark chapter as completed/uncompleted router.post( "/:courseId/chapters/:chapterId/progress", From 40b670792ea26d411d30e2efd082f56938cadacf Mon Sep 17 00:00:00 2001 From: charlottenguyen05 Date: Mon, 5 Jan 2026 19:04:31 +0100 Subject: [PATCH 2/3] feat: put the position of rating above of the share button and delete wrong typo --- .../src/components/courses/CourseDetailPage.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/components/courses/CourseDetailPage.tsx b/client/src/components/courses/CourseDetailPage.tsx index d8031db..b8d5c33 100644 --- a/client/src/components/courses/CourseDetailPage.tsx +++ b/client/src/components/courses/CourseDetailPage.tsx @@ -1,7 +1,7 @@ "use client"; import type React from "react"; -import { useState, useEffect, use } from "react"; +import { useState, useEffect } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; import ReactMarkdown from "react-markdown"; import { @@ -752,8 +752,14 @@ const CourseDetailPage: React.FC = () => {
+
+ { }} /> + + {averageRating.toFixed(1)} ({ratingCount} ratings) + +
{/* Social Share Buttons */} -
+
Share on:
-
- { }} /> - - {averageRating.toFixed(1)} ({ratingCount} ratings) - -
From 0aa53209228a14f1309bfe63b4abdea738dc5a1d Mon Sep 17 00:00:00 2001 From: charlottenguyen05 Date: Tue, 6 Jan 2026 14:12:52 +0100 Subject: [PATCH 3/3] fix: delete unneccessary changes because of formatting --- client/src/components/courses/CoursesPage.tsx | 48 ++++++++++--------- server/routes/courses.js | 1 - 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/client/src/components/courses/CoursesPage.tsx b/client/src/components/courses/CoursesPage.tsx index 61da9b7..48127c6 100644 --- a/client/src/components/courses/CoursesPage.tsx +++ b/client/src/components/courses/CoursesPage.tsx @@ -118,12 +118,12 @@ const CoursesPage: React.FC = () => { prevCourses.map((course) => course.id === courseId ? { - ...course, - is_bookmarked: response.bookmarked, - bookmark_count: response.bookmarked - ? course.bookmark_count + 1 - : course.bookmark_count - 1, - } + ...course, + is_bookmarked: response.bookmarked, + bookmark_count: response.bookmarked + ? course.bookmark_count + 1 + : course.bookmark_count - 1, + } : course ) ); @@ -152,9 +152,9 @@ const CoursesPage: React.FC = () => { prevCourses.map((course) => course.id === courseId ? { - ...course, - is_enrolled: !isEnrolled, - } + ...course, + is_enrolled: !isEnrolled, + } : course ) ); @@ -297,12 +297,12 @@ const CoursesPage: React.FC = () => { {filter === "my-courses" ? "You haven't created any courses yet." : filter === "bookmarked" - ? "You haven't bookmarked any courses yet." - : filter === "enrolled" - ? "You haven't enrolled in any courses yet." - : searchTerm - ? "Try adjusting your search terms." - : "Be the first to create a course!"} + ? "You haven't bookmarked any courses yet." + : filter === "enrolled" + ? "You haven't enrolled in any courses yet." + : searchTerm + ? "Try adjusting your search terms." + : "Be the first to create a course!"}

{isAuth && ( { ) } disabled={enrollingCourse === course.id} - className={`px-4 py-2 rounded-lg font-medium transition-colors duration-300 text-sm flex items-center space-x-1 ${course.is_enrolled - ? "bg-red-600 hover:bg-red-700 text-white" - : "bg-alien-green text-royal-black hover:bg-alien-green/90 shadow-alien-glow" - } disabled:opacity-50 disabled:cursor-not-allowed`} + className={`px-4 py-2 rounded-lg font-medium transition-colors duration-300 text-sm flex items-center space-x-1 ${ + course.is_enrolled + ? "bg-red-600 hover:bg-red-700 text-white" + : "bg-alien-green text-royal-black hover:bg-alien-green/90 shadow-alien-glow" + } disabled:opacity-50 disabled:cursor-not-allowed`} > {enrollingCourse === course.id ? ( <> @@ -464,10 +465,11 @@ const CoursesPage: React.FC = () => { onClick={() => setPagination((prev) => ({ ...prev, page })) } - className={`px-3 py-2 rounded-lg transition-colors duration-200 ${pagination.page === page - ? "bg-alien-green text-royal-black" - : "bg-smoke-gray border border-smoke-light text-white hover:bg-smoke-light" - }`} + className={`px-3 py-2 rounded-lg transition-colors duration-200 ${ + pagination.page === page + ? "bg-alien-green text-royal-black" + : "bg-smoke-gray border border-smoke-light text-white hover:bg-smoke-light" + }`} > {page} diff --git a/server/routes/courses.js b/server/routes/courses.js index 8b100dc..75540ba 100644 --- a/server/routes/courses.js +++ b/server/routes/courses.js @@ -1099,7 +1099,6 @@ router.delete("/:id/enroll", authenticateToken, async (req, res) => { } }); - // Mark chapter as completed/uncompleted router.post( "/:courseId/chapters/:chapterId/progress",