Building Course Listing Page in MERN Stack Course Management App

Last Updated : 4 Sep, 2024

The Course Listing page is an important part of the course website, designed to help learners discover and explore various learning opportunities. It presents an organized catalogue of courses, each highlighted with important details like the title, instructor, category, and a brief summary.

Key Features

This page often features intuitive filters and sorting options, enabling users to quickly find courses that match their specific interests, skill levels, or career goals. Categories and tags are commonly used to group similar courses, making navigation seamless and efficient.

High-quality images or banners paired with concise descriptions make the courses appealing and easy to browse. The inclusion of user ratings and reviews further assists learners in making informed decisions.

In essence, the Course Listing page is a user-friendly gateway that connects learners with the educational content they seek, streamlining the journey from exploration to enrollment.

Approach To Implement Course Listing

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.

Note: Steps are provided as per the previous series of Educational Websites

Backend Example

JavaScript
// 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;
JavaScript
// 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}`);
})
JavaScript
// 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;
JavaScript
//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.",
    });
});
JavaScript
// 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;
JavaScript
// 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.js

Frontend Example

JavaScript
//src/components/CoursePage/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;
JavaScript
//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;
JavaScript
//src/components/Admin/AdminCourse/AdminCourse.jsx

import {
    Box,
    Button,
    Grid,
    HStack,
    Heading,
    Image,
    Table,
    TableCaption,
    TableContainer,
    Tbody,
    Td,
    Th,
    Thead,
    Tr,
    useDisclosure,
} from "@chakra-ui/react";
import React, { useState, useEffect } from "react";
import Sidebar from "../Sidebar";
import { RiDeleteBin7Fill } from "react-icons/ri";
import CourseModal from "./CourseModal";
import axios from "axios";

const AdminCourses = () => {
    const [courses, setCourses] = useState([]);
    const { isOpen, onClose, onOpen } = useDisclosure();

    // Fetch all courses on component mount
    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();
    }, []);

    const courseDetailsHandler = (courseId) => {
        onOpen();
    };

    const deleteButtonHandler = (courseId) => {
        console.log(courseId);
    };

    const deleteLectureButtonHandler = (courseId, lectureId) => {
        console.log(courseId);
        console.log(lectureId);
    };

    const addLectureHandler = (e, courseId, title, description, video) => {
        e.preventDefault();
    };

    return (
        <Grid minH={"100vh"} templateColumns={["1fr", "5fr 1fr"]}>
            <Box p={["0", "8"]} overflowX={"auto"}>
                <Heading
                    textTransform={"uppercase"}
                    my={16}
                    textAlign={["center", "left"]}
                >
                    All Courses
                </Heading>
                <TableContainer w={["100vw", "full"]}>
                    <Table variant={"simple"} size={"lg"}>
                        <TableCaption>All available courses in the database</TableCaption>
                        <Thead>
                            <Tr>
                                <Th>Id</Th>
                                <Th>Poster</Th>
                                <Th>Title</Th>
                                <Th>Category</Th>
                                <Th>Creator</Th>
                                <Th isNumeric>Views</Th>
                                <Th isNumeric>Lectures</Th>
                                <Th isNumeric>Action</Th>
                            </Tr>
                        </Thead>
                        <Tbody>
                            {courses.map((item) => (
                                <Row
                                    key={item._id}
                                    item={item}
                                    courseDetailsHandler={courseDetailsHandler}
                                    deleteButtonHandler={deleteButtonHandler}
                                />
                            ))}
                        </Tbody>
                    </Table>
                </TableContainer>

                <CourseModal
                    isOpen={isOpen}
                    onClose={onClose}
                    id={"adbnfndfj"}
                    deleteButtonHandler={deleteLectureButtonHandler}
                    addLectureHandler={addLectureHandler}
                    courseTitle={"App course"}
                />
            </Box>
            <Sidebar />
        </Grid>
    );
};

export default AdminCourses;

function Row({ item, courseDetailsHandler, deleteButtonHandler }) {
    return (
        <Tr>
            <Td>#{item._id}</Td>
            <Td>
                <Image src={item.poster.url} boxSize="50px" objectFit="cover" />
            </Td>
            <Td>{item.title}</Td>
            <Td textTransform={"uppercase"}>{item.category}</Td>
            <Td>{item.createdBy}</Td>
            <Td isNumeric>{item.views}</Td>
            <Td isNumeric>{item.numOfVideos}</Td>
            <Td isNumeric>
                <HStack justifyContent={"flex-end"}>
                    <Button
                        onClick={() => courseDetailsHandler(item._id)}
                        variant={"outline"}
                        color={"purple.500"}
                    >
                        View Lectures
                    </Button>
                    <Button
                        onClick={() => deleteButtonHandler(item._id)}
                        color={"purple.600"}
                    >
                        <RiDeleteBin7Fill />
                    </Button>
                </HStack>
            </Td>
        </Tr>
    );
}
JavaScript
//src/components/Admin/AdminCourse/CourseModal.jsx

import {
    Box,
    Button,
    Grid,
    Heading,
    Input,
    Modal,
    ModalBody,
    ModalCloseButton,
    ModalContent,
    ModalFooter,
    ModalHeader,
    ModalOverlay,
    Stack,
    Text,
    VStack,
} from "@chakra-ui/react";
import React, { useState } from "react";
import { RiDeleteBin7Fill } from "react-icons/ri";
import { fileUploadCss } from "../../Auth/Register";
const CourseModal = ({
    isOpen,
    onClose,
    id,
    deleteButtonHandler,
    courseTitle,
    lectures = [1, 2, 3, 4, 5, 6, 7, 8],
    addLectureHandler,
}) => {
    const [title, setTitle] = useState("");
    const [description, setDescription] = useState("");
    const [video, setVideo] = useState("");
    const [videoPrev, setVideoPrev] = useState("");

    const chnageVideoHandler = (e) => {
        const file = e.target.files[0];
        const reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = () => {
            setVideoPrev(reader.result);
            setVideo(file);
        };
    };
    const handleClose = () => {
        setTitle("");
        setDescription("");
        setVideo("");
        setVideoPrev("");
        onClose();
    };
    return (
        <Modal
            isOpen={isOpen}
            size={"full"}
            onClose={handleClose}
            scrollBehavior="outside"
        >
            <ModalOverlay />
            <ModalContent>
                <ModalHeader>{courseTitle}</ModalHeader>
                <ModalCloseButton />
                <ModalBody p="16">
                    <Grid templateColumns={["1fr", "3fr 1fr"]}>
                        <Box px={["0", "16"]}>
                            <Box my={"5"}>
                                <Heading children={courseTitle} />
                                <Heading children={`#${id}`} size={"sm"} opacity={"0.4"} />
                            </Box>
                            <Heading children={"Lectures"} size={"lg"} />

                            {lectures.map((item, i) => (
                                <VideoCard
                                    key={i}
                                    title={"App Develpoment"}
                                    description={"This is a intro"}
                                    num={i + 1}
                                    lectureId={"#45"}
                                    courseId={id}
                                    deleteButtonHandler={deleteButtonHandler}
                                />
                            ))}
                        </Box>
                        <Box>
                            <form
                                onSubmit={(e) =>
                                    addLectureHandler(e, id, title, description, video)
                                }
                            >
                                <VStack spacing={"4"}>
                                    <Heading
                                        children="Add Lecture"
                                        size={"md"}
                                        textTransform={"uppercase"}
                                    />
                                    <Input
                                        focusBorderColor="purple.300"
                                        placeholder="Title"
                                        value={title}
                                        onChange={(e) => setTitle(e.target.value)}
                                    />
                                    <Input
                                        focusBorderColor="purple.300"
                                        placeholder="Description"
                                        value={description}
                                        onChange={(e) => setDescription(e.target.value)}
                                    />
                                    <Input
                                        accept="video/mp4"
                                        css={{
                                            "&::file-selector-button": {
                                                ...fileUploadCss,
                                                color: "purple",
                                            },
                                        }}
                                        required
                                        type="file"
                                        focusBorderColor="purple.300"
                                        onChange={chnageVideoHandler}
                                    />
                                    {videoPrev && (
                                        <video
                                            controlsList="nodownload"
                                            controls
                                            src={videoPrev}
                                        ></video>
                                    )}
                                    <Button w={"full"} colorScheme="purple" type="submit">
                                        Upload
                                    </Button>
                                </VStack>
                            </form>
                        </Box>
                    </Grid>
                </ModalBody>
                <ModalFooter>
                    <Button onClick={handleClose}>Close</Button>
                </ModalFooter>
            </ModalContent>
        </Modal>
    );
};

export default CourseModal;

function VideoCard({
    title,
    description,
    num,
    lectureId,
    courseId,
    deleteButtonHandler,
}) {
    return (
        <Stack
            direction={["column", "row"]}
            my={"8"}
            borderRadius={"lg"}
            boxShadow={"0 0 10px rgba(107,70,193,0.5) "}
            justifyContent={["flex-start", "space-between"]}
            p={["4", "8"]}
        >
            <Box>
                <Heading size={"sm"} children={`#${num} ${title}`} />
                <Text children={description} />
            </Box>
            <Button
                color={"purple.600"}
                onClick={() => deleteButtonHandler(courseId, lectureId)}
            >
                <RiDeleteBin7Fill />
            </Button>
        </Stack>
    );
}
JavaScript
//src/components/Admin/Dashboard/CreateCourses

import {
    Button,
    Container,
    Grid,
    Heading,
    Image,
    Input,
    Select,
    VStack,
} from "@chakra-ui/react";
import React, { useState } from "react";
import Sidebar from "../Sidebar";
import axios from "axios";

const CreateCourses = () => {
    const [title, setTitle] = useState("");
    const [description, setDescription] = useState("");
    const [createdBy, setCreatedBy] = useState("");
    const [category, setCategory] = useState("");
    const [image, setImage] = useState(null);
    const [imagePrev, setImagePrev] = useState("");

    const categories = [
        "App Development",
        "Web Development",
        "Data Structures & Algorithm",
        "Machine Learning",
        "Artificial Intelligence",
        "Data Science",
    ];

    const changeImageHandler = (e) => {
        const file = e.target.files[0];
        const reader = new FileReader();

        reader.readAsDataURL(file);
        reader.onload = () => {
            setImagePrev(reader.result);
            setImage(file);
        };
    };

    const submitHandler = async (e) => {
        e.preventDefault();

        const formData = new FormData();
        formData.append("title", title);
        formData.append("description", description);
        formData.append("category", category);
        formData.append("createdBy", createdBy);
        formData.append("file", image);

        try {
            const { data } = await axios.post(
                "http://localhost:4000/api/v1/createcourse",
                formData,
                {
                    withCredentials: true,
                    headers: {
                        "Content-Type": "multipart/form-data",
                    },
                }
            );

            console.log(data.message);
            setTitle("");
            setDescription("");
            setCreatedBy("");
            setCategory("");
            setImage(null);
            setImagePrev("");
        } catch (error) {
            console.error(error.response.data.message);
        }
    };

    return (
        <Grid minH={"100vh"} templateColumns={["1fr", "5fr 1fr"]}>
            <Container py={"16"}>
                <form onSubmit={submitHandler}>
                    <Heading
                        textTransform={"uppercase"}
                        my={16}
                        textAlign={["center", "left"]}
                    >
                        Create Course
                    </Heading>
                    <VStack m={"auto"} spacing={"8"}>
                        <Input
                            value={title}
                            onChange={(e) => setTitle(e.target.value)}
                            placeholder="Title"
                            type="text"
                            focusBorderColor="purple.300"
                        />
                        <Input
                            value={description}
                            onChange={(e) => setDescription(e.target.value)}
                            placeholder="Description"
                            type="text"
                            focusBorderColor="purple.300"
                        />
                        <Input
                            value={createdBy}
                            onChange={(e) => setCreatedBy(e.target.value)}
                            placeholder="Creator Name"
                            type="text"
                            focusBorderColor="purple.300"
                        />
                        <Select
                            focusBorderColor="purple.300"
                            value={category}
                            onChange={(e) => setCategory(e.target.value)}
                        >
                            <option value={""}>Category</option>
                            {categories.map((item) => (
                                <option key={item} value={item}>
                                    {item}
                                </option>
                            ))}
                        </Select>

                        <Input
                            accept="image/*"
                            css={{
                                "&::file-selector-button": {
                                    color: "purple",
                                },
                            }}
                            required
                            id="chooseAvatar"
                            type="file"
                            focusBorderColor="purple.300"
                            onChange={changeImageHandler}
                        />
                        {imagePrev && (
                            <Image src={imagePrev} boxSize="64" objectFit={"contain"} />
                        )}
                        <Button w={"full"} colorScheme="purple" type="submit">
                            Create
                        </Button>
                    </VStack>
                </form>
            </Container>
            <Sidebar />
        </Grid>
    );
};

export default CreateCourses;
JavaScript
//src/components/Admin/Dashboard/Chart.jsx

import React from "react";
import {
    Chart as ChartJs,
    CategoryScale,
    LinearScale,
    PointElement,
    LineElement,
    Title,
    ArcElement,
    Legend,
} from "chart.js";
import { Line, Doughnut } from "react-chartjs-2";
ChartJs.register(
    CategoryScale,
    LinearScale,
    PointElement,
    LineElement,
    Title,
    ArcElement,
    Legend
);

export const LineChart = () => {
    const labels = getLastYearMonths();

    const options = {
        responsive: true,
        plugins: {
            legend: {
                position: "bottom",
            },
            title: {
                display: true,
                text: "Yearly Views",
            },
        },
    };
    const data = {
        labels,
        datasets: [
            {
                label: "Views",
                data: [1, 2, 3, 4],
                borderColor: "rgba(107,70,193,0.5)",
                backgroundColor: "#6b46c1",
            },
        ],
    };
    return <Line options={options} data={data} />;
};

export const DoughnutChart = () => {
    const data = {
        labels: ["Subscribed", "Not Subscribed"],
        datasets: [
            {
                label: "Views",
                data: [3, 50],
                borderColor: ["rgba(62,12,171)", "rgba(214,43,129)"],
                backgroundColor: ["rgba(62,12,171,0.3)", "rgba(214,43,129,0.3)"],
                borderWidth: 1,
            },
        ],
    };
    return <Doughnut data={data} />;
};

function getLastYearMonths() {
    const labels = [];

    const months = [
        "January",
        "February",
        "March",
        "April",
        "May",
        "June",
        "July",
        "August",
        "September",
        "October",
        "November",
        "Decmeber",
    ];
    const currentMonth = new Date().getMonth();

    const remain = 11 - currentMonth;

    for (let i = currentMonth; i < months.length; i--) {
        const element = months[i];
        labels.unshift(element);
        if (i === 0) break;
    }
    for (let i = 11; i > remain; i--) {
        if (i === currentMonth) break;
        const element = months[i];
        labels.unshift(element);
    }
    return labels;
}
JavaScript
//src/components/Admin/Dashboard/Dashboard.jsx

import {
    Box,
    Grid,
    HStack,
    Heading,
    Progress,
    Stack,
    Text,
} from "@chakra-ui/react";
import React from "react";
import Sidebar from "../Sidebar";
import { RiArrowDownLine, RiArrowUpLine } from "react-icons/ri";
import { DoughnutChart, LineChart } from "./Chart";

const Databox = ({ title, qty, qtypercentage, profit }) => (
    <Box
        w={["full", "20%"]}
        boxShadow={"-2px 0 10px rgba(107, 70,193,0.5)"}
        p={"8"}
        borderRadius={"lg"}
    >
        <Text children={title} />
        <HStack spacing={4}>
            <Text fontSize={"2xl"} fontWeight={"bold"} children={qty} />
            <HStack>
                <Text children={`${qtypercentage}%`} />
                {profit ? (
                    <RiArrowUpLine color="green" />
                ) : (
                    <RiArrowDownLine color="red" />
                )}
            </HStack>
        </HStack>
        <Text opacity={0.6} children={"Since Last Month"} />
    </Box>
);

const Bar = ({ title, value, profit }) => (
    <Box py={"4"} px={["0", "20"]}>
        <Heading size="sm" children={title} mb={"2"} />
        <HStack w={"full"} alignItems={"center"}>
            <Text children={profit ? "0%" : `-${value}%`} />
            <Progress w={"full"} value={profit ? value : 0} colorScheme="purple" />
            <Text children={`${value > 100 ? value : 100}%`} />
        </HStack>
    </Box>
);

const Dashboard = () => {
    return (
        <Grid minH={"100vh"} templateColumns={["1fr", "5fr 1fr"]}>
            <Box boxSizing="border-box" py="16" px={["4", "0"]}>
                <Text
                    textAlign={"center"}
                    opacity={0.5}
                    children={`Last change was on ${String(new Date()).split("G")[0]}`}
                />
                <Heading
                    children="Dashboard"
                    ml={["0", "16"]}
                    mb={"16"}
                    textAlign={["center", "left"]}
                />

                <Stack
                    direction={["column", "row"]}
                    minH={"24"}
                    justifyContent={"space-evenly"}
                >
                    <Databox
                        title={"Subscription"}
                        qty={123}
                        qtypercentage={30}
                        profit={true}
                    />
                    <Databox
                        title={"Subscription"}
                        qty={183}
                        qtypercentage={70}
                        profit={true}
                    />
                    <Databox
                        title={"Subscription"}
                        qty={13}
                        qtypercentage={10}
                        profit={false}
                    />
                </Stack>
                <Box
                    m={["0", "16"]}
                    borderRadius={"lg"}
                    p={["0", "16"]}
                    mt={["4", "16"]}
                    boxShadow={"-2px 0 10px rgba(107, 70,193,0.5)"}
                >
                    <Heading
                        children="View Graph"
                        textAlign={["center", "left"]}
                        size={"md"}
                        pt={["8", "0"]}
                        ml={["0", "16"]}
                    />

                    {/*Line Chart */}
                    <LineChart />
                </Box>
                <Grid templateColumns={["1fr", "2fr 1fr"]}>
                    <Box p="4">
                        <Heading
                            textAlign={["center", "left"]}
                            children="Progress Bar"
                            size={"md"}
                            my={"8"}
                            ml={["0", "16"]}
                        />

                        <Bar profit={true} title="Views" value={30} />
                        <Bar profit={true} title="Users" value={78} />
                        <Bar profit={false} title="Subscription" value={10} />
                    </Box>
                    <Box p={["0", "16"]} boxSizing="border-box" py={"4"}>
                        <Heading
                            textAlign={"center"}
                            size={"md"}
                            mb={"4"}
                            children="Users"
                        />

                        <DoughnutChart />
                    </Box>
                </Grid>
            </Box>
            <Sidebar />
        </Grid>
    );
};

export default Dashboard;
JavaScript
//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;
JavaScript
//src/components/Admin/Sidebar.jsx

import { Button, VStack } from "@chakra-ui/react";
import React from "react";
import {
    RiAddCircleFill,
    RiDashboardFill,
    RiEyeFill,
    RiUser3Fill,
} from "react-icons/ri";
import { Link, useLocation } from "react-router-dom";

const Sidebar = () => {
    const location = useLocation();
    return (
        <VStack
            spacing={"8"}
            p={"16"}
            boxShadow={"-2px 0 10px rgba(107, 70,193,0.5)"}
        >
            <LinkButton
                Icon={RiDashboardFill}
                text={"Dashboard"}
                url={"dashboard"}
                active={location.pathname === "/admin/dashboard"}
            />
            <LinkButton
                Icon={RiAddCircleFill}
                text={"Create Course"}
                url={"createcourse"}
                active={location.pathname === "/admin/createcourse"}
            />
            <LinkButton
                Icon={RiEyeFill}
                text={"Courses"}
                url={"courses"}
                active={location.pathname === "/admin/courses"}
            />
            <LinkButton
                Icon={RiUser3Fill}
                text={"users"}
                url={"users"}
                active={location.pathname === "/admin/users"}
            />
        </VStack>
    );
};

export default Sidebar;

function LinkButton({ url, Icon, text, active }) {
    return (
        <Link to={`/admin/${url}`}>
            <Button
                fontSize={"largeer"}
                variant={"ghost"}
                colorScheme={active ? "purple" : ""}
            >
                <Icon style={{ margin: "4px" }} />
                {text}
            </Button>
        </Link>
    );
}


Start your frontend using the below command:

npm start

Output


Comment

Explore