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 |
59 |
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 | toggleQuestion(q._id)}
42 | >
43 | {q.question}
44 |
45 | {activeQuestion === q._id ? (
46 |
47 | ) : (
48 |
49 | )}
50 |
51 |
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 |
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 |
72 |
75 |
76 |
Hello <%= user.name %>,
77 |
78 | Thank you for registering with ELearning. To activate your account,
79 | please use the following activation code:
80 |
81 |
<%= activationCode %>
82 |
83 | Please enter this code on the activation page within the next 5
84 | minutes.
85 |
86 |
87 | If you did not register for a ELearning account, please ignore this
88 | email.
89 |
90 |
91 |
97 |
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 |
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 |
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 |
61 | What are the benefits for students in this course?
62 |
63 |
64 | {benefits.map((benefit: any, index: number) => (
65 |
handleBenefitChange(index, e.target.value)}
74 | />
75 | ))}
76 |
80 |
81 |
82 |
83 |
84 | What are the prerequisites for starting this course?
85 |
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 |
87 |
88 |
89 |
90 | {Object.keys(verifyNumber).map((key, index) => (
91 | handleInputChange(index, e.target.value)}
104 | />
105 | ))}
106 |
107 |
108 |
109 |
110 |
111 | Verify OTP
112 |
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 |
80 |
83 |
84 |
85 |
86 |
87 |
88 |
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 |
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 |
112 |
113 | Home Page
114 |
115 |
116 |
117 | Course Page
118 |
119 |
120 |
121 |
122 |
123 | Profile Page
124 |
125 |
126 |
127 | Enroll Course List
128 |
129 |
130 |
131 |
132 |
133 | Course Details Page
134 |
135 |
136 |
137 |
138 |
139 | ## 📱 Admin Side Screenshots
140 |
141 |
142 |
143 |
144 |
145 | Dashboard Page
146 |
147 |
148 |
149 | Order Analysis Page
150 |
151 |
152 |
153 |
154 |
155 | Create Course Page
156 |
157 |
158 |
159 | Course List
160 |
161 |
162 |
163 |
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 |
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 |
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 |
81 |
82 |
83 |
84 |
85 |
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 | toggleSection(section)}
65 | >
66 | {isSectionVisible ? (
67 |
68 | ) : (
69 |
70 | )}
71 |
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 |
--------------------------------------------------------------------------------