├── Backend
├── config
│ ├── cloudinary.js
│ └── database.js
├── controllers
│ ├── Auth.js
│ ├── Category.js
│ ├── Course.js
│ ├── CourseProgress.js
│ ├── Payment.js
│ ├── Profile.js
│ ├── RatingAndReview.js
│ ├── Section.js
│ ├── SubSection.js
│ └── User.js
├── middleware
│ └── verifyJWT.js
├── models
│ ├── Course.js
│ ├── CourseProgress.js
│ ├── Profile.js
│ ├── RatingAndReview.js
│ ├── Section.js
│ ├── SubSection.js
│ └── User.js
├── nodemon.config.json
├── package-lock.json
├── package.json
├── routes
│ ├── Auth.js
│ ├── Course.js
│ ├── CourseProgress.js
│ ├── Instructor.js
│ ├── Payment.js
│ ├── Profile.js
│ ├── RatingAndReview.js
│ └── User.js
├── server.js
└── utils
│ └── fileUploader.js
├── Frontend
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ └── vite.svg
├── src
│ ├── App.css
│ ├── App.jsx
│ ├── assets
│ │ ├── images
│ │ │ ├── 567828_67d0.jpg
│ │ │ ├── 851712_fc61_6.jpg
│ │ │ ├── Udemy-logo.png
│ │ │ ├── angela_yu.jpg
│ │ │ ├── blankProfile.webp
│ │ │ ├── cart.png
│ │ │ ├── check.png
│ │ │ ├── comp1.svg
│ │ │ ├── comp2.svg
│ │ │ ├── comp3.svg
│ │ │ ├── comp4.svg
│ │ │ ├── comp5.svg
│ │ │ ├── front-img.jpg
│ │ │ ├── get-rewarded.jpg
│ │ │ ├── heart.png
│ │ │ ├── imagePlaceholder.jpg
│ │ │ ├── infinity.png
│ │ │ ├── inspire-learners.jpg
│ │ │ ├── jpeg1.jpg
│ │ │ ├── jpeg2.jpg
│ │ │ ├── jpeg3.jpg
│ │ │ ├── jpeg4.jpg
│ │ │ ├── lady.jpg
│ │ │ ├── logo-long.svg
│ │ │ ├── logo-udemy-words.svg
│ │ │ ├── logo-udemy.svg
│ │ │ ├── man.jpg
│ │ │ ├── pic1.jpg
│ │ │ ├── pic10.jpg
│ │ │ ├── pic11.jpg
│ │ │ ├── pic13.jpg
│ │ │ ├── pic14.jpg
│ │ │ ├── pic15.jpg
│ │ │ ├── pic16.jpg
│ │ │ ├── pic17.jpg
│ │ │ ├── pic18.jpg
│ │ │ ├── pic19.jpg
│ │ │ ├── pic2.jpg
│ │ │ ├── pic20.jpg
│ │ │ ├── pic21.jpg
│ │ │ ├── pic22.jpg
│ │ │ ├── pic3.jpg
│ │ │ ├── pic4.jpg
│ │ │ ├── pic5.jpg
│ │ │ ├── pic6.jpg
│ │ │ ├── pic7.jpg
│ │ │ ├── pic8.jpg
│ │ │ ├── pic9.jpg
│ │ │ ├── play.png
│ │ │ ├── python.jpg
│ │ │ ├── pythonImage.jpg
│ │ │ ├── rating.png
│ │ │ ├── rustlang.png
│ │ │ ├── scroll-arrow.png
│ │ │ ├── scroll.png
│ │ │ ├── search.png
│ │ │ ├── star.png
│ │ │ ├── teach-your-way.jpg
│ │ │ ├── teaching-front.jpg
│ │ │ ├── ulogo.png
│ │ │ ├── world.png
│ │ │ └── worldwide.png
│ │ ├── logo-udemy.svg
│ │ └── react.svg
│ ├── components
│ │ ├── NavLayout.jsx
│ │ ├── Navbar.jsx
│ │ └── Spinner.jsx
│ ├── data
│ │ ├── categories.js
│ │ ├── dashoard-links.js
│ │ ├── priceTier.js
│ │ └── roles.js
│ ├── features
│ │ ├── Auth
│ │ │ ├── Login
│ │ │ │ └── index.jsx
│ │ │ ├── PersistLogin.jsx
│ │ │ ├── RequireAuth.jsx
│ │ │ └── Signup
│ │ │ │ └── index.jsx
│ │ ├── Common
│ │ │ └── Profile
│ │ │ │ ├── BasicInformation.jsx
│ │ │ │ ├── ProfileLayout.jsx
│ │ │ │ └── ProfilePhoto.jsx
│ │ ├── Courses
│ │ │ ├── Course
│ │ │ │ ├── Checkout.jsx
│ │ │ │ ├── ClaimCertificate.jsx
│ │ │ │ ├── CourseProgress.jsx
│ │ │ │ ├── InstructorInfo.jsx
│ │ │ │ ├── RatingAndReview.jsx
│ │ │ │ ├── VideoPlayer.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── Home
│ │ │ │ ├── CourseCard.jsx
│ │ │ │ ├── Section.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── SearchCourses
│ │ │ │ ├── WideCourseCard.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── Success
│ │ │ │ └── index.jsx
│ │ │ └── Teaching
│ │ │ │ └── index.jsx
│ │ ├── Instructor
│ │ │ ├── Course
│ │ │ │ ├── CourseLayout.jsx
│ │ │ │ ├── CreateCourse
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── Curriculum
│ │ │ │ │ ├── AddContent.jsx
│ │ │ │ │ ├── ConfirmDeleteModal.jsx
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── Goals
│ │ │ │ │ ├── LearningObjectives.jsx
│ │ │ │ │ ├── Prerequisites.jsx
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── LandingPage
│ │ │ │ │ ├── CoursePromo.jsx
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── useIsMount.jsx
│ │ │ │ └── Sidebar.jsx
│ │ │ └── DashBoard
│ │ │ │ └── index.jsx
│ │ └── Student
│ │ │ └── MyCourses
│ │ │ ├── CourseProgressCard.jsx
│ │ │ └── index.jsx
│ ├── hooks
│ │ └── useAuth.js
│ ├── index.css
│ ├── main.jsx
│ ├── reducers
│ │ ├── api
│ │ │ ├── authApi.js
│ │ │ ├── baseApi.js
│ │ │ ├── courseApi.js
│ │ │ ├── courseProgressApi.js
│ │ │ ├── paymentApi.js
│ │ │ ├── ratingAndReview.js
│ │ │ └── userApi.js
│ │ └── authSlice.js
│ ├── state
│ │ └── store.js
│ └── utils
│ │ └── CourseInfo.js
├── tailwind.config.js
└── vite.config.js
└── README.md
/Backend/config/cloudinary.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary").v2;
2 |
3 | exports.cloudinaryConnect = () => {
4 | try {
5 | cloudinary.config({
6 | cloud_name: process.env.CLOUD_NAME,
7 | api_key: process.env.CLOUDINARY_API_KEY,
8 | api_secret: process.env.CLOUDINARY_API_SECRET,
9 | });
10 | } catch (error) {
11 | console.log(error);
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/Backend/config/database.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | require("dotenv").config();
3 |
4 | exports.connect = () => {
5 | mongoose.connect(process.env.MONGODB_URL, {
6 | useNewUrlParser: true,
7 | useUnifiedTopology:true,
8 | })
9 | .then(() => console.log("DB Connected Successfully"))
10 | .catch( (error) => {
11 | console.log("DB Connection Failed");
12 | console.error(error);
13 | process.exit(1);
14 | } )
15 | };
--------------------------------------------------------------------------------
/Backend/controllers/Auth.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcrypt");
2 | const jwt = require("jsonwebtoken");
3 | const User = require("../models/User");
4 |
5 | const handleLogin = async (req, res) => {
6 | const { email, password } = req.body;
7 | if (!email || !password)
8 | return res.render("login", {
9 | message: "Email and Password Required",
10 | });
11 |
12 | const foundUser = await User.findOne({ email: email }).exec();
13 | if (!foundUser)
14 | return res.status(500).json({
15 | message: "User Not Available",
16 | });
17 | if (!bcrypt.compare(password, foundUser.password))
18 | return res.status(401).json({
19 | message: "Unauthorized",
20 | });
21 |
22 | try {
23 | const accessToken = jwt.sign(
24 | {
25 | userInfo: {
26 | userId: foundUser._id,
27 | roles: foundUser.roles,
28 | },
29 | },
30 | process.env.ACCESS_TOKEN_SECRET,
31 | { expiresIn: "30m" }
32 | );
33 | const refreshToken = jwt.sign(
34 | { userId: foundUser._id },
35 | process.env.REFRESH_TOKEN_SECRET,
36 | { expiresIn: "1d" }
37 | );
38 |
39 | res.cookie("jwt", refreshToken, {
40 | httpOnly: true,
41 | sameSite: "None",
42 | secure: true,
43 | maxAge: 24 * 60 * 60 * 1000,
44 | });
45 | res.status(200).json({ success: true, accessToken });
46 | } catch (error) {
47 | console.log(error);
48 | return res.status(500).json({
49 | message: "Server Side Error Occured",
50 | });
51 | }
52 | };
53 |
54 | const handleRegister = async (req, res) => {
55 | const { fullName, email, password, roles } = req.body;
56 | if (!fullName || !email || !password || roles.length == 0) {
57 | return res.status(400).json({
58 | message: "Required Fields Missing",
59 | });
60 | }
61 |
62 | const duplicate = await User.findOne({ email: email }).exec();
63 |
64 | if (duplicate)
65 | return res.status(409).json({
66 | message: "User Already Exist",
67 | });
68 | const [firstName, lastName] = fullName.split(" ");
69 |
70 | try {
71 | const hashedPassword = await bcrypt.hash(password, 10);
72 | await User.create({
73 | email: email,
74 | password: hashedPassword,
75 | firstName: firstName,
76 | lastName: lastName,
77 | roles: roles,
78 | });
79 |
80 | return res.status(200).json({
81 | success: true,
82 | message: "Registerd",
83 | });
84 | } catch (err) {
85 | console.log(err);
86 | return res.status(500).json({
87 | message: "Internal Error Occured",
88 | });
89 | }
90 | };
91 |
92 | const handleRefresh = (req, res) => {
93 | const cookies = req.cookies;
94 |
95 | if (!cookies?.jwt) return res.status(401).json({ message: "Unauthorized" });
96 |
97 | const refreshToken = cookies.jwt;
98 |
99 | jwt.verify(
100 | refreshToken,
101 | process.env.REFRESH_TOKEN_SECRET,
102 | async (err, decoded) => {
103 | if (err) return res.status(403).json({ message: "Forbidden" });
104 |
105 | const foundUser = await User.findById(decoded.userId).exec();
106 |
107 | if (!foundUser) return res.status(401).json({ message: "Unauthorized" });
108 |
109 | const accessToken = jwt.sign(
110 | {
111 | userInfo: {
112 | userId: foundUser._id,
113 | roles: foundUser.roles,
114 | },
115 | },
116 | process.env.ACCESS_TOKEN_SECRET,
117 | { expiresIn: "15m" }
118 | );
119 |
120 | res.json({ success: true, accessToken });
121 | }
122 | );
123 | };
124 |
125 | const handleLogout = (req, res) => {
126 | const cookies = req.cookies;
127 | if (!cookies?.jwt) return res.status(204); //No content
128 | res.clearCookie("jwt", { httpOnly: true, sameSite: "None", secure: true });
129 | res.json({ message: "Cookie cleared" });
130 | };
131 |
132 | const handleAddInstructor = async (req, res) => {
133 | try {
134 | const isUser = await User.findById(req.userInfo.userId);
135 | if (!isUser) {
136 | return res.status(404).json({
137 | success: false,
138 | message: "User not found",
139 | });
140 | }
141 | if (isUser.roles.includes("Instructor")) {
142 | return res.status(409).json({
143 | success: true,
144 | message: "User Already has Instructor role",
145 | });
146 | }
147 | const updatedUser = await User.findByIdAndUpdate(
148 | {
149 | _id: isUser._id,
150 | },
151 | {
152 | $push: {
153 | roles: "Instructor",
154 | },
155 | },
156 | { new: true }
157 | );
158 | const accessToken = jwt.sign(
159 | {
160 | userInfo: {
161 | userId: updatedUser._id,
162 | roles: updatedUser.roles,
163 | },
164 | },
165 | process.env.ACCESS_TOKEN_SECRET,
166 | { expiresIn: "15m" }
167 | );
168 |
169 | res.json({ success: true, accessToken });
170 | } catch (error) {
171 | console.log(error);
172 | return res.status(500).json({
173 | success: false,
174 | message: "Internal Error Occured",
175 | error: error.message,
176 | });
177 | }
178 | };
179 |
180 | module.exports = {
181 | handleLogin,
182 | handleRegister,
183 | handleRefresh,
184 | handleLogout,
185 | handleAddInstructor,
186 | };
187 |
--------------------------------------------------------------------------------
/Backend/controllers/Category.js:
--------------------------------------------------------------------------------
1 | const Category = require("../models/Category");
2 |
3 | exports.createCategory = async (req, res) => {
4 | try {
5 | const { name, description } = req.body;
6 | if (!name) {
7 | return res
8 | .status(400)
9 | .json({ success: false, message: "All fields are required" });
10 | }
11 | const CategorysDetails = await Category.create({
12 | name: name,
13 | description: description,
14 | });
15 | console.log(CategorysDetails);
16 | return res.status(200).json({
17 | success: true,
18 | message: "Categorys Created Successfully",
19 | });
20 | } catch (error) {
21 | return res.status(500).json({
22 | success: true,
23 | message: error.message,
24 | });
25 | }
26 | };
27 |
--------------------------------------------------------------------------------
/Backend/controllers/CourseProgress.js:
--------------------------------------------------------------------------------
1 | const SubSection = require("../models/SubSection");
2 | const CourseProgress = require("../models/CourseProgress");
3 | const Course = require("../models/Course");
4 | exports.updateCourseProgress = async (req, res) => {
5 | const { courseId, subSectionId } = req.body;
6 | const userId = req.userInfo.userId;
7 | try {
8 | const subSection = await SubSection.findById(subSectionId);
9 | if (!subSection) {
10 | return res.status(404).json({
11 | success: false,
12 | message: "SubSection Not Found",
13 | });
14 | }
15 | const courseProgress = await CourseProgress.findOne({
16 | courseId: courseId,
17 | userId: userId,
18 | });
19 |
20 | if (!courseProgress) {
21 | return res.status(404).json({
22 | success: false,
23 | message: "Course Progress Not Found",
24 | });
25 | }
26 |
27 | if (courseProgress.completedVideos.includes(subSectionId)) {
28 | return res.status(400).json({
29 | success: false,
30 | message: "Already Completed",
31 | });
32 | }
33 |
34 | courseProgress.completedVideos.push(subSectionId);
35 | await courseProgress.save();
36 |
37 | return res.status(200).json({
38 | success: true,
39 | message: "SubSection Updated Successfully",
40 | });
41 | } catch (error) {
42 | console.log(error);
43 | return res.status(500).json({
44 | success: false,
45 | message: "Internal Error Occured",
46 | });
47 | }
48 | };
49 |
50 | exports.getUserCourseProgress = async (req, res) => {
51 | try {
52 | const { courseId } = req.params;
53 | const userProgress = await CourseProgress.findOne({
54 | courseId: courseId,
55 | userId: req.userInfo.userId,
56 | }).populate("userId");
57 |
58 | return res.json({
59 | userProgress,
60 | });
61 | } catch (error) {
62 | console.log(error);
63 | return res.status(500).json({
64 | message: "Internal Error Occured",
65 | });
66 | }
67 | };
68 |
--------------------------------------------------------------------------------
/Backend/controllers/Payment.js:
--------------------------------------------------------------------------------
1 | const Course = require("../models/Course");
2 | const CourseProgress = require("../models/CourseProgress");
3 | const User = require("../models/User");
4 |
5 | const stripe = require("stripe")(process.env.STRIPE_KEY);
6 |
7 | exports.createSession = async (req, res) => {
8 | const { courseId } = req.body;
9 | const courseDetails = await Course.findById(courseId);
10 | if (!courseDetails) {
11 | return res.status(400).json({
12 | success: false,
13 | message: "Course Not Found",
14 | });
15 | }
16 |
17 | const session = await stripe.checkout.sessions.create({
18 | payment_method_types: ["card"],
19 | line_items: [
20 | {
21 | price_data: {
22 | currency: "inr",
23 | product_data: {
24 | name: courseDetails.courseName,
25 | images: [courseDetails.previewImage],
26 | },
27 | unit_amount: courseDetails.price * 100,
28 | },
29 | quantity: 1,
30 | },
31 | ],
32 | mode: "payment",
33 | metadata: {
34 | userId: req.userInfo.userId,
35 | courseId: courseId,
36 | },
37 | success_url: "http://localhost:5173/success/{CHECKOUT_SESSION_ID}",
38 | cancel_url: `http://localhost:5173/course/${courseId}`,
39 | });
40 | return res.json({ success: true, id: session.id });
41 | };
42 |
43 | exports.enrollStudent = async (req, res) => {
44 | try {
45 | const session = await stripe.checkout.sessions.retrieve(
46 | req.params.sessionId
47 | );
48 | if (!session) {
49 | return res.status(400).json({
50 | success: false,
51 | message: "An Error Occured",
52 | });
53 | }
54 | const userId = session.metadata.userId;
55 | const courseId = session.metadata.courseId;
56 |
57 | const alreddyEnrolled = await Course.findOne({ _id: courseId });
58 | if (alreddyEnrolled.studentsEnrolled.includes(userId)) {
59 | return res.status(409).json({
60 | success: false,
61 | message: "Already Enrolled",
62 | });
63 | }
64 |
65 | const enrolledCourse = await Course.findByIdAndUpdate(
66 | courseId,
67 | {
68 | $push: { studentsEnrolled: userId },
69 | },
70 | { new: true }
71 | );
72 | if (!enrolledCourse) {
73 | return res.status(400).json({
74 | success: false,
75 | message: "Course Not Found",
76 | });
77 | }
78 | const courseProgress = await CourseProgress.create({
79 | courseId: courseId,
80 | userId: userId,
81 | completedVideos: [],
82 | });
83 |
84 | const enrolledStudent = await User.findByIdAndUpdate(
85 | userId,
86 | {
87 | $push: {
88 | enrolledCourses: courseId,
89 | courseProgress: courseProgress._id,
90 | },
91 | },
92 | { new: true }
93 | );
94 | return res.status(200).json({
95 | success: true,
96 | message: "Student Enrolled Successfully",
97 | });
98 | } catch (error) {
99 | console.log(error);
100 | return res.status(500).json({
101 | success: false,
102 | message: "Internal Error Occured",
103 | });
104 | }
105 | };
106 |
--------------------------------------------------------------------------------
/Backend/controllers/Profile.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/User");
2 | const { uploadFileToCloudinary } = require("../utils/fileUploader");
3 | exports.editProfile = async (req, res) => {
4 | try {
5 | const { firstName, lastName, headline, biography } = req.body;
6 | const user = await User.findById(req.userInfo.userId);
7 | if (!user) {
8 | return res.status(404).json({
9 | success: false,
10 | message: "User Not Fund",
11 | });
12 | }
13 | if (firstName && lastName && headline && biography) {
14 | user.firstName = firstName;
15 | user.lastName = lastName;
16 | user.headline = headline;
17 | user.biography = biography;
18 | }
19 |
20 | if (req.files?.profilePicture) {
21 | const uploadDetails = await uploadFileToCloudinary(
22 | req.files.profilePicture,
23 | process.env.PROFILE_FOLDER_NAME
24 | );
25 | user.profilePicture = uploadDetails.secure_url;
26 | }
27 | await user.save();
28 | return res.status(200).json({
29 | success: true,
30 | message: "Profile Updated Successfully",
31 | });
32 | } catch (error) {
33 | console.log(error);
34 | return res.status(500).json({
35 | success: false,
36 | message: "Internal Error Occured",
37 | error: error.message,
38 | });
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/Backend/controllers/RatingAndReview.js:
--------------------------------------------------------------------------------
1 | const Course = require("../models/Course");
2 | const RatingAndReview = require("../models/RatingAndReview");
3 |
4 | exports.createRatingAndReview = async (req, res) => {
5 | try {
6 | const userId = req.userInfo.userId;
7 | const { rating, review, courseId } = req.body;
8 | const courseDetails = await Course.findOne({
9 | _id: courseId,
10 | studentsEnrolled: { $elemMatch: { $eq: userId } },
11 | });
12 | if (!courseDetails) {
13 | return res.status(404).json({
14 | success: false,
15 | message: "Student is not enrolled for this course",
16 | });
17 | }
18 |
19 | const alreadyReviewed = await RatingAndReview.findOne({
20 | user: userId,
21 | course: courseId,
22 | });
23 |
24 | if (alreadyReviewed) {
25 | return res.status(409).json({
26 | success: false,
27 | message: "You have already reviewed",
28 | });
29 | }
30 |
31 | const ratingAndReview = await RatingAndReview.create({
32 | rating,
33 | review,
34 | user: userId,
35 | course: courseId,
36 | });
37 |
38 | const updateCourseDetails = await Course.findByIdAndUpdate(
39 | courseId,
40 | {
41 | $push: { ratingAndReviews: ratingAndReview._id },
42 | },
43 | { new: true }
44 | );
45 |
46 | return res.status(200).json({
47 | success: true,
48 | message: "Rating and Review added Successfully",
49 | });
50 | } catch (error) {
51 | console.log(error);
52 | return res.status(500).json({
53 | success: false,
54 | message: "Internal Error Occured",
55 | });
56 | }
57 | };
58 |
59 | exports.editRatingAndReview = async (req, res) => {
60 | try {
61 | const userId = req.userInfo.userId;
62 | const { rating, review, courseId } = req.body;
63 | const courseDetails = await Course.findOne({
64 | _id: courseId,
65 | studentsEnrolled: { $elemMatch: { $eq: userId } },
66 | });
67 | if (!courseDetails) {
68 | return res.status(404).json({
69 | success: false,
70 | message: "Student is not enrolled for this course",
71 | });
72 | }
73 |
74 | const ratingAndReview = await RatingAndReview.findOneAndUpdate(
75 | { course: courseId, user: userId },
76 | { rating, review }
77 | );
78 |
79 | return res.status(200).json({
80 | success: true,
81 | message: "Rating and Review Updated Successfully",
82 | });
83 | } catch (error) {
84 | console.log(error);
85 | return res.status(500).json({
86 | success: false,
87 | message: "Internal Error Occured",
88 | });
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/Backend/controllers/Section.js:
--------------------------------------------------------------------------------
1 | const Section = require("../models/Section");
2 | const Course = require("../models/Course");
3 | const SubSection = require("../models/SubSection");
4 |
5 | exports.getAllSections = async (req, res) => {
6 | const sections = await Section.find().populate("subSection").exec();
7 | return res.json({
8 | success: true,
9 | data: sections,
10 | message: "Success",
11 | });
12 | };
13 |
14 | exports.createSection = async (req, res) => {
15 | try {
16 | const { sectionName, courseId } = req.body;
17 |
18 | if (!sectionName || !courseId) {
19 | return res.status(400).json({
20 | success: false,
21 | message: "Missing required fields",
22 | });
23 | }
24 |
25 | const newSection = await Section.create({ sectionName });
26 |
27 | const updatedCourse = await Course.findByIdAndUpdate(
28 | courseId,
29 | {
30 | $push: {
31 | courseContent: newSection._id,
32 | },
33 | },
34 | { new: true }
35 | );
36 |
37 | res.status(200).json({
38 | success: true,
39 | message: "Section created successfully",
40 | });
41 | } catch (error) {
42 | res.status(500).json({
43 | success: false,
44 | message: "Internal server error",
45 | error: error.message,
46 | });
47 | }
48 | };
49 |
50 | exports.updateSection = async (req, res) => {
51 | try {
52 | const { sectionName, sectionId } = req.body;
53 | const section = await Section.findByIdAndUpdate(
54 | sectionId,
55 | {
56 | sectionName,
57 | },
58 | { new: true }
59 | );
60 |
61 | res.status(200).json({
62 | success: true,
63 | message: "Section Updated SUCCESSFULLY!",
64 | });
65 | } catch (error) {
66 | console.error("Error updating section:", error);
67 | res.status(500).json({
68 | success: false,
69 | message: "Internal server error",
70 | });
71 | }
72 | };
73 |
74 | exports.deleteSection = async (req, res) => {
75 | try {
76 | console.log(req.body);
77 | const { deleteSectionId, courseId } = req.body;
78 | const sectionId = deleteSectionId;
79 | await Course.findByIdAndUpdate(courseId, {
80 | $pull: {
81 | courseContent: sectionId,
82 | },
83 | });
84 | const section = await Section.findById(sectionId);
85 | if (!section) {
86 | return res.status(404).json({
87 | success: false,
88 | message: "Section not Found",
89 | });
90 | }
91 |
92 | await SubSection.deleteMany({ _id: { $in: section.subSection } });
93 |
94 | await Section.findByIdAndDelete(sectionId);
95 |
96 | const course = await Course.findById(courseId)
97 | .populate({
98 | path: "courseContent",
99 | populate: {
100 | path: "subSection",
101 | },
102 | })
103 | .exec();
104 |
105 | res.status(200).json({
106 | success: true,
107 | message: "Section deleted",
108 | data: course,
109 | });
110 | } catch (error) {
111 | console.error("Error deleting section:", error);
112 | res.status(500).json({
113 | success: false,
114 | message: "Internal server error",
115 | });
116 | }
117 | };
118 |
--------------------------------------------------------------------------------
/Backend/controllers/SubSection.js:
--------------------------------------------------------------------------------
1 | const SubSection = require("../models/SubSection");
2 | const Section = require("../models/Section");
3 | const { uploadFileToCloudinary } = require("../utils/fileUploader");
4 |
5 | exports.createSubSection = async (req, res) => {
6 | try {
7 | const { sectionId, subSectionName } = req.body;
8 | console.log(sectionId, subSectionName);
9 |
10 | if (!sectionId || !subSectionName) {
11 | return res.status(400).json({
12 | success: false,
13 | message: "all fields are REQUIRED !!",
14 | });
15 | }
16 |
17 | const subSectionDetails = await SubSection.create({
18 | subSectionName,
19 | });
20 |
21 | const updatedSection = await Section.findByIdAndUpdate(
22 | { _id: sectionId },
23 | {
24 | $push: {
25 | subSection: subSectionDetails._id,
26 | },
27 | },
28 | {
29 | new: true,
30 | }
31 | ).populate("subSection");
32 |
33 | //sending.. final response
34 | return res.status(200).json({
35 | success: true,
36 | message: "subSection created SUCCESSFULLY !",
37 | data: updatedSection,
38 | });
39 | } catch (error) {
40 | return res.status(500).json({
41 | success: false,
42 | message: "an error ocurred while creating sub-Section!",
43 | error: error.message,
44 | });
45 | }
46 | };
47 |
48 | exports.updateSubSection = async (req, res) => {
49 | try {
50 | const { subSectionId, subSectionName } = req.body;
51 | const subSection = await SubSection.findById(subSectionId);
52 |
53 | if (!subSection) {
54 | return res.status(404).json({
55 | success: false,
56 | message: "SubSection not found",
57 | });
58 | }
59 |
60 | if (subSectionName !== undefined) {
61 | subSection.subSectionName = subSectionName;
62 | }
63 |
64 | await subSection.save();
65 |
66 | return res.json({
67 | success: true,
68 | message: "Section updated successfully",
69 | });
70 | } catch (error) {
71 | console.error(error);
72 | return res.status(500).json({
73 | success: false,
74 | message: "An error occurred while updating the section",
75 | });
76 | }
77 | };
78 |
79 | exports.deleteSubSection = async (req, res) => {
80 | try {
81 | const { subSectionId, sectionId } = req.body;
82 | await Section.findByIdAndUpdate(
83 | { _id: sectionId },
84 | {
85 | $pull: {
86 | subSection: subSectionId,
87 | },
88 | }
89 | );
90 | const subSection = await SubSection.findByIdAndDelete({
91 | _id: subSectionId,
92 | });
93 |
94 | if (!subSection) {
95 | return res
96 | .status(404)
97 | .json({ success: false, message: "SubSection not found" });
98 | }
99 |
100 | const updatedSection = await Section.findById(sectionId).populate(
101 | "subSection"
102 | );
103 |
104 | return res.json({
105 | success: true,
106 | data: updatedSection,
107 | message: "SubSection deleted successfully",
108 | });
109 | } catch (error) {
110 | console.error(error);
111 | return res.status(500).json({
112 | success: false,
113 | message: "An error occurred while deleting the SubSection",
114 | });
115 | }
116 | };
117 |
118 | exports.addSubSectionContent = async (req, res) => {
119 | try {
120 | const { subSectionId } = req.body;
121 | const subSection = await SubSection.findById(subSectionId);
122 | if (!subSectionId) {
123 | return res.status(400).json({
124 | success: false,
125 | message: "All fields required",
126 | });
127 | }
128 | if (!subSection) {
129 | return res.status(404).json({
130 | success: false,
131 | message: "Fields not found",
132 | });
133 | }
134 | if (!req.files || req.files.videoFile == undefined) {
135 | return res.status(404).json({
136 | success: false,
137 | message: "Video Not Found",
138 | });
139 | }
140 | const uploadDetails = await uploadFileToCloudinary(
141 | req.files.videoFile,
142 | process.env.FOLDER_NAME
143 | );
144 | await SubSection.findByIdAndUpdate(
145 | subSectionId,
146 | {
147 | videoUrl: uploadDetails.secure_url,
148 | timeDuration: uploadDetails.duration,
149 | videoDate: uploadDetails.created_at,
150 | videoName: req.files.videoFile.name,
151 | },
152 | { new: true }
153 | );
154 | return res.json({
155 | success: true,
156 | message: "Video Uploaded successfully",
157 | });
158 | } catch (error) {
159 | console.error(error);
160 | return res.status(500).json({
161 | success: false,
162 | message: "An error occurred while uploding video",
163 | });
164 | }
165 | };
166 |
--------------------------------------------------------------------------------
/Backend/controllers/User.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/User");
2 |
3 | exports.getInstructorDetails = async (req, res) => {
4 | try {
5 | const instructorDetails = await User.findById(req.userInfo.userId)
6 | .populate("courses")
7 | .select("-password");
8 | if (!instructorDetails) {
9 | return res.status(404).json({
10 | success: false,
11 | message: "Instructor Not Found",
12 | });
13 | }
14 | return res.status(200).json({
15 | success: true,
16 | data: instructorDetails,
17 | });
18 | } catch (error) {
19 | return res.status(500).json({
20 | success: false,
21 | message: "Internal Error Occured",
22 | error: error.message,
23 | });
24 | }
25 | };
26 |
27 | exports.getUserDetails = async (req, res) => {
28 | try {
29 | const userDetails = await User.findById(req.userInfo.userId).populate({
30 | path: "courseProgress",
31 | populate: {
32 | path: "courseId",
33 | populate: {
34 | path: "courseContent",
35 | populate: {
36 | path: "subSection",
37 | },
38 | },
39 | },
40 | });
41 | return res.status(200).json({
42 | success: true,
43 | data: userDetails,
44 | });
45 | } catch (error) {
46 | console.log(error);
47 | return res.status(500).json({
48 | success: false,
49 | message: "Internal Error Occured",
50 | error: error.message,
51 | });
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/Backend/middleware/verifyJWT.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 |
3 | exports.verifyJWT = (req, res, next) => {
4 | const authHeader = req.headers.authorization || req.headers.Authorization;
5 |
6 | if (!authHeader?.startsWith("Bearer ")) {
7 | return res.status(401).json({ message: "Unauthorized" });
8 | }
9 |
10 | const token = authHeader.split(" ")[1];
11 |
12 | jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, decoded) => {
13 | if (err) {
14 | console.log(err);
15 | return res.status(403).json({ message: "Forbidden" });
16 | }
17 | req.userInfo = decoded.userInfo;
18 | next();
19 | });
20 | };
21 |
22 | exports.isStudent = (req, res, next) => {
23 | try {
24 | if (!req.userInfo.roles.includes("Student")) {
25 | return res.status(401).json({
26 | success: false,
27 | message: "Protected route for students only",
28 | });
29 | }
30 | next();
31 | } catch (error) {
32 | return res.status(500).json({
33 | success: false,
34 | message: "Internal error occured",
35 | });
36 | }
37 | };
38 |
39 | exports.isInstructor = (req, res, next) => {
40 | try {
41 | if (!req.userInfo.roles.includes("Instructor")) {
42 | return res.status(401).json({
43 | success: false,
44 | message: "Protected route for instructor only",
45 | });
46 | }
47 | next();
48 | } catch (error) {
49 | console.log(error);
50 | return res.status(500).json({
51 | success: false,
52 | message: "Internal error occured",
53 | });
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/Backend/models/Course.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const courseSchema = new mongoose.Schema(
4 | {
5 | courseName: {
6 | type: String,
7 | },
8 | courseSubtitle: {
9 | type: String,
10 | },
11 |
12 | description: {
13 | type: String,
14 | },
15 |
16 | locale: {
17 | type: String,
18 | },
19 | instructionalLevel: {
20 | type: String,
21 | },
22 |
23 | category: {
24 | type: String,
25 | },
26 | subCategory: {
27 | type: String,
28 | },
29 |
30 | previewImage: {
31 | type: String,
32 | },
33 |
34 | promoVideo: {
35 | type: String,
36 | },
37 | learningObjectives: {
38 | type: [
39 | {
40 | objective: String,
41 | },
42 | ],
43 | },
44 | prerequisites: {
45 | type: [
46 | {
47 | prerequisite: String,
48 | },
49 | ],
50 | },
51 | instructor: {
52 | type: mongoose.Schema.Types.ObjectId,
53 | ref: "User",
54 | required: true,
55 | },
56 | courseContent: [
57 | {
58 | type: mongoose.Schema.Types.ObjectId,
59 | ref: "Section",
60 | },
61 | ],
62 | ratingAndReviews: [
63 | {
64 | type: mongoose.Schema.Types.ObjectId,
65 | ref: "RatingAndReview",
66 | },
67 | ],
68 |
69 | studentsEnrolled: [
70 | {
71 | type: mongoose.Schema.Types.ObjectId,
72 | ref: "User",
73 | },
74 | ],
75 | price: {
76 | type: String,
77 | },
78 | status: {
79 | type: String,
80 | enum: ["Draft", "Published"],
81 | },
82 | },
83 | { timestamps: true }
84 | );
85 |
86 | module.exports = mongoose.model("Course", courseSchema);
87 |
--------------------------------------------------------------------------------
/Backend/models/CourseProgress.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const courseProgress = new mongoose.Schema({
4 | courseId: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | ref: "Course",
7 | },
8 | userId: {
9 | type: mongoose.Schema.Types.ObjectId,
10 | ref: "User",
11 | },
12 | completedVideos: [
13 | {
14 | type: mongoose.Schema.Types.ObjectId,
15 | ref: "SubSection",
16 | },
17 | ],
18 | });
19 |
20 | module.exports = mongoose.model("courseProgress", courseProgress);
21 |
--------------------------------------------------------------------------------
/Backend/models/Profile.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const profileSchema = new mongoose.Schema({
4 | firstName: {
5 | type: String,
6 | },
7 | lastName: {
8 | type: String,
9 | },
10 | headline: {
11 | type: String,
12 | },
13 | biography: {
14 | type: String,
15 | },
16 | profilePicture: {
17 | type: String,
18 | },
19 | });
20 |
21 | module.exports = mongoose.model("Profile", profileSchema);
22 |
--------------------------------------------------------------------------------
/Backend/models/RatingAndReview.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const ratingAndReviewSchema = new mongoose.Schema(
4 | {
5 | user: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "User",
8 | },
9 | rating: {
10 | type: Number,
11 | },
12 | review: {
13 | type: String,
14 | },
15 | course: {
16 | type: mongoose.Schema.Types.ObjectId,
17 | ref: "Course",
18 | },
19 | },
20 | { timestamps: true }
21 | );
22 |
23 | module.exports = mongoose.model("RatingAndReview", ratingAndReviewSchema);
24 |
--------------------------------------------------------------------------------
/Backend/models/Section.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const sectionSchema = new mongoose.Schema({
4 | sectionName: {
5 | type: String,
6 | },
7 | subSection: [
8 | {
9 | type: mongoose.Schema.Types.ObjectId,
10 | required: true,
11 | ref: "SubSection",
12 | },
13 | ],
14 | });
15 |
16 | module.exports = mongoose.model("Section", sectionSchema);
17 |
--------------------------------------------------------------------------------
/Backend/models/SubSection.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const subSectionSchema = new mongoose.Schema({
4 | subSectionName: {
5 | type: String,
6 | },
7 | timeDuration: {
8 | type: String,
9 | },
10 | videoName: {
11 | type: String,
12 | },
13 | videoUrl: {
14 | type: String,
15 | },
16 | videoDate: {
17 | type: String,
18 | },
19 | });
20 |
21 | module.exports = mongoose.model("SubSection", subSectionSchema);
22 |
--------------------------------------------------------------------------------
/Backend/models/User.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const userSchema = new mongoose.Schema({
4 | firstName: {
5 | type: String,
6 | },
7 | lastName: {
8 | type: String,
9 | },
10 | headline: {
11 | type: String,
12 | },
13 | biography: {
14 | type: String,
15 | },
16 | profilePicture: {
17 | type: String,
18 | },
19 | email: { type: String, required: true },
20 | password: { type: String, required: true },
21 | courses: [
22 | /*Created Courses*/
23 | {
24 | type: mongoose.Schema.Types.ObjectId,
25 | ref: "Course",
26 | },
27 | ],
28 | roles: {
29 | type: [String],
30 | },
31 | enrolledCourses: {
32 | type: mongoose.Schema.Types.ObjectId,
33 | ref: "Course",
34 | },
35 | courseProgress: [
36 | {
37 | type: mongoose.Schema.Types.ObjectId,
38 | ref: "courseProgress",
39 | },
40 | ],
41 | });
42 |
43 | module.exports = mongoose.model("User", userSchema);
44 |
--------------------------------------------------------------------------------
/Backend/nodemon.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": [
3 | "node_modules"
4 | ]
5 | }
--------------------------------------------------------------------------------
/Backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "nodemon server.js --config nodemon.config.json",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "bcrypt": "^5.1.1",
15 | "cloudinary": "^1.41.0",
16 | "cookie-parser": "^1.4.6",
17 | "cors": "^2.8.5",
18 | "dotenv": "^16.3.1",
19 | "ejs": "^3.1.9",
20 | "express": "^4.18.2",
21 | "express-fileupload": "^1.4.3",
22 | "jsonwebtoken": "^9.0.2",
23 | "mongoose": "^8.0.0",
24 | "stripe": "^14.9.0"
25 | },
26 | "devDependencies": {
27 | "nodemon": "^3.0.1"
28 | },
29 | "proxy": "http://localhost:5173"
30 | }
31 |
--------------------------------------------------------------------------------
/Backend/routes/Auth.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const {
4 | handleLogin,
5 | handleRegister,
6 | handleRefresh,
7 | handleLogout,
8 | handleAddInstructor,
9 | } = require("../controllers/Auth.js");
10 | const { verifyJWT } = require("../middleware/verifyJWT.js");
11 |
12 | router.post("/login", handleLogin);
13 |
14 | router.post("/signup", handleRegister);
15 |
16 | router.get("/refresh", handleRefresh);
17 |
18 | router.post("/logout", handleLogout);
19 |
20 | //Add Instructor Role to User
21 | router.get("/addInstructor", verifyJWT, handleAddInstructor);
22 |
23 | module.exports = router;
24 |
--------------------------------------------------------------------------------
/Backend/routes/Course.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 |
4 | const {
5 | createCourse,
6 | handleEditGoals,
7 | handleEditBasics,
8 | getInstructorCourseDetails,
9 | getCategoryCourses,
10 | getFullCourseDetails,
11 | getSearchCourses,
12 | publishCourse,
13 | getStudentDetails,
14 | } = require("../controllers/Course");
15 |
16 | const {
17 | getAllSections,
18 | createSection,
19 | updateSection,
20 | deleteSection,
21 | } = require("../controllers/Section");
22 |
23 | const {
24 | createSubSection,
25 | addSubSectionContent,
26 | updateSubSection,
27 | deleteSubSection,
28 | } = require("../controllers/SubSection");
29 |
30 | const { verifyJWT, isInstructor } = require("../middleware/verifyJWT");
31 |
32 | //----------------------------Instructor Course --------------------------------------
33 | router.post("/createCourse", verifyJWT, isInstructor, createCourse);
34 | router.post("/publishCourse", verifyJWT, isInstructor, publishCourse);
35 | router.get("/getCourseDetails/:courseId", getInstructorCourseDetails);
36 |
37 | //------------------------------- Section ----------------------------------------------
38 | router.get("/getsections", getAllSections);
39 | router.post("/createSection", verifyJWT, isInstructor, createSection);
40 | router.post("/updateSection", verifyJWT, isInstructor, updateSection);
41 | router.post("/deleteSection", verifyJWT, isInstructor, deleteSection);
42 |
43 | router.post("/editCourseGoals", verifyJWT, isInstructor, handleEditGoals);
44 | router.post("/editCourseBasics", verifyJWT, isInstructor, handleEditBasics);
45 |
46 | // ----------------------------- SubSection -------------------------------------
47 | router.post("/createSubSection", verifyJWT, isInstructor, createSubSection);
48 | router.post("/updateSubSection", verifyJWT, isInstructor, updateSubSection);
49 | router.post("/deleteSubSection", verifyJWT, isInstructor, deleteSubSection);
50 | router.post(
51 | "/addSubSectionContent",
52 | verifyJWT,
53 | isInstructor,
54 | addSubSectionContent
55 | );
56 |
57 | //----------------------------- Course ----------------------------------------
58 | router.get("/getCategoryCourses/:category", getCategoryCourses);
59 | router.get("/getFullCourseDetails/:courseId", verifyJWT, getFullCourseDetails);
60 | router.get("/student", verifyJWT, getStudentDetails);
61 | router.get("/search/:query", getSearchCourses);
62 |
63 | module.exports = router;
64 |
--------------------------------------------------------------------------------
/Backend/routes/CourseProgress.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | getUserCourseProgress,
4 | updateCourseProgress,
5 | } = require("../controllers/CourseProgress");
6 | const { verifyJWT } = require("../middleware/verifyJWT");
7 | const router = express.Router();
8 |
9 | router.get(
10 | "/getUserCourseProgress/:courseId",
11 | verifyJWT,
12 | getUserCourseProgress
13 | );
14 | router.post("/updateCourseProgress", verifyJWT, updateCourseProgress);
15 |
16 | module.exports = router;
17 |
--------------------------------------------------------------------------------
/Backend/routes/Instructor.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { verifyJWT, isInstructor } = require("../middleware/verifyJWT");
3 | const { getInstructorDetails } = require("../controllers/User");
4 | const router = express.Router();
5 |
6 | router.get("/", verifyJWT, isInstructor, getInstructorDetails);
7 |
8 | module.exports = router;
9 |
--------------------------------------------------------------------------------
/Backend/routes/Payment.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { createSession, enrollStudent } = require("../controllers/Payment");
3 | const { verifyJWT } = require("../middleware/verifyJWT");
4 | const router = express.Router();
5 |
6 | router.post("/createSession", verifyJWT, createSession);
7 | router.get("/completePayment/:sessionId", verifyJWT, enrollStudent);
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/Backend/routes/Profile.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const { editProfile } = require("../controllers/Profile");
4 | const { verifyJWT } = require("../middleware/verifyJWT");
5 |
6 | router.post("/edit", verifyJWT, editProfile);
7 |
8 | module.exports = router;
9 |
--------------------------------------------------------------------------------
/Backend/routes/RatingAndReview.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | createRatingAndReview,
4 | editRatingAndReview,
5 | } = require("../controllers/RatingAndReview");
6 | const { verifyJWT } = require("../middleware/verifyJWT");
7 |
8 | const router = express.Router();
9 |
10 | router.post("/create", verifyJWT, createRatingAndReview);
11 | router.post("/edit", verifyJWT, editRatingAndReview);
12 |
13 | module.exports = router;
14 |
--------------------------------------------------------------------------------
/Backend/routes/User.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { getUserDetails } = require("../controllers/User");
3 | const { verifyJWT } = require("../middleware/verifyJWT");
4 | const router = express.Router();
5 |
6 | router.get("/getUserDetails", verifyJWT, getUserDetails);
7 |
8 | module.exports = router;
9 |
--------------------------------------------------------------------------------
/Backend/server.js:
--------------------------------------------------------------------------------
1 | const dotenv = require("dotenv");
2 | dotenv.config();
3 | const express = require("express");
4 | const app = express();
5 |
6 | const authRoutes = require("./routes/Auth");
7 | const profileRoutes = require("./routes/Profile");
8 | const userRoutes = require("./routes/User");
9 | const instructorRoutes = require("./routes/Instructor");
10 | const paymentRoutes = require("./routes/Payment");
11 | const ratingAndReviewRoutes = require("./routes/RatingAndReview");
12 | const courseProgressRoutes = require("./routes/CourseProgress");
13 | const courseRoutes = require("./routes/Course");
14 | const database = require("./config/database");
15 | const cookieParser = require("cookie-parser");
16 | const cors = require("cors");
17 | const { cloudinaryConnect } = require("./config/cloudinary");
18 | fileUpload = require("express-fileupload");
19 |
20 | const PORT = process.env.PORT || 4000;
21 |
22 | //database connect
23 | database.connect();
24 | //middlewares
25 | app.use(express.json());
26 | app.use(cookieParser());
27 | app.use(
28 | cors({
29 | origin: ["http://localhost:5173"],
30 | credentials: true,
31 | })
32 | );
33 |
34 | app.use(
35 | fileUpload({
36 | useTempFiles: true,
37 | tempFileDir: "/tmp",
38 | })
39 | );
40 | //cloudinary connection
41 | cloudinaryConnect();
42 |
43 | //routes
44 | app.use("/api/v1/auth", authRoutes);
45 | app.use("/api/v1/profile", profileRoutes);
46 | app.use("/api/v1/course", courseRoutes);
47 | app.use("/api/v1/user", userRoutes);
48 | app.use("/api/v1/instructor", instructorRoutes);
49 | app.use("/api/v1/payment", paymentRoutes);
50 | app.use("/api/v1/ratingAndReview", ratingAndReviewRoutes);
51 | app.use("/api/v1/courseProgress", courseProgressRoutes);
52 |
53 | app.get("/", (req, res) => {
54 | return res.json({
55 | success: true,
56 | message: "Your server is up and running....",
57 | });
58 | });
59 |
60 | app.listen(PORT, () => {
61 | console.log(`App is running at ${PORT}`);
62 | });
63 |
--------------------------------------------------------------------------------
/Backend/utils/fileUploader.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary").v2;
2 |
3 | exports.uploadFileToCloudinary = async (file, folder, height, quality) => {
4 | const options = { folder };
5 | if (height) {
6 | options.height = height;
7 | }
8 | if (quality) {
9 | options.quality = quality;
10 | }
11 | options.resource_type = "auto";
12 |
13 | return await cloudinary.uploader.upload(file.tempFilePath, options);
14 | };
15 |
--------------------------------------------------------------------------------
/Frontend/README.md:
--------------------------------------------------------------------------------
1 | ## Learning Management System (Udemy Clone)
2 |
--------------------------------------------------------------------------------
/Frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@fortawesome/fontawesome-svg-core": "^6.4.2",
14 | "@fortawesome/free-solid-svg-icons": "^6.4.2",
15 | "@fortawesome/react-fontawesome": "^0.2.0",
16 | "@react-pdf/renderer": "^3.1.14",
17 | "@reduxjs/toolkit": "^1.9.7",
18 | "@smastrom/react-rating": "^1.4.0",
19 | "@stripe/react-stripe-js": "^2.4.0",
20 | "@stripe/stripe-js": "^2.2.2",
21 | "jwt-decode": "^4.0.0",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0",
24 | "react-hook-form": "^7.48.2",
25 | "react-icons": "^4.12.0",
26 | "react-loader-spinner": "^6.1.0",
27 | "react-redux": "^8.1.3",
28 | "react-router-dom": "^6.19.0"
29 | },
30 | "devDependencies": {
31 | "@hookform/devtools": "^4.3.1",
32 | "@types/react": "^18.2.15",
33 | "@types/react-dom": "^18.2.7",
34 | "@vitejs/plugin-react": "^4.0.3",
35 | "autoprefixer": "^10.4.16",
36 | "eslint": "^8.45.0",
37 | "eslint-plugin-react": "^7.32.2",
38 | "eslint-plugin-react-hooks": "^4.6.0",
39 | "eslint-plugin-react-refresh": "^0.4.3",
40 | "postcss": "^8.4.31",
41 | "tailwindcss": "^3.3.5",
42 | "vite": "^4.4.5"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/Frontend/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Frontend/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/App.css
--------------------------------------------------------------------------------
/Frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Routes, Route } from "react-router-dom";
2 | import Teaching from "./features/Courses/Teaching";
3 | import Signup from "./features/Auth/Signup";
4 | import CreateCourse from "./features/Instructor/Course/CreateCourse";
5 | import CourseLayout from "./features/Instructor/Course/CourseLayout";
6 | import CourseGoals from "./features/Instructor/Course/Goals";
7 | import Dashboard from "./features/Instructor/DashBoard";
8 | import LandingPage from "./features/Instructor/Course/LandingPage";
9 | import Curriculum from "./features/Instructor/Course/Curriculum";
10 | import Home from "./features/Courses/Home";
11 | import Login from "./features/auth/Login";
12 | import PersistLogin from "./features/Auth/PersistLogin";
13 | import RequireAuth from "./features/Auth/RequireAuth";
14 | import ProfileLayout from "./features/Common/Profile/ProfileLayout";
15 | import BasicInformation from "./features/Common/Profile/BasicInformation";
16 | import ProfilePhoto from "./features/Common/Profile/ProfilePhoto";
17 | import Course from "./features/Courses/Course";
18 | import Success from "./features/Courses/Success";
19 | import MyCourses from "./features/Student/MyCourses";
20 | import { ROLES } from "./data/roles";
21 | import "@smastrom/react-rating/style.css";
22 | import "./App.css";
23 | import SearchCourses from "./features/Courses/SearchCourses";
24 | import NavLayout from "./components/NavLayout";
25 |
26 | function App() {
27 | return (
28 |
29 |
30 | }
33 | />
34 | }
37 | />
38 | } />
39 | }>
40 | }>
41 | } />
42 | } />
43 | } />
44 | } />
45 |
46 |
49 | }
50 | >
51 | } />
52 | } />
53 | }>
54 | }
57 | />
58 | } />
59 |
60 |
61 |
62 | }>
63 | } />
64 | } />
65 | }>
66 | } />
67 | } />
68 |
69 | }>
70 | } />
71 | }
74 | />
75 | } />
76 |
77 |
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | export default App;
85 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/567828_67d0.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/567828_67d0.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/851712_fc61_6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/851712_fc61_6.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/Udemy-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/Udemy-logo.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/angela_yu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/angela_yu.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/blankProfile.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/blankProfile.webp
--------------------------------------------------------------------------------
/Frontend/src/assets/images/cart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/cart.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/check.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/comp1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/comp2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/comp3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/comp4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/comp5.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/front-img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/front-img.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/get-rewarded.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/get-rewarded.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/heart.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/imagePlaceholder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/imagePlaceholder.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/infinity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/infinity.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/inspire-learners.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/inspire-learners.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/jpeg1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/jpeg1.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/jpeg2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/jpeg2.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/jpeg3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/jpeg3.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/jpeg4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/jpeg4.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/lady.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/lady.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/logo-long.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/logo-udemy-words.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/logo-udemy.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Frontend/src/assets/images/man.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/man.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic1.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic10.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic11.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic13.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic14.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic15.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic16.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic17.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic18.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic19.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic2.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic20.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic21.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic22.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic22.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic3.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic4.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic5.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic6.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic7.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic8.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pic9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pic9.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/play.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/python.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/python.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/pythonImage.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/pythonImage.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/rating.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/rating.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/rustlang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/rustlang.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/scroll-arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/scroll-arrow.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/scroll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/scroll.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/search.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/star.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/teach-your-way.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/teach-your-way.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/teaching-front.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/teaching-front.jpg
--------------------------------------------------------------------------------
/Frontend/src/assets/images/ulogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/ulogo.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/world.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/world.png
--------------------------------------------------------------------------------
/Frontend/src/assets/images/worldwide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aditya7812/Learning-Management-System/03de86f910e89f2f32d1f2b21192cad10a0d9575/Frontend/src/assets/images/worldwide.png
--------------------------------------------------------------------------------
/Frontend/src/assets/logo-udemy.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Frontend/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Frontend/src/components/NavLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 | import Navbar from "./Navbar";
3 |
4 | const NavLayout = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default NavLayout;
14 |
--------------------------------------------------------------------------------
/Frontend/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useNavigate } from "react-router-dom";
2 | import { useEffect, useRef } from "react";
3 | import { MdLogout } from "react-icons/md";
4 | import { useLogoutMutation } from "../reducers/api/authApi";
5 | import useAuth from "../hooks/useAuth";
6 | import Logo from "../assets/logo-udemy.svg";
7 | import Search from "../assets/images/search.png";
8 | import BlankProfile from "../assets/images/blankProfile.webp";
9 | import { useGetUserDetailsQuery } from "../reducers/api/userApi";
10 |
11 | function Navbar() {
12 | const { data: userDetails } = useGetUserDetailsQuery();
13 | const [logout, { isSuccess }] = useLogoutMutation();
14 | const navigate = useNavigate();
15 | const queryRef = useRef(null);
16 | const { isStudent, isInstructor } = useAuth();
17 |
18 | useEffect(() => {
19 | if (isSuccess) navigate("/join/login");
20 | }, [isSuccess, navigate]);
21 | const handleSubmit = (e) => {
22 | e.preventDefault();
23 | navigate(`/search/${queryRef.current.value}`);
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Categories
34 |
52 | {isInstructor ? (
53 |
Instructor
54 | ) : (
55 |
Teach on Udemy
56 | )}
57 | {isStudent ? (
58 |
59 |
My Learning
60 |
67 |
76 |
77 |
78 |
79 |
80 |
81 | ) : (
82 |
83 |
84 | Log In
85 |
86 |
87 | Sign Up
88 |
89 |
90 | )}
91 |
92 | );
93 | }
94 |
95 | export default Navbar;
96 |
--------------------------------------------------------------------------------
/Frontend/src/components/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import { TailSpin } from "react-loader-spinner";
2 | const Spinner = () => {
3 | return (
4 |
5 |
15 |
16 | );
17 | };
18 | export default Spinner;
19 |
--------------------------------------------------------------------------------
/Frontend/src/data/categories.js:
--------------------------------------------------------------------------------
1 | export const categories = [
2 | {
3 | Development: [
4 | "Android Development",
5 | "iOS Development",
6 | "Web Development",
7 | "Game Development",
8 | "Software Testing",
9 | "Programming Languauge",
10 | "No-code Development",
11 | ],
12 | },
13 | {
14 | Finance: [
15 | "Accounting",
16 | "Economics",
17 | "Finance",
18 | "Taxes",
19 | "Investing",
20 | "Money Management Tools",
21 | "Compliance",
22 | ],
23 | },
24 | {
25 | Design: [
26 | "Web Design",
27 | "Desing Tools",
28 | "Game Design",
29 | "Fashion Design",
30 | "Architectural Design",
31 | "Other Design",
32 | ],
33 | },
34 | {
35 | "Personal Development": [
36 | "Personal Transformation",
37 | "Leadership",
38 | "Career Development",
39 | "Happiness",
40 | "Creativity",
41 | "Influence",
42 | ],
43 | },
44 | {
45 | Lifestyle: [
46 | "Arts",
47 | "Makeup",
48 | "Food",
49 | "Gaming",
50 | "Travel",
51 | "Home Improvement",
52 | ],
53 | },
54 | {
55 | Business: [
56 | "Entrepreneueship",
57 | "Communication",
58 | "Management",
59 | "Sales",
60 | "Operations",
61 | "Business Law",
62 | "Industry",
63 | "Media",
64 | ],
65 | },
66 | ];
67 |
--------------------------------------------------------------------------------
/Frontend/src/data/dashoard-links.js:
--------------------------------------------------------------------------------
1 | export const DashboardLinks = [
2 | {
3 | title: "Plan your course",
4 | sublinks: [
5 | {
6 | subtitle: "Intended Learners",
7 | link: "goals",
8 | },
9 | ],
10 | },
11 | {
12 | title: "Create your content",
13 | sublinks: [
14 | {
15 | subtitle: "Curriculum",
16 | link: "curriculum",
17 | },
18 | ],
19 | },
20 | {
21 | title: "Publish your Course",
22 | sublinks: [
23 | {
24 | subtitle: "Course landing page",
25 | link: "basics",
26 | },
27 | ],
28 | },
29 | ];
30 |
--------------------------------------------------------------------------------
/Frontend/src/data/priceTier.js:
--------------------------------------------------------------------------------
1 | export const PRICE_TIER = [
2 | {
3 | FREE: 0,
4 | },
5 | {
6 | "₹499": 499,
7 | },
8 | {
9 | "₹799": 799,
10 | },
11 | {
12 | "₹999": 999,
13 | },
14 | {
15 | "₹1499": 1499,
16 | },
17 | {
18 | "₹1999": 1999,
19 | },
20 | ];
21 |
--------------------------------------------------------------------------------
/Frontend/src/data/roles.js:
--------------------------------------------------------------------------------
1 | export const ROLES = {
2 | Student: "Student",
3 | Instructor: "Instructor",
4 | };
5 |
--------------------------------------------------------------------------------
/Frontend/src/features/Auth/Login/index.jsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { useDispatch } from "react-redux";
3 | import { Link, useLocation, useNavigate } from "react-router-dom";
4 | import { useLoginMutation } from "../../../reducers/api/authApi";
5 | import { setCredentials } from "../../../reducers/authSlice";
6 |
7 | const Login = () => {
8 | const navigate = useNavigate();
9 | const location = useLocation();
10 | const dispatch = useDispatch();
11 | const [login] = useLoginMutation();
12 | const { register, handleSubmit } = useForm();
13 | let from = location.state?.from?.pathname || "/";
14 |
15 | const onSubmit = async (data) => {
16 | const result = await login({ ...data }).unwrap();
17 | if (result.success) {
18 | dispatch(setCredentials(result.accessToken));
19 | navigate(from, { replace: true });
20 | }
21 | };
22 |
23 | return (
24 |
25 |
54 |
58 | New to udemy
59 |
60 |
61 | Home
62 |
63 |
64 | );
65 | //{errors && This field is required }
66 | };
67 |
68 | export default Login;
69 |
--------------------------------------------------------------------------------
/Frontend/src/features/Auth/PersistLogin.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { Navigate, Outlet, useLocation } from "react-router-dom";
3 | import { useEffect, useRef, useState } from "react";
4 | import Spinner from "../../components/Spinner";
5 | import { useRefreshMutation } from "../../reducers/api/authApi";
6 |
7 | const PersistLogin = () => {
8 | const location = useLocation();
9 | const token = useSelector((state) => state.auth.token);
10 | const effectRan = useRef(false);
11 | const [success, setSuccess] = useState(false);
12 | const [refresh, { isLoading, isError, isSuccess, isUninitialized }] =
13 | useRefreshMutation();
14 |
15 | useEffect(() => {
16 | if (effectRan.current === true) {
17 | const verifyRefreshToken = async () => {
18 | try {
19 | await refresh();
20 | setSuccess(true);
21 | } catch (err) {
22 | console.error(err);
23 | }
24 | };
25 |
26 | if (!token) verifyRefreshToken();
27 | }
28 |
29 | return () => (effectRan.current = true);
30 |
31 | // eslint-disable-next-line
32 | }, []);
33 | if (isLoading) {
34 | return ;
35 | }
36 | if (isError) {
37 | if (location.pathname == "/") {
38 | return ;
39 | }
40 | return ;
41 | }
42 | if (success && isSuccess) {
43 | return ;
44 | }
45 | if (token && isUninitialized) {
46 | return ;
47 | }
48 | };
49 |
50 | export default PersistLogin;
51 |
--------------------------------------------------------------------------------
/Frontend/src/features/Auth/RequireAuth.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { useLocation, Navigate, Outlet } from "react-router-dom";
3 | import useAuth from "../../hooks/useAuth";
4 |
5 | const RequireAuth = ({ allowedRoles }) => {
6 | const location = useLocation();
7 | const { roles } = useAuth();
8 |
9 | if (roles.some((role) => allowedRoles.includes(role))) {
10 | return ;
11 | }
12 | return ;
13 | };
14 | export default RequireAuth;
15 |
--------------------------------------------------------------------------------
/Frontend/src/features/Auth/Signup/index.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { useForm } from "react-hook-form";
3 | import { useSignupMutation } from "../../../reducers/api/authApi";
4 | import { Link, useNavigate } from "react-router-dom";
5 |
6 | const Signup = ({ roles }) => {
7 | const navigate = useNavigate();
8 | const [signup] = useSignupMutation();
9 | const { register, handleSubmit } = useForm();
10 |
11 | const onSubmit = async (data) => {
12 | const result = await signup({ roles, ...data }).unwrap();
13 |
14 | if (result.success) {
15 | navigate("/join/login", { replace: true });
16 | }
17 | };
18 |
19 | return (
20 |
21 |
60 |
64 | Already have account
65 |
66 |
67 | );
68 | //{errors && This field is required }
69 | };
70 |
71 | export default Signup;
72 |
--------------------------------------------------------------------------------
/Frontend/src/features/Common/Profile/BasicInformation.jsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import {
3 | useEditProfileMutation,
4 | useGetUserDetailsQuery,
5 | } from "../../../reducers/api/userApi";
6 |
7 | const BasicInformation = () => {
8 | const { data } = useGetUserDetailsQuery();
9 | const [editProfile] = useEditProfileMutation();
10 | const { register, handleSubmit, getValues } = useForm({
11 | defaultValues: {
12 | firstName: data?.firstName,
13 | lastName: data?.lastName,
14 | headline: data?.headline,
15 | biography: data?.biography,
16 | },
17 | });
18 | const onSubmit = async () => {
19 | const firstName = getValues("firstName");
20 | const lastName = getValues("lastName");
21 | const headline = getValues("headline");
22 | const biography = getValues("biography");
23 |
24 | await editProfile({
25 | firstName,
26 | lastName,
27 | headline,
28 | biography,
29 | });
30 | };
31 | return (
32 |
88 | );
89 | };
90 |
91 | export default BasicInformation;
92 |
--------------------------------------------------------------------------------
/Frontend/src/features/Common/Profile/ProfileLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Link, Outlet } from "react-router-dom";
2 | import Spinner from "../../../components/Spinner";
3 | import { useGetUserDetailsQuery } from "../../../reducers/api/userApi";
4 | import useAuth from "../../../hooks/useAuth";
5 |
6 | const ProfileLayout = () => {
7 | const { isLoading } = useGetUserDetailsQuery();
8 | const { isInstructor } = useAuth();
9 | if (isLoading) return ;
10 | return (
11 |
12 |
Profile & Settings
13 |
14 |
15 |
20 | Udemy Profile
21 |
22 |
23 |
24 |
25 | Profile Picture
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default ProfileLayout;
35 |
--------------------------------------------------------------------------------
/Frontend/src/features/Common/Profile/ProfilePhoto.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useForm } from "react-hook-form";
3 | import {
4 | useEditProfileMutation,
5 | useGetUserDetailsQuery,
6 | } from "../../../reducers/api/userApi";
7 |
8 | const ProfilePhoto = () => {
9 | const [inputImage, setInputImage] = useState(false);
10 | const { data } = useGetUserDetailsQuery();
11 | const [editProfile, { isLoading }] = useEditProfileMutation();
12 | const { register, handleSubmit, getValues, watch } = useForm();
13 | const pictureWatch = watch("profilePicture");
14 | useEffect(() => {
15 | setInputImage(getValues("profilePicture")[0]);
16 | // eslint-disable-next-line react-hooks/exhaustive-deps
17 | }, [pictureWatch]);
18 |
19 | const onSubmit = async () => {
20 | const profilePicture = getValues("profilePicture")[0];
21 | await editProfile({
22 | profilePicture,
23 | });
24 | };
25 |
26 | return (
27 |
49 | );
50 | };
51 |
52 | export default ProfilePhoto;
53 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Course/Checkout.jsx:
--------------------------------------------------------------------------------
1 | import { loadStripe } from "@stripe/stripe-js";
2 | import { useParams } from "react-router-dom";
3 | import { useCreateSessionMutation } from "../../../reducers/api/paymentApi";
4 |
5 | export default function Checkout() {
6 | const { courseId } = useParams();
7 | const [createSession] = useCreateSessionMutation();
8 |
9 | const handleSubmit = async (e) => {
10 | e.preventDefault();
11 | try {
12 | const stripe = await loadStripe(import.meta.env.VITE_STRIPE_PROMISE);
13 |
14 | const response = await createSession({ courseId }).unwrap();
15 | if (!response.success) {
16 | return;
17 | }
18 | const result = await stripe.redirectToCheckout({
19 | sessionId: response.id,
20 | });
21 | if (result.error) {
22 | console.log(result.error);
23 | }
24 | } catch (error) {
25 | console.log(error);
26 | }
27 | };
28 |
29 | return (
30 |
34 | Buy Now
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Course/ClaimCertificate.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Page,
3 | Text,
4 | View,
5 | Document,
6 | StyleSheet,
7 | Image,
8 | PDFDownloadLink,
9 | } from "@react-pdf/renderer";
10 | import Logo from "../../../assets/images/ulogo.png";
11 |
12 | // Create styles
13 | const styles = StyleSheet.create({
14 | page: {
15 | flexDirection: "row",
16 | backgroundColor: "#E4E4E4",
17 | },
18 | text: {
19 | padding: "20px",
20 | fontSize: "20px",
21 | },
22 | title: {
23 | padding: "20px",
24 | fontSize: "50px",
25 | fontWeight: "bold",
26 | },
27 | name: {
28 | marginTop: "100px",
29 | padding: "20px",
30 | fontSize: "40px",
31 | fontWeight: "bold",
32 | },
33 | });
34 |
35 | // Create Document Component
36 | // eslint-disable-next-line react/prop-types
37 | function PDF({ userName, courseName }) {
38 | return (
39 |
40 |
41 |
42 |
43 | Fake Certificate of Completion
44 |
45 |
46 | {courseName}
47 | {userName}
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | // eslint-disable-next-line react/prop-types
55 | export default function ClaimCertificate({ courseName, userName }) {
56 | return (
57 |
58 |
}
60 | fileName="certificate.pdf"
61 | style={{ width: "full" }}
62 | className="bg-purple-700 text-white font-bold p-2 text-center mt-4 mb-2 text-lg block"
63 | >
64 | {/*eslint-disable-next-line no-unused-vars*/}
65 | {({ blob, url, loading, error }) =>
66 | loading ? "Loading Certificate..." : "Claim Certificate!"
67 | }
68 |
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Course/CourseProgress.jsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "react-router-dom";
2 | import Spinner from "../../../components/Spinner";
3 | import ClaimCertificate from "./ClaimCertificate";
4 | import { useGetFullCourseDetailsQuery } from "../../../reducers/api/courseApi";
5 | import { useGetUserProgressQuery } from "../../../reducers/api/courseProgressApi";
6 |
7 | const CourseProgress = () => {
8 | const { courseId } = useParams();
9 | const { data, isLoading } = useGetFullCourseDetailsQuery(courseId);
10 | const { data: courseProgress } = useGetUserProgressQuery(courseId);
11 | if (isLoading) {
12 | return ;
13 | }
14 | let totalContent = 0;
15 | let userContent = 0;
16 | data?.courseDetails.courseContent.forEach((section) => {
17 | section?.subSection?.forEach((subsection) => {
18 | if (
19 | courseProgress?.userProgress?.completedVideos.includes(subsection?._id)
20 | ) {
21 | userContent++;
22 | }
23 | totalContent++;
24 | });
25 | });
26 |
27 | return (
28 |
29 |
Course Progress
30 |
31 |
32 |
33 | {Math.round((userContent / totalContent) * 100)} %
34 |
35 |
36 | {/* If Progress is 100% Then Claim Certificate */}
37 | {userContent / totalContent == 1 && (
38 |
46 | )}
47 |
48 | );
49 | };
50 |
51 | export default CourseProgress;
52 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Course/InstructorInfo.jsx:
--------------------------------------------------------------------------------
1 | import { useGetFullCourseDetailsQuery } from "../../../reducers/api/courseApi";
2 | import { useParams } from "react-router-dom";
3 | import { IoIosStar, IoMdPlayCircle } from "react-icons/io";
4 | import { SlBadge } from "react-icons/sl";
5 | import { MdPeople } from "react-icons/md";
6 | import BlankProfile from "../../../assets/images/blankProfile.webp";
7 | import { CourseInfo } from "../../../utils/CourseInfo";
8 | import Spinner from "../../../components/Spinner";
9 |
10 | const InstructorInfo = () => {
11 | const { courseId } = useParams();
12 | const { data, isLoading } = useGetFullCourseDetailsQuery(courseId);
13 | if (isLoading) {
14 | return ;
15 | }
16 | const info = data?.courseDetails.instructor;
17 | const { studentsEnrolled, ratingAvg, ratingLength } = CourseInfo(
18 | info?.courses
19 | );
20 |
21 | return (
22 |
23 |
Instructor
24 |
25 | {info?.firstName + " " + info?.lastName}
26 |
27 |
{info?.headline}
28 |
29 |
34 |
35 |
36 | {ratingAvg} Instructor Rating
37 |
38 |
39 | {ratingLength} Review
40 |
41 |
42 | {studentsEnrolled} Students
43 |
44 |
45 | {info?.courses.length} Courses
46 |
47 |
48 |
49 |
{info?.biography}
50 |
51 | );
52 | };
53 |
54 | export default InstructorInfo;
55 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Course/RatingAndReview.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { Rating } from "@smastrom/react-rating";
3 | import { useState } from "react";
4 | import { useParams } from "react-router-dom";
5 | import {
6 | useCreateRatingMutation,
7 | useEditRatingMutation,
8 | } from "../../../reducers/api/ratingAndReview";
9 | import { useGetFullCourseDetailsQuery } from "../../../reducers/api/courseApi";
10 | import Spinner from "../../../components/Spinner";
11 |
12 | import useAuth from "../../../hooks/useAuth";
13 |
14 | const RatingAndReview = ({ rating = 0, review = "" }) => {
15 | const { courseId } = useParams();
16 | const { data, isLoading } = useGetFullCourseDetailsQuery(courseId);
17 | const { userId } = useAuth();
18 | const [inputRating, setInputRating] = useState(rating);
19 | const [inputReview, setInputReview] = useState(review);
20 | const [isEditRating, setIsEditRating] = useState(false);
21 | const [createRating] = useCreateRatingMutation();
22 | const [editRating] = useEditRatingMutation();
23 | if (isLoading) {
24 | return ;
25 | }
26 |
27 | let userRating;
28 | if (data?.courseDetails?.studentsEnrolled?.includes(userId)) {
29 | userRating = data?.courseDetails?.ratingAndReviews?.find(
30 | (rating) => rating?.user._id == userId
31 | );
32 | }
33 |
34 | const handleRating = (e) => {
35 | setInputRating(e);
36 | };
37 |
38 | const handleReview = (e) => {
39 | setInputReview(e.target.value);
40 | };
41 |
42 | const handleSubmit = async (e) => {
43 | e.preventDefault();
44 | const rating = inputRating;
45 | const review = inputReview;
46 | if (!isEditRating) {
47 | await createRating({ courseId, rating, review });
48 | } else {
49 | await editRating({ courseId, rating, review });
50 |
51 | setIsEditRating(false);
52 | }
53 | };
54 |
55 | return (
56 |
57 |
58 | {data?.courseDetails?.studentsEnrolled?.includes(userId) ? (
59 | userRating && !isEditRating ? (
60 |
61 |
Your Review
62 |
63 |
64 |
69 |
{userRating.updatedAt.substring(0, 10)}
70 |
71 |
72 |
{userRating?.review}
73 |
{
77 | setIsEditRating(true);
78 | }}
79 | >
80 | Edit
81 |
82 |
83 |
84 |
85 | ) : (
86 |
104 | )
105 | ) : null}
106 |
107 |
108 |
Reviews
109 | {data?.courseDetails.ratingAndReviews.map((rating) => {
110 | return (
111 |
115 |
116 | {rating.user.firstName + " " + rating.user.lastName}
117 |
118 |
119 |
120 |
{rating.updatedAt?.substring(0, 10)}
121 |
122 |
{rating.review}
123 |
124 | );
125 | })}
126 |
127 |
128 | );
129 | };
130 |
131 | export default RatingAndReview;
132 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Course/VideoPlayer.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { useParams } from "react-router-dom";
3 | import { useUpdateCourseProgressMutation } from "../../../reducers/api/courseProgressApi";
4 |
5 | /* eslint-disable react/prop-types */
6 | const VideoPlayer = ({
7 | setIsVideoPlaying,
8 | section = "",
9 | currentSectionIndex,
10 | setCurrentSectionIndex,
11 | currentSubSectionIndex,
12 | setCurrentSubSectionIndex,
13 | courseContent = "",
14 | }) => {
15 | const { courseId } = useParams();
16 | const [updateCourseProgress] = useUpdateCourseProgressMutation();
17 |
18 | const handleVideoEnd = async () => {
19 | if (currentSubSectionIndex + 1 < section.subSection.length) {
20 | setCurrentSubSectionIndex(currentSubSectionIndex + 1);
21 | } else {
22 | if (currentSectionIndex + 1 < courseContent.length) {
23 | setCurrentSubSectionIndex(0);
24 | setCurrentSectionIndex(currentSectionIndex + 1);
25 | } else {
26 | setCurrentSubSectionIndex(0);
27 | setCurrentSectionIndex(0);
28 | }
29 | }
30 | await updateCourseProgress({
31 | courseId,
32 | subSectionId:
33 | courseContent[currentSectionIndex].subSection[currentSubSectionIndex]
34 | ._id,
35 | });
36 | };
37 | return (
38 | {
41 | setIsVideoPlaying(false);
42 | }}
43 | >
44 |
e.stopPropagation()}
47 | >
48 |
57 | Your Browser Does not Support Video Tag
58 |
59 |
60 | {
61 | courseContent[currentSectionIndex].subSection[
62 | currentSubSectionIndex
63 | ].subSectionName
64 | }
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default VideoPlayer;
72 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Home/CourseCard.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { CourseInfo } from "../../../utils/CourseInfo";
3 | import { Link } from "react-router-dom";
4 | import { Rating, ThinStar } from "@smastrom/react-rating";
5 | const ratingStyles = {
6 | itemShapes: ThinStar,
7 | activeFillColor: "#cfb700",
8 | inactiveFillColor: "#fbf1a9",
9 | };
10 |
11 | const CourseCard = ({ course }) => {
12 | const { ratingAvg, ratingLength } = CourseInfo([course]);
13 |
14 | return (
15 |
16 |
17 |
22 |
{course?.courseName}
23 |
24 | {course?.instructor?.firstName} {course?.instructor?.lastName}
25 |
26 |
27 |
{ratingAvg}
28 |
34 |
({ratingLength})
35 |
36 |
{course?.price > 0 ? "₹ " + course?.price : "FREE"}
37 |
38 |
39 | );
40 | };
41 |
42 | export default CourseCard;
43 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Home/Section.jsx:
--------------------------------------------------------------------------------
1 | import { useGetCategoryCoursesQuery } from "../../../reducers/api/courseApi";
2 | import Spinner from "../../../components/Spinner";
3 | import CourseCard from "./CourseCard";
4 |
5 | const Section = () => {
6 | const { data, isLoading } = useGetCategoryCoursesQuery("Development");
7 | if (isLoading) {
8 | return ;
9 | }
10 |
11 | return (
12 |
13 | {data?.map((course, index) => {
14 | return ;
15 | })}
16 |
17 | );
18 | };
19 |
20 | export default Section;
21 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import FrontImg from "../../../assets/images/front-img.jpg";
2 | import Section from "./Section";
3 |
4 | const Home = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
Learning that gets you
11 |
12 | Skills for your present (and your future). Get started with us.
13 |
14 |
15 |
16 |
17 |
What to learn next
18 |
19 | Popular Courses in Development
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Home;
31 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/SearchCourses/WideCourseCard.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | import { Link } from "react-router-dom";
4 | import { CourseInfo } from "../../../utils/CourseInfo";
5 | import { Rating } from "@smastrom/react-rating";
6 |
7 | const WideCourseCard = ({ course }) => {
8 | const { ratingAvg, ratingLength } = CourseInfo([course]);
9 |
10 | return (
11 |
15 |
16 |
17 |
18 |
{course.courseName}
19 |
{course.courseSubtitle}
20 |
21 | {course.instructor.firstName} {course.instructor.lastName}
22 |
23 |
24 |
{ratingAvg}
25 |
26 |
({ratingLength})
27 |
28 |
{course.instructionalLevel} Level
29 |
30 |
31 | {course.price == 0 ? "FREE " : "₹ " + course.price}
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default WideCourseCard;
39 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/SearchCourses/index.jsx:
--------------------------------------------------------------------------------
1 | import { useSearchCoursesQuery } from "../../../reducers/api/courseApi";
2 | import { useParams } from "react-router-dom";
3 | import Spinner from "../../../components/Spinner";
4 | import WideCourseCard from "./WideCourseCard";
5 |
6 | const SearchCourses = () => {
7 | const params = useParams();
8 | const { query } = params;
9 | const { data, isLoading } = useSearchCoursesQuery(query);
10 | if (isLoading) {
11 | return ;
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 | {data?.courses.length} results for {query}
19 |
20 |
21 |
22 | {data?.courses.map((course) => {
23 | return ;
24 | })}
25 |
26 |
27 | );
28 | };
29 |
30 | export default SearchCourses;
31 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Success/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useParams } from "react-router-dom";
2 | import { useEnrollStudentQuery } from "../../../reducers/api/paymentApi";
3 | import Spinner from "../../../components/Spinner";
4 |
5 | const Success = () => {
6 | const { sessionId } = useParams();
7 | const { data, isLoading } = useEnrollStudentQuery(sessionId);
8 | if (isLoading) {
9 | return ;
10 | }
11 |
12 | return (
13 |
14 |
15 |
16 |
17 | {data?.success
18 | ? "You Have Successfully Enrolled For This Course"
19 | : "An Error Occured"}
20 |
21 |
22 |
26 | View Purchased Courses
27 |
28 |
29 | Browse Other Courses
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Success;
39 |
--------------------------------------------------------------------------------
/Frontend/src/features/Courses/Teaching/index.jsx:
--------------------------------------------------------------------------------
1 | import TeachingHome from "../../../assets/images/teaching-front.jpg";
2 | import TeachYourWay from "../../../assets/images/teach-your-way.jpg";
3 | import InspireLearners from "../../../assets/images/inspire-learners.jpg";
4 | import GetRewarded from "../../../assets/images/get-rewarded.jpg";
5 | import useAuth from "../../../hooks/useAuth";
6 | import { useLocation, useNavigate } from "react-router-dom";
7 | import { useAddInstructorMutation } from "../../../reducers/api/authApi";
8 |
9 | const Teaching = () => {
10 | const { status } = useAuth();
11 | const location = useLocation();
12 | const navigate = useNavigate();
13 | const [addInstructor] = useAddInstructorMutation();
14 | const handleStartCLick = async () => {
15 | if (!status) {
16 | navigate("join/instructor-login", {
17 | state: { from: location },
18 | replace: true,
19 | });
20 | }
21 | if (status == "Instructor") {
22 | navigate("/instructor");
23 | }
24 | if (status == "Student") {
25 | const result = await addInstructor();
26 | if (result?.data?.success) navigate("/instructor");
27 | }
28 | };
29 |
30 | return (
31 |
32 |
33 |
38 |
39 |
Come teach with us
40 |
41 | Beacome an instructor and change lives - including your own
42 |
43 |
47 | Get Started
48 |
49 |
50 |
51 |
52 |
53 | So many reasons to start
54 |
55 |
56 |
57 |
58 |
59 |
60 |
Teach Your way
61 |
62 | Publish the course you want, in the way you want, and always have
63 | control of your own content
64 |
65 |
66 |
67 |
68 |
69 |
70 |
Inspire Learners
71 |
72 | Teach what you know and help learners explore their interest, gain
73 | new skills, and advance their careers
74 |
75 |
76 |
77 |
78 |
79 |
80 |
Get Rewarded
81 |
82 | Expand your professional network, build your expertise, and earn
83 | money on each paid enrollment
84 |
85 |
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | export default Teaching;
93 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/CourseLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet, Link, useParams } from "react-router-dom";
2 | import { FaChevronLeft } from "react-icons/fa";
3 | import Spinner from "../../../components/Spinner";
4 | import Sidebar from "./Sidebar";
5 | import {
6 | useGetCourseDetailsQuery,
7 | usePublishCourseMutation,
8 | } from "../../../reducers/api/courseApi";
9 |
10 | const CourseLayout = () => {
11 | const params = useParams();
12 | const { courseId } = params;
13 | const { data, isLoading } = useGetCourseDetailsQuery(courseId);
14 | const [publishCourse, { isLoading: isPublishLoading }] =
15 | usePublishCourseMutation();
16 | const handlePublishCourse = async () => {
17 | await publishCourse({ courseId });
18 | };
19 | if (isLoading) {
20 | return ;
21 | }
22 | let totalDuration = 0;
23 | data?.courseDetails.courseContent.forEach((section) => {
24 | section.subSection.forEach((subsection) => {
25 | console.log(subsection.timeDuration);
26 | totalDuration = totalDuration + Math.round(subsection.timeDuration);
27 | });
28 | });
29 | totalDuration = Math.ceil(totalDuration / 60);
30 |
31 | return (
32 | <>
33 |
34 |
35 |
36 |
37 |
Back To Courses
38 |
39 |
{data?.courseDetails?.courseName}
40 |
41 | {data?.courseDetails?.status}
42 |
43 |
{totalDuration} minutes Content Length
44 |
45 |
46 | {data?.courseDetails.status != "Published" && (
47 |
53 | Publish
54 |
55 | )}
56 |
57 |
58 |
64 | >
65 | );
66 | };
67 |
68 | export default CourseLayout;
69 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/CreateCourse/index.jsx:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import { useNavigate } from "react-router-dom";
3 | import { useCreateCourseMutation } from "../../../../reducers/api/courseApi";
4 |
5 | const CreateCourse = () => {
6 | const navigate = useNavigate();
7 | const { register, handleSubmit } = useForm();
8 |
9 | const [createCourse, { loading }] = useCreateCourseMutation();
10 |
11 | const onSubmit = async (data) => {
12 | try {
13 | const result = await createCourse(data).unwrap();
14 | navigate(`/instructor/course/${result.data._id}/manage/basics`);
15 |
16 | //dispatch(setCredentials({ accessToken }))
17 | //navigate('/home')
18 | } catch (err) {
19 | console.log(err);
20 | }
21 | };
22 | return (
23 |
24 | {loading &&
Loading
}
25 |
67 |
68 | );
69 | };
70 |
71 | export default CreateCourse;
72 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/Curriculum/AddContent.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { useFormContext, Controller } from "react-hook-form";
3 | import { useState } from "react";
4 | import { useAddSubSectionContentMutation } from "../../../../reducers/api/courseApi";
5 |
6 | const AddContent = ({
7 | setIsAddNewContent,
8 | videoUrl,
9 | subSectionId,
10 | videoName,
11 | date,
12 | }) => {
13 | const [isLoading, setIsloading] = useState(false);
14 | const [isReplaceContent, setIsReplaceContent] = useState(false);
15 | const [addSubSectionContent] = useAddSubSectionContentMutation();
16 | const { setValue, resetField, control, register, getValues } =
17 | useFormContext();
18 | const handleFileChange = (e) => {
19 | // When a new file is selected, update the form state
20 | setValue("file", e.target.files);
21 | };
22 |
23 | const handleUploadContent = async () => {
24 | setIsloading(true);
25 |
26 | const file = getValues("file")[0];
27 | await addSubSectionContent({
28 | subSectionId,
29 | file,
30 | });
31 |
32 | setIsAddNewContent(false);
33 | setIsReplaceContent(false);
34 | setIsloading(false);
35 | };
36 |
37 | const handleCancel = () => {
38 | setIsAddNewContent(false);
39 | setIsReplaceContent(false);
40 | resetField("file");
41 | };
42 |
43 | return (
44 |
45 | {videoUrl && !isReplaceContent ? (
46 |
47 |
48 |
49 |
50 |
51 |
52 | Name
53 |
54 | Date
55 |
56 |
57 |
58 |
59 |
60 | {videoName}
61 |
62 | {date}
63 |
64 |
65 |
66 |
{
69 | setIsReplaceContent(true);
70 | }}
71 | >
72 | Replace
73 |
74 |
75 |
76 | ) : (
77 | <>
78 |
79 | File:{" "}
80 | (
85 |
90 | )}
91 | />
92 |
93 |
94 |
95 |
100 | Cancel
101 |
102 |
108 | Submit
109 |
110 |
111 | >
112 | )}
113 |
114 | );
115 | };
116 | export default AddContent;
117 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/Curriculum/ConfirmDeleteModal.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | const ConfirmDeleteModal = ({ onCancel, onConfirm }) => {
3 | return (
4 |
8 |
e.stopPropagation()}
11 | >
12 |
Are you sure you want to delete this Section?
13 |
14 |
15 | Cancel
16 |
17 |
22 | Confirm
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default ConfirmDeleteModal;
31 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/Goals/LearningObjectives.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useFieldArray, useForm } from "react-hook-form";
3 | import { useParams } from "react-router-dom";
4 | import { MdDelete } from "react-icons/md";
5 | import {
6 | useEditCourseGoalsMutation,
7 | useGetCourseDetailsQuery,
8 | } from "../../../../reducers/api/courseApi";
9 |
10 | const LearningObjectives = () => {
11 | const params = useParams();
12 | const { courseId } = params;
13 |
14 | const { data } = useGetCourseDetailsQuery(courseId);
15 | const [editCourseGoals, { isLoading }] = useEditCourseGoalsMutation();
16 |
17 | const { register, control, setValue, handleSubmit } = useForm({
18 | defaultValues: {
19 | learningObjectives: data?.courseDetails.learningObjectives
20 | ? [...data.courseDetails.learningObjectives]
21 | : [],
22 | },
23 | });
24 |
25 | const { fields, append, remove } = useFieldArray({
26 | name: "learningObjectives",
27 | control,
28 | });
29 |
30 | useEffect(() => {
31 | data.courseDetails.learningObjectives.forEach((goal, index) => {
32 | setValue(`learningObjectives.${index}.objective`, goal.objective);
33 | });
34 | // eslint-disable-next-line react-hooks/exhaustive-deps
35 | }, []);
36 |
37 | const onSubmit = async (data) => {
38 | await editCourseGoals({ data, courseId });
39 | };
40 |
41 | return (
42 |
86 | );
87 | };
88 |
89 | export default LearningObjectives;
90 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/Goals/Prerequisites.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useFieldArray, useForm } from "react-hook-form";
3 | import { useParams } from "react-router-dom";
4 | import { MdDelete } from "react-icons/md";
5 | import {
6 | useEditCourseGoalsMutation,
7 | useGetCourseDetailsQuery,
8 | } from "../../../../reducers/api/courseApi";
9 |
10 | const Prerequisites = () => {
11 | const params = useParams();
12 | const { courseId } = params;
13 |
14 | const { data } = useGetCourseDetailsQuery(courseId);
15 | const [editCourseGoals, { isLoading }] = useEditCourseGoalsMutation();
16 | const { register, control, setValue, handleSubmit } = useForm({
17 | defaultValues: {
18 | prerequisites: data?.courseDetails.prerequisites
19 | ? [...data.courseDetails.prerequisites]
20 | : [],
21 | },
22 | });
23 | const { fields, append, remove } = useFieldArray({
24 | name: "prerequisites",
25 | control,
26 | });
27 |
28 | useEffect(() => {
29 | data.courseDetails.prerequisites.forEach((prerequisite, index) => {
30 | setValue(
31 | `prerequisites.${index}.prerequisite`,
32 | prerequisite.prerequisite
33 | );
34 | });
35 | // eslint-disable-next-line react-hooks/exhaustive-deps
36 | }, []);
37 |
38 | const onSubmit = async (data) => {
39 | await editCourseGoals({ data, courseId });
40 | };
41 | return (
42 |
89 | );
90 | };
91 | export default Prerequisites;
92 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/Goals/index.jsx:
--------------------------------------------------------------------------------
1 | import LearningObjectives from "./LearningObjectives";
2 | import Prerequisites from "./Prerequisites";
3 |
4 | const CourseGoals = () => {
5 | return (
6 |
7 |
Intended Learners
8 |
9 |
10 | The following descriptions will be publicly visible on your Course
11 | Landing Page and will have a direct impact on your course performance.
12 | These descriptions will help learners decide if your course is right
13 | for them.
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default CourseGoals;
23 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/LandingPage/CoursePromo.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { useFormContext, Controller } from "react-hook-form";
3 | import imagePlaceholder from "../../../../assets/images/imagePlaceholder.jpg";
4 | import { useState } from "react";
5 |
6 | const CoursePromo = ({ previewImage, promoVideo }) => {
7 | const [isAddImage, setIsAddImage] = useState(false);
8 | const [isAddVideo, setIsAddVideo] = useState(false);
9 | const { register, control } = useFormContext();
10 | return (
11 |
12 |
13 | Course Image
14 |
15 |
16 |
21 |
22 |
23 |
24 | Upload your course image here. It must meet our course image quality
25 | standards to be accepted. Important guidelines: 750x422 pixels;
26 | .jpg, .jpeg,. gif, or .png. no text on the image.
27 |
28 | {!isAddImage && previewImage ? (
29 |
{
32 | setIsAddImage(true);
33 | }}
34 | className="font-bold"
35 | >
36 | Change Image
37 |
38 | ) : (
39 |
(
44 |
45 | )}
46 | />
47 | )}
48 |
49 |
50 |
51 |
52 | Promotional Video
53 |
54 |
55 |
56 |
57 | Your Browser does not support video tag
58 |
59 |
60 |
61 |
62 |
63 | Your promo video is a quick and compelling way for students to
64 | preview what they will learn in your course. Students considering
65 | your course are more likely to enroll if your promo video is
66 | well-made.
67 |
68 | {!isAddVideo && promoVideo ? (
69 |
{
72 | setIsAddVideo(true);
73 | }}
74 | className="font-bold"
75 | >
76 | Change
77 |
78 | ) : (
79 |
(
84 |
85 | )}
86 | />
87 | )}
88 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default CoursePromo;
95 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/LandingPage/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useParams } from "react-router-dom";
3 | import { FormProvider, useForm } from "react-hook-form";
4 | import { useIsMount } from "./useIsMount";
5 | import CoursePromo from "./CoursePromo";
6 | import {
7 | useEditCourseBasicsMutation,
8 | useGetCourseDetailsQuery,
9 | } from "../../../../reducers/api/courseApi";
10 | import { categories } from "../../../../data/categories";
11 | import { PRICE_TIER } from "../../../../data/priceTier";
12 |
13 | const locales = ["English", "German", "French", "Spanish", "Hindi", "Marathi"];
14 | const instructionaleLevel = ["Beginner", "Medium", "Advanced", "All"];
15 |
16 | const LandingPage = () => {
17 | const isMount = useIsMount();
18 | const params = useParams();
19 | const { courseId } = params;
20 | const { data } = useGetCourseDetailsQuery(courseId);
21 | const [editCourseBasics] = useEditCourseBasicsMutation();
22 |
23 | const [subCategories, setSubCategories] = useState(
24 | data?.courseDetails?.category
25 | ? categories?.find((category) => category[data?.courseDetails?.category])[
26 | data?.courseDetails?.category
27 | ]
28 | : []
29 | );
30 | const methods = useForm({
31 | defaultValues: {
32 | courseName: data.courseDetails?.courseName,
33 | courseSubtitle: data.courseDetails?.courseSubtitle,
34 | description: data.courseDetails?.description,
35 | locale: data.courseDetails?.locale,
36 | instructionalLevel: data.courseDetails?.instructionalLevel,
37 | category: data.courseDetails?.category,
38 | subCategory: data.courseDetails?.subCategory,
39 | price: data.courseDetails?.price,
40 | previewImage: "",
41 | promoVideo: "",
42 | },
43 | });
44 | const { register, handleSubmit, formState, getValues, watch } = methods;
45 | const watchCategory = watch("category");
46 | const { isDirty } = formState;
47 | useEffect(() => {
48 | if (!isMount) {
49 | const selectedCategoryObject = categories.find(
50 | (category) => category[getValues("category")]
51 | );
52 | setSubCategories(
53 | selectedCategoryObject
54 | ? selectedCategoryObject[getValues("category")]
55 | : []
56 | );
57 | }
58 | // eslint-disable-next-line react-hooks/exhaustive-deps
59 | }, [watchCategory]);
60 |
61 | const onSubmit = async () => {
62 | //const formData = new FormData();
63 | const courseName = getValues("courseName");
64 | const courseSubtitle = getValues("courseSubtitle");
65 | const description = getValues("description");
66 | const locale = getValues("locale");
67 | const instructionalLevel = getValues("instructionalLevel");
68 | const category = getValues("category");
69 | const subCategory = getValues("subCategory");
70 | const price = getValues("price");
71 | const previewImage = getValues("previewImage")[0]
72 | ? getValues("previewImage")[0]
73 | : null;
74 | const promoVideo = getValues("promoVideo")
75 | ? getValues("promoVideo")[0]
76 | : null;
77 | await editCourseBasics({
78 | courseId,
79 | courseName,
80 | courseSubtitle,
81 | description,
82 | locale,
83 | instructionalLevel,
84 | category,
85 | subCategory,
86 | price,
87 | previewImage,
88 | promoVideo,
89 | });
90 | };
91 | return (
92 |
249 | );
250 | };
251 |
252 | export default LandingPage;
253 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/LandingPage/useIsMount.jsx:
--------------------------------------------------------------------------------
1 | // Fire useEffect expect on Initial Render
2 |
3 | import { useRef, useEffect } from "react";
4 |
5 | export const useIsMount = () => {
6 | const isMountRef = useRef(true);
7 | useEffect(() => {
8 | isMountRef.current = false;
9 | }, []);
10 | return isMountRef.current;
11 | };
12 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/Course/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import { Link, useParams } from "react-router-dom";
2 | import { DashboardLinks } from "../../../data/dashoard-links";
3 |
4 | const Sidebar = () => {
5 | const params = useParams();
6 | const { courseId } = params;
7 | return (
8 |
9 | {DashboardLinks.map((ele, i) => {
10 | return (
11 |
12 |
{ele.title}
13 |
14 | {ele.sublinks.map((subtitle, index) => {
15 | return (
16 |
17 |
20 | {subtitle.subtitle}
21 |
22 |
23 | );
24 | })}
25 |
26 |
27 | );
28 | })}
29 |
30 | );
31 | };
32 |
33 | export default Sidebar;
34 |
--------------------------------------------------------------------------------
/Frontend/src/features/Instructor/DashBoard/index.jsx:
--------------------------------------------------------------------------------
1 | import Spinner from "../../../components/Spinner";
2 | import { Link } from "react-router-dom";
3 | import BlankProfile from "../../../assets/images/blankProfile.webp";
4 | import { useGetInstructorDetailsQuery } from "../../../reducers/api/courseApi";
5 | import imagePlaceholder from "../../../assets/images/imagePlaceholder.jpg";
6 |
7 | const Dashboard = () => {
8 | const { data, isLoading } = useGetInstructorDetailsQuery();
9 |
10 | if (isLoading) {
11 | return ;
12 | }
13 | const profilePicture = data?.profilePicture;
14 | return (
15 | <>
16 |
17 |
18 |
Student
19 |
20 |
25 |
26 |
27 |
28 |
Jump into Course Creation
29 |
33 | Create Your Course
34 |
35 |
36 |
37 |
38 | {data?.courses.length > 0 && (
39 |
Your Courses
40 | )}
41 | {data?.courses?.map((course) => {
42 | return (
43 |
44 |
51 |
52 |
53 |
{course.courseName}
54 |
{course.status}
55 |
56 |
60 | Edit
61 |
62 |
63 |
64 | );
65 | })}{" "}
66 |
67 | >
68 | );
69 | };
70 |
71 | export default Dashboard;
72 |
--------------------------------------------------------------------------------
/Frontend/src/features/Student/MyCourses/CourseProgressCard.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import { Link } from "react-router-dom";
3 |
4 | const CourseProgressCard = ({ course }) => {
5 | let progressVid = 0;
6 | let total = 0;
7 | course.courseId.courseContent.forEach((section) => {
8 | section.subSection.forEach((subsection) => {
9 | if (course.completedVideos.includes(subsection._id)) {
10 | progressVid++;
11 | }
12 | total++;
13 | });
14 | });
15 | let progress = 0;
16 | if (total > 0) {
17 | progress = progressVid / total;
18 | }
19 |
20 | return (
21 |
25 |
26 | {course.courseId.courseName}
27 |
28 |
29 | {progress * 100} %
30 |
31 |
32 | );
33 | };
34 |
35 | export default CourseProgressCard;
36 |
--------------------------------------------------------------------------------
/Frontend/src/features/Student/MyCourses/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import Spinner from "../../../components/Spinner";
3 | import { useGetUserDetailsQuery } from "../../../reducers/api/userApi";
4 | import CourseProgressCard from "./CourseProgressCard";
5 |
6 | const MyCourses = () => {
7 | const { data, isLoading } = useGetUserDetailsQuery();
8 | if (isLoading) {
9 | return ;
10 | }
11 | return (
12 |
13 |
16 | {!data?.courseProgress.length ? (
17 |
21 | Browse New Courses
22 |
23 | ) : (
24 |
25 | {data?.courseProgress.map((course) => {
26 | return ;
27 | })}
28 |
29 | )}
30 |
31 | );
32 | };
33 |
34 | export default MyCourses;
35 |
--------------------------------------------------------------------------------
/Frontend/src/hooks/useAuth.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux/es/hooks/useSelector";
2 | import { selectCurrentToken } from "../reducers/authSlice";
3 | import { jwtDecode } from "jwt-decode";
4 |
5 | const useAuth = () => {
6 | const token = useSelector(selectCurrentToken);
7 | let isStudent = false;
8 | let isInstructor = false;
9 | let status = "";
10 | if (token) {
11 | const decoded = jwtDecode(token);
12 | const { userId, roles } = decoded.userInfo;
13 | isStudent = roles?.includes("Student");
14 | isInstructor = roles?.includes("Instructor");
15 | if (isStudent) status = "Student";
16 | if (isInstructor) status = "Instructor";
17 | return { userId, roles, status, isStudent, isInstructor };
18 | }
19 | return { userId: "", roles: [], isStudent, isInstructor, status };
20 | };
21 |
22 | export default useAuth;
23 |
--------------------------------------------------------------------------------
/Frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
--------------------------------------------------------------------------------
/Frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.jsx'
4 | import './index.css'
5 | import {store} from './state/store.js'
6 | import { Provider } from 'react-redux'
7 |
8 | ReactDOM.createRoot(document.getElementById('root')).render(
9 |
10 |
11 |
12 |
13 | ,
14 | )
15 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/api/authApi.js:
--------------------------------------------------------------------------------
1 | import { setCredentials, logOut } from "../authSlice";
2 | import { apiSlice } from "./baseApi";
3 | export const authApi = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | login: builder.mutation({
6 | query: (data) => ({
7 | url: "auth/login",
8 | method: "POST",
9 | body: data,
10 | }),
11 | }),
12 | signup: builder.mutation({
13 | query: (data) => ({
14 | url: "auth/signup",
15 | method: "POST",
16 | body: data,
17 | }),
18 | }),
19 | refresh: builder.mutation({
20 | query: () => ({
21 | url: "/auth/refresh",
22 | method: "GET",
23 | }),
24 | async onQueryStarted(arg, { dispatch, queryFulfilled }) {
25 | try {
26 | const { data } = await queryFulfilled;
27 | const { accessToken } = data;
28 | dispatch(setCredentials(accessToken));
29 | } catch (err) {
30 | console.log(err);
31 | }
32 | },
33 | }),
34 | logout: builder.mutation({
35 | query: () => ({
36 | url: "/auth/logout",
37 | method: "POST",
38 | }),
39 | async onQueryStarted(arg, { dispatch, queryFulfilled }) {
40 | try {
41 | await queryFulfilled;
42 | dispatch(logOut());
43 | setTimeout(() => {
44 | dispatch(apiSlice.util.resetApiState());
45 | }, 1000);
46 | } catch (error) {
47 | console.log(error);
48 | }
49 | },
50 | }),
51 | addInstructor: builder.mutation({
52 | query: () => ({
53 | url: "/auth/addInstructor",
54 | method: "GET",
55 | }),
56 | async onQueryStarted(arg, { dispatch, queryFulfilled }) {
57 | try {
58 | const { data } = await queryFulfilled;
59 | const { accessToken } = data;
60 | dispatch(setCredentials(accessToken));
61 | } catch (err) {
62 | console.log(err);
63 | }
64 | },
65 | }),
66 | }),
67 | });
68 |
69 | export const {
70 | useLoginMutation,
71 | useSignupMutation,
72 | useRefreshMutation,
73 | useLogoutMutation,
74 | useAddInstructorMutation,
75 | } = authApi;
76 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/api/baseApi.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
2 | import { setCredentials } from "../authSlice";
3 |
4 | const baseQuery = fetchBaseQuery({
5 | baseUrl: "http://localhost:4000/api/v1",
6 | credentials: "include",
7 | prepareHeaders: (headers, { getState }) => {
8 | const token = getState().auth.token;
9 |
10 | if (token) {
11 | headers.set("authorization", `Bearer ${token}`);
12 | }
13 | return headers;
14 | },
15 | });
16 |
17 | const baseQueryWithReauth = async (args, api, extraOptions) => {
18 | let result = await baseQuery(args, api, extraOptions);
19 |
20 | if (result?.error?.status === 403) {
21 | const refreshResult = await baseQuery("/auth/refresh", api, extraOptions);
22 |
23 | if (refreshResult?.data) {
24 | api.dispatch(setCredentials({ ...refreshResult.data }));
25 |
26 | result = await baseQuery(args, api, extraOptions);
27 | } else {
28 | if (refreshResult?.error?.status === 403) {
29 | refreshResult.error.data.message = "Your login has expired.";
30 | }
31 | return refreshResult;
32 | }
33 | }
34 |
35 | return result;
36 | };
37 |
38 | export const apiSlice = createApi({
39 | baseQuery: baseQueryWithReauth,
40 | tagTypes: ["Course", "User", "Courses", "CourseProgress"],
41 | // eslint-disable-next-line no-unused-vars
42 | endpoints: (builder) => ({}),
43 | });
44 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/api/courseApi.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | import { apiSlice } from "./baseApi";
4 |
5 | // I think RTK query is not working as expected with form data that's why we need this two functions
6 | const dataForm = ({ subSectionId = null, file = null }) => {
7 | const formData = new FormData();
8 | formData.append("subSectionId", subSectionId);
9 | formData.append("videoFile", file);
10 | return formData;
11 | };
12 |
13 | const basicsDataForm = ({
14 | courseId,
15 | courseName,
16 | courseSubtitle,
17 | description,
18 | locale,
19 | instructionalLevel,
20 | category,
21 | subCategory,
22 | price,
23 | previewImage,
24 | promoVideo,
25 | }) => {
26 | const formData = new FormData();
27 | formData.append("courseId", courseId);
28 | formData.append("courseName", courseName);
29 | formData.append("courseSubtitle", courseSubtitle);
30 | formData.append("description", description);
31 | formData.append("locale", locale);
32 | formData.append("instructionalLevel", instructionalLevel);
33 | formData.append("category", category);
34 | formData.append("subCategory", subCategory);
35 | formData.append("price", price);
36 | formData.append("previewImage", previewImage);
37 | formData.append("promoVideo", promoVideo);
38 | return formData;
39 | };
40 |
41 | export const courseApi = apiSlice.injectEndpoints({
42 | endpoints: (builder) => ({
43 | createCourse: builder.mutation({
44 | query: (data) => ({
45 | url: "course/createcourse",
46 | method: "POST",
47 | body: data,
48 | }),
49 | invalidatesTags: ["Course"],
50 | }),
51 | getAllCourses: builder.query({
52 | query: () => ({
53 | url: "course/getAllCourses",
54 | }),
55 | transformResponse: (response, meta, arg) => response.data,
56 | providesTags: ["Course"],
57 | }),
58 | getCategoryCourses: builder.query({
59 | query: (category) => ({
60 | url: `course/getCategoryCourses/${category}`,
61 | }),
62 | transformResponse: (response, meta, arg) => response.data,
63 | providesTags: ["Course"],
64 | }),
65 | searchCourses: builder.query({
66 | query: (query) => ({
67 | url: `course/search/${query}`,
68 | }),
69 | providesTags: ["Course"],
70 | }),
71 | getInstructorDetails: builder.query({
72 | query: () => ({
73 | url: "instructor",
74 | }),
75 | transformResponse: (response) => response.data,
76 | providesTags: ["Course"],
77 | }),
78 | getCourseDetails: builder.query({
79 | query: (courseId) => ({
80 | url: `course/getCourseDetails/${courseId}`,
81 | }),
82 | transformResponse: (response, meta, arg) => response.data,
83 | providesTags: ["Course"],
84 | }),
85 | getFullCourseDetails: builder.query({
86 | query: (courseId) => ({
87 | url: `course/getFullCourseDetails/${courseId}`,
88 | }),
89 | transformResponse: (response, meta, arg) => response.data,
90 | providesTags: ["Course"],
91 | }),
92 |
93 | getsections: builder.query({
94 | query: () => ({
95 | url: "course/getsections",
96 | }),
97 | providesTags: ["Course"],
98 | transformResponse: (response, meta, arg) => response.data,
99 | }),
100 | editCourseGoals: builder.mutation({
101 | query: (data) => ({
102 | url: "course/editCourseGoals",
103 | method: "POST",
104 | body: data,
105 | }),
106 | invalidatesTags: ["Course"],
107 | }),
108 | editCourseBasics: builder.mutation({
109 | query: (data) => ({
110 | url: "course/editCourseBasics",
111 | method: "POST",
112 | body: basicsDataForm(data),
113 | }),
114 | invalidatesTags: ["Course"],
115 | }),
116 |
117 | createSection: builder.mutation({
118 | query: (data) => ({
119 | url: "course/createSection",
120 | method: "POST",
121 | body: data,
122 | }),
123 | invalidatesTags: ["Course"],
124 | }),
125 |
126 | updateSection: builder.mutation({
127 | query: (data) => ({
128 | url: "course/updateSection",
129 | method: "POST",
130 | body: data,
131 | }),
132 | invalidatesTags: ["Course"],
133 | }),
134 |
135 | deleteSection: builder.mutation({
136 | query: (data) => ({
137 | url: "course/deleteSection",
138 | method: "POST",
139 | body: data,
140 | }),
141 | invalidatesTags: ["Course"],
142 | }),
143 |
144 | createSubSection: builder.mutation({
145 | query: (data) => ({
146 | url: "course/createSubSection",
147 | method: "POST",
148 | body: data,
149 | }),
150 | invalidatesTags: ["Course"],
151 | }),
152 | addSubSectionContent: builder.mutation({
153 | query: (data) => ({
154 | url: "course/addSubSectionContent",
155 | method: "POST",
156 | body: dataForm({
157 | file: data.file,
158 | subSectionId: data.subSectionId,
159 | }),
160 | }),
161 | invalidatesTags: ["Course"],
162 | }),
163 | updateSubSection: builder.mutation({
164 | query: (data) => ({
165 | url: "course/updateSubSection",
166 | method: "POST",
167 | body: data,
168 | }),
169 | invalidatesTags: ["Course"],
170 | }),
171 | deleteSubSection: builder.mutation({
172 | query: (data) => ({
173 | url: "course/deleteSubSection",
174 | method: "POST",
175 | body: data,
176 | }),
177 | invalidatesTags: ["Course"],
178 | }),
179 | publishCourse: builder.mutation({
180 | query: (data) => ({
181 | url: "course/publishCourse",
182 | method: "POST",
183 | body: data,
184 | }),
185 | invalidatesTags: ["Course"],
186 | }),
187 | }),
188 | });
189 |
190 | export const {
191 | useCreateCourseMutation,
192 | useGetAllCoursesQuery,
193 | useGetCourseDetailsQuery,
194 | useSearchCoursesQuery,
195 | useGetFullCourseDetailsQuery,
196 | useGetCategoryCoursesQuery,
197 | useGetInstructorDetailsQuery,
198 | useGetsectionsQuery,
199 | useEditCourseGoalsMutation,
200 | useEditCourseBasicsMutation,
201 | useCreateSectionMutation,
202 | useUpdateSectionMutation,
203 | useDeleteSectionMutation,
204 | useCreateSubSectionMutation,
205 | useAddSubSectionContentMutation,
206 | useUpdateSubSectionMutation,
207 | useDeleteSubSectionMutation,
208 | usePublishCourseMutation,
209 | } = courseApi;
210 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/api/courseProgressApi.js:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "./baseApi";
2 |
3 | export const courseProgressApi = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | getUserProgress: builder.query({
6 | query: (courseId) => ({
7 | url: `courseProgress/getUserCourseProgress/${courseId}`,
8 | }),
9 | providesTags: ["CourseProgress"],
10 | }),
11 | updateCourseProgress: builder.mutation({
12 | query: (data) => ({
13 | url: "courseProgress/updateCourseProgress",
14 | method: "POST",
15 | body: data,
16 | }),
17 | invalidatesTags: ["CourseProgress"],
18 | }),
19 | }),
20 | });
21 |
22 | export const { useGetUserProgressQuery, useUpdateCourseProgressMutation } =
23 | courseProgressApi;
24 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/api/paymentApi.js:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "./baseApi";
2 |
3 | export const paymentApi = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | createSession: builder.mutation({
6 | query: (data) => ({
7 | url: "payment/createSession",
8 | method: "POST",
9 | body: data,
10 | }),
11 | }),
12 | enrollStudent: builder.query({
13 | query: (sessionId) => ({
14 | url: `payment/completePayment/${sessionId}`,
15 | method: "GET",
16 | }),
17 | }),
18 | }),
19 | });
20 |
21 | export const { useCreateSessionMutation, useEnrollStudentQuery } = paymentApi;
22 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/api/ratingAndReview.js:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "./baseApi";
2 |
3 | export const ratingAndReviewApi = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | createRating: builder.mutation({
6 | query: (data) => ({
7 | url: "ratingAndReview/create",
8 | method: "POST",
9 | body: data,
10 | }),
11 | invalidatesTags: ["Course"],
12 | }),
13 | editRating: builder.mutation({
14 | query: (data) => ({
15 | url: "ratingAndReview/edit",
16 | method: "POST",
17 | body: data,
18 | }),
19 | invalidatesTags: ["Course"],
20 | }),
21 | }),
22 | });
23 |
24 | export const { useCreateRatingMutation, useEditRatingMutation } =
25 | ratingAndReviewApi;
26 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/api/userApi.js:
--------------------------------------------------------------------------------
1 | import { setCredentials } from "../authSlice";
2 | import { apiSlice } from "./baseApi";
3 |
4 | const profileData = ({
5 | firstName = "",
6 | lastName = "",
7 | headline = "",
8 | biography = "",
9 | profilePicture = "",
10 | }) => {
11 | const formData = new FormData();
12 | formData.append("firstName", firstName);
13 | formData.append("lastName", lastName);
14 | formData.append("headline", headline);
15 | formData.append("biography", biography);
16 | formData.append("profilePicture", profilePicture);
17 | return formData;
18 | };
19 | export const instructorApi = apiSlice.injectEndpoints({
20 | endpoints: (builder) => ({
21 | editProfile: builder.mutation({
22 | query: (data) => ({
23 | url: "profile/edit",
24 | method: "POST",
25 | body: profileData(data),
26 | }),
27 | invalidatesTags: ["User"],
28 | }),
29 |
30 | getUserDetails: builder.query({
31 | query: () => ({
32 | url: "user/getUserDetails",
33 | method: "GET",
34 | }),
35 | transformResponse: (response) => response.data,
36 | providesTags: ["User"],
37 | }),
38 | refresh: builder.mutation({
39 | query: () => ({
40 | url: "/auth/refresh",
41 | method: "GET",
42 | }),
43 | async onQueryStarted(arg, { dispatch, queryFulfilled }) {
44 | try {
45 | const { data } = await queryFulfilled;
46 | const { accessToken } = data;
47 | dispatch(setCredentials(accessToken));
48 | } catch (err) {
49 | console.log(err);
50 | }
51 | },
52 | }),
53 | }),
54 | });
55 |
56 | export const { useEditProfileMutation, useGetUserDetailsQuery } = instructorApi;
57 |
--------------------------------------------------------------------------------
/Frontend/src/reducers/authSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const authSlice = createSlice({
4 | name: "auth",
5 | initialState: { token: null },
6 | reducers: {
7 | setCredentials: (state, action) => {
8 | state.token = action.payload;
9 | },
10 | // eslint-disable-next-line no-unused-vars
11 | logOut: (state, action) => {
12 | state.token = null;
13 | },
14 | },
15 | });
16 |
17 | export const { setCredentials, logOut } = authSlice.actions;
18 |
19 | export default authSlice.reducer;
20 |
21 | export const selectCurrentToken = (state) => state.auth.token;
22 |
--------------------------------------------------------------------------------
/Frontend/src/state/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { setupListeners } from "@reduxjs/toolkit/query";
3 | import { apiSlice } from "../reducers/api/baseApi";
4 | import authReducer from "../reducers/authSlice";
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | [apiSlice.reducerPath]: apiSlice.reducer,
9 | auth: authReducer,
10 | },
11 | middleware: (getDefaultMiddleware) =>
12 | getDefaultMiddleware().concat(apiSlice.middleware),
13 | });
14 |
15 | setupListeners(store.dispatch);
16 |
--------------------------------------------------------------------------------
/Frontend/src/utils/CourseInfo.js:
--------------------------------------------------------------------------------
1 | export function CourseInfo(courses) {
2 | let studentsEnrolled = 0;
3 | courses?.forEach((course) => {
4 | studentsEnrolled += course.studentsEnrolled.length;
5 | });
6 |
7 | let totalRating = 0;
8 | let ratingLength = 0;
9 | courses?.forEach((course) => {
10 | course.ratingAndReviews.forEach((review) => {
11 | totalRating += review.rating;
12 | });
13 | ratingLength += course.ratingAndReviews.length;
14 | });
15 |
16 | let ratingAvg = 0;
17 | if (ratingLength > 0) {
18 | ratingAvg = totalRating / ratingLength;
19 | }
20 |
21 | return {
22 | studentsEnrolled: studentsEnrolled,
23 | ratingLength: ratingLength,
24 | ratingAvg: ratingAvg,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/Frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | colors: {
10 | 'light-gray': '#f7f9fa',
11 | },
12 | }
13 | },
14 | plugins: [],
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/Frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Learning Management System (Udemy Clone)
3 |
4 | Full Stack Project project build with MERN Stack along with other technologies.
5 |
6 | 
7 |
8 |
9 | ## Tech Stack
10 |
11 | **Client:** React, Redux-Toolkit, RTK Query, React-Hook-Form, TailwindCSS, react-pdf
12 |
13 | **Server:** Node, Express, MongoDB, Stripe, JWT, Cloudinary
14 |
15 |
16 | ## Features
17 |
18 |
19 | - Login/Signup
20 | - Two Roles (Student, Instructor)
21 |
22 | #### For Student
23 | - Create/Edit Profile
24 | - See Published Courses
25 | - See Course Details
26 | - Purchase Course
27 | - See Course Progress
28 | - Download Certificate
29 | - Give/Edit Rating/Review to Enrolled Course
30 | - See All Enrolled Course
31 |
32 | #### For Instructor
33 | - Create Course
34 | - Create/Edit Course Details
35 | - Add Sections/SubSections to Course
36 | - Set Price, Category, Subcateogry Level, Language of Course
37 |
38 | ##### User can have both roles at a time
39 |
40 |
41 | ## Run Locally
42 |
43 | Clone the project
44 |
45 | ```bash
46 | git clone https://github.com/aditya7812/Learning-Management-System.git
47 | ```
48 |
49 | Go to the project directory
50 |
51 | ```bash
52 | cd Learning-Management-System
53 | ```
54 |
55 | Install backend dependencies
56 |
57 | ```bash
58 | cd Backend
59 | npm install
60 | ```
61 |
62 | Start the server
63 |
64 | ```bash
65 | npm run dev
66 | ```
67 |
68 | Install frontend dependencies
69 |
70 | ```bash
71 | cd Frontend
72 | npm install
73 | ```
74 |
75 | Start the server
76 |
77 | ```bash
78 | npm run dev
79 | ```
80 |
81 |
82 | ## Environment Variables
83 |
84 | To run this project, you will need to add the following environment variables to your .env file
85 | ### Frontend
86 |
87 | `VITE_STRIPE_PROMISE`
88 |
89 | ### Backend
90 |
91 | `ACCESS_TOKEN_SECRET`
92 |
93 | `REFRESH_TOKEN_SECRET`
94 |
95 | `MONGODB_URL`
96 |
97 | `CLOUDINARY_API_KEY`
98 |
99 | `CLOUDINARY_API_SECRET`
100 |
101 | `CLOUD_NAME`
102 |
103 | `FOLDER_NAME`
104 |
105 | `PROFILE_FOLDER_NAME`
106 |
107 | `STRIPE_KEY`
108 |
109 |
110 |
111 | ## Screenshots
112 |
113 | #### Home
114 |
115 |
116 | 
117 |
118 |
119 | #### Login
120 |
121 | 
122 |
123 |
124 | #### Signup
125 |
126 | 
127 |
128 |
129 | #### Course Details
130 |
131 | 
132 |
133 | 
134 |
135 |
136 | #### Purchase
137 |
138 | 
139 |
140 |
141 | #### Certificate
142 |
143 | 
144 |
145 |
146 | #### Rating and Review
147 |
148 | 
149 |
150 |
151 | #### Purchased Courses
152 |
153 | 
154 |
155 |
156 | #### Search
157 |
158 | 
159 |
160 |
161 | #### Profile
162 |
163 | 
164 |
165 |
166 | #### Instructor Dashboard
167 |
168 | 
169 |
170 |
171 | #### Course Creation
172 |
173 | 
174 |
175 | 
176 |
177 | 
178 |
179 | 
180 |
181 | 
182 |
183 | 
184 |
185 |
186 |
187 | ## Roadmap
188 |
189 | - Form validation with Zod
190 | - Display Success/Error Notification to User
191 | - Responsive Design
192 | - Different Payment Features
193 | - Instructor Earning Dashboard
194 | - Filter Courses by price/rating/category
195 |
196 | ##### If you want any other features you can use pull request
197 |
198 |
199 | ## Refernce
200 | [Dave Gray Mern Stack Course](https://github.com/gitdagray/mern_stack_course)
201 |
202 | Love Babbar Megaproject Repository
203 |
204 |
205 |
206 |
207 |
208 | ## License
209 |
210 | [MIT](https://choosealicense.com/licenses/mit/)
211 |
212 |
213 |
--------------------------------------------------------------------------------