Giter VIP home page Giter VIP logo

ambizo's Introduction

Ambizo

A website made for the "Canadian Chamber of Commerce" used to connect Teachers to Students (Either individual or corporate students). Teachers upload their courses which comprise of VODs and small tests. Students can enroll in these courses and access the content.

Motivation

We understand the importance of a learning platform that is packed with features and easy to use (for both students and instructors), in order for students to feel right at home when trying to digest new material and concepts. With that in mind, we created "Ambizo", to connect students and instructors all across the globe through an elegant UI and an enjoyable and easy-to-use UX.

The original reason we created this website was for a university project for the following educational benefits:

  • Learn how to properly use the Agile Methodology to plan out a project and develop the software.

  • Learn the process of following a given set of System Requirements to develop a software.

  • Learn to research and master the use of the MERN Stack.

  • Learn how to work together as a team on GitHub.

Build Status

As of writing this file, there aren't any bugs that we are aware of. Website is currently not deployed.

Code Style

  • Use "Prettier" as a code formatter to keep the code style consistent

  • Only use single-line comments to describe sub-sections of a longer piece of code or to describe code that is not the easiest to understand

  • All function names are camelCased

  • Use function declaration, not function expression

  • Used both synchronous and asynchronous functions depending on whether or not the database is called in the function

  • Use braces with all control flow statements and loops

Screenshots

Homepage

Home Page Home Page 2

Login Page

Login Page

All Courses Page

All Courses Page Filter Courses

Sign up Page

SignUp Page SignUp Page 2 SignUp Page 3 SignUp Page 4

Course Details Page

Updated Course Page 1 Updated Course Page 2 Updated Course Page not completed progress

Trainee Homepage

Trainee HomePage Trainee HomePage 2

Trainee Profile

Trainee Profile Change Password Page Navbar dropdown

Instructor Homepage

Instructor Home Page 1 Instructor Home Page 2

Instructor Profile

Instructor Profile

Instructor Reports

Instructor Reports

Admin Homepage

Admin HomePage

Admin Add User

Admin add user

Admin Discounts Page

Admin Discounts Page Admin Discounts Page 2 Admin Discounts Page 3

Popup Modal

PopUp Modal, Admin

Admin Refund Requests

Admin Refund requests

Admin Reports

Admin reports

Admin Course Access Requests

Admin Course Access Requests

Tech Stack

MERN Stack:

  • M: MongoDB

  • E: ExpressJS

  • R: ReactJS

  • N: NodeJS

Additional

  • Json Web Tokens (JWT) for Authentication / Authorization

  • Axios for HTTP

  • Packages used can be found in the credits section of this document

Features

As a Guest

  • Sign Up as an individual trainee

  • Login as a(n): -Individual Trainee -Corporate Trainee -Instructor -Administrator

As a Guest / Logged in User of any kind

  • View most popular courses, on Home Page

  • View all courses

  • Search for a course by title, subject, or instructor

  • Filter courses by:

    • Subject
    • Rating
    • Price Range
  • Select Country of residence

  • Live Currency Conversion:

    • Currencies displayed change depending on end-users' selected location (Value stays the same)
  • Watch a course preview and view subtitle names

  • View course ratings

  • View instructor profile and ratings

As a Logged in User of Any Kind

  • Change Password

As an Instructor

  • Must accept Terms and Conditions when logging in for the first time

  • Upload a course, which consists of the following:

    • Course Name
    • Course Image
    • Brief Course Description
    • Course Price
    • Course Preview Video
    • Course Subtitles (VODs)
    • Exercises
  • Create an exercise inside a subtitle

  • Live Previews when:

    • Creating a course
    • Creating an exercise for a subtitle
  • Fill in Instructor Profile, which consists of:

    • Instructor Name
    • Instructor Contact Details
    • Instructor Bio
    • Instructor Courses
  • View "My Profile"

  • View "My Courses"

  • View "My Reports"

  • View "All Courses"

  • Apply a promotion to your courses (Discount)

  • View Wallet Amount

As a Corporate / Individual Trainee

  • View "My Profile"

  • View "My Courses"

  • View "My Reports"

  • Enroll in a course as an individual trainee by paying

  • Enroll in a course for free as a corporate trainee by requesting access from an Administrator

  • View Course Progress (How much of the course you completed)

  • View Course Length (Hours and Minutes)

  • View Course rating (Stars and number of ratings)

  • Rate a course

  • View Course Reviews

  • Rate an instructor

  • Request a refund as an individual trainee if course progress < 50%

    • Cannot refund the same course more than once, to prevent loopholes
  • Write Notes while watching a VOD and download them

  • Receive a certificate upon completing a course

As an Administrator

  • Add the following:

    • Corporate Trainee
    • Instructor
    • Administrator
  • Apply discounts to courses

  • Remove discounts

  • View and handle Reports

  • View and handle Corporate Trainee access requests

  • View and handle Refund requests

Code Examples

Routes:

function App() {

    return (
        <Router>
            <>
                <Routes>
                    <Route path='*' element={<Navigate to="/404" />} />
                    <Route path='/404' element={<NotFound />} />
                    <Route path='/allcourses' element={<CoursesPage />} />
                    <Route path='/coursedetails/:courseId' element={<CourseDetailsPage />} />
                    <Route path='/search/:searchTerm' element={<SearchPage />} />
                    <Route path='/requestPasswordReset' element={<RequestPasswordResetPage />} />
                    <Route path='/resetPassword/:resetToken' element={<PasswordResetPage />} />
                    <Route path='/user/:username' element={<UserProfile />} />
                    <Route path='/login' element={<LoginPage />} />
                    <Route path='/signUp' element={<SignUpPage />} />

                    <Route path="/" element={<HomepageRoutes />} />

                    <Route path="/" element={<AdminRoutes />}>
                        <Route path="/addadmin" element={ <AddAdministrator /> } />
                        <Route path="/addtrainee" element={ <AddCorporateTrainee /> } />
                        <Route path="/addinstructor" element={ <AddInstructor /> } />
                        <Route path='/pricesanddiscounts' element={<AdminSetPromotion />} />
                        <Route path='/courseaccessrequests' element={<CoursesAccessRequests />} />
                        <Route path='/refundrequests' element={ <RefundRequestsPage /> } />
                    </Route>

                    <Route path="/" element={<InstructorRoutes />}>
                        <Route path="/mycourses" element={<InstructorCoursesPage />} />
                        <Route path="/addcourse" element={<AddCourse />} />
                        <Route path='/addExercise/:courseId/:exerciseNum' element={<AddExercise /> } />
                        <Route path='/:mycourses/search/:searchTerm' element={<SearchPage />} />
                        <Route path='/definediscount/:courseId' element={<DiscountPage />} />
                    </Route>

                    <Route path="/" element={<UserRoutes />}>
                        <Route path='/exercise/:courseId/:exerciseNum' element={<ExercisePage />} />
                        <Route path='/settings' element={<SettingsPage />} />
                        <Route path='/allreports' element={<ReportsPage />} />
                        <Route path='/checkout/:courseId' element={<CheckoutPage />} />
                    </Route>
                    
                </Routes>
                <Footer />
                <ScrollToTopButton />
            </>
        </Router>
    )
} 

Updating subtitle progress and mailing the completion certificate if the overall course progress

reaches 100%:

router.put("/updateSubtitleProgress", verifyJWT, async (req, res) => {
    try{
        if (req.User.Type !== 'corporateTrainee' && req.User.Type !== 'individualTrainee'){
            return handleError(res, "Invalid Access")
        }

        let updateStatement = {
            "$set": {}
        }
        updateStatement["$set"]["EnrolledCourses.$.progress."+req.query.subtitleNum] = req.body.newProgress;
        
        let trainee = null;
        if(req.User.Type === 'corporateTrainee'){
           trainee = await corporateTrainee.findOneAndUpdate({Username: req.User.Username, "EnrolledCourses.courseId": req.query.courseId}, updateStatement, {new: true})
        }
        else{
            trainee = await individualTrainee.findOneAndUpdate({Username: req.User.Username, "EnrolledCourses.courseId": req.query.courseId}, updateStatement, {new: true})
        }


        let completedCourse = trainee.EnrolledCourses.filter(enrolledCourse => enrolledCourse.courseId === req.query.courseId);
        if(!completedCourse){
            return handleError(res, "Not Enrolled in course")
        }
        completedCourse = completedCourse[0];

        if(completedCourse.certificateSent){
            return res.send("Progress Updated Successfully");
        }

        let Course = await course.findById(req.query.courseId);

        let courseCompleted = true;
        for(let i =0; i<Course.Subtitles.length; i++){
            if(!completedCourse.progress[i] || completedCourse.progress[i]!== 1){
                courseCompleted = false
                break;
            }
        }

        if(!courseCompleted){
            return res.send("Progress Updated Successfully");
        }

        await mailCertificate(trainee, Course.Title, req.User.Type, req.query.courseId, res);
    }
    catch(err){
        handleError(res, err);
    }
})

async function mailCertificate(trainee, courseName, type, courseId, res) {
    try{
        let certificateFileName = "";
        if(trainee.Name.charAt(trainee.Name.length - 1) === 's'){
            certificateFileName =  + trainee.Name + "' Certificate.pdf"
        }
        else{
            certificateFileName = trainee.Name + "'s Certificate.pdf"
        }
    
        var transporter = nodemailer.createTransport({
            service: 'gmail',
            auth: {
              user: process.env.BUSINESS_EMAIL,
              pass: process.env.BUSINESS_EMAIL_PASSWORD
            }
        });
    
        var mailOptions = {
            from: process.env.BUSINESS_EMAIL.toString(),
            to: trainee.Email,
            subject: 'Course Certificate',
            html: 
            `<h1>Hi ${trainee.Name},
            </h1><p>Congratulations on completing ${courseName}!</p>
            <p>Kindly find the certificate attached below</p>
            <p>You can now also download the certificate from the website</p>`,
            attachments: [{
                filename: `${certificateFileName}.pdf`,
                content: generateCertificate(trainee.Name, courseName)
            }]
        }

        await transporter.sendMail(mailOptions)
    
        if(type === 'corporateTrainee'){
            await corporateTrainee.findOneAndUpdate(
                {Username: trainee.Username, "EnrolledCourses.courseId": courseId}, 
            {
                $set: {
                    'EnrolledCourses.$.certificateSent': true
                }
            })
        }
        else{
            await individualTrainee.findOneAndUpdate(
                {Username: trainee.Username, "EnrolledCourses.courseId": courseId}, 
                {
                    $set: {
                        'EnrolledCourses.$.certificateSent': true
                    }
                })
        }
        return res.send("Progress Updated and Email Sent Successfully");
    }
    catch(err){
        console.log(err)
        return res.send("Error occured in emailing the certificate");
    }
}

Fetching all courses, filtering them according to applied filters and search term, and calcuating

their prices according to selected country and currently active discount:

router.get("/getCourses", async (req,res) => {
    try{
        let filter = {}
        let courses = null;
        let exchangeRateToUSD = null;
        let exchangeRateToCountry = null;

        if(req.query.subject){
            filter = {...filter, Subject: req.query.subject.split(',')};
        }
        if(req.query.rating){
            filter = {...filter, Rating: {$gte: req.query.rating}};
        }
        if(req.query.price){
            exchangeRateToUSD = await currencyConverter.from(req.query.currencyCode).to("USD").convert()
            const priceRange = req.query.price.split(',');
            const minPrice = priceRange[0] * exchangeRateToUSD;
            let maxPrice = priceRange[1] * exchangeRateToUSD;
            if(maxPrice){
                if(maxPrice < 0){
                    maxPrice = 0;
                }
                filter = {...filter, PriceInUSD: {$lte: maxPrice, $gte: minPrice}};
            }
            else{
                filter = {...filter, PriceInUSD: {$gte: minPrice}};
            }
        }
        if(req.query.searchTerm){
            filter ={
                ...filter,
                $or: [
                    {Title: {$regex: '.*' + req.query.searchTerm + '.*', $options: 'i'}},
                    {Subject: {$regex: '.*' + req.query.searchTerm + '.*', $options: 'i'}},
                    {InstructorName: {$regex: '.*' + req.query.searchTerm + '.*', $options: 'i'}}
                ]
            }
        }

        if(!req.query.price){
             [courses, exchangeRateToUSD, exchangeRateToCountry] = await Promise.all(
                [
                    course.find(filter)
                    , 
                    currencyConverter.from(req.query.currencyCode).to("USD").convert()
                    ,
                    currencyConverter.from("USD").to(req.query.currencyCode).convert()
                ]
                );
        }
        else{
            courses = await course.find(filter);
            exchangeRateToCountry = await currencyConverter.from("USD").to(req.query.currencyCode).convert();
        }

        const currentDate = new Date();
        const currentDay = currentDate.getDate();
        const currentMonth = currentDate.getMonth();
        const currentYear = currentDate.getFullYear();

        courses.forEach(course => {
            course.PriceInUSD = (course.PriceInUSD * exchangeRateToCountry).toFixed(2)
            if(currentYear > course.DiscountExpiryDate.getFullYear()) {
                course.Discount = 0;
            }
            else if(currentYear == course.DiscountExpiryDate.getFullYear()) {
                if(currentMonth > course.DiscountExpiryDate.getMonth()) {
                    course.Discount = 0;
                }
                else if(currentMonth == course.DiscountExpiryDate.getMonth()) {
                    if(currentDay > course.DiscountExpiryDate.getDate()) {
                        course.Discount = 0;
                    }
                }
            }
        })
        res.json(courses);
    }
    catch(err){
        handleError(res, err.message);
    }
})

Request password post that sends password reset email

router.post('/requestPasswordReset', async (req, res) => {
    const {username} = req.body;
    if( !username ){
        return res.status(400)
        .json({
            message: 'Missing username field in the body of the request.'
        });
    }

    const requestingUser = await user.findOne({Username: username});

    if( !requestingUser ){
        return res.status(404)
        .json({
            message: 'There is no user with this username.'
        });
    }
    const { _id, Username, Type} = requestingUser;
    let userWithEmail;
    switch(Type){
        case "admin": 
            userWithEmail = await administrator.findOne({Username: Username}); 
            break;
        case "instructor": 
            userWithEmail = await instructor.findOne({Username: Username}); 
            break;
        case "corporateTrainee": 
            userWithEmail = await corporateTrainee.findOne({Username: Username}); 
            break;
        case "individualTrainee": 
            userWithEmail = await individualTrainee.findOne({Username: Username});
            break;
        default:
            return res.status(500)
            .json({
                message: 'Failed in finding user email.'
            });
    }
    if(! userWithEmail ){
        return res.status(500)
            .json({
                message: 'Failed in finding user email.'
            });
    }

    const { Name, Email } = userWithEmail;

    var transporter = nodemailer.createTransport({
        service: 'gmail',
        auth: {
          user: process.env.BUSINESS_EMAIL,
          pass: process.env.BUSINESS_EMAIL_PASSWORD
        }
      });

      const jwtPayload = {
        userId: _id,
        username: Username,
        email: Email,
      }
      jwt.sign(
        jwtPayload,
        process.env.RESET_PASSWORD_ACCESS_TOKEN_SECRET,
        {expiresIn: 7200},
        async (error, token) => {
            if(error){
                console.log(error);
                return res.status(500).json({
                    message: 'An error has occured while creating the JWT for the request.'
                });
            }
            const newUsedResetPasswordToken = new usedResetPasswordToken({
                ResetToken: token.toString()
            });

            newUsedResetPasswordToken.save().then(
                _ => {
                    const link = `${process.env.FRONTEND_URL}resetPassword/${token}`

                    var mailOptions = {
                        from: process.env.BUSINESS_EMAIL.toString(),
                        to: Email,
                        subject: 'Password Reset',
                        html: `<h1>Hi ${Name},</h1><p>It seem's you have forgotten your password.</p><p>Don't worry, Click <span><a href=${link}>HERE</a></span> to reset it.</p><p>Please note that the link will expire after 2 hours. After the 2 hours, you have to submit a new reset password request.</p>`
                    }
                    transporter.sendMail(mailOptions)
                    .then(info => {
                        return res.status(200).json({
                            message: 'Reset password email has been sent successfully.',
                            emailSentTo: Email
                        })
                    })
                    .catch(error => {
                        console.log(error);
                        return res.status(500).json({
                            message: 'Sending the reset password email failed.',
                        });
                    });
                }
            ).catch(error => {
                console.log(error);
                return res.status(500).json({message: 'An error happened while saving the reset token to the database.'})
            })
        }
      );
});

Subtitle video

function updateDuration(event){
        if(!props.duration && props.instructorLoggedInCourse){
            const seconds = event.target.getDuration()
            const minutes = Math.floor(seconds / 60)
            let newSubtitle = {
                subtitle: props.subtitle,
                duration: minutes,
                youtubeLink: subtitleDetails.youtubeLink,
                description: subtitleDetails.description
            }
            InstructorService.addSubtitleDetails(props.index , newSubtitle, props.courseId)
                .then((res) => {
                    props.modifyCourseDetailsPageSubtitle(newSubtitle, props.index, res.data.newTotalMinutes);
                })
                .catch((error) => {
                    setMessage({ text: error.response.data, type: "form--errormessage" })
                })
        }
    }

function validateYouTubeUrl(youtubeLink) {
        var p = /^(?:https?:\/\/)?(?:m\.|www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
        if(youtubeLink.match(p)){
            return youtubeLink.match(p)[1];
        }
        return false;
    }

    const opts = {
        height: '390',
        width: '95%',
        playerVars: {
          // https://developers.google.com/youtube/player_parameters
          autoplay: 0,
        },
    }

// Located in the html:

 <YouTube className="subtitle--video" videoId={validateYouTubeUrl(props.youtubeLink)} 
 opts={opts} onPlay={props.updateSubtitleProgress} onReady={updateDuration}/>

Administrator Services Page

import http from "../http-common-post"
import httpPost from "../http-common-post";

class AdministratorService {
    addAdmin(AdminData) {
        return http.post("/admin/addAdministrator", AdminData);
    }
    addTrainee(traineeData) {
        return http.post("/admin/addCorporateTrainee", traineeData);
    }
    addInstructor(instructorData) {
        return http.post("/admin/addInstructor", instructorData);
    }
    getAllReports() {
        return http.get(`/admin/getAllReports/`);
    }
    updateReportStatus(reportId, status) {
        return httpPost.put("/admin/updatereportstatus/?reportId=" + reportId, { Status: status });
    }
    applyDiscount(courses, discountPercentage, expiryDate) {
        return httpPost.put("/admin/applyDiscount/?courses=" + courses + "&discount=" + discountPercentage + "&expiryDate=" + expiryDate)
    }
    getAllAccessRequests() {
        return http.get("/admin/getAllAccessRequests")
    }
    grantAccess(corporateUsername, courseId) {
        return httpPost.post("/admin/grantAccess/?corporateUsername=" + corporateUsername + "&courseId=" + courseId)
    }
    declineAccess(corporateUsername, courseId) {
        return httpPost.post("/admin/declineAccess/?corporateUsername=" + corporateUsername + "&courseId=" + courseId)
    }
    getAllRefundRequests(){
        return http.get('admin/getAllRefundRequests');
    }
    acceptRefundRequest(reqBody){
        return httpPost.post('/admin/acceptRefundRequest', reqBody);
    }
    rejectRefundRequest(reqBody){
        return httpPost.post('/admin/rejectRefundRequest', reqBody);
    }
}

export default new AdministratorService();

Divide Reports Based on Status

let unseenCount = 0;
    let pendingCount = 0;
    let resolvedCount = 0;

    let reportList = reports?.map((report, index) => {
        if (report.Status === "unseen") {
            unseenCount++;
        } else if (report.Status === "pending") {
            pendingCount++;
        } else if (report.Status === "resolved") {
            resolvedCount++;
        }
        return (
            <Report
                key={index}
                index={index}
                reportId={report._id}
                userType={userType}
                reporter={report.Username}
                date={report.createdAt.substring(0, 10)}
                courseTitle={report.CourseTitle}
                courseId={report.CourseId}
                description={report.Description}
                type={report.Type}
                status={report.Status}
                followUp={report.FollowUp}
                updateFollowUp={updateFollowUp}
                updateStatus = {updateStatus}
            />
        );
    });

    reportList = _.sortBy(reportList, ["props.status"]);

    return (
        <>
            <Helmet>
                <title>Reports</title>
            </Helmet>
            <div className={"loader-container" + (!isLoading ? " hidden" : "")}>
                <div className="spinner"> </div>
            </div>
            {isLoading ?
                (
                    <>
                        <Header />
                    </>
                )
                :
                (
                    <>
                        <Header />
                        <div className="reports--headerdiv">
                            <h1 className="reports--header">Reported Problems</h1>
                            <img className="reports--pricesimage" src={AdminReportsImage} alt='Reports' />
                        </div>
                        {unseenCount === 0 && pendingCount === 0 && resolvedCount === 0 &&
                            <div className="reports--number">
                                <h2>No Reports</h2>
                            </div>
                        }
                        {unseenCount > 0 &&
                            <>
                            <div className="reports--number">
                                <h2>{unseenCount} Unseen {unseenCount === 1 ? "Report" : "Reports"}</h2>
                            </div>
                            <section className="reports-list">
                                {reportList.slice(pendingCount + resolvedCount)}
                            </section>
                            </>
                        }
                        {pendingCount > 0 &&
                            <>
                            <div className="reports--number">
                                <h2>{pendingCount} Pending {pendingCount === 1 ? "Report" : "Reports"}</h2>
                            </div>
                            <section className="reports-list">
                                {reportList.slice(0, pendingCount)}
                            </section>
                            </>
                        }
                        {resolvedCount > 0 &&
                            <>
                            <div className="reports--number">
                                <h2>{resolvedCount} Resolved {resolvedCount === 1 ? "Report" : "Reports"}</h2>
                            </div>
                            <section className="reports-list">
                                {reportList.slice(pendingCount, pendingCount + resolvedCount)}
                            </section>
                            </>
                        }    
                    </>
                )
            }
        </>
    );
}

Axios Instance With Bearer Token Authorization Header

import axios from "axios";

const apiClient = axios.create({
    baseURL: "http://localhost:5000/api",
    headers: {
        "Content-type": "application/json"
    }
});

apiClient.interceptors.request.use(function (config) {
    config.headers["Authorization"] = sessionStorage.getItem("Token");
    return config;
});

export default apiClient;

Installation

Step 1: Make sure you have Git, VS Code, and Node installed:

https://git-scm.com/
https://code.visualstudio.com/
https://nodejs.org/en/

Step 2: Create a new folder on your local machine, right click in the folder, and click "Git Bash Here":

Git bash Here

Step 3: In the Git Bash terminal type the following and press enter:

git clone https://github.com/Advanced-Computer-Lab-2022/Ambizo.git

Step 4: A new folder should be created. Right click on the folder and click "Open with Code"

Open with VS

Step 5: After Visual Studio Code opens, right click in the "backend" folder and click "new file". Name the file ".env" and paste the following into it:

MONGO_URI=mongodb+srv://youssefelshorbagy:[email protected]/ACL?retryWrites=true&w=majority
PORT=5000
ACCESS_TOKEN_SECRET=225712fdf1d775dd6df573b3b9434806a537531c6ec25ccb39710e0f4d942b3177986137819c9a6613d6534d2584c8d6e67e8f80dec03990511b69e203f2bed9
RESET_PASSWORD_ACCESS_TOKEN_SECRET = f4d91ac0f31d515118995b07d152ec1c4aad931ec0f1137dbf4c803abdc0b0ca8ec784a5fcb10b619cc819a06178c357ddfd7023506a6fa3f9ecce30481c8090
BUSINESS_EMAIL = [email protected]
BUSINESS_EMAIL_PASSWORD = zkpcjykdforumvtz
FRONTEND_URL = http://localhost:3000/
STRIPE_PUBLISHABLE_KEY = pk_test_51MEyDECiBvnkrLCQwKbaLB5jbOmmA1bq9noW99PGEPG3Njd3mS7wEuc7WzAZxriQ6vuzI1JKv9M08nXiTPK4iX7U00WWJLERUl
STRIPE_SECRET_KEY = sk_test_51MEyDECiBvnkrLCQHfYBCVD8HB1dw2SL8SszrqbU8WNlKOZtcaaptaIth3ZcA5EQwFJMqio0QwB2qPFEnQr6IOkL00RWoQGvTa

New File

Step 6: Create 2 New Terminals from the top left of the screen:

New Terminal

Step 7: In the first terminal type the following lines one-by-one and press ENTER after each line (Wait for the third line to finish installing after you press enter):

        cd Ambizo
        cd backend
        npm install --force
        node index.js

Step 8: In the second terminal type the following lines one-by-one and press ENTER after each line (Wait for the third line to finish installing after you press enter):

        cd Ambizo
        cd frontend
        npm install --force
        npm start

Step 9: The website should automatically launch itself locally on your default browser; however, if it does not, type the following in your browser search bar:

    http://localhost:3000/

Enjoy :)

API Documentation

Tests

  • Postman tests ran to make sure parameters were passed and recieved correctly.

  • Tested the website in real-world applications using different user roles, to make sure there were no loopholes or errors in the system.

How to Use

Launch the website with the steps found in the "Installation" section, and login from the top right corner of the screen as either a(n) Individual Trainee, Corporate Trainee, Instructor, or Administrator using the login credentials below. Then proceed to follow the steps below.

Testing Login Credentials:
///////////////////////// AS A CORPORATE/INDIVIDUAL TRAINEE ////////////////////////

    Individual Trainee:
        
        UserName: omar.sameh
        PassWord: Ambizo123!

    Corporate Trainee:
        
        UserName: omar.elrasas
        PassWord: Ambizo123!

//////////////////////////////// AS AN INSTRUCTOR //////////////////////////////////// 

        UserName: slim.abdelzaher
        PassWord: Balabizo123!

////////////////////////////// AS AN ADMINISTRATOR //////////////////////////////////////

        UserName: super.admin
        PassWord: Admin123!

User Journeys

As a guest

  • Sign Up from the top right corner

  • Login from the top right corner

Video
As.a.Guest.mp4

As a trainee

  • Click on "View All courses" on the Home Page.

  • Browse the courses and pick a course you might be interested in, or need for your education Feel free to apply a filter from the top right corner of the courses page, or use the search bar at the top pf the screen in order to narrow down your search.

  • Click on the course and watch the course preview.

  • If you're interested and you're an individual trainee, buy the course by clicking the "Enroll" button under the course thumbnail and paying; however, if you're not interested, use your browser's back arrow to navigate to the previous page and search for a course you're interested in until you find one, and enroll in it.

  • On the other hand, if you're a corporate trainee, you will need to click on "Request Access" under the course thumbnail, and must wait until an Admin either accepts or rejects your access request.

  • After you enroll in a course, open the course details page, scroll down to the subtitles, and watch them one-by-one, while writing notes and downloading them. After each subtitle, make sure you solve the uploaded exercise (found in the bottom right corner of the subtitle dropdown).

  • After completing a course, you will recieve an email with your certificate. You can also download your certificate on the course details page from the right side of the screen by clicking "here" written in red.

  • You can rate the course and instructor from the top of the course details page, under the course thumbnail (Photo).

  • If your course progress is less than 50%, you can refund the course from the "Request a refund" button under your course progress, on the right side of the screen.

  • If you ever face a problem, you can report a problem from the bottom right corner of the course details page.

  • Whatever page you're on, you can always look at the top right corner of the screen to see your wallet.

  • If you ever want to return to the Home page, scroll to the top of the page and click on the company logo at the top left corner of the screen.

  • In the Home Page, you can click on "My Courses" or scroll down to see the courses you are enrolled in.

  • You can also click on "My Profile" to see your profile, where you can click on "Setting and Privacy" where you can change your Password.

  • Furthermore, if you go back to the Home Page, you can click "View My Reports" to see the status of your reported problems.

  • Finally, if you ever need to log out, click on your name in the top right corner and click "Log Out"

Videos
Trainee.Enrolling.in.a.course.mp4
Trainee.Course.and.notes.mp4
Trainee.Last.mp4
Corporate.Trainee.mp4

As an instructor

  • On the Home Page, you can click "Create a Course" where you will be able to create a course consisting of a title, a thumbnail, course description, VODs, etc. all while seeing a preview of what it will all look like, on the right side of the screen.

  • After creating a course, you can navigate to the Home page by clicking on the company logo at the top left corner of the screen.

  • Once you're back on the Home page, you can scroll down or click on "View My Courses" to see the courses you have created.

  • You can also click on "View All Courses" to see what other instructors are up to.

  • You can browse the courses and apply a filter from the top right corner of the courses page, or you can use the search bar at the top pf the screen to search for particular courses.

  • You can click on a course to see the course details page, which contains details about the course, such as the course description, course preview video, etc.

  • You can go back to your existing courses and edit them as you please. You can do things such as view, add, or delete exercises, add a discount, change the course preview video, etc.

  • You can see how many people are enrolled in your course, under the course description.

  • You can also see the course ratings if you scroll down to the bottom of the course details page.

  • If you ever face a problem, you can report a problem from the bottom right corner of the course details page.

  • Whatever page you're on, if you're an individual trainee, you can always look at the top right corner of the screen to see your wallet.

  • If you go back to the home page, you can also click on "My Profile" to see your profile, where you can add your contact details, add your bio, and view your ratings.

  • On your profile page, you can also click on "Setting and Privacy" where you can change your Password.

  • Furthermore, if you go back to the Home Page, you can click "View My Reports" to see the status of your reported problems.

  • Finally, if you ever need to log out, click on your name in the top right corner and click "Log Out"

Videos
As.an.instructor.without.adding.course-00.00.00.000-00.01.43.791.mp4
As.an.instructor.adding.course-00.00.00.000-00.03.09.931.mp4

As an administrator

  • On the Home Page, you can click on "Add Another Admin", "Add Instructor", or "Add Corporate Trainee" in order to add an administrator, an instructor, or a corporate trainee to the system.

  • You can click on the company logo at the top left corner of the screen to return to the Home Page.

  • On the Home Page, you can also click on "View Reported Problems" to view and handle reports. You can set them as either "Pending" or "Resolved".

  • On the Home Page, you can also click on "View Corporate Requests" to view and handle pending corporate trainee requests to enroll in a course (you can either accept or decline).

  • On the Home Page, you can also click on "View Refund Requests" to view and handle refund requests from individual trainees (you can either accept or decline).

  • On the Home Page, you can scroll down to view the most popular courses and can click on "View All Courses" to view all courses.

  • Moreoever, you can also click on "Prices and Discounts" on the Home Page. There you can select courses and apply discounts to them, setting the discount amount(%) and expiry date. If you scroll to the bottom of the page, you can also select courses to remove their discounts before the expiry date.

  • Finally, if you ever need to log out, click on your name in the top right corner and click "Log Out".

Video
As.an.Admin.mp4

Contributing

Contributions are always welcome!

For Contributions, email us at : [email protected]

Credits

npm packages:

Backend:
    
    currency-converter-lt
    jspdf
    bcrypt
    nodemailer
    stripe

Frontend:

    react-pdf
    country-to-currency
    multiselect-react-dropdown
    react-calendar
    react-helmet
    react-select
    react-select-country-list
    react-youtube
    sweetalert
    underscore
    jspdf

Additional:

Vectors used: https://www.freepik.com/author/stories

License

Stripe-node

Copyright (C) 2011 Ask Bjørn Hansen
Copyright (C) 2013 Stripe, Inc. (https://stripe.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

React-stripe-js

MIT License

Copyright (c) 2017 Stripe

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

React

MIT License

Copyright (c) Meta Platforms, Inc. and affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Sweetalert

The MIT License (MIT)

Copyright (c) 2014-present Tristan Edwards

Permission is hereby granted, free of charge, to any person obtaining a copy of 
this software and associated documentation files (the "Software"), to deal in the 
Software without restriction, including without limitation the rights to use, copy, 
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 
and to permit persons to whom the Software is furnished to do so, subject to the 
following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
DEALINGS IN THE SOFTWARE.

jsPDF

Copyright
(c) 2010-2021 James Hall, https://github.com/MrRio/jsPDF
(c) 2015-2021 yWorks GmbH, https://www.yworks.com/

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Nodemailer

Copyright (c) 2011-2017 Andris Reinman

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

ambizo's People

Contributors

abdelrahman-hanafy-dev avatar ligtexpe45 avatar nada-abdelfattah avatar omarr01 avatar omartarek126 avatar youssefelshorbagy avatar

Stargazers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.