├── client ├── .eslintrc.json ├── .DS_Store ├── app │ ├── .DS_Store │ ├── favicon.ico │ ├── admin │ │ ├── .DS_Store │ │ ├── page.tsx │ │ ├── invoices │ │ │ └── page.tsx │ │ ├── create-course │ │ │ └── page.tsx │ │ ├── users-analytics │ │ │ └── page.tsx │ │ ├── courses-analytics │ │ │ └── page.tsx │ │ ├── orders-analytics │ │ │ └── page.tsx │ │ ├── edit-course │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── users │ │ │ └── page.tsx │ │ ├── courses │ │ │ └── page.tsx │ │ ├── hero │ │ │ └── page.tsx │ │ ├── team │ │ │ └── page.tsx │ │ ├── categories │ │ │ └── page.tsx │ │ └── faq │ │ │ └── page.tsx │ ├── components │ │ ├── .DS_Store │ │ ├── Loader │ │ │ ├── Loader.tsx │ │ │ └── Loader.css │ │ ├── Admin │ │ │ ├── DashboardHero.tsx │ │ │ ├── sidebar │ │ │ │ └── Icon.tsx │ │ │ ├── Course │ │ │ │ ├── CourseOptions.tsx │ │ │ │ └── CourseData.tsx │ │ │ ├── Analytics │ │ │ │ ├── CourseAnalytics.tsx │ │ │ │ ├── UserAnalytics.tsx │ │ │ │ └── OrdersAnalytics.tsx │ │ │ ├── DashboardHeader.tsx │ │ │ └── Customization │ │ │ │ ├── EditCategories.tsx │ │ │ │ └── EditHero.tsx │ │ ├── Route │ │ │ ├── Courses.tsx │ │ │ └── Hero.tsx │ │ ├── Review │ │ │ └── ReviewCard.tsx │ │ ├── Course │ │ │ ├── CourseContent.tsx │ │ │ ├── CourseCard.tsx │ │ │ ├── CourseDetailsPage.tsx │ │ │ └── CourseContentList.tsx │ │ ├── FAQ │ │ │ └── FAQ.tsx │ │ ├── Payment │ │ │ └── CheckOutForm.tsx │ │ ├── Profile │ │ │ ├── SideBarProfile.tsx │ │ │ ├── Profile.tsx │ │ │ ├── ChangePassword.tsx │ │ │ └── ProfileInfo.tsx │ │ ├── Auth │ │ │ ├── Verification.tsx │ │ │ └── Login.tsx │ │ └── Footer.tsx │ ├── hooks │ │ ├── userAuth.tsx │ │ ├── useProtected.tsx │ │ └── adminProtected.tsx │ ├── Provider.tsx │ ├── course │ │ └── [id] │ │ │ └── page.tsx │ ├── utils │ │ ├── theme-provider.tsx │ │ ├── Heading.tsx │ │ ├── ThemeSwitcher.tsx │ │ ├── CustomModal.tsx │ │ ├── Ratings.tsx │ │ ├── CoursePlayer.tsx │ │ └── NavItems.tsx │ ├── styles │ │ └── style.ts │ ├── about │ │ ├── page.tsx │ │ └── About.tsx │ ├── policy │ │ ├── page.tsx │ │ └── Policy.tsx │ ├── faq │ │ └── page.tsx │ ├── course-access │ │ └── [id] │ │ │ └── page.tsx │ ├── page.tsx │ ├── profile │ │ └── page.tsx │ ├── layout.tsx │ ├── globals.css │ └── courses │ │ └── page.tsx ├── public │ ├── .DS_Store │ ├── assests │ │ ├── .DS_Store │ │ ├── avatar.png │ │ ├── client-1.jpg │ │ ├── client-2.jpg │ │ ├── client-3.jpg │ │ ├── banner-img-1.png │ │ └── business-img.png │ ├── vercel.svg │ └── next.svg ├── redux │ ├── .DS_Store │ └── features │ │ ├── .DS_Store │ │ ├── store.ts │ │ ├── notifications │ │ └── notificationsApi.ts │ │ ├── auth │ │ ├── authSlice.ts │ │ └── authApi.ts │ │ ├── layout │ │ └── layoutApi.ts │ │ ├── analytics │ │ └── analyticsApi.ts │ │ ├── api │ │ └── apiSlice.ts │ │ ├── orders │ │ └── ordersApi.ts │ │ ├── user │ │ └── userApi.ts │ │ └── courses │ │ └── coursesApi.ts ├── postcss.config.js ├── pages │ ├── _app.tsx │ └── api │ │ └── auth │ │ └── [...nextauth].ts ├── next-env.d.ts ├── .env.example ├── next.config.js ├── .gitignore ├── tsconfig.json ├── tailwind.config.ts ├── package.json └── README.md ├── .DS_Store ├── @types └── custom.d.ts ├── middleware ├── catchAsyncErrors.ts ├── catchAsynchError.ts ├── error.ts └── auth.ts ├── util ├── ErrorHandler.ts ├── redis.ts └── db.ts ├── utils ├── ErrorHandler.ts ├── redis.ts ├── db.ts ├── analytics.generator.ts ├── sendMail.ts └── jwt.ts ├── .gitignore ├── routes ├── layout.route.ts ├── notification.route.ts ├── analytics.route.ts ├── order.route.ts ├── user.route.ts └── course.route.ts ├── mails ├── question-reply.ejs └── activation-mail.ejs ├── models ├── order.Model.ts ├── notification.Model.ts ├── layout.model.ts ├── user.model.ts └── course.model.ts ├── socketServer.ts ├── .env.example ├── services ├── course.service.ts ├── order.service.ts └── user.service.ts ├── package.json ├── controllers ├── analytics.controller.ts └── notification.controller.ts ├── app.ts ├── server.ts └── README.md /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/.DS_Store -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/.DS_Store -------------------------------------------------------------------------------- /client/app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/app/.DS_Store -------------------------------------------------------------------------------- /client/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/app/favicon.ico -------------------------------------------------------------------------------- /client/public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/.DS_Store -------------------------------------------------------------------------------- /client/redux/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/redux/.DS_Store -------------------------------------------------------------------------------- /client/app/admin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/app/admin/.DS_Store -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/app/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/app/components/.DS_Store -------------------------------------------------------------------------------- /client/public/assests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/assests/.DS_Store -------------------------------------------------------------------------------- /client/public/assests/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/assests/avatar.png -------------------------------------------------------------------------------- /client/redux/features/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/redux/features/.DS_Store -------------------------------------------------------------------------------- /client/public/assests/client-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/assests/client-1.jpg -------------------------------------------------------------------------------- /client/public/assests/client-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/assests/client-2.jpg -------------------------------------------------------------------------------- /client/public/assests/client-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/assests/client-3.jpg -------------------------------------------------------------------------------- /client/public/assests/banner-img-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/assests/banner-img-1.png -------------------------------------------------------------------------------- /client/public/assests/business-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lathiya50/Learning-Management-system/HEAD/client/public/assests/business-img.png -------------------------------------------------------------------------------- /client/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | 3 | export default function MyApp({ Component, pageProps }: AppProps) { 4 | return 5 | } -------------------------------------------------------------------------------- /@types/custom.d.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | import { IUser } from "../models/user.model"; 3 | 4 | declare global { 5 | namespace Express{ 6 | interface Request{ 7 | user?: IUser 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /middleware/catchAsyncErrors.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | export const CatchAsyncError = 4 | (theFunc: any) => (req: Request, res: Response, next: NextFunction) => { 5 | Promise.resolve(theFunc(req, res, next)).catch(next); 6 | }; 7 | -------------------------------------------------------------------------------- /client/app/hooks/userAuth.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | 3 | export default function UserAuth() { 4 | const { user } = useSelector((state: any) => state.auth); 5 | 6 | if (user) { 7 | return true; 8 | } else { 9 | return false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /middleware/catchAsynchError.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | const CatchAsyncError= (theFunc: any) => (req:Request, res:Response, next:NextFunction) => { 4 | Promise.resolve(theFunc(req, res, next)).catch(next); 5 | } 6 | export default CatchAsyncError; -------------------------------------------------------------------------------- /client/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /util/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | class ErrorHandler extends Error{ 2 | statusCode: Number; 3 | constructor(message: any, statusCode: Number) { 4 | super(message); 5 | this.statusCode = statusCode; 6 | Error.captureStackTrace(this, this.constructor); 7 | } 8 | } 9 | export default ErrorHandler; -------------------------------------------------------------------------------- /client/app/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Loader.css"; 3 | 4 | const Loader = () => { 5 | return ( 6 |
7 |
8 |
9 | ); 10 | }; 11 | 12 | export default Loader; 13 | -------------------------------------------------------------------------------- /utils/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | class ErrorHandler extends Error { 2 | statusCode: Number; 3 | 4 | constructor(message:any, statusCode:Number){ 5 | super(message); 6 | this.statusCode = statusCode; 7 | 8 | Error.captureStackTrace(this,this.constructor); 9 | } 10 | } 11 | 12 | export default ErrorHandler; -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SERVER_URI = "your_server_uri" 2 | NEXT_PUBLIC_SOCKET_SERVER_URI = "your_socket_server_uri" 3 | GITHUB_CLIENT_ID = your_github_client_id 4 | GITHUB_CLIENT_SECRET = your_github_client_secret 5 | GOOGLE_CLIENT_ID = your_google_client_id 6 | GOOGLE_CLIENT_SECRET = your_google_client_secret 7 | SECRET = your_secret_key 8 | -------------------------------------------------------------------------------- /client/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ['res.cloudinary.com','randomuser.me'], 5 | }, 6 | experimental:{ 7 | reactRoot: true, 8 | suppressHydrationWarning: true, 9 | } 10 | } 11 | 12 | module.exports = nextConfig 13 | -------------------------------------------------------------------------------- /client/app/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { store } from "../redux/features/store"; 4 | 5 | interface ProvidersProps { 6 | children: any; 7 | } 8 | 9 | export function Providers({ children }: ProvidersProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /util/redis.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from "ioredis"; 2 | require("dotenv").config(); 3 | 4 | const redisClient = () => { 5 | if (process.env.REDIS_URL) { 6 | console.log("Redis connected"); 7 | return process.env.REDIS_URL; 8 | } 9 | throw new Error(" Redis not connected"); 10 | } 11 | export const redis = new Redis(redisClient()); -------------------------------------------------------------------------------- /client/app/course/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from "react"; 3 | import CourseDetailsPage from "../../components/Course/CourseDetailsPage"; 4 | 5 | 6 | const Page = ({params}:any) => { 7 | return ( 8 |
9 | 10 |
11 | ) 12 | } 13 | 14 | export default Page; 15 | -------------------------------------------------------------------------------- /utils/redis.ts: -------------------------------------------------------------------------------- 1 | import {Redis} from 'ioredis'; 2 | require('dotenv').config(); 3 | 4 | const redisClient = () => { 5 | if(process.env.REDIS_URL){ 6 | console.log(`Redis connected`); 7 | return process.env.REDIS_URL; 8 | } 9 | throw new Error('Redis connection failed'); 10 | }; 11 | 12 | export const redis = new Redis(redisClient()); 13 | -------------------------------------------------------------------------------- /client/app/components/Loader/Loader.css: -------------------------------------------------------------------------------- 1 | .loader { 2 | border: 4px solid #1494d3; 3 | border-top: 4px solid #1f2937; 4 | border-radius: 50%; 5 | width: 48px; 6 | height: 48px; 7 | animation: spin 1s linear infinite; 8 | } 9 | 10 | @keyframes spin { 11 | 0% { transform: rotate(0deg); } 12 | 100% { transform: rotate(360deg); } 13 | } 14 | -------------------------------------------------------------------------------- /client/app/utils/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import * as React from 'react' 3 | import {ThemeProvider as NextThemesProvider} from "next-themes"; 4 | import type { ThemeProviderProps } from 'next-themes/dist/types'; 5 | 6 | export function ThemeProvider({children, ...props} : ThemeProviderProps){ 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /client/app/hooks/useProtected.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import UserAuth from "./userAuth"; 3 | import React from "react"; 4 | 5 | interface ProtectedProps{ 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function Protected({children}: ProtectedProps){ 10 | const isAuthenticated = UserAuth(); 11 | 12 | return isAuthenticated ? children : redirect("/"); 13 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | pnpm-debug.log* 10 | lerna-debug.log* 11 | yarn.lock 12 | .env 13 | 14 | node_modules 15 | dist 16 | dist-ssr 17 | *.local 18 | 19 | # Editor directories and files 20 | .vscode/* 21 | !.vscode/extensions.json 22 | .idea 23 | .DS_Store 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | client/.next 2 | client/node_modules 3 | 4 | node_modules 5 | .next 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | lerna-debug.log* 14 | yarn.lock 15 | .env 16 | 17 | node_modules 18 | dist 19 | dist-ssr 20 | *.local 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | !.vscode/extensions.json 25 | .idea 26 | .DS_Store 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw? 32 | -------------------------------------------------------------------------------- /util/db.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import mongoose from "mongoose"; 3 | 4 | const dbUrl: string = process.env.DB_URL || ''; 5 | const connectDB =async () => { 6 | try { 7 | await mongoose.connect(dbUrl).then((data:any) => { 8 | console.log("Database connected with " + data.connection.host); 9 | }) 10 | } catch (error:any) { 11 | console.log(error.message); 12 | setTimeout(connectDB,5000) 13 | } 14 | } 15 | 16 | export default connectDB; -------------------------------------------------------------------------------- /client/app/styles/style.ts: -------------------------------------------------------------------------------- 1 | export const styles = { 2 | title: "text-[25px] text-black dark:text-white font-[500] font-Poppins text-center py-2", 3 | label:"text-[16px] font-Poppins text-black dark:text-white", 4 | input:"w-full text-black dark:text-white bg-transparent border rounded h-[40px] px-2 outline-none mt-[10px] font-Poppins", 5 | button:"flex flex-row justify-center items-center py-3 px-6 rounded-full cursor-pointer bg-[#2190ff] min-h-[45px] w-full text-[16px] font-Poppins font-semibold" 6 | } -------------------------------------------------------------------------------- /utils/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | require('dotenv').config(); 3 | 4 | const dbUrl:string = process.env.DB_URL || ''; 5 | 6 | const connectDB = async () => { 7 | try { 8 | await mongoose.connect(dbUrl).then((data:any) => { 9 | console.log(`Database connected with ${data.connection.host}`) 10 | }) 11 | } catch (error:any) { 12 | console.log(error.message); 13 | setTimeout(connectDB, 5000); 14 | } 15 | } 16 | 17 | export default connectDB; -------------------------------------------------------------------------------- /client/app/hooks/adminProtected.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import React from "react"; 3 | import { useSelector } from "react-redux"; 4 | 5 | interface ProtectedProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function AdminProtected({ children }: ProtectedProps) { 10 | const { user } = useSelector((state: any) => state.auth); 11 | 12 | if (user) { 13 | const isAdmin = user?.role === "admin"; 14 | return isAdmin ? children : redirect("/"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /routes/layout.route.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { authorizeRoles, isAutheticated } from "../middleware/auth"; 3 | import { createLayout, editLayout, getLayoutByType } from "../controllers/layout.controller"; 4 | const layoutRouter = express.Router(); 5 | 6 | layoutRouter.post("/create-layout", isAutheticated,authorizeRoles("admin"), createLayout); 7 | 8 | layoutRouter.put("/edit-layout", isAutheticated,authorizeRoles("admin"), editLayout); 9 | 10 | layoutRouter.get("/get-layout/:type",getLayoutByType); 11 | 12 | 13 | export default layoutRouter; -------------------------------------------------------------------------------- /client/app/utils/Heading.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | 3 | interface HeadProps { 4 | title: string; 5 | description: string; 6 | keywords: string; 7 | } 8 | 9 | const Heading: FC = ({ title, description, keywords }) => { 10 | return ( 11 | <> 12 | {title} 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default Heading; -------------------------------------------------------------------------------- /routes/notification.route.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { authorizeRoles, isAutheticated } from "../middleware/auth"; 3 | import { getNotifications, updateNotification } from "../controllers/notification.controller"; 4 | const notificationRoute = express.Router(); 5 | 6 | notificationRoute.get( 7 | "/get-all-notifications", 8 | isAutheticated, 9 | authorizeRoles("admin"), 10 | getNotifications 11 | ); 12 | notificationRoute.put("/update-notification/:id", isAutheticated, authorizeRoles("admin"), updateNotification); 13 | 14 | export default notificationRoute; 15 | -------------------------------------------------------------------------------- /mails/question-reply.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | New Reply Notification 8 | 9 | 10 |

Hello <%= name %>,

11 |

A new reply has been added to your question in the video "<%= title %>".

12 |

Please login to our website to view the reply and continue the discussion.

13 |

Thank you for being a part of our community!

14 | 15 | 16 | -------------------------------------------------------------------------------- /client/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/order.Model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, {Document,Model,Schema} from "mongoose"; 2 | 3 | 4 | export interface IOrder extends Document{ 5 | courseId: string; 6 | userId?:string; 7 | payment_info: object; 8 | } 9 | 10 | const orderSchema = new Schema({ 11 | courseId: { 12 | type: String, 13 | required: true 14 | }, 15 | userId:{ 16 | type: String, 17 | required: true 18 | }, 19 | payment_info:{ 20 | type: Object, 21 | // required: true 22 | }, 23 | },{timestamps: true}); 24 | 25 | const OrderModel: Model = mongoose.model('Order',orderSchema); 26 | 27 | export default OrderModel; -------------------------------------------------------------------------------- /client/app/components/Admin/DashboardHero.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import DashboardHeader from "./DashboardHeader"; 3 | import DashboardWidgets from "../../components/Admin/Widgets/DashboardWidgets"; 4 | 5 | type Props = { 6 | isDashboard?: boolean; 7 | }; 8 | 9 | const DashboardHero = ({isDashboard}: Props) => { 10 | const [open, setOpen] = useState(false); 11 | 12 | return ( 13 |
14 | 15 | { 16 | isDashboard && ( 17 | 18 | ) 19 | } 20 |
21 | ); 22 | }; 23 | 24 | export default DashboardHero; 25 | -------------------------------------------------------------------------------- /socketServer.ts: -------------------------------------------------------------------------------- 1 | import { Server as SocketIOServer } from "socket.io"; 2 | import http from "http"; 3 | 4 | export const initSocketServer = (server: http.Server) => { 5 | const io = new SocketIOServer(server); 6 | 7 | io.on("connection", (socket) => { 8 | console.log("A user connected"); 9 | 10 | // Listen for 'notification' event from the frontend 11 | socket.on("notification", (data) => { 12 | // Broadcast the notification data to all connected clients (admin dashboard) 13 | io.emit("newNotification", data); 14 | }); 15 | 16 | socket.on("disconnect", () => { 17 | console.log("A user disconnected"); 18 | }); 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /routes/analytics.route.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { authorizeRoles, isAutheticated } from "../middleware/auth"; 3 | import { getCoursesAnalytics, getOrderAnalytics, getUsersAnalytics } from "../controllers/analytics.controller"; 4 | const analyticsRouter = express.Router(); 5 | 6 | 7 | analyticsRouter.get("/get-users-analytics", isAutheticated,authorizeRoles("admin"), getUsersAnalytics); 8 | 9 | analyticsRouter.get("/get-orders-analytics", isAutheticated,authorizeRoles("admin"), getOrderAnalytics); 10 | 11 | analyticsRouter.get("/get-courses-analytics", isAutheticated,authorizeRoles("admin"), getCoursesAnalytics); 12 | 13 | 14 | export default analyticsRouter; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8000 2 | NODE_ENV=development 3 | DB_URL='your_mongodb_url' 4 | CLOUD_NAME=your_cloudinary_name 5 | CLOUD_API_KEY=your_cloudinary_api_key 6 | CLOUD_SECRET_KEY=your_cloudinary_secret_key 7 | REDIS_URL=your_redis_url 8 | ACTIVATION_SECRET=your_activation_secret 9 | ACCESS_TOKEN=your_access_token_secret 10 | REFRESH_TOKEN=your_refresh_token_secret 11 | ACCESS_TOKEN_EXPIRE=5 12 | REFRESH_TOKEN_EXPIRE=3 13 | SMTP_HOST=smtp.gmail.com 14 | SMTP_PORT=465 15 | SMTP_SERVICE=gmail 16 | SMTP_MAIL=your_email@gmail.com 17 | SMTP_PASSWORD=your_email_app_password 18 | VDOCIPHER_API_SECRET=your_vdocipher_api_secret 19 | STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key 20 | STRIPE_SECRET_KEY=your_stripe_secret_key -------------------------------------------------------------------------------- /routes/order.route.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { authorizeRoles, isAutheticated } from "../middleware/auth"; 3 | import { 4 | createOrder, 5 | getAllOrders, 6 | newPayment, 7 | sendStripePublishableKey, 8 | } from "../controllers/order.controller"; 9 | const orderRouter = express.Router(); 10 | 11 | orderRouter.post("/create-order", isAutheticated, createOrder); 12 | 13 | orderRouter.get( 14 | "/get-orders", 15 | isAutheticated, 16 | authorizeRoles("admin"), 17 | getAllOrders 18 | ); 19 | 20 | orderRouter.get("/payment/stripepublishablekey", sendStripePublishableKey); 21 | 22 | orderRouter.post("/payment", isAutheticated, newPayment); 23 | 24 | export default orderRouter; 25 | -------------------------------------------------------------------------------- /services/course.service.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import CourseModel from "../models/course.model"; 3 | import { CatchAsyncError } from "../middleware/catchAsyncErrors"; 4 | 5 | // create course 6 | export const createCourse = CatchAsyncError(async(data:any,res:Response)=>{ 7 | const course = await CourseModel.create(data); 8 | res.status(201).json({ 9 | success:true, 10 | course 11 | }); 12 | }) 13 | 14 | // Get All Courses 15 | export const getAllCoursesService = async (res: Response) => { 16 | const courses = await CourseModel.find().sort({ createdAt: -1 }); 17 | 18 | res.status(201).json({ 19 | success: true, 20 | courses, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /client/redux/features/store.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { configureStore } from "@reduxjs/toolkit"; 3 | import { apiSlice } from "./api/apiSlice"; 4 | import authSlice from "./auth/authSlice"; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | [apiSlice.reducerPath]: apiSlice.reducer, 9 | auth: authSlice, 10 | }, 11 | devTools: false, 12 | middleware: (getDefaultMiddleware) => 13 | getDefaultMiddleware().concat(apiSlice.middleware), 14 | }); 15 | 16 | // call the load user function on every page load 17 | const initializeApp = async () => { 18 | await store.dispatch( 19 | apiSlice.endpoints.loadUser.initiate({}, { forceRefetch: true }) 20 | ); 21 | }; 22 | 23 | initializeApp(); 24 | -------------------------------------------------------------------------------- /services/order.service.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Response } from "express"; 2 | import { CatchAsyncError } from "../middleware/catchAsyncErrors"; 3 | import OrderModel from "../models/order.Model"; 4 | 5 | 6 | // create new order 7 | export const newOrder = CatchAsyncError(async(data:any,res:Response) => { 8 | const order = await OrderModel.create(data); 9 | 10 | res.status(201).json({ 11 | succcess:true, 12 | order, 13 | }) 14 | 15 | }); 16 | 17 | // Get All Orders 18 | export const getAllOrdersService = async (res: Response) => { 19 | const orders = await OrderModel.find().sort({ createdAt: -1 }); 20 | 21 | res.status(201).json({ 22 | success: true, 23 | orders, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /client/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GoogleProvider from "next-auth/providers/google"; 3 | import GithubProvider from "next-auth/providers/github"; 4 | console.log(process.env.GOOGLE_CLIENT_ID,'red'); 5 | export const authOptions = { 6 | providers: [ 7 | GoogleProvider({ 8 | clientId: process.env.GOOGLE_CLIENT_ID || '', 9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', 10 | }), 11 | GithubProvider({ 12 | clientId: process.env.GITHUB_CLIENT_ID || '', 13 | clientSecret: process.env.GITHUB_CLIENT_SECRET || '', 14 | }) 15 | ], 16 | secret: process.env.SECRET, 17 | } 18 | 19 | export default NextAuth(authOptions); -------------------------------------------------------------------------------- /models/notification.Model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, {Document,Model,Schema} from "mongoose"; 2 | 3 | export interface INotification extends Document{ 4 | title: string; 5 | message: string; 6 | status: string; 7 | userId: string; 8 | } 9 | 10 | const notificationSchema = new Schema({ 11 | title:{ 12 | type: String, 13 | required: true 14 | }, 15 | message:{ 16 | type:String, 17 | required: true, 18 | }, 19 | status:{ 20 | type: String, 21 | required: true, 22 | default: "unread" 23 | } 24 | },{timestamps: true}); 25 | 26 | 27 | const NotificationModel: Model = mongoose.model('Notification',notificationSchema); 28 | 29 | export default NotificationModel; -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /client/redux/features/notifications/notificationsApi.ts: -------------------------------------------------------------------------------- 1 | import { apiSlice } from "../api/apiSlice"; 2 | 3 | export const notificationsApi = apiSlice.injectEndpoints({ 4 | endpoints: (builder) => ({ 5 | getAllNotifications: builder.query({ 6 | query: () => ({ 7 | url: "get-all-notifications", 8 | method: "GET", 9 | credentials: "include" as const, 10 | }), 11 | }), 12 | updateNotificationStatus: builder.mutation({ 13 | query: (id) => ({ 14 | url: `/update-notification/${id}`, 15 | method: "PUT", 16 | credentials: "include" as const, 17 | }), 18 | }), 19 | }), 20 | }); 21 | 22 | export const { 23 | useGetAllNotificationsQuery, 24 | useUpdateNotificationStatusMutation, 25 | } = notificationsApi; 26 | -------------------------------------------------------------------------------- /client/redux/features/auth/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | token: "", 5 | user: "", 6 | }; 7 | 8 | const authSlice = createSlice({ 9 | name: "auth", 10 | initialState, 11 | reducers: { 12 | userRegistration: (state, action: PayloadAction<{token: string}>) => { 13 | state.token = action.payload.token; 14 | }, 15 | userLoggedIn: (state, action:PayloadAction<{accessToken:string,user:string}>) => { 16 | state.token = action.payload.accessToken; 17 | state.user = action.payload.user; 18 | }, 19 | userLoggedOut: (state) => { 20 | state.token = ""; 21 | state.user = ""; 22 | }, 23 | }, 24 | }); 25 | 26 | export const { userRegistration, userLoggedIn, userLoggedOut } = 27 | authSlice.actions; 28 | 29 | export default authSlice.reducer; -------------------------------------------------------------------------------- /client/redux/features/layout/layoutApi.ts: -------------------------------------------------------------------------------- 1 | import { apiSlice } from "../api/apiSlice"; 2 | 3 | export const layoutApi = apiSlice.injectEndpoints({ 4 | endpoints: (builder) => ({ 5 | getHeroData: builder.query({ 6 | query: (type) => ({ 7 | url: `get-layout/${type}`, 8 | method: "GET", 9 | credentials: "include" as const, 10 | }), 11 | }), 12 | editLayout: builder.mutation({ 13 | query: ({ type, image, title, subTitle, faq, categories }) => ({ 14 | url: `edit-layout`, 15 | body: { 16 | type, 17 | image, 18 | title, 19 | subTitle, 20 | faq, 21 | categories, 22 | }, 23 | method: "PUT", 24 | credentials: "include" as const, 25 | }), 26 | }), 27 | }), 28 | }); 29 | 30 | export const { useGetHeroDataQuery,useEditLayoutMutation } = layoutApi; 31 | -------------------------------------------------------------------------------- /client/app/utils/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, useEffect } from "react"; 3 | import { useTheme } from "next-themes"; 4 | import { BiMoon, BiSun } from "react-icons/bi"; 5 | 6 | export const ThemeSwitcher = () => { 7 | const [mounted, setMounted] = useState(false); 8 | const { theme, setTheme } = useTheme(); 9 | 10 | useEffect(() => setMounted(true), []); 11 | 12 | if (!mounted) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
18 | {theme === "light" ? ( 19 | setTheme("dark")} 24 | /> 25 | ) : ( 26 | setTheme("light")} 30 | /> 31 | )} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /client/app/utils/CustomModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import {Modal,Box} from "@mui/material"; 3 | 4 | type Props = { 5 | open: boolean; 6 | setOpen: (open: boolean) => void; 7 | activeItem: any; 8 | component: any; 9 | setRoute?: (route: string) => void; 10 | refetch?:any; 11 | } 12 | 13 | const CustomModal: FC = ({open,setOpen,setRoute,component:Component,refetch}) => { 14 | return ( 15 | setOpen(false)} 18 | aria-labelledby="modal-modal-title" 19 | aria-describedby="modal-modal-description" 20 | > 21 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default CustomModal -------------------------------------------------------------------------------- /client/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import Heading from "../utils/Heading"; 4 | import Header from "../components/Header"; 5 | import About from "./About"; 6 | import Footer from "../components/Footer"; 7 | 8 | type Props = {}; 9 | 10 | const Page = (props: Props) => { 11 | const [open, setOpen] = useState(false); 12 | const [activeItem, setActiveItem] = useState(2); 13 | const [route, setRoute] = useState("Login"); 14 | 15 | return ( 16 |
17 | 22 |
29 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Page; 36 | -------------------------------------------------------------------------------- /services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | import { redis } from "../utils/redis"; 3 | import userModel from "../models/user.model"; 4 | 5 | // get user by id 6 | export const getUserById = async (id: string, res: Response) => { 7 | const userJson = await redis.get(id); 8 | 9 | if (userJson) { 10 | const user = JSON.parse(userJson); 11 | res.status(201).json({ 12 | success: true, 13 | user, 14 | }); 15 | } 16 | }; 17 | 18 | // Get All users 19 | export const getAllUsersService = async (res: Response) => { 20 | const users = await userModel.find().sort({ createdAt: -1 }); 21 | 22 | res.status(201).json({ 23 | success: true, 24 | users, 25 | }); 26 | }; 27 | 28 | // update user role 29 | export const updateUserRoleService = async (res:Response,id: string,role:string) => { 30 | const user = await userModel.findByIdAndUpdate(id, { role }, { new: true }); 31 | 32 | res.status(201).json({ 33 | success: true, 34 | user, 35 | }); 36 | } -------------------------------------------------------------------------------- /client/app/policy/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import Heading from "../utils/Heading"; 4 | import Header from "../components/Header"; 5 | import Footer from "../components/Footer"; 6 | import Policy from "./Policy"; 7 | 8 | type Props = {}; 9 | 10 | const Page = (props: Props) => { 11 | const [open, setOpen] = useState(false); 12 | const [activeItem, setActiveItem] = useState(3); 13 | const [route, setRoute] = useState("Login"); 14 | 15 | return ( 16 |
17 | 22 |
29 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Page; 36 | -------------------------------------------------------------------------------- /client/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | darkMode:["class"], 10 | theme: { 11 | extend: { 12 | fontFamily:{ 13 | Poppins: ["var(--font-Poppins)"], 14 | Josefin: ["var(--font-Josefin)"], 15 | }, 16 | backgroundImage: { 17 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 18 | 'gradient-conic': 19 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 20 | }, 21 | screens:{ 22 | "1000px": "1000px", 23 | "1100px": "1100px", 24 | "1200px": "1200px", 25 | "1300px": "1300px", 26 | "1500px": "1500px", 27 | "800px": "800px", 28 | "400px": "400px", 29 | } 30 | }, 31 | }, 32 | plugins: [], 33 | } 34 | export default config 35 | -------------------------------------------------------------------------------- /client/app/faq/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import Heading from "../utils/Heading"; 4 | import Header from "../components/Header"; 5 | import Footer from "../components/Footer"; 6 | import FAQ from "../components/FAQ/FAQ"; 7 | 8 | type Props = {}; 9 | 10 | const Page = (props: Props) => { 11 | const [open, setOpen] = useState(false); 12 | const [activeItem, setActiveItem] = useState(4); 13 | const [route, setRoute] = useState("Login"); 14 | 15 | return ( 16 |
17 | 22 |
29 |
30 | 31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Page; 37 | -------------------------------------------------------------------------------- /client/app/admin/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React from "react"; 3 | import Heading from "../utils/Heading"; 4 | import AdminSidebar from "../components/Admin/sidebar/AdminSidebar"; 5 | import AdminProtected from "../hooks/adminProtected"; 6 | import DashboardHero from "../components/Admin/DashboardHero"; 7 | 8 | type Props = {}; 9 | 10 | const page = (props: Props) => { 11 | return ( 12 |
13 | 14 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 | ); 30 | }; 31 | 32 | export default page; 33 | -------------------------------------------------------------------------------- /client/app/admin/invoices/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 4 | import Heading from '../../../app/utils/Heading'; 5 | import DashboardHeader from '../../../app/components/Admin/DashboardHeader'; 6 | import AllInvoices from "../../../app/components/Admin/Order/AllInvoices"; 7 | 8 | type Props = {} 9 | 10 | const page = (props: Props) => { 11 | return ( 12 |
13 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | export default page -------------------------------------------------------------------------------- /client/app/admin/create-course/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 4 | import Heading from '../../../app/utils/Heading'; 5 | import CreateCourse from "../../components/Admin/Course/CreateCourse"; 6 | import DashboardHeader from '../../../app/components/Admin/DashboardHeader'; 7 | 8 | type Props = {} 9 | 10 | const page = (props: Props) => { 11 | return ( 12 |
13 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | export default page -------------------------------------------------------------------------------- /client/app/admin/users-analytics/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 4 | import Heading from '../../utils/Heading'; 5 | import DashboardHeader from '../../components/Admin/DashboardHeader'; 6 | import UserAnalytics from '../../../app/components/Admin/Analytics/UserAnalytics'; 7 | 8 | type Props = {} 9 | 10 | const page = (props: Props) => { 11 | return ( 12 |
13 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | export default page -------------------------------------------------------------------------------- /client/app/admin/courses-analytics/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 4 | import Heading from '../../../app/utils/Heading'; 5 | import CourseAnalytics from "../../components/Admin/Analytics/CourseAnalytics"; 6 | import DashboardHeader from '../../../app/components/Admin/DashboardHeader'; 7 | 8 | type Props = {} 9 | 10 | const page = (props: Props) => { 11 | return ( 12 |
13 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | export default page -------------------------------------------------------------------------------- /client/app/admin/orders-analytics/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 4 | import Heading from '../../../app/utils/Heading'; 5 | import OrdersAnalytics from "../../components/Admin/Analytics/OrdersAnalytics"; 6 | import DashboardHeader from '../../../app/components/Admin/DashboardHeader'; 7 | 8 | type Props = {} 9 | 10 | const page = (props: Props) => { 11 | return ( 12 |
13 | 18 |
19 |
20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | 31 | export default page -------------------------------------------------------------------------------- /client/redux/features/analytics/analyticsApi.ts: -------------------------------------------------------------------------------- 1 | import { apiSlice } from "../api/apiSlice"; 2 | 3 | export const analyticsApi = apiSlice.injectEndpoints({ 4 | endpoints: (builder) => ({ 5 | getCoursesAnalytics: builder.query({ 6 | query: () => ({ 7 | url: 'get-courses-analytics', 8 | method: 'GET', 9 | credentials: 'include' as const, 10 | }), 11 | }), 12 | getUsersAnalytics: builder.query({ 13 | query: () => ({ 14 | url: 'get-users-analytics', 15 | method: 'GET', 16 | credentials: 'include' as const, 17 | }) 18 | }), 19 | getOrdersAnalytics: builder.query({ 20 | query: () => ({ 21 | url: 'get-orders-analytics', 22 | method: 'GET', 23 | credentials: 'include' as const, 24 | }) 25 | }), 26 | }), 27 | }); 28 | 29 | export const { useGetCoursesAnalyticsQuery,useGetUsersAnalyticsQuery,useGetOrdersAnalyticsQuery } = analyticsApi; -------------------------------------------------------------------------------- /client/app/admin/edit-course/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React from 'react' 3 | import AdminSidebar from "../../../components/Admin/sidebar/AdminSidebar"; 4 | import Heading from '../../../../app/utils/Heading'; 5 | import DashboardHeader from '../../../../app/components/Admin/DashboardHeader'; 6 | import EditCourse from "../../../components/Admin/Course/EditCourse"; 7 | 8 | type Props = {} 9 | 10 | const page = ({params}:any) => { 11 | const id = params?.id; 12 | 13 | return ( 14 |
15 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 | ) 31 | } 32 | 33 | export default page -------------------------------------------------------------------------------- /client/app/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import DashboardHero from '@/app/components/Admin/DashboardHero' 3 | import AdminProtected from '@/app/hooks/adminProtected' 4 | import Heading from '@/app/utils/Heading' 5 | import React from 'react' 6 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 7 | import AllUsers from "../../components/Admin/Users/AllUsers"; 8 | 9 | type Props = {} 10 | 11 | const page = (props: Props) => { 12 | return ( 13 |
14 | 15 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | export default page -------------------------------------------------------------------------------- /client/app/admin/courses/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import DashboardHero from '@/app/components/Admin/DashboardHero' 3 | import AdminProtected from '@/app/hooks/adminProtected' 4 | import Heading from '@/app/utils/Heading' 5 | import React from 'react' 6 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 7 | import AllCourses from "../../components/Admin/Course/AllCourses"; 8 | 9 | type Props = {} 10 | 11 | const page = (props: Props) => { 12 | return ( 13 |
14 | 15 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | export default page -------------------------------------------------------------------------------- /client/app/admin/hero/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import DashboardHero from '@/app/components/Admin/DashboardHero' 3 | import AdminProtected from '@/app/hooks/adminProtected' 4 | import Heading from '@/app/utils/Heading' 5 | import React from 'react' 6 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 7 | import EditHero from "../../components/Admin/Customization/EditHero"; 8 | 9 | type Props = {} 10 | 11 | const page = (props: Props) => { 12 | return ( 13 |
14 | 15 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | export default page -------------------------------------------------------------------------------- /client/app/course-access/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import CourseContent from "@/app/components/Course/CourseContent"; 3 | import Loader from "@/app/components/Loader/Loader"; 4 | import { useLoadUserQuery } from "@/redux/features/api/apiSlice"; 5 | import { redirect } from "next/navigation"; 6 | import React, { useEffect } from "react"; 7 | 8 | type Props = { 9 | params:any; 10 | } 11 | 12 | const Page = ({params}: Props) => { 13 | const id = params.id; 14 | const { isLoading, error, data,refetch } = useLoadUserQuery(undefined, {}); 15 | 16 | useEffect(() => { 17 | if (data) { 18 | const isPurchased = data.user.courses.find( 19 | (item: any) => item._id === id 20 | ); 21 | if (!isPurchased) { 22 | redirect("/"); 23 | } 24 | } 25 | if (error) { 26 | redirect("/"); 27 | } 28 | }, [data,error]); 29 | 30 | return ( 31 | <> 32 | { 33 | isLoading ? ( 34 | 35 | ) : ( 36 |
37 | 38 |
39 | ) 40 | } 41 | 42 | ) 43 | } 44 | 45 | export default Page -------------------------------------------------------------------------------- /client/app/admin/team/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import DashboardHero from "@/app/components/Admin/DashboardHero"; 3 | import AdminProtected from "@/app/hooks/adminProtected"; 4 | import Heading from "@/app/utils/Heading"; 5 | import React from "react"; 6 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 7 | import AllUsers from "../../components/Admin/Users/AllUsers"; 8 | 9 | type Props = {}; 10 | 11 | const page = (props: Props) => { 12 | return ( 13 |
14 | 15 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default page; 35 | -------------------------------------------------------------------------------- /client/app/admin/categories/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import DashboardHero from "@/app/components/Admin/DashboardHero"; 3 | import AdminProtected from "@/app/hooks/adminProtected"; 4 | import Heading from "@/app/utils/Heading"; 5 | import React from "react"; 6 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 7 | import EditCategories from "../../components/Admin/Customization/EditCategories"; 8 | 9 | type Props = {}; 10 | 11 | const page = (props: Props) => { 12 | return ( 13 |
14 | 15 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default page; 35 | -------------------------------------------------------------------------------- /client/app/admin/faq/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import DashboardHero from "@/app/components/Admin/DashboardHero"; 3 | import AdminProtected from "@/app/hooks/adminProtected"; 4 | import Heading from "@/app/utils/Heading"; 5 | import React from "react"; 6 | import AdminSidebar from "../../components/Admin/sidebar/AdminSidebar"; 7 | import EditFaq from "../../components/Admin/Customization/EditFaq"; 8 | 9 | type Props = {}; 10 | 11 | const page = (props: Props) => { 12 | return ( 13 |
14 | 15 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default page; 36 | -------------------------------------------------------------------------------- /utils/analytics.generator.ts: -------------------------------------------------------------------------------- 1 | import { Document, Model } from "mongoose"; 2 | 3 | interface MonthData { 4 | month: string; 5 | count: number; 6 | } 7 | 8 | export async function generateLast12MothsData( 9 | model: Model 10 | ): Promise<{ last12Months: MonthData[] }> { 11 | const last12Months: MonthData[] = []; 12 | const currentDate = new Date(); 13 | currentDate.setDate(currentDate.getDate() + 1); 14 | 15 | for (let i = 11; i >= 0; i--) { 16 | const endDate = new Date( 17 | currentDate.getFullYear(), 18 | currentDate.getMonth(), 19 | currentDate.getDate() - i * 28 20 | ); 21 | const startDate = new Date( 22 | endDate.getFullYear(), 23 | endDate.getMonth(), 24 | endDate.getDate() - 28 25 | ); 26 | 27 | const monthYear = endDate.toLocaleString("default", { 28 | day: "numeric", 29 | month: "short", 30 | year: "numeric", 31 | }); 32 | const count = await model.countDocuments({ 33 | createdAt: { 34 | $gte: startDate, 35 | $lt: endDate, 36 | }, 37 | }); 38 | last12Months.push({ month: monthYear, count }); 39 | } 40 | return { last12Months }; 41 | } 42 | -------------------------------------------------------------------------------- /client/app/utils/Ratings.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import { AiFillStar, AiOutlineStar } from 'react-icons/ai'; 3 | import { BsStarHalf } from 'react-icons/bs'; 4 | 5 | type Props = { 6 | rating: number; 7 | } 8 | 9 | const Ratings:FC = ({ rating }) => { 10 | const stars = []; 11 | 12 | for (let i = 1; i <= 5; i++) { 13 | if (i <= rating) { 14 | stars.push( 15 | 21 | ); 22 | } else if (i === Math.ceil(rating) && !Number.isInteger(rating)) { 23 | stars.push( 24 | 30 | ); 31 | } else { 32 | stars.push( 33 | 39 | ); 40 | } 41 | } 42 | return
{stars}
; 43 | }; 44 | 45 | export default Ratings; -------------------------------------------------------------------------------- /client/redux/features/api/apiSlice.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 2 | import { userLoggedIn } from "../auth/authSlice"; 3 | 4 | export const apiSlice = createApi({ 5 | reducerPath: "api", 6 | baseQuery: fetchBaseQuery({ 7 | baseUrl: process.env.NEXT_PUBLIC_SERVER_URI, 8 | }), 9 | endpoints: (builder) => ({ 10 | refreshToken: builder.query({ 11 | query: (data) => ({ 12 | url: "refresh", 13 | method: "GET", 14 | credentials: "include" as const, 15 | }), 16 | }), 17 | loadUser: builder.query({ 18 | query: (data) => ({ 19 | url: "me", 20 | method: "GET", 21 | credentials: "include" as const, 22 | }), 23 | async onQueryStarted(arg, { queryFulfilled, dispatch }) { 24 | try { 25 | const result = await queryFulfilled; 26 | dispatch( 27 | userLoggedIn({ 28 | accessToken: result.data.accessToken, 29 | user: result.data.user, 30 | }) 31 | ); 32 | } catch (error: any) { 33 | console.log(error); 34 | } 35 | }, 36 | }), 37 | }), 38 | }); 39 | 40 | 41 | export const { useRefreshTokenQuery, useLoadUserQuery } = apiSlice; 42 | -------------------------------------------------------------------------------- /middleware/error.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import ErrorHandler from "../utils/ErrorHandler"; 3 | 4 | export const ErrorMiddleware = ( 5 | err: any, 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | err.statusCode = err.statusCode || 500; 11 | err.message = err.message || "Internal server error"; 12 | 13 | // wrong mongodb id error 14 | if (err.name === "CastError") { 15 | const message = `Resource not found. Invalid: ${err.path}`; 16 | err = new ErrorHandler(message, 400); 17 | } 18 | 19 | // Duplicate key error 20 | if (err.code === 11000) { 21 | const message = `Duplicate ${Object.keys(err.keyValue)} entered`; 22 | err = new ErrorHandler(message, 400); 23 | } 24 | 25 | // wrong jwt error 26 | if (err.name === "JsonWebTokenError") { 27 | const message = `Json web token is invalid, try again`; 28 | err = new ErrorHandler(message, 400); 29 | } 30 | 31 | // JWT expired error 32 | if (err.name === "TokenExpiredError") { 33 | const message = `Json web token is expired, try again`; 34 | err = new ErrorHandler(message, 400); 35 | } 36 | 37 | res.status(err.statusCode).json({ 38 | success: false, 39 | message: err.message, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /utils/sendMail.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | import nodemailer, {Transporter} from 'nodemailer'; 3 | import ejs from 'ejs'; 4 | import path from 'path'; 5 | 6 | interface EmailOptions{ 7 | email:string; 8 | subject:string; 9 | template:string; 10 | data: {[key:string]:any}; 11 | } 12 | 13 | const sendMail = async (options: EmailOptions):Promise => { 14 | const transporter: Transporter = nodemailer.createTransport({ 15 | host: process.env.SMTP_HOST, 16 | port: parseInt(process.env.SMTP_PORT || '587'), 17 | service: process.env.SMTP_SERVICE, 18 | auth:{ 19 | user: process.env.SMTP_MAIL, 20 | pass: process.env.SMTP_PASSWORD, 21 | }, 22 | }); 23 | 24 | const {email,subject,template,data} = options; 25 | 26 | // get the pdath to the email template file 27 | const templatePath = path.join(__dirname,'../mails',template); 28 | 29 | // Render the email template with EJS 30 | const html:string = await ejs.renderFile(templatePath,data); 31 | 32 | const mailOptions = { 33 | from: process.env.SMTP_MAIL, 34 | to: email, 35 | subject, 36 | html 37 | }; 38 | 39 | await transporter.sendMail(mailOptions); 40 | }; 41 | 42 | export default sendMail; 43 | 44 | -------------------------------------------------------------------------------- /client/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { FC, useEffect, useState } from "react"; 3 | import Heading from "./utils/Heading"; 4 | import Header from "./components/Header"; 5 | import Hero from "./components/Route/Hero"; 6 | import Courses from "./components/Route/Courses"; 7 | import Reviews from "./components/Route/Reviews"; 8 | import FAQ from "./components/FAQ/FAQ"; 9 | import Footer from "./components/Footer"; 10 | 11 | interface Props { } 12 | 13 | const Page: FC = (props) => { 14 | const [open, setOpen] = useState(false); 15 | const [activeItem, setActiveItem] = useState(0); 16 | const [route, setRoute] = useState("Login"); 17 | 18 | return ( 19 | <> 20 | 25 |
26 |
33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 | ); 41 | }; 42 | 43 | export default Page; 44 | -------------------------------------------------------------------------------- /client/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { FC, useState } from "react"; 3 | import Protected from "../hooks/useProtected"; 4 | import Heading from "../utils/Heading"; 5 | import Header from "../components/Header"; 6 | import Profile from "../components/Profile/Profile"; 7 | import { useSelector } from "react-redux"; 8 | import Footer from "../components/Footer"; 9 | 10 | type Props = {}; 11 | 12 | const Page: FC = (props) => { 13 | const [open, setOpen] = useState(false); 14 | const [activeItem, setActiveItem] = useState(5); 15 | const [route, setRoute] = useState("Login"); 16 | const {user} = useSelector((state:any) => state.auth); 17 | 18 | return ( 19 |
20 | 21 | 26 |
33 | 34 |
35 | 36 |
37 | ); 38 | }; 39 | 40 | export default Page; 41 | -------------------------------------------------------------------------------- /client/redux/features/orders/ordersApi.ts: -------------------------------------------------------------------------------- 1 | import { apiSlice } from "../api/apiSlice"; 2 | 3 | export const ordersApi = apiSlice.injectEndpoints({ 4 | endpoints: (builder) => ({ 5 | getAllOrders: builder.query({ 6 | query: (type) => ({ 7 | url: `get-orders`, 8 | method: "GET", 9 | credentials: "include" as const, 10 | }), 11 | }), 12 | getStripePublishablekey: builder.query({ 13 | query: () => ({ 14 | url: `payment/stripepublishablekey`, 15 | method: "GET", 16 | credentials: "include" as const, 17 | }), 18 | }), 19 | createPaymentIntent: builder.mutation({ 20 | query: (amount) => ({ 21 | url: "payment", 22 | method: "POST", 23 | body: { 24 | amount, 25 | }, 26 | credentials: "include" as const, 27 | }), 28 | }), 29 | createOrder: builder.mutation({ 30 | query: ({ courseId, payment_info }) => ({ 31 | url: "create-order", 32 | body: { 33 | courseId, 34 | payment_info, 35 | }, 36 | method: "POST", 37 | credentials: "include" as const, 38 | }), 39 | }), 40 | }), 41 | }); 42 | 43 | export const { useGetAllOrdersQuery,useGetStripePublishablekeyQuery, useCreatePaymentIntentMutation ,useCreateOrderMutation} = 44 | ordersApi; 45 | -------------------------------------------------------------------------------- /client/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/layout.model.ts: -------------------------------------------------------------------------------- 1 | import {Schema,model,Document} from "mongoose"; 2 | 3 | export interface FaqItem extends Document{ 4 | question: string; 5 | answer: string; 6 | } 7 | 8 | export interface Category extends Document{ 9 | title:string; 10 | } 11 | 12 | export interface BannerImage extends Document{ 13 | public_id:string; 14 | url: string; 15 | } 16 | 17 | interface Layout extends Document{ 18 | type: string; 19 | faq: FaqItem[]; 20 | categories: Category[]; 21 | banner:{ 22 | image: BannerImage; 23 | title: string; 24 | subTitle: string; 25 | }; 26 | } 27 | 28 | const faqSchema = new Schema ({ 29 | question: {type: String}, 30 | answer: {type: String}, 31 | }); 32 | 33 | const categorySchema = new Schema ({ 34 | title: {type:String}, 35 | }); 36 | 37 | const bannerImageSchema = new Schema ({ 38 | public_id: {type:String}, 39 | url: {type:String}, 40 | }); 41 | 42 | 43 | const layoutSchema = new Schema({ 44 | type:{type:String}, 45 | faq: [faqSchema], 46 | categories: [categorySchema], 47 | banner:{ 48 | image: bannerImageSchema, 49 | title: {type:String}, 50 | subTitle: {type:String}, 51 | }, 52 | }); 53 | 54 | const LayoutModel = model('Layout',layoutSchema); 55 | 56 | export default LayoutModel; -------------------------------------------------------------------------------- /client/app/utils/CoursePlayer.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | 4 | type Props = { 5 | videoUrl: string; 6 | title: string; 7 | }; 8 | 9 | const CoursePlayer: FC = ({ videoUrl }) => { 10 | const [videoData, setVideoData] = useState({ 11 | otp: "", 12 | playbackInfo: "", 13 | }); 14 | 15 | useEffect(() => { 16 | axios 17 | .post(`${process.env.NEXT_PUBLIC_SERVER_URI}getVdoCipherOTP`, { 18 | videoId: videoUrl, 19 | }) 20 | .then((res) => { 21 | setVideoData(res.data); 22 | }); 23 | }, [videoUrl]); 24 | 25 | return ( 26 |
29 | {videoData.otp && videoData.playbackInfo !== "" && ( 30 | 43 | )} 44 |
45 | ); 46 | }; 47 | 48 | export default CoursePlayer; 49 | -------------------------------------------------------------------------------- /client/app/components/Route/Courses.tsx: -------------------------------------------------------------------------------- 1 | import { useGetUsersAllCoursesQuery } from "@/redux/features/courses/coursesApi"; 2 | import React, { useEffect, useState } from "react"; 3 | import CourseCard from "../Course/CourseCard"; 4 | 5 | type Props = {}; 6 | 7 | const Courses = (props: Props) => { 8 | const { data, isLoading } = useGetUsersAllCoursesQuery({}); 9 | const [courses, setCourses] = useState([]); 10 | 11 | useEffect(() => { 12 | setCourses(data?.courses); 13 | }, [data]); 14 | 15 | return ( 16 |
17 |
18 |

19 | Expand Your Career Opportunity{" "} 20 |
21 | Opportunity With Our Courses 22 |

23 |
24 |
25 |
26 | {courses && 27 | courses.map((item: any, index: number) => ( 28 | 29 | ))} 30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Courses; 37 | -------------------------------------------------------------------------------- /client/app/components/Admin/sidebar/Icon.tsx: -------------------------------------------------------------------------------- 1 | import HomeOutlinedIcon from "@mui/icons-material/HomeOutlined"; 2 | import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; 3 | import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"; 4 | import PeopleOutlinedIcon from "@mui/icons-material/PeopleOutlined"; 5 | import ReceiptOutlinedIcon from "@mui/icons-material/ReceiptOutlined"; 6 | import BarChartOutlinedIcon from "@mui/icons-material/BarChartOutlined"; 7 | import MapOutlinedIcon from "@mui/icons-material/MapOutlined"; 8 | import GroupsIcon from "@mui/icons-material/Groups"; 9 | import OndemandVideoIcon from "@mui/icons-material/OndemandVideo"; 10 | import VideoCallIcon from "@mui/icons-material/VideoCall"; 11 | import WebIcon from "@mui/icons-material/Web"; 12 | import QuizIcon from "@mui/icons-material/Quiz"; 13 | import WysiwygIcon from "@mui/icons-material/Wysiwyg"; 14 | import ManageHistoryIcon from "@mui/icons-material/ManageHistory"; 15 | import SettingsIcon from "@mui/icons-material/Settings"; 16 | import ExitToAppIcon from "@mui/icons-material/ExitToApp"; 17 | 18 | export { 19 | HomeOutlinedIcon, 20 | ArrowForwardIosIcon, 21 | ArrowBackIosIcon, 22 | PeopleOutlinedIcon, 23 | ReceiptOutlinedIcon, 24 | BarChartOutlinedIcon, 25 | MapOutlinedIcon, 26 | GroupsIcon, 27 | OndemandVideoIcon, 28 | VideoCallIcon, 29 | WebIcon, 30 | QuizIcon, 31 | WysiwygIcon, 32 | ManageHistoryIcon, 33 | SettingsIcon, 34 | ExitToAppIcon, 35 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@emotion/react": "^11.11.1", 13 | "@emotion/styled": "^11.11.0", 14 | "@mui/icons-material": "^5.14.14", 15 | "@mui/material": "^5.14.14", 16 | "@mui/x-data-grid": "^6.11.2", 17 | "@reduxjs/toolkit": "^1.9.5", 18 | "@stripe/react-stripe-js": "^2.1.2", 19 | "@stripe/stripe-js": "^2.1.0", 20 | "@types/node": "20.4.9", 21 | "@types/react": "18.2.19", 22 | "@types/react-dom": "18.2.7", 23 | "autoprefixer": "10.4.14", 24 | "axios": "^1.4.0", 25 | "bufferutil": "^4.0.7", 26 | "eslint": "8.46.0", 27 | "eslint-config-next": "13.4.13", 28 | "formik": "^2.4.3", 29 | "next": "13.4.13", 30 | "next-auth": "^4.23.0", 31 | "next-themes": "^0.2.1", 32 | "postcss": "8.4.27", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "react-hot-toast": "^2.4.1", 36 | "react-icons": "^4.10.1", 37 | "react-pro-sidebar": "^0.7.1", 38 | "react-redux": "^8.1.2", 39 | "recharts": "^2.8.0", 40 | "socket.io-client": "^4.7.2", 41 | "tailwindcss": "3.3.3", 42 | "timeago.js": "^4.0.2", 43 | "typescript": "5.1.6", 44 | "utf-8-validate": "^6.0.3", 45 | "ws": "^8.13.0", 46 | "yup": "^1.2.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "commonjs", 6 | "main": "build/server.js", 7 | "engines": { 8 | "node":"18.x" 9 | }, 10 | "scripts": { 11 | "dev": "ts-node-dev --respawn --transpile-only server.ts", 12 | "start":"node build/server.js", 13 | "build":"tsc" 14 | }, 15 | "author": "EC & SPM Project", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@types/bcryptjs": "^2.4.2", 19 | "@types/cookie-parser": "^1.4.3", 20 | "@types/cors": "^2.8.13", 21 | "@types/express": "^4.17.17", 22 | "@types/jsonwebtoken": "^9.0.2", 23 | "@types/node": "^20.4.0", 24 | "@types/socket.io": "^3.0.2", 25 | "@types/validator": "^13.7.17", 26 | "axios": "^1.4.0", 27 | "bcryptjs": "^2.4.3", 28 | "cloudinary": "^1.39.0", 29 | "cookie-parser": "^1.4.6", 30 | "cors": "^2.8.5", 31 | "dotenv": "^16.3.1", 32 | "ejs": "^3.1.9", 33 | "express": "^4.18.2", 34 | "express-rate-limit": "^6.10.0", 35 | "ioredis": "^5.3.2", 36 | "jsonwebtoken": "^9.0.1", 37 | "mongoose": "^7.4.1", 38 | "node-cron": "^3.0.2", 39 | "nodemailer": "^6.9.4", 40 | "socket.io": "^4.7.2", 41 | "stripe": "^13.3.0", 42 | "ts-node-dev": "^2.0.0", 43 | "typescript": "^5.1.6" 44 | }, 45 | "devDependencies": { 46 | "@types/ejs": "^3.1.2", 47 | "@types/node-cron": "^3.0.8", 48 | "@types/nodemailer": "^6.4.9" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /routes/user.route.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | activateUser, 4 | deleteUser, 5 | getAllUsers, 6 | getUserInfo, 7 | loginUser, 8 | logoutUser, 9 | registrationUser, 10 | socialAuth, 11 | updatePassword, 12 | updateProfilePicture, 13 | updateUserInfo, 14 | updateUserRole, 15 | } from "../controllers/user.controller"; 16 | import { authorizeRoles, isAutheticated } from "../middleware/auth"; 17 | const userRouter = express.Router(); 18 | 19 | userRouter.post("/registration", registrationUser); 20 | 21 | userRouter.post("/activate-user", activateUser); 22 | 23 | userRouter.post("/login", loginUser); 24 | 25 | userRouter.get("/logout",isAutheticated, logoutUser); 26 | 27 | userRouter.get("/me", isAutheticated, getUserInfo); 28 | 29 | userRouter.post("/social-auth", socialAuth); 30 | 31 | userRouter.put("/update-user-info",isAutheticated, updateUserInfo); 32 | 33 | userRouter.put("/update-user-password", isAutheticated, updatePassword); 34 | 35 | userRouter.put("/update-user-avatar", isAutheticated, updateProfilePicture); 36 | 37 | userRouter.get( 38 | "/get-users", 39 | isAutheticated, 40 | authorizeRoles("admin"), 41 | getAllUsers 42 | ); 43 | 44 | userRouter.put( 45 | "/update-user", 46 | isAutheticated, 47 | authorizeRoles("admin"), 48 | updateUserRole 49 | ); 50 | 51 | userRouter.delete( 52 | "/delete-user/:id", 53 | isAutheticated, 54 | authorizeRoles("admin"), 55 | deleteUser 56 | ); 57 | 58 | export default userRouter; 59 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /client/app/components/Admin/Course/CourseOptions.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from 'react'; 2 | import { IoMdCheckmark } from 'react-icons/io'; 3 | 4 | type Props = { 5 | active: number; 6 | setActive: (active: number) => void; 7 | } 8 | 9 | const CourseOptions: FC = ({ active, setActive }) => { 10 | const options = [ 11 | "Course Information", 12 | "Course Options", 13 | "Course Content", 14 | "Course Preview", 15 | ]; 16 | return ( 17 |
18 | {options.map((option:any, index:number) => ( 19 |
20 |
index ? "bg-blue-500" : "bg-[#384766]" 23 | } relative`} 24 | > 25 | 26 | {index !== options.length - 1 && ( 27 |
index ? "bg-blue-500" : "bg-[#384766]" 30 | } bottom-[-100%]`} 31 | /> 32 | )} 33 |
34 |
41 | {option} 42 |
43 |
44 | ))} 45 |
46 | ) 47 | } 48 | 49 | export default CourseOptions -------------------------------------------------------------------------------- /routes/course.route.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | addAnwser, 4 | addQuestion, 5 | addReplyToReview, 6 | addReview, 7 | deleteCourse, 8 | editCourse, 9 | generateVideoUrl, 10 | getAdminAllCourses, 11 | getAllCourses, 12 | getCourseByUser, 13 | getSingleCourse, 14 | uploadCourse, 15 | } from "../controllers/course.controller"; 16 | import { authorizeRoles, isAutheticated } from "../middleware/auth"; 17 | const courseRouter = express.Router(); 18 | 19 | courseRouter.post( 20 | "/create-course", 21 | isAutheticated, 22 | authorizeRoles("admin"), 23 | uploadCourse 24 | ); 25 | 26 | courseRouter.put( 27 | "/edit-course/:id", 28 | isAutheticated, 29 | authorizeRoles("admin"), 30 | editCourse 31 | ); 32 | 33 | courseRouter.get("/get-course/:id", getSingleCourse); 34 | 35 | courseRouter.get("/get-courses", getAllCourses); 36 | 37 | courseRouter.get( 38 | "/get-admin-courses", 39 | isAutheticated, 40 | authorizeRoles("admin"), 41 | getAdminAllCourses 42 | ); 43 | 44 | courseRouter.get("/get-course-content/:id", isAutheticated, getCourseByUser); 45 | 46 | courseRouter.put("/add-question", isAutheticated, addQuestion); 47 | 48 | courseRouter.put("/add-answer", isAutheticated, addAnwser); 49 | 50 | courseRouter.put("/add-review/:id", isAutheticated, addReview); 51 | 52 | courseRouter.put( 53 | "/add-reply", 54 | isAutheticated, 55 | authorizeRoles("admin"), 56 | addReplyToReview 57 | ); 58 | 59 | courseRouter.post("/getVdoCipherOTP", generateVideoUrl); 60 | 61 | courseRouter.delete( 62 | "/delete-course/:id", 63 | isAutheticated, 64 | authorizeRoles("admin"), 65 | deleteCourse 66 | ); 67 | 68 | export default courseRouter; 69 | -------------------------------------------------------------------------------- /utils/jwt.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import { Response } from "express"; 3 | import { IUser } from "../models/user.model"; 4 | import { redis } from "./redis"; 5 | 6 | interface ITokenOptions { 7 | expires: Date; 8 | maxAge: number; 9 | httpOnly: boolean; 10 | sameSite: "lax" | "strict" | "none" | undefined; 11 | secure?: boolean; 12 | } 13 | 14 | // parse enviroment variables to integrates with fallback values 15 | const accessTokenExpire = parseInt( 16 | process.env.ACCESS_TOKEN_EXPIRE || "300", 17 | 10 18 | ); 19 | const refreshTokenExpire = parseInt( 20 | process.env.REFRESH_TOKEN_EXPIRE || "1200", 21 | 10 22 | ); 23 | 24 | // options for cookies 25 | export const accessTokenOptions: ITokenOptions = { 26 | expires: new Date(Date.now() + accessTokenExpire * 60 * 60 * 1000), 27 | maxAge: accessTokenExpire * 60 * 60 * 1000, 28 | httpOnly: true, 29 | sameSite: "none", 30 | secure:true, 31 | }; 32 | 33 | export const refreshTokenOptions: ITokenOptions = { 34 | expires: new Date(Date.now() + refreshTokenExpire * 24 * 60 * 60 * 1000), 35 | maxAge: refreshTokenExpire * 24 * 60 * 60 * 1000, 36 | httpOnly: true, 37 | sameSite: "none", 38 | secure: true, 39 | }; 40 | 41 | export const sendToken = (user: IUser, statusCode: number, res: Response) => { 42 | const accessToken = user.SignAccessToken(); 43 | const refreshToken = user.SignRefreshToken(); 44 | 45 | // upload session to redis 46 | redis.set(user._id, JSON.stringify(user) as any,); 47 | 48 | res.cookie("access_token", accessToken, accessTokenOptions); 49 | res.cookie("refresh_token", refreshToken, refreshTokenOptions); 50 | 51 | res.status(statusCode).json({ 52 | success: true, 53 | user, 54 | accessToken, 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /client/redux/features/user/userApi.ts: -------------------------------------------------------------------------------- 1 | import { apiSlice } from "../api/apiSlice"; 2 | 3 | export const userApi = apiSlice.injectEndpoints({ 4 | endpoints: (builder) => ({ 5 | updateAvatar: builder.mutation({ 6 | query: (avatar) => ({ 7 | url: "update-user-avatar", 8 | method: "PUT", 9 | body: { avatar }, 10 | credentials: "include" as const, 11 | }), 12 | }), 13 | editProfile: builder.mutation({ 14 | query: ({ name }) => ({ 15 | url: "update-user-info", 16 | method: "PUT", 17 | body: { 18 | name, 19 | }, 20 | credentials: "include" as const, 21 | }), 22 | }), 23 | updatePassword: builder.mutation({ 24 | query: ({ oldPassword, newPassword }) => ({ 25 | url: "update-user-password", 26 | method: "PUT", 27 | body: { 28 | oldPassword, 29 | newPassword, 30 | }, 31 | credentials: "include" as const, 32 | }), 33 | }), 34 | getAllUsers: builder.query({ 35 | query: () => ({ 36 | url: "get-users", 37 | method: "GET", 38 | credentials: "include" as const, 39 | }), 40 | }), 41 | updateUserRole: builder.mutation({ 42 | query: ({ email, role }) => ({ 43 | url: "update-user", 44 | method: "PUT", 45 | body: { email, role }, 46 | credentials: "include" as const, 47 | }), 48 | }), 49 | deleteUser: builder.mutation({ 50 | query: (id) => ({ 51 | url: `delete-user/${id}`, 52 | method: "DELETE", 53 | credentials: "include" as const, 54 | }), 55 | }), 56 | }), 57 | }); 58 | 59 | export const { 60 | useUpdateAvatarMutation, 61 | useEditProfileMutation, 62 | useUpdatePasswordMutation, 63 | useGetAllUsersQuery, 64 | useUpdateUserRoleMutation, 65 | useDeleteUserMutation 66 | } = userApi; 67 | -------------------------------------------------------------------------------- /middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { CatchAsyncError } from "./catchAsyncErrors"; 3 | import ErrorHandler from "../utils/ErrorHandler"; 4 | import jwt, { JwtPayload } from "jsonwebtoken"; 5 | import { redis } from "../utils/redis"; 6 | import { updateAccessToken } from "../controllers/user.controller"; 7 | 8 | // authenticated user 9 | export const isAutheticated = CatchAsyncError( 10 | async (req: Request, res: Response, next: NextFunction) => { 11 | const access_token = req.cookies.access_token as string; 12 | 13 | if (!access_token) { 14 | return next( 15 | new ErrorHandler("Please login to access this resource", 400) 16 | ); 17 | } 18 | 19 | const decoded = jwt.decode(access_token) as JwtPayload; 20 | 21 | if (!decoded) { 22 | return next(new ErrorHandler("access token is not valid", 400)); 23 | } 24 | 25 | // check if the access token is expired 26 | if (decoded.exp && decoded.exp <= Date.now() / 1000) { 27 | try { 28 | await updateAccessToken(req, res, next); 29 | } catch (error) { 30 | return next(error); 31 | } 32 | } else { 33 | const user = await redis.get(decoded.id); 34 | 35 | if (!user) { 36 | return next( 37 | new ErrorHandler("Please login to access this resource", 400) 38 | ); 39 | } 40 | 41 | req.user = JSON.parse(user); 42 | 43 | next(); 44 | } 45 | } 46 | ); 47 | 48 | // validate user role 49 | export const authorizeRoles = (...roles: string[]) => { 50 | return (req: Request, res: Response, next: NextFunction) => { 51 | if (!roles.includes(req.user?.role || "")) { 52 | return next( 53 | new ErrorHandler( 54 | `Role: ${req.user?.role} is not allowed to access this resource`, 55 | 403 56 | ) 57 | ); 58 | } 59 | next(); 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /client/app/components/Review/ReviewCard.tsx: -------------------------------------------------------------------------------- 1 | import Ratings from "@/app/utils/Ratings"; 2 | import Image from "next/image"; 3 | import React from "react"; 4 | 5 | type Props = { 6 | item: any; 7 | }; 8 | 9 | const ReviewCard = (props: Props) => { 10 | return ( 11 |
12 |
13 | 20 |
21 |
22 |
23 | {props.item.name} 24 |
25 |
26 | {props.item.profession} 27 |
28 |
29 | 30 |
31 | {/* for mobile */} 32 |
33 |
34 |
35 | {props.item.name} 36 |
37 |
38 | {props.item.profession} 39 |
40 |
41 | 42 |
43 |
44 |

49 | {props.item.comment} 50 |

51 |
52 | ); 53 | }; 54 | 55 | export default ReviewCard; 56 | -------------------------------------------------------------------------------- /controllers/analytics.controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import ErrorHandler from "../utils/ErrorHandler"; 3 | import { CatchAsyncError } from "../middleware/catchAsyncErrors"; 4 | import { generateLast12MothsData } from "../utils/analytics.generator"; 5 | import userModel from "../models/user.model"; 6 | import CourseModel from "../models/course.model"; 7 | import OrderModel from "../models/order.Model"; 8 | 9 | // get users analytics --- only for admin 10 | export const getUsersAnalytics = CatchAsyncError( 11 | async (req: Request, res: Response, next: NextFunction) => { 12 | try { 13 | const users = await generateLast12MothsData(userModel); 14 | 15 | res.status(200).json({ 16 | success: true, 17 | users, 18 | }); 19 | } catch (error: any) { 20 | return next(new ErrorHandler(error.message, 500)); 21 | } 22 | } 23 | ); 24 | 25 | // get courses analytics --- only for admin 26 | export const getCoursesAnalytics = CatchAsyncError( 27 | async (req: Request, res: Response, next: NextFunction) => { 28 | try { 29 | const courses = await generateLast12MothsData(CourseModel); 30 | 31 | res.status(200).json({ 32 | success: true, 33 | courses, 34 | }); 35 | } catch (error: any) { 36 | return next(new ErrorHandler(error.message, 500)); 37 | } 38 | } 39 | ); 40 | 41 | 42 | // get order analytics --- only for admin 43 | export const getOrderAnalytics = CatchAsyncError( 44 | async (req: Request, res: Response, next: NextFunction) => { 45 | try { 46 | const orders = await generateLast12MothsData(OrderModel); 47 | 48 | res.status(200).json({ 49 | success: true, 50 | orders, 51 | }); 52 | } catch (error: any) { 53 | return next(new ErrorHandler(error.message, 500)); 54 | } 55 | } 56 | ); 57 | -------------------------------------------------------------------------------- /controllers/notification.controller.ts: -------------------------------------------------------------------------------- 1 | import NotificationModel from "../models/notification.Model"; 2 | import { NextFunction, Request, Response } from "express"; 3 | import { CatchAsyncError } from "../middleware/catchAsyncErrors"; 4 | import ErrorHandler from "../utils/ErrorHandler"; 5 | import cron from "node-cron"; 6 | 7 | // get all notifications --- only admin 8 | export const getNotifications = CatchAsyncError( 9 | async (req: Request, res: Response, next: NextFunction) => { 10 | try { 11 | const notifications = await NotificationModel.find().sort({ 12 | createdAt: -1, 13 | }); 14 | 15 | res.status(201).json({ 16 | success: true, 17 | notifications, 18 | }); 19 | } catch (error: any) { 20 | return next(new ErrorHandler(error.message, 500)); 21 | } 22 | } 23 | ); 24 | 25 | // update notification status --- only admin 26 | export const updateNotification = CatchAsyncError( 27 | async (req: Request, res: Response, next: NextFunction) => { 28 | try { 29 | const notification = await NotificationModel.findById(req.params.id); 30 | if (!notification) { 31 | return next(new ErrorHandler("Notification not found", 404)); 32 | } else { 33 | notification.status 34 | ? (notification.status = "read") 35 | : notification?.status; 36 | } 37 | 38 | await notification.save(); 39 | 40 | const notifications = await NotificationModel.find().sort({ 41 | createdAt: -1, 42 | }); 43 | 44 | res.status(201).json({ 45 | success: true, 46 | notifications, 47 | }); 48 | } catch (error: any) { 49 | return next(new ErrorHandler(error.message, 500)); 50 | } 51 | } 52 | ); 53 | 54 | // delete notification --- only admin 55 | cron.schedule("0 0 0 * * *", async() => { 56 | const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); 57 | await NotificationModel.deleteMany({status:"read",createdAt: {$lt: thirtyDaysAgo}}); 58 | console.log('Deleted read notifications'); 59 | }); -------------------------------------------------------------------------------- /client/app/components/Course/CourseContent.tsx: -------------------------------------------------------------------------------- 1 | import { useGetCourseContentQuery } from "@/redux/features/courses/coursesApi"; 2 | import React, { useState } from "react"; 3 | import Loader from "../Loader/Loader"; 4 | import Heading from "@/app/utils/Heading"; 5 | import CourseContentMedia from "./CourseContentMedia"; 6 | import Header from "../Header"; 7 | import CourseContentList from "./CourseContentList"; 8 | 9 | type Props = { 10 | id: string; 11 | user:any; 12 | }; 13 | 14 | const CourseContent = ({ id,user }: Props) => { 15 | const { data: contentData, isLoading,refetch } = useGetCourseContentQuery(id,{refetchOnMountOrArgChange:true}); 16 | const [open, setOpen] = useState(false); 17 | const [route, setRoute] = useState('Login') 18 | const data = contentData?.content; 19 | 20 | const [activeVideo, setActiveVideo] = useState(0); 21 | 22 | return ( 23 | <> 24 | {isLoading ? ( 25 | 26 | ) : ( 27 | <> 28 |
29 |
30 | 35 |
36 | 44 |
45 |
46 | 51 |
52 |
53 | 54 | )} 55 | 56 | ); 57 | }; 58 | 59 | export default CourseContent; 60 | -------------------------------------------------------------------------------- /client/app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import "./globals.css"; 3 | import { Poppins } from "next/font/google"; 4 | import { Josefin_Sans } from "next/font/google"; 5 | import { ThemeProvider } from "./utils/theme-provider"; 6 | import { Toaster } from "react-hot-toast"; 7 | import { Providers } from "./Provider"; 8 | import { SessionProvider } from "next-auth/react"; 9 | import React, { FC, useEffect } from "react"; 10 | import { useLoadUserQuery } from "@/redux/features/api/apiSlice"; 11 | import Loader from "./components/Loader/Loader"; 12 | import socketIO from "socket.io-client"; 13 | const ENDPOINT = process.env.NEXT_PUBLIC_SOCKET_SERVER_URI || ""; 14 | const socketId = socketIO(ENDPOINT, { transports: ["websocket"] }); 15 | 16 | const poppins = Poppins({ 17 | subsets: ["latin"], 18 | weight: ["400", "500", "600", "700"], 19 | variable: "--font-Poppins", 20 | }); 21 | 22 | const josefin = Josefin_Sans({ 23 | subsets: ["latin"], 24 | weight: ["400", "500", "600", "700"], 25 | variable: "--font-Josefin", 26 | }); 27 | 28 | export default function RootLayout({ 29 | children, 30 | }: { 31 | children: React.ReactNode; 32 | }) { 33 | return ( 34 | 35 | 38 | 39 | 40 | 41 | 42 |
{children}
43 |
44 | 45 |
46 |
47 |
48 | 49 | 50 | ); 51 | } 52 | 53 | const Custom: FC<{ children: React.ReactNode }> = ({ children }) => { 54 | const { isLoading } = useLoadUserQuery({}); 55 | 56 | useEffect(() => { 57 | socketId.on("connection", () => { }); 58 | }, []); 59 | 60 | return <>{isLoading ? :
{children}
}; 61 | }; 62 | -------------------------------------------------------------------------------- /client/app/components/Course/CourseCard.tsx: -------------------------------------------------------------------------------- 1 | import Ratings from "@/app/utils/Ratings"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import React, { FC } from "react"; 5 | import { AiOutlineUnorderedList } from "react-icons/ai"; 6 | 7 | type Props = { 8 | item: any; 9 | isProfile?: boolean; 10 | }; 11 | 12 | const CourseCard: FC = ({ item, isProfile }) => { 13 | return ( 14 | 17 |
18 | 26 |
27 |

28 | {item.name} 29 |

30 |
31 | 32 |
37 | {item.purchased} Students 38 |
39 |
40 |
41 |
42 |

43 | {item.price === 0 ? "Free" : item.price + "$"} 44 |

45 |
46 | {item.estimatedPrice}$ 47 |
48 |
49 |
50 | 51 |
52 | {item.courseData?.length} Lectures 53 |
54 |
55 |
56 |
57 | 58 | ); 59 | }; 60 | 61 | export default CourseCard; 62 | -------------------------------------------------------------------------------- /client/app/utils/NavItems.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | export const navItemsData = [ 5 | { 6 | name: "Home", 7 | url: "/", 8 | }, 9 | { 10 | name: "Courses", 11 | url: "/courses", 12 | }, 13 | { 14 | name: "About", 15 | url: "/about", 16 | }, 17 | { 18 | name: "Policy", 19 | url: "/policy", 20 | }, 21 | { 22 | name: "FAQ", 23 | url: "/faq", 24 | }, 25 | ]; 26 | 27 | type Props = { 28 | activeItem: number; 29 | isMobile: boolean; 30 | }; 31 | 32 | const NavItems: React.FC = ({ activeItem, isMobile }) => { 33 | return ( 34 | <> 35 |
36 | {navItemsData && 37 | navItemsData.map((i, index) => ( 38 | 39 | 46 | {i.name} 47 | 48 | 49 | ))} 50 |
51 | {isMobile && ( 52 |
53 |
54 | 55 | ELearning 58 | 59 |
60 | {navItemsData && 61 | navItemsData.map((i, index) => ( 62 | 63 | 70 | {i.name} 71 | 72 | 73 | ))} 74 |
75 | )} 76 | 77 | ); 78 | }; 79 | 80 | export default NavItems; 81 | -------------------------------------------------------------------------------- /client/app/components/Admin/Analytics/CourseAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BarChart, 4 | Bar, 5 | ResponsiveContainer, 6 | XAxis, 7 | Label, 8 | YAxis, 9 | LabelList, 10 | } from "recharts"; 11 | import Loader from "../../Loader/Loader"; 12 | import { useGetCoursesAnalyticsQuery } from "@/redux/features/analytics/analyticsApi"; 13 | import { styles } from "@/app/styles/style"; 14 | 15 | type Props = {}; 16 | 17 | const CourseAnalytics = (props: Props) => { 18 | const { data, isLoading } = useGetCoursesAnalyticsQuery({}); 19 | 20 | // const analyticsData = [ 21 | // { name: 'Jun 2023', uv: 3 }, 22 | // { name: 'July 2023', uv: 2 }, 23 | // { name: 'August 2023', uv: 5 }, 24 | // { name: 'Sept 2023', uv: 7 }, 25 | // { name: 'October 2023', uv: 2 }, 26 | // { name: 'Nov 2023', uv: 5 }, 27 | // { name: 'December 2023', uv: 7 }, 28 | // ]; 29 | 30 | const analyticsData: any = []; 31 | 32 | data && 33 | data.courses.last12Months.forEach((item: any) => { 34 | analyticsData.push({ name: item.month, uv: item.count }); 35 | }); 36 | 37 | const minValue = 0; 38 | 39 | return ( 40 | <> 41 | {isLoading ? ( 42 | 43 | ) : ( 44 |
45 |
46 |

47 | Courses Analytics 48 |

49 |

50 | Last 12 months analytics data{" "} 51 |

52 |
53 | 54 |
55 | 56 | 57 | 58 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 | )} 69 | 70 | ); 71 | }; 72 | 73 | export default CourseAnalytics; 74 | -------------------------------------------------------------------------------- /client/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | /* Chrome, Safari, Edge, Opera */ 20 | input::-webkit-outer-spin-button, 21 | input::-webkit-inner-spin-button { 22 | -webkit-appearance: none; 23 | margin: 0; 24 | } 25 | ::-webkit-scrollbar { 26 | width: 8px; 27 | } 28 | 29 | /* Track */ 30 | ::-webkit-scrollbar-track { 31 | background: #1771c6; 32 | } 33 | 34 | /* Handle */ 35 | ::-webkit-scrollbar-thumb { 36 | background: #f5f5f5b0; 37 | } 38 | 39 | /* Handle on hover */ 40 | ::-webkit-scrollbar-thumb:hover { 41 | background: #32ae7e; 42 | } 43 | 44 | /* Firefox */ 45 | input[type="number"] { 46 | -moz-appearance: textfield; 47 | } 48 | body { 49 | color: rgb(var(--foreground-rgb)); 50 | } 51 | .hero_animation { 52 | background-image: linear-gradient(147.92deg, hsla(239, 76%, 53%, 0.456) 10.41%, hsla(0, 0%, 100%, 0) 89.25%); 53 | animation: changeBackgroundColor 8s infinite alternate; 54 | } 55 | 56 | .text-gradient { 57 | background: linear-gradient(90deg, #3e7fd9 2.34%, #5c3bd6 100.78%); 58 | -webkit-background-clip: text; 59 | background-clip: text; 60 | -webkit-text-fill-color: transparent; 61 | text-fill-color: transparent; 62 | } 63 | 64 | 65 | @keyframes changeBackgroundColor { 66 | 0%, 100% { 67 | opacity: 1; 68 | } 69 | 16.67% { 70 | opacity: 0.9; 71 | } 72 | 33.33% { 73 | opacity: 0.8; 74 | } 75 | 50% { 76 | opacity: 0.6; 77 | } 78 | 66.67% { 79 | opacity: 0.5; 80 | } 81 | 83.33% { 82 | opacity: 0.4; 83 | } 84 | } 85 | 86 | @keyframes shake { 87 | 0% { 88 | transform: translateX(0); 89 | } 90 | 20% { 91 | transform: translateX(-2px); 92 | } 93 | 40% { 94 | transform: translateX(2px); 95 | } 96 | 60% { 97 | transform: translateX(-2px); 98 | } 99 | 80% { 100 | transform: translateX(2px); 101 | } 102 | 100% { 103 | transform: translateX(0); 104 | } 105 | } 106 | 107 | .shake { 108 | animation: shake 0.5s ease-in-out; 109 | } -------------------------------------------------------------------------------- /client/app/components/FAQ/FAQ.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from '@/app/styles/style'; 2 | import { useGetHeroDataQuery } from '@/redux/features/layout/layoutApi'; 3 | import React, { useEffect, useState } from 'react' 4 | import { HiMinus, HiPlus } from 'react-icons/hi'; 5 | 6 | type Props = {} 7 | 8 | const FAQ = (props: Props) => { 9 | const { data } = useGetHeroDataQuery("FAQ", { 10 | }); 11 | const [activeQuestion, setActiveQuestion] = useState(null); 12 | const [questions, setQuestions] = useState([]); 13 | 14 | useEffect(() => { 15 | if (data) { 16 | setQuestions(data.layout?.faq); 17 | } 18 | }, [data]); 19 | 20 | const toggleQuestion = (id: any) => { 21 | setActiveQuestion(activeQuestion === id ? null : id); 22 | }; 23 | 24 | return ( 25 |
26 |
27 |

28 | Frequently Asked Questions 29 |

30 |
31 |
32 | {questions?.map((q) => ( 33 |
38 |
39 | 52 |
53 | {activeQuestion === q._id && ( 54 |
55 |

{q.answer}

56 |
57 | )} 58 |
59 | ))} 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | ) 68 | } 69 | 70 | export default FAQ -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import express, { NextFunction, Request, Response } from "express"; 3 | export const app = express(); 4 | import cors from "cors"; 5 | import cookieParser from "cookie-parser"; 6 | import { ErrorMiddleware } from "./middleware/error"; 7 | import userRouter from "./routes/user.route"; 8 | import courseRouter from "./routes/course.route"; 9 | import orderRouter from "./routes/order.route"; 10 | import notificationRouter from "./routes/notification.route"; 11 | import analyticsRouter from "./routes/analytics.route"; 12 | import layoutRouter from "./routes/layout.route"; 13 | import { rateLimit } from 'express-rate-limit' 14 | 15 | // body parser 16 | app.use(express.json({ limit: "50mb" })); 17 | 18 | // cookie parser 19 | app.use(cookieParser()); 20 | 21 | // cors => cross origin resource sharing 22 | // origin: process.env.ORIGIN, 23 | app.use( 24 | cors({ 25 | origin: ['http://localhost:3000', "https://elearninglms.netlify.app"], 26 | credentials: true, 27 | }) 28 | ); 29 | // app.use(cors({ origin: process.env.ORIGIN, credentials: true, })) 30 | 31 | // api requests limit 32 | const limiter = rateLimit({ 33 | windowMs: 15 * 60 * 1000, 34 | max: 100, 35 | standardHeaders: 'draft-7', 36 | legacyHeaders: false, 37 | handler: (req: Request, res: Response) => { 38 | res.status(429).json({ 39 | success: false, 40 | message: 'Too many requests, please try again later.', 41 | }); 42 | } 43 | }) 44 | 45 | // routes 46 | app.use( 47 | "/api/v1", 48 | userRouter, 49 | orderRouter, 50 | courseRouter, 51 | notificationRouter, 52 | analyticsRouter, 53 | layoutRouter 54 | ); 55 | 56 | // testing api 57 | app.get("/test", async (req: Request, res: Response, next: NextFunction) => { 58 | try { 59 | res.status(200).json({ 60 | success: true, 61 | message: "API is working", 62 | }); 63 | } catch (error) { 64 | next(error); 65 | } 66 | }); 67 | 68 | // unknown route 69 | app.all("*", (req: Request, res: Response, next: NextFunction) => { 70 | const err = new Error(`Route ${req.originalUrl} not found`) as any; 71 | err.statusCode = 404; 72 | next(err); 73 | }); 74 | 75 | // middleware calls 76 | app.use(limiter); 77 | app.use(ErrorMiddleware); 78 | 79 | // Add error handling middleware for express 80 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 81 | console.error('Express error:', err); 82 | res.status(err.status || 500).json({ 83 | success: false, 84 | message: err.message || 'Internal server error', 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | import http from "http"; 3 | import connectDB from "./utils/db"; 4 | import { initSocketServer } from "./socketServer"; 5 | import { app } from "./app"; 6 | import cluster from "cluster"; 7 | import os from "os"; 8 | require("dotenv").config(); 9 | 10 | // Add global error handler for uncaught exceptions 11 | process.on('uncaughtException', (err) => { 12 | console.error('Uncaught Exception:', err); 13 | // Log the error but keep the process running 14 | }); 15 | 16 | // Add handler for unhandled promise rejections 17 | process.on('unhandledRejection', (reason, promise) => { 18 | console.error('Unhandled Rejection at:', promise, 'reason:', reason); 19 | // Log the error but keep the process running 20 | }); 21 | 22 | if (cluster.isPrimary) { 23 | // Get the number of available CPU cores 24 | const numCPUs = os.cpus().length; 25 | 26 | // Fork workers for each CPU core 27 | for (let i = 0; i < numCPUs; i++) { 28 | cluster.fork(); 29 | } 30 | 31 | // Enhanced worker crash handling 32 | cluster.on('exit', (worker, code, signal) => { 33 | console.log(`Worker ${worker.process.pid} died with code ${code} and signal ${signal}`); 34 | console.log('Starting a new worker...'); 35 | cluster.fork(); 36 | }); 37 | 38 | } else { 39 | // Worker process 40 | const server = http.createServer(app); 41 | 42 | // Wrap server initialization in try-catch 43 | try { 44 | // cloudinary config 45 | cloudinary.config({ 46 | cloud_name: process.env.CLOUD_NAME, 47 | api_key: process.env.CLOUD_API_KEY, 48 | api_secret: process.env.CLOUD_SECRET_KEY, 49 | }); 50 | 51 | initSocketServer(server); 52 | 53 | // Add error handler for the server 54 | server.on('error', (error) => { 55 | console.error('Server error:', error); 56 | // Attempt to recover or restart the server 57 | }); 58 | 59 | // create server 60 | server.listen(process.env.PORT, () => { 61 | console.log(`Worker ${process.pid} started - Server is connected with port ${process.env.PORT}`); 62 | // Wrap database connection in try-catch 63 | connectDB().catch(err => { 64 | console.error('Database connection error:', err); 65 | // Server will continue running even if DB connection fails 66 | }); 67 | }); 68 | } catch (error) { 69 | console.error('Server initialization error:', error); 70 | // Attempt to restart the worker 71 | process.exit(1); // Cluster will start a new worker 72 | } 73 | } -------------------------------------------------------------------------------- /models/user.model.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | import mongoose, { Document, Model, Schema } from "mongoose"; 3 | import bcrypt from "bcryptjs"; 4 | import jwt from "jsonwebtoken"; 5 | 6 | const emailRegexPattern: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 7 | 8 | export interface IUser extends Document { 9 | name: string; 10 | email: string; 11 | password: string; 12 | avatar: { 13 | public_id: string; 14 | url: string; 15 | }; 16 | role: string; 17 | isVerified: boolean; 18 | courses: Array<{ courseId: string }>; 19 | comparePassword: (password: string) => Promise; 20 | SignAccessToken: () => string; 21 | SignRefreshToken: () => string; 22 | } 23 | 24 | const userSchema: Schema = new mongoose.Schema( 25 | { 26 | name: { 27 | type: String, 28 | required: [true, "Please enter your name"], 29 | }, 30 | email: { 31 | type: String, 32 | required: [true, "Please enter your email"], 33 | validate: { 34 | validator: function (value: string) { 35 | return emailRegexPattern.test(value); 36 | }, 37 | message: "please enter a valid email", 38 | }, 39 | unique: true, 40 | }, 41 | password: { 42 | type: String, 43 | minlength: [6, "Password must be at least 6 characters"], 44 | select: false, 45 | }, 46 | avatar: { 47 | public_id: String, 48 | url: String, 49 | }, 50 | role: { 51 | type: String, 52 | default: "user", 53 | }, 54 | isVerified: { 55 | type: Boolean, 56 | default: false, 57 | }, 58 | courses: [ 59 | { 60 | courseId: String, 61 | }, 62 | ], 63 | }, 64 | { timestamps: true } 65 | ); 66 | 67 | // Hash Password before saving 68 | userSchema.pre("save", async function (next) { 69 | if (!this.isModified("password")) { 70 | next(); 71 | } 72 | this.password = await bcrypt.hash(this.password, 10); 73 | next(); 74 | }); 75 | 76 | // sign access token 77 | userSchema.methods.SignAccessToken = function () { 78 | return jwt.sign({ id: this._id }, process.env.ACCESS_TOKEN || "", { 79 | expiresIn: "5m", 80 | }); 81 | }; 82 | 83 | // sign refresh token 84 | userSchema.methods.SignRefreshToken = function () { 85 | return jwt.sign({ id: this._id }, process.env.REFRESH_TOKEN || "", { 86 | expiresIn: "3d", 87 | }); 88 | }; 89 | 90 | // compare password 91 | userSchema.methods.comparePassword = async function ( 92 | enteredPassword: string 93 | ): Promise { 94 | return await bcrypt.compare(enteredPassword, this.password); 95 | }; 96 | 97 | const userModel: Model = mongoose.model("User", userSchema); 98 | 99 | export default userModel; 100 | -------------------------------------------------------------------------------- /client/app/components/Course/CourseDetailsPage.tsx: -------------------------------------------------------------------------------- 1 | import { useGetCourseDetailsQuery } from "@/redux/features/courses/coursesApi"; 2 | import React, { useEffect, useState } from "react"; 3 | import Loader from "../Loader/Loader"; 4 | import Heading from "@/app/utils/Heading"; 5 | import Header from "../Header"; 6 | import Footer from "../Footer"; 7 | import CourseDetails from "./CourseDetails"; 8 | import { 9 | useCreatePaymentIntentMutation, 10 | useGetStripePublishablekeyQuery, 11 | } from "@/redux/features/orders/ordersApi"; 12 | import { loadStripe } from "@stripe/stripe-js"; 13 | import { useLoadUserQuery } from "@/redux/features/api/apiSlice"; 14 | 15 | type Props = { 16 | id: string; 17 | }; 18 | 19 | const CourseDetailsPage = ({ id }: Props) => { 20 | const [route, setRoute] = useState("Login"); 21 | const [open, setOpen] = useState(false); 22 | const { data, isLoading } = useGetCourseDetailsQuery(id); 23 | const { data: config } = useGetStripePublishablekeyQuery({}); 24 | const [createPaymentIntent, { data: paymentIntentData }] = 25 | useCreatePaymentIntentMutation(); 26 | const { data: userData } = useLoadUserQuery(undefined, {}); 27 | const [stripePromise, setStripePromise] = useState(null); 28 | const [clientSecret, setClientSecret] = useState(""); 29 | 30 | useEffect(() => { 31 | if (config) { 32 | const publishablekey = config?.publishablekey; 33 | setStripePromise(loadStripe(publishablekey)); 34 | } 35 | if (data && userData?.user) { 36 | const amount = Math.round(data.course.price * 100); 37 | createPaymentIntent(amount); 38 | } 39 | }, [config, data, userData]); 40 | 41 | useEffect(() => { 42 | if (paymentIntentData) { 43 | setClientSecret(paymentIntentData?.client_secret); 44 | } 45 | }, [paymentIntentData]); 46 | 47 | return ( 48 | <> 49 | {isLoading ? ( 50 | 51 | ) : ( 52 |
53 | 60 |
67 | {stripePromise && ( 68 | 75 | )} 76 |
77 |
78 | )} 79 | 80 | ); 81 | }; 82 | 83 | export default CourseDetailsPage; 84 | -------------------------------------------------------------------------------- /client/app/components/Payment/CheckOutForm.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from "@/app/styles/style"; 2 | import { useLoadUserQuery } from "@/redux/features/api/apiSlice"; 3 | import { useCreateOrderMutation } from "@/redux/features/orders/ordersApi"; 4 | import { 5 | LinkAuthenticationElement, 6 | PaymentElement, 7 | useElements, 8 | useStripe, 9 | } from "@stripe/react-stripe-js"; 10 | import { redirect } from "next/navigation"; 11 | import React, { useEffect, useState } from "react"; 12 | import { toast } from "react-hot-toast"; 13 | import socketIO from "socket.io-client"; 14 | const ENDPOINT = process.env.NEXT_PUBLIC_SOCKET_SERVER_URI || ""; 15 | const socketId = socketIO(ENDPOINT, { transports: ["websocket"] }); 16 | 17 | type Props = { 18 | setOpen: any; 19 | data: any; 20 | user:any; 21 | refetch:any; 22 | }; 23 | 24 | const CheckOutForm = ({ data,user,refetch }: Props) => { 25 | const stripe = useStripe(); 26 | const elements = useElements(); 27 | const [message, setMessage] = useState(""); 28 | const [createOrder, { data: orderData, error }] = useCreateOrderMutation(); 29 | const [isLoading, setIsLoading] = useState(false); 30 | 31 | const handleSubmit = async (e: any) => { 32 | e.preventDefault(); 33 | if (!stripe || !elements) { 34 | return; 35 | } 36 | setIsLoading(true); 37 | const { error, paymentIntent } = await stripe.confirmPayment({ 38 | elements, 39 | redirect: "if_required", 40 | }); 41 | if (error) { 42 | setMessage(error.message); 43 | setIsLoading(false); 44 | } else if (paymentIntent && paymentIntent.status === "succeeded") { 45 | setIsLoading(false); 46 | createOrder({ courseId: data._id, payment_info: paymentIntent }); 47 | } 48 | }; 49 | 50 | useEffect(() => { 51 | if(orderData){ 52 | refetch(); 53 | socketId.emit("notification", { 54 | title: "New Order", 55 | message: `You have a new order from ${data.name}`, 56 | userId: user._id, 57 | }); 58 | redirect(`/course-access/${data._id}`); 59 | } 60 | if(error){ 61 | if ("data" in error) { 62 | const errorMessage = error as any; 63 | toast.error(errorMessage.data.message); 64 | } 65 | } 66 | }, [orderData,error]) 67 | 68 | 69 | return ( 70 |
71 | 72 | 73 | 78 | {/* Show any error or success messages */} 79 | {message && ( 80 |
81 | {message} 82 |
83 | )} 84 | 85 | ); 86 | }; 87 | 88 | export default CheckOutForm; 89 | -------------------------------------------------------------------------------- /mails/activation-mail.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ELearning Activation Email 5 | 6 | 7 | 69 | 70 | 71 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /client/app/components/Admin/Analytics/UserAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from "@/app/styles/style"; 2 | import { useGetUsersAnalyticsQuery } from "@/redux/features/analytics/analyticsApi"; 3 | import React, { FC } from "react"; 4 | import { 5 | AreaChart, 6 | Area, 7 | XAxis, 8 | YAxis, 9 | Tooltip, 10 | ResponsiveContainer, 11 | } from "recharts"; 12 | import Loader from "../../Loader/Loader" 13 | 14 | type Props = { 15 | isDashboard?: boolean; 16 | } 17 | 18 | // const analyticsData = [ 19 | // { name: "January 2023", count: 440 }, 20 | // { name: "February 2023", count: 8200 }, 21 | // { name: "March 2023", count: 4033 }, 22 | // { name: "April 2023", count: 4502 }, 23 | // { name: "May 2023", count: 2042 }, 24 | // { name: "Jun 2023", count: 3454 }, 25 | // { name: "July 2023", count: 356 }, 26 | // { name: "Aug 2023", count: 5667 }, 27 | // { name: "Sept 2023", count: 1320 }, 28 | // { name: "Oct 2023", count: 6526 }, 29 | // { name: "Nov 2023", count: 5480 }, 30 | // { name: "December 2023", count: 485 }, 31 | // ]; 32 | 33 | const UserAnalytics = ({isDashboard}:Props) => { 34 | const { data, isLoading } = useGetUsersAnalyticsQuery({}); 35 | 36 | const analyticsData: any = []; 37 | 38 | data && 39 | data.users.last12Months.forEach((item: any) => { 40 | analyticsData.push({ name: item.month, count: item.count }); 41 | }); 42 | 43 | 44 | return ( 45 | <> 46 | { 47 | isLoading ? ( 48 | 49 | ) : ( 50 |
51 |
52 |

53 | Users Analytics 54 |

55 | { 56 | !isDashboard && ( 57 |

58 | Last 12 months analytics data{" "} 59 |

60 | ) 61 | } 62 |
63 | 64 |
65 | 66 | 75 | 76 | 77 | 78 | 84 | 85 | 86 |
87 |
88 | ) 89 | } 90 | 91 | ) 92 | } 93 | 94 | export default UserAnalytics -------------------------------------------------------------------------------- /client/app/about/About.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { styles } from "../styles/style"; 3 | 4 | const About = () => { 5 | return ( 6 |
7 |
8 |

9 | What is E-Learning? 10 |

11 | 12 |
13 |
14 |

15 | Are you ready to take your programming skills to the next level? Look 16 | no further than E-learning, the premier programming community 17 | dedicated to helping new programmers achieve their goals and reach 18 | their full potential. 19 |
20 |
21 | As the founder and CEO of E-learning, I know firsthand the challenges 22 | that come with learning and growing in the programming industry. 23 | That's why I created E-learning – to provide new 24 | programmers with the resources and support they need to succeed. 25 |
26 |
27 | Our YouTube channel is a treasure trove of informative videos on 28 | everything from programming basics to advanced techniques. But 29 | that's just the beginning. Our affordable courses are designed to 30 | give you the high-quality education you need to succeed in the 31 | industry, without breaking the bank. 32 |
33 |
34 | At E-learning, we believe that price should never be a barrier to 35 | achieving your dreams. That's why our courses are priced low 36 | – so that anyone, regardless of their financial situation, can 37 | access the tools and knowledge they need to succeed. 38 |
39 |
40 | But E-learning is more than just a community – we're a 41 | family. Our supportive community of like-minded individuals is here to 42 | help you every step of the way, whether you're just starting out 43 | or looking to take your skills to the next level. 44 |
45 |
46 | With E-learning by your side, there's nothing standing between 47 | you and your dream job. Our courses and community will provide you 48 | with the guidance, support, and motivation you need to unleash your 49 | full potential and become a skilled programmer. 50 |
51 |
52 | So what are you waiting for? Join the E-learning family today and 53 | let's conquer the programming industry together! With our 54 | affordable courses, informative videos, and supportive community, the 55 | sky's the limit. 56 |

57 |
58 | Shahriarsajeeb's 59 |
60 | Founder and CEO of E-learning 61 |
62 |
63 |
64 |
65 |
66 |
67 | ); 68 | }; 69 | 70 | export default About; 71 | -------------------------------------------------------------------------------- /client/app/components/Admin/Analytics/OrdersAnalytics.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from "@/app/styles/style"; 2 | import { useGetOrdersAnalyticsQuery } from "@/redux/features/analytics/analyticsApi"; 3 | import React, { useEffect } from "react"; 4 | import { 5 | LineChart, 6 | Line, 7 | XAxis, 8 | YAxis, 9 | CartesianGrid, 10 | Tooltip, 11 | Legend, 12 | ResponsiveContainer, 13 | } from "recharts"; 14 | import Loader from "../../Loader/Loader"; 15 | 16 | // const analyticsData = [ 17 | // { 18 | // name: "Page A", 19 | // Count: 4000, 20 | // }, 21 | // { 22 | // name: "Page B", 23 | // Count: 3000, 24 | // }, 25 | // { 26 | // name: "Page C", 27 | // Count: 5000, 28 | // }, 29 | // { 30 | // name: "Page D", 31 | // Count: 1000, 32 | // }, 33 | // { 34 | // name: "Page E", 35 | // Count: 4000, 36 | // }, 37 | // { 38 | // name: "Page F", 39 | // Count: 800, 40 | // }, 41 | // { 42 | // name: "Page G", 43 | // Count: 200, 44 | // }, 45 | // ]; 46 | 47 | type Props = { 48 | isDashboard?: boolean; 49 | }; 50 | 51 | export default function OrdersAnalytics({ isDashboard }: Props) { 52 | const {data, isLoading } = useGetOrdersAnalyticsQuery({}); 53 | 54 | const analyticsData: any = []; 55 | 56 | data && 57 | data.orders.last12Months.forEach((item: any) => { 58 | analyticsData.push({ name: item.name, Count: item.count }); 59 | }); 60 | 61 | return ( 62 | <> 63 | {isLoading ? ( 64 | 65 | ) : ( 66 |
67 |
70 |

75 | Orders Analytics 76 |

77 | {!isDashboard && ( 78 |

79 | Last 12 months analytics data{" "} 80 |

81 | )} 82 |
83 |
88 | 92 | 103 | 104 | 105 | 106 | 107 | {!isDashboard && } 108 | 109 | 110 | 111 |
112 |
113 | )} 114 | 115 | ); 116 | } 117 | -------------------------------------------------------------------------------- /models/course.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document, Model, Schema } from "mongoose"; 2 | import { IUser } from "./user.model"; 3 | 4 | export interface IComment extends Document { 5 | user: IUser; 6 | question: string; 7 | questionReplies: IComment[]; 8 | } 9 | 10 | interface IReview extends Document { 11 | user: IUser; 12 | rating?: number; 13 | comment: string; 14 | commentReplies?: IReview[]; 15 | } 16 | 17 | interface ILink extends Document { 18 | title: string; 19 | url: string; 20 | } 21 | 22 | interface ICourseData extends Document { 23 | title: string; 24 | description: string; 25 | videoUrl: string; 26 | videoThumbnail: object; 27 | videoSection: string; 28 | videoLength: number; 29 | videoPlayer: string; 30 | links: ILink[]; 31 | suggestion: string; 32 | questions: IComment[]; 33 | } 34 | 35 | export interface ICourse extends Document { 36 | name: string; 37 | description: string; 38 | categories: string; 39 | price: number; 40 | estimatedPrice?: number; 41 | thumbnail: object; 42 | tags: string; 43 | level: string; 44 | demoUrl: string; 45 | benefits: { title: string }[]; 46 | prerequisites: { title: string }[]; 47 | reviews: IReview[]; 48 | courseData: ICourseData[]; 49 | ratings?: number; 50 | purchased: number; 51 | } 52 | 53 | const reviewSchema = new Schema({ 54 | user: Object, 55 | rating: { 56 | type: Number, 57 | default: 0, 58 | }, 59 | comment: String, 60 | commentReplies: [Object], 61 | },{timestamps:true}); 62 | 63 | const linkSchema = new Schema({ 64 | title: String, 65 | url: String, 66 | }); 67 | 68 | const commentSchema = new Schema({ 69 | user: Object, 70 | question: String, 71 | questionReplies: [Object], 72 | },{timestamps:true}); 73 | 74 | const courseDataSchema = new Schema({ 75 | videoUrl: String, 76 | videoThumbnail: Object, 77 | title: String, 78 | videoSection: String, 79 | description: String, 80 | videoLength: Number, 81 | videoPlayer: String, 82 | links: [linkSchema], 83 | suggestion: String, 84 | questions: [commentSchema], 85 | }); 86 | 87 | const courseSchema = new Schema({ 88 | name: { 89 | type: String, 90 | required: true, 91 | }, 92 | description: { 93 | type: String, 94 | required: true, 95 | }, 96 | categories:{ 97 | type:String, 98 | required: true, 99 | }, 100 | price: { 101 | type: Number, 102 | required: true, 103 | }, 104 | estimatedPrice: { 105 | type: Number, 106 | }, 107 | thumbnail: { 108 | public_id: { 109 | type: String, 110 | }, 111 | url: { 112 | type: String, 113 | }, 114 | }, 115 | tags:{ 116 | type: String, 117 | required: true, 118 | }, 119 | level:{ 120 | type: String, 121 | required: true, 122 | }, 123 | demoUrl:{ 124 | type: String, 125 | required: true, 126 | }, 127 | benefits: [{title: String}], 128 | prerequisites: [{title: String}], 129 | reviews: [reviewSchema], 130 | courseData: [courseDataSchema], 131 | ratings:{ 132 | type: Number, 133 | default: 0, 134 | }, 135 | purchased:{ 136 | type: Number, 137 | default: 0, 138 | }, 139 | },{timestamps: true}); 140 | 141 | 142 | const CourseModel: Model = mongoose.model("Course", courseSchema); 143 | 144 | export default CourseModel; 145 | -------------------------------------------------------------------------------- /client/app/components/Profile/SideBarProfile.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React, { FC } from "react"; 3 | import avatarDefault from "../../../public/assests/avatar.png"; 4 | import { RiLockPasswordLine } from "react-icons/ri"; 5 | import { SiCoursera } from "react-icons/si"; 6 | import { AiOutlineLogout } from "react-icons/ai"; 7 | import { MdOutlineAdminPanelSettings } from "react-icons/md"; 8 | import Link from "next/link"; 9 | 10 | type Props = { 11 | user: any; 12 | active: number; 13 | avatar: string | null; 14 | setActive: (active: number) => void; 15 | logOutHandler: any; 16 | }; 17 | 18 | const SideBarProfile: FC = ({ 19 | user, 20 | active, 21 | avatar, 22 | setActive, 23 | logOutHandler, 24 | }) => { 25 | return ( 26 |
27 |
setActive(1)} 32 | > 33 | 42 |
43 | My Account 44 |
45 |
46 |
setActive(2)} 51 | > 52 | 53 |
54 | Change Password 55 |
56 |
57 |
setActive(3)} 62 | > 63 | 64 |
65 | Enrolled Courses 66 |
67 |
68 | {user.role === "admin" && ( 69 | 75 | 76 |
77 | Admin Dashboard 78 |
79 | 80 | )} 81 |
logOutHandler()} 86 | > 87 | 88 |
89 | Log Out 90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export default SideBarProfile; 97 | -------------------------------------------------------------------------------- /client/redux/features/auth/authApi.ts: -------------------------------------------------------------------------------- 1 | import { apiSlice } from "../api/apiSlice"; 2 | import { userLoggedIn, userLoggedOut, userRegistration } from "./authSlice"; 3 | 4 | type RegistrationResponse = { 5 | message: string; 6 | activationToken: string; 7 | }; 8 | 9 | type RegistrationData = {}; 10 | 11 | export const authApi = apiSlice.injectEndpoints({ 12 | endpoints: (builder) => ({ 13 | // endpoints here 14 | register: builder.mutation({ 15 | query: (data) => ({ 16 | url: "registration", 17 | method: "POST", 18 | body: data, 19 | credentials: "include" as const, 20 | }), 21 | async onQueryStarted(arg, { queryFulfilled, dispatch }) { 22 | try { 23 | const result = await queryFulfilled; 24 | dispatch( 25 | userRegistration({ 26 | token: result.data.activationToken, 27 | }) 28 | ); 29 | } catch (error: any) { 30 | console.log(error); 31 | } 32 | }, 33 | }), 34 | activation: builder.mutation({ 35 | query: ({ activation_token, activation_code }) => ({ 36 | url: "activate-user", 37 | method: "POST", 38 | body: { 39 | activation_token, 40 | activation_code, 41 | }, 42 | }), 43 | }), 44 | login: builder.mutation({ 45 | query: ({ email, password }) => ({ 46 | url: "login", 47 | method: "POST", 48 | body: { 49 | email, 50 | password, 51 | }, 52 | credentials: "include" as const, 53 | }), 54 | async onQueryStarted(arg, { queryFulfilled, dispatch }) { 55 | try { 56 | const result = await queryFulfilled; 57 | dispatch( 58 | userLoggedIn({ 59 | accessToken: result.data.accessToken, 60 | user: result.data.user, 61 | }) 62 | ); 63 | } catch (error: any) { 64 | console.log(error); 65 | } 66 | }, 67 | }), 68 | socialAuth: builder.mutation({ 69 | query: ({ email, name, avatar }) => ({ 70 | url: "social-auth", 71 | method: "POST", 72 | body: { 73 | email, 74 | name, 75 | avatar, 76 | }, 77 | credentials: "include" as const, 78 | }), 79 | async onQueryStarted(arg, { queryFulfilled, dispatch }) { 80 | try { 81 | const result = await queryFulfilled; 82 | dispatch( 83 | userLoggedIn({ 84 | accessToken: result.data.accessToken, 85 | user: result.data.user, 86 | }) 87 | ); 88 | } catch (error: any) { 89 | console.log(error); 90 | } 91 | }, 92 | }), 93 | logOut: builder.query({ 94 | query: () => ({ 95 | url: "logout", 96 | method: "GET", 97 | credentials: "include" as const, 98 | }), 99 | async onQueryStarted(arg, { queryFulfilled, dispatch }) { 100 | try { 101 | dispatch( 102 | userLoggedOut() 103 | ); 104 | } catch (error: any) { 105 | console.log(error); 106 | } 107 | }, 108 | }), 109 | }), 110 | }); 111 | 112 | export const { 113 | useRegisterMutation, 114 | useActivationMutation, 115 | useLoginMutation, 116 | useSocialAuthMutation, 117 | useLogOutQuery 118 | } = authApi; 119 | -------------------------------------------------------------------------------- /client/redux/features/courses/coursesApi.ts: -------------------------------------------------------------------------------- 1 | import { apiSlice } from "../api/apiSlice"; 2 | 3 | export const coursesApi = apiSlice.injectEndpoints({ 4 | endpoints: (builder) => ({ 5 | createCourse: builder.mutation({ 6 | query: (data) => ({ 7 | url: "create-course", 8 | method: "POST", 9 | body: data, 10 | credentials: "include" as const, 11 | }), 12 | }), 13 | getAllCourses: builder.query({ 14 | query: () => ({ 15 | url: "get-admin-courses", 16 | method: "GET", 17 | credentials: "include" as const, 18 | }), 19 | }), 20 | deleteCourse: builder.mutation({ 21 | query: (id) => ({ 22 | url: `delete-course/${id}`, 23 | method: "DELETE", 24 | credentials: "include" as const, 25 | }), 26 | }), 27 | editCourse: builder.mutation({ 28 | query: ({ id, data }) => ({ 29 | url: `edit-course/${id}`, 30 | method: "PUT", 31 | body: data, 32 | credentials: "include" as const, 33 | }), 34 | }), 35 | getUsersAllCourses: builder.query({ 36 | query: () => ({ 37 | url: "get-courses", 38 | method: "GET", 39 | credentials: "include" as const, 40 | }), 41 | }), 42 | getCourseDetails: builder.query({ 43 | query: (id: any) => ({ 44 | url: `get-course/${id}`, 45 | method: "GET", 46 | credentials: "include" as const, 47 | }), 48 | }), 49 | getCourseContent: builder.query({ 50 | query: (id) => ({ 51 | url: `get-course-content/${id}`, 52 | method: "GET", 53 | credentials: "include" as const, 54 | }), 55 | }), 56 | addNewQuestion: builder.mutation({ 57 | query: ({ question, courseId, contentId }) => ({ 58 | url: "add-question", 59 | body: { 60 | question, 61 | courseId, 62 | contentId, 63 | }, 64 | method: "PUT", 65 | credentials: "include" as const, 66 | }), 67 | }), 68 | addAnswerInQuestion: builder.mutation({ 69 | query: ({ answer, courseId, contentId, questionId }) => ({ 70 | url: "add-answer", 71 | body: { 72 | answer, 73 | courseId, 74 | contentId, 75 | questionId, 76 | }, 77 | method: "PUT", 78 | credentials: "include" as const, 79 | }), 80 | }), 81 | addReviewInCourse: builder.mutation({ 82 | query: ({ review, rating, courseId }: any) => ({ 83 | url: `add-review/${courseId}`, 84 | body: { 85 | review, 86 | rating, 87 | }, 88 | method: "PUT", 89 | credentials: "include" as const, 90 | }), 91 | }), 92 | addReplyInReview: builder.mutation({ 93 | query: ({ comment, courseId, reviewId }: any) => ({ 94 | url: `add-reply`, 95 | body: { 96 | comment, courseId, reviewId 97 | }, 98 | method: "PUT", 99 | credentials: "include" as const, 100 | }), 101 | }), 102 | }), 103 | }); 104 | 105 | export const { 106 | useCreateCourseMutation, 107 | useGetAllCoursesQuery, 108 | useDeleteCourseMutation, 109 | useEditCourseMutation, 110 | useGetUsersAllCoursesQuery, 111 | useGetCourseDetailsQuery, 112 | useGetCourseContentQuery, 113 | useAddNewQuestionMutation, 114 | useAddAnswerInQuestionMutation, 115 | useAddReviewInCourseMutation, 116 | useAddReplyInReviewMutation 117 | } = coursesApi; 118 | -------------------------------------------------------------------------------- /client/app/components/Profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { FC, useEffect, useState } from "react"; 3 | import SideBarProfile from "./SideBarProfile"; 4 | import { useLogOutQuery } from "../../../redux/features/auth/authApi"; 5 | import { signOut } from "next-auth/react"; 6 | import ProfileInfo from "./ProfileInfo"; 7 | import ChangePassword from "./ChangePassword"; 8 | import CourseCard from "../Course/CourseCard"; 9 | import { useGetUsersAllCoursesQuery } from "@/redux/features/courses/coursesApi"; 10 | 11 | type Props = { 12 | user: any; 13 | }; 14 | 15 | const Profile: FC = ({ user }) => { 16 | const [scroll, setScroll] = useState(false); 17 | const [avatar, setAvatar] = useState(null); 18 | const [logout, setLogout] = useState(false); 19 | const [courses, setCourses] = useState([]); 20 | const { data, isLoading } = useGetUsersAllCoursesQuery(undefined, {}); 21 | 22 | const {} = useLogOutQuery(undefined, { 23 | skip: !logout ? true : false, 24 | }); 25 | 26 | const [active, setActive] = useState(1); 27 | 28 | const logOutHandler = async () => { 29 | setLogout(true); 30 | await signOut(); 31 | }; 32 | 33 | if (typeof window !== "undefined") { 34 | window.addEventListener("scroll", () => { 35 | if (window.scrollY > 85) { 36 | setScroll(true); 37 | } else { 38 | setScroll(false); 39 | } 40 | }); 41 | } 42 | 43 | useEffect(() => { 44 | if (data) { 45 | const filteredCourses = user.courses 46 | .map((userCourse: any) => 47 | data.courses.find((course: any) => course._id === userCourse._id) 48 | ) 49 | .filter((course: any) => course !== undefined); 50 | setCourses(filteredCourses); 51 | } 52 | }, [data]); 53 | 54 | return ( 55 |
56 |
61 | 68 |
69 | {active === 1 && ( 70 |
71 | 72 |
73 | )} 74 | 75 | {active === 2 && ( 76 |
77 | 78 |
79 | )} 80 | 81 | {active === 3 && ( 82 |
83 |
84 | {courses && 85 | courses.map((item: any, index: number) => ( 86 | 87 | ))} 88 |
89 | {courses.length === 0 && ( 90 |

91 | You don't have any purchased courses! 92 |

93 | )} 94 |
95 | )} 96 |
97 | ); 98 | }; 99 | 100 | export default Profile; 101 | -------------------------------------------------------------------------------- /client/app/components/Profile/ChangePassword.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from "@/app/styles/style"; 2 | import { useUpdatePasswordMutation } from "@/redux/features/user/userApi"; 3 | import React, { FC, useEffect, useState } from "react"; 4 | import { toast } from "react-hot-toast"; 5 | 6 | type Props = {}; 7 | 8 | const ChangePassword: FC = (props) => { 9 | const [oldPassword, setOldPassword] = useState(""); 10 | const [newPassword, setNewPassword] = useState(""); 11 | const [confirmPassword, setConfirmPassword] = useState(""); 12 | const [updatePassword, { isSuccess, error }] = useUpdatePasswordMutation(); 13 | 14 | const passwordChangeHandler = async (e: any) => { 15 | e.preventDefault(); 16 | if (newPassword !== confirmPassword) { 17 | toast.error("Passwords do not match"); 18 | } else { 19 | await updatePassword({ oldPassword, newPassword }); 20 | } 21 | }; 22 | 23 | useEffect(() => { 24 | if (isSuccess) { 25 | toast.success("Password changed successfully"); 26 | } 27 | if (error) { 28 | if ("data" in error) { 29 | const errorData = error as any; 30 | toast.error(errorData.data.message); 31 | } 32 | } 33 | }, [isSuccess, error]); 34 | 35 | return ( 36 |
37 |

38 | Change Password 39 |

40 |
41 |
46 |
47 | 50 | setOldPassword(e.target.value)} 56 | /> 57 |
58 |
59 | 62 | setNewPassword(e.target.value)} 68 | /> 69 |
70 |
71 | 74 | setConfirmPassword(e.target.value)} 80 | /> 81 | 87 |
88 |
89 |
90 |
91 | ); 92 | }; 93 | 94 | export default ChangePassword; 95 | -------------------------------------------------------------------------------- /client/app/components/Admin/DashboardHeader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { ThemeSwitcher } from "@/app/utils/ThemeSwitcher"; 3 | import { 4 | useGetAllNotificationsQuery, 5 | useUpdateNotificationStatusMutation, 6 | } from "@/redux/features/notifications/notificationsApi"; 7 | import React, { FC, useEffect, useState } from "react"; 8 | import { IoMdNotificationsOutline } from "react-icons/io"; 9 | import socketIO from "socket.io-client"; 10 | import { format } from "timeago.js"; 11 | const ENDPOINT = process.env.NEXT_PUBLIC_SOCKET_SERVER_URI || ""; 12 | const socketId = socketIO(ENDPOINT, { transports: ["websocket"] }); 13 | 14 | type Props = { 15 | open?: boolean; 16 | setOpen?: any; 17 | }; 18 | 19 | const DashboardHeader: FC = ({ open, setOpen }) => { 20 | const { data, refetch } = useGetAllNotificationsQuery(undefined, { 21 | refetchOnMountOrArgChange: true, 22 | }); 23 | const [updateNotificationStatus, { isSuccess }] = 24 | useUpdateNotificationStatusMutation(); 25 | const [notifications, setNotifications] = useState([]); 26 | const [audio] = useState( 27 | typeof window !== "undefined" && 28 | new Audio( 29 | "https://res.cloudinary.com/damk25wo5/video/upload/v1693465789/notification_vcetjn.mp3" 30 | ) 31 | ); 32 | 33 | const playNotificationSound = () => { 34 | audio.play(); 35 | }; 36 | 37 | useEffect(() => { 38 | if (data) { 39 | setNotifications( 40 | data.notifications.filter((item: any) => item.status === "unread") 41 | ); 42 | } 43 | if (isSuccess) { 44 | refetch(); 45 | } 46 | audio.load(); 47 | }, [data, isSuccess,audio]); 48 | 49 | useEffect(() => { 50 | socketId.on("newNotification", (data) => { 51 | refetch(); 52 | playNotificationSound(); 53 | }); 54 | }, []); 55 | 56 | const handleNotificationStatusChange = async (id: string) => { 57 | await updateNotificationStatus(id); 58 | }; 59 | 60 | return ( 61 |
62 | 63 |
setOpen(!open)} 66 | > 67 | 68 | 69 | {notifications && notifications.length} 70 | 71 |
72 | {open && ( 73 |
74 |
75 | Notifications 76 |
77 | {notifications && 78 | notifications.map((item: any, index: number) => ( 79 |
83 |
84 |

{item.title}

85 |

handleNotificationStatusChange(item._id)} 88 | > 89 | Mark as read 90 |

91 |
92 |

93 | {item.message} 94 |

95 |

96 | {format(item.createdAt)} 97 |

98 |
99 | ))} 100 |
101 | )} 102 |
103 | ); 104 | }; 105 | 106 | export default DashboardHeader; 107 | -------------------------------------------------------------------------------- /client/app/components/Admin/Course/CourseData.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from "@/app/styles/style"; 2 | import React, { FC } from "react"; 3 | import {AiOutlinePlusCircle} from "react-icons/ai"; 4 | import { toast } from "react-hot-toast"; 5 | 6 | type Props = { 7 | benefits: { title: string }[]; 8 | setBenefits: (benefits: { title: string }[]) => void; 9 | prerequisites: { title: string }[]; 10 | setPrerequisites: (prerequisites: { title: string }[]) => void; 11 | active: number; 12 | setActive: (active: number) => void; 13 | }; 14 | 15 | const CourseData: FC = ({ 16 | benefits, 17 | setBenefits, 18 | prerequisites, 19 | setPrerequisites, 20 | active, 21 | setActive, 22 | }) => { 23 | 24 | const handleBenefitChange = (index: number, value: any) => { 25 | const updatedBenefits = [...benefits]; 26 | updatedBenefits[index].title = value; 27 | setBenefits(updatedBenefits); 28 | }; 29 | 30 | const handleAddBenefit = () => { 31 | setBenefits([...benefits, { title: "" }]); 32 | }; 33 | 34 | const handlePrerequisitesChange = (index: number, value: any) => { 35 | const updatedPrerequisites = [...prerequisites]; 36 | updatedPrerequisites[index].title = value; 37 | setPrerequisites(updatedPrerequisites); 38 | }; 39 | 40 | const handleAddPrerequisites = () => { 41 | setPrerequisites([...prerequisites, { title: "" }]); 42 | }; 43 | 44 | const prevButton = () => { 45 | setActive(active - 1); 46 | } 47 | 48 | const handleOptions = () => { 49 | if (benefits[benefits.length - 1]?.title !== "" && prerequisites[prerequisites.length - 1]?.title !== "") { 50 | setActive(active + 1); 51 | } else{ 52 | toast.error("Please fill the fields for go to next!") 53 | } 54 | }; 55 | 56 | 57 | return ( 58 |
59 |
60 | 63 |
64 | {benefits.map((benefit: any, index: number) => ( 65 | handleBenefitChange(index, e.target.value)} 74 | /> 75 | ))} 76 | 80 |
81 | 82 |
83 | 86 |
87 | {prerequisites.map((prerequisites: any, index: number) => ( 88 | handlePrerequisitesChange(index, e.target.value)} 97 | /> 98 | ))} 99 | 104 |
105 |
106 |
prevButton()} 109 | > 110 | Prev 111 |
112 |
handleOptions()} 115 | > 116 | Next 117 |
118 |
119 |
120 | ); 121 | }; 122 | 123 | export default CourseData; 124 | -------------------------------------------------------------------------------- /client/app/components/Route/Hero.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useGetHeroDataQuery } from "@/redux/features/layout/layoutApi"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import React, { FC, useState } from "react"; 6 | import { BiSearch } from "react-icons/bi"; 7 | import Loader from "../Loader/Loader"; 8 | import { useRouter } from "next/navigation"; 9 | import img1 from '../../../public/assests/banner-img-1.png' 10 | 11 | type Props = {}; 12 | 13 | const Hero: FC = (props) => { 14 | const { data,isLoading } = useGetHeroDataQuery("Banner", {}); 15 | const [search,setSearch] = useState(""); 16 | const router = useRouter() 17 | 18 | 19 | const handleSearch = () => { 20 | if(search === ""){ 21 | return 22 | }else{ 23 | router.push(`/courses?title=${search}`); 24 | } 25 | } 26 | 27 | 28 | return ( 29 | <> 30 | { 31 | isLoading ? ( 32 | 33 | ) : ( 34 |
35 |
36 |
37 | 44 |
45 |
46 |

47 | {data?.layout?.banner?.title} 48 |

49 |
50 |

51 | {data?.layout?.banner?.subTitle} 52 |

53 |
54 |
55 |
56 | setSearch(e.target.value)} 61 | className="bg-transparent border dark:border-none dark:bg-[#575757] dark:placeholder:text-[#ffffffdd] rounded-[5px] p-2 w-full h-full outline-none text-[#0000004e] dark:text-[#ffffffe6] text-[20px] font-[500] font-Josefin" 62 | /> 63 |
66 | 67 |
68 |
69 |
70 |
71 |
72 | 77 | 82 | 87 |

88 | 500K+ People already trusted us.{" "} 89 | 93 | View Courses 94 | {" "} 95 |

96 |
97 |
98 |
99 |
100 | ) 101 | } 102 | 103 | ); 104 | }; 105 | 106 | export default Hero; 107 | -------------------------------------------------------------------------------- /client/app/components/Auth/Verification.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from "@/app/styles/style"; 2 | import { useActivationMutation } from "@/redux/features/auth/authApi"; 3 | import React, { FC, useEffect, useRef, useState } from "react"; 4 | import { toast } from "react-hot-toast"; 5 | import { VscWorkspaceTrusted } from "react-icons/vsc"; 6 | import { useSelector } from "react-redux"; 7 | 8 | type Props = { 9 | setRoute: (route: string) => void; 10 | }; 11 | 12 | type VerifyNumber = { 13 | "0": string; 14 | "1": string; 15 | "2": string; 16 | "3": string; 17 | }; 18 | 19 | const Verification: FC = ({ setRoute }) => { 20 | const { token } = useSelector((state: any) => state.auth); 21 | const [activation, { isSuccess, error }] = useActivationMutation(); 22 | const [invalidError, setInvalidError] = useState(false); 23 | 24 | useEffect(() => { 25 | if (isSuccess) { 26 | toast.success("Account activated successfully"); 27 | setRoute("Login"); 28 | } 29 | if (error) { 30 | if ("data" in error) { 31 | const errorData = error as any; 32 | toast.error(errorData.data.message); 33 | setInvalidError(true); 34 | } else { 35 | console.log("An error occured:", error); 36 | } 37 | } 38 | }, [isSuccess, error]); 39 | 40 | const inputRefs = [ 41 | useRef(null), 42 | useRef(null), 43 | useRef(null), 44 | useRef(null), 45 | ]; 46 | 47 | const [verifyNumber, setVerifyNumber] = useState({ 48 | 0: "", 49 | 1: "", 50 | 2: "", 51 | 3: "", 52 | }); 53 | 54 | const verificationHandler = async () => { 55 | const verificationNumber = Object.values(verifyNumber).join(""); 56 | if (verificationNumber.length !== 4) { 57 | setInvalidError(true); 58 | return; 59 | } 60 | await activation({ 61 | activation_token: token, 62 | activation_code: verificationNumber, 63 | }); 64 | }; 65 | 66 | const handleInputChange = (index: number, value: string) => { 67 | setInvalidError(false); 68 | const newVerifyNumber = { ...verifyNumber, [index]: value }; 69 | setVerifyNumber(newVerifyNumber); 70 | 71 | if (value === "" && index > 0) { 72 | inputRefs[index - 1].current?.focus(); 73 | } else if (value.length === 1 && index < 3) { 74 | inputRefs[index + 1].current?.focus(); 75 | } 76 | }; 77 | 78 | return ( 79 |
80 |

Verify Your Account

81 |
82 |
83 |
84 | 85 |
86 |
87 |
88 |
89 |
90 | {Object.keys(verifyNumber).map((key, index) => ( 91 | handleInputChange(index, e.target.value)} 104 | /> 105 | ))} 106 |
107 |
108 |
109 |
110 | 113 |
114 |
115 |
116 | Go back to sign in?{" "} 117 | setRoute("Login")} 120 | > 121 | Sign in 122 | 123 |
124 |
125 | ); 126 | }; 127 | 128 | export default Verification; 129 | -------------------------------------------------------------------------------- /client/app/components/Profile/ProfileInfo.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { styles } from "../../../app/styles/style"; 3 | import React, { FC, useEffect, useState } from "react"; 4 | import { AiOutlineCamera } from "react-icons/ai"; 5 | import avatarIcon from "../../../public/assests/avatar.png"; 6 | import { 7 | useEditProfileMutation, 8 | useUpdateAvatarMutation, 9 | } from "@/redux/features/user/userApi"; 10 | import { useLoadUserQuery } from "@/redux/features/api/apiSlice"; 11 | import { toast } from "react-hot-toast"; 12 | 13 | type Props = { 14 | avatar: string | null; 15 | user: any; 16 | }; 17 | 18 | const ProfileInfo: FC = ({ avatar, user }) => { 19 | const [name, setName] = useState(user && user.name); 20 | const [updateAvatar, { isSuccess, error }] = useUpdateAvatarMutation(); 21 | const [editProfile, { isSuccess: success, error: updateError }] = 22 | useEditProfileMutation(); 23 | const [loadUser, setLoadUser] = useState(false); 24 | const {} = useLoadUserQuery(undefined, { skip: loadUser ? false : true }); 25 | 26 | const imageHandler = async (e: any) => { 27 | const fileReader = new FileReader(); 28 | 29 | fileReader.onload = () => { 30 | if (fileReader.readyState === 2) { 31 | const avatar = fileReader.result; 32 | updateAvatar(avatar); 33 | } 34 | }; 35 | fileReader.readAsDataURL(e.target.files[0]); 36 | }; 37 | 38 | useEffect(() => { 39 | if (isSuccess) { 40 | setLoadUser(true); 41 | } 42 | if (error || updateError) { 43 | console.log(error); 44 | } 45 | if(success){ 46 | toast.success("Profile updated successfully!"); 47 | setLoadUser(true); 48 | } 49 | }, [isSuccess, error,success, updateError]); 50 | 51 | const handleSubmit = async (e: any) => { 52 | e.preventDefault(); 53 | if (name !== "") { 54 | await editProfile({ 55 | name: name, 56 | }); 57 | } 58 | }; 59 | 60 | return ( 61 | <> 62 |
63 |
64 | 71 | 79 | 84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | 93 | setName(e.target.value)} 99 | /> 100 |
101 |
102 | 103 | 110 |
111 | 117 |
118 |
119 |
120 |
121 | 122 | ); 123 | }; 124 | 125 | export default ProfileInfo; 126 | -------------------------------------------------------------------------------- /client/app/courses/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useGetUsersAllCoursesQuery } from "@/redux/features/courses/coursesApi"; 3 | import { useGetHeroDataQuery } from "@/redux/features/layout/layoutApi"; 4 | import { useSearchParams } from "next/navigation"; 5 | import React, { useEffect, useState } from "react"; 6 | import Loader from "../components/Loader/Loader"; 7 | import Header from "../components/Header"; 8 | import Heading from "../utils/Heading"; 9 | import { styles } from "../styles/style"; 10 | import CourseCard from "../components/Course/CourseCard"; 11 | import Footer from "../components/Footer"; 12 | 13 | type Props = {}; 14 | 15 | const Page = (props: Props) => { 16 | const searchParams = useSearchParams(); 17 | const search = searchParams?.get("title"); 18 | const { data, isLoading } = useGetUsersAllCoursesQuery(undefined, {}); 19 | const { data: categoriesData } = useGetHeroDataQuery("Categories", {}); 20 | const [route, setRoute] = useState("Login"); 21 | const [open, setOpen] = useState(false); 22 | const [courses, setcourses] = useState([]); 23 | const [category, setCategory] = useState("All"); 24 | 25 | useEffect(() => { 26 | if (category === "All") { 27 | setcourses(data?.courses); 28 | } 29 | if (category !== "All") { 30 | setcourses( 31 | data?.courses.filter((item: any) => item.categories === category) 32 | ); 33 | } 34 | if (search) { 35 | setcourses( 36 | data?.courses.filter((item: any) => 37 | item.name.toLowerCase().includes(search.toLowerCase()) 38 | ) 39 | ); 40 | } 41 | }, [data, category, search]); 42 | 43 | const categories = categoriesData?.layout?.categories; 44 | 45 | return ( 46 |
47 | {isLoading ? ( 48 | 49 | ) : ( 50 | <> 51 |
58 |
59 | 66 |
67 |
68 |
setCategory("All")} 73 | > 74 | All 75 |
76 | {categories && 77 | categories.map((item: any, index: number) => ( 78 |
79 |
setCategory(item.title)} 86 | > 87 | {item.title} 88 |
89 |
90 | ))} 91 |
92 | { 93 | courses && courses.length === 0 && ( 94 |

95 | {search ? "No courses found!" : "No courses found in this category. Please try another one!"} 96 |

97 | ) 98 | } 99 |
100 |
101 |
102 | {courses && 103 | courses.map((item: any, index: number) => ( 104 | 105 | ))} 106 |
107 |
108 |
109 | 110 | )} 111 |
112 | ); 113 | }; 114 | 115 | export default Page; 116 | -------------------------------------------------------------------------------- /client/app/policy/Policy.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { styles } from "../styles/style"; 3 | 4 | type Props = {}; 5 | 6 | const Policy = (props: Props) => { 7 | return ( 8 |
9 |
10 |

11 | Platform Terms and Condition 12 |

13 |
    14 |

    15 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 16 | blanditiis architecto quasi impedit in dicta nisi, asperiores 17 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 18 | accusantium quod numquam dolores! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 19 | blanditiis architecto quasi impedit in dicta nisi, asperiores 20 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 21 | accusantium quod numquam dolores! 22 |

    23 |
    24 |

    25 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 26 | blanditiis architecto quasi impedit in dicta nisi, asperiores 27 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 28 | accusantium quod numquam dolores! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 29 | blanditiis architecto quasi impedit in dicta nisi, asperiores 30 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 31 | accusantium quod numquam dolores! 32 |

    33 |
    34 |

    35 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 36 | blanditiis architecto quasi impedit in dicta nisi, asperiores 37 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 38 | accusantium quod numquam dolores! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 39 | blanditiis architecto quasi impedit in dicta nisi, asperiores 40 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 41 | accusantium quod numquam dolores! 42 |

    43 |
    44 |

    45 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 46 | blanditiis architecto quasi impedit in dicta nisi, asperiores 47 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 48 | accusantium quod numquam dolores! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 49 | blanditiis architecto quasi impedit in dicta nisi, asperiores 50 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 51 | accusantium quod numquam dolores! 52 |

    53 |
    54 |

    55 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 56 | blanditiis architecto quasi impedit in dicta nisi, asperiores 57 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 58 | accusantium quod numquam dolores! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 59 | blanditiis architecto quasi impedit in dicta nisi, asperiores 60 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 61 | accusantium quod numquam dolores! 62 |

    63 |
    64 |

    65 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 66 | blanditiis architecto quasi impedit in dicta nisi, asperiores 67 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 68 | accusantium quod numquam dolores! Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facere 69 | blanditiis architecto quasi impedit in dicta nisi, asperiores 70 | voluptatum eos alias facilis assumenda ex beatae, culpa dignissimos 71 | accusantium quod numquam dolores! 72 |

    73 |
74 |
75 |
76 | ); 77 | }; 78 | 79 | export default Policy; 80 | -------------------------------------------------------------------------------- /client/app/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import React from 'react' 3 | 4 | type Props = {} 5 | 6 | const Footer = (props: Props) => { 7 | return ( 8 |
9 |
10 |
11 |
12 |
13 |
14 |

About

15 |
    16 |
  • 17 | 21 | Our Story 22 | 23 |
  • 24 |
  • 25 | 29 | Privacy Policy 30 | 31 |
  • 32 |
  • 33 | 37 | FAQ 38 | 39 |
  • 40 |
41 |
42 |
43 |

Quick Links

44 |
    45 |
  • 46 | 50 | Courses 51 | 52 |
  • 53 |
  • 54 | 58 | My Account 59 | 60 |
  • 61 |
  • 62 | 66 | Course Dashboard 67 | 68 |
  • 69 |
70 |
71 |
72 |

Social Links

73 |
    74 |
  • 75 | 79 | Youtube 80 | 81 |
  • 82 |
  • 83 | 87 | Instagram 88 | 89 |
  • 90 |
  • 91 | 95 | github 96 | 97 |
  • 98 |
99 |
100 |
101 |

Contact Info

102 |

103 | Call Us: 1-885-665-2022 104 |

105 | 106 |

107 | Address: +7011 Vermont Ave, Los Angeles, CA 90044 108 |

109 | 110 |

111 | Mail Us: hello@elearning.com 112 |

113 | 114 |
115 |
116 |
117 |

118 | Copyright © 2023 ELearning | All Rights Reserved 119 |

120 |
121 |
122 |
123 | ) 124 | } 125 | 126 | export default Footer -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🌟 Introduction 2 | 3 | # 🚀 E-learning - Learning Management System 4 | 5 | ## 📋 Introduction 6 | 7 | E-learning is a comprehensive Learning Management System (LMS) built with modern technologies. It provides a robust platform for creating, managing, and delivering online courses with features like user authentication, course management, real-time notifications, and advanced administrative controls. 8 | 9 | ## ✨ Features 10 | 11 | ### 👥 User Management 12 | 13 | - Secure user registration and login system 14 | - Email verification for account activation 15 | - Social authentication integration 16 | - JWT-based authentication with access token refresh 17 | - Profile management with avatar upload 18 | - Password reset functionality 19 | 20 | ### 📚 Course Management 21 | 22 | - Course creation and editing interface 23 | - Rich content management system 24 | - Course preview functionality 25 | - Student enrollment system 26 | - Progress tracking 27 | - Q&A section with threaded discussions 28 | - Course review and rating system 29 | 30 | ### 💡 Learning Experience 31 | 32 | - Intuitive course navigation 33 | - Interactive content delivery 34 | - Question and answer forum 35 | - Course reviews and ratings 36 | - Progress tracking 37 | - Personalized dashboard 38 | 39 | ### 👨‍💼 Administration 40 | 41 | - Comprehensive admin dashboard 42 | - User management system 43 | - Course oversight and moderation 44 | - Team member management 45 | - Analytics and reporting 46 | - Last 28 days user statistics 47 | - Annual order analytics 48 | - Notification metrics 49 | 50 | ### 🎨 Content Customization 51 | 52 | - Dynamic layout management 53 | - FAQ management 54 | - Hero banner customization 55 | - Course category organization 56 | - Responsive design 57 | 58 | ### ⚙️ Technical Features 59 | 60 | - Advanced caching system 61 | - Real-time notifications 62 | - Cloud-based media management 63 | - Redis integration 64 | - Secure payment processing 65 | - Automated notification cleanup 66 | - Error handling system 67 | 68 | ## 🌐 Live Preview 69 | 70 | [Visit E-learning](https://elearninglms.netlify.app/) 71 | 72 | ## 🛠️ Tech Stack 73 | 74 | ### Frontend 75 | 76 | - Next.js 77 | - Redux Toolkit 78 | - TailwindCSS 79 | - Socket.io-client 80 | 81 | ### Backend 82 | 83 | - Node.js 84 | - Express.js 85 | - MongoDB 86 | - Redis 87 | - Socket.io 88 | 89 | ### Cloud Services 90 | 91 | - Cloudinary (Media Management) 92 | - JWT (Authentication) 93 | - OAuth (Social Login) 94 | 95 | ### Development Tools 96 | 97 | - TypeScript 98 | - ESLint 99 | - Prettier 100 | - Git 101 | 102 | ## Test Credential 103 | 104 | - Email: test@gmail.com 105 | - Password: test123 106 | 107 | ## 📱 Screenshots 108 | 109 | 110 | 111 | 115 | 119 | 120 | 121 | 125 | 129 | 130 | 131 | 135 | 136 |
112 | Home Page 113 |

Home Page

114 |
116 | Course Page 117 |

Course Page

118 |
122 | Profile Page 123 |

Profile Page

124 |
126 | Enroll Course List 127 |

Enroll Course List

128 |
132 | Course Details Page 133 |

Course Details Page

134 |
137 | 138 | 139 | ## 📱 Admin Side Screenshots 140 | 141 | 142 | 143 | 147 | 151 | 152 | 153 | 157 | 161 | 162 | 163 |
144 | Dashboard Page 145 |

Dashboard Page

146 |
148 | 149 |

Order Analysis Page

150 |
154 | 155 |

Create Course Page

156 |
158 | 159 |

Course List

160 |
164 | -------------------------------------------------------------------------------- /client/app/components/Auth/Login.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { FC, useEffect, useState } from "react"; 3 | import { useFormik } from "formik"; 4 | import * as Yup from "yup"; 5 | import { 6 | AiOutlineEye, 7 | AiOutlineEyeInvisible, 8 | AiFillGithub, 9 | } from "react-icons/ai"; 10 | import { FcGoogle } from "react-icons/fc"; 11 | import { styles } from "../../../app/styles/style"; 12 | import { useLoginMutation } from "@/redux/features/auth/authApi"; 13 | import { toast } from "react-hot-toast"; 14 | import {signIn} from "next-auth/react"; 15 | 16 | type Props = { 17 | setRoute: (route: string) => void; 18 | setOpen: (open: boolean) => void; 19 | refetch:any; 20 | }; 21 | 22 | const schema = Yup.object().shape({ 23 | email: Yup.string() 24 | .email("Invalid email!") 25 | .required("Please enter your email!"), 26 | password: Yup.string().required("Please enter your password!").min(6), 27 | }); 28 | 29 | const Login: FC = ({ setRoute, setOpen,refetch }) => { 30 | const [show, setShow] = useState(false); 31 | const [login, { isSuccess, error }] = useLoginMutation(); 32 | const formik = useFormik({ 33 | initialValues: { email: "", password: "" }, 34 | validationSchema: schema, 35 | onSubmit: async ({ email, password }) => { 36 | await login({ email, password }); 37 | }, 38 | }); 39 | 40 | useEffect(() => { 41 | if (isSuccess) { 42 | toast.success("Login Successfully!"); 43 | setOpen(false); 44 | refetch(); 45 | } 46 | if (error) { 47 | if ("data" in error) { 48 | const errorData = error as any; 49 | toast.error(errorData.data.message); 50 | } 51 | } 52 | }, [isSuccess, error]); 53 | 54 | const { errors, touched, values, handleChange, handleSubmit } = formik; 55 | 56 | return ( 57 |
58 |

Login with ELearning

59 |
60 | 63 | 74 | {errors.email && touched.email && ( 75 | {errors.email} 76 | )} 77 |
78 | 81 | 92 | {!show ? ( 93 | setShow(true)} 97 | /> 98 | ) : ( 99 | setShow(false)} 103 | /> 104 | )} 105 | {errors.password && touched.password && ( 106 | {errors.password} 107 | )} 108 |
109 |
110 | 111 |
112 |
113 |
114 | Or join with 115 |
116 |
117 | signIn("google")} 119 | /> 120 | signIn("github")} /> 121 |
122 |
123 | Not have any account?{" "} 124 | setRoute("Sign-Up")} 127 | > 128 | Sign up 129 | 130 |
131 |
132 |
133 |
134 | ); 135 | }; 136 | 137 | export default Login; 138 | -------------------------------------------------------------------------------- /client/app/components/Admin/Customization/EditCategories.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useEditLayoutMutation, 3 | useGetHeroDataQuery, 4 | } from "@/redux/features/layout/layoutApi"; 5 | import React, { useEffect, useState } from "react"; 6 | import Loader from "../../Loader/Loader"; 7 | import { styles } from "@/app/styles/style"; 8 | import { AiOutlineDelete } from "react-icons/ai"; 9 | import { IoMdAddCircleOutline } from "react-icons/io"; 10 | import { toast } from "react-hot-toast"; 11 | 12 | type Props = {}; 13 | 14 | const EditCategories = (props: Props) => { 15 | const { data, isLoading,refetch } = useGetHeroDataQuery("Categories", { 16 | refetchOnMountOrArgChange: true, 17 | }); 18 | const [editLayout, { isSuccess: layoutSuccess, error }] = 19 | useEditLayoutMutation(); 20 | const [categories, setCategories] = useState([]); 21 | 22 | useEffect(() => { 23 | if (data) { 24 | setCategories(data.layout?.categories); 25 | } 26 | if (layoutSuccess) { 27 | refetch(); 28 | toast.success("Categories updated successfully"); 29 | } 30 | 31 | if (error) { 32 | if ("data" in error) { 33 | const errorData = error as any; 34 | toast.error(errorData?.data?.message); 35 | } 36 | } 37 | }, [data, layoutSuccess, error,refetch]); 38 | 39 | const handleCategoriesAdd = (id: any, value: string) => { 40 | setCategories((prevCategory: any) => 41 | prevCategory.map((i: any) => (i._id === id ? { ...i, title: value } : i)) 42 | ); 43 | }; 44 | 45 | const newCategoriesHandler = () => { 46 | if (categories[categories.length - 1].title === "") { 47 | toast.error("Category title cannot be empty"); 48 | } else { 49 | setCategories((prevCategory: any) => [...prevCategory, { title: "" }]); 50 | } 51 | }; 52 | 53 | const areCategoriesUnchanged = ( 54 | originalCategories: any[], 55 | newCategories: any[] 56 | ) => { 57 | return JSON.stringify(originalCategories) === JSON.stringify(newCategories); 58 | }; 59 | 60 | const isAnyCategoryTitleEmpty = (categories: any[]) => { 61 | return categories.some((q) => q.title === ""); 62 | }; 63 | 64 | const editCategoriesHandler = async () => { 65 | if ( 66 | !areCategoriesUnchanged(data.layout?.categories, categories) && 67 | !isAnyCategoryTitleEmpty(categories) 68 | ) { 69 | await editLayout({ 70 | type: "Categories", 71 | categories, 72 | }); 73 | } 74 | }; 75 | 76 | return ( 77 | <> 78 | {isLoading ? ( 79 | 80 | ) : ( 81 |
82 |

All Categories

83 | {categories && 84 | categories.map((item: any, index: number) => { 85 | return ( 86 |
87 |
88 | 92 | handleCategoriesAdd(item._id, e.target.value) 93 | } 94 | placeholder="Enter category title..." 95 | /> 96 | { 99 | setCategories((prevCategory: any) => 100 | prevCategory.filter((i: any) => i._id !== item._id) 101 | ); 102 | }} 103 | /> 104 |
105 |
106 | ); 107 | })} 108 |
109 |
110 |
111 | 115 |
116 |
null 131 | : editCategoriesHandler 132 | } 133 | > 134 | Save 135 |
136 |
137 | )} 138 | 139 | ); 140 | }; 141 | 142 | export default EditCategories; 143 | -------------------------------------------------------------------------------- /client/app/components/Admin/Customization/EditHero.tsx: -------------------------------------------------------------------------------- 1 | import { styles } from "@/app/styles/style"; 2 | import { 3 | useEditLayoutMutation, 4 | useGetHeroDataQuery, 5 | } from "@/redux/features/layout/layoutApi"; 6 | import React, { FC, useEffect, useState } from "react"; 7 | import { toast } from "react-hot-toast"; 8 | import { AiOutlineCamera } from "react-icons/ai"; 9 | 10 | type Props = {}; 11 | 12 | const EditHero: FC = (props: Props) => { 13 | const [image, setImage] = useState(""); 14 | const [title, setTitle] = useState(""); 15 | const [subTitle, setSubTitle] = useState(""); 16 | const { data,refetch } = useGetHeroDataQuery("Banner", { 17 | refetchOnMountOrArgChange: true 18 | }); 19 | const [editLayout, { isLoading, isSuccess, error }] = useEditLayoutMutation(); 20 | 21 | useEffect(() => { 22 | if (data) { 23 | setTitle(data?.layout?.banner.title); 24 | setSubTitle(data?.layout?.banner.subTitle); 25 | setImage(data?.layout?.banner?.image?.url); 26 | } 27 | if (isSuccess) { 28 | toast.success("Hero updated successfully!"); 29 | refetch(); 30 | } 31 | if (error) { 32 | if ("data" in error) { 33 | const errorData = error as any; 34 | toast.error(errorData?.data?.message); 35 | } 36 | } 37 | }, [data, isSuccess, error]); 38 | 39 | const handleUpdate = (e: any) => { 40 | const file = e.target.files?.[0]; 41 | if (file) { 42 | const reader = new FileReader(); 43 | reader.onload = (e: any) => { 44 | if (reader.readyState === 2) { 45 | setImage(e.target.result as string); 46 | } 47 | }; 48 | reader.readAsDataURL(file); 49 | } 50 | }; 51 | 52 | const handleEdit = async () => { 53 | await editLayout({ 54 | type: "Banner", 55 | image, 56 | title, 57 | subTitle, 58 | }); 59 | }; 60 | 61 | return ( 62 | <> 63 |
64 |
65 |
66 |
67 | 72 | 80 | 83 |
84 |
85 |
86 | 100 |
101 |
102 |
103 |
null 121 | } 122 | > 123 | Save 124 |
125 |
126 |
127 | 128 | ); 129 | }; 130 | 131 | export default EditHero; 132 | -------------------------------------------------------------------------------- /client/app/components/Course/CourseContentList.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from "react"; 2 | import { BsChevronDown, BsChevronUp } from "react-icons/bs"; 3 | import { MdOutlineOndemandVideo } from "react-icons/md"; 4 | 5 | type Props = { 6 | data: any; 7 | activeVideo?: number; 8 | setActiveVideo?: any; 9 | isDemo?: boolean; 10 | }; 11 | 12 | const CourseContentList: FC = (props) => { 13 | const [visibleSections, setVisibleSections] = useState>( 14 | new Set() 15 | ); 16 | 17 | // Find unique video sections 18 | const videoSections: string[] = [ 19 | ...new Set(props.data?.map((item: any) => item.videoSection)), 20 | ]; 21 | 22 | let totalCount: number = 0; // Total count of videos from previous sections 23 | 24 | const toggleSection = (section: string) => { 25 | const newVisibleSections = new Set(visibleSections); 26 | if (newVisibleSections.has(section)) { 27 | newVisibleSections.delete(section); 28 | } else { 29 | newVisibleSections.add(section); 30 | } 31 | setVisibleSections(newVisibleSections); 32 | }; 33 | 34 | return ( 35 |
36 | {videoSections.map((section: string, sectionIndex: number) => { 37 | 38 | const isSectionVisible = visibleSections.has(section); 39 | 40 | // Filter videos by section 41 | const sectionVideos: any[] = props.data.filter( 42 | (item: any) => item.videoSection === section 43 | ); 44 | 45 | const sectionVideoCount: number = sectionVideos.length; // Number of videos in the current section 46 | const sectionVideoLength: number = sectionVideos.reduce( 47 | (totalLength: number, item: any) => totalLength + item.videoLength, 48 | 0 49 | ); 50 | const sectionStartIndex: number = totalCount; // Start index of videos within the current section 51 | totalCount += sectionVideoCount; // Update the total count of videos 52 | 53 | const sectionContentHours: number = sectionVideoLength / 60; 54 | 55 | return ( 56 |
57 |
58 | {/* Render video section */} 59 |
61 |

{section}

62 | 72 |
73 |
74 |
75 | {sectionVideoCount} Lessons ·{" "} 76 | {sectionVideoLength < 60 77 | ? sectionVideoLength 78 | : sectionContentHours.toFixed(2)}{" "} 79 | {sectionVideoLength > 60 ? "hours" : "minutes"} 80 |
81 |
82 | {isSectionVisible && ( 83 |
84 | {sectionVideos.map((item: any, index: number) => { 85 | const videoIndex: number = sectionStartIndex + index; // Calculate the video index within the overall list 86 | const contentLength: number = item.videoLength / 60; 87 | return ( 88 |
props.isDemo ? null : props?.setActiveVideo(videoIndex)} 94 | > 95 |
96 |
97 | 102 |
103 |

104 | {item.title} 105 |

106 |
107 |
108 | {item.videoLength > 60 ? contentLength.toFixed(2) : item.videoLength}{" "} 109 | {item.videoLength > 60 ? "hours" : "minutes"} 110 |
111 |
112 | ); 113 | })} 114 |
115 | )} 116 |
117 | ); 118 | })} 119 |
120 | ); 121 | }; 122 | 123 | export default CourseContentList; 124 | --------------------------------------------------------------------------------