Building Learning Dashboard In MERN Stack Course Management App

Last Updated : 23 Sep, 2024

In today’s digital age, the landscape of education is rapidly evolving. One of the most significant advancements in this transformation is the rise of course websites and learning dashboards. These tools are revolutionizing how students and educators interact with educational content, manage learning progress, and achieve academic goals.

In this article, we will explore how educational websites and dashboards are reshaping education and the benefits they bring to learners and educators alike.

What Are Educational Websites and Learning Dashboards?

Course Websites: These are online platforms designed to provide learning and education. They offer a wide range of resources, including interactive lessons, and multimedia content. These websites cater to various educational needs, from primary education to advanced degrees, and cover diverse subjects.

Learning Dashboards: These are sophisticated tools that provide a centralized interface for tracking and managing learning activities. They integrate data from various educational sources, allowing users to monitor progress, set goals, and receive personalized recommendations. Learning dashboards can be part of larger educational platforms or standalone tools.

Approach to implement Educational Websites: Learning Dashboard

Backend

  • Fetch User Data: Retrieve user details and their associated playlist from the database, including course details.
  • Populate Data: Use populate to include course and instructor details within the playlist.
  • Check User Existence: Return a 404 error if the user is not found.
  • Calculate Metrics: Count the total number of courses and lectures from the playlist.
  • Prepare Response: Structure the response to include user details, total courses, total lectures, and course information.
  • Define Course Schema: Create a Mongoose schema for the Course model, defining properties like title, description, lectures, and creator.
  • Set Up Route: Configure a route to access the dashboard data, ensuring the user is authenticated.

Frontend

  • Fetch Data: Use axios within useEffect to fetch dashboard data from the API and store it in the component’s state.
  • Loading State: Display a spinner while data is being fetched.
  • Display User Info: Show user details, total courses, and lectures in a styled container.
  • List Courses: Render a list of courses with details and a progress bar; allow toggling of lecture details.
  • Handle Lectures: Use Collapse to show/hide lecture details, including video playback for each lecture.

Backend Example

JavaScript
// server.js

const app = require("./app")
const { config } = require("dotenv")
const database = require('./config/database');
const cloudinary = require('cloudinary').v2;

database.connectedToDatabase();

cloudinary.config({
    cloud_name: process.env.CLOUD_NAME,
    api_key: process.env.API_KEY,
    api_secret: 'process.env.API_SECRET',
});

config({
    path: './config/config.env'
})

app.listen(process.env.PORT, () => {
    console.log(`Server is Running at ${process.env.PORT}`);
})
JavaScript
//app.js

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const user = require('./routes/userRoute')
const course = require('./routes/courseRoutes');
const ErrorMiddleware = require('./middleware/Error');
const cookieParser = require('cookie-parser');
const enrollment = require('./routes/enrollmentRoutes')
const instructorRoutes = require('./routes/instructorRoutes');
const cors = require('cors')


app.use(cors({
    origin: ['http://localhost:3000'],
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
}));

app.use(express.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser())

app.use('/api/v1', user)
app.use('/api/v1', enrollment)
app.use('/api/v1', course)
app.use('/api/v1', instructorRoutes);
app.use(ErrorMiddleware);


module.exports = app;
JavaScript
// models/courseModel.js

const mongoose = require('mongoose');

const courseSchema = mongoose.Schema({
    title: {
        type: String,
        required: [true, "Please enter course title"],
        minLength: [4, "Title must be at least 4 characters"],
        maxLength: [80, " Title can't exceed 80 characters"],
    },
    description: {
        type: String,
        required: [true, "Please enter course title"],
        minLength: [20, "Title must be at least 20 characters"],

    },
    lectures: [
        {
            title: {
                type: String,
                required: true,
            },
            description: {
                type: String,
                required: true,
            },

            video: {
                public_id: {
                    type: String,

                },
                url: {
                    type: String,

                },

            },
        }
    ],
    poster: {
        public_id: {
            type: String,
            required: true
        },
        url: {
            type: String,
            required: true
        },

    },
    views: {
        type: Number,
        default: 0,

    },
    numOfVideos: {
        type: Number,
        default: 0,
    },
    category: {
        type: String,
        required: true,

    },
    user: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
    },
    createdBy: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'Instructor',
        required: [true, "Enter Course Creator"],
    },
    createdAt: {
        type: Date,
        default: Date.now,
    },
});

const CourseModel = mongoose.model("Course", courseSchema);

module.exports = CourseModel;
JavaScript
// controllers/courseController.js

const catchAsyncError = require("../middleware/catchAsyncError");
const Course = require("../models/courseModel");
const User = require('../models/userModel')
const getDataUri = require("../utils/dataUri");
const ErrorHandler = require("../utils/errorHandler");
const cloudinary = require("cloudinary").v2;

exports.createCourse = catchAsyncError(async (req, res, next) => {
    const { title, description, category, createdBy } = req.body;
    if (!title || !description || !category || !createdBy)
        return next(new ErrorHandler("Please Add all Field", 404));

    const file = req.file;

    const fileUri = getDataUri(file);


    const myCloud = await cloudinary.uploader.upload(fileUri.content);

    await Course.create({
        title,
        description,
        category,
        createdBy,
        poster: {
            public_id: myCloud.public_id,
            url: myCloud.secure_url,
        },
    })
    res.status(201).json({
        success: true,
        message: "Course Created Successfully. You can add Lecture Now",
    });

});

exports.getAllCourse = catchAsyncError(async (req, res, next) => {
    const courses = await Course.find()
        .populate({
            path: 'createdBy',

        })
        .select('-lectures');
    res.status(200).json({
        success: true,
        courses,
    });
});


exports.getCourseLectures = catchAsyncError(async (req, res, next) => {
    const course = await Course.findById(req.params.id);

    if (!course) return next(new ErrorHandler("Course not found", 404));

    course.views += 1;
    await course.save();
    res.status(200).json({
        success: true,
        lectures: course.lectures,
    });
});


exports.addLecture = catchAsyncError(async (req, res, next) => {
    const { title, description } = req.body;
    const course = await Course.findById(req.params.id);

    if (!course) return next(new ErrorHandler("Course not found", 404));

    const file = req.file;
    if (!file) return next(new ErrorHandler("No file uploaded", 400));

    try {
        const fileUri = getDataUri(file);


        const myCloud = await cloudinary.uploader.upload(fileUri.content, {
            resource_type: 'video',
        });


        course.lectures.push({
            title,
            description,
            video: {
                public_id: myCloud.public_id,
                url: myCloud.secure_url,
            }
        });

        course.numOfVideos = course.lectures.length;
        await course.save();

        res.status(200).json({
            success: true,
            message: "Lecture Added Successfully in Course"
        });
    } catch (error) {
        console.error('Error uploading to Cloudinary:', error);
        return next(new ErrorHandler("Error uploading video", 500));
    }
});


// delete course
exports.deleteCourse = catchAsyncError(async (req, res, next) => {
    const { id } = req.params;

    const course = await Course.findById(id);

    if (!course) return next(new ErrorHandler("Course not found", 404));

    await cloudinary.uploader.destroy(course.poster.public_id);

    for (let i = 0; i < course.lectures.length; i++) {
        const singleLecture = course.lectures[i];
        await cloudinary.uploader.destroy(singleLecture.video.public_id);
    }


    await course.deleteOne();

    res.status(201).json({
        success: true,
        message: "Course deleted Successfully.",
    });

});



exports.deleteLecture = catchAsyncError(async (req, res, next) => {
    const { courseId, lectureId } = req.query;

    const course = await Course.findById(courseId);

    if (!course) return next(new ErrorHandler("Course not found", 404));

    // Find the lecture with the specified ID
    const lecture = course.lectures.find(item => item._id.toString() === lectureId.toString());

    if (!lecture) {
        return next(new ErrorHandler("Lecture not found", 404));
    }

    // Check if lecture.video is defined before accessing its properties
    if (lecture.video && lecture.video.public_id) {
        await cloudinary.uploader.destroy(lecture.video.public_id, {
            resource_type: 'video',
        });

    }

    // Filter out the deleted lecture
    course.lectures = course.lectures.filter(item => item._id.toString() !== lectureId.toString());
    course.numOfVideos = course.lectures.length;
    await course.save();

    res.status(201).json({
        success: true,
        message: "Lecture deleted Successfully.",
    });
});

exports.getDashboardData = catchAsyncError(async (req, res, next) => {
    const userId = req.user.id;

    // Fetch the user with their playlist
    const user = await User.findById(userId)
        .select('name email playlist')
        .populate({
            path: 'playlist.course',
            select: 'title description category createdBy lectures',
            populate: {
                path: 'createdBy',
                select: 'name poster'
            }
        });

    if (!user) {
        return next(new ErrorHandler("User not found", 404));
    }

    // Extract courses from the playlist
    const courses = user.playlist.map(item => item.course);

    const totalCourses = courses.length;
    const totalLectures = courses.reduce((acc, course) => acc + course.lectures.length, 0);


    res.status(200).json({
        success: true,
        data: {
            user: {
                name: user.name,
                email: user.email
            },
            totalCourses,
            totalLectures,
            courses
        }
    });
});
JavaScript
// routes/coursRoutes.js

const express = require('express');
const {
    getAllCourse,
    createCourse,
    getCourseLectures,
    addLecture,
    deleteCourse,
    deleteLecture,
    getDashboardData,
} = require('../controllers/courseController');
const singleUpload = require('../middleware/multer');
const { authorizAdmin, isAuthenticatedUser } = require('../middleware/auth');
const videoUpload = require('../middleware/upload');

const router = express.Router();
router
    .route('/createcourse')
    .post(isAuthenticatedUser, authorizAdmin, singleUpload, createCourse);
router.route('/course').get(getAllCourse);

router
    .route('/course/:id')
    .get(isAuthenticatedUser, getCourseLectures)
    .post(isAuthenticatedUser, authorizAdmin, videoUpload, addLecture)
    .delete(isAuthenticatedUser, authorizAdmin, deleteCourse);
router
    .route('/lecture')
    .delete(isAuthenticatedUser, authorizAdmin, deleteLecture);
router.get('/dashboard', isAuthenticatedUser, getDashboardData);
module.exports = router;


Start your backend using the below command

node server.js


Frontend Example

JavaScript
// App.js

import React from 'react';
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';
import Home from './components/Home/Home';
import Header from '../src/components/Layout/Header/Header';
import Courses from './components/Courses/Courses';
import Login from './components/Auth/Login';
import Register from './components/Auth/Register';
import Subscribe from './components/Payments/Subscribe';
import CoursePage from './components/CoursePage/CoursePage';
import Profile from './components/Profile/Profile';
import Dashboard from './components/Admin/Dashboard/Dashboard';
import CreateCourses from './components/Admin/CreateCourse/CreateCourses';
import Users from './components/Admin/Users/Users';
import EnrollmentForm from './components/Auth/EnrollmentForm';
import InstructorProfiles from './components/Instructor/Instructor';
import InstructorProfile from './components/Instructor/InstructorProfile';
import LearningDashboard from './components/Courses/LearningDashboard';

function App() {
    return (
        <Router>
            <Header />
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/courses" element={<Courses />} />
                <Route path="/course/:id" element={<CoursePage />} />
                <Route path="/profile" element={<Profile />} />
                <Route path="/login" element={<Login />} />
                <Route path="/signup" element={<Register />} />
                <Route path="/admin/dashboard" element={<Dashboard />} />
                <Route path="/admin/createcourse" element={<CreateCourses />} />
                <Route path="/admin/courses" element={<AdminCourses />} />
                <Route path="/admin/users" element={<Users />} />
                <Route path="/enroll" element={<EnrollmentForm />} />
                <Route path="/inst" element={<InstructorProfiles />} />
                <Route path="/instructor/:id" element={<InstructorProfile />} />
                <Route path="/learningdash" element={<LearningDashboard />} />
            </Routes>
            <Footer />
        </Router>
    );
}

export default App;
JavaScript
// src/copmonents/Layout/Header.jsx

import {
    Button,
    Drawer,
    DrawerBody,
    DrawerContent,
    DrawerHeader,
    DrawerOverlay,
    HStack,
    useDisclosure,
    VStack,
} from "@chakra-ui/react"
import React from "react"
import {
    RiDashboardFill,
    RiLogoutBoxLine,
    RiMenu5Fill
} from "react-icons/ri";
import { Link, useNavigate } from "react-router-dom";

import { ColorModeSwitcher } from "../../../ColorModeSwitcher"

const LinkButton
    = ({ url = "/", title = "Home", onClose }) => (

        <Link to={url} onClick={onClose}>
            <Button variant={"ghost"}>{
                title}</Button>
        </Link>);

const Header = () => {
    const { isOpen, onOpen, onClose } = useDisclosure();
    const navigate = useNavigate()
    const user = JSON.parse(localStorage.getItem("currentUser"));
    const logoutHandler = () => {
        localStorage.removeItem("currentUser");
        navigate("/")
    }
    return (
        <>
            <ColorModeSwitcher />
            <Button onClick={onOpen}
                colorScheme="yellow"
                width={"12"}
                height={"12"}
                rounded={"full"}
                zIndex={"overlay"}
                position={"fixed"}
                top={"6"}
                left={"6"}
            >

                <RiMenu5Fill />
            </Button>
            <Drawer placement='left' onClose={onClose} isOpen={isOpen}>
                <DrawerOverlay />
                <DrawerContent>
                    <DrawerHeader borderBottomWidth={"1px"}>BUNDLER</DrawerHeader>

                    <DrawerBody>
                        <VStack spacing={'12'} alignItems={'flex-start'}>
                            <LinkButton onClose={onClose} url='/" title="Home' />
                            <LinkButton onClose={onClose} url="/courses" title="ALL Courses" />
                            <LinkButton onClose={onClose} url="/learningdash" title="Learning Dashboard" />
                            <LinkButton onClose={onClose} url="request" title="Request Courses" />
                            <LinkButton onClose={onClose} url="/contact" title="Contact" />
                            <LinkButton onClose={onClose} url="/about" title="About" />

                            <HStack justifyContent={"space-evenly"} position={"absolute"}
                                bottom={"2rem"}
                                width={"80%"}
                            >
                                {
                                    user ? (<>
                                        <VStack>
                                            <HStack>
                                                <Link to="/profile" onClick={onClose}>
                                                    <Button colorScheme="yellow" variant={"ghost"}>Profile
                                                    </Button></Link>
                                                <Button variant={"ghost"} onClick={logoutHandler}>
                                                    <RiLogoutBoxLine />Logout
                                                </Button>
                                            </HStack>
                                            {
                                                user && user.role === "admin" && 
                                                <Link to={"/admin/dashboard"} onClick={onClose}>
                                                    <Button colorScheme="purple" variant={"ghost"}>
                                                    <RiDashboardFill style={{ margin: "4px" }} />Dashboard</Button>
                                                </Link>
                                            }
                                        </VStack>
                                    </>) : (<>
                                        <Link to='/login" onClick={onClose}><Button colorScheme="yellow'>Login
                                        </Button></Link>
                                <p>OR</p>
                                <Link to='/signup" onClick={onClose}><Button colorScheme="yellow'>Sign up
                                </Button></Link>
                        </>)
                                }
                    </HStack>

                </VStack>
            </DrawerBody>
        </DrawerContent >
            </Drawer >
        </>
    )
}

export default Header
JavaScript
// src/components/Courses/LearningDashboard.jsx

import {
    Avatar,
    Box,
    Button,
    Center,
    Collapse,
    Container,
    Flex,
    Heading,
    Image,
    List,
    ListItem,
    Progress,
    SimpleGrid,
    Spinner,
    Stack,
    Text,
    useColorMode,
    useDisclosure,
} from '@chakra-ui/react';
import axios from 'axios';
import React, { useEffect, useState } from 'react';

const LearningDashboard = () => {
    const [dashboardData, setDashboardData] = useState(null);
    const { isOpen, onToggle } = useDisclosure();
    const { colorMode } = useColorMode();
    const token = localStorage.getItem('authToken');

    useEffect(() => {
        const fetchDashboardData = async () => {
            try {
                const { data } = await axios.get(
                    'http://localhost:4000/api/v1/dashboard',
                    {
                        headers: {
                            Authorization: `Bearer ${token}`,
                        },
                    },
                );
                console.log(data, 'skfk');
                setDashboardData(data);
            } catch (error) {
                console.error('Error fetching dashboard data:', error);
            }
        };

        fetchDashboardData();
    }, [token]);

    if (!dashboardData)
        return (
            <Center height="100vh">
                <Spinner size="xl" />
            </Center>
        );

    const { user, totalCourses, totalLectures, courses } = dashboardData.data;

    const calculateProgress = (course) => {
        const completedLectures = course.completedLectures || 0;
        return (completedLectures / course.lectures.length) * 100;
    };

    return (
        <Container maxW="container.lg" py={6}>
            <Stack spacing={8}>
                <Box
                    position="relative"
                    top={0}
                    p={6}
                    shadow="md"
                    borderWidth="1px"
                    borderRadius="md"
                    bg={colorMode === 'light' ? 'white' : 'gray.800'}
                    color={colorMode === 'light' ? 'black' : 'white'}
                    zIndex={1}
                >
                    {' '}
                    <Heading as="h1" size="2xl" textAlign="center">
                        Learning Dashboard
                    </Heading>
                    <Text fontSize="xl" mt={2} textAlign="center">
                        Welcome, {user.name}!
                    </Text>
                    <Avatar name={user.name} mt={4} size="xl" display="block" mx="auto" />
                    <Box mt={6} textAlign="center">
                        <Text fontSize="lg">
                            <strong>Email: </strong> {user.email}
                        </Text>
                        <Text fontSize="lg">
                            <strong>Total Courses:</strong> {totalCourses}
                        </Text>
                        <Text fontSize="lg">
                            <strong>Total Lectures:</strong> {totalLectures}
                        </Text>
                    </Box>
                </Box>

                {/* Courses Container */}
                <Box>
                    <Heading as="h2" size="lg" mb={4}>
                        Your Courses
                    </Heading>
                    <List spacing={4}>
                        {courses.map((course) => (
                            <ListItem
                                key={course._id}
                                p={4}
                                shadow="md"
                                borderWidth="1px"
                                borderRadius="md"
                                bg={colorMode === 'light' ? 'gray.50' : 'gray.700'}
                                color={colorMode === 'light' ? 'black' : 'white'}
                                _hover={{ bg: colorMode === 'light' ? 'gray.100' : 'gray.600' }}
                            >
                                <Flex direction="column" align="flex-start">
                                    <Heading as="h3" size="md">
                                        {course.title}
                                    </Heading>
                                    <Text mt={2}>{course.description}</Text>
                                    <Text mt={2}>
                                        <strong>Category:</strong>
                                        {course.category}
                                    </Text>
                                    <Text mt={2}>
                                        <strong>Created By:</strong>
                                        {course.createdBy.name}
                                    </Text>
                                    <Box mt={4}>
                                        <Text>
                                            <strong>Progress:</strong>
                                        </Text>
                                        <Progress
                                            value={calculateProgress(course)}
                                            colorScheme="yellow"
                                            size="sm"
                                        />
                                    </Box>
                                    <Button mt={4} onClick={onToggle} colorScheme="teal">
                                        {isOpen ? 'Hide Lectures' : 'Show Lectures'}
                                    </Button>
                                    <Collapse in={isOpen}>
                                        <Box
                                            mt={4}
                                            p={4}
                                            borderWidth="1px"
                                            borderRadius="md"
                                            bg={colorMode === 'light' ? 'white' : 'gray.800'}
                                        >
                                            <Heading as="h4" size="sm" mb={2}>
                                                Lectures
                                            </Heading>
                                            <List spacing={2}>
                                                {course.lectures.map((lecture) => (
                                                    <ListItem
                                                        key={lecture._id}
                                                        p={2}
                                                        borderWidth="1px"
                                                        borderRadius="md"
                                                        bg={colorMode === 'light' ? 'gray.100' : 'gray.600'}
                                                        color={colorMode === 'light' ? 'black' : 'white'}
                                                    >
                                                        <Flex direction="column" align="flex-start">
                                                            <Text fontWeight="bold">{lecture.title}</Text>
                                                            <Box mt={2} w="full">
                                                                <video
                                                                    width="100%"
                                                                    controls
                                                                    src={lecture.video.url}
                                                                    style={{
                                                                        borderRadius: '8px',
                                                                    }}
                                                                >
                                                                    Your browser does not support the video tag.
                                                                </video>
                                                            </Box>
                                                        </Flex>
                                                    </ListItem>
                                                ))}
                                            </List>
                                        </Box>
                                    </Collapse>
                                </Flex>
                            </ListItem>
                        ))}
                    </List>
                </Box>
            </Stack>
        </Container>
    );
};

export default LearningDashboard;


Start your app using the below command:

npm start

Output:

Comment

Explore