Comrades Online Learning Platform

Implementation of a fully functional online learning platform through the use of the MERN stack

alt text alt text alt text alt text alt text alt text

Motivation 💪

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.

Build status ⚙️

Build Status

  • Project is still in development
  • Further improvents to UI/UX
  • Implementing a payment gateway
  • Unit Tests to be added

Code Style 📕

MongoDB

  • 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

Screenshots 📷

All Courses

Alt AllCourses

Landing Page

Alt Home

Admin View

Alt AdminView

Course Preview

Alt Preview

Course Promotions

Alt CoursePromotions

My Courses

Alt AdminView

Login

Alt Login

Instructor's Courses

Alt InstructorView

Tech Stack 💻

Client: React, Bootstrap & Material UI

Server: Node, Express, MongoDB

Features 📃

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.

Code Examples 💾

Below is an example of how to login using on our platform.

Frontend:

React Component Code:

<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>

handleSubmit Method:

const handleSubmit = async (e) => {
    e.preventDefault()
    await login(username.toLowerCase(), password)
}

useLogin Hook login Method

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)
    }
  }

Backend:

User Model Method

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
}

Installation 🔌

Clone the repo using:

gh repo clone Advanced-Computer-Lab-2022/Comrades

Make sure NPM is installed:

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.

Install required packages for frontend:

  cd front_end
  npm install

Install required packages for backend:

  cd back_end
  npm install my-project

Create a .env file in the backend:

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.

Finally to run the program:

Make sure you have 2 terminals open, one for the front and one for the back.

To run the frontend:

cd front_end
npm start

To run the backend:

Please note that npm run_dev only for development mode.

cd back_end
npm run dev

API Reference 📚

Please visit the following link for the full API references documentation(Preferably don't use a mobile although its supported):

Comrades API Documentation

Tests ✏️

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);
});
});

Contributing 🤝

Contributions are always welcomed, make sure to reach out for us by email: akoussy24@gmail.com

Credits 💰

License ⚖️

MIT

ExchangeRate-API