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
// 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
//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;
//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/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>
);
}
//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>
);
}
//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;
//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;
}
//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;
//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;
//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