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.
Full album: Google Drive folder
- Motivation
- Build Status
- Tech Stack
- Features
- Code Style
- Installation
- Usage
- Code Examples
- API Reference
- Tests
- Contribution
- Credits
- Acknowledgements
- License
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.
Currently in build 1.0, with some known bugs in the connections between frontend and backend.
| Layer | Technologies |
|---|---|
| Client | Next.js (React Framework), TailwindCSS, TypeScript |
| Server | Node.js, Express, Mongoose, MongoDB |
- 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
- 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
# Open the project in Visual Studio Code
npm installFrontend (run in its own terminal):
cd Frontend
npm run dev
# or
nodemon run devBackend (run in a separate terminal):
cd Backend
node app.js
# or
nodemon app.jsimport 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 Footerasync 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 });
}
}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");
}
});
}
}Base URL: http://localhost:5000
| 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) |
| 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?) |
| 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?) |
| 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) |
- 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 |
Contributions are welcome, especially:
- Fixing missing connections between frontend and backend
- New backend functions that improve quality of life for users
- 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
This project uses the Apache 2.0 License (required due to Stripe usage). See apache.org/licenses/LICENSE-2.0.
















