├── .nvmrc
├── public
├── _redirects
├── robots.txt
├── logo.png
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── server
├── .gitignore
├── config
│ ├── razorpay.js
│ ├── cloudinary.js
│ └── database.js
├── routes
│ ├── Contact.js
│ ├── Payments.js
│ ├── profile.js
│ ├── user.js
│ └── Course.js
├── models
│ ├── Subsection.js
│ ├── Section.js
│ ├── Category.js
│ ├── Profile.js
│ ├── CourseProgress.js
│ ├── RatingandReview.js
│ ├── Course.js
│ ├── OTP.js
│ └── User.js
├── utils
│ ├── imageUploader.js
│ ├── secToDuration.js
│ └── mailSender.js
├── package.json
├── controllers
│ ├── ContactUs.js
│ ├── courseProgress.js
│ ├── resetPassword.js
│ ├── Category.js
│ ├── Section.js
│ └── RatingandReview.js
├── index.js
├── mail
│ └── templates
│ │ ├── emailVerificationTemplate.js
│ │ ├── passwordUpdate.js
│ │ ├── paymentSuccessEmail.js
│ │ ├── courseEnrollmentEmail.js
│ │ └── contactFormRes.js
└── middleware
│ └── auth.js
├── src
├── assets
│ ├── Images
│ │ ├── banner.mp4
│ │ ├── frame.png
│ │ ├── login.webp
│ │ ├── signup.webp
│ │ ├── Instructor.png
│ │ ├── aboutus1.webp
│ │ ├── aboutus2.webp
│ │ ├── aboutus3.webp
│ │ ├── boxoffice.png
│ │ ├── FoundingStory.png
│ │ ├── TimelineImage.png
│ │ ├── Know_your_progress.png
│ │ ├── Plan_your_lessons.png
│ │ └── Compare_with_others.png
│ ├── Logo
│ │ ├── rzp_logo.png
│ │ ├── Logo-Full-Dark.png
│ │ ├── Logo-Full-Light.png
│ │ ├── Logo-Small-Dark.png
│ │ └── Logo-Small-Light.png
│ └── TimeLineLogo
│ │ ├── Logo4.svg
│ │ ├── Logo3.svg
│ │ ├── Logo1.svg
│ │ └── Logo2.svg
├── utils
│ ├── dateFormatter.js
│ ├── constants.js
│ └── avgRating.js
├── pages
│ ├── Error.jsx
│ ├── Login.jsx
│ ├── Signup.jsx
│ ├── Dashboard.jsx
│ ├── Contact.jsx
│ ├── ViewCourse.jsx
│ ├── ForgotPassword.jsx
│ └── VerifyEmail.jsx
├── hooks
│ ├── useRouteMatch.js
│ └── useOnClickOutside.js
├── data
│ ├── navbar-links.js
│ ├── dashboard-links.js
│ └── footer-links.js
├── components
│ ├── core
│ │ ├── HomePage
│ │ │ ├── HighlightText.jsx
│ │ │ ├── Button.jsx
│ │ │ ├── InstructorSection.jsx
│ │ │ ├── CourseCard.jsx
│ │ │ ├── LearningLanguageSection.jsx
│ │ │ ├── CodeBlocks.jsx
│ │ │ ├── ExploreMore.jsx
│ │ │ └── Timeline.jsx
│ │ ├── Auth
│ │ │ ├── OpenRoute.jsx
│ │ │ ├── PrivateRoute.jsx
│ │ │ ├── Template.jsx
│ │ │ ├── ProfileDropdown.jsx
│ │ │ └── LoginForm.jsx
│ │ ├── AboutPage
│ │ │ ├── ContactFormSection.jsx
│ │ │ ├── Quote.jsx
│ │ │ ├── Stats.jsx
│ │ │ └── LearningGrid.jsx
│ │ ├── Course
│ │ │ ├── CourseSubSectionAccordion.jsx
│ │ │ ├── CourseAccordionBar.jsx
│ │ │ └── CourseDetailsCard.jsx
│ │ ├── Dashboard
│ │ │ ├── Settings
│ │ │ │ ├── index.jsx
│ │ │ │ ├── DeleteAccount.jsx
│ │ │ │ └── ChangeProfilePicture.jsx
│ │ │ ├── Cart
│ │ │ │ ├── index.jsx
│ │ │ │ ├── RenderTotalAmount.jsx
│ │ │ │ └── RenderCartCourses.jsx
│ │ │ ├── SidebarLink.jsx
│ │ │ ├── MyCourses.jsx
│ │ │ ├── AddCourse
│ │ │ │ ├── index.jsx
│ │ │ │ ├── CourseInformation
│ │ │ │ │ ├── RequirementsField.jsx
│ │ │ │ │ └── ChipInput.jsx
│ │ │ │ ├── RenderSteps.jsx
│ │ │ │ ├── PublishCourse
│ │ │ │ │ └── index.jsx
│ │ │ │ └── Upload.jsx
│ │ │ ├── EditCourse
│ │ │ │ └── index.jsx
│ │ │ ├── Sidebar.jsx
│ │ │ ├── InstructorDashboard
│ │ │ │ └── InstructorChart.jsx
│ │ │ └── EnrolledCourses.jsx
│ │ ├── ContactUsPage
│ │ │ ├── ContactForm.jsx
│ │ │ └── ContactDetails.jsx
│ │ └── Catalog
│ │ │ ├── Course_Slider.jsx
│ │ │ └── Course_Card.jsx
│ └── Common
│ │ ├── IconBtn.jsx
│ │ ├── Tab.jsx
│ │ ├── ConfirmationModal.jsx
│ │ ├── RatingStars.jsx
│ │ └── ReviewSlider.jsx
├── services
│ ├── apiConnector.js
│ ├── formatDate.js
│ ├── operations
│ │ ├── pageAndComponntDatas.js
│ │ ├── profileAPI.js
│ │ └── SettingsAPI.js
│ └── apis.js
├── slices
│ ├── profileSlice.js
│ ├── authSlice.js
│ ├── courseSlice.js
│ ├── viewCourseSlice.js
│ └── cartSlice.js
├── reducer
│ └── index.js
└── index.js
├── .prettierignore
├── .editorconfig
├── .gitignore
├── prettier.config.js
├── package.json
├── README.md
└── tailwind.config.js
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.18.0
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
2 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/src/assets/Images/banner.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/banner.mp4
--------------------------------------------------------------------------------
/src/assets/Images/frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/frame.png
--------------------------------------------------------------------------------
/src/assets/Images/login.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/login.webp
--------------------------------------------------------------------------------
/src/assets/Logo/rzp_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Logo/rzp_logo.png
--------------------------------------------------------------------------------
/src/assets/Images/signup.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/signup.webp
--------------------------------------------------------------------------------
/src/assets/Images/Instructor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/Instructor.png
--------------------------------------------------------------------------------
/src/assets/Images/aboutus1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/aboutus1.webp
--------------------------------------------------------------------------------
/src/assets/Images/aboutus2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/aboutus2.webp
--------------------------------------------------------------------------------
/src/assets/Images/aboutus3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/aboutus3.webp
--------------------------------------------------------------------------------
/src/assets/Images/boxoffice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/boxoffice.png
--------------------------------------------------------------------------------
/src/assets/Images/FoundingStory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/FoundingStory.png
--------------------------------------------------------------------------------
/src/assets/Images/TimelineImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/TimelineImage.png
--------------------------------------------------------------------------------
/src/assets/Logo/Logo-Full-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Logo/Logo-Full-Dark.png
--------------------------------------------------------------------------------
/src/assets/Logo/Logo-Full-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Logo/Logo-Full-Light.png
--------------------------------------------------------------------------------
/src/assets/Logo/Logo-Small-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Logo/Logo-Small-Dark.png
--------------------------------------------------------------------------------
/src/assets/Logo/Logo-Small-Light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Logo/Logo-Small-Light.png
--------------------------------------------------------------------------------
/src/assets/Images/Know_your_progress.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/Know_your_progress.png
--------------------------------------------------------------------------------
/src/assets/Images/Plan_your_lessons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/Plan_your_lessons.png
--------------------------------------------------------------------------------
/src/assets/Images/Compare_with_others.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/I-PranjalMishra/CodePlay-Edtech-MERN/HEAD/src/assets/Images/Compare_with_others.png
--------------------------------------------------------------------------------
/server/config/razorpay.js:
--------------------------------------------------------------------------------
1 | const Razorpay = require("razorpay");
2 |
3 | exports.instance = new Razorpay({
4 | key_id: process.env.RAZORPAY_KEY,
5 | key_secret: process.env.RAZORPAY_SECRET,
6 | });
7 |
--------------------------------------------------------------------------------
/src/utils/dateFormatter.js:
--------------------------------------------------------------------------------
1 | export const formattedDate = (date) => {
2 | return new Date(date).toLocaleDateString("en-US", {
3 | month: "long",
4 | day: "numeric",
5 | year: "numeric",
6 | })
7 | }
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .next
4 | build
5 | .contentlayer
6 | package-lock.json
7 | yarn.lock
8 | pnpm-lock.yaml
9 | .env
10 | .env.local
11 | .env.development
12 | .env.test
13 | .env.production
14 |
--------------------------------------------------------------------------------
/src/pages/Error.jsx:
--------------------------------------------------------------------------------
1 | function Error() {
2 | return (
3 |
",
13 | "",
14 | "^types$",
15 | "^@local/(.*)$",
16 | "^@/config/(.*)$",
17 | "^@/lib/(.*)$",
18 | "^@/components/(.*)$",
19 | "^@/styles/(.*)$",
20 | "^[./]",
21 | ],
22 | importOrderSeparation: false,
23 | importOrderSortSpecifiers: true,
24 | importOrderBuiltinModulesToTop: true,
25 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
26 | importOrderMergeDuplicateImports: true,
27 | importOrderCombineTypeAndValueImports: true,
28 | plugins: ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/core/AboutPage/Quote.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import HighlightText from '../HomePage/HighlightText'
3 |
4 | const Quote = () => {
5 | return (
6 |
7 | We are passionate about revolutionizing the way we learn. Our
8 | innovative platform ,{" "}
9 |
10 | {" "}
11 | expertise
12 |
13 | , and community to create an
14 |
15 | {" "}
16 | unparalleled educational
17 | experience.
18 |
19 |
20 | )
21 | }
22 |
23 | export default Quote
--------------------------------------------------------------------------------
/src/pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux"
2 | import { Outlet } from "react-router-dom"
3 |
4 | import Sidebar from "../components/core/Dashboard/Sidebar"
5 |
6 | function Dashboard() {
7 | const { loading: profileLoading } = useSelector((state) => state.profile)
8 | const { loading: authLoading } = useSelector((state) => state.auth)
9 |
10 | if (profileLoading || authLoading) {
11 | return (
12 |
15 | )
16 | }
17 |
18 | return (
19 |
27 | )
28 | }
29 |
30 | export default Dashboard
31 |
--------------------------------------------------------------------------------
/src/slices/courseSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 | const initialState = {
4 | step: 1,
5 | course: null,
6 | editCourse: false,
7 | paymentLoading: false,
8 | }
9 |
10 | const courseSlice = createSlice({
11 | name: "course",
12 | initialState,
13 | reducers: {
14 | setStep: (state, action) => {
15 | state.step = action.payload
16 | },
17 | setCourse: (state, action) => {
18 | state.course = action.payload
19 | },
20 | setEditCourse: (state, action) => {
21 | state.editCourse = action.payload
22 | },
23 | setPaymentLoading: (state, action) => {
24 | state.paymentLoading = action.payload
25 | },
26 | resetCourseState: (state) => {
27 | state.step = 1
28 | state.course = null
29 | state.editCourse = false
30 | },
31 | },
32 | })
33 |
34 | export const {
35 | setStep,
36 | setCourse,
37 | setEditCourse,
38 | setPaymentLoading,
39 | resetCourseState,
40 | } = courseSlice.actions
41 |
42 | export default courseSlice.reducer
43 |
--------------------------------------------------------------------------------
/src/assets/TimeLineLogo/Logo4.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/data/dashboard-links.js:
--------------------------------------------------------------------------------
1 | import { ACCOUNT_TYPE } from "../utils/constants"
2 |
3 | export const sidebarLinks = [
4 | {
5 | id: 1,
6 | name: "My Profile",
7 | path: "/dashboard/my-profile",
8 | icon: "VscAccount",
9 | },
10 | {
11 | id: 2,
12 | name: "Dashboard",
13 | path: "/dashboard/instructor",
14 | type: ACCOUNT_TYPE.INSTRUCTOR,
15 | icon: "VscDashboard",
16 | },
17 | {
18 | id: 3,
19 | name: "My Courses",
20 | path: "/dashboard/my-courses",
21 | type: ACCOUNT_TYPE.INSTRUCTOR,
22 | icon: "VscVm",
23 | },
24 | {
25 | id: 4,
26 | name: "Add Course",
27 | path: "/dashboard/add-course",
28 | type: ACCOUNT_TYPE.INSTRUCTOR,
29 | icon: "VscAdd",
30 | },
31 | {
32 | id: 5,
33 | name: "Enrolled Courses",
34 | path: "/dashboard/enrolled-courses",
35 | type: ACCOUNT_TYPE.STUDENT,
36 | icon: "VscMortarBoard",
37 | },
38 | {
39 | id: 7,
40 | name: "Cart",
41 | path: "/dashboard/cart",
42 | type: ACCOUNT_TYPE.STUDENT,
43 | icon: "VscArchive",
44 | },
45 | ]
46 |
--------------------------------------------------------------------------------
/server/routes/profile.js:
--------------------------------------------------------------------------------
1 | const express = require("express")
2 | const router = express.Router()
3 | const { auth, isInstructor } = require("../middleware/auth")
4 | const {
5 | deleteAccount,
6 | updateProfile,
7 | getAllUserDetails,
8 | updateDisplayPicture,
9 | getEnrolledCourses,
10 | instructorDashboard,
11 | } = require("../controllers/profile")
12 |
13 | // ********************************************************************************************************
14 | // Profile routes
15 | // ********************************************************************************************************
16 | // Delet User Account
17 | router.delete("/deleteProfile", auth, deleteAccount)
18 | router.put("/updateProfile", auth, updateProfile)
19 | router.get("/getUserDetails", auth, getAllUserDetails)
20 | // Get Enrolled Courses
21 | router.get("/getEnrolledCourses", auth, getEnrolledCourses)
22 | router.put("/updateDisplayPicture", auth, updateDisplayPicture)
23 | router.get("/instructorDashboard", auth, isInstructor, instructorDashboard)
24 |
25 | module.exports = router
26 |
--------------------------------------------------------------------------------
/src/assets/TimeLineLogo/Logo3.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/Common/ConfirmationModal.jsx:
--------------------------------------------------------------------------------
1 | import IconBtn from "./IconBtn"
2 |
3 | export default function ConfirmationModal({ modalData }) {
4 | return (
5 |
6 |
7 |
8 | {modalData?.text1}
9 |
10 |
11 | {modalData?.text2}
12 |
13 |
14 |
18 |
22 | {modalData?.btn2Text}
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/core/AboutPage/Stats.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Stats = [
4 | { count: "5K", label: "Active Students" },
5 | { count: "10+", label: "Mentors" },
6 | { count: "200+", label: "Courses" },
7 | { count: "50+", label: "Awards" },
8 | ];
9 |
10 | const StatsComponenet = () => {
11 | return (
12 |
13 | {/* Stats */}
14 |
15 |
16 | {Stats.map((data, index) => {
17 | return (
18 |
19 |
20 | {data.count}
21 |
22 |
23 | {data.label}
24 |
25 |
26 | );
27 | })}
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default StatsComponenet;
35 |
--------------------------------------------------------------------------------
/src/slices/viewCourseSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 | const initialState = {
4 | courseSectionData: [],
5 | courseEntireData: [],
6 | completedLectures: [],
7 | totalNoOfLectures: 0,
8 | }
9 |
10 | const viewCourseSlice = createSlice({
11 | name: "viewCourse",
12 | initialState,
13 | reducers: {
14 | setCourseSectionData: (state, action) => {
15 | state.courseSectionData = action.payload
16 | },
17 | setEntireCourseData: (state, action) => {
18 | state.courseEntireData = action.payload
19 | },
20 | setTotalNoOfLectures: (state, action) => {
21 | state.totalNoOfLectures = action.payload
22 | },
23 | setCompletedLectures: (state, action) => {
24 | state.completedLectures = action.payload
25 | },
26 | updateCompletedLectures: (state, action) => {
27 | state.completedLectures = [...state.completedLectures, action.payload]
28 | },
29 | },
30 | })
31 |
32 | export const {
33 | setCourseSectionData,
34 | setEntireCourseData,
35 | setTotalNoOfLectures,
36 | setCompletedLectures,
37 | updateCompletedLectures,
38 | } = viewCourseSlice.actions
39 |
40 | export default viewCourseSlice.reducer
41 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/Cart/index.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux"
2 |
3 | import RenderCartCourses from "./RenderCartCourses"
4 | import RenderTotalAmount from "./RenderTotalAmount"
5 |
6 | export default function Cart() {
7 | const { total, totalItems } = useSelector((state) => state.cart)
8 | const { paymentLoading } = useSelector((state) => state.course)
9 |
10 | if (paymentLoading)
11 | return (
12 |
15 | )
16 |
17 | return (
18 | <>
19 | Cart
20 |
21 | {totalItems} Courses in Cart
22 |
23 | {total > 0 ? (
24 |
25 |
26 |
27 |
28 | ) : (
29 |
30 | Your cart is empty
31 |
32 | )}
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/assets/TimeLineLogo/Logo1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/Cart/RenderTotalAmount.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux"
2 | import { useNavigate } from "react-router-dom"
3 |
4 | import { BuyCourse } from "../../../../services/operations/studentFeaturesAPI"
5 | import IconBtn from "../../../Common/IconBtn"
6 |
7 | export default function RenderTotalAmount() {
8 | const { total, cart } = useSelector((state) => state.cart)
9 | const { token } = useSelector((state) => state.auth)
10 | const { user } = useSelector((state) => state.profile)
11 | const navigate = useNavigate()
12 | const dispatch = useDispatch()
13 |
14 | const handleBuyCourse = () => {
15 | const courses = cart.map((course) => course._id)
16 | BuyCourse(token, courses, user, navigate, dispatch)
17 | }
18 |
19 | return (
20 |
21 |
Total:
22 |
₹ {total}
23 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/hooks/useOnClickOutside.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | // This hook detects clicks outside of the specified component and calls the provided handler function.
4 | export default function useOnClickOutside(ref, handler) {
5 | useEffect(() => {
6 | // Define the listener function to be called on click/touch events
7 | const listener = (event) => {
8 | // If the click/touch event originated inside the ref element, do nothing
9 | if (!ref.current || ref.current.contains(event.target)) {
10 | return;
11 | }
12 | // Otherwise, call the provided handler function
13 | handler(event);
14 | };
15 |
16 | // Add event listeners for mousedown and touchstart events on the document
17 | document.addEventListener("mousedown", listener);
18 | document.addEventListener("touchstart", listener);
19 |
20 | // Cleanup function to remove the event listeners when the component unmounts or when the ref/handler dependencies change
21 | return () => {
22 | document.removeEventListener("mousedown", listener);
23 | document.removeEventListener("touchstart", listener);
24 | };
25 | }, [ref, handler]); // Only run this effect when the ref or handler function changes
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/Contact.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | import Footer from "../components/Common/Footer"
4 | import ReviewSlider from "../components/Common/ReviewSlider"
5 | import ContactDetails from "../components/core/ContactUsPage/ContactDetails"
6 | import ContactForm from "../components/core/ContactUsPage/ContactForm"
7 |
8 | const Contact = () => {
9 | return (
10 |
11 |
12 | {/* Contact Details */}
13 |
14 |
15 |
16 |
17 | {/* Contact Form */}
18 |
19 |
20 |
21 |
22 |
23 | {/* Reviws from Other Learner */}
24 |
25 | Reviews from other learners
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default Contact
35 |
--------------------------------------------------------------------------------
/src/components/Common/RatingStars.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react"
2 | import {
3 | TiStarFullOutline,
4 | TiStarHalfOutline,
5 | TiStarOutline,
6 | } from "react-icons/ti"
7 |
8 | function RatingStars({ Review_Count, Star_Size }) {
9 | const [starCount, SetStarCount] = useState({
10 | full: 0,
11 | half: 0,
12 | empty: 0,
13 | })
14 |
15 | useEffect(() => {
16 | const wholeStars = Math.floor(Review_Count) || 0
17 | SetStarCount({
18 | full: wholeStars,
19 | half: Number.isInteger(Review_Count) ? 0 : 1,
20 | empty: Number.isInteger(Review_Count) ? 5 - wholeStars : 4 - wholeStars,
21 | })
22 | }, [Review_Count])
23 | return (
24 |
25 | {[...new Array(starCount.full)].map((_, i) => {
26 | return
27 | })}
28 | {[...new Array(starCount.half)].map((_, i) => {
29 | return
30 | })}
31 | {[...new Array(starCount.empty)].map((_, i) => {
32 | return
33 | })}
34 |
35 | )
36 | }
37 |
38 | export default RatingStars
39 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/SidebarLink.jsx:
--------------------------------------------------------------------------------
1 | import * as Icons from "react-icons/vsc"
2 | import { useDispatch } from "react-redux"
3 | import { NavLink, matchPath, useLocation } from "react-router-dom"
4 |
5 | import { resetCourseState } from "../../../slices/courseSlice"
6 |
7 | export default function SidebarLink({ link, iconName }) {
8 | const Icon = Icons[iconName]
9 | const location = useLocation()
10 | const dispatch = useDispatch()
11 |
12 | const matchRoute = (route) => {
13 | return matchPath({ path: route }, location.pathname)
14 | }
15 |
16 | return (
17 | dispatch(resetCourseState())}
20 | className={`relative px-8 py-2 text-sm font-medium ${
21 | matchRoute(link.path)
22 | ? "bg-yellow-800 text-yellow-50"
23 | : "bg-opacity-0 text-richblack-300"
24 | } transition-all duration-200`}
25 | >
26 |
31 |
32 | {/* Icon Goes Here */}
33 |
34 | {link.name}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/core/Catalog/Course_Slider.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react"
2 | // Import Swiper React components
3 | import { Swiper, SwiperSlide } from "swiper/react"
4 |
5 | // Import Swiper styles
6 | import "swiper/css"
7 | import "swiper/css/free-mode"
8 | import "swiper/css/pagination"
9 | // import "../../.."
10 | // Import required modules
11 | import { FreeMode, Pagination } from "swiper"
12 |
13 | // import { getAllCourses } from "../../services/operations/courseDetailsAPI"
14 | import Course_Card from "./Course_Card"
15 |
16 | function Course_Slider({ Courses }) {
17 | return (
18 | <>
19 | {Courses?.length ? (
20 |
32 | {Courses?.map((course, i) => (
33 |
34 |
35 |
36 | ))}
37 |
38 | ) : (
39 | No Course Found
40 | )}
41 | >
42 | )
43 | }
44 |
45 | export default Course_Slider
46 |
--------------------------------------------------------------------------------
/src/assets/TimeLineLogo/Logo2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/server/models/Course.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose")
2 |
3 | // Define the Courses schema
4 | const coursesSchema = new mongoose.Schema({
5 | courseName: { type: String },
6 | courseDescription: { type: String },
7 | instructor: {
8 | type: mongoose.Schema.Types.ObjectId,
9 | required: true,
10 | ref: "user",
11 | },
12 | whatYouWillLearn: {
13 | type: String,
14 | },
15 | courseContent: [
16 | {
17 | type: mongoose.Schema.Types.ObjectId,
18 | ref: "Section",
19 | },
20 | ],
21 | ratingAndReviews: [
22 | {
23 | type: mongoose.Schema.Types.ObjectId,
24 | ref: "RatingAndReview",
25 | },
26 | ],
27 | price: {
28 | type: Number,
29 | },
30 | thumbnail: {
31 | type: String,
32 | },
33 | tag: {
34 | type: [String],
35 | required: true,
36 | },
37 | category: {
38 | type: mongoose.Schema.Types.ObjectId,
39 | // required: true,
40 | ref: "Category",
41 | },
42 | studentsEnroled: [
43 | {
44 | type: mongoose.Schema.Types.ObjectId,
45 | required: true,
46 | ref: "user",
47 | },
48 | ],
49 | instructions: {
50 | type: [String],
51 | },
52 | status: {
53 | type: String,
54 | enum: ["Draft", "Published"],
55 | },
56 | createdAt: { type: Date, default: Date.now },
57 | })
58 |
59 | // Export the Courses model
60 | module.exports = mongoose.model("Course", coursesSchema)
61 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/MyCourses.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { VscAdd } from "react-icons/vsc"
3 | import { useSelector } from "react-redux"
4 | import { useNavigate } from "react-router-dom"
5 |
6 | import { fetchInstructorCourses } from "../../../services/operations/courseDetailsAPI"
7 | import IconBtn from "../../Common/IconBtn"
8 | import CoursesTable from "./InstructorCourses/CoursesTable"
9 |
10 | export default function MyCourses() {
11 | const { token } = useSelector((state) => state.auth)
12 | const navigate = useNavigate()
13 | const [courses, setCourses] = useState([])
14 |
15 | useEffect(() => {
16 | const fetchCourses = async () => {
17 | const result = await fetchInstructorCourses(token)
18 | if (result) {
19 | setCourses(result)
20 | }
21 | }
22 | fetchCourses()
23 | // eslint-disable-next-line react-hooks/exhaustive-deps
24 | }, [])
25 |
26 | return (
27 |
28 |
29 |
My Courses
30 | navigate("/dashboard/add-course")}
33 | >
34 |
35 |
36 |
37 | {courses &&
}
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
20 | CodePlay
21 |
22 |
23 |
24 |
25 |
26 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/server/models/OTP.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const mailSender = require("../utils/mailSender");
3 | const emailTemplate = require("../mail/templates/emailVerificationTemplate");
4 | const OTPSchema = new mongoose.Schema({
5 | email: {
6 | type: String,
7 | required: true,
8 | },
9 | otp: {
10 | type: String,
11 | required: true,
12 | },
13 | createdAt: {
14 | type: Date,
15 | default: Date.now,
16 | expires: 60 * 5, // The document will be automatically deleted after 5 minutes of its creation time
17 | },
18 | });
19 |
20 | // Define a function to send emails
21 | async function sendVerificationEmail(email, otp) {
22 | // Create a transporter to send emails
23 |
24 | // Define the email options
25 |
26 | // Send the email
27 | try {
28 | const mailResponse = await mailSender(
29 | email,
30 | "Verification Email",
31 | emailTemplate(otp)
32 | );
33 | console.log("Email sent successfully: ", mailResponse.response);
34 | } catch (error) {
35 | console.log("Error occurred while sending email: ", error);
36 | throw error;
37 | }
38 | }
39 |
40 | // Define a post-save hook to send email after the document has been saved
41 | OTPSchema.pre("save", async function (next) {
42 | console.log("New document saved to database");
43 |
44 | // Only send an email when a new document is created
45 | if (this.isNew) {
46 | await sendVerificationEmail(this.email, this.otp);
47 | }
48 | next();
49 | });
50 |
51 | const OTP = mongoose.model("OTP", OTPSchema);
52 |
53 | module.exports = OTP;
54 |
--------------------------------------------------------------------------------
/src/components/core/HomePage/InstructorSection.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CTAButton from "../../../components/core/HomePage/Button";
3 | import { FaArrowRight } from "react-icons/fa";
4 | import Instructor from "../../../assets/Images/Instructor.png";
5 | import HighlightText from './HighlightText';
6 |
7 | const InstructorSection = () => {
8 | return (
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 | Become an
21 |
22 |
23 |
24 |
25 | Instructors from around the world teach millions of students on
26 | CodePlay. We provide the tools and skills to teach what you
27 | love.
28 |
29 |
30 |
31 |
32 |
33 | Start Teaching Today
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default InstructorSection
--------------------------------------------------------------------------------
/src/components/core/Dashboard/AddCourse/index.jsx:
--------------------------------------------------------------------------------
1 | import RenderSteps from "./RenderSteps"
2 |
3 | export default function AddCourse() {
4 | return (
5 | <>
6 |
7 |
8 |
9 | Add Course
10 |
11 |
12 |
13 |
14 |
15 | {/* Course Upload Tips */}
16 |
17 |
⚡ Course Upload Tips
18 |
19 | Set the Course Price option or make it free.
20 | Standard size for the course thumbnail is 1024x576.
21 | Video section controls the course overview video.
22 | Course Builder is where you create & organize a course.
23 |
24 | Add Topics in the Course Builder section to create lessons,
25 | quizzes, and assignments.
26 |
27 |
28 | Information from the Additional Data section shows up on the
29 | course single page.
30 |
31 | Make Announcements to notify any important
32 | Notes to all enrolled students at once.
33 |
34 |
35 |
36 | >
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/core/ContactUsPage/ContactDetails.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import * as Icon1 from "react-icons/bi"
3 | import * as Icon3 from "react-icons/hi2"
4 | import * as Icon2 from "react-icons/io5"
5 |
6 | const contactDetails = [
7 | {
8 | icon: "HiChatBubbleLeftRight",
9 | heading: "Chat on us",
10 | description: "Our friendly team is here to help.",
11 | details: "info@codeplay.com",
12 | },
13 | {
14 | icon: "BiWorld",
15 | heading: "Visit us",
16 | description: "Come and say hello at our office HQ.",
17 | details:
18 | "CodePlay Office, Bhopal-462022",
19 | },
20 | {
21 | icon: "IoCall",
22 | heading: "Call us",
23 | description: "Mon - Fri From 8am to 5pm",
24 | details: "+123 456 7869",
25 | },
26 | ]
27 |
28 | const ContactDetails = () => {
29 | return (
30 |
31 | {contactDetails.map((ele, i) => {
32 | let Icon = Icon1[ele.icon] || Icon2[ele.icon] || Icon3[ele.icon]
33 | return (
34 |
38 |
39 |
40 |
41 | {ele?.heading}
42 |
43 |
44 |
{ele?.description}
45 |
{ele?.details}
46 |
47 | )
48 | })}
49 |
50 | )
51 | }
52 |
53 | export default ContactDetails
54 |
--------------------------------------------------------------------------------
/src/components/core/HomePage/CourseCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | // Importing React Icons
4 | import { HiUsers } from "react-icons/hi";
5 | import { ImTree } from "react-icons/im";
6 |
7 | const CourseCard = ({cardData, currentCard, setCurrentCard}) => {
8 | return (
9 | setCurrentCard(cardData?.heading)}
16 | >
17 |
18 |
23 | {cardData?.heading}
24 |
25 |
26 |
{cardData?.description}
27 |
28 |
29 |
34 | {/* Level */}
35 |
36 |
37 |
{cardData?.level}
38 |
39 |
40 | {/* Flow Chart */}
41 |
42 |
43 |
{cardData?.lessionNumber} Lesson
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default CourseCard;
51 |
--------------------------------------------------------------------------------
/server/routes/user.js:
--------------------------------------------------------------------------------
1 | // Import the required modules
2 | const express = require("express")
3 | const router = express.Router()
4 |
5 | // Import the required controllers and middleware functions
6 | const {
7 | login,
8 | signup,
9 | sendotp,
10 | changePassword,
11 | } = require("../controllers/Auth")
12 | const {
13 | resetPasswordToken,
14 | resetPassword,
15 | } = require("../controllers/resetPassword")
16 |
17 | const { auth } = require("../middleware/auth")
18 |
19 | // Routes for Login, Signup, and Authentication
20 |
21 | // ********************************************************************************************************
22 | // Authentication routes
23 | // ********************************************************************************************************
24 |
25 | // Route for user login
26 | router.post("/login", login)
27 |
28 | // Route for user signup
29 | router.post("/signup", signup)
30 |
31 | // Route for sending OTP to the user's email
32 | router.post("/sendotp", sendotp)
33 |
34 | // Route for Changing the password
35 | router.post("/changepassword", auth, changePassword)
36 |
37 | // ********************************************************************************************************
38 | // Reset Password
39 | // ********************************************************************************************************
40 |
41 | // Route for generating a reset password token
42 | router.post("/reset-password-token", resetPasswordToken)
43 |
44 | // Route for resetting user's password after verification
45 | router.post("/reset-password", resetPassword)
46 |
47 | // Export the router for use in the main application
48 | module.exports = router
49 |
--------------------------------------------------------------------------------
/server/controllers/courseProgress.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose")
2 | const Section = require("../models/Section")
3 | const SubSection = require("../models/Subsection")
4 | const CourseProgress = require("../models/CourseProgress")
5 | const Course = require("../models/Course")
6 |
7 | exports.updateCourseProgress = async (req, res) => {
8 | const { courseId, subsectionId } = req.body
9 | const userId = req.user.id
10 |
11 | try {
12 | // Check if the subsection is valid
13 | const subsection = await SubSection.findById(subsectionId)
14 | if (!subsection) {
15 | return res.status(404).json({ error: "Invalid subsection" })
16 | }
17 |
18 | // Find the course progress document for the user and course
19 | let courseProgress = await CourseProgress.findOne({
20 | courseID: courseId,
21 | userId: userId,
22 | })
23 |
24 | if (!courseProgress) {
25 | // If course progress doesn't exist, create a new one
26 | return res.status(404).json({
27 | success: false,
28 | message: "Course progress Does Not Exist",
29 | })
30 | } else {
31 | // If course progress exists, check if the subsection is already completed
32 | if (courseProgress.completedVideos.includes(subsectionId)) {
33 | return res.status(400).json({ error: "Subsection already completed" })
34 | }
35 |
36 | // Push the subsection into the completedVideos array
37 | courseProgress.completedVideos.push(subsectionId)
38 | }
39 |
40 | // Save the updated course progress
41 | await courseProgress.save()
42 |
43 | return res.status(200).json({ message: "Course progress updated" })
44 | } catch (error) {
45 | console.error(error)
46 | return res.status(500).json({ error: "Internal server error" })
47 | }
48 | }
49 |
50 |
51 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | // Importing necessary modules and packages
2 | const express = require("express");
3 | const app = express();
4 | const userRoutes = require("./routes/user");
5 | const profileRoutes = require("./routes/profile");
6 | const courseRoutes = require("./routes/Course");
7 | const paymentRoutes = require("./routes/Payments");
8 | const contactUsRoute = require("./routes/Contact");
9 | const database = require("./config/database");
10 | const cookieParser = require("cookie-parser");
11 | const cors = require("cors");
12 | const { cloudinaryConnect } = require("./config/cloudinary");
13 | const fileUpload = require("express-fileupload");
14 | const dotenv = require("dotenv");
15 |
16 | // Setting up port number
17 | const PORT = process.env.PORT || 4000;
18 |
19 | // Loading environment variables from .env file
20 | dotenv.config();
21 |
22 | // Connecting to database
23 | database.connect();
24 |
25 | // Middlewares
26 | app.use(express.json());
27 | app.use(cookieParser());
28 | app.use(
29 | cors({
30 | origin: "*",
31 | credentials: true,
32 | })
33 | );
34 | app.use(
35 | fileUpload({
36 | useTempFiles: true,
37 | tempFileDir: "/tmp/",
38 | })
39 | );
40 |
41 | // Connecting to cloudinary
42 | cloudinaryConnect();
43 |
44 | // Setting up routes
45 | app.use("/api/v1/auth", userRoutes);
46 | app.use("/api/v1/profile", profileRoutes);
47 | app.use("/api/v1/course", courseRoutes);
48 | app.use("/api/v1/payment", paymentRoutes);
49 | app.use("/api/v1/reach", contactUsRoute);
50 |
51 | // Testing the server
52 | app.get("/", (req, res) => {
53 | return res.json({
54 | success: true,
55 | message: "Your server is up and running ...",
56 | });
57 | });
58 |
59 | // Listening to the server
60 | app.listen(PORT, () => {
61 | console.log(`App is listening at ${PORT}`);
62 | });
63 |
64 | // End of code.
65 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/EditCourse/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { useDispatch, useSelector } from "react-redux"
3 | import { useParams } from "react-router-dom"
4 |
5 | import {
6 | fetchCourseDetails,
7 | getFullDetailsOfCourse,
8 | } from "../../../../services/operations/courseDetailsAPI"
9 | import { setCourse, setEditCourse } from "../../../../slices/courseSlice"
10 | import RenderSteps from "../AddCourse/RenderSteps"
11 |
12 | export default function EditCourse() {
13 | const dispatch = useDispatch()
14 | const { courseId } = useParams()
15 | const { course } = useSelector((state) => state.course)
16 | const [loading, setLoading] = useState(false)
17 | const { token } = useSelector((state) => state.auth)
18 |
19 | useEffect(() => {
20 | ;(async () => {
21 | setLoading(true)
22 | const result = await getFullDetailsOfCourse(courseId, token)
23 | if (result?.courseDetails) {
24 | dispatch(setEditCourse(true))
25 | dispatch(setCourse(result?.courseDetails))
26 | }
27 | setLoading(false)
28 | })()
29 | // eslint-disable-next-line react-hooks/exhaustive-deps
30 | }, [])
31 |
32 | if (loading) {
33 | return (
34 |
37 | )
38 | }
39 |
40 | return (
41 |
42 |
43 | Edit Course
44 |
45 |
46 | {course ? (
47 |
48 | ) : (
49 |
50 | Course not found
51 |
52 | )}
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/Settings/DeleteAccount.jsx:
--------------------------------------------------------------------------------
1 | import { FiTrash2 } from "react-icons/fi"
2 | import { useDispatch, useSelector } from "react-redux"
3 | import { useNavigate } from "react-router-dom"
4 |
5 | import { deleteProfile } from "../../../../services/operations/SettingsAPI"
6 |
7 | export default function DeleteAccount() {
8 | const { token } = useSelector((state) => state.auth)
9 | const dispatch = useDispatch()
10 | const navigate = useNavigate()
11 |
12 | async function handleDeleteAccount() {
13 | try {
14 | dispatch(deleteProfile(token, navigate))
15 | } catch (error) {
16 | console.log("ERROR MESSAGE - ", error.message)
17 | }
18 | }
19 |
20 | return (
21 | <>
22 |
23 |
24 |
25 |
26 |
27 |
28 | Delete Account
29 |
30 |
31 |
Would you like to delete account?
32 |
33 | This account may contain Paid Courses. Deleting your account is
34 | permanent and will remove all the contain associated with it.
35 |
36 |
37 |
42 | I want to delete my account.
43 |
44 |
45 |
46 | >
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/core/Auth/Template.jsx:
--------------------------------------------------------------------------------
1 | import { FcGoogle } from "react-icons/fc"
2 | import { useSelector } from "react-redux"
3 |
4 | import frameImg from "../../../assets/Images/frame.png"
5 | import LoginForm from "./LoginForm"
6 | import SignupForm from "./SignupForm"
7 |
8 | function Template({ title, description1, description2, image, formType }) {
9 | const { loading } = useSelector((state) => state.auth)
10 |
11 | return (
12 |
13 | {loading ? (
14 |
15 | ) : (
16 |
17 |
18 |
19 | {title}
20 |
21 |
22 | {description1} {" "}
23 |
24 | {description2}
25 |
26 |
27 | {formType === "signup" ?
:
}
28 |
29 |
30 |
37 |
45 |
46 |
47 | )}
48 |
49 | )
50 | }
51 |
52 | export default Template
53 |
--------------------------------------------------------------------------------
/src/components/core/HomePage/LearningLanguageSection.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import HighlightText from './HighlightText'
3 | import CTAButton from "../../../components/core/HomePage/Button";
4 | import Know_your_progress from "../../../assets/Images/Know_your_progress.png";
5 | import Compare_with_others from "../../../assets/Images/Compare_with_others.svg";
6 | import Plan_your_lessons from "../../../assets/Images/Plan_your_lessons.svg";
7 |
8 | const LearningLanguageSection = () => {
9 | return (
10 |
11 |
12 | Your swiss knife for
13 |
14 |
15 | Using spin making learning multiple languages easy. with 20+
16 | languages realistic voice-over, progress tracking, custom schedule
17 | and more.
18 |
19 |
36 |
37 |
38 |
39 |
40 | Learn More
41 |
42 |
43 |
44 | )
45 | }
46 |
47 | export default LearningLanguageSection
--------------------------------------------------------------------------------
/src/pages/ViewCourse.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { useDispatch, useSelector } from "react-redux"
3 | import { Outlet, useParams } from "react-router-dom"
4 |
5 | import CourseReviewModal from "../components/core/ViewCourse/CourseReviewModal"
6 | import VideoDetailsSidebar from "../components/core/ViewCourse/VideoDetailsSidebar"
7 | import { getFullDetailsOfCourse } from "../services/operations/courseDetailsAPI"
8 | import {
9 | setCompletedLectures,
10 | setCourseSectionData,
11 | setEntireCourseData,
12 | setTotalNoOfLectures,
13 | } from "../slices/viewCourseSlice"
14 |
15 | export default function ViewCourse() {
16 | const { courseId } = useParams()
17 | const { token } = useSelector((state) => state.auth)
18 | const dispatch = useDispatch()
19 | const [reviewModal, setReviewModal] = useState(false)
20 |
21 | useEffect(() => {
22 | ;(async () => {
23 | const courseData = await getFullDetailsOfCourse(courseId, token)
24 | // console.log("Course Data here... ", courseData.courseDetails)
25 | dispatch(setCourseSectionData(courseData.courseDetails.courseContent))
26 | dispatch(setEntireCourseData(courseData.courseDetails))
27 | dispatch(setCompletedLectures(courseData.completedVideos))
28 | let lectures = 0
29 | courseData?.courseDetails?.courseContent?.forEach((sec) => {
30 | lectures += sec.subSection.length
31 | })
32 | dispatch(setTotalNoOfLectures(lectures))
33 | })()
34 | // eslint-disable-next-line react-hooks/exhaustive-deps
35 | }, [])
36 |
37 | return (
38 | <>
39 |
47 | {reviewModal && }
48 | >
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/core/Catalog/Course_Card.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react"
2 | // Icons
3 | import { FaRegStar, FaStar } from "react-icons/fa"
4 | import ReactStars from "react-rating-stars-component"
5 | import { Link } from "react-router-dom"
6 |
7 | import GetAvgRating from "../../../utils/avgRating"
8 | import RatingStars from "../../Common/RatingStars"
9 |
10 | function Course_Card({ course, Height }) {
11 | // const avgReviewCount = GetAvgRating(course.ratingAndReviews)
12 | // console.log(course.ratingAndReviews)
13 | const [avgReviewCount, setAvgReviewCount] = useState(0)
14 | useEffect(() => {
15 | const count = GetAvgRating(course.ratingAndReviews)
16 | setAvgReviewCount(count)
17 | }, [course])
18 | // console.log("count............", avgReviewCount)
19 |
20 | return (
21 | <>
22 |
23 |
24 |
25 |
30 |
31 |
32 |
{course?.courseName}
33 |
34 | {course?.instructor?.firstName} {course?.instructor?.lastName}
35 |
36 |
37 | {avgReviewCount || 0}
38 |
39 |
40 |
41 | {course?.ratingAndReviews?.length} Ratings
42 |
43 |
44 |
Rs. {course?.price}
45 |
46 |
47 |
48 | >
49 | )
50 | }
51 |
52 | export default Course_Card
53 |
--------------------------------------------------------------------------------
/server/models/User.js:
--------------------------------------------------------------------------------
1 | // Import the Mongoose library
2 | const mongoose = require("mongoose")
3 |
4 | // Define the user schema using the Mongoose Schema constructor
5 | const userSchema = new mongoose.Schema(
6 | {
7 | // Define the name field with type String, required, and trimmed
8 | firstName: {
9 | type: String,
10 | required: true,
11 | trim: true,
12 | },
13 | lastName: {
14 | type: String,
15 | required: true,
16 | trim: true,
17 | },
18 | // Define the email field with type String, required, and trimmed
19 | email: {
20 | type: String,
21 | required: true,
22 | trim: true,
23 | },
24 |
25 | // Define the password field with type String and required
26 | password: {
27 | type: String,
28 | required: true,
29 | },
30 | // Define the role field with type String and enum values of "Admin", "Student", or "Visitor"
31 | accountType: {
32 | type: String,
33 | enum: ["Admin", "Student", "Instructor"],
34 | required: true,
35 | },
36 | active: {
37 | type: Boolean,
38 | default: true,
39 | },
40 | approved: {
41 | type: Boolean,
42 | default: true,
43 | },
44 | additionalDetails: {
45 | type: mongoose.Schema.Types.ObjectId,
46 | required: true,
47 | ref: "Profile",
48 | },
49 | courses: [
50 | {
51 | type: mongoose.Schema.Types.ObjectId,
52 | ref: "Course",
53 | },
54 | ],
55 | token: {
56 | type: String,
57 | },
58 | resetPasswordExpires: {
59 | type: Date,
60 | },
61 | image: {
62 | type: String,
63 | },
64 | courseProgress: [
65 | {
66 | type: mongoose.Schema.Types.ObjectId,
67 | ref: "courseProgress",
68 | },
69 | ],
70 |
71 | // Add timestamps for when the document is created and last modified
72 | },
73 | { timestamps: true }
74 | )
75 |
76 | // Export the Mongoose model for the user schema, using the name "user"
77 | module.exports = mongoose.model("user", userSchema)
78 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codeplay-client",
3 | "description": "This is the client side of the CodePlay App",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@ramonak/react-progress-bar": "^5.0.3",
8 | "@reduxjs/toolkit": "^1.9.5",
9 | "@testing-library/jest-dom": "^5.16.5",
10 | "@testing-library/react": "^13.4.0",
11 | "@testing-library/user-event": "^13.5.0",
12 | "axios": "^1.3.5",
13 | "chart.js": "^4.3.0",
14 | "copy-to-clipboard": "^3.3.3",
15 | "nodemon": "^3.0.1",
16 | "react": "^18.2.0",
17 | "react-chartjs-2": "^5.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-dropzone": "^14.2.3",
20 | "react-hook-form": "^7.43.9",
21 | "react-hot-toast": "^2.4.0",
22 | "react-icons": "^4.8.0",
23 | "react-markdown": "^8.0.7",
24 | "react-otp-input": "^3.0.0",
25 | "react-rating-stars-component": "^2.2.0",
26 | "react-redux": "^8.0.5",
27 | "react-router-dom": "^6.9.0",
28 | "react-scripts": "5.0.1",
29 | "react-super-responsive-table": "^5.2.1",
30 | "react-type-animation": "^3.0.1",
31 | "showdown": "^2.1.0",
32 | "swiper": "^9.3.1",
33 | "video-react": "^0.16.0",
34 | "web-vitals": "^2.1.4"
35 | },
36 | "scripts": {
37 | "start": "react-scripts start",
38 | "build": "react-scripts build",
39 | "test": "react-scripts test",
40 | "eject": "react-scripts eject",
41 | "server": "cd server && npm run dev",
42 | "dev": "concurrently -n \"client,server\" -c \"bgBlue,bgYellow\" \"npm start\" \"npm run server\""
43 | },
44 | "eslintConfig": {
45 | "extends": [
46 | "react-app",
47 | "react-app/jest"
48 | ]
49 | },
50 | "browserslist": {
51 | "production": [
52 | ">0.2%",
53 | "not dead",
54 | "not op_mini all"
55 | ],
56 | "development": [
57 | "last 1 chrome version",
58 | "last 1 firefox version",
59 | "last 1 safari version"
60 | ]
61 | },
62 | "devDependencies": {
63 | "@ianvs/prettier-plugin-sort-imports": "^3.7.2",
64 | "concurrently": "^8.0.1",
65 | "prettier": "^2.8.8",
66 | "prettier-plugin-tailwindcss": "^0.3.0",
67 | "tailwindcss": "^3.2.7"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/core/Course/CourseAccordionBar.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react"
2 | import { AiOutlineDown } from "react-icons/ai"
3 |
4 | import CourseSubSectionAccordion from "./CourseSubSectionAccordion"
5 |
6 | export default function CourseAccordionBar({ course, isActive, handleActive }) {
7 | const contentEl = useRef(null)
8 |
9 | // Accordian state
10 | const [active, setActive] = useState(false)
11 | useEffect(() => {
12 | setActive(isActive?.includes(course._id))
13 | }, [isActive])
14 | const [sectionHeight, setSectionHeight] = useState(0)
15 | useEffect(() => {
16 | setSectionHeight(active ? contentEl.current.scrollHeight : 0)
17 | }, [active])
18 |
19 | return (
20 |
21 |
22 |
{
25 | handleActive(course._id)
26 | }}
27 | >
28 |
29 |
34 |
35 |
36 |
{course?.sectionName}
37 |
38 |
39 |
40 | {`${course.subSection.length || 0} lecture(s)`}
41 |
42 |
43 |
44 |
45 |
52 |
53 | {course?.subSection?.map((subSec, i) => {
54 | return
55 | })}
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/src/data/footer-links.js:
--------------------------------------------------------------------------------
1 | export const FooterLink2 = [
2 | {
3 | title: "Subjects",
4 | links: [
5 | { title: "Al", link: "/al" },
6 | { title: "Cloud Computing", link: "/cloud-computing" },
7 | { title: "Code Foundations", link: "/code-foundations" },
8 | { title: "Computer Science", link: "/computer-science" },
9 | { title: "Cybersecurity", link: "/cybersecurity" },
10 | { title: "Data Analytics", link: "/data-analytics" },
11 | { title: "Data Science", link: "/data-science" },
12 | { title: "Data Visualization", link: "/data-visualization" },
13 | { title: "Developer Tools", link: "/developer-tools" },
14 | { title: "DevOps", link: "/devops" },
15 | { title: "Game Development", link: "/game-development" },
16 | { title: "IT", link: "/it" },
17 | { title: "Machine Learning", link: "/machine-learning" },
18 | { title: "Math", link: "/math" },
19 | { title: "Mobile Development", link: "/mobile-development" },
20 | { title: "Web Design", link: "/web-design" },
21 | { title: "Web Development", link: "/web-development" },
22 | ],
23 | },
24 | {
25 | title: "Languages",
26 | links: [
27 | { title: "Bash", link: "/bash" },
28 | { title: "C++", link: "/c++" },
29 | { title: "C#", link: "/csharp" },
30 | { title: "Go", link: "/go" },
31 | { title: "HTML & CSS", link: "/html-css" },
32 | { title: "Java", link: "/java" },
33 | { title: "JavaScript", link: "/javascript" },
34 | { title: "Kotlin", link: "/kotlin" },
35 | { title: "PHP", link: "/php" },
36 | { title: "Python", link: "/python" },
37 | { title: "R", link: "/r" },
38 | { title: "Ruby", link: "/ruby" },
39 | { title: "SQL", link: "/sql" },
40 | { title: "Swift", link: "/swift" },
41 | ],
42 | },
43 | {
44 | title: "Career building",
45 | links: [
46 | {title: "Career paths", link: "/career-paths"},
47 | {title: "Career services", link: "/career-services"},
48 | {title: "Interview prep", link: "/interview-prep"},
49 | {title: "Professional certification", link: "/professional-certification"},
50 | {title: "-", link: "/hi"},
51 | {title: "Full Catalog", link: "/full-catalog"},
52 | {title: "Beta Content", link: "/beta-content"}
53 | ]
54 | }
55 | ];
56 |
--------------------------------------------------------------------------------
/src/components/core/Auth/ProfileDropdown.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react"
2 | import { AiOutlineCaretDown } from "react-icons/ai"
3 | import { VscDashboard, VscSignOut } from "react-icons/vsc"
4 | import { useDispatch, useSelector } from "react-redux"
5 | import { Link, useNavigate } from "react-router-dom"
6 |
7 | import useOnClickOutside from "../../../hooks/useOnClickOutside"
8 | import { logout } from "../../../services/operations/authAPI"
9 |
10 | export default function ProfileDropdown() {
11 | const { user } = useSelector((state) => state.profile)
12 | const dispatch = useDispatch()
13 | const navigate = useNavigate()
14 | const [open, setOpen] = useState(false)
15 | const ref = useRef(null)
16 |
17 | useOnClickOutside(ref, () => setOpen(false))
18 |
19 | if (!user) return null
20 |
21 | return (
22 | setOpen(true)}>
23 |
24 |
29 |
30 |
31 | {open && (
32 | e.stopPropagation()}
34 | className="absolute top-[118%] right-0 z-[1000] divide-y-[1px] divide-richblack-700 overflow-hidden rounded-md border-[1px] border-richblack-700 bg-richblack-800"
35 | ref={ref}
36 | >
37 |
setOpen(false)}>
38 |
39 |
40 | Dashboard
41 |
42 |
43 |
{
45 | dispatch(logout(navigate))
46 | setOpen(false)
47 | }}
48 | className="flex w-full items-center gap-x-1 py-[10px] px-[12px] text-sm text-richblack-100 hover:bg-richblack-700 hover:text-richblack-25"
49 | >
50 |
51 | Logout
52 |
53 |
54 | )}
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/server/mail/templates/emailVerificationTemplate.js:
--------------------------------------------------------------------------------
1 | const otpTemplate = (otp) => {
2 | return `
3 |
4 |
5 |
6 |
7 | OTP Verification Email
8 |
64 |
65 |
66 |
67 |
68 |
69 |
71 |
OTP Verification Email
72 |
73 |
Dear User,
74 |
Thank you for registering with CodePlay. To complete your registration, please use the following OTP
75 | (One-Time Password) to verify your account:
76 |
${otp}
77 |
This OTP is valid for 5 minutes. If you did not request this verification, please disregard this email.
78 | Once your account is verified, you will have access to our platform and its features.
79 |
80 |
If you have any questions or need assistance, please feel free to reach out to us at
info@codeplay.com . We are here to help!
82 |
83 |
84 |
85 | `;
86 | };
87 | module.exports = otpTemplate;
88 |
--------------------------------------------------------------------------------
/src/components/core/HomePage/CodeBlocks.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import CTAButton from "./Button";
3 | import { TypeAnimation } from "react-type-animation";
4 | import { FaArrowRight } from "react-icons/fa";
5 |
6 | const CodeBlocks = ({
7 | position,
8 | heading,
9 | subheading,
10 | ctabtn1,
11 | ctabtn2,
12 | codeblock,
13 | backgroundGradient,
14 | codeColor,
15 | }) => {
16 | return (
17 |
18 |
19 |
20 | {/* Section 1 */}
21 |
22 | {heading}
23 |
24 | {/* Sub Heading */}
25 |
26 | {subheading}
27 |
28 |
29 | {/* Button Group */}
30 |
31 |
32 |
33 | {ctabtn1.btnText}
34 |
35 |
36 |
37 |
38 | {ctabtn2.btnText}
39 |
40 |
41 |
42 |
43 | {/* Section 2 */}
44 |
45 | {backgroundGradient}
46 | {/* Indexing */}
47 |
48 |
1
49 |
2
50 |
3
51 |
4
52 |
5
53 |
6
54 |
7
55 |
8
56 |
9
57 |
10
58 |
11
59 |
60 |
61 | {/* Codes */}
62 |
65 |
75 |
76 |
77 |
78 | );
79 | };
80 |
81 | export default CodeBlocks;
82 |
--------------------------------------------------------------------------------
/server/mail/templates/passwordUpdate.js:
--------------------------------------------------------------------------------
1 | exports.passwordUpdated = (email, name) => {
2 | return `
3 |
4 |
5 |
6 |
7 | Password Update Confirmation
8 |
53 |
54 |
55 |
56 |
57 |
58 |
60 |
Password Update Confirmation
61 |
62 |
Hey ${name},
63 |
Your password has been successfully updated for the email ${email} .
64 |
65 |
If you did not request this password change, please contact us immediately to secure your account.
66 |
67 |
If you have any questions or need further assistance, please feel free to reach out to us
68 | at
69 |
info@codeplay.com . We are here to help!
70 |
71 |
72 |
73 |
74 | `;
75 | };
76 |
--------------------------------------------------------------------------------
/src/slices/cartSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 | import { toast } from "react-hot-toast"
3 |
4 | const initialState = {
5 | cart: localStorage.getItem("cart")
6 | ? JSON.parse(localStorage.getItem("cart"))
7 | : [],
8 | total: localStorage.getItem("total")
9 | ? JSON.parse(localStorage.getItem("total"))
10 | : 0,
11 | totalItems: localStorage.getItem("totalItems")
12 | ? JSON.parse(localStorage.getItem("totalItems"))
13 | : 0,
14 | }
15 |
16 | const cartSlice = createSlice({
17 | name: "cart",
18 | initialState,
19 | reducers: {
20 | addToCart: (state, action) => {
21 | const course = action.payload
22 | const index = state.cart.findIndex((item) => item._id === course._id)
23 |
24 | if (index >= 0) {
25 | // If the course is already in the cart, do not modify the quantity
26 | toast.error("Course already in cart")
27 | return
28 | }
29 | // If the course is not in the cart, add it to the cart
30 | state.cart.push(course)
31 | // Update the total quantity and price
32 | state.totalItems++
33 | state.total += course.price
34 | // Update to localstorage
35 | localStorage.setItem("cart", JSON.stringify(state.cart))
36 | localStorage.setItem("total", JSON.stringify(state.total))
37 | localStorage.setItem("totalItems", JSON.stringify(state.totalItems))
38 | // show toast
39 | toast.success("Course added to cart")
40 | },
41 | removeFromCart: (state, action) => {
42 | const courseId = action.payload
43 | const index = state.cart.findIndex((item) => item._id === courseId)
44 |
45 | if (index >= 0) {
46 | // If the course is found in the cart, remove it
47 | state.totalItems--
48 | state.total -= state.cart[index].price
49 | state.cart.splice(index, 1)
50 | // Update to localstorage
51 | localStorage.setItem("cart", JSON.stringify(state.cart))
52 | localStorage.setItem("total", JSON.stringify(state.total))
53 | localStorage.setItem("totalItems", JSON.stringify(state.totalItems))
54 | // show toast
55 | toast.success("Course removed from cart")
56 | }
57 | },
58 | resetCart: (state) => {
59 | state.cart = []
60 | state.total = 0
61 | state.totalItems = 0
62 | // Update to localstorage
63 | localStorage.removeItem("cart")
64 | localStorage.removeItem("total")
65 | localStorage.removeItem("totalItems")
66 | },
67 | },
68 | })
69 |
70 | export const { addToCart, removeFromCart, resetCart } = cartSlice.actions
71 |
72 | export default cartSlice.reducer
73 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/Cart/RenderCartCourses.jsx:
--------------------------------------------------------------------------------
1 | import { FaStar } from "react-icons/fa"
2 | import { RiDeleteBin6Line } from "react-icons/ri"
3 | import ReactStars from "react-rating-stars-component"
4 | import { useDispatch, useSelector } from "react-redux"
5 |
6 | import { removeFromCart } from "../../../../slices/cartSlice"
7 |
8 | export default function RenderCartCourses() {
9 | const { cart } = useSelector((state) => state.cart)
10 | const dispatch = useDispatch()
11 | return (
12 |
13 | {cart.map((course, indx) => (
14 |
20 |
21 |
26 |
27 |
28 | {course?.courseName}
29 |
30 |
31 | {course?.category?.name}
32 |
33 |
34 | 4.5
35 | }
42 | fullIcon={ }
43 | />
44 |
45 | {course?.ratingAndReviews?.length} Ratings
46 |
47 |
48 |
49 |
50 |
51 |
dispatch(removeFromCart(course._id))}
53 | className="flex items-center gap-x-1 rounded-md border border-richblack-600 bg-richblack-700 py-3 px-[12px] text-pink-200"
54 | >
55 |
56 | Remove
57 |
58 |
59 | ₹ {course?.price}
60 |
61 |
62 |
63 | ))}
64 |
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { VscSignOut } from "react-icons/vsc"
3 | import { useDispatch, useSelector } from "react-redux"
4 | import { useNavigate } from "react-router-dom"
5 |
6 | import { sidebarLinks } from "../../../data/dashboard-links"
7 | import { logout } from "../../../services/operations/authAPI"
8 | import ConfirmationModal from "../../Common/ConfirmationModal"
9 | import SidebarLink from "./SidebarLink"
10 |
11 | export default function Sidebar() {
12 | const { user, loading: profileLoading } = useSelector(
13 | (state) => state.profile
14 | )
15 | const { loading: authLoading } = useSelector((state) => state.auth)
16 | const dispatch = useDispatch()
17 | const navigate = useNavigate()
18 | // to keep track of confirmation modal
19 | const [confirmationModal, setConfirmationModal] = useState(null)
20 |
21 | if (profileLoading || authLoading) {
22 | return (
23 |
26 | )
27 | }
28 |
29 | return (
30 | <>
31 |
32 |
33 | {sidebarLinks.map((link) => {
34 | if (link.type && user?.accountType !== link.type) return null
35 | return (
36 |
37 | )
38 | })}
39 |
40 |
41 |
42 |
46 |
48 | setConfirmationModal({
49 | text1: "Are you sure?",
50 | text2: "You will be logged out of your account.",
51 | btn1Text: "Logout",
52 | btn2Text: "Cancel",
53 | btn1Handler: () => dispatch(logout(navigate)),
54 | btn2Handler: () => setConfirmationModal(null),
55 | })
56 | }
57 | className="px-8 py-2 text-sm font-medium text-richblack-300"
58 | >
59 |
60 |
61 | Logout
62 |
63 |
64 |
65 |
66 | {confirmationModal && }
67 | >
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/src/services/operations/profileAPI.js:
--------------------------------------------------------------------------------
1 | import { toast } from "react-hot-toast"
2 |
3 | import { setLoading, setUser } from "../../slices/profileSlice"
4 | import { apiConnector } from "../apiConnector"
5 | import { profileEndpoints } from "../apis"
6 | import { logout } from "./authAPI"
7 |
8 | const {
9 | GET_USER_DETAILS_API,
10 | GET_USER_ENROLLED_COURSES_API,
11 | GET_INSTRUCTOR_DATA_API,
12 | } = profileEndpoints
13 |
14 | export function getUserDetails(token, navigate) {
15 | return async (dispatch) => {
16 | const toastId = toast.loading("Loading...")
17 | dispatch(setLoading(true))
18 | try {
19 | const response = await apiConnector("GET", GET_USER_DETAILS_API, null, {
20 | Authorization: `Bearer ${token}`,
21 | })
22 | console.log("GET_USER_DETAILS API RESPONSE............", response)
23 |
24 | if (!response.data.success) {
25 | throw new Error(response.data.message)
26 | }
27 | const userImage = response.data.data.image
28 | ? response.data.data.image
29 | : `https://api.dicebear.com/5.x/initials/svg?seed=${response.data.data.firstName} ${response.data.data.lastName}`
30 | dispatch(setUser({ ...response.data.data, image: userImage }))
31 | } catch (error) {
32 | dispatch(logout(navigate))
33 | console.log("GET_USER_DETAILS API ERROR............", error)
34 | toast.error("Could Not Get User Details")
35 | }
36 | toast.dismiss(toastId)
37 | dispatch(setLoading(false))
38 | }
39 | }
40 |
41 | export async function getUserEnrolledCourses(token) {
42 | const toastId = toast.loading("Loading...")
43 | let result = []
44 | try {
45 | const response = await apiConnector(
46 | "GET",
47 | GET_USER_ENROLLED_COURSES_API,
48 | null,
49 | {
50 | Authorization: `Bearer ${token}`,
51 | }
52 | )
53 |
54 |
55 | if (!response.data.success) {
56 | throw new Error(response.data.message)
57 | }
58 | result = response.data.data
59 | } catch (error) {
60 | console.log("GET_USER_ENROLLED_COURSES_API API ERROR............", error)
61 | toast.error("Could Not Get Enrolled Courses")
62 | }
63 | toast.dismiss(toastId)
64 | return result
65 | }
66 |
67 | export async function getInstructorData(token) {
68 | const toastId = toast.loading("Loading...")
69 | let result = []
70 | try {
71 | const response = await apiConnector("GET", GET_INSTRUCTOR_DATA_API, null, {
72 | Authorization: `Bearer ${token}`,
73 | })
74 | console.log("GET_INSTRUCTOR_DATA_API API RESPONSE............", response)
75 | result = response?.data?.courses
76 | } catch (error) {
77 | console.log("GET_INSTRUCTOR_DATA_API API ERROR............", error)
78 | toast.error("Could Not Get Instructor Data")
79 | }
80 | toast.dismiss(toastId)
81 | return result
82 | }
83 |
--------------------------------------------------------------------------------
/src/pages/ForgotPassword.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { BiArrowBack } from "react-icons/bi"
3 | import { useDispatch, useSelector } from "react-redux"
4 | import { Link } from "react-router-dom"
5 |
6 | import { getPasswordResetToken } from "../services/operations/authAPI"
7 |
8 | function ForgotPassword() {
9 | const [email, setEmail] = useState("")
10 | const [emailSent, setEmailSent] = useState(false)
11 | const dispatch = useDispatch()
12 | const { loading } = useSelector((state) => state.auth)
13 |
14 | const handleOnSubmit = (e) => {
15 | e.preventDefault()
16 | dispatch(getPasswordResetToken(email, setEmailSent))
17 | }
18 |
19 | return (
20 |
21 | {loading ? (
22 |
23 | ) : (
24 |
25 |
26 | {!emailSent ? "Reset your password" : "Check email"}
27 |
28 |
29 | {!emailSent
30 | ? "Have no fear. We'll email you instructions to reset your password. If you dont have access to your email we can try account recovery"
31 | : `We have sent the reset email to ${email}`}
32 |
33 |
57 |
58 |
59 |
60 | Back To Login
61 |
62 |
63 |
64 |
65 | )}
66 |
67 | )
68 | }
69 |
70 | export default ForgotPassword
71 |
--------------------------------------------------------------------------------
/server/controllers/resetPassword.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/User")
2 | const mailSender = require("../utils/mailSender")
3 | const bcrypt = require("bcrypt")
4 | const crypto = require("crypto")
5 | exports.resetPasswordToken = async (req, res) => {
6 | try {
7 | const email = req.body.email
8 | const user = await User.findOne({ email: email })
9 | if (!user) {
10 | return res.json({
11 | success: false,
12 | message: `This Email: ${email} is not Registered With Us Enter a Valid Email `,
13 | })
14 | }
15 | const token = crypto.randomBytes(20).toString("hex")
16 |
17 | const updatedDetails = await User.findOneAndUpdate(
18 | { email: email },
19 | {
20 | token: token,
21 | resetPasswordExpires: Date.now() + 3600000,
22 | },
23 | { new: true }
24 | )
25 | console.log("DETAILS", updatedDetails)
26 |
27 | // const url = `http://localhost:3000/update-password/${token}`
28 | const url = `https://codeplay-edtech-project.vercel.app/update-password/${token}`
29 |
30 | await mailSender(
31 | email,
32 | "Password Reset",
33 | `Your Link for email verification is ${url}. Please click this url to reset your password.`
34 | )
35 |
36 | res.json({
37 | success: true,
38 | message:
39 | "Email Sent Successfully, Please Check Your Email to Continue Further",
40 | })
41 | } catch (error) {
42 | return res.json({
43 | error: error.message,
44 | success: false,
45 | message: `Some Error in Sending the Reset Message`,
46 | })
47 | }
48 | }
49 |
50 | exports.resetPassword = async (req, res) => {
51 | try {
52 | const { password, confirmPassword, token } = req.body
53 |
54 | if (confirmPassword !== password) {
55 | return res.json({
56 | success: false,
57 | message: "Password and Confirm Password Does not Match",
58 | })
59 | }
60 | const userDetails = await User.findOne({ token: token })
61 | if (!userDetails) {
62 | return res.json({
63 | success: false,
64 | message: "Token is Invalid",
65 | })
66 | }
67 | if (!(userDetails.resetPasswordExpires > Date.now())) {
68 | return res.status(403).json({
69 | success: false,
70 | message: `Token is Expired, Please Regenerate Your Token`,
71 | })
72 | }
73 | const encryptedPassword = await bcrypt.hash(password, 10)
74 | await User.findOneAndUpdate(
75 | { token: token },
76 | { password: encryptedPassword },
77 | { new: true }
78 | )
79 | res.json({
80 | success: true,
81 | message: `Password Reset Successful`,
82 | })
83 | } catch (error) {
84 | return res.json({
85 | error: error.message,
86 | success: false,
87 | message: `Some Error in Updating the Password`,
88 | })
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/server/mail/templates/paymentSuccessEmail.js:
--------------------------------------------------------------------------------
1 | exports.paymentSuccessEmail = (name, amount, orderId, paymentId) => {
2 | return `
3 |
4 |
5 |
6 |
7 | Payment Confirmation
8 |
65 |
66 |
67 |
68 |
69 |
70 |
72 |
Course Payment Confirmation
73 |
74 |
Dear ${name},
75 |
We have received a payment of ₹${amount}
.
76 |
Your Payment ID is ${paymentId}
77 |
Your Order ID is ${orderId}
78 |
79 |
If you have any questions or need assistance, please feel free to reach out to us at
info@codeplay.com . We are here to help!
81 |
82 |
83 |
84 | `
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/AddCourse/CourseInformation/RequirementsField.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { useSelector } from "react-redux"
3 |
4 | export default function RequirementsField({
5 | name,
6 | label,
7 | register,
8 | setValue,
9 | errors,
10 | getValues,
11 | }) {
12 | const { editCourse, course } = useSelector((state) => state.course)
13 | const [requirement, setRequirement] = useState("")
14 | const [requirementsList, setRequirementsList] = useState([])
15 |
16 | useEffect(() => {
17 | if (editCourse) {
18 | setRequirementsList(course?.instructions)
19 | }
20 | register(name, { required: true, validate: (value) => value.length > 0 })
21 | // eslint-disable-next-line react-hooks/exhaustive-deps
22 | }, [])
23 |
24 | useEffect(() => {
25 | setValue(name, requirementsList)
26 | // eslint-disable-next-line react-hooks/exhaustive-deps
27 | }, [requirementsList])
28 |
29 | const handleAddRequirement = () => {
30 | if (requirement) {
31 | setRequirementsList([...requirementsList, requirement])
32 | setRequirement("")
33 | }
34 | }
35 |
36 | const handleRemoveRequirement = (index) => {
37 | const updatedRequirements = [...requirementsList]
38 | updatedRequirements.splice(index, 1)
39 | setRequirementsList(updatedRequirements)
40 | }
41 |
42 | return (
43 |
44 |
45 | {label} *
46 |
47 |
48 | setRequirement(e.target.value)}
53 | className="form-style w-full"
54 | />
55 |
60 | Add
61 |
62 |
63 | {requirementsList.length > 0 && (
64 |
65 | {requirementsList.map((requirement, index) => (
66 |
67 | {requirement}
68 | handleRemoveRequirement(index)}
72 | >
73 | clear
74 |
75 |
76 | ))}
77 |
78 | )}
79 | {errors[name] && (
80 |
81 | {label} is required
82 |
83 | )}
84 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/src/components/core/HomePage/ExploreMore.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { HomePageExplore } from "../../../data/homepage-explore";
3 | import CourseCard from "./CourseCard";
4 | import HighlightText from "./HighlightText";
5 |
6 | const tabsName = [
7 | "Free",
8 | "New to coding",
9 | "Most popular",
10 | "Skills paths",
11 | "Career paths",
12 | ];
13 |
14 | const ExploreMore = () => {
15 | const [currentTab, setCurrentTab] = useState(tabsName[0]);
16 | const [courses, setCourses] = useState(HomePageExplore[0].courses);
17 | const [currentCard, setCurrentCard] = useState(
18 | HomePageExplore[0].courses[0].heading
19 | );
20 |
21 | const setMyCards = (value) => {
22 | setCurrentTab(value);
23 | const result = HomePageExplore.filter((course) => course.tag === value);
24 | setCourses(result[0].courses);
25 | setCurrentCard(result[0].courses[0].heading);
26 | };
27 |
28 | return (
29 |
30 | {/* Explore more section */}
31 |
32 |
33 | Unlock the
34 |
35 |
36 | Learn to Build Anything You Can Imagine
37 |
38 |
39 |
40 |
41 | {/* Tabs Section */}
42 |
43 | {tabsName.map((ele, index) => {
44 | return (
45 |
setMyCards(ele)}
53 | >
54 | {ele}
55 |
56 | );
57 | })}
58 |
59 |
60 |
61 | {/* Cards Group */}
62 |
63 | {courses.map((ele, index) => {
64 | return (
65 |
71 | );
72 | })}
73 |
74 |
75 | );
76 | };
77 |
78 | export default ExploreMore;
79 |
--------------------------------------------------------------------------------
/src/components/core/Auth/LoginForm.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai"
3 | import { useDispatch } from "react-redux"
4 | import { Link, useNavigate } from "react-router-dom"
5 |
6 | import { login } from "../../../services/operations/authAPI"
7 |
8 | function LoginForm() {
9 | const navigate = useNavigate()
10 | const dispatch = useDispatch()
11 | const [formData, setFormData] = useState({
12 | email: "",
13 | password: "",
14 | })
15 |
16 | const [showPassword, setShowPassword] = useState(false)
17 |
18 | const { email, password } = formData
19 |
20 | const handleOnChange = (e) => {
21 | setFormData((prevData) => ({
22 | ...prevData,
23 | [e.target.name]: e.target.value,
24 | }))
25 | }
26 |
27 | const handleOnSubmit = (e) => {
28 | e.preventDefault()
29 | dispatch(login(email, password, navigate))
30 | }
31 |
32 | return (
33 |
87 | )
88 | }
89 |
90 | export default LoginForm
91 |
--------------------------------------------------------------------------------
/src/services/apis.js:
--------------------------------------------------------------------------------
1 | const BASE_URL = process.env.REACT_APP_BASE_URL
2 |
3 | // AUTH ENDPOINTS
4 | export const endpoints = {
5 | SENDOTP_API: BASE_URL + "/auth/sendotp",
6 | SIGNUP_API: BASE_URL + "/auth/signup",
7 | LOGIN_API: BASE_URL + "/auth/login",
8 | RESETPASSTOKEN_API: BASE_URL + "/auth/reset-password-token",
9 | RESETPASSWORD_API: BASE_URL + "/auth/reset-password",
10 | }
11 |
12 | // PROFILE ENDPOINTS
13 | export const profileEndpoints = {
14 | GET_USER_DETAILS_API: BASE_URL + "/profile/getUserDetails",
15 | GET_USER_ENROLLED_COURSES_API: BASE_URL + "/profile/getEnrolledCourses",
16 | GET_INSTRUCTOR_DATA_API: BASE_URL + "/profile/instructorDashboard",
17 | }
18 |
19 | // STUDENTS ENDPOINTS
20 | export const studentEndpoints = {
21 | COURSE_PAYMENT_API: BASE_URL + "/payment/capturePayment",
22 | COURSE_VERIFY_API: BASE_URL + "/payment/verifyPayment",
23 | SEND_PAYMENT_SUCCESS_EMAIL_API: BASE_URL + "/payment/sendPaymentSuccessEmail",
24 | }
25 |
26 | // COURSE ENDPOINTS
27 | export const courseEndpoints = {
28 | GET_ALL_COURSE_API: BASE_URL + "/course/getAllCourses",
29 | COURSE_DETAILS_API: BASE_URL + "/course/getCourseDetails",
30 | EDIT_COURSE_API: BASE_URL + "/course/editCourse",
31 | COURSE_CATEGORIES_API: BASE_URL + "/course/showAllCategories",
32 | CREATE_COURSE_API: BASE_URL + "/course/createCourse",
33 | CREATE_SECTION_API: BASE_URL + "/course/addSection",
34 | CREATE_SUBSECTION_API: BASE_URL + "/course/addSubSection",
35 | UPDATE_SECTION_API: BASE_URL + "/course/updateSection",
36 | UPDATE_SUBSECTION_API: BASE_URL + "/course/updateSubSection",
37 | GET_ALL_INSTRUCTOR_COURSES_API: BASE_URL + "/course/getInstructorCourses",
38 | DELETE_SECTION_API: BASE_URL + "/course/deleteSection",
39 | DELETE_SUBSECTION_API: BASE_URL + "/course/deleteSubSection",
40 | DELETE_COURSE_API: BASE_URL + "/course/deleteCourse",
41 | GET_FULL_COURSE_DETAILS_AUTHENTICATED:
42 | BASE_URL + "/course/getFullCourseDetails",
43 | LECTURE_COMPLETION_API: BASE_URL + "/course/updateCourseProgress",
44 | CREATE_RATING_API: BASE_URL + "/course/createRating",
45 | }
46 |
47 | // RATINGS AND REVIEWS
48 | export const ratingsEndpoints = {
49 | REVIEWS_DETAILS_API: BASE_URL + "/course/getReviews",
50 | }
51 |
52 | // CATAGORIES API
53 | export const categories = {
54 | CATEGORIES_API: BASE_URL + "/course/showAllCategories",
55 | }
56 |
57 | // CATALOG PAGE DATA
58 | export const catalogData = {
59 | CATALOGPAGEDATA_API: BASE_URL + "/course/getCategoryPageDetails",
60 | }
61 | // CONTACT-US API
62 | export const contactusEndpoint = {
63 | CONTACT_US_API: BASE_URL + "/reach/contact",
64 | }
65 |
66 | // SETTINGS PAGE API
67 | export const settingsEndpoints = {
68 | UPDATE_DISPLAY_PICTURE_API: BASE_URL + "/profile/updateDisplayPicture",
69 | UPDATE_PROFILE_API: BASE_URL + "/profile/updateProfile",
70 | CHANGE_PASSWORD_API: BASE_URL + "/auth/changepassword",
71 | DELETE_PROFILE_API: BASE_URL + "/profile/deleteProfile",
72 | }
73 |
--------------------------------------------------------------------------------
/server/mail/templates/courseEnrollmentEmail.js:
--------------------------------------------------------------------------------
1 | exports.courseEnrollmentEmail = (courseName, name) => {
2 | return `
3 |
4 |
5 |
6 |
7 | Course Registration Confirmation
8 |
65 |
66 |
67 |
68 |
69 |
70 |
72 |
Course Registration Confirmation
73 |
74 |
Dear ${name},
75 |
You have successfully registered for the course "${courseName}" . We
76 | are excited to have you as a participant!
77 |
Please log in to your learning dashboard to access the course materials and start your learning journey.
78 |
79 |
Go to Dashboard
80 |
81 |
If you have any questions or need assistance, please feel free to reach out to us at
info@codeplay.com . We are here to help!
83 |
84 |
85 |
86 | `;
87 | };
88 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/AddCourse/RenderSteps.jsx:
--------------------------------------------------------------------------------
1 | import { FaCheck } from "react-icons/fa"
2 | import { useSelector } from "react-redux"
3 |
4 | import CourseBuilderForm from "./CourseBuilder/CourseBuilderForm"
5 | import CourseInformationForm from "./CourseInformation/CourseInformationForm"
6 | import PublishCourse from "./PublishCourse"
7 |
8 | export default function RenderSteps() {
9 | const { step } = useSelector((state) => state.course)
10 |
11 | const steps = [
12 | {
13 | id: 1,
14 | title: "Course Information",
15 | },
16 | {
17 | id: 2,
18 | title: "Course Builder",
19 | },
20 | {
21 | id: 3,
22 | title: "Publish",
23 | },
24 | ]
25 |
26 | return (
27 | <>
28 |
29 | {steps.map((item) => (
30 | <>
31 |
35 | item.id && "bg-yellow-50 text-yellow-50"}} `}
41 | >
42 | {step > item.id ? (
43 |
44 | ) : (
45 | item.id
46 | )}
47 |
48 |
49 |
50 | {item.id !== steps.length && (
51 | <>
52 |
item.id ? "border-yellow-50" : "border-richblack-500"
55 | } `}
56 | >
57 | >
58 | )}
59 | >
60 | ))}
61 |
62 |
63 |
64 | {steps.map((item) => (
65 | <>
66 |
70 |
71 |
= item.id ? "text-richblack-5" : "text-richblack-500"
74 | }`}
75 | >
76 | {item.title}
77 |
78 |
79 |
80 | >
81 | ))}
82 |
83 | {/* Render specific component based on current step */}
84 | {step === 1 && }
85 | {step === 2 && }
86 | {step === 3 && }
87 | >
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CodePlay Edtech MERN Application
2 |
3 | Welcome to the CodePlay EdTech Platform Repository!
4 |
5 | This comprehensive repository houses the backbone of the revolutionary CodePlay EdTech platform – a dynamic and feature-rich MERN stack application that is set to reshape the landscape of online education. With a seamless blend of technology and education, CodePlay empowers both instructors and students, fostering a vibrant learning community.
6 |
7 | * Project Link :- https://codeplay-edtech-project.vercel.app/
8 |
9 | ## Features
10 |
11 | **1. MERN Stack Architecture** : The foundation of CodePlay is built upon the robust MERN (MongoDB, Express.js, React, Node.js) stack. This choice of technology ensures scalability, performance, and maintainability, allowing for a smooth and immersive user experience.
12 |
13 | **2. Instructor Empowerment** : Instructors are the heart of CodePlay. With this platform, educators can effortlessly craft and publish their courses, utilizing a user-friendly interface that allows them to upload course materials and videos. CodePlay's intuitive content creation tools give instructors the freedom to focus on delivering high-quality education.
14 |
15 | **3. Interactive Learning** : Students enrolled on CodePlay gain access to a diverse range of courses. From Webdev to Datascience, CodePlay caters to an array of subjects. The platform offers interactive video lectures, creating an immersive and well-rounded learning experience.
16 |
17 | **4. Seamless Course Purchase** : Students can browse through the extensive catalog of courses, read descriptions, view course previews, and make secure payments. The platform's seamless payment gateway ensures a hassle-free purchasing process, enabling students to jumpstart their learning journey with ease.
18 |
19 | **5. Personalized Dashboards** : Both instructors and students have personalized dashboards that provide a snapshot of their engagement on the platform. Instructors can track the performance of their courses, monitor student enrollment, and gain insights into their earnings through comprehensible graphs and statistics.
20 |
21 | **6. Financial Insights** : CodePlay empowers instructors with a transparent financial overview. The platform displays real-time data on course sales, student enrollment, and income earned. This invaluable information assists instructors in refining their teaching strategies and creating content that resonates with their audience.
22 |
23 | ## Technologies Used
24 |
25 | The CodePlay is built using the following technologies:
26 |
27 | #### React JS, Tailwind
28 | - Front-End Development.
29 | #### Node JS, Express JS
30 | - Back-End Development.
31 | #### Razorpay
32 | - Payment Gateway.
33 | #### MongoDB
34 | - Database.
35 |
36 | ## Acknowledgements
37 |
38 | The CodePlay was created by Pranjal Mishra. Special thanks to Love Babbar Bhaiya and CodeHelp.
39 |
40 | ## Contact
41 |
42 | If you have any questions or suggestions regarding this project, please feel free to contact Email - PranjalMishraContact@gmail.com.
43 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/InstructorDashboard/InstructorChart.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import { Chart, registerables } from "chart.js"
3 | import { Pie } from "react-chartjs-2"
4 |
5 | Chart.register(...registerables)
6 |
7 | export default function InstructorChart({ courses }) {
8 | // State to keep track of the currently selected chart
9 | const [currChart, setCurrChart] = useState("students")
10 |
11 | // Function to generate random colors for the chart
12 | const generateRandomColors = (numColors) => {
13 | const colors = []
14 | for (let i = 0; i < numColors; i++) {
15 | const color = `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(
16 | Math.random() * 256
17 | )}, ${Math.floor(Math.random() * 256)})`
18 | colors.push(color)
19 | }
20 | return colors
21 | }
22 |
23 | // Data for the chart displaying student information
24 | const chartDataStudents = {
25 | labels: courses.map((course) => course.courseName),
26 | datasets: [
27 | {
28 | data: courses.map((course) => course.totalStudentsEnrolled),
29 | backgroundColor: generateRandomColors(courses.length),
30 | },
31 | ],
32 | }
33 |
34 | // Data for the chart displaying income information
35 | const chartIncomeData = {
36 | labels: courses.map((course) => course.courseName),
37 | datasets: [
38 | {
39 | data: courses.map((course) => course.totalAmountGenerated),
40 | backgroundColor: generateRandomColors(courses.length),
41 | },
42 | ],
43 | }
44 |
45 | // Options for the chart
46 | const options = {
47 | maintainAspectRatio: false,
48 | }
49 |
50 | return (
51 |
52 |
Visualize
53 |
54 | {/* Button to switch to the "students" chart */}
55 | setCurrChart("students")}
57 | className={`rounded-sm p-1 px-3 transition-all duration-200 ${
58 | currChart === "students"
59 | ? "bg-richblack-700 text-yellow-50"
60 | : "text-yellow-400"
61 | }`}
62 | >
63 | Students
64 |
65 | {/* Button to switch to the "income" chart */}
66 | setCurrChart("income")}
68 | className={`rounded-sm p-1 px-3 transition-all duration-200 ${
69 | currChart === "income"
70 | ? "bg-richblack-700 text-yellow-50"
71 | : "text-yellow-400"
72 | }`}
73 | >
74 | Income
75 |
76 |
77 |
78 | {/* Render the Pie chart based on the selected chart */}
79 |
83 |
84 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/server/mail/templates/contactFormRes.js:
--------------------------------------------------------------------------------
1 | exports.contactUsEmail = (
2 | email,
3 | firstname,
4 | lastname,
5 | message,
6 | phoneNo,
7 | countrycode
8 | ) => {
9 | return `
10 |
11 |
12 |
13 |
14 | Contact Form Confirmation
15 |
72 |
73 |
74 |
75 |
76 |
77 |
79 |
Contact Form Confirmation
80 |
81 |
Dear ${firstname} ${lastname},
82 |
Thank you for contacting us. We have received your message and will respond to you as soon as possible.
83 |
84 |
Here are the details you provided:
85 |
Name: ${firstname} ${lastname}
86 |
Email: ${email}
87 |
Phone Number: ${phoneNo}
88 |
Message: ${message}
89 |
We appreciate your interest and will get back to you shortly.
90 |
91 |
If you have any further questions or need immediate assistance, please feel free to reach
92 | out to us at
info@codeplay.com . We are here to help!
93 |
94 |
95 |
96 | `
97 | }
98 |
--------------------------------------------------------------------------------
/server/middleware/auth.js:
--------------------------------------------------------------------------------
1 | // Importing required modules
2 | const jwt = require("jsonwebtoken");
3 | const dotenv = require("dotenv");
4 | const User = require("../models/User");
5 | // Configuring dotenv to load environment variables from .env file
6 | dotenv.config();
7 |
8 | // This function is used as middleware to authenticate user requests
9 | exports.auth = async (req, res, next) => {
10 | try {
11 | // Extracting JWT from request cookies, body or header
12 | const token =
13 | req.cookies.token ||
14 | req.body.token ||
15 | req.header("Authorization").replace("Bearer ", "");
16 |
17 | // If JWT is missing, return 401 Unauthorized response
18 | if (!token) {
19 | return res.status(401).json({ success: false, message: `Token Missing` });
20 | }
21 |
22 | try {
23 | // Verifying the JWT using the secret key stored in environment variables
24 | const decode = await jwt.verify(token, process.env.JWT_SECRET);
25 | console.log(decode);
26 | // Storing the decoded JWT payload in the request object for further use
27 | req.user = decode;
28 | } catch (error) {
29 | // If JWT verification fails, return 401 Unauthorized response
30 | return res
31 | .status(401)
32 | .json({ success: false, message: "token is invalid" });
33 | }
34 |
35 | // If JWT is valid, move on to the next middleware or request handler
36 | next();
37 | } catch (error) {
38 | // If there is an error during the authentication process, return 401 Unauthorized response
39 | return res.status(401).json({
40 | success: false,
41 | message: `Something Went Wrong While Validating the Token`,
42 | });
43 | }
44 | };
45 | exports.isStudent = async (req, res, next) => {
46 | try {
47 | const userDetails = await User.findOne({ email: req.user.email });
48 |
49 | if (userDetails.accountType !== "Student") {
50 | return res.status(401).json({
51 | success: false,
52 | message: "This is a Protected Route for Students",
53 | });
54 | }
55 | next();
56 | } catch (error) {
57 | return res
58 | .status(500)
59 | .json({ success: false, message: `User Role Can't be Verified` });
60 | }
61 | };
62 | exports.isAdmin = async (req, res, next) => {
63 | try {
64 | const userDetails = await User.findOne({ email: req.user.email });
65 |
66 | if (userDetails.accountType !== "Admin") {
67 | return res.status(401).json({
68 | success: false,
69 | message: "This is a Protected Route for Admin",
70 | });
71 | }
72 | next();
73 | } catch (error) {
74 | return res
75 | .status(500)
76 | .json({ success: false, message: `User Role Can't be Verified` });
77 | }
78 | };
79 | exports.isInstructor = async (req, res, next) => {
80 | try {
81 | const userDetails = await User.findOne({ email: req.user.email });
82 | console.log(userDetails);
83 |
84 | console.log(userDetails.accountType);
85 |
86 | if (userDetails.accountType !== "Instructor") {
87 | return res.status(401).json({
88 | success: false,
89 | message: "This is a Protected Route for Instructor",
90 | });
91 | }
92 | next();
93 | } catch (error) {
94 | return res
95 | .status(500)
96 | .json({ success: false, message: `User Role Can't be Verified` });
97 | }
98 | };
99 |
--------------------------------------------------------------------------------
/src/components/core/AboutPage/LearningGrid.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import HighlightText from "../../../components/core/HomePage/HighlightText";
3 | import CTAButton from "../../../components/core/HomePage/Button";
4 |
5 | const LearningGridArray = [
6 | {
7 | order: -1,
8 | heading: "World-Class Learning for",
9 | highliteText: "Anyone, Anywhere",
10 | description:
11 | "CodePlay partners with more than 275+ leading universities and companies to bring flexible, affordable, job-relevant online learning to individuals and organizations worldwide.",
12 | BtnText: "Learn More",
13 | BtnLink: "/",
14 | },
15 | {
16 | order: 1,
17 | heading: "Curriculum Based on Industry Needs",
18 | description:
19 | "Save time and money! The Belajar curriculum is made to be easier to understand and in line with industry needs.",
20 | },
21 | {
22 | order: 2,
23 | heading: "Our Learning Methods",
24 | description:
25 | "CodePlay partners with more than 275+ leading universities and companies to bring",
26 | },
27 | {
28 | order: 3,
29 | heading: "Certification",
30 | description:
31 | "CodePlay partners with more than 275+ leading universities and companies to bring",
32 | },
33 | {
34 | order: 4,
35 | heading: `Rating "Auto-grading"`,
36 | description:
37 | "CodePlay partners with more than 275+ leading universities and companies to bring",
38 | },
39 | {
40 | order: 5,
41 | heading: "Ready to Work",
42 | description:
43 | "CodePlay partners with more than 275+ leading universities and companies to bring",
44 | },
45 | ];
46 |
47 | const LearningGrid = () => {
48 | return (
49 |
50 | {LearningGridArray.map((card, i) => {
51 | return (
52 |
62 | {card.order < 0 ? (
63 |
64 |
65 | {card.heading}
66 |
67 |
68 |
69 | {card.description}
70 |
71 |
72 |
73 |
74 | {card.BtnText}
75 |
76 |
77 |
78 | ) : (
79 |
80 |
{card.heading}
81 |
82 |
83 | {card.description}
84 |
85 |
86 | )}
87 |
88 | );
89 | })}
90 |
91 | );
92 | };
93 |
94 | export default LearningGrid;
95 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/Settings/ChangeProfilePicture.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react"
2 | import { FiUpload } from "react-icons/fi"
3 | import { useDispatch, useSelector } from "react-redux"
4 |
5 | import { updateDisplayPicture } from "../../../../services/operations/SettingsAPI"
6 | import IconBtn from "../../../Common/IconBtn"
7 |
8 | export default function ChangeProfilePicture() {
9 | const { token } = useSelector((state) => state.auth)
10 | const { user } = useSelector((state) => state.profile)
11 | const dispatch = useDispatch()
12 |
13 | const [loading, setLoading] = useState(false)
14 | const [imageFile, setImageFile] = useState(null)
15 | const [previewSource, setPreviewSource] = useState(null)
16 |
17 | const fileInputRef = useRef(null)
18 |
19 | const handleClick = () => {
20 | fileInputRef.current.click()
21 | }
22 |
23 | const handleFileChange = (e) => {
24 | const file = e.target.files[0]
25 | // console.log(file)
26 | if (file) {
27 | setImageFile(file)
28 | previewFile(file)
29 | }
30 | }
31 |
32 | const previewFile = (file) => {
33 | const reader = new FileReader()
34 | reader.readAsDataURL(file)
35 | reader.onloadend = () => {
36 | setPreviewSource(reader.result)
37 | }
38 | }
39 |
40 | const handleFileUpload = () => {
41 | try {
42 | console.log("uploading...")
43 | setLoading(true)
44 | const formData = new FormData()
45 | formData.append("displayPicture", imageFile)
46 | // console.log("formdata", formData)
47 | dispatch(updateDisplayPicture(token, formData)).then(() => {
48 | setLoading(false)
49 | })
50 | } catch (error) {
51 | console.log("ERROR MESSAGE - ", error.message)
52 | }
53 | }
54 |
55 | useEffect(() => {
56 | if (imageFile) {
57 | previewFile(imageFile)
58 | }
59 | }, [imageFile])
60 | return (
61 | <>
62 |
63 |
64 |
69 |
70 |
Change Profile Picture
71 |
72 |
79 |
84 | Select
85 |
86 |
90 | {!loading && (
91 |
92 | )}
93 |
94 |
95 |
96 |
97 |
98 | >
99 | )
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/core/HomePage/Timeline.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TimeLineImage from "../../../assets/Images/TimelineImage.png";
3 | import Logo1 from "../../../assets/TimeLineLogo/Logo1.svg";
4 | import Logo2 from "../../../assets/TimeLineLogo/Logo2.svg";
5 | import Logo3 from "../../../assets/TimeLineLogo/Logo3.svg";
6 | import Logo4 from "../../../assets/TimeLineLogo/Logo4.svg";
7 |
8 | const TimeLine = [
9 | {
10 | Logo: Logo1,
11 | Heading: "Leadership",
12 | Description: "Fully committed to the success company",
13 | },
14 | {
15 | Logo: Logo2,
16 | Heading: "Responsibility",
17 | Description: "Students will always be our top priority",
18 | },
19 | {
20 | Logo: Logo3,
21 | Heading: "Flexibility",
22 | Description: "The ability to switch is an important skills",
23 | },
24 | {
25 | Logo: Logo4,
26 | Heading: "Solve the problem",
27 | Description: "Code your way to a solution",
28 | },
29 | ];
30 |
31 |
32 | const TimelineSection = () => {
33 | return (
34 |
35 |
36 |
37 | {TimeLine.map((ele, i) => {
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 |
{ele.Heading}
46 |
{ele.Description}
47 |
48 |
49 |
54 |
55 | );
56 | })}
57 |
58 |
59 |
60 | {/* Section 1 */}
61 |
62 |
10
63 |
64 | Years experiences
65 |
66 |
67 |
68 | {/* Section 2 */}
69 |
70 |
250
71 |
72 | types of courses
73 |
74 |
75 |
76 |
77 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default TimelineSection;
89 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{js,jsx,ts,tsx}"],
4 | theme: {
5 | fontFamily: {
6 | inter: ["Inter", "sans-serif"],
7 | "edu-sa": ["Edu SA Beginner", "cursive"],
8 | mono: ["Roboto Mono", "monospace"],
9 | },
10 | colors: {
11 | white: "#fff",
12 | black: "#000",
13 | transparent: "#ffffff00",
14 | richblack: {
15 | 5: "#F1F2FF",
16 | 25: "#DBDDEA",
17 | 50: "#C5C7D4",
18 | 100: "#AFB2BF",
19 | 200: "#999DAA",
20 | 300: "#838894",
21 | 400: "#6E727F",
22 | 500: "#585D69",
23 | 600: "#424854",
24 | 700: "#2C333F",
25 | 800: "#161D29",
26 | 900: "#000814",
27 | },
28 | richblue: {
29 | 5: "#ECF5FF",
30 | 25: "#C6D6E1",
31 | 50: "#A0B7C3",
32 | 100: "#7A98A6",
33 | 200: "#537988",
34 | 300: "#2D5A6A",
35 | 400: "#073B4C",
36 | 500: "#063544",
37 | 600: "#042E3B",
38 | 700: "#032833",
39 | 800: "#01212A",
40 | 900: "#001B22",
41 | },
42 | blue: {
43 | 5: "#EAF5FF",
44 | 25: "#B4DAEC",
45 | 50: "#7EC0D9",
46 | 100: "#47A5C5",
47 | 200: "#118AB2",
48 | 300: "#0F7A9D",
49 | 400: "#0C6A87",
50 | 500: "#0A5A72",
51 | 600: "#074B5D",
52 | 700: "#053B48",
53 | 800: "#022B32",
54 | 900: "#001B1D",
55 | },
56 | caribbeangreen: {
57 | 5: "#C1FFFD",
58 | 25: "#83F1DE",
59 | 50: "#44E4BF",
60 | 100: "#06D6A0",
61 | 200: "#05BF8E",
62 | 300: "#05A77B",
63 | 400: "#049069",
64 | 500: "#037957",
65 | 600: "#026144",
66 | 700: "#014A32",
67 | 800: "#01321F",
68 | 900: "#001B0D",
69 | },
70 | brown: {
71 | 5: "#FFF4C4",
72 | 25: "#FFE395",
73 | 50: "#FFD166",
74 | 100: "#E7BC5B",
75 | 200: "#CFA64F",
76 | 300: "#B89144",
77 | 400: "#A07C39",
78 | 500: "#88662D",
79 | 600: "#705122",
80 | 700: "#593C17",
81 | 800: "#41260B",
82 | 900: "#291100",
83 | },
84 | pink: {
85 | 5: "#FFF1F1",
86 | 25: "#FBC7D1",
87 | 50: "#F79CB0",
88 | 100: "#F37290",
89 | 200: "#EF476F",
90 | 300: "#D43D63",
91 | 400: "#BA3356",
92 | 500: "#9F294A",
93 | 600: "#841E3E",
94 | 700: "#691432",
95 | 800: "#4F0A25",
96 | 900: "#340019",
97 | },
98 | yellow: {
99 | 5: "#FFF970",
100 | 25: "#FFE83D",
101 | 50: "#FFD60A",
102 | 100: "#E7C009",
103 | 200: "#CFAB08",
104 | 300: "#B69507",
105 | 400: "#9E8006",
106 | 500: "#866A04",
107 | 600: "#6E5503",
108 | 700: "#553F02",
109 | 800: "#3D2A01",
110 | 900: "#251400",
111 | },
112 | "pure-greys": {
113 | 5: "#F9F9F9",
114 | 25: "#E2E2E2",
115 | 50: "#CCCCCC",
116 | 100: "#B5B5B5",
117 | 200: "#9E9E9E",
118 | 300: "#888888",
119 | 400: "#717171",
120 | 500: "#5B5B5B",
121 | 600: "#444444",
122 | 700: "#2D2D2D",
123 | 800: "#171717",
124 | 900: "#141414",
125 | },
126 | },
127 | extend: {
128 | maxWidth: {
129 | maxContent: "1260px",
130 | maxContentTab: "650px"
131 | },
132 | },
133 | },
134 | plugins: [],
135 | };
136 |
--------------------------------------------------------------------------------
/server/controllers/Category.js:
--------------------------------------------------------------------------------
1 | const Category = require("../models/Category")
2 |
3 | function getRandomInt(max) {
4 | return Math.floor(Math.random() * max)
5 | }
6 | exports.createCategory = async (req, res) => {
7 | try {
8 | const { name, description } = req.body
9 | if (!name) {
10 | return res
11 | .status(400)
12 | .json({ success: false, message: "All fields are required" })
13 | }
14 | const CategorysDetails = await Category.create({
15 | name: name,
16 | description: description,
17 | })
18 | console.log(CategorysDetails)
19 | return res.status(200).json({
20 | success: true,
21 | message: "Categorys Created Successfully",
22 | })
23 | } catch (error) {
24 | return res.status(500).json({
25 | success: true,
26 | message: error.message,
27 | })
28 | }
29 | }
30 |
31 | exports.showAllCategories = async (req, res) => {
32 | try {
33 | const allCategorys = await Category.find()
34 | res.status(200).json({
35 | success: true,
36 | data: allCategorys,
37 | })
38 | } catch (error) {
39 | return res.status(500).json({
40 | success: false,
41 | message: error.message,
42 | })
43 | }
44 | }
45 |
46 | exports.categoryPageDetails = async (req, res) => {
47 | try {
48 | const { categoryId } = req.body
49 |
50 | // Get courses for the specified category
51 | const selectedCategory = await Category.findById(categoryId)
52 | .populate({
53 | path: "courses",
54 | match: { status: "Published" },
55 | populate: "ratingAndReviews",
56 | })
57 | .exec()
58 |
59 | console.log("SELECTED COURSE", selectedCategory)
60 | // Handle the case when the category is not found
61 | if (!selectedCategory) {
62 | console.log("Category not found.")
63 | return res
64 | .status(404)
65 | .json({ success: false, message: "Category not found" })
66 | }
67 | // Handle the case when there are no courses
68 | if (selectedCategory.courses.length === 0) {
69 | console.log("No courses found for the selected category.")
70 | return res.status(404).json({
71 | success: false,
72 | message: "No courses found for the selected category.",
73 | })
74 | }
75 |
76 | // Get courses for other categories
77 | const categoriesExceptSelected = await Category.find({
78 | _id: { $ne: categoryId },
79 | })
80 | let differentCategory = await Category.findOne(
81 | categoriesExceptSelected[getRandomInt(categoriesExceptSelected.length)]
82 | ._id
83 | )
84 | .populate({
85 | path: "courses",
86 | match: { status: "Published" },
87 | })
88 | .exec()
89 | console.log()
90 | // Get top-selling courses across all categories
91 | const allCategories = await Category.find()
92 | .populate({
93 | path: "courses",
94 | match: { status: "Published" },
95 | })
96 | .exec()
97 | const allCourses = allCategories.flatMap((category) => category.courses)
98 | const mostSellingCourses = allCourses
99 | .sort((a, b) => b.sold - a.sold)
100 | .slice(0, 10)
101 |
102 | res.status(200).json({
103 | success: true,
104 | data: {
105 | selectedCategory,
106 | differentCategory,
107 | mostSellingCourses,
108 | },
109 | })
110 | } catch (error) {
111 | return res.status(500).json({
112 | success: false,
113 | message: "Internal server error",
114 | error: error.message,
115 | })
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/AddCourse/PublishCourse/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import { useForm } from "react-hook-form"
3 | import { useDispatch, useSelector } from "react-redux"
4 | import { useNavigate } from "react-router-dom"
5 |
6 | import { editCourseDetails } from "../../../../../services/operations/courseDetailsAPI"
7 | import { resetCourseState, setStep } from "../../../../../slices/courseSlice"
8 | import { COURSE_STATUS } from "../../../../../utils/constants"
9 | import IconBtn from "../../../../Common/IconBtn"
10 |
11 | export default function PublishCourse() {
12 | const { register, handleSubmit, setValue, getValues } = useForm()
13 |
14 | const dispatch = useDispatch()
15 | const navigate = useNavigate()
16 | const { token } = useSelector((state) => state.auth)
17 | const { course } = useSelector((state) => state.course)
18 | const [loading, setLoading] = useState(false)
19 |
20 | useEffect(() => {
21 | if (course?.status === COURSE_STATUS.PUBLISHED) {
22 | setValue("public", true)
23 | }
24 | }, [])
25 |
26 | const goBack = () => {
27 | dispatch(setStep(2))
28 | }
29 |
30 | const goToCourses = () => {
31 | dispatch(resetCourseState())
32 | navigate("/dashboard/my-courses")
33 | }
34 |
35 | const handleCoursePublish = async () => {
36 | // check if form has been updated or not
37 | if (
38 | (course?.status === COURSE_STATUS.PUBLISHED &&
39 | getValues("public") === true) ||
40 | (course?.status === COURSE_STATUS.DRAFT && getValues("public") === false)
41 | ) {
42 |
43 | goToCourses()
44 | return
45 | }
46 | const formData = new FormData()
47 | formData.append("courseId", course._id)
48 | const courseStatus = getValues("public")
49 | ? COURSE_STATUS.PUBLISHED
50 | : COURSE_STATUS.DRAFT
51 | formData.append("status", courseStatus)
52 | setLoading(true)
53 | const result = await editCourseDetails(formData, token)
54 | if (result) {
55 | goToCourses()
56 | }
57 | setLoading(false)
58 | }
59 |
60 | const onSubmit = (data) => {
61 | // console.log(data)
62 | handleCoursePublish()
63 | }
64 |
65 | return (
66 |
67 |
68 | Publish Settings
69 |
70 |
99 |
100 | )
101 | }
102 |
--------------------------------------------------------------------------------
/src/pages/VerifyEmail.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import OtpInput from "react-otp-input";
3 | import { Link } from "react-router-dom";
4 | import { BiArrowBack } from "react-icons/bi";
5 | import { RxCountdownTimer } from "react-icons/rx";
6 | import { useDispatch, useSelector } from "react-redux";
7 | import { sendOtp, signUp } from "../services/operations/authAPI";
8 | import { useNavigate } from "react-router-dom";
9 |
10 | function VerifyEmail() {
11 | const [otp, setOtp] = useState("");
12 | const { signupData, loading } = useSelector((state) => state.auth);
13 | const dispatch = useDispatch();
14 | const navigate = useNavigate();
15 |
16 | useEffect(() => {
17 | // Only allow access of this route when user has filled the signup form
18 | if (!signupData) {
19 | navigate("/signup");
20 | }
21 | // eslint-disable-next-line react-hooks/exhaustive-deps
22 | }, []);
23 |
24 | const handleVerifyAndSignup = (e) => {
25 | e.preventDefault();
26 | const {
27 | accountType,
28 | firstName,
29 | lastName,
30 | email,
31 | password,
32 | confirmPassword,
33 | } = signupData;
34 |
35 | dispatch(
36 | signUp(
37 | accountType,
38 | firstName,
39 | lastName,
40 | email,
41 | password,
42 | confirmPassword,
43 | otp,
44 | navigate
45 | )
46 | );
47 | };
48 |
49 | return (
50 |
51 | {loading ? (
52 |
55 | ) : (
56 |
57 |
58 | Verify Email
59 |
60 |
61 | A verification code has been sent to you. Enter the code below
62 |
63 |
90 |
91 |
92 |
93 | Back To Signup
94 |
95 |
96 |
dispatch(sendOtp(signupData.email))}
99 | >
100 |
101 | Resend it
102 |
103 |
104 |
105 | )}
106 |
107 | );
108 | }
109 |
110 | export default VerifyEmail;
111 |
--------------------------------------------------------------------------------
/server/controllers/Section.js:
--------------------------------------------------------------------------------
1 | const Section = require("../models/Section")
2 | const Course = require("../models/Course")
3 | const SubSection = require("../models/Subsection")
4 | // CREATE a new section
5 | exports.createSection = async (req, res) => {
6 | try {
7 | // Extract the required properties from the request body
8 | const { sectionName, courseId } = req.body
9 |
10 | // Validate the input
11 | if (!sectionName || !courseId) {
12 | return res.status(400).json({
13 | success: false,
14 | message: "Missing required properties",
15 | })
16 | }
17 |
18 | // Create a new section with the given name
19 | const newSection = await Section.create({ sectionName })
20 |
21 | // Add the new section to the course's content array
22 | const updatedCourse = await Course.findByIdAndUpdate(
23 | courseId,
24 | {
25 | $push: {
26 | courseContent: newSection._id,
27 | },
28 | },
29 | { new: true }
30 | )
31 | .populate({
32 | path: "courseContent",
33 | populate: {
34 | path: "subSection",
35 | },
36 | })
37 | .exec()
38 |
39 | // Return the updated course object in the response
40 | res.status(200).json({
41 | success: true,
42 | message: "Section created successfully",
43 | updatedCourse,
44 | })
45 | } catch (error) {
46 | // Handle errors
47 | res.status(500).json({
48 | success: false,
49 | message: "Internal server error",
50 | error: error.message,
51 | })
52 | }
53 | }
54 |
55 | // UPDATE a section
56 | exports.updateSection = async (req, res) => {
57 | try {
58 | const { sectionName, sectionId, courseId } = req.body
59 | const section = await Section.findByIdAndUpdate(
60 | sectionId,
61 | { sectionName },
62 | { new: true }
63 | )
64 | const course = await Course.findById(courseId)
65 | .populate({
66 | path: "courseContent",
67 | populate: {
68 | path: "subSection",
69 | },
70 | })
71 | .exec()
72 | console.log(course)
73 | res.status(200).json({
74 | success: true,
75 | message: section,
76 | data: course,
77 | })
78 | } catch (error) {
79 | console.error("Error updating section:", error)
80 | res.status(500).json({
81 | success: false,
82 | message: "Internal server error",
83 | error: error.message,
84 | })
85 | }
86 | }
87 |
88 | // DELETE a section
89 | exports.deleteSection = async (req, res) => {
90 | try {
91 | const { sectionId, courseId } = req.body
92 | await Course.findByIdAndUpdate(courseId, {
93 | $pull: {
94 | courseContent: sectionId,
95 | },
96 | })
97 | const section = await Section.findById(sectionId)
98 | console.log(sectionId, courseId)
99 | if (!section) {
100 | return res.status(404).json({
101 | success: false,
102 | message: "Section not found",
103 | })
104 | }
105 | // Delete the associated subsections
106 | await SubSection.deleteMany({ _id: { $in: section.subSection } })
107 |
108 | await Section.findByIdAndDelete(sectionId)
109 |
110 | // find the updated course and return it
111 | const course = await Course.findById(courseId)
112 | .populate({
113 | path: "courseContent",
114 | populate: {
115 | path: "subSection",
116 | },
117 | })
118 | .exec()
119 |
120 | res.status(200).json({
121 | success: true,
122 | message: "Section deleted",
123 | data: course,
124 | })
125 | } catch (error) {
126 | console.error("Error deleting section:", error)
127 | res.status(500).json({
128 | success: false,
129 | message: "Internal server error",
130 | error: error.message,
131 | })
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/AddCourse/CourseInformation/ChipInput.jsx:
--------------------------------------------------------------------------------
1 | // Importing React hook for managing component state
2 | import { useEffect, useState } from "react"
3 | // Importing React icon component
4 | import { MdClose } from "react-icons/md"
5 | import { useSelector } from "react-redux"
6 |
7 | // Defining a functional component ChipInput
8 | export default function ChipInput({
9 | // Props to be passed to the component
10 | label,
11 | name,
12 | placeholder,
13 | register,
14 | errors,
15 | setValue,
16 | getValues,
17 | }) {
18 | const { editCourse, course } = useSelector((state) => state.course)
19 |
20 | // Setting up state for managing chips array
21 | const [chips, setChips] = useState([])
22 |
23 | useEffect(() => {
24 | if (editCourse) {
25 | // console.log(course)
26 | setChips(course?.tag)
27 | }
28 | register(name, { required: true, validate: (value) => value.length > 0 })
29 | // eslint-disable-next-line react-hooks/exhaustive-deps
30 | }, [])
31 |
32 | useEffect(() => {
33 | setValue(name, chips)
34 | // eslint-disable-next-line react-hooks/exhaustive-deps
35 | }, [chips])
36 |
37 | // Function to handle user input when chips are added
38 | const handleKeyDown = (event) => {
39 | // Check if user presses "Enter" or ","
40 | if (event.key === "Enter" || event.key === ",") {
41 | // Prevent the default behavior of the event
42 | event.preventDefault()
43 | // Get the input value and remove any leading/trailing spaces
44 | const chipValue = event.target.value.trim()
45 | // Check if the input value exists and is not already in the chips array
46 | if (chipValue && !chips.includes(chipValue)) {
47 | // Add the chip to the array and clear the input
48 | const newChips = [...chips, chipValue]
49 | setChips(newChips)
50 | event.target.value = ""
51 | }
52 | }
53 | }
54 |
55 | // Function to handle deletion of a chip
56 | const handleDeleteChip = (chipIndex) => {
57 | // Filter the chips array to remove the chip with the given index
58 | const newChips = chips.filter((_, index) => index !== chipIndex)
59 | setChips(newChips)
60 | }
61 |
62 | // Render the component
63 | return (
64 |
65 | {/* Render the label for the input */}
66 |
67 | {label} *
68 |
69 | {/* Render the chips and input */}
70 |
71 | {/* Map over the chips array and render each chip */}
72 | {chips.map((chip, index) => (
73 |
77 | {/* Render the chip value */}
78 | {chip}
79 | {/* Render the button to delete the chip */}
80 | handleDeleteChip(index)}
84 | >
85 |
86 |
87 |
88 | ))}
89 | {/* Render the input for adding new chips */}
90 |
98 |
99 | {/* Render an error message if the input is required and not filled */}
100 | {errors[name] && (
101 |
102 | {label} is required
103 |
104 | )}
105 |
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/src/services/operations/SettingsAPI.js:
--------------------------------------------------------------------------------
1 | import { toast } from "react-hot-toast"
2 |
3 | import { setUser } from "../../slices/profileSlice"
4 | import { apiConnector } from "../apiConnector"
5 | import { settingsEndpoints } from "../apis"
6 | import { logout } from "./authAPI"
7 |
8 | const {
9 | UPDATE_DISPLAY_PICTURE_API,
10 | UPDATE_PROFILE_API,
11 | CHANGE_PASSWORD_API,
12 | DELETE_PROFILE_API,
13 | } = settingsEndpoints
14 |
15 | export function updateDisplayPicture(token, formData) {
16 | return async (dispatch) => {
17 | const toastId = toast.loading("Loading...")
18 | try {
19 | const response = await apiConnector(
20 | "PUT",
21 | UPDATE_DISPLAY_PICTURE_API,
22 | formData,
23 | {
24 | "Content-Type": "multipart/form-data",
25 | Authorization: `Bearer ${token}`,
26 | }
27 | )
28 | console.log(
29 | "UPDATE_DISPLAY_PICTURE_API API RESPONSE............",
30 | response
31 | )
32 |
33 | if (!response.data.success) {
34 | throw new Error(response.data.message)
35 | }
36 | toast.success("Display Picture Updated Successfully")
37 | dispatch(setUser(response.data.data))
38 | } catch (error) {
39 | console.log("UPDATE_DISPLAY_PICTURE_API API ERROR............", error)
40 | toast.error("Could Not Update Display Picture")
41 | }
42 | toast.dismiss(toastId)
43 | }
44 | }
45 |
46 | export function updateProfile(token, formData) {
47 | return async (dispatch) => {
48 | const toastId = toast.loading("Loading...")
49 | try {
50 | const response = await apiConnector("PUT", UPDATE_PROFILE_API, formData, {
51 | Authorization: `Bearer ${token}`,
52 | })
53 | console.log("UPDATE_PROFILE_API API RESPONSE............", response)
54 |
55 | if (!response.data.success) {
56 | throw new Error(response.data.message)
57 | }
58 | const userImage = response.data.updatedUserDetails.image
59 | ? response.data.updatedUserDetails.image
60 | : `https://api.dicebear.com/5.x/initials/svg?seed=${response.data.updatedUserDetails.firstName} ${response.data.updatedUserDetails.lastName}`
61 | dispatch(
62 | setUser({ ...response.data.updatedUserDetails, image: userImage })
63 | )
64 | toast.success("Profile Updated Successfully")
65 | } catch (error) {
66 | console.log("UPDATE_PROFILE_API API ERROR............", error)
67 | toast.error("Could Not Update Profile")
68 | }
69 | toast.dismiss(toastId)
70 | }
71 | }
72 |
73 | export async function changePassword(token, formData) {
74 | const toastId = toast.loading("Loading...")
75 | try {
76 | const response = await apiConnector("POST", CHANGE_PASSWORD_API, formData, {
77 | Authorization: `Bearer ${token}`,
78 | })
79 | console.log("CHANGE_PASSWORD_API API RESPONSE............", response)
80 |
81 | if (!response.data.success) {
82 | throw new Error(response.data.message)
83 | }
84 | toast.success("Password Changed Successfully")
85 | } catch (error) {
86 | console.log("CHANGE_PASSWORD_API API ERROR............", error)
87 | toast.error(error.response.data.message)
88 | }
89 | toast.dismiss(toastId)
90 | }
91 |
92 | export function deleteProfile(token, navigate) {
93 | return async (dispatch) => {
94 | const toastId = toast.loading("Loading...")
95 | try {
96 | const response = await apiConnector("DELETE", DELETE_PROFILE_API, null, {
97 | Authorization: `Bearer ${token}`,
98 | })
99 | console.log("DELETE_PROFILE_API API RESPONSE............", response)
100 |
101 | if (!response.data.success) {
102 | throw new Error(response.data.message)
103 | }
104 | toast.success("Profile Deleted Successfully")
105 | dispatch(logout(navigate))
106 | } catch (error) {
107 | console.log("DELETE_PROFILE_API API ERROR............", error)
108 | toast.error("Could Not Delete Profile")
109 | }
110 | toast.dismiss(toastId)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/components/Common/ReviewSlider.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react"
2 | import ReactStars from "react-rating-stars-component"
3 | // Import Swiper React components
4 | import { Swiper, SwiperSlide } from "swiper/react"
5 |
6 | // Import Swiper styles
7 | import "swiper/css"
8 | import "swiper/css/free-mode"
9 | import "swiper/css/pagination"
10 | import "../../App.css"
11 | // Icons
12 | import { FaStar } from "react-icons/fa"
13 | // Import required modules
14 | import { Autoplay, FreeMode, Pagination } from "swiper"
15 |
16 | // Get apiFunction and the endpoint
17 | import { apiConnector } from "../../services/apiConnector"
18 | import { ratingsEndpoints } from "../../services/apis"
19 |
20 | function ReviewSlider() {
21 | const [reviews, setReviews] = useState([])
22 | const truncateWords = 15
23 |
24 | useEffect(() => {
25 | ;(async () => {
26 | const { data } = await apiConnector(
27 | "GET",
28 | ratingsEndpoints.REVIEWS_DETAILS_API
29 | )
30 | if (data?.success) {
31 | setReviews(data?.data)
32 | }
33 | })()
34 | }, [])
35 |
36 | // console.log(reviews)
37 |
38 | return (
39 |
40 |
41 |
53 | {reviews.map((review, i) => {
54 | return (
55 |
56 |
57 |
58 |
67 |
68 |
{`${review?.user?.firstName} ${review?.user?.lastName}`}
69 |
70 | {review?.course?.courseName}
71 |
72 |
73 |
74 |
75 | {review?.review.split(" ").length > truncateWords
76 | ? `${review?.review
77 | .split(" ")
78 | .slice(0, truncateWords)
79 | .join(" ")} ...`
80 | : `${review?.review}`}
81 |
82 |
83 |
84 | {review.rating.toFixed(1)}
85 |
86 | }
93 | fullIcon={ }
94 | />
95 |
96 |
97 |
98 | )
99 | })}
100 |
101 |
102 |
103 |
104 | )
105 | }
106 |
107 | export default ReviewSlider
108 |
--------------------------------------------------------------------------------
/server/controllers/RatingandReview.js:
--------------------------------------------------------------------------------
1 | const RatingAndReview = require("../models/RatingandReview")
2 | const Course = require("../models/Course")
3 | const mongoose = require("mongoose")
4 |
5 | // Create a new rating and review
6 | exports.createRating = async (req, res) => {
7 | try {
8 | const userId = req.user.id
9 | const { rating, review, courseId } = req.body
10 |
11 | // Check if the user is enrolled in the course
12 |
13 | const courseDetails = await Course.findOne({
14 | _id: courseId,
15 | studentsEnroled: { $elemMatch: { $eq: userId } },
16 | })
17 |
18 | if (!courseDetails) {
19 | return res.status(404).json({
20 | success: false,
21 | message: "Student is not enrolled in this course",
22 | })
23 | }
24 |
25 | // Check if the user has already reviewed the course
26 | const alreadyReviewed = await RatingAndReview.findOne({
27 | user: userId,
28 | course: courseId,
29 | })
30 |
31 | if (alreadyReviewed) {
32 | return res.status(403).json({
33 | success: false,
34 | message: "Course already reviewed by user",
35 | })
36 | }
37 |
38 | // Create a new rating and review
39 | const ratingReview = await RatingAndReview.create({
40 | rating,
41 | review,
42 | course: courseId,
43 | user: userId,
44 | })
45 |
46 | // Add the rating and review to the course
47 | await Course.findByIdAndUpdate(courseId, {
48 | $push: {
49 | ratingAndReviews: ratingReview,
50 | },
51 | })
52 | await courseDetails.save()
53 |
54 | return res.status(201).json({
55 | success: true,
56 | message: "Rating and review created successfully",
57 | ratingReview,
58 | })
59 | } catch (error) {
60 | console.error(error)
61 | return res.status(500).json({
62 | success: false,
63 | message: "Internal server error",
64 | error: error.message,
65 | })
66 | }
67 | }
68 |
69 | // Get the average rating for a course
70 | exports.getAverageRating = async (req, res) => {
71 | try {
72 | const courseId = req.body.courseId
73 |
74 | // Calculate the average rating using the MongoDB aggregation pipeline
75 | const result = await RatingAndReview.aggregate([
76 | {
77 | $match: {
78 | course: new mongoose.Types.ObjectId(courseId), // Convert courseId to ObjectId
79 | },
80 | },
81 | {
82 | $group: {
83 | _id: null,
84 | averageRating: { $avg: "$rating" },
85 | },
86 | },
87 | ])
88 |
89 | if (result.length > 0) {
90 | return res.status(200).json({
91 | success: true,
92 | averageRating: result[0].averageRating,
93 | })
94 | }
95 |
96 | // If no ratings are found, return 0 as the default rating
97 | return res.status(200).json({ success: true, averageRating: 0 })
98 | } catch (error) {
99 | console.error(error)
100 | return res.status(500).json({
101 | success: false,
102 | message: "Failed to retrieve the rating for the course",
103 | error: error.message,
104 | })
105 | }
106 | }
107 |
108 | // Get all rating and reviews
109 | exports.getAllRatingReview = async (req, res) => {
110 | try {
111 | const allReviews = await RatingAndReview.find({})
112 | .sort({ rating: "desc" })
113 | .populate({
114 | path: "user",
115 | select: "firstName lastName email image", // Specify the fields you want to populate from the "Profile" model
116 | })
117 | .populate({
118 | path: "course",
119 | select: "courseName", //Specify the fields you want to populate from the "Course" model
120 | })
121 | .exec()
122 |
123 | res.status(200).json({
124 | success: true,
125 | data: allReviews,
126 | })
127 | } catch (error) {
128 | console.error(error)
129 | return res.status(500).json({
130 | success: false,
131 | message: "Failed to retrieve the rating and review for the course",
132 | error: error.message,
133 | })
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/EnrolledCourses.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 | import ProgressBar from "@ramonak/react-progress-bar"
3 | import { BiDotsVerticalRounded } from "react-icons/bi"
4 | import { useSelector } from "react-redux"
5 | import { useNavigate } from "react-router-dom"
6 |
7 | import { getUserEnrolledCourses } from "../../../services/operations/profileAPI"
8 |
9 | export default function EnrolledCourses() {
10 | const { token } = useSelector((state) => state.auth)
11 | const navigate = useNavigate()
12 |
13 | const [enrolledCourses, setEnrolledCourses] = useState(null)
14 |
15 | useEffect(() => {
16 | ;(async () => {
17 | try {
18 | const res = await getUserEnrolledCourses(token) // Getting all the published and the drafted courses
19 |
20 | // Filtering the published course out
21 | const filterPublishCourse = res.filter((ele) => ele.status !== "Draft")
22 | // console.log(
23 | // "Viewing all the couse that is Published",
24 | // filterPublishCourse
25 | // )
26 |
27 | setEnrolledCourses(filterPublishCourse)
28 | } catch (error) {
29 | console.log("Could not fetch enrolled courses.")
30 | }
31 | })()
32 | // eslint-disable-next-line react-hooks/exhaustive-deps
33 | }, [])
34 |
35 | return (
36 | <>
37 | Enrolled Courses
38 | {!enrolledCourses ? (
39 |
42 | ) : !enrolledCourses.length ? (
43 |
44 | You have not enrolled in any course yet.
45 | {/* TODO: Modify this Empty State */}
46 |
47 | ) : (
48 |
49 | {/* Headings */}
50 |
51 |
Course Name
52 |
Duration
53 |
Progress
54 |
55 | {/* Course Names */}
56 | {enrolledCourses.map((course, i, arr) => (
57 |
63 |
{
66 | navigate(
67 | `/view-course/${course?._id}/section/${course.courseContent?.[0]?._id}/sub-section/${course.courseContent?.[0]?.subSection?.[0]?._id}`
68 | )
69 | }}
70 | >
71 |
76 |
77 |
{course.courseName}
78 |
79 | {course.courseDescription.length > 50
80 | ? `${course.courseDescription.slice(0, 50)}...`
81 | : course.courseDescription}
82 |
83 |
84 |
85 |
{course?.totalDuration}
86 |
87 |
Progress: {course.progressPercentage || 0}%
88 |
93 |
94 |
95 | ))}
96 |
97 | )}
98 | >
99 | )
100 | }
101 |
--------------------------------------------------------------------------------
/server/routes/Course.js:
--------------------------------------------------------------------------------
1 | // Import the required modules
2 | const express = require("express")
3 | const router = express.Router()
4 |
5 | // Import the Controllers
6 |
7 | // Course Controllers Import
8 | const {
9 | createCourse,
10 | getAllCourses,
11 | getCourseDetails,
12 | getFullCourseDetails,
13 | editCourse,
14 | getInstructorCourses,
15 | deleteCourse,
16 | } = require("../controllers/Course")
17 |
18 | // Tags Controllers Import
19 |
20 | // Categories Controllers Import
21 | const {
22 | showAllCategories,
23 | createCategory,
24 | categoryPageDetails,
25 | } = require("../controllers/Category")
26 |
27 | // Sections Controllers Import
28 | const {
29 | createSection,
30 | updateSection,
31 | deleteSection,
32 | } = require("../controllers/Section")
33 |
34 | // Sub-Sections Controllers Import
35 | const {
36 | createSubSection,
37 | updateSubSection,
38 | deleteSubSection,
39 | } = require("../controllers/Subsection")
40 |
41 | // Rating Controllers Import
42 | const {
43 | createRating,
44 | getAverageRating,
45 | getAllRatingReview,
46 | } = require("../controllers/RatingandReview")
47 | const {
48 | updateCourseProgress,
49 | getProgressPercentage,
50 | } = require("../controllers/courseProgress")
51 | // Importing Middlewares
52 | const { auth, isInstructor, isStudent, isAdmin } = require("../middleware/auth")
53 |
54 | // ********************************************************************************************************
55 | // Course routes
56 | // ********************************************************************************************************
57 |
58 | // Courses can Only be Created by Instructors
59 | router.post("/createCourse", auth, isInstructor, createCourse)
60 | // Edit Course routes
61 | router.post("/editCourse", auth, isInstructor, editCourse)
62 | //Add a Section to a Course
63 | router.post("/addSection", auth, isInstructor, createSection)
64 | // Update a Section
65 | router.post("/updateSection", auth, isInstructor, updateSection)
66 | // Delete a Section
67 | router.post("/deleteSection", auth, isInstructor, deleteSection)
68 | // Edit Sub Section
69 | router.post("/updateSubSection", auth, isInstructor, updateSubSection)
70 | // Delete Sub Section
71 | router.post("/deleteSubSection", auth, isInstructor, deleteSubSection)
72 | // Add a Sub Section to a Section
73 | router.post("/addSubSection", auth, isInstructor, createSubSection)
74 | // Get all Courses Under a Specific Instructor
75 | router.get("/getInstructorCourses", auth, isInstructor, getInstructorCourses)
76 | // Get all Registered Courses
77 | router.get("/getAllCourses", getAllCourses)
78 | // Get Details for a Specific Courses
79 | router.post("/getCourseDetails", getCourseDetails)
80 | // Get Details for a Specific Courses
81 | router.post("/getFullCourseDetails", auth, getFullCourseDetails)
82 | // To Update Course Progress
83 | router.post("/updateCourseProgress", auth, isStudent, updateCourseProgress)
84 | // To get Course Progress
85 | // router.post("/getProgressPercentage", auth, isStudent, getProgressPercentage)
86 | // Delete a Course
87 | router.delete("/deleteCourse", deleteCourse)
88 |
89 | // ********************************************************************************************************
90 | // Category routes (Only by Admin)
91 | // ********************************************************************************************************
92 | // Category can Only be Created by Admin
93 | // TODO: Put IsAdmin Middleware here
94 | router.post("/createCategory", auth, isAdmin, createCategory)
95 | router.get("/showAllCategories", showAllCategories)
96 | router.post("/getCategoryPageDetails", categoryPageDetails)
97 |
98 | // ********************************************************************************************************
99 | // Rating and Review
100 | // ********************************************************************************************************
101 | router.post("/createRating", auth, isStudent, createRating)
102 | router.get("/getAverageRating", getAverageRating)
103 | router.get("/getReviews", getAllRatingReview)
104 |
105 | module.exports = router
106 |
--------------------------------------------------------------------------------
/src/components/core/Course/CourseDetailsCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import copy from "copy-to-clipboard"
3 | import { toast } from "react-hot-toast"
4 | import { BsFillCaretRightFill } from "react-icons/bs"
5 | import { FaShareSquare } from "react-icons/fa"
6 | import { useDispatch, useSelector } from "react-redux"
7 | import { useNavigate } from "react-router-dom"
8 |
9 | import { addToCart } from "../../../slices/cartSlice"
10 | import { ACCOUNT_TYPE } from "../../../utils/constants"
11 |
12 |
13 | function CourseDetailsCard({ course, setConfirmationModal, handleBuyCourse }) {
14 | const { user } = useSelector((state) => state.profile)
15 | const { token } = useSelector((state) => state.auth)
16 | const navigate = useNavigate()
17 | const dispatch = useDispatch()
18 |
19 | const {
20 | thumbnail: ThumbnailImage,
21 | price: CurrentPrice,
22 | _id: courseId,
23 | } = course
24 |
25 | const handleShare = () => {
26 | copy(window.location.href)
27 | toast.success("Link copied to clipboard")
28 | }
29 |
30 | const handleAddToCart = () => {
31 | if (user && user?.accountType === ACCOUNT_TYPE.INSTRUCTOR) {
32 | toast.error("You are an Instructor. You can't buy a course.")
33 | return
34 | }
35 | if (token) {
36 | dispatch(addToCart(course))
37 | return
38 | }
39 | setConfirmationModal({
40 | text1: "You are not logged in!",
41 | text2: "Please login to add To Cart",
42 | btn1Text: "Login",
43 | btn2Text: "Cancel",
44 | btn1Handler: () => navigate("/login"),
45 | btn2Handler: () => setConfirmationModal(null),
46 | })
47 | }
48 |
49 | // console.log("Student already enrolled ", course?.studentsEnroled, user?._id)
50 |
51 | return (
52 | <>
53 |
56 | {/* Course Image */}
57 |
62 |
63 |
64 |
65 | Rs. {CurrentPrice}
66 |
67 |
68 | navigate("/dashboard/enrolled-courses")
73 | : handleBuyCourse
74 | }
75 | >
76 | {user && course?.studentsEnroled.includes(user?._id)
77 | ? "Go To Course"
78 | : "Buy Now"}
79 |
80 | {(!user || !course?.studentsEnroled.includes(user?._id)) && (
81 |
82 | Add to Cart
83 |
84 | )}
85 |
86 |
87 |
88 | 30-Day Money-Back Guarantee
89 |
90 |
91 |
92 |
93 |
94 | This Course Includes :
95 |
96 |
97 | {course?.instructions?.map((item, i) => {
98 | return (
99 |
100 |
101 | {item}
102 |
103 | )
104 | })}
105 |
106 |
107 |
108 |
112 | Share
113 |
114 |
115 |
116 |
117 | >
118 | )
119 | }
120 |
121 | export default CourseDetailsCard
122 |
--------------------------------------------------------------------------------
/src/components/core/Dashboard/AddCourse/Upload.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from "react"
2 | import { useDropzone } from "react-dropzone"
3 | import { FiUploadCloud } from "react-icons/fi"
4 | import { useSelector } from "react-redux"
5 |
6 | import "video-react/dist/video-react.css"
7 | import { Player } from "video-react"
8 |
9 | export default function Upload({
10 | name,
11 | label,
12 | register,
13 | setValue,
14 | errors,
15 | video = false,
16 | viewData = null,
17 | editData = null,
18 | }) {
19 | const { course } = useSelector((state) => state.course)
20 | const [selectedFile, setSelectedFile] = useState(null)
21 | const [previewSource, setPreviewSource] = useState(
22 | viewData ? viewData : editData ? editData : ""
23 | )
24 | const inputRef = useRef(null)
25 |
26 | const onDrop = (acceptedFiles) => {
27 | const file = acceptedFiles[0]
28 | if (file) {
29 | previewFile(file)
30 | setSelectedFile(file)
31 | }
32 | }
33 |
34 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
35 | accept: !video
36 | ? { "image/*": [".jpeg", ".jpg", ".png"] }
37 | : { "video/*": [".mp4"] },
38 | onDrop,
39 | })
40 |
41 | const previewFile = (file) => {
42 | // console.log(file)
43 | const reader = new FileReader()
44 | reader.readAsDataURL(file)
45 | reader.onloadend = () => {
46 | setPreviewSource(reader.result)
47 | }
48 | }
49 |
50 | useEffect(() => {
51 | register(name, { required: true })
52 | // eslint-disable-next-line react-hooks/exhaustive-deps
53 | }, [register])
54 |
55 | useEffect(() => {
56 | setValue(name, selectedFile)
57 | // eslint-disable-next-line react-hooks/exhaustive-deps
58 | }, [selectedFile, setValue])
59 |
60 | return (
61 |
62 |
63 | {label} {!viewData && * }
64 |
65 |
70 | {previewSource ? (
71 |
72 | {!video ? (
73 |
78 | ) : (
79 |
80 | )}
81 | {!viewData && (
82 |
{
85 | setPreviewSource("")
86 | setSelectedFile(null)
87 | setValue(name, null)
88 | }}
89 | className="mt-3 text-richblack-400 underline"
90 | >
91 | Cancel
92 |
93 | )}
94 |
95 | ) : (
96 |
100 |
101 |
102 |
103 |
104 |
105 | Drag and drop an {!video ? "image" : "video"}, or click to{" "}
106 | Browse a
107 | file
108 |
109 |
110 | Aspect ratio 16:9
111 | Recommended size 1024x576
112 |
113 |
114 | )}
115 |
116 | {errors[name] && (
117 |
118 | {label} is required
119 |
120 | )}
121 |
122 | )
123 | }
124 |
--------------------------------------------------------------------------------