The Course Detail Page is an important feature in any educational listing platform. It serves as the primary destination for prospective students to explore course offerings in depth, helping them make informed decisions about their educational journey.
Key Features Of Course Detail Page
On this page, users can find comprehensive information about the course, including the syllabus, learning objectives, prerequisites, and duration. It often includes details about the instructor, providing insights into their qualifications and teaching style. This page may also showcase student reviews and ratings, offering social proof and helping users gauge the course's quality and relevance.
Visual elements like course images, videos, and sample lessons are often incorporated to enhance user engagement. Additionally, the Course Detail Page typically provides clear call-to-action buttons, such as "Enroll Now" or "Add to Wishlist," guiding users toward the next step in their learning path.
The Course Detail Page is a vital tool for educational platforms, combining essential course information with user-friendly design to attract and retain students.
Approach to Implement Course Detail Page
Backend
- Create Course: Handle course creation with image upload using Cloudinary and save course details to MongoDB.
- Get All Courses: Fetch all courses excluding lectures to optimize data retrieval.
- Get Course Lectures: Retrieve lectures for a specific course by incrementing the views count.
- Add Lecture: Upload lecture videos to Cloudinary, and update the course with lecture details.
- Delete Course: Remove a course and associated media files from Cloudinary and MongoDB.
- Delete Lecture: Delete a specific lecture, remove its video from Cloudinary, and update the course.
- Routes: Implement secure Express routes with authentication and authorization for creating, updating, retrieving, and deleting courses and lectures.
Frontend
- Course Component: Displays individual course details with an image, title, description, creator, lecture count, and views, and includes buttons for viewing the course and adding it to a playlist.
- Courses Component: Fetches course data from an API, provides a search bar and category filters, and renders a list of courses based on user input.
- State Management: Uses React hooks (useState and useEffect) to manage and filter course data dynamically.
- UI Layout: Utilizes Chakra UI components for a responsive design, including buttons for category selection and a stack layout for course display.
Backend Example
// app.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const user = require('./routes/userRoute')
const course = require('./routes/courseRoutes');
const ErrorMiddleware = require('./middleware/Error');
const cookieParser = require('cookie-parser');
const enrollment = require('./routes/enrollmentRoutes')
const cors = require('cors')
app.use(cors({
origin: ['http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}));
app.use(express.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser())
app.use('/api/v1', enrollment)
app.use('/api/v1', user);
app.use('/api/v1', course)
app.use(ErrorMiddleware);
module.exports = app;
// server.js
const app = require("./app")
const { config } = require("dotenv")
const database = require('./config/database');
const cloudinary = require('cloudinary').v2;
database.connectedToDatabase();
config({
path: './config/config.env'
})
app.listen(process.env.PORT, () => {
console.log(`Server is Running at ${process.env.PORT}`);
})
// models/courseModel.js
const mongoose = require('mongoose');
const courseSchema = mongoose.Schema({
title: {
type: String,
required: [true, "Please enter course title"],
minLength: [4, "Title must be at least 4 characters"],
maxLength: [80, " Title can't exceed 80 characters"],
},
description: {
type: String,
required: [true, "Please enter course title"],
minLength: [20, "Title must be at least 20 characters"],
},
lectures: [
{
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
video: {
public_id: {
type: String,
required: true
},
url: {
type: String,
required: true
},
},
}
],
poster: {
public_id: {
type: String,
required: true
},
url: {
type: String,
required: true
},
},
views: {
type: Number,
default: 0,
},
numOfVideos: {
type: Number,
default: 0,
},
category: {
type: String,
required: true,
},
createdBy: {
type: String,
required: [true, "Enter Course Creator Name"],
},
createdAt: {
type: Date,
default: Date.now,
},
});
const CourseModel = mongoose.model("Course", courseSchema);
module.exports = CourseModel;
//controllers/couserContorller.js
const catchAsyncError = require("../middleware/catchAsyncError");
const Course = require("../models/courseModel");
const getDataUri = require("../utils/datauri");
const ErrorHandler = require("../utils/errorHandler");
const cloudinary = require("cloudinary").v2;
exports.createCourse = catchAsyncError(async (req, res, next) => {
const { title, description, category, createdBy } = req.body;
if (!title || !description || !category || !createdBy)
return next(new ErrorHandler("Please Add all Field", 404));
const file = req.file;
const fileUri = getDataUri(file);
const myCloud = await cloudinary.uploader.upload(fileUri.content);
await Course.create({
title,
description,
category,
createdBy,
poster: {
public_id: myCloud.public_id,
url: myCloud.secure_url,
},
})
res.status(201).json({
success: true,
message: "Course Created Successfully. You can add Lecture Now",
});
});
exports.getAllCourse = catchAsyncError(async (req, res, next) => {
const courses = await Course.find().select("-lectures");
res.status(200).json({
success: true,
courses,
});
});
exports.getCourseLectures = catchAsyncError(async (req, res, next) => {
const course = await Course.findById(req.params.id);
if (!course) return next(new ErrorHandler("Course not found", 404));
course.views += 1;
await course.save();
res.status(200).json({
success: true,
lectures: course.lectures,
});
});
exports.addLecture = catchAsyncError(async (req, res, next) => {
const { title, description } = req.body;
const course = await Course.findById(req.params.id);
if (!course) return next(new ErrorHandler("Course not found", 404));
const file = req.file;
const fileUri = getDataUri(file);
const myCloud = await cloudinary.uploader.upload(fileUri.content, {
resource_type: "video",
});
course.lectures.push({
title,
description,
video: {
public_id: myCloud.public_id,
url: myCloud.secure_url,
}
})
course.numOfVideos = course.lectures.length;
await course.save();
res.status(200).json({
success: true,
message: "Lecture Added Successfully in Course"
});
});
// delete course
exports.deleteCourse = catchAsyncError(async (req, res, next) => {
const { id } = req.params;
const course = await Course.findById(id);
if (!course) return next(new ErrorHandler("Course not found", 404));
await cloudinary.uploader.destroy(course.poster.public_id);
for (let i = 0; i < course.lectures.length; i++) {
const singleLecture = course.lectures[i];
await cloudinary.uploader.destroy(singleLecture.video.public_id);
}
await course.deleteOne();
res.status(201).json({
success: true,
message: "Course deleted Successfully.",
});
});
exports.deleteLecture = catchAsyncError(async (req, res, next) => {
const { courseId, lectureId } = req.query;
const course = await Course.findById(courseId);
if (!course) return next(new ErrorHandler("Course not found", 404));
// Find the lecture with the specified ID
const lecture = course.lectures.find(item => item._id.toString() === lectureId.toString());
if (!lecture) {
return next(new ErrorHandler("Lecture not found", 404));
}
// Check if lecture.video is defined before accessing its properties
if (lecture.video && lecture.video.public_id) {
await cloudinary.uploader.destroy(lecture.video.public_id, {
resource_type: 'video',
});
}
// Filter out the deleted lecture
course.lectures = course.lectures.filter(item => item._id.toString() !== lectureId.toString());
course.numOfVideos = course.lectures.length;
await course.save();
res.status(201).json({
success: true,
message: "Lecture deleted Successfully.",
});
});
// routes/courseRoutes.js
const express = require("express");
const {
getAllCourse,
createCourse,
getCourseLectures,
addLecture,
deleteCourse,
deleteLecture,
} = require("../controllers/courseController");
const singleUpload = require("../middleware/multer");
const { authorizAdmin, isAuthenticatedUser } = require("../middleware/auth");
const router = express.Router();
router.route("/createcourse").post(singleUpload, createCourse);
router.route("/course").get(getAllCourse);
router
.route("/course/:id")
.get(isAuthenticatedUser, getCourseLectures)
.post(isAuthenticatedUser, authorizAdmin, singleUpload, addLecture)
.delete(isAuthenticatedUser, authorizAdmin, deleteCourse);
router
.route("/lecture")
.delete(isAuthenticatedUser, authorizAdmin, deleteLecture);
module.exports = router;
// middleware/auth.js
const ErrorHandler = require("../utils/errorHandler");
const catchAsyncErrors = require("./catchAsyncError");
const jwt = require("jsonwebtoken");
const User = require("../models/userModel");
exports.isAuthenticatedUser = catchAsyncErrors(async (req, res, next) => {
const { token } = req.cookies;
if (!token) {
return next(new ErrorHandler("Please Login to access this resource", 401));
}
const decodedData = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decodedData.id);
next();
});
exports.authorizAdmin = (req, res, next) => {
if (req.user.role !== 'admin')
return next(
new ErrorHandler(`${req.user.role} is not allowed to access this resource`, 403)
);
next();
}
Start your server using the below command:
node index.jsFrontend Example
// App.jsx
import React from "react";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import Home from "./components/Home/Home";
import Header from "../src/components/Layout/Header/Header";
import Courses from "./components/Courses/Courses";
import Footer from "./components/Layout/Footer/Footer";
import Login from "./components/Auth/Login";
import Register from "./components/Auth/Register";
import ForgotPassword from "./components/Auth/ForgotPassword";
import ResetPassword from "./components/Auth/ResetPassword";
import Contact from "./components/Contact/Contact";
import Request from "./components/Request/Request";
import About from "./components/About/About";
import Subscribe from "./components/Payments/Subscribe";
import CoursePage from "./components/CoursePage/CoursePage";
import Profile from "./components/Profile/Profile";
import ChangePassword from "./components/Profile/ChangePassword";
import UpdateProfile from "./components/Profile/UpdateProfile";
import Dashboard from "./components/Admin/Dashboard/Dashboard";
import CreateCourses from "./components/Admin/CreateCourse/CreateCourses";
import AdminCourses from "./components/Admin/AdminCourses/AdminCourses";
import Users from "./components/Admin/Users/Users";
import EnrollmentForm from "./components/Auth/EnrollmentForm";
function App() {
return (
<Router>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/courses" element={<Courses />} />
<Route path="/course/:id" element={<CoursePage />} />
<Route path="/contact" element={<Contact />} />
<Route path="/request" element={<Request />} />
<Route path="/about" element={<About />} />
<Route path="/profile" element={<Profile />} />
<Route path="/changepassword" element={<ChangePassword />} />
<Route path="/updateprofile" element={<UpdateProfile />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Register />} />
<Route path="/forgetpassword" element={<ForgotPassword />} />
<Route path="/resetpassword/:token" element={<ResetPassword />} />
<Route path="/subscribe" element={<Subscribe />} />
<Route path="/admin/dashboard" element={<Dashboard />} />
<Route path="/admin/createcourse" element={<CreateCourses />} />
<Route path="/admin/courses" element={<AdminCourses />} />
<Route path="/admin/users" element={<Users />} />
<Route path="/enroll" element={<EnrollmentForm />} />
</Routes>
<Footer />
</Router>
);
}
export default App;
// src/components/CouserPage/CoursePage.jsx
import { Box, Grid, Heading, Text, VStack } from "@chakra-ui/react";
import React, { useState } from "react";
import intro from "../../assets/video/intro.mp4";
const CoursePage = () => {
const [lectureNumber, setLecturNumber] = useState(0);
const lectures = [
{
_id: "sadfvdc1",
title: "sample1",
description: "sample sescfr seeekdk dkdfd",
video: {
url: "dfsldd",
},
},
{
_id: "sadfvdc2",
title: "sample2",
description: "sample sescfr seeekdk dkdfd",
video: {
url: "dfsldd",
},
},
{
_id: "sadfvdc2",
title: "sample2",
description: "sample sescfr seeekdk dkdfd",
video: {
url: "dfsldd",
},
},
];
return (
<Grid minH={"90vh"} templateColumns={["1fr", "3fr 1fr"]}>
<Box>
<video
width={"100%"}
controls
controlsList="nodownload noremoteplayback"
disablePictureInPicture
disableRemotePlayback
src={intro}
></video>
<Heading
m="4"
children={`#${lectureNumber + 1} ${lectures[lectureNumber].title}`}
/>
<Heading m="4" children="Description" />
<Text m={"4"} children={lectures[lectureNumber].description} />
</Box>
<VStack>
{lectures.map((element, index) => (
<button
onClick={() => setLecturNumber(index)}
key={element._id}
style={{
width: "100%",
padding: "1rem",
textAlign: "center",
margin: 0,
borderBottom: "1px solid rgba(0,0,0,0.2)",
}}
>
<Text noOfLines={1}>
#{index + 1} {element.title}
</Text>
</button>
))}
</VStack>
</Grid>
);
};
export default CoursePage;
// src/components/Course/Course.jsx
import {
Button,
Container,
HStack,
Heading,
Image,
Input,
Stack,
Text,
VStack,
} from "@chakra-ui/react";
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import axios from "axios";
const Course = ({
views,
title,
imageSrc,
id,
addToPlaylistHandler,
creator,
description,
lectureCount,
}) => {
return (
<VStack className="course" alignItems={["center", "flex-start"]}>
<Image src={imageSrc} boxSize={"60"} objectFit={"contain"} />
<Heading
textAlign={["center", "left"]}
maxW={"200px"}
size={"sm"}
fontFamily={"sans-serif"}
noOfLines={3}
children={title}
/>
<Text children={description} noOfLines={2} />
<HStack>
<Text
children={"Creator :"}
fontWeight={"bold"}
textTransform={"uppercase"}
/>
<Text
children={creator}
fontFamily={"body"}
textTransform={"uppercase"}
/>
</HStack>
<Heading
textAlign={"center"}
size={"xs"}
children={`Lectures - ${lectureCount}`}
textTransform={"uppercase"}
/>
<Heading
size={"xs"}
children={`Views - ${views}`}
textTransform={"uppercase"}
/>
<Stack direction={["column", "row"]} alignItems={"center"}>
<Link to={`/course/${id}`}>
<Button colorScheme="yellow">Watch Now</Button>
</Link>
<Button
variant={"ghost"}
colorScheme="yellow"
onClick={() => addToPlaylistHandler(id)}
>
Add To Playlist
</Button>
</Stack>
</VStack>
);
};
const Courses = () => {
const [courses, setCourses] = useState([]);
const [keyword, setKeyword] = useState("");
const [category, setCategory] = useState("");
const categories = [
"App Development",
"Web Development",
"Data Structures & Algorithm",
"Machine Learning",
"Artificial Intelligence",
"Data Science",
];
useEffect(() => {
const fetchCourses = async () => {
try {
const { data } = await axios.get(
"http://localhost:4000/api/v1/course",
{
withCredentials: true,
}
);
setCourses(data.courses);
} catch (error) {
console.error(error.response.data.message);
}
};
fetchCourses();
}, []);
// Filter courses based on keyword and category
const filteredCourses = courses.filter(
(course) =>
course.title.toLowerCase().includes(keyword.toLowerCase()) &&
(category ? course.category === category : true)
);
const addToPlaylistHandler = (courseId) => {
console.log("Add To playlist", courseId);
};
return (
<Container minH={"95vh"} maxW={"container.lg"} paddingY={"8"}>
<Heading children="All Courses" m={"8"} />
<Input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="Search a course"
type="text"
focusBorderColor="yellow"
/>
<HStack
overflowX={"auto"}
padding={"8"}
css={{ "&::-webkit-scrollbar": { display: "none" } }}
>
{categories.map((item, index) => (
<Button
colorScheme="yellow"
variant={"ghost"}
key={index}
onClick={() => setCategory(item)}
minW={"60"}
>
<Text children={item} />
</Button>
))}
<Button
colorScheme="yellow"
variant={"ghost"}
onClick={() => setCategory("")}
minW={"60"}
>
<Text children="All Categories" />
</Button>
</HStack>
<Stack
direction={["column", "row"]}
flexWrap={"wrap"}
justifyContent={["flex-start", "space-evenly"]}
alignItems={["center", "flex-start"]}
>
{filteredCourses.length > 0 ? (
filteredCourses.map((course) => (
<Course
key={course._id}
title={course.title}
description={course.description}
views={course.views}
imageSrc={course.poster.url}
id={course._id}
creator={course.createdBy}
lectureCount={course.numOfVideos}
addToPlaylistHandler={addToPlaylistHandler}
/>
))
) : (
<Text>No courses found</Text>
)}
</Stack>
</Container>
);
};
export default Courses;
Start your frontend using the below command:
npm start