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:
+
+
+ handleRatingSubmit(rating)} className="bg-alien-green text-royal-black px-3 py-2 rounded-lg font-semibold hover:bg-alien-green/90 transition-colors duration-300">
+ Submit Rating
- )}
+
+
+
+
)}
{/* 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 && (
{
)
}
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 ? (
<>
@@ -444,11 +464,10 @@ 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/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:
{
@@ -782,12 +788,6 @@ const CourseDetailPage: React.FC = () => {
-
- { }} />
-
- {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",