Skip to content
 
 

Latest commit

 

History

489 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Expecto-Patronum — Online Learning Platform

An online learning platform where students can take courses across various fields, built as a university capstone project by a 5-person team using the MERN stack.

Next.js React TypeScript TailwindCSS Node.js Express MongoDB Mongoose Stripe

Screenshots

Full album: Google Drive folder


Table of Contents


Motivation

What started as a university project quickly became a journey of learning and a passion project, where all 5 team members put their best effort into creating an online learning platform that rivals the best out there in terms of UI and design.

Build Status

Currently in build 1.0, with some known bugs in the connections between frontend and backend.

Tech Stack

Layer Technologies
Client Next.js (React Framework), TailwindCSS, TypeScript
Server Node.js, Express, Mongoose, MongoDB

Features

  • Exceptional UI
  • Live previews
  • Fullscreen mode
  • Cross-platform support
  • Adaptive to different screen resolutions
  • Efficient backend code
  • Encrypted, secure user data
  • Secure credit card transaction handling
  • Eye-friendly color palette

Code Style

  • Standard style used across the MERN stack and Visual Studio Code
  • MVC (Model, View, Controller) pattern following the DRY principle
  • Consistent spacing and indentation to delineate loops, functions, and conditionals

Installation

# Open the project in Visual Studio Code
npm install

Usage

Frontend (run in its own terminal):

cd Frontend
npm run dev
# or
nodemon run dev

Backend (run in a separate terminal):

cd Backend
node app.js
# or
nodemon app.js

Code Examples

Footer Component

import React from 'react'
import classNames from 'classnames'
import { FiFacebook, FiTwitter, FiInstagram, FiLinkedin } from 'react-icons/fi'
import Link from 'next/link'

type Props = {}

const Footer = (props: Props) => {
  return (
    <div style={{backgroundColor: '#222222'}} className='text-white mx-0 fluid-container block relative z-50'>
        <div className='row mx-0'>
            <div className={footerCol}>
                <h4 className={colHeader}><div className={colHeaderText}>Website</div></h4>
                <ul className={colData}>
                    <li className={colItems}><Link href=''>About Us</Link></li>
                    <li className={colItems}><Link href=''>Our Services</Link></li>
                    <li className={colItems}><Link href=''>Privacy Policy</Link></li>
                    <li className={colItems}><Link href=''>Affiliate Program</Link></li>
                </ul>
            </div>
            <div className={footerCol}>
                <h4 className={colHeader}><div className={colHeaderText}>Get Help</div></h4>
                <ul className={colData}>
                    <li className={colItems}><Link href=''>FAQ</Link></li>
                    <li className={colItems}><Link href=''>Shopping</Link></li>
                    <li className={colItems}><Link href=''>Returns</Link></li>
                    <li className={colItems}><Link href=''>Order Status</Link></li>
                    <li className={colItems}><Link href=''>Payment Options</Link></li>
                </ul>
            </div>
            <div className={footerCol}>
                <h4 className={colHeader}><div className={colHeaderText}>Online Shop</div></h4>
                <ul className={colData}>
                    <li className={colItems}><Link href=''>Web Applications</Link></li>
                    <li className={colItems}><Link href=''>Mobile Applications</Link></li>
                    <li className={colItems}><Link href=''>Desktop Applications</Link></li>
                    <li className={colItems}><Link href=''>Other</Link></li>
                </ul>
            </div>
            <div className={footerCol}>
                <h4 className={colHeader}><div className={colHeaderText}>Follow Us</div></h4>
                <ul className={`${colData} flex flex-wrap`}>
                    <li><button className={colIcons}><FiFacebook /></button></li>
                    <li><button className={colIcons}><FiTwitter /></button></li>
                    <li><button className={colIcons}><FiInstagram /></button></li>
                    <li><button className={colIcons}><FiLinkedin /></button></li>
                </ul>
            </div>
        </div>
    </div>
  )
}

const footerCol = classNames(`col-6 col-md-3 p-4`);
const colHeader = classNames(`w-fit relative right-3 border-b-[3px] border-canadian-red px-1 pb-1 skew-x-[40deg]`);
const colHeaderText = classNames(`-skew-x-[40deg] ml-2`);
const colData = classNames(`text-left mt-3 ml-1`);
const colItems = classNames(`text-sm text-bright-gray py-1.5 hover:scale-105 hover:text-white transition-all duration-300 w-fit`);
const colIcons = classNames(`text-sm p-1.25 flex border-canadian-red text-canadian-red hover:text-white hover:border-white items-center justify-center m-2 z-0 scale-125 rounded-full border-1.5 before:content-[""] before:inline-block before:absolute before:z-behind before:bottom-1 before:right-2.75 before:w-6 before:h-6 before:rounded-full hover:scale-135 transition-all duration-300`);

export default Footer

Instructor — Add Course

async function addCourse(req, res, next) {
  try {
    var exists = await CourseTable.findOne({ "title": req.body.title });
    if (exists) {
      res.status(400).send("Course title already used");
    } else {
      var x = await User.find({ "_id": req.user._id }, { firstname: 1, lastname: 1, _id: 0 });
      var y = Object.values(x)[0];
      var name = y.firstname + " " + y.lastname;

      const newCourse = new CourseTable({
        instructorID: req.user._id,
        title: req.body.title,
        summary: req.body.summary,
        subtitles: req.body.subtitles,
        subject: req.body.subject,
        price: req.body.price,
        skills: req.body.skills,
        level: req.body.level,
        courseHours: req.body.courseHours,
        courseVideo: req.body.courseVideo,
        instructorName: name,
        discountPrice: req.body.price,
        courseImage: req.body.courseImage
      });
      newCourse.save();

      if (req.body.exercises) {
        var z = Object.values(newCourse)[0];
        let exercises = req.body.exercises;
        for (var i = 0; i < exercises.length; i++) {
          var q = exercises[i];
          q.courseID = z._id;
          const newExercise = await new ExerciseTable(q);
          await newExercise.save();
        }

        var courseid = z._id;
        const exe = await ExerciseTable.find({ courseID: courseid })
          .select({ "_id": 1, "subtitleName": 1, "exerciseTitle": 1 });

        for (var i = 0; i < exe.length; i++) {
          var z = exe[i];
          if (z.subtitleName) {
            await CourseTable.updateOne(
              { "_id": courseid, "subtitles.header": z.subtitleName },
              { "$push": { "subtitles.$.exercise": { "exerciseID": z._id, "exerciseName": z.exerciseTitle } } }
            );
          } else {
            await CourseTable.updateOne({ "_id": courseid }, { "$set": { "finalExam": z._id } });
          }
        }
      }
      res.send("Course Added");
    }
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
}

User — Register

async function register(req, res) {
  const saltHash = genPassword(req.body.password);
  const salt = saltHash.salt;
  const hash = saltHash.hash;
  let EmailLowerCase = req.body.email.toLowerCase();

  var exists1 = await User.findOne({ "email": EmailLowerCase });
  var exists2 = await User.findOne({ "username": req.body.username });

  if (exists1 || exists2) {
    if (exists1 && exists2) {
      res.status(400).send("username and email already used");
    } else if (exists1) {
      res.status(400).send("email already used");
    } else {
      res.status(400).send("username already used");
    }
  } else {
    const newUser = new User({
      username: req.body.username,
      hash: hash,
      salt: salt,
      gender: req.body.gender,
      email: EmailLowerCase,
      firstname: req.body.firstname,
      lastname: req.body.lastname,
      role: req.body.role
    });

    newUser.save((err, newUser) => {
      if (err) {
        res.status(400).send("Error registering new user please try again.");
      } else {
        let token = CreateToken({ id: newUser._id, email: newUser.email });
        MailValidate(newUser.email, "http://localhost:3000/Auth/FeedBack/EmailConfirmed", token);
        res.status(200).send("Verify your email");
      }
    });
  }
}

API Reference

Base URL: http://localhost:5000

User Endpoints

Method Endpoint Description
POST /user/register Register a new user (username, password, firstname, lastname, gender, role, email)
POST /user/forgetPassword Send a password-reset email (email)
GET /user/forgetPassword Send a password-reset email (exerciseID, CourseID)
POST /user/changePassword Change password (oldPassword, password)
PUT /user/ChangeEmail Change the account email (userMail)
PUT /user/giveCourseRating Rate a course, or edit an existing rating (rating, oldRating?, courseId)
PUT /user/giveCourseReview Review a course, or edit an existing review (rating, review, oldReview?, courseId)
PUT /user/addPaymentMethod Save credit card details via Stripe customer (creditCardNumber, ccv, expiration, cardHolderName)
PUT /user/buyCourse Purchase a course, using a saved card (customerId) or new card details, plus courseID, Amount
GET /user/ViewMyCourses View owned courses (page?)
PUT /user/selectCourse Open a course page (courseID)
PUT /user/watchVideo Update watch progress for a video (courseID, videoURL, videotime) — progress won't double-count if the video was already watched
GET /user/takeExam Take a course's exam (examID)
GET /user/logout Log out (logout, clearCookie)
GET /user/viewProfile View profile data (user_id, courseID, excerciseID, answers[])
PUT /user/addNote Add a note while watching a video (user_id, courseID, content, subtitle, timestamp, note, subtitleName, contentName, subtitleIndex, contentIndex)
PUT /user/viewNotes View all notes (user_id, courseID)
POST /user/reportProblem Report a bug or issue (user_id, courseID, type: financial | other | technical, body, startDate)

Course Endpoints

Method Endpoint Description
GET /course/getPrice Get a course's price (id)
GET /course/ Search/filter courses (price?, keyword?, subject?, rating?, page?)
GET /course/viewPopularCourses List courses ranked by number of buyers (CurrentPageid?, coursesPerPage?)

Instructor Endpoints

Method Endpoint Description
GET /instructor/viewCourses View the instructor's courses (page?)
GET /instructor/filterCourse Filter/search the instructor's courses (price?, keyword?, subject?)
PUT /instructor/updateBio Update instructor biography (newBio)
GET /instructor/filterByRatings Filter courses/reviews by rating (CurrentPage?, rating)
PUT /instructor/viewAmountOwned View amount owed to the instructor by the platform
POST /instructor/addCourse Add a new course (title, summary, subtitles, subject, exercises, skills, level, courseVideo, courseHours, price, courseImage)
POST /instructor/discount Add a discount to a course (courseID, discount, StartDate, endDate)
POST /instructor/canceldiscount Cancel a course discount (courseId)
GET /instructor/viewProfile View instructor profile
GET /instructor/viewInstructorPopularCourses View the instructor's most popular courses (CurrentPage?)

Admin Endpoints

Method Endpoint Description
GET /admin/viewCourseRequests View course requests, 5 per page (page?)
GET /admin/viewRefunds View refund requests, 5 per page (page?)
PUT /admin/grantAccess Grant/revoke a corporate trainee's access to a course (granted)
PUT /admin/refund Accept/deny a refund for a corporate trainee (refund: Accept or otherwise)
PUT /admin/givePromotion Apply a promotion to courses (courseID[], Promotion, StartDate, EndDate)
GET /admin/viewReportedFunctions View reported problems, 5 per page (page?)
PUT /admin/markReportedProblem Update a reported problem's status (status, ProblemID)

Tests

  • Backend: tested via Postman to verify data correctness and table updates.
  • Frontend: tested manually by running the app and checking look and functionality.
  • To run the Postman collections: open Postman → Import → paste one of the links below.
Collection Link
Collection 1 Open in Postman
Collection 2 Open in Postman
Collection 3 Open in Postman

Contribution

Contributions are welcome, especially:

  • Fixing missing connections between frontend and backend
  • New backend functions that improve quality of life for users

Credits

  • Dr. Mervat and her incredible Teaching Assistants (Nada, Hadwa, Noha)
  • Dr. Angela Yu, for her Web Development Bootcamp course
  • Stack Overflow
  • The 5 team members who created this project

Acknowledgements

License

This project uses the Apache 2.0 License (required due to Stripe usage). See apache.org/licenses/LICENSE-2.0.

About

Full-stack online learning platform (MERN + Next.js) with course marketplace, instructor tools, and Stripe payments.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages