Implementation of a fully functional online learning platform through the use of the MERN stack
The goal of this project is to provide a valuable resource for people seeking to enhance their skills and understanding. It offers a convenient and accessible method for individuals to learn new things and achieve their goals through online courses and exercises. By providing the opportunity for students to practice and apply their learning, the project aids them in not only gaining new information, but also evaluating their comprehension and retaining it for longer periods of time. The project is committed to being the best it can be and continues to work towards this aim.
- Project is still in development
- Further improvents to UI/UX
- Implementing a payment gateway
- Unit Tests to be added
-
The architecture for the features is MVC (Model View Controller) Learn More Here.
-
We used camelCase for variables.
-
Codes are formatted in VS Code using alt + shift + F.
-
Indentation was used to assist in identifying control flow and blocks of code.
-
Tabs are used for spacing
Client: React, Bootstrap & Material UI
Server: Node, Express, MongoDB
There are four different types of users for the system: Administrators, Instructors, Individual Trainees, and Corporate Trainees.
-
As an Administrator, you can add instructors and corporate trainees to the system, view and resolve reported problems, view and grant access requests from corporate trainees, and view refund requests from individual trainees.
-
As an Instructor, you can create and edit draft courses, publish draft courses for trainees to enroll in, close a published course to prevent further enrollment, view your settlements and update your profile, and add promotions for a specific period.
-
As an Individual Trainee, you can search and filter courses, pay for a course, report problems, watch videos and solve exercises from your courses, see your progress, receive a certificate by mail, request a refund, and rate a course and its instructor.
-
As a Corporate Trainee, you can search and filter courses, send access requests for specific courses, watch videos and solve exercises from your courses, see your progress, receive a certificate by mail, and rate a course and its instructor.
-
As a Guest, you can sign up as an individual trainee and search and filter courses.
Below is an example of how to login using on our platform.
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="formBasicEmail">
<Form.Label>Username</Form.Label>
<Form.Control type="username" placeholder="Enter your username" onChange={(e) => setUsername(e.target.value)} value={username} />
</Form.Group>
<Form.Group className="mb-3" controlId="formBasicPassword">
<Form.Label>Password</Form.Label>
<Form.Control type="password" placeholder="Password" onChange={(e) => setPassword(e.target.value)} value={password} />
</Form.Group>
<Button onClick={handleShow} variant="link" style={{ marginLeft: "-10px" }}>
Forgot your password?
</Button>
<Button disabled={isLoading} variant="dark" type="submit">
Login
</Button>
<Button href="/" variant="danger" style={{ marginLeft: "10px", borderRadius: "0px" }}>
Cancel
</Button>
{error && <div className="error">{error}</div>}
</Form>
const handleSubmit = async (e) => {
e.preventDefault()
await login(username.toLowerCase(), password)
}
const login = async (username, password) => {
setIsLoading(true)
setError(null)
const response = await fetch('/api/users/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username, password })
})
const json = await response.json()
if (!response.ok) {
setIsLoading(false)
setError(json.error)
}
if (response.ok) {
// save the user to local storage
localStorage.setItem('user', JSON.stringify(json))
// update the auth context
dispatch({type: 'LOGIN', payload: json})
// update loading state
setIsLoading(false)
}
}
userSchema.statics.login = async function (username, password) {
if (!username || !password) {
throw Error('All fields must be filled')
}
const user = await this.findOne({ Username: username })
if (!user) {
throw Error('Incorrect username')
}
const match = await bcrypt.compare(password, user.Password)
if (!match) {
throw Error('Incorrect password')
}
return user
}
gh repo clone Advanced-Computer-Lab-2022/Comrades
npm install -g npm
Make sure Node.JS is installed, you can get it through this link.
Install the required packages for frontend and backend as mentioned before in the installation section.
cd front_end
npm install
cd back_end
npm install my-project
It should be placed at same directory level with server.JS and should include the following:
PORT=4000
MONGO_URI=''
SECRET=whateveryoulike
- PORT can be anything but make sure its working.
- MONGO_URI can be found through the connection Tab at MongoDB, use this link for more information: link.
- SECRET can be whatever you prefer.
Make sure you have 2 terminals open, one for the front and one for the back.
cd front_end
npm start
Please note that npm run_dev only for development mode.
cd back_end
npm run dev
Please visit the following link for the full API references documentation(Preferably don't use a mobile although its supported):
Getting courses of an instructor
describe("GET getCoursesInstructor/:query", () => {
test("Get courses of instructor by ID", (done) => {
request(app)
.get("/api/users/getCoursesInstructor/63a21cbdd7dcdba272cadbb6")
.expect(200)
.expect((res) => {
res.body[0].Username = "testingituser";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
});
Getting an instructor by ID
describe("GET getInstructorByID/:query", () => {
test("Get an instructor by ID", (done) => {
request(app)
.get("/api/users/getInstructorByID/63a21cbdd7dcdba272cadbb6")
.expect(200)
.expect((res) => {
res.body.Username = "testingituser";
})
.end((err, res) => {
if (err) return done(err);
return done();
});
});
});
Rating an instructor
describe('POST /rateInstructor', () => {
test('it should rate the instructor and return the updated user object', async () => {
const res = await request(app).post('/API/Users/rateInstructor').send({
name: 'testInstructor',
Rating: 5,
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Username', 'testInstructor');
expect(res.body).toHaveProperty('Rating', 5);
});
});
Receiving an email to change password
describe('POST /recieveEmailToChangePassword', () => {
test('it should send an email to the specified address', async () => {
const res = await request(app).post('/API/Users/recieveEmailToChangePassword').send({
Email: 'test@example.com',
Password: 'newPw',
});
expect(res.statusCode).toBe(200);
});
});
Changing a password without Token
describe('POST /changePasswordNoToken', () => {
test('it should change the password for the specified username and return the updated user object', async () => {
const res = await request(app).post('/API/Users/changePasswordNoToken').send({
Token: 'testUsername',
Password: 'newPassword',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Username', 'testUsername');
expect(res.body).toHaveProperty('Password', 'newPassword');
});
});
Changing password with a Token
describe('POST /changePassword', () => {
test('it should change the password for the specified username and return the updated user object', async () => {
const res = await request(app).post('/API/Users/changePassword').send({
Token: 'testToken',
Password: 'newPassword',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Password', 'newPassword');
});
});
Get ratings of an instructor
describe('GET /getRatingsInstructor', () => {
test('it should return the user object for the specified instructor', async () => {
const res = await request(app).get('/API/Users/getRatingsInstructor?name=testInstructor&Rating=5');
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Username', 'testInstructor');
expect(res.body).toHaveProperty('Rating', 5);
});
});
Review Instructor
describe('POST /reviewInstructor', () => {
test('it should post a review about the instructor and return the updated user object', async () => {
const res = await request(app).post('/API/Users/reviewInstructor').send({
Reviewer: 'testUser',
Review: 'This instructor was great!',
Instructor: 'testInstructor',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Username', 'testInstructor');
expect(res.body).toHaveProperty('Reviews', ['This instructor was great!']);
});
});
Get reviews of an instructor
describe('GET /getReviewsInstructor', () => {
test('it should return an array of reviews for the specified instructor', async () => {
const res = await request(app).get('/API/Users/getReviewsInstructor?instructor=testInstructor');
expect(res.statusCode).toBe(200);
expect(res.body).toEqual(['This instructor was great!']);
});
});
Change an Email
describe('POST /changeEmail', () => {
test('it should change the email for the specified user', async () => {
const res = await request(app).post('/API/Users/changeEmail').send({
User: 'testUser',
Email: 'new@example.com',
});
expect(res.statusCode).toBe(200);
});
});
Changing a user's bio
describe('POST /changeBio', () => {
test('it should change the bio for the specified user', async () => {
const res = await request(app).post('/API/Users/changeBio').send({
Username: 'testInstructor',
Biography: 'I am an experienced instructor.',
});
expect(res.statusCode).toBe(200);
});
});
Email a certificate
describe('POST /emailCertificate', () => {
test('it should send an email with a link to the user's certificate', async () => {
const res = await request(app).post('/API/Users/emailCertificate').send({
Username: 'testUser',
CourseID: 'testCourse',
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ done: 'done' });
});
});
Requesting a refund
describe('POST /requestRefund', () => {
test('it should refund the specified course and return the updated user object', async () => {
const res = await request(app).post('/API/Users/requestRefund').send({
Username: 'testUser',
CourseID: 'testCourse',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('done');
expect(res.body.done).toHaveProperty('Username', 'testUser');
});
});
Marking a user as finished subtitle
describe('POST /userFinishSubtitle', () => {
test('it should store data about the user finishing the specified subtitle and return the updated user object', async () => {
const res = await request(app).post('/API/Users/userFinishSubtitle').send({
Username: 'testUser',
CourseID: 'testCourse',
SubtitleID: 'testSubtitle',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('done');
expect(res.body.done).toHaveProperty('Username', 'testUser');
});
});
Signing a user up for a course
describe('POST /addCourseToUser', () => {
test('it should sign the user up for the specified course and return a success message', async () => {
const res = await request(app).post('/API/Users/addCourseToUser').send({
Username: 'testUser',
CourseName: 'testCourse',
NumSubtitles: 10,
AmountPaid: 100,
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ done: 'Done' });
});
});
Issue a refund for a user
describe('POST /issueRefund', () => {
test('it should add money to the user's wallet and return a success message', async () => {
const res = await request(app).post('/API/Users/issueRefund').send({
Username: 'testUser',
Amount: 50,
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ done: 'Done' });
});
});
Admin creating a user
describe('POST /createUserByAdmin', () => {
test('it should create a new user and return the user object', async () => {
const res = await request(app).post('/API/Users/createUserByAdmin').send({
Email: 'test@example.com',
Username: 'testUser',
Password: 'testPassword',
UserType: 'Student',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Username', 'testUser');
expect(res.body).toHaveProperty('Email', 'test@example.com');
expect(res.body).toHaveProperty('Password', 'testPassword');
expect(res.body).toHaveProperty('UserType', 'Student');
});
});
Signup a user
describe('POST /createUserByAdmin', () => {
test('it should create a new user and return the user object', async () => {
const res = await request(app).post('/API/Users/createUserByAdmin').send({
Email: 'test@example.com',
Username: 'testUser',
Password: 'testPassword',
UserType: 'Student',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Username', 'testUser');
expect(res.body).toHaveProperty('Email', 'test@example.com');
expect(res.body).toHaveProperty('Password', 'testPassword');
expect(res.body).toHaveProperty('UserType', 'Student');
});
});
Rate a course
describe('POST /rateCourse', () => {
test('it should rate the specified course and return the updated course object', async () => {
const res = await request(app).post('/API/Courses/rateCourse').send({
id: 'testCourse',
Rating: 4,
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('id', 'testCourse');
expect(res.body).toHaveProperty('Rating', 4);
});
});
Create a course
describe('POST /createCourse', () => {
test('it should create a new course and return the course object', async () => {
const res = await request(app).post('/API/Courses/createCourse').send({
Preview: 'testPreview',
Title: 'testTitle',
Subject: 'testSubject',
Subtitles: 10,
Instructor: 'testInstructor',
Price: 100,
CreditHours: 5,
Discount: 0,
Description: 'This is a test course.',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Preview', 'testPreview');
expect(res.body).toHaveProperty('Title', 'testTitle');
expect(res.body).toHaveProperty('Subject', 'testSubject');
expect(res.body).toHaveProperty('Subtitles', 10);
expect(res.body).toHaveProperty('Instructor', 'testInstructor');
expect(res.body).toHaveProperty('Price', 100);
expect(res.body).toHaveProperty('CreditHours', 5);
expect(res.body).toHaveProperty('Discount', 0);
expect(res.body).toHaveProperty('Description', 'This is a test course.');
});
});
Get a currency
describe('GET /getCurrency', () => {
test('it should return the rate for converting dollars to the specified currency', async () => {
const res = await request(app).get('/API/Courses/getCurrency').query({
country: 'US',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('code', 'USD');
expect(res.body).toHaveProperty('rate');
});
});
Get all courses
describe('GET /getCourses', () => {
test('it should return all courses', async () => {
const res = await request(app).get('/API/Courses/getCourses');
expect(res.statusCode).toBe(200);
expect(res.body).toBeInstanceOf(Array);
});
});
Searching for a course
describe('POST /search', () => {
test('it should search for courses based on the provided query and return the matching courses', async () => {
const res = await request(app).post('/API/Courses/search').send({
query: 'testTitle',
});
expect(res.statusCode).toBe(200);
expect(res.body).toBeInstanceOf(Array);
});
});
Get a course by ID
describe('GET /getCourseById', () => {
test('it should return the course with the specified ID', async () => {
const res = await request(app).get('/API/Courses/getCourseById').query({
id: 'testCourse',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('id', 'testCourse');
});
});
Get a course by Name
describe('GET /getCourseByName', () => {
test('it should return the course with the specified name', async () => {
const res = await request(app).get('/API/Courses/getCourseByName').query({
id: 'testTitle',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Name', 'testTitle');
});
});
Get a subtitle by index and course ID
describe('GET /getSubtitleByIndexAndCourseID', () => {
test('it should return the specified subtitle for the specified course', async () => {
const res = await request(app).get('/API/Courses/getSubtitleByIndexAndCourseID').query({
id: 'testCourse',
index: 1,
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('Index', 1);
});
});
Get reviews of a course by ID
describe('GET /getCourseReviewsById', () => {
it('should return an array of course reviews for the specified course id', () => {
// Make a GET request to the API with a course id
const res = await request(app).get('/getCourseReviewsById?id=123');
// Assert that the response status code is 200
expect(res.statusCode).toBe(200);
// Assert that the response body is an array of course reviews
expect(Array.isArray(res.body)).toBe(true);
});
it('should return a 400 status code if no course id is provided', () => {
// Make a GET request to the API without a course id
const res = await request(app).get('/getCourseReviewsById');
// Assert that the response status code is 400
expect(res.statusCode).toBe(400);
});
});
Get a list of countries
describe('GET /getCountries', () => {
it('should return an array of countries', () => {
// Make a GET request to the API
const res = await request(app).get('/getCountries');
// Assert that the response status code is 200
expect(res.statusCode).toBe(200);
// Assert that the response body is an array of countries
expect(Array.isArray(res.body)).toBe(true);
});
});
Search as an instructor in your own courses
describe('GET /searchInstructor', () => {
it('should return an array of courses matching the search criteria', () => {
// Make a GET request to the API with instructor and search parameters
const res = await request(app).get('/searchInstructor?Instructor=John&Search=Math');
// Assert that the response status code is 200
expect(res.statusCode).toBe(200);
// Assert that the response body is an array of courses
expect(Array.isArray(res.body)).toBe(true);
});
it('should return a 400 status code if no instructor or search criteria is provided', () => {
// Make a GET request to the API without instructor and search parameters
const res = await request(app).get('/searchInstructor');
// Assert that the response status code is 400
expect(res.statusCode).toBe(400);
});
});
Filter courses by subject as an instructor
describe('GET /filterCoursesBySubjectInstructor', () => {
it('should return an array of courses matching the subject and instructor criteria', () => {
// Make a GET request to the API with instructor and subject parameters
const res = await request(app).get('/filterCoursesBySubjectInstructor?Instructor=John&Subject=Math');
// Assert that the response status code is 200
expect(res.statusCode).toBe(200);
// Assert that the response body is an array of courses
expect(Array.isArray(res.body)).toBe(true);
});
it('should return a 400 status code if no instructor or subject is provided', () => {
// Make a GET request to the API without instructor and subject parameters
const res = await request(app).get('/filterCoursesBySubjectInstructor');
// Assert that the response status code is 400
expect(res.statusCode).toBe(400);
});
});
Filter courses by price as an instructor
describe('GET /filterCoursesByPriceInstructor', () => {
it('should return an array of courses matching the price and instructor criteria', () => {
// Make a GET request to the API with instructor and price parameters
const res = await request(app).get('/filterCoursesByPriceInstructor?Instructor=John&Price=100');
// Assert that the response status code is 200
expect(res.statusCode).toBe(200);
// Assert that the response body is an array of courses
expect(Array.isArray(res.body)).toBe(true);
});
it('should return a 400 status code if no instructor or price is provided', () => {
// Make a GET request to the API without instructor and price parameters
const res = await request(app).get('/filterCoursesByPriceInstructor');
// Assert that the response status code is 400
expect(res.statusCode).toBe(400);
});
});
Filter courses by price as a user
describe('GET /filterCoursesByPrice', () => {
it('should return an array of courses matching the price criteria', () => {
// Make a GET request to the API with a price query parameter
const res = await request(app).get('/filterCoursesByPrice?query=100');
// Assert that the response status code is 200
expect(res.statusCode).toBe(200);
// Assert that the response body is an array of courses
expect(Array.isArray(res.body)).toBe(true);
});
it('should return a 400 status code if no price query is provided', () => {
// Make a GET request to the API without a price query parameter
const res = await request(app).get('/filterCoursesByPrice');
// Assert that the response status code is 400
expect(res.statusCode).toBe(400);
});
});
Contributions are always welcomed, make sure to reach out for us by email: akoussy24@gmail.com