├── .gitignore
├── README.md
├── client
├── build
│ ├── asset-manifest.json
│ ├── favicon.ico
│ ├── index.html
│ ├── manifest.json
│ ├── service-worker.js
│ └── static
│ │ ├── css
│ │ ├── main.cb2e88d7.css
│ │ └── main.cb2e88d7.css.map
│ │ ├── js
│ │ ├── main.9083edaf.js
│ │ └── main.9083edaf.js.map
│ │ └── media
│ │ └── bath_logo_black.9c2a90f6.svg
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
│ ├── actions
│ ├── courseActions.js
│ ├── errorActions.js
│ ├── typesActions.js
│ └── userActions.js
│ ├── components
│ ├── App
│ │ ├── App.css
│ │ └── App.js
│ ├── Auth
│ │ └── Auth.js
│ ├── Container
│ │ ├── Container.css
│ │ └── Container.js
│ ├── Course
│ │ ├── Course.css
│ │ ├── Course.js
│ │ ├── CourseFiles
│ │ │ └── CourseFiles.js
│ │ ├── CourseUsers
│ │ │ └── CourseUsers.js
│ │ └── CourseVideos
│ │ │ └── CourseVideos.js
│ ├── Courses
│ │ └── Courses.js
│ ├── CreateCourse
│ │ ├── CreateCourse.css
│ │ └── CreateCourse.js
│ ├── CreateUser
│ │ └── CreateUser.js
│ ├── EditCourse
│ │ └── EditCourse.js
│ ├── Header
│ │ ├── Header.css
│ │ └── Header.js
│ ├── Home
│ │ ├── Home.css
│ │ └── Home.js
│ ├── Login
│ │ ├── Login.css
│ │ └── Login.js
│ ├── Main
│ │ └── Main.js
│ ├── MyCourses
│ │ └── MyCourses.js
│ ├── NoPage
│ │ └── NoPage.js
│ ├── Sidebar
│ │ ├── Sidebar.css
│ │ └── Sidebar.js
│ └── Users
│ │ └── Users.js
│ ├── configStore.js
│ ├── education-logo-login.png
│ ├── education-logo.png
│ ├── helpers.js
│ ├── index.css
│ ├── index.js
│ ├── reducers
│ ├── courseReducer.js
│ ├── errorReducer.js
│ ├── index.js
│ └── userReducer.js
│ ├── registerServiceWorker.js
│ └── test
│ └── Components.test.js
├── docs
├── assets
│ ├── anchor.js
│ ├── bass-addons.css
│ ├── bass.css
│ ├── fonts
│ │ ├── EOT
│ │ │ ├── SourceCodePro-Bold.eot
│ │ │ └── SourceCodePro-Regular.eot
│ │ ├── LICENSE.txt
│ │ ├── OTF
│ │ │ ├── SourceCodePro-Bold.otf
│ │ │ └── SourceCodePro-Regular.otf
│ │ ├── TTF
│ │ │ ├── SourceCodePro-Bold.ttf
│ │ │ └── SourceCodePro-Regular.ttf
│ │ ├── WOFF
│ │ │ ├── OTF
│ │ │ │ ├── SourceCodePro-Bold.otf.woff
│ │ │ │ └── SourceCodePro-Regular.otf.woff
│ │ │ └── TTF
│ │ │ │ ├── SourceCodePro-Bold.ttf.woff
│ │ │ │ └── SourceCodePro-Regular.ttf.woff
│ │ ├── WOFF2
│ │ │ ├── OTF
│ │ │ │ ├── SourceCodePro-Bold.otf.woff2
│ │ │ │ └── SourceCodePro-Regular.otf.woff2
│ │ │ └── TTF
│ │ │ │ ├── SourceCodePro-Bold.ttf.woff2
│ │ │ │ └── SourceCodePro-Regular.ttf.woff2
│ │ └── source-code-pro.css
│ ├── github.css
│ ├── site.js
│ ├── split.css
│ ├── split.js
│ └── style.css
└── index.html
├── package.json
├── server
├── auth
│ └── adminAuth.js
├── config.js
├── configDB.js
├── models
│ ├── Course.js
│ └── User.js
├── routes
│ ├── course.js
│ └── user.js
└── server.js
└── storage.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /client/node_modules
6 |
7 | # testing
8 | /coverage
9 |
10 | # Server public for images
11 | # /server/public
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # old logos
28 | /client/src/old_logos
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # React Education App
4 |
5 | This is an education App built with React, Redux, Express.js and MongoDB. Additional technologies include google cloud API and the YouTube API.
6 |
7 |
8 | ### Installation
9 |
10 | - Please clone this repository using `git clone https://github.com/awesomedev08/React-Education-App.git` first. There will be two package.json files, one for the server side and the other for the client side.
11 | This is because server and client have to start at the same time. If you do not have NPM installed, please do so at `https://www.npmjs.com`. This is also a very good project for having a starter platform with react.js and redux.
12 |
13 | - In your directory after cloning please do the following:
14 |
15 | > Install dependencies for server in `package.json`
16 |
17 | ```shell
18 | $ cd react-education-app
19 | $ npm install
20 | ```
21 | > Install dependencies on client side
22 |
23 | ```shell
24 | $ cd react-education-app/client
25 | $ npm install
26 | ```
27 |
28 | ### Setup
29 |
30 | In order to use the web app you need to register for the Google cloud storage API. Please visit the website for registering.
31 | You will also need an gCloud API key which you can get here .
32 |
33 | - Please go to `storage.json` and put your gCloud data accordingly
34 | - In `server/config.js` please change the following data:
35 |
36 | ```javascript
37 | const bucketName = '' // Here comes your bucketName
38 | const Storage = require('@google-cloud/storage');
39 | const projectId = ''; // Here comes your project ID
40 | ```
41 | - You can you any database service for MongoDB. I recommend using Mlab . The go to `server/configDB.js` and change your data accordingly.
42 |
43 | ```javascript
44 | mongoose.connect(''); // Here put the database you want to connect to.
45 | ```
46 | - Finally, please go to `client/src/components/Course/CourseVideos` and change to your youTube key. You can get it here :
47 |
48 | ```javascript
49 | let youtubeKey = ''; // Put your youtube key here
50 | ```
51 |
52 | ## Features
53 |
54 | As an Admin you are able to:
55 |
56 | - Create Courses
57 | - Create Students
58 | - Enable/Disable Courses
59 | - Upload Files
60 | - Upload YouTube videos
61 |
62 | As a student you can:
63 |
64 | - Attend Courses
65 | - Review Courses
66 | - Download files
67 | - Watch Youtube videos that were previously uploaded to Youtube.
68 |
69 | This web app has been tested regirously and is fully responsive.
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/client/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "main.css": "static/css/main.cb2e88d7.css",
3 | "main.css.map": "static/css/main.cb2e88d7.css.map",
4 | "main.js": "static/js/main.9083edaf.js",
5 | "main.js.map": "static/js/main.9083edaf.js.map",
6 | "static/media/bath_logo_black.svg": "static/media/bath_logo_black.9c2a90f6.svg"
7 | }
--------------------------------------------------------------------------------
/client/build/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/client/build/favicon.ico
--------------------------------------------------------------------------------
/client/build/index.html:
--------------------------------------------------------------------------------
1 |
React App
--------------------------------------------------------------------------------
/client/build/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/build/service-worker.js:
--------------------------------------------------------------------------------
1 | "use strict";var precacheConfig=[["/index.html","f3fd0e6e93c2bc7cbcb0c34a7d377b38"],["/static/css/main.cb2e88d7.css","fd5791cf9c4d6ea1c9484ac3b9a912a1"],["/static/js/main.9083edaf.js","ca995608c23d944e5fa1a59b92311be9"],["/static/media/bath_logo_black.9c2a90f6.svg","9c2a90f66fe3409c752c058950ce5486"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var n=new URL(e);return"/"===n.pathname.slice(-1)&&(n.pathname+=t),n.toString()},cleanResponse=function(t){return t.redirected?("body"in t?Promise.resolve(t.body):t.blob()).then(function(e){return new Response(e,{headers:t.headers,status:t.status,statusText:t.statusText})}):Promise.resolve(t)},createCacheKey=function(e,t,n,r){var a=new URL(e);return r&&a.pathname.match(r)||(a.search+=(a.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(n)),a.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var n=new URL(t).pathname;return e.some(function(e){return n.match(e)})},stripIgnoredUrlParameters=function(e,n){var t=new URL(e);return t.hash="",t.search=t.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(t){return n.every(function(e){return!e.test(t[0])})}).map(function(e){return e.join("=")}).join("&"),t.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],n=e[1],r=new URL(t,self.location),a=createCacheKey(r,hashParamName,n,/\.\w{8}\./);return[r.toString(),a]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(r){return setOfCachedUrls(r).then(function(n){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(t){if(!n.has(t)){var e=new Request(t,{credentials:"same-origin"});return fetch(e).then(function(e){if(!e.ok)throw new Error("Request for "+t+" returned a response with status "+e.status);return cleanResponse(e).then(function(e){return r.put(t,e)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var n=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(t){return t.keys().then(function(e){return Promise.all(e.map(function(e){if(!n.has(e.url))return t.delete(e)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(t){if("GET"===t.request.method){var e,n=stripIgnoredUrlParameters(t.request.url,ignoreUrlParametersMatching),r="index.html";(e=urlsToCacheKeys.has(n))||(n=addDirectoryIndex(n,r),e=urlsToCacheKeys.has(n));var a="/index.html";!e&&"navigate"===t.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],t.request.url)&&(n=new URL(a,self.location).toString(),e=urlsToCacheKeys.has(n)),e&&t.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(n)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(e){return console.warn('Couldn\'t serve response for "%s" from cache: %O',t.request.url,e),fetch(t.request)}))}});
--------------------------------------------------------------------------------
/client/build/static/css/main.cb2e88d7.css:
--------------------------------------------------------------------------------
1 | body{margin:0;padding:0;font-family:sans-serif}.align-middle{text-align:center}.header{background-color:#002c56;border-radius:0!important}.header-title{color:#fff!important;display:inline-block}body,p{font-weight:400;font-family:Noto Sans,Trebuchet MS,sans-serif;margin-bottom:1.33778rem;letter-spacing:0;color:#202329;font-style:normal;text-rendering:optimizeLegibility;-webkit-font-feature-settings:"kern";-moz-font-feature-settings:"kern";-moz-font-feature-settings:"kern=1";-o-font-feature-settings:"kern";-webkit-font-kerning:normal;font-kerning:normal}.component-divider{border-bottom:3px solid #d27;margin-bottom:3rem}.component-divider-sub{border-bottom:2px solid #c8d7e6;margin-bottom:3rem}.sidebar{position:fixed;top:0;bottom:0;left:0;z-index:100;padding:48px 0 0;-webkit-box-shadow:inset -1px 0 0 rgba(0,0,0,.1);box-shadow:inset -1px 0 0 rgba(0,0,0,.1)}.sidebar-sticky{position:relative;top:0;height:calc(100vh - 48px);padding-top:.5rem;overflow-x:hidden;overflow-y:auto}@supports ((position:-webkit-sticky) or (position:sticky)){.sidebar-sticky{position:-webkit-sticky;position:sticky}}.sidebar .nav-link{font-weight:500;color:#333}.sidebar .nav-link.active{color:#007bff}.sidebar .nav-link.active .feather,.sidebar .nav-link:hover .feather{color:inherit}.sidebar-heading{font-size:.75rem;text-transform:uppercase}.nav-item a:link,.nav-item a:visited{color:#002b56!important}.navbar-nav span[role=button]{line-height:18px;padding:18px;display:inline-block}.navbar-nav a{color:#fff!important}.loggedIn{color:#fff;display:inline-block;font-size:.8em}.navbar-header{width:100%}.bath-logo{height:130%;padding-right:10px}@media screen and (max-width:767px){.header-title{font-size:1em}}.main-container{margin-left:auto !Important;float:none}.panelPointer{cursor:pointer}.margin-right{margin-right:5px}.error{color:red}.no-bullets{list-style-type:none;padding-left:0}.no-bullets li{padding:10px 0}.present-text{word-wrap:break-word}.margin-up{margin-top:20px}.margin-down{margin-bottom:20px}.no-padding{padding-left:0}.display-file-input{display:inline-block!important}.form-signin{width:100%;max-width:330px;padding:15px;margin:auto;text-align:center!important;margin-top:10%}.form-signin .form-control{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;height:auto;font-size:16px}
2 | /*# sourceMappingURL=main.cb2e88d7.css.map*/
--------------------------------------------------------------------------------
/client/build/static/css/main.cb2e88d7.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["index.css","components/App/App.css","components/Sidebar/Sidebar.css","components/Header/Header.css","components/Container/Container.css","components/Home/Home.css","components/CreateCourse/CreateCourse.css","components/Course/Course.css","components/Login/Login.css"],"names":[],"mappings":"AAAA,KACE,SACA,UACA,sBAAwB,CAE1B,cACE,iBAAmB,CCNrB,QACE,yBACA,yBAA8B,CAGhC,cACE,qBACA,oBAAsB,CAIxB,OACI,gBACA,8CACA,yBACA,iBACA,cACA,kBACA,kCACA,qCACA,kCACA,oCACA,gCACA,4BACQ,mBAAqB,CAGjC,mBACE,6BACA,kBAAoB,CAItB,uBAEE,gCACA,kBAAoB,CCjCtB,SACE,eACA,MACA,SACA,OACA,YACA,iBACA,iDACQ,wCAA6C,CAGvD,gBACE,kBACA,MACA,0BACA,kBACA,kBACA,eAAiB,CAGnB,2DACE,gBACE,wBACA,eAAiB,CAClB,CAGH,mBACE,gBACA,UAAY,CAId,0BACE,aAAe,CAGjB,qEAEE,aAAe,CAGjB,iBACE,iBACA,wBAA0B,CAG5B,qCACE,uBAA0B,CAI5B,8BACE,iBACA,aACA,oBAAsB,CAGxB,cACE,oBAAwB,CC9D1B,UACE,WACA,qBACA,cAAiB,CAInB,eACE,UAAW,CAGb,WACE,YACA,kBAAoB,CAKtB,oCACI,cACI,aAAe,CAClB,CCrBL,gBACE,4BACA,UAAY,CCFd,cACE,cAAgB,CCDlB,cACE,gBAAkB,CAGpB,OACE,SAAW,CCLb,YACE,qBACA,cAAkB,CAGpB,eACE,cAAuB,CAGzB,cACE,oBAAsB,CAGxB,WACE,eAAiB,CAGnB,aACE,kBAAoB,CAGtB,YACE,cAAkB,CAGpB,oBACE,8BAAiC,CCxBnC,aACE,WACA,gBACA,aACA,YACA,4BACA,cAAgB,CAElB,2BACE,kBACA,8BACQ,sBACR,YACA,cAAgB","file":"static/css/main.cb2e88d7.css","sourcesContent":["body {\n margin: 0;\n padding: 0;\n font-family: sans-serif;\n}\n.align-middle{\n text-align: center;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/index.css",".header {\n background-color: #002c56;\n border-radius: 0px !important;\n}\n\n.header-title {\n color: white !important;\n display: inline-block;\n}\n\n/*Ref: University of Bath Website: https://goo.gl/bGbbxb*/\nbody, p {\n font-weight: 400;\n font-family: \"Noto Sans\",\"Trebuchet MS\",sans-serif;\n margin-bottom: 1.33778rem;\n letter-spacing: 0;\n color: #202329;\n font-style: normal;\n text-rendering: optimizeLegibility;\n -webkit-font-feature-settings: 'kern';\n -moz-font-feature-settings: 'kern';\n -moz-font-feature-settings: 'kern=1';\n -o-font-feature-settings: 'kern';\n -webkit-font-kerning: normal;\n font-kerning: normal;\n}\n\n.component-divider{\n border-bottom: 3px solid #d27;\n margin-bottom: 3rem;\n}\n\n\n.component-divider-sub{\n\n border-bottom: 2px solid #c8d7e6;\n margin-bottom: 3rem;\n\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/App/App.css","\n/*Ref: Based on the example https://goo.gl/5sHujd */\n\n.sidebar {\n position: fixed;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 100;\n padding: 48px 0 0;\n -webkit-box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);\n box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);\n}\n\n.sidebar-sticky {\n position: relative;\n top: 0;\n height: calc(100vh - 48px);\n padding-top: .5rem;\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n@supports ((position: -webkit-sticky) or (position: sticky)) {\n .sidebar-sticky {\n position: -webkit-sticky;\n position: sticky;\n }\n}\n\n.sidebar .nav-link {\n font-weight: 500;\n color: #333;\n}\n\n\n.sidebar .nav-link.active {\n color: #007bff;\n}\n\n.sidebar .nav-link:hover .feather,\n.sidebar .nav-link.active .feather {\n color: inherit;\n}\n\n.sidebar-heading {\n font-size: .75rem;\n text-transform: uppercase;\n}\n\n.nav-item a:link, .nav-item a:visited {\n color: #002b56 !important;\n}\n\n/* Ref as example from https://goo.gl/MGtAfh */\n.navbar-nav span[role=button] {\n line-height: 18px;\n padding: 18px;\n display: inline-block;\n}\n\n.navbar-nav a{\n color: white !important;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/Sidebar/Sidebar.css",".loggedIn{\n color: white;\n display: inline-block;\n font-size: 0.8em;\n\n}\n\n.navbar-header{\n width:100%;\n}\n\n.bath-logo{\n height:130%;\n padding-right: 10px;\n}\n\n\n/* sm screen */\n@media screen and (max-width: 767px) {\n .header-title {\n font-size: 1em;\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/Header/Header.css",".main-container{\n margin-left: auto !Important;\n float: none; \n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/Container/Container.css",".panelPointer{\n cursor: pointer;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/Home/Home.css",".margin-right{\n margin-right: 5px;\n}\n\n.error{\n color: red; \n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/CreateCourse/CreateCourse.css",".no-bullets{\n list-style-type: none;\n padding-left: 0px;\n}\n\n.no-bullets li{\n padding: 10px 0 10px 0;\n}\n\n.present-text {\n word-wrap: break-word;\n}\n\n.margin-up{\n margin-top: 20px;\n}\n\n.margin-down{\n margin-bottom: 20px;\n}\n\n.no-padding{\n padding-left: 0px;\n}\n\n.display-file-input{\n display: inline-block !important;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/Course/Course.css","/* Ref: Based on bootstrap example https://goo.gl/HrQtz4 */\n\n.form-signin {\n width: 100%;\n max-width: 330px;\n padding: 15px;\n margin: auto;\n text-align: center !important;\n margin-top: 10%;\n}\n.form-signin .form-control {\n position: relative;\n -webkit-box-sizing: border-box;\n box-sizing: border-box;\n height: auto;\n font-size: 16px;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./src/components/Login/Login.css"],"sourceRoot":""}
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "babel-cli": "^6.26.0",
8 | "babel-preset-es2015": "^6.24.1",
9 | "babel-preset-stage-0": "^6.24.1",
10 | "bootstrap": "^4.1.1",
11 | "foreman": "^3.0.0",
12 | "jwt-decode": "^2.2.0",
13 | "marked": "^0.4.0",
14 | "react": "^16.4.2",
15 | "react-bootstrap": "0.32.1",
16 | "react-dom": "^16.4.2",
17 | "react-redux": "^5.0.7",
18 | "react-router-dom": "^4.3.1",
19 | "react-scripts": "1.1.4",
20 | "redux": "^4.0.0",
21 | "redux-thunk": "^2.3.0"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test --env=jsdom",
27 | "eject": "react-scripts eject"
28 | },
29 | "proxy": "http://localhost:3001",
30 | "devDependencies": {
31 | "enzyme": "^3.4.4",
32 | "enzyme-adapter-react-16": "^1.2.0",
33 | "fetch-mock": "^6.5.2",
34 | "jest": "^23.5.0",
35 | "react-addons-test-utils": "^15.6.2",
36 | "redux-mock-store": "^1.5.3"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
23 | React App
24 |
25 |
26 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/client/src/actions/courseActions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * courseActions module
3 | * Ref: Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module courseActions
5 | */
6 |
7 | /* Import all reducers*/
8 | import {
9 | ADD_COURSE,
10 | GET_COURSES,
11 | GET_COURSE,
12 | UPDATE_COURSE,
13 | DELETE_COURSE,
14 | CLEAN_COURSE,
15 | ERRORS
16 | } from './typesActions';
17 | import axios from 'axios';
18 |
19 |
20 | /** Action function to add course */
21 | export const addCourse = courseData => dispatch => {
22 | axios.post('/api/course/', courseData)
23 | .then(res =>
24 | dispatch({
25 | type: ADD_COURSE,
26 | payload: res.data
27 | })
28 | )
29 | .catch(err =>
30 | dispatch({
31 | type: ERRORS,
32 | payload: err.response.data
33 | })
34 | );
35 | };
36 |
37 |
38 | /** Action function to get courses */
39 | export const getCourses = () => dispatch => {
40 | axios.get('/api/course')
41 | .then(response =>
42 | dispatch({
43 | type: GET_COURSES,
44 | payload: response.data
45 | }))
46 | .catch(err =>
47 | dispatch({
48 | type: GET_COURSES,
49 | payload: null
50 | })
51 | );
52 | };
53 |
54 |
55 | /** Action function to delete course */
56 | export const deleteCourse = courseId => dispatch => {
57 | axios.delete(`/api/course/${courseId}`)
58 | .then(res =>{
59 | dispatch({
60 | type: DELETE_COURSE,
61 | payload: res.data
62 | })}
63 | )
64 | .catch(err =>
65 | dispatch({
66 | type: DELETE_COURSE,
67 | payload: null
68 | })
69 | );
70 | };
71 |
72 |
73 |
74 | /** Action function to get course */
75 | export const getCourse = courseId => dispatch => {
76 | dispatch({
77 | type: CLEAN_COURSE
78 | })
79 | axios.get(`/api/course/${courseId}`)
80 | .then(res =>
81 | dispatch({
82 | type: GET_COURSE,
83 | payload: res.data
84 | })
85 | )
86 | .catch(err =>
87 | dispatch({
88 | type: GET_COURSE,
89 | payload: null
90 | })
91 | );
92 | };
93 |
94 |
95 | /** Action function to update course */
96 | export const updateCourse = courseData => dispatch => {
97 | axios.put(`/api/course/${courseData._id}`, courseData)
98 | .then(res =>
99 | dispatch({
100 | type: UPDATE_COURSE,
101 | payload: res.data
102 | })
103 | )
104 | .catch(err =>
105 | dispatch({
106 | type: UPDATE_COURSE,
107 | payload: null
108 | })
109 | );
110 | };
111 |
112 |
113 | /** Action function to download file */
114 | export const downloadFile = (path, fileName, fileId, courseId) => dispatch => {
115 | // Ref:
116 | axios({
117 | method: 'GET',
118 | url:`/api/course/file/${path}/${courseId}/${fileId}`,
119 | responseType: 'blob'})
120 | .then(res => {
121 |
122 | /* Ref: https://goo.gl/SGdqBm */
123 | let aTag = document.createElement('a');
124 | let url = window.URL.createObjectURL(new Blob([res.data]));
125 | aTag.href = url;
126 | aTag.setAttribute('download', fileName);
127 | document.body.appendChild(aTag);
128 | aTag.click();
129 | })
130 | .catch(err => console.log(err))
131 | };
132 |
133 |
134 | /** Action function to upload file */
135 | export const uploadFile = file => dispatch => {
136 | axios.post(`/api/course/file`,file)
137 | .then(res =>
138 | dispatch({
139 | type: GET_COURSE,
140 | payload: res.data
141 | })
142 | )
143 | .catch(err =>
144 | dispatch({
145 | type: GET_COURSE,
146 | payload: null
147 | })
148 | );
149 | };
150 |
151 |
152 | /** Action function to delete file */
153 | export const deleteFile = (courseId,fileId) => dispatch => {
154 | axios.delete(`/api/course/file/${courseId}/${fileId}`)
155 | .then(res =>
156 | dispatch({
157 | type: GET_COURSE,
158 | payload: res.data
159 | })
160 | )
161 | .catch(err =>
162 | dispatch({
163 | type: GET_COURSE,
164 | payload: null
165 | })
166 | );
167 | };
168 |
169 |
170 | /** Action function to upload video */
171 | export const uploadVideo = (url, courseId, title, youtubeId, thumbnail) => dispatch => {
172 | axios.post('/api/course/video', {url, courseId, title, youtubeId, thumbnail})
173 | .then(res =>
174 | dispatch({
175 | type: GET_COURSE,
176 | payload: res.data
177 | })
178 | ).catch(err =>
179 | dispatch({
180 | type: GET_COURSE,
181 | payload: null
182 | })
183 | );
184 | };
185 |
186 | /** Action function to watch video */
187 | export const watchVideo = (courseId,videoId, youtubeId) => dispatch => {
188 | axios.get(`/api/course/video/${courseId}/${videoId}`)
189 | .then(res => {
190 | window.location.href = `https://www.youtube.com/watch?v=${youtubeId}`
191 | dispatch({
192 | type: GET_COURSE,
193 | payload: res.data
194 | })
195 | }).catch(err =>
196 | dispatch({
197 | type: GET_COURSE,
198 | payload: null
199 | })
200 | );
201 | };
202 |
203 | /** Action function to delete video */
204 | export const deleteVideo = (courseId,videoId) => dispatch => {
205 | axios.delete(`/api/course/video/${courseId}/${videoId}`)
206 | .then(res =>
207 | dispatch({
208 | type: GET_COURSE,
209 | payload: res.data
210 | })
211 | ).catch(err =>
212 | dispatch({
213 | type: GET_COURSE,
214 | payload: null
215 | })
216 | );
217 | };
218 |
219 |
220 | /** Action function to enroll in course */
221 | export const enrollCourse = userData => dispatch => {
222 | axios.post('/api/course/enrollCourse', userData)
223 | .then(res =>
224 | dispatch({
225 | type: GET_COURSE,
226 | payload: res.data
227 | }))
228 | .catch(err =>
229 | dispatch({
230 | type: ERRORS,
231 | payload: err.response.data
232 | })
233 | );
234 | };
235 |
236 | /** Action function to clean course */
237 | export const cleanCourse = () => dispatch => {
238 | dispatch({type: CLEAN_COURSE})
239 | };
240 |
241 |
242 | /** Action function to leave course */
243 | export const leaveCourse = (courseId,enrollId) => dispatch => {
244 | axios.delete(`/api/course/leaveCourse/${courseId}/${enrollId}`)
245 | .then(res =>
246 | dispatch({
247 | type: GET_COURSE,
248 | payload: res.data
249 | }))
250 | .catch(err =>
251 | dispatch({
252 | type: ERRORS,
253 | payload: err.response.data
254 | })
255 | );
256 | };
257 |
--------------------------------------------------------------------------------
/client/src/actions/errorActions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * errorActions module
3 | * @module errorActions
4 | */
5 |
6 | /* Import all error reducers */
7 | import {
8 | REMOVE_ERRORS
9 | } from './typesActions';
10 |
11 | /** Action function to remove Error that is in state */
12 | export const removeError = () => dispatch => {
13 | dispatch({
14 | type: REMOVE_ERRORS
15 | })
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/actions/typesActions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * typesActions module - Redux Examples taken from https://bit.ly/2BIGB2T
3 | * @module typesActions
4 | */
5 |
6 | export const GET_COURSES = 'GET_COURSES';
7 | export const ADD_COURSE = 'ADD_COURSE';
8 | export const GET_COURSE = 'GET_COURSE';
9 | export const DELETE_COURSE = 'DELETE_COURSE';
10 | export const UPDATE_COURSE = 'UPDATE_COURSE';
11 | export const ADD_USER = 'ADD_USER';
12 | export const GET_USERS = 'GET_USERS';
13 | export const DELETE_USER = 'DELETE_USER';
14 | export const LOGIN_USER = 'LOGIN_USER';
15 | export const LOGOUT_USER = 'LOGOUT_USER';
16 | export const CLEAN_COURSE = 'CLEAN_COURSE';
17 | export const ERRORS = 'ERRORS';
18 | export const REMOVE_ERRORS = 'REMOVE_ERRORS';
19 |
--------------------------------------------------------------------------------
/client/src/actions/userActions.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * userActions module - Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module userActions
5 | */
6 |
7 |
8 | import {
9 | ADD_USER,
10 | GET_USERS,
11 | DELETE_USER,
12 | LOGIN_USER,
13 | LOGOUT_USER,
14 | ERRORS
15 | } from './typesActions';
16 |
17 | import axios from 'axios';
18 | import {setAuthToken} from './../helpers'
19 | import jwtDecode from 'jwt-decode';
20 |
21 |
22 |
23 | /** Action function to add user */
24 | export const addUser = userData => dispatch => {
25 | axios.post('/api/user/', userData)
26 | .then(res =>
27 | dispatch({
28 | type: ADD_USER,
29 | payload: res.data
30 | })
31 | )
32 | .catch(err =>
33 | dispatch({
34 | type: ERRORS,
35 | payload: err.response.data
36 | })
37 | );
38 | };
39 |
40 |
41 | /** Action function to get all users */
42 | export const getUsers = () => dispatch => {
43 | axios.get('/api/user/')
44 | .then(res =>
45 | dispatch({
46 | type: GET_USERS,
47 | payload: res.data
48 | })
49 | )
50 | .catch(err =>
51 | dispatch({
52 | type: ERRORS,
53 | payload: null
54 | })
55 | );
56 | };
57 |
58 | /** Action function to set user in state */
59 | export const setUser = userData => {
60 | return {
61 | type: LOGIN_USER,
62 | payload: userData
63 | }
64 | }
65 |
66 | /** Action function to delete user */
67 | export const deleteUser = userId => dispatch => {
68 | axios.delete(`/api/user/${userId}`)
69 | .then(res =>
70 | dispatch({
71 | type: DELETE_USER,
72 | payload: res.data
73 | })
74 | )
75 | .catch(err =>
76 | dispatch({
77 | type: ERRORS,
78 | payload: null
79 | })
80 | );
81 | };
82 |
83 |
84 | /** Action function to login the user */
85 | export const loginUser = userData => dispatch => {
86 | axios.post('/api/user/login', userData)
87 | .then(res =>{
88 |
89 | // Ref: Seen example from https://goo.gl/HCaXX2
90 | let token = res.data.token
91 | localStorage.setItem('jwtToken', token)
92 | setAuthToken(token)
93 | let dUser = jwtDecode(token)
94 | dispatch(setUser(dUser))
95 | })
96 | .catch(err =>
97 | dispatch({
98 | type: ERRORS,
99 | payload: err.response.data
100 | })
101 | );
102 | };
103 |
104 |
105 |
106 | /** Action function to log out the user */
107 | export const logoutUser = () => dispatch => {
108 |
109 | setAuthToken(false)
110 | localStorage.removeItem('jwtToken');
111 | dispatch({
112 | type: LOGOUT_USER
113 | })
114 | };
115 |
--------------------------------------------------------------------------------
/client/src/components/App/App.css:
--------------------------------------------------------------------------------
1 | .header {
2 | background-color: #002c56;
3 | border-radius: 0px !important;
4 | }
5 |
6 | .header-title {
7 | color: white !important;
8 | display: inline-block;
9 | }
10 |
11 | body, p {
12 | font-weight: 400;
13 | font-family: "Noto Sans","Trebuchet MS",sans-serif;
14 | margin-bottom: 1.33778rem;
15 | letter-spacing: 0;
16 | color: #202329;
17 | font-style: normal;
18 | text-rendering: optimizeLegibility;
19 | -webkit-font-feature-settings: 'kern';
20 | -moz-font-feature-settings: 'kern';
21 | -moz-font-feature-settings: 'kern=1';
22 | -o-font-feature-settings: 'kern';
23 | font-kerning: normal;
24 | }
25 |
26 | .component-divider{
27 | border-bottom: 3px solid #d27;
28 | margin-bottom: 3rem;
29 | }
30 |
31 |
32 | .component-divider-sub{
33 |
34 | border-bottom: 2px solid #c8d7e6;
35 | margin-bottom: 3rem;
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import './App.css';
3 |
4 | import Main from '../Main/Main';
5 | import Login from '../Login/Login';
6 | import {BrowserRouter, Route, Switch} from "react-router-dom";
7 |
8 |
9 | /**
10 | * App React Class
11 | * @class App
12 | */
13 | class App extends Component {
14 |
15 | render() {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 | }
28 | }
29 |
30 | export default App;
31 |
--------------------------------------------------------------------------------
/client/src/components/Auth/Auth.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 |
4 |
5 | /**
6 | * Auth React Class - Example seen from https://goo.gl/oKVi33
7 | * @class Auth
8 | */
9 | export default function(InComponent,role = null){
10 | class Auth extends React.Component {
11 |
12 | componentWillMount(){
13 | let auth = this.props.auth
14 |
15 |
16 | /*Check if user is student or admin - admin is 1 - student is 0*/
17 | if (Object.keys(auth).length === 0) {
18 | alert('You must be logged In to see this Page')
19 | this.props.history.push('/login')
20 | return
21 | }
22 |
23 | if (role) {
24 | if (auth.role !== 1) {
25 | alert('You must be an administrator to see this page')
26 | this.props.history.push('/')
27 | }
28 | }
29 | }
30 |
31 | render() {
32 | return ()
33 | }
34 | }
35 |
36 | const reduxProps = state => {
37 | return ({
38 | auth: state.user.authUser
39 | })
40 | };
41 |
42 | return connect(reduxProps)(Auth);
43 | }
44 |
--------------------------------------------------------------------------------
/client/src/components/Container/Container.css:
--------------------------------------------------------------------------------
1 | .main-container{
2 | margin-left: auto !Important;
3 | float: none;
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/components/Container/Container.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Route,Switch, } from 'react-router-dom'
3 | import './Container.css';
4 |
5 |
6 | import Home from '../Home/Home';
7 | import Courses from '../Courses/Courses';
8 | import MyCourses from '../MyCourses/MyCourses';
9 | import CreateCourse from '../CreateCourse/CreateCourse';
10 | import EditCourse from '../EditCourse/EditCourse';
11 | import CreateUser from '../CreateUser/CreateUser';
12 | import Course from '../Course/Course';
13 | import Users from '../Users/Users';
14 | import Login from '../Login/Login';
15 | import NoPage from '../NoPage/NoPage';
16 | import Auth from '../Auth/Auth'
17 |
18 | /**
19 | * Container React Class - This is where router is implemented to switch between pages
20 | * @class Container
21 | */
22 | class Container extends Component {
23 | render() {
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | )
40 | }
41 | }
42 |
43 | export default Container;
44 |
--------------------------------------------------------------------------------
/client/src/components/Course/Course.css:
--------------------------------------------------------------------------------
1 | .no-bullets{
2 | list-style-type: none;
3 | padding-left: 0px;
4 | }
5 |
6 | .no-bullets li{
7 | padding: 10px 0 10px 0;
8 | }
9 |
10 | .present-text {
11 | word-wrap: break-word;
12 | }
13 |
14 | .margin-up{
15 | margin-top: 20px;
16 | }
17 |
18 | .margin-down{
19 | margin-bottom: 20px;
20 | }
21 |
22 | .no-padding{
23 | padding-left: 0px;
24 | }
25 |
26 | .display-file-input{
27 | display: inline-block !important;
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/components/Course/Course.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | Grid,
4 | Row,
5 | Col,
6 | Button} from 'react-bootstrap';
7 | import { Link } from 'react-router-dom'
8 | import './Course.css';
9 |
10 | import CourseFiles from './CourseFiles/CourseFiles'
11 | import CourseVideos from './CourseVideos/CourseVideos'
12 | import CourseUsers from './CourseUsers/CourseUsers'
13 |
14 |
15 | import { getCourse, deleteCourse, enrollCourse, leaveCourse, cleanCourse } from '../../actions/courseActions';
16 | import { connect } from 'react-redux';
17 |
18 |
19 | /**
20 | * Course React Class
21 | * @class Course
22 | */
23 | class Course extends Component {
24 |
25 |
26 | constructor(props) {
27 | super(props);
28 |
29 | this.leave = this.leave.bind(this);
30 | this.enroll = this.enroll.bind(this);
31 | }
32 |
33 |
34 | componentWillReceiveProps(newProps) {
35 | let course = newProps.course
36 | if (course == null) {
37 | this.props.history.push('/NoPage')
38 | }
39 | }
40 |
41 | /** leave function inside component */
42 | leave(enrollId){
43 | this.props.leaveCourse(this.props.match.params.courseId,enrollId)
44 | alert('Course left')
45 | }
46 |
47 | /** enroll function inside component with obj with user info */
48 | enroll(){
49 |
50 | let enrollObj = {
51 | courseId: this.props.match.params.courseId,
52 | userId: this.props.auth._id
53 | }
54 |
55 | this.props.enrollCourse(enrollObj)
56 | alert('Course Entered')
57 |
58 | }
59 |
60 | /** delete function inside component */
61 | deleteCourse(courseId,e){
62 | this.props.deleteCourse(courseId)
63 | alert('Course Deleted')
64 | this.props.history.push('/all-courses')
65 | }
66 |
67 |
68 | componentWillMount(){
69 | let courseId = this.props.match.params.courseId
70 | this.props.getCourse(courseId)
71 | }
72 |
73 | /* When course component is left clean cours in state */
74 | componentWillUnmount(){
75 | this.props.cleanCourse()
76 | }
77 |
78 |
79 | render() {
80 |
81 | let courseData = this.props.course
82 | let role = this.props.auth.role
83 | let enrollId = null;
84 |
85 | /*Check if tstudent is enrolled in course*/
86 | if (Object.keys(courseData).length > 0 && courseData.members.length > 0 && role === 0) {
87 | enrollId = courseData.members.find(el => el.userId === this.props.auth._id)
88 | if (enrollId) {
89 | enrollId = enrollId._id
90 | }
91 | }
92 |
93 |
94 |
95 | return (
96 |
97 |
98 | Course Overview - {courseData.name}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | -
107 | Name: {courseData.name}
108 |
109 | -
110 | From: {courseData.dateFrom}
111 |
112 | -
113 | To: {courseData.dateTo}
114 |
115 | -
116 | Status: {courseData.courseStatus}
117 |
118 |
119 |
120 |
121 |
122 | -
123 | Description:
{courseData.description}
124 |
125 |
126 |
127 |
128 |
129 | {/*All 3 subcomponents*/}
130 | { Object.keys(courseData).length > 0 && }
131 |
132 | { Object.keys(courseData).length > 0 && }
133 |
134 | { Object.keys(courseData).length > 0 && }
135 |
136 |
137 |
138 |
139 |
140 |
141 | {/*Show different buttons depending on being student or admin*/}
142 | { role === 1 &&
143 |
144 |
145 |
146 |
147 | }
148 |
149 | { role === 0 &&
150 |
151 | {!enrollId && }
152 | {enrollId && }
153 |
154 | }
155 |
156 |
157 |
158 |
159 |
160 | )
161 | }
162 |
163 | }
164 |
165 |
166 | const reduxProps = state => {
167 | return ({
168 | course: state.course.course,
169 | auth: state.user.authUser
170 | })
171 | };
172 |
173 |
174 | export default connect(reduxProps, { deleteCourse, getCourse, enrollCourse, leaveCourse,cleanCourse })(Course);
175 |
--------------------------------------------------------------------------------
/client/src/components/Course/CourseFiles/CourseFiles.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | Table,
4 | Row,
5 | Col,
6 | Button,
7 | FormGroup,
8 | FormControl,
9 | ControlLabel,
10 | Form} from 'react-bootstrap';
11 | import {determineName} from '../../../helpers'
12 |
13 | import { uploadFile, deleteFile, downloadFile } from '../../../actions/courseActions';
14 | import { connect } from 'react-redux';
15 |
16 | /**
17 | * CourseFiles React Class
18 | * @class CourseFiles
19 | */
20 | class CourseFiles extends Component {
21 |
22 | constructor(props) {
23 | super(props);
24 |
25 | this.state = {
26 | file: '',
27 | courseId: this.props.courseId,
28 | isDisabled: true,
29 | isDisabledDelete: false
30 | }
31 |
32 |
33 | this.setFile = this.setFile.bind(this);
34 | this.uploadFile = this.uploadFile.bind(this);
35 | this.downloadFile = this.downloadFile.bind(this);
36 | this.deleteFile = this.deleteFile.bind(this);
37 |
38 | }
39 |
40 | componentWillReceiveProps(newProps) {
41 | if (newProps) {
42 | this.setState({
43 | isDisabled: true,
44 | isDisabledDelete: false,
45 | file: ''
46 | })
47 | }
48 | }
49 |
50 | /** Download file inside component */
51 | downloadFile(path, fileName, fileId){
52 | this.props.downloadFile(path, fileName, fileId,this.props.courseId)
53 | }
54 |
55 |
56 | /** Delete file */
57 | deleteFile(materialId){
58 | this.setState({isDisabledDelete: true})
59 | this.props.deleteFile(this.props.courseId,materialId)
60 | }
61 |
62 |
63 | /** Setfile from JavaScript and HTML5 */
64 | setFile(e){
65 | let fileObj = e.target.files[0]
66 | this.setState({ file: fileObj, isDisabled: false})
67 | }
68 |
69 |
70 | /** Upload file inside component */
71 | uploadFile(e){
72 | e.preventDefault()
73 | this.setState({isDisabled: true})
74 |
75 |
76 |
77 | let fileObj = this.state.file
78 | let form = new FormData();
79 | let fileName = fileObj.name
80 |
81 | form.append('file', fileObj);
82 | form.append('courseId', this.state.courseId);
83 |
84 | /* Check if file with same name already uploaded */
85 | let findName = this.props.filePaths.findIndex(data => data.fileName === fileName)
86 | if (findName > -1) {
87 | alert('File with same name already uploaded')
88 | this.setState({isDisabled: false})
89 | return false;
90 | }
91 |
92 |
93 | this.props.uploadFile(form)
94 |
95 | }
96 |
97 |
98 | /** Generate files for rendering table */
99 | generateFiles(fileData){
100 | return fileData.map((val,index) => {
101 | return (
102 |
103 | {val.fileName} |
104 | {determineName(val.fileName)} Document |
105 | {val.clicks.length} |
106 |
107 |
108 | |
109 |
110 | { this.props.role === 1 &&
111 |
112 | | }
113 |
);
114 | })
115 | }
116 |
117 |
118 | render() {
119 |
120 | let files = this.generateFiles(this.props.filePaths)
121 | let role = this.props.role
122 |
123 | return (
124 |
125 |
126 |
127 | Course Files
128 |
129 | {role === 1 && }
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | Filename |
152 | Type |
153 | Clicks |
154 | Download |
155 | {role === 1 && Delete | }
156 |
157 |
158 |
159 | {files}
160 |
161 |
162 |
163 |
164 |
165 | )
166 | }
167 | }
168 |
169 |
170 | export default connect(null, { uploadFile, deleteFile, downloadFile })(CourseFiles);
171 |
--------------------------------------------------------------------------------
/client/src/components/Course/CourseUsers/CourseUsers.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | Table,
4 | Row,
5 | Col
6 | } from 'react-bootstrap';
7 | import axios from 'axios'
8 | import { connect } from 'react-redux';
9 |
10 |
11 |
12 | /**
13 | * CourseUsers React Class
14 | * @class CourseUsers
15 | */
16 | class CourseUsers extends Component {
17 |
18 | constructor(props) {
19 | super(props);
20 |
21 | this.state = {
22 | finalMembers: [],
23 | }
24 |
25 | }
26 |
27 |
28 |
29 | componentWillReceiveProps(nextProps){
30 | if (nextProps.members.length !== this.props.members.length) {
31 | this.callApiMembers(nextProps.members)
32 | }
33 | }
34 |
35 |
36 |
37 | componentWillMount(){
38 | this.callApiMembers(this.props.members)
39 | }
40 |
41 | /** connect user ids with database call */
42 | callApiMembers(members){
43 | let memberCalls = members.map(el => axios.get(`/api/user/${el.userId}`))
44 |
45 | Promise.all(memberCalls).then(members => {
46 | let memberData = members.map(el => el.data)
47 | this.setState({finalMembers: this.generateMembers(memberData)})
48 | })
49 | }
50 |
51 |
52 | /** Generate members to display in tabular form */
53 | generateMembers(members){
54 | return members.map((val,index) => {
55 | return (
56 |
57 | {val.firstName} |
58 | {val.lastName} |
59 | {val.email} |
60 | enrolled |
61 |
);
62 | })
63 | }
64 |
65 | render() {
66 |
67 | return(
68 |
69 |
70 |
71 | Course Users
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | First name |
82 | Last name |
83 | Email |
84 | Status |
85 |
86 |
87 |
88 | {this.state.finalMembers}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
100 | }
101 |
102 |
103 | export default connect(null)(CourseUsers);
104 |
--------------------------------------------------------------------------------
/client/src/components/Course/CourseVideos/CourseVideos.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | Table,
4 | Row,
5 | Col,
6 | Button,
7 | FormGroup,
8 | FormControl,
9 | ControlLabel,
10 | Image,
11 | Form} from 'react-bootstrap';
12 | import axios from 'axios'
13 |
14 | import { uploadVideo, deleteVideo,watchVideo } from '../../../actions/courseActions';
15 | import { connect } from 'react-redux';
16 |
17 | import {setAuthToken} from './../../../helpers'
18 |
19 | /**
20 | * CourseVideos React Class
21 | * @class CourseVideos
22 | */
23 | class CourseVideos extends Component {
24 |
25 | constructor(props) {
26 | super(props);
27 |
28 | this.state = {
29 | url: '',
30 | courseId: this.props.courseId,
31 | isDisabled: true,
32 | isDisabledDelete: false
33 | }
34 | this.handleInput = this.handleInput.bind(this);
35 | this.submitForm = this.submitForm.bind(this);
36 |
37 | }
38 |
39 |
40 | componentWillReceiveProps(newProps) {
41 | if (newProps) {
42 | this.setState({
43 | isDisabled: false,
44 | isDisabledDelete: false,
45 | url: ''
46 | })
47 | }
48 | }
49 |
50 |
51 | /*Check disabled state for button */
52 | handleInput(e){
53 | let url = e.target.value
54 | this.setState({
55 | url,
56 | isDisabled: url === '' ? true : false
57 | });
58 | }
59 |
60 | /** Check if video url is a youtube video url with regex - Regex taken from https://goo.gl/unbFN5 */
61 | checkUrlVideo(url){
62 |
63 | let urlRegex = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
64 | let result = url.match(urlRegex)
65 | if (result) {
66 | return result;
67 | }
68 | return false;
69 | }
70 |
71 |
72 | submitForm(e) {
73 | e.preventDefault();
74 |
75 | /*Get rid of whitespaces*/
76 | let url = this.state.url.replace(/\s+/g, '');
77 | let courseId = this.state.courseId
78 |
79 | /*Check valid youtube string*/
80 | let result = this.checkUrlVideo(url);
81 | if (result === false) {
82 | alert('Your string is not a valid youtube string')
83 | return;
84 | }
85 |
86 | /*Youtube id and key*/
87 | let youtubeId = result[1];
88 | let youtubeKey = '' // Put your youtube key here
89 |
90 |
91 | let checkRepeat = this.props.videos.findIndex(data => {
92 | return data.youtubeId === youtubeId
93 | })
94 |
95 |
96 | if (checkRepeat > -1) {
97 | alert('The same youtube title is used already')
98 | return;
99 | }
100 |
101 | this.setState({isDisabled: true})
102 | setAuthToken(false)
103 |
104 |
105 | /*Call youtube API and get response */
106 | axios.get(`https://www.googleapis.com/youtube/v3/videos?id=${youtubeId}&key=${youtubeKey}&part=snippet`)
107 | .then(response => {
108 |
109 | /*Set token */
110 | let token = localStorage.getItem('jwtToken')
111 | setAuthToken(token)
112 |
113 | let title = response.data.items[0].snippet.title
114 | let thumbnail = response.data.items[0].snippet.thumbnails.default.url
115 | this.props.uploadVideo(url, courseId, title, youtubeId, thumbnail)
116 |
117 |
118 |
119 | }).catch(err => console.log(err))
120 | }
121 |
122 | /** watch video inside component*/
123 | watchVideo(videoObj){
124 | this.props.watchVideo(this.state.courseId, videoObj._id, videoObj.youtubeId)
125 | }
126 |
127 | /** delete video inside component*/
128 | deleteVideo(videoId){
129 | this.props.deleteVideo(this.state.courseId, videoId)
130 | }
131 |
132 | /** generate videos to display in tabular form*/
133 | generateVideos(videos){
134 | return videos.map((val,index) => {
135 | return (
136 |
137 |
138 |
139 | |
140 | {val.title} |
141 | {val.clicks.length} |
142 |
143 |
144 | |
145 |
146 | {this.props.role === 1 &&
147 |
148 | | }
149 |
);
150 | })
151 | }
152 |
153 | render() {
154 |
155 | let videos = this.generateVideos(this.props.videos)
156 | let role = this.props.role
157 |
158 |
159 | return(
160 |
161 |
162 |
163 | Course Videos
164 | {role === 1 && }
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 | Videothumb |
188 | Videoname |
189 | Clicks |
190 | Watch |
191 | {role === 1 && Delete | }
192 |
193 |
194 |
195 | {videos}
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 | )
205 | }
206 |
207 | }
208 |
209 |
210 | export default connect(null, { uploadVideo, deleteVideo,watchVideo })(CourseVideos);
211 |
--------------------------------------------------------------------------------
/client/src/components/Courses/Courses.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Table, Grid, Row, Col, Button } from 'react-bootstrap';
3 | import { Link } from 'react-router-dom'
4 | import { getCourses } from '../../actions/courseActions';
5 | import { connect } from 'react-redux';
6 |
7 | /**
8 | * Courses React Class
9 | * @class Courses
10 | */
11 | export class Courses extends Component {
12 |
13 | componentDidMount() {
14 | this.props.getCourses();
15 | }
16 |
17 | /** Calculate how much material is in */
18 | calcMaterial(videos, filePaths){
19 | return videos.length + filePaths.length
20 | }
21 |
22 | /** Generate courses table depending on student and admin view. This depends if it is enabled or disabled*/
23 | generateCourses(courseData){
24 | let role = this.props.auth.role
25 | let filterCourses = courseData.filter(el => {
26 | if (role === 1) { return true }
27 | else {
28 | if (el.courseStatus === 'enabled') {
29 | return true
30 | }
31 | return
32 | }
33 | })
34 |
35 | return filterCourses.map(val => {
36 | return (
37 |
38 | {val.name} |
39 | {this.calcMaterial(val.videos, val.filePaths)} |
40 | {val.members.length} |
41 | {role === 1 && {val.courseStatus} | }
42 |
43 |
44 |
45 |
46 | |
47 |
)
48 | })
49 | }
50 |
51 |
52 |
53 |
54 | render() {
55 | let courses = this.generateCourses(this.props.courses)
56 | return (
57 |
58 |
59 | All Courses
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Course name |
69 | Total material |
70 | People enrolled |
71 | {this.props.auth.role === 1 && Status | }
72 | Action |
73 |
74 |
75 |
76 | {courses}
77 |
78 |
79 |
80 |
81 |
82 | )
83 | }
84 | }
85 |
86 |
87 | const reduxProps = state => {
88 | return ({
89 | courses: state.course.courses,
90 | auth: state.user.authUser
91 | })
92 | };
93 |
94 |
95 | export default connect(reduxProps, { getCourses })(Courses);
96 |
--------------------------------------------------------------------------------
/client/src/components/CreateCourse/CreateCourse.css:
--------------------------------------------------------------------------------
1 | .margin-right{
2 | margin-right: 5px;
3 | }
4 |
5 | .error{
6 | color: red;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/components/CreateCourse/CreateCourse.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | Grid,
4 | Row,
5 | Col,
6 | Form,
7 | FormGroup,
8 | FormControl,
9 | Button,
10 | ControlLabel
11 | } from 'react-bootstrap';
12 | import './CreateCourse.css';
13 | import {errorMessages} from '../../helpers';
14 |
15 | import { addCourse } from '../../actions/courseActions';
16 | import { connect } from 'react-redux';
17 |
18 | /**
19 | * CreateCourse React Class
20 | * @class CreateCourse
21 | */
22 | class CreateCourse extends Component {
23 |
24 | constructor(props) {
25 | super(props);
26 |
27 | this.state = {
28 | name: '',
29 | dateFrom: '',
30 | dateTo: '',
31 | description: '',
32 | courseStatus: 'enabled',
33 | nameValid: null,
34 | dateFromValid: null,
35 | dateToValid: null,
36 | descriptionValid: null,
37 | isSend: false
38 | };
39 |
40 | this.submitForm = this.submitForm.bind(this);
41 | this.handleInput = this.handleInput.bind(this);
42 |
43 |
44 | }
45 |
46 |
47 |
48 | /** Validation check if in form everything is valid*/
49 | isValid(){
50 |
51 | let checkValid = true
52 |
53 | if (this.state.name.length < 6) {
54 | this.setState({ nameValid: errorMessages['name']['length'] })
55 | checkValid = false
56 | }else{
57 | this.setState({nameValid: null})
58 | }
59 |
60 | if (this.state.dateFrom === '') {
61 | this.setState({ dateFromValid: errorMessages['dateFrom']['length'] })
62 | checkValid = false
63 | }else{
64 | this.setState({dateFromValid: null})
65 | }
66 |
67 | if (this.state.dateTo === '') {
68 | this.setState({ dateToValid: errorMessages['dateTo']['length']})
69 | checkValid = false
70 | }else{
71 | this.setState({dateToValid: null})
72 | }
73 |
74 | if (this.state.description.length < 6) {
75 | this.setState({ descriptionValid: errorMessages['description']['length']})
76 | checkValid = false
77 | }else{
78 | this.setState({descriptionValid: null})
79 | }
80 |
81 | return checkValid;
82 | }
83 |
84 |
85 |
86 |
87 | handleInput(e) {
88 |
89 | let name = e.target.id
90 | let val = e.target.value
91 | this.setState({[name]: val})
92 |
93 | if (this.state.isSend === true) {
94 | this.isValid()
95 | }
96 | }
97 |
98 | /** After submit clean form*/
99 | cleanForm(){
100 | this.setState({
101 | name: '',
102 | dateFrom: '',
103 | dateTo: '',
104 | description: '',
105 | courseStatus: 'enabled'
106 | })
107 | alert('Course created')
108 | }
109 |
110 |
111 | submitForm(e) {
112 | e.preventDefault();
113 |
114 | this.setState({isSend: true})
115 | if (this.isValid() === false) { return false; }
116 |
117 | this.props.addCourse(this.state)
118 | this.cleanForm()
119 |
120 | }
121 |
122 |
123 |
124 |
125 | render() {
126 |
127 |
128 | return (
129 |
130 | Create Course
131 |
132 |
133 |
134 |
135 |
136 |
205 |
206 |
207 |
208 |
209 | )
210 | }
211 | }
212 |
213 | const reduxProps = state => {
214 | return ({courses: state.course.courses})
215 | };
216 |
217 | export default connect(reduxProps, { addCourse })(CreateCourse);
218 |
--------------------------------------------------------------------------------
/client/src/components/CreateUser/CreateUser.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | Grid,
4 | Row,
5 | Col,
6 | Form,
7 | FormGroup,
8 | FormControl,
9 | Button,
10 | ControlLabel
11 | } from 'react-bootstrap';
12 |
13 | import { addUser } from '../../actions/userActions';
14 | import { removeError } from '../../actions/errorActions';
15 |
16 | import { connect } from 'react-redux';
17 |
18 | /**
19 | * CreateUser React Class
20 | * @class CreateUser
21 | */
22 | class CreateUser extends Component {
23 |
24 | constructor(props) {
25 | super(props);
26 |
27 | this.state = {
28 | firstName: '',
29 | lastName: '',
30 | email: '',
31 | password: '',
32 | role: 0,
33 | firstNameValid: null,
34 | lastNameValid: null,
35 | emailValid: null,
36 | passwordValid: null,
37 | isSend: false
38 | };
39 |
40 | this.submitForm = this.submitForm.bind(this);
41 | this.handleInput = this.handleInput.bind(this);
42 |
43 | }
44 |
45 | /*When a new user is created cleanForm and clean all errors in state from Redux*/
46 | componentWillReceiveProps(newProps) {
47 |
48 | if (newProps.errorMesage.err) {
49 | alert(newProps.errorMesage.err)
50 | this.props.removeError()
51 | }
52 |
53 | if (newProps.user !== this.props.user) {
54 | this.cleanForm()
55 | }
56 | }
57 |
58 | isValid(){
59 |
60 | let checkValid = true
61 |
62 | if (this.state.firstName === '') {
63 | this.setState({ firstNameValid: 'First name cannot be empty' })
64 | checkValid = false
65 | }else{
66 | this.setState({firstNameValid: null})
67 | }
68 |
69 | if (this.state.lastName === '') {
70 | this.setState({ lastNameValid: 'Last name cannot be empty' })
71 | checkValid = false
72 | }else{
73 | this.setState({lastNameValid: null})
74 | }
75 |
76 | if (this.state.email === '') {
77 | this.setState({ emailValid: 'Email can not be empty'})
78 | checkValid = false
79 | }else{
80 | this.setState({emailValid: null})
81 | }
82 |
83 | if (this.state.password === '') {
84 | this.setState({ passwordValid: 'Password field cannot be empty'})
85 | checkValid = false
86 | }else{
87 | this.setState({passwordValid: null})
88 | }
89 |
90 | return checkValid;
91 | }
92 |
93 |
94 |
95 |
96 | handleInput(e) {
97 | let name = e.target.id
98 | let val = e.target.value
99 | this.setState({[name]: val})
100 |
101 | if (this.state.isSend === true) {
102 | this.isValid()
103 | }
104 | }
105 |
106 | cleanForm(){
107 | this.setState({
108 | firstName: '',
109 | lastName: '',
110 | email: '',
111 | password: ''
112 | })
113 | alert('User created')
114 | }
115 |
116 |
117 | submitForm(e) {
118 | e.preventDefault();
119 | this.setState({isSend: true})
120 | if (this.isValid() === false) { return false; }
121 | this.props.addUser(this.state)
122 |
123 |
124 | }
125 |
126 |
127 |
128 |
129 | render() {
130 |
131 |
132 | return (
133 |
134 | Create User
135 |
136 |
137 |
138 |
139 |
208 |
209 |
210 |
211 |
212 | )
213 | }
214 | }
215 |
216 |
217 |
218 | const reduxProps = state => {
219 | return ({
220 | errorMesage: state.errors.message,
221 | users: state.user.users,
222 | user: state.user.user
223 | })
224 | };
225 |
226 | export default connect(reduxProps, { addUser, removeError })(CreateUser);
227 |
--------------------------------------------------------------------------------
/client/src/components/EditCourse/EditCourse.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import {
3 | Grid,
4 | Row,
5 | Col,
6 | Form,
7 | FormGroup,
8 | FormControl,
9 | Button,
10 | ControlLabel
11 | } from 'react-bootstrap';
12 | import {errorMessages} from '../../helpers';
13 |
14 | import { getCourse, updateCourse } from '../../actions/courseActions';
15 | import { connect } from 'react-redux';
16 |
17 |
18 | /**
19 | * EditCourse React Class
20 | * @class EditCourse
21 | */
22 | class EditCourse extends Component {
23 |
24 | constructor(props) {
25 | super(props);
26 | this.state = {
27 | name: '',
28 | dateFrom: '',
29 | dateTo: '',
30 | description: '',
31 | courseStatus: 'enabled',
32 | nameValid: null,
33 | dateFromValid: null,
34 | dateToValid: null,
35 | descriptionValid: null,
36 | isSend: false
37 | };
38 |
39 | this.submitForm = this.submitForm.bind(this);
40 | this.handleInput = this.handleInput.bind(this);
41 |
42 |
43 | }
44 |
45 | /** Check validation */
46 | isValid(){
47 |
48 | let checkValid = true
49 |
50 | if (this.state.name.length < 6) {
51 | this.setState({ nameValid: errorMessages['name']['length'] })
52 | checkValid = false
53 | }else{
54 | this.setState({nameValid: null})
55 | }
56 |
57 | if (this.state.dateFrom === '') {
58 | this.setState({ dateFromValid: errorMessages['dateFrom']['length'] })
59 | checkValid = false
60 | }else{
61 | this.setState({dateFromValid: null})
62 | }
63 |
64 | if (this.state.dateTo === '') {
65 | this.setState({ dateToValid: errorMessages['dateTo']['length']})
66 | checkValid = false
67 | }else{
68 | this.setState({dateToValid: null})
69 | }
70 |
71 | if (this.state.description.length < 6) {
72 | this.setState({ descriptionValid: errorMessages['description']['length']})
73 | checkValid = false
74 | }else{
75 | this.setState({descriptionValid: null})
76 | }
77 |
78 | return checkValid;
79 | }
80 |
81 |
82 | /** Assign all data from form before sending */
83 | assignData(data){
84 | this.setState({
85 | _id: data._id,
86 | name: data.name,
87 | dateFrom: data.dateFrom,
88 | dateTo: data.dateTo,
89 | description: data.description,
90 | courseStatus: data.courseStatus
91 | });
92 | }
93 |
94 |
95 |
96 | handleInput(e) {
97 |
98 | let name = e.target.id
99 | let val = e.target.value
100 | this.setState({[name]: val})
101 | if (this.state.isSend === true) {
102 | this.isValid()
103 | }
104 | }
105 |
106 |
107 |
108 |
109 | submitForm(e) {
110 | e.preventDefault();
111 |
112 | let courseData = this.state
113 | this.setState({isSend: true})
114 | if (this.isValid() === false) { return false; }
115 |
116 | this.props.updateCourse(courseData)
117 | alert('Course Edited')
118 |
119 | }
120 |
121 | componentWillReceiveProps(newProps) {
122 | let course = newProps.course
123 | if (Object.keys(course).length !== 0) {
124 | this.assignData(course)
125 | }
126 | }
127 |
128 |
129 |
130 | componentDidMount() {
131 | let courseId = this.props.match.params.courseId
132 | this.props.getCourse(courseId)
133 | }
134 |
135 |
136 |
137 | render() {
138 |
139 | return (
140 |
141 | Edit Course
142 |
143 |
144 |
145 |
146 |
213 |
214 |
215 |
216 |
217 | )
218 | }
219 | }
220 |
221 | const reduxProps = state => {
222 | return ({course: state.course.course})
223 | };
224 |
225 | export default connect(reduxProps, { getCourse,updateCourse })(EditCourse);
226 |
--------------------------------------------------------------------------------
/client/src/components/Header/Header.css:
--------------------------------------------------------------------------------
1 | .loggedIn{
2 | color: white;
3 | display: inline-block;
4 | font-size: 0.8em;
5 |
6 | }
7 |
8 | .navbar-header{
9 | width:100%;
10 | }
11 |
12 | .education-logo{
13 | height:130%;
14 | padding-right: 10px;
15 | }
16 |
17 |
18 | /* sm screen */
19 | @media screen and (max-width: 767px) {
20 | .header-title {
21 | font-size: 1em;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/client/src/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Navbar,Nav, NavItem } from 'react-bootstrap';
3 |
4 |
5 | import Sidebar from './../Sidebar/Sidebar'
6 |
7 | import { connect } from 'react-redux';
8 | import './Header.css';
9 |
10 | /**
11 | * Header React Class - Ref: based on react-bootstrap examples : https://goo.gl/pBMvSb
12 | * @class Header
13 | */
14 | class Header extends Component {
15 |
16 | render() {
17 |
18 | let role = this.props.auth.role
19 | return(
20 |
21 |
22 |
23 |
24 |
25 |
Education App
26 |
27 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | )
46 | }
47 | }
48 |
49 | const reduxProps = state => {
50 | return ({
51 | auth: state.user.authUser
52 | })
53 | };
54 |
55 | export default connect(reduxProps)(Header);
56 |
--------------------------------------------------------------------------------
/client/src/components/Home/Home.css:
--------------------------------------------------------------------------------
1 | .panelPointer{
2 | cursor: pointer;
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/components/Home/Home.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Grid,Row,Col,Panel } from 'react-bootstrap';
3 | import { connect } from 'react-redux';
4 | import './Home.css';
5 |
6 |
7 |
8 |
9 | /**
10 | * Home React Class - Depending on role show different output
11 | * @class Home
12 | */
13 | class Home extends Component {
14 |
15 | constructor(props) {
16 | super(props);
17 | this.handleClick = this.handleClick.bind(this);
18 | }
19 |
20 | handleClick(name){
21 | this.props.history.push(name);
22 | }
23 |
24 |
25 | render() {
26 | let role = this.props.auth.role
27 | return (
28 |
29 | Home
30 |
31 |
32 |
33 | ´
34 | this.handleClick('/all-courses', e)}>
35 | See All Courses
36 |
37 |
38 | { role === 1 &&
39 |
40 | this.handleClick('/create-course', e)}>
41 | Create Course
42 |
43 |
44 | }
45 |
46 | { role === 1 &&
47 |
48 | this.handleClick('/all-users', e)}>
49 | All Users
50 |
51 |
52 | }
53 |
54 |
55 |
56 | )
57 | }
58 | }
59 |
60 |
61 | const reduxProps = state => {
62 | return ({
63 | auth: state.user.authUser
64 | })
65 | };
66 |
67 | export default connect(reduxProps)(Home);
68 |
--------------------------------------------------------------------------------
/client/src/components/Login/Login.css:
--------------------------------------------------------------------------------
1 | /* Ref: Based on bootstrap example https://goo.gl/HrQtz4 */
2 |
3 | .form-signin {
4 | width: 100%;
5 | max-width: 330px;
6 | padding: 15px;
7 | margin: auto;
8 | text-align: center !important;
9 | margin-top: 10%;
10 | }
11 | .form-signin .form-control {
12 | position: relative;
13 | box-sizing: border-box;
14 | height: auto;
15 | font-size: 16px;
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/components/Login/Login.js:
--------------------------------------------------------------------------------
1 |
2 | import React, {Component} from 'react';
3 | import {
4 | Form,
5 | FormGroup,
6 | FormControl,
7 | Button,
8 | ControlLabel,
9 | } from 'react-bootstrap';
10 | import './Login.css';
11 |
12 |
13 |
14 | import { loginUser } from '../../actions/userActions';
15 | import { connect } from 'react-redux';
16 | import { removeError } from '../../actions/errorActions';
17 |
18 |
19 |
20 | /**
21 | * Login React Class - Shows a different view
22 | * @class Login
23 | */
24 | class Login extends Component {
25 |
26 |
27 | constructor(props) {
28 | super(props);
29 |
30 | this.state = {
31 | email: '',
32 | password: ''
33 | };
34 |
35 | this.submitForm = this.submitForm.bind(this);
36 | this.handleInput = this.handleInput.bind(this);
37 |
38 | }
39 |
40 | componentWillReceiveProps(newProps) {
41 |
42 | if (newProps.errorMesage.err) {
43 | alert(newProps.errorMesage.err)
44 | this.props.removeError()
45 | }
46 |
47 | if (Object.keys(newProps.auth).length > 0 ) {
48 | this.props.history.push('/')
49 | }
50 |
51 | }
52 |
53 |
54 | handleInput(e) {
55 | let name = e.target.id
56 | let val = e.target.value
57 | this.setState({[name]: val})
58 |
59 | }
60 |
61 | submitForm(e){
62 | e.preventDefault();
63 | this.props.loginUser(this.state)
64 | }
65 |
66 |
67 | render(){
68 |
69 | return(
70 |
102 |
103 | )
104 | }
105 |
106 | }
107 |
108 | const reduxProps = state => {
109 | return ({
110 | auth: state.user.authUser,
111 | errorMesage: state.errors.message
112 | })
113 | };
114 |
115 |
116 | export default connect(reduxProps, { loginUser, removeError })(Login);
117 |
--------------------------------------------------------------------------------
/client/src/components/Main/Main.js:
--------------------------------------------------------------------------------
1 |
2 | import React, {Component} from 'react';
3 | import Header from '../Header/Header';
4 | import Sidebar from '../Sidebar/Sidebar';
5 | import Container from '../Container/Container';
6 |
7 | /**
8 | * Main React Class
9 | * @class Main
10 | */
11 | class Main extends Component {
12 |
13 | render() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 | }
23 |
24 | export default Main;
25 |
--------------------------------------------------------------------------------
/client/src/components/MyCourses/MyCourses.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Table, Grid, Row, Col, Button } from 'react-bootstrap';
3 | import { Link } from 'react-router-dom'
4 | import { getCourses } from '../../actions/courseActions';
5 | import { connect } from 'react-redux';
6 |
7 |
8 | /**
9 | * MyCourses React Class - shows all courses from a student perspective - Almost same code as courses
10 | * @class MyCourses
11 | */
12 | class MyCourses extends Component {
13 |
14 | componentDidMount() {
15 | this.props.getCourses();
16 | }
17 |
18 | /** Calculate materials*/
19 | calcMaterial(videos, filePaths){
20 | return videos.length + filePaths.length
21 | }
22 |
23 | /** Generate courses that apply to the current user */
24 | generateCourses(courseData){
25 |
26 | let auth = this.props.auth
27 |
28 |
29 | let filterCourses = courseData.filter(el => {
30 | if (el.courseStatus === 'enabled') {
31 | let index = el.members.findIndex(val => val.userId === auth._id)
32 | return index > -1
33 | }
34 | })
35 |
36 |
37 | return filterCourses.map(val => {
38 | return (
39 |
40 | {val.name} |
41 | {this.calcMaterial(val.videos, val.filePaths)} |
42 | {val.members.length} |
43 |
44 |
45 |
46 |
47 | |
48 |
)
49 | })
50 | }
51 |
52 |
53 |
54 |
55 | render() {
56 | let courses = this.generateCourses(this.props.courses)
57 | return (
58 |
59 | My Courses
60 |
61 |
62 |
63 |
64 |
65 |
71 |
72 |
73 |
74 | Course name
75 | |
76 |
77 | Total material
78 | |
79 |
80 | People enrolled
81 | |
82 | Action |
83 |
84 |
85 |
86 | {courses}
87 |
88 |
89 |
90 |
91 |
92 | )
93 | }
94 | }
95 |
96 |
97 | const reduxProps = state => {
98 | return ({
99 | courses: state.course.courses,
100 | auth: state.user.authUser
101 | })
102 | };
103 |
104 |
105 | export default connect(reduxProps, { getCourses })(MyCourses);
106 |
--------------------------------------------------------------------------------
/client/src/components/NoPage/NoPage.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Grid,Row,Col } from 'react-bootstrap';
3 |
4 |
5 | /**
6 | * NoPage React Class - Shows 404 when page not present
7 | * @class NoPage
8 | */
9 | class NoPage extends Component {
10 |
11 | render() {
12 | return (
13 |
14 |
15 |
16 | This Page as not found 404
17 |
18 |
19 | )
20 | }
21 | }
22 |
23 |
24 | export default NoPage
25 |
--------------------------------------------------------------------------------
/client/src/components/Sidebar/Sidebar.css:
--------------------------------------------------------------------------------
1 |
2 | /*Ref: Based on the example https://goo.gl/5sHujd */
3 |
4 | .sidebar {
5 | position: fixed;
6 | top: 0;
7 | bottom: 0;
8 | left: 0;
9 | z-index: 100;
10 | padding: 48px 0 0;
11 | box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
12 | }
13 |
14 | .sidebar-sticky {
15 | position: relative;
16 | top: 0;
17 | height: calc(100vh - 48px);
18 | padding-top: .5rem;
19 | overflow-x: hidden;
20 | overflow-y: auto;
21 | }
22 |
23 | @supports ((position: -webkit-sticky) or (position: sticky)) {
24 | .sidebar-sticky {
25 | position: -webkit-sticky;
26 | position: sticky;
27 | }
28 | }
29 |
30 | .sidebar .nav-link {
31 | font-weight: 500;
32 | color: #333;
33 | }
34 |
35 |
36 | .sidebar .nav-link.active {
37 | color: #007bff;
38 | }
39 |
40 | .sidebar .nav-link:hover .feather,
41 | .sidebar .nav-link.active .feather {
42 | color: inherit;
43 | }
44 |
45 | .sidebar-heading {
46 | font-size: .75rem;
47 | text-transform: uppercase;
48 | }
49 |
50 | .nav-item a:link, .nav-item a:visited {
51 | color: #002b56 !important;
52 | }
53 |
54 | /* Ref as example from https://goo.gl/MGtAfh */
55 | .navbar-nav span[role=button] {
56 | line-height: 18px;
57 | padding: 18px;
58 | display: inline-block;
59 | }
60 |
61 | .navbar-nav a{
62 | color: white !important;
63 | }
64 |
--------------------------------------------------------------------------------
/client/src/components/Sidebar/Sidebar.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import './Sidebar.css';
3 | import { Link } from 'react-router-dom'
4 | import { connect } from 'react-redux';
5 | import { logoutUser } from '../../actions/userActions';
6 | import { Nav, NavItem } from 'react-bootstrap';
7 |
8 | /**
9 | * Sidebar React Class - Shows Sidebar depending on the user role
10 | * @class Sidebar
11 | */
12 | class Sidebar extends Component {
13 |
14 | constructor(props) {
15 | super(props);
16 |
17 | this.state = {menu: [
18 | {name: "Home", url: "/", role: 0},
19 | {name: "All Courses", url: "/all-courses", role: 0},
20 | {name: "My Courses", url: "/my-courses", role: 2},
21 | {name: "All Users", url: "/all-users", role: 1},
22 | {name: "Create Course", url: "/create-course", role: 1},
23 | {name: "Create User", url: "/create-user", role: 1}
24 | ]}
25 |
26 | this.logout = this.logout.bind(this);
27 | }
28 |
29 |
30 | /** logout is a different function in link list*/
31 | logout(e){
32 | this.props.logoutUser()
33 | }
34 |
35 | /** Generating asll links depending on user role*/
36 | generateLinks(menuItems){
37 | return menuItems.map((exp,i) => {
38 |
39 | let isResponsive = this.props.isResponsive;
40 | if (isResponsive) {
41 | return (
42 | {exp.name}
43 | )
44 |
45 | }else{
46 | return (
47 | {exp.name}
48 | )
49 | }
50 | })
51 | }
52 |
53 | render() {
54 | /*role --> student = 0 and admin = 1*/
55 | let role = this.props.auth.role
56 | let isResponsive = this.props.isResponsive;
57 |
58 |
59 | let menuItems = this.state.menu.filter(el => {
60 | if (role === 1){
61 | if (el.role === 2) {
62 | return false
63 | }
64 | return true
65 | }
66 |
67 | if (role === 0) {
68 | if (el.role === 0 || el.role === 2) {
69 | return true;
70 | }
71 | }
72 | return false
73 | })
74 | let finalLinks = this.generateLinks(menuItems)
75 |
76 | if (isResponsive) {
77 |
78 |
79 | return(
85 | )
86 |
87 | }else{
88 |
89 | return (
90 |
100 | )
101 |
102 | }
103 | }
104 | }
105 |
106 |
107 |
108 | const reduxProps = state => {
109 | return ({
110 | auth: state.user.authUser
111 | })
112 | };
113 |
114 | export default connect(reduxProps, {logoutUser})(Sidebar);
115 |
--------------------------------------------------------------------------------
/client/src/components/Users/Users.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import { Table, Grid, Row, Col, Button } from 'react-bootstrap';
3 | import { getUsers, deleteUser } from '../../actions/userActions';
4 | import { connect } from 'react-redux';
5 |
6 | /**
7 | * Users React Class - Shows all users
8 | * @class Users
9 | */
10 |
11 | class Users extends Component {
12 |
13 |
14 | constructor(props) {
15 | super(props);
16 | this.deleteUser = this.deleteUser.bind(this);
17 | }
18 |
19 |
20 | componentDidMount() {
21 | this.props.getUsers();
22 | }
23 |
24 | /** delete specific user*/
25 | deleteUser(userId){
26 |
27 | this.props.deleteUser(userId)
28 | alert('Users deleted')
29 |
30 | }
31 |
32 | /** generate all users*/
33 | generateUsers(userData){
34 |
35 | return userData.map(val => {
36 | return (
37 |
38 | {val.firstName} |
39 | {val.lastName} |
40 | {val.email} |
41 | {defineRole(val.role)} |
42 |
43 |
44 |
45 | |
46 |
)
47 | })
48 |
49 | }
50 |
51 | render() {
52 | let users = this.generateUsers(this.props.users)
53 | return (
54 |
55 |
56 | All Users
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | First name |
67 | Last name |
68 | Email |
69 | Role |
70 | Delete |
71 |
72 |
73 |
74 | {users}
75 |
76 |
77 |
78 |
79 |
80 | )
81 | }
82 | }
83 |
84 | /** show student or admin in the table*/
85 | const defineRole = role => {
86 | return role === 0 ? 'student' : 'admin'
87 | }
88 |
89 |
90 | const reduxProps = state => {
91 | return ({users: state.user.users})
92 | };
93 |
94 |
95 | export default connect(reduxProps, { getUsers, deleteUser })(Users);
96 |
--------------------------------------------------------------------------------
/client/src/configStore.js:
--------------------------------------------------------------------------------
1 | /**
2 | * configStore module
3 | * Ref: Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module configStore
5 | */
6 |
7 | import { createStore, applyMiddleware } from 'redux';
8 | import thunk from 'redux-thunk';
9 | import rootReducer from './reducers';
10 |
11 | const initialState = {};
12 | const middleware = [thunk];
13 |
14 |
15 |
16 | const configStore = createStore(
17 | rootReducer,
18 | initialState,
19 | applyMiddleware(...middleware)
20 | );
21 |
22 |
23 |
24 | export default configStore;
25 |
--------------------------------------------------------------------------------
/client/src/education-logo-login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/client/src/education-logo-login.png
--------------------------------------------------------------------------------
/client/src/education-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/client/src/education-logo.png
--------------------------------------------------------------------------------
/client/src/helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * helpers module
3 | * Ref: Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module helpers
5 | */
6 |
7 |
8 |
9 | import axios from 'axios'
10 |
11 | const allowedFormats = {
12 | 'PDF': ['pdf'],
13 | 'Word' : ['doc', 'docx', 'docm'],
14 | 'Excel': ['xls', 'xlt', 'xlm', 'xlsx'],
15 | 'Powerpoint' : ['ppt', 'pot', 'ppt'],
16 | 'PNG': ['png'],
17 | 'JPG' : ['jpg', 'jpeg']
18 | }
19 |
20 |
21 | const errorMessages = {
22 | 'name': {
23 | 'length': 'The name field has to be longer than 6 characters'
24 | },
25 | 'dateFrom': {
26 | 'length': 'The duration from field can not be empty'
27 | },
28 | 'dateTo': {
29 | 'length': 'The duration to field can not be empty'
30 | },
31 | 'description': {
32 | 'length': 'The description fiels should be longer than 6 characters'
33 | }
34 | }
35 |
36 |
37 | /** determine name for showing extension in file */
38 | function determineName(fileName){
39 |
40 | let splitName = fileName.split(".");
41 | let lastElement = splitName[splitName.length-1]
42 |
43 | let results = Object.keys(allowedFormats).filter((el,key) => {
44 | let formats = allowedFormats[el]
45 | let isFormat = formats.findIndex(element => element === lastElement)
46 |
47 | if (isFormat > -1) {
48 | let final = Object.getOwnPropertyNames(allowedFormats)[key]
49 | return final
50 | }
51 | })
52 |
53 | return results[0]
54 |
55 | }
56 |
57 |
58 | /** Ref: As seen from stackoverflow https://goo.gl/6n8rJv */
59 | function setAuthToken(token){
60 | if (token) {
61 | axios.defaults.headers.common['token'] = `${token}`
62 | }else{
63 | delete axios.defaults.headers.common['token']
64 | }
65 | }
66 |
67 |
68 | export {allowedFormats, errorMessages, determineName, setAuthToken};
69 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 | .align-middle{
7 | text-align: center;
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * index.js module
3 | * @module index.js
4 | */
5 |
6 | import React from 'react';
7 | import ReactDOM from 'react-dom';
8 | import './index.css';
9 | import App from './components/App/App.js';
10 | import { BrowserRouter } from 'react-router-dom'
11 |
12 | import { Provider } from 'react-redux'
13 | import configStore from './configStore'
14 | import {setAuthToken} from './helpers'
15 |
16 | import jwtDecode from 'jwt-decode';
17 | import { setUser } from './actions/userActions';
18 |
19 | // Ref: Example seen from https://goo.gl/6WBzMn
20 | if (localStorage.jwtToken) {
21 | setAuthToken(localStorage.jwtToken)
22 | configStore.dispatch(setUser(jwtDecode(localStorage.jwtToken)))
23 | }
24 |
25 |
26 | ReactDOM.render(
27 |
28 |
29 |
30 |
31 |
32 | , document.getElementById('root'));
33 |
--------------------------------------------------------------------------------
/client/src/reducers/courseReducer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * courseReducer module
3 | * Ref: Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module courseReducer
5 | */
6 |
7 |
8 |
9 | import {
10 | ADD_COURSE,
11 | GET_COURSES,
12 | GET_COURSE,
13 | UPDATE_COURSE,
14 | DELETE_COURSE,
15 | CLEAN_COURSE,
16 | } from '../actions/typesActions';
17 |
18 |
19 | const firstState = {
20 | courses: [],
21 | course: {}
22 | }
23 |
24 |
25 | /*CHANGE STATE ACCORDINGLY*/
26 | export default function(state = firstState, action) {
27 | switch (action.type) {
28 | case GET_COURSES:
29 | return {
30 | ...state,
31 | courses: action.payload
32 | };
33 | case ADD_COURSE:
34 | return {
35 | ...state,
36 | courses: [action.payload, ...state.courses]
37 | };
38 | case GET_COURSE:
39 | return {
40 | ...state,
41 | course: action.payload
42 | };
43 |
44 | case CLEAN_COURSE:
45 | return {
46 | ...state,
47 | course: {}
48 | };
49 |
50 | case DELETE_COURSE:
51 | return {
52 | ...state,
53 | courses: state.courses.filter(el => {
54 | return el._id !== action.payload._id
55 | })
56 | };
57 | case UPDATE_COURSE:
58 | return {
59 | ...state,
60 | course: action.payload
61 | };
62 | default:
63 | return state;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/src/reducers/errorReducer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * errorReducer module
3 | * Ref: Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module errorReducer
5 | */
6 | import {
7 | ERRORS,
8 | REMOVE_ERRORS
9 | } from '../actions/typesActions';
10 |
11 | const firstState = {
12 | message: {}
13 | }
14 |
15 | export default function(state = firstState, action) {
16 | switch (action.type) {
17 | case ERRORS:
18 | return {
19 | message: action.payload
20 | };
21 |
22 | case REMOVE_ERRORS:
23 | return {
24 | message: {}
25 | };
26 |
27 | default:
28 | return state;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/client/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * index.js module for configuration
3 | * Ref: Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module index.js reducer configuration
5 | */
6 | import { combineReducers } from 'redux';
7 | import courseReducer from './courseReducer';
8 | import userReducer from './userReducer';
9 | import errorReducer from './errorReducer';
10 |
11 | const reducers = combineReducers({
12 | course: courseReducer,
13 | user: userReducer,
14 | errors: errorReducer
15 | });
16 |
17 | export default reducers
18 |
--------------------------------------------------------------------------------
/client/src/reducers/userReducer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * userReducer module
3 | * Ref: Redux Examples taken from https://bit.ly/2BIGB2T
4 | * @module userReducer
5 | */
6 |
7 | import {
8 | ADD_USER,
9 | GET_USERS,
10 | DELETE_USER,
11 | LOGIN_USER,
12 | LOGOUT_USER
13 | } from '../actions/typesActions';
14 |
15 | const firstState = {
16 | users: [],
17 | user: {},
18 | authUser: {}
19 | }
20 |
21 | export default function(state = firstState, action) {
22 | switch (action.type) {
23 |
24 | case ADD_USER:
25 | return {
26 | ...state,
27 | users: [action.payload, ...state.users],
28 | user: action.payload
29 | };
30 | case GET_USERS:
31 | return {
32 | ...state,
33 | users: action.payload
34 | };
35 | case DELETE_USER:
36 | return {
37 | ...state,
38 | users: state.users.filter(el => {
39 | return el._id !== action.payload._id
40 | })
41 | };
42 | case LOGIN_USER:
43 | return {
44 | ...state,
45 | authUser: action.payload
46 | };
47 | case LOGOUT_USER:
48 | return {
49 | ...state,
50 | authUser: {}
51 | };
52 | default:
53 | return state;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log('New content is available; please refresh.');
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log('Content is cached for offline use.');
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch(error => {
80 | console.error('Error during service worker registration:', error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then(response => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get('content-type').indexOf('javascript') === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then(registration => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | 'No internet connection found. App is running in offline mode.'
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ('serviceWorker' in navigator) {
113 | navigator.serviceWorker.ready.then(registration => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/client/src/test/Components.test.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * test components module
4 | * Ref: https://bit.ly/2BpDf4p
5 | * Ref: https://bit.ly/2MCYu7p
6 | * @module Component.test.js
7 | */
8 |
9 | import React from 'react';
10 |
11 | import App from './../components/App/App';
12 | import Courses from './../components/Courses/Courses';
13 | import Course from './../components/Course/Course';
14 | import Users from './../components/Users/Users';
15 | import CreateCourse from './../components/CreateCourse/CreateCourse';
16 | import CreateUser from './../components/CreateUser/CreateUser';
17 | import EditCourse from './../components/EditCourse/EditCourse';
18 | import Home from './../components/Home/Home';
19 | import Login from './../components/Login/Login';
20 | import MyCourses from './../components/MyCourses/MyCourses';
21 | import Sidebar from './../components/Sidebar/Sidebar';
22 | import Auth from './../components/Auth/Auth';
23 | import NoPage from './../components/NoPage/NoPage';
24 | import Main from './../components/Main/Main';
25 |
26 |
27 | // Get mock data for API simulation
28 | import configureMockStore from "redux-mock-store";
29 | import { Provider } from "react-redux";
30 | const mockStore = configureMockStore();
31 | const store = mockStore({});
32 |
33 |
34 |
35 |
36 | import { shallow } from 'enzyme';
37 | import { configure } from 'enzyme';
38 | import Adapter from 'enzyme-adapter-react-16';
39 | configure({ adapter: new Adapter() });
40 |
41 |
42 |
43 |
44 | // Renders App
45 | describe('Testing Component', () => {
46 | const wrapper = shallow()
47 | it('renders', () => {
48 | expect(wrapper.length).toBe(1);
49 | });
50 | })
51 |
52 | // Renders Courses
53 | describe('Testing Component', () => {
54 | const wrapper = shallow(
55 |
56 |
57 | )
58 | it('renders', () => {
59 | expect(wrapper.length).toBe(1);
60 | });
61 | })
62 |
63 |
64 | // Renders Users
65 | describe('Testing Component', () => {
66 | const wrapper = shallow(
67 |
68 |
69 | )
70 | it('renders', () => {
71 | expect(wrapper.length).toBe(1);
72 | });
73 | })
74 |
75 |
76 | // Renders CreateCourse
77 | describe('Testing Component', () => {
78 | const wrapper = shallow(
79 |
80 |
81 | )
82 | it('renders', () => {
83 | expect(wrapper.length).toBe(1);
84 | });
85 | })
86 |
87 | // Renders CreateUser
88 | describe('Testing Component', () => {
89 | const wrapper = shallow(
90 |
91 |
92 | )
93 | it('renders', () => {
94 | expect(wrapper.length).toBe(1);
95 | });
96 | })
97 |
98 |
99 | // Renders EditCourse
100 | describe('Testing Component', () => {
101 | const wrapper = shallow(
102 |
103 |
104 | )
105 | it('renders', () => {
106 | expect(wrapper.length).toBe(1);
107 | });
108 | })
109 |
110 |
111 | // Renders Home Component
112 | describe('Testing Component', () => {
113 | const wrapper = shallow(
114 |
115 |
116 | )
117 | it('renders', () => {
118 | expect(wrapper.length).toBe(1);
119 | });
120 | })
121 |
122 |
123 | // Renders Login component
124 | describe('Testing Component', () => {
125 | const wrapper = shallow(
126 |
127 |
128 | )
129 | it('renders', () => {
130 | expect(wrapper.length).toBe(1);
131 | });
132 | })
133 |
134 | // Renders Course component
135 | describe('Testing Component', () => {
136 | const wrapper = shallow(
137 |
138 |
139 | )
140 | it('renders', () => {
141 | expect(wrapper.length).toBe(1);
142 | });
143 | })
144 |
145 | // Renders MyCourses component
146 | describe('Testing Component', () => {
147 | const wrapper = shallow(
148 |
149 |
150 | )
151 | it('renders', () => {
152 | expect(wrapper.length).toBe(1);
153 | });
154 | })
155 |
156 | // Renders Sidebar component
157 | describe('Testing Component', () => {
158 | const wrapper = shallow(
159 |
160 |
161 | )
162 | it('renders', () => {
163 | expect(wrapper.length).toBe(1);
164 | });
165 | })
166 |
167 | // Renders Auth component
168 | describe('Testing Component', () => {
169 | const wrapper = shallow(
170 |
171 |
172 | )
173 | it('renders', () => {
174 | expect(wrapper.length).toBe(1);
175 | });
176 | })
177 |
178 |
179 | // Renders NoPage component
180 | describe('Testing Component', () => {
181 | const wrapper = shallow(
182 |
183 |
184 | )
185 | it('renders', () => {
186 | expect(wrapper.length).toBe(1);
187 | });
188 | })
189 |
190 |
191 | // Renders Main component
192 | describe('Testing Component', () => {
193 | const wrapper = shallow(
194 |
195 |
196 | )
197 | it('renders', () => {
198 | expect(wrapper.length).toBe(1);
199 | });
200 | })
201 |
--------------------------------------------------------------------------------
/docs/assets/anchor.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * AnchorJS - v4.0.0 - 2017-06-02
3 | * https://github.com/bryanbraun/anchorjs
4 | * Copyright (c) 2017 Bryan Braun; Licensed MIT
5 | */
6 | /* eslint-env amd, node */
7 |
8 | // https://github.com/umdjs/umd/blob/master/templates/returnExports.js
9 | (function(root, factory) {
10 | 'use strict';
11 | if (typeof define === 'function' && define.amd) {
12 | // AMD. Register as an anonymous module.
13 | define([], factory);
14 | } else if (typeof module === 'object' && module.exports) {
15 | // Node. Does not work with strict CommonJS, but
16 | // only CommonJS-like environments that support module.exports,
17 | // like Node.
18 | module.exports = factory();
19 | } else {
20 | // Browser globals (root is window)
21 | root.AnchorJS = factory();
22 | root.anchors = new root.AnchorJS();
23 | }
24 | })(this, function() {
25 | 'use strict';
26 | function AnchorJS(options) {
27 | this.options = options || {};
28 | this.elements = [];
29 |
30 | /**
31 | * Assigns options to the internal options object, and provides defaults.
32 | * @param {Object} opts - Options object
33 | */
34 | function _applyRemainingDefaultOptions(opts) {
35 | opts.icon = opts.hasOwnProperty('icon') ? opts.icon : '\ue9cb'; // Accepts characters (and also URLs?), like '#', '¶', '❡', or '§'.
36 | opts.visible = opts.hasOwnProperty('visible') ? opts.visible : 'hover'; // Also accepts 'always' & 'touch'
37 | opts.placement = opts.hasOwnProperty('placement')
38 | ? opts.placement
39 | : 'right'; // Also accepts 'left'
40 | opts.class = opts.hasOwnProperty('class') ? opts.class : ''; // Accepts any class name.
41 | // Using Math.floor here will ensure the value is Number-cast and an integer.
42 | opts.truncate = opts.hasOwnProperty('truncate')
43 | ? Math.floor(opts.truncate)
44 | : 64; // Accepts any value that can be typecast to a number.
45 | }
46 |
47 | _applyRemainingDefaultOptions(this.options);
48 |
49 | /**
50 | * Checks to see if this device supports touch. Uses criteria pulled from Modernizr:
51 | * https://github.com/Modernizr/Modernizr/blob/da22eb27631fc4957f67607fe6042e85c0a84656/feature-detects/touchevents.js#L40
52 | * @returns {Boolean} - true if the current device supports touch.
53 | */
54 | this.isTouchDevice = function() {
55 | return !!(
56 | 'ontouchstart' in window ||
57 | (window.DocumentTouch && document instanceof DocumentTouch)
58 | );
59 | };
60 |
61 | /**
62 | * Add anchor links to page elements.
63 | * @param {String|Array|Nodelist} selector - A CSS selector for targeting the elements you wish to add anchor links
64 | * to. Also accepts an array or nodeList containing the relavant elements.
65 | * @returns {this} - The AnchorJS object
66 | */
67 | this.add = function(selector) {
68 | var elements,
69 | elsWithIds,
70 | idList,
71 | elementID,
72 | i,
73 | index,
74 | count,
75 | tidyText,
76 | newTidyText,
77 | readableID,
78 | anchor,
79 | visibleOptionToUse,
80 | indexesToDrop = [];
81 |
82 | // We reapply options here because somebody may have overwritten the default options object when setting options.
83 | // For example, this overwrites all options but visible:
84 | //
85 | // anchors.options = { visible: 'always'; }
86 | _applyRemainingDefaultOptions(this.options);
87 |
88 | visibleOptionToUse = this.options.visible;
89 | if (visibleOptionToUse === 'touch') {
90 | visibleOptionToUse = this.isTouchDevice() ? 'always' : 'hover';
91 | }
92 |
93 | // Provide a sensible default selector, if none is given.
94 | if (!selector) {
95 | selector = 'h2, h3, h4, h5, h6';
96 | }
97 |
98 | elements = _getElements(selector);
99 |
100 | if (elements.length === 0) {
101 | return this;
102 | }
103 |
104 | _addBaselineStyles();
105 |
106 | // We produce a list of existing IDs so we don't generate a duplicate.
107 | elsWithIds = document.querySelectorAll('[id]');
108 | idList = [].map.call(elsWithIds, function assign(el) {
109 | return el.id;
110 | });
111 |
112 | for (i = 0; i < elements.length; i++) {
113 | if (this.hasAnchorJSLink(elements[i])) {
114 | indexesToDrop.push(i);
115 | continue;
116 | }
117 |
118 | if (elements[i].hasAttribute('id')) {
119 | elementID = elements[i].getAttribute('id');
120 | } else if (elements[i].hasAttribute('data-anchor-id')) {
121 | elementID = elements[i].getAttribute('data-anchor-id');
122 | } else {
123 | tidyText = this.urlify(elements[i].textContent);
124 |
125 | // Compare our generated ID to existing IDs (and increment it if needed)
126 | // before we add it to the page.
127 | newTidyText = tidyText;
128 | count = 0;
129 | do {
130 | if (index !== undefined) {
131 | newTidyText = tidyText + '-' + count;
132 | }
133 |
134 | index = idList.indexOf(newTidyText);
135 | count += 1;
136 | } while (index !== -1);
137 | index = undefined;
138 | idList.push(newTidyText);
139 |
140 | elements[i].setAttribute('id', newTidyText);
141 | elementID = newTidyText;
142 | }
143 |
144 | readableID = elementID.replace(/-/g, ' ');
145 |
146 | // The following code builds the following DOM structure in a more effiecient (albeit opaque) way.
147 | // '';
148 | anchor = document.createElement('a');
149 | anchor.className = 'anchorjs-link ' + this.options.class;
150 | anchor.href = '#' + elementID;
151 | anchor.setAttribute('aria-label', 'Anchor link for: ' + readableID);
152 | anchor.setAttribute('data-anchorjs-icon', this.options.icon);
153 |
154 | if (visibleOptionToUse === 'always') {
155 | anchor.style.opacity = '1';
156 | }
157 |
158 | if (this.options.icon === '\ue9cb') {
159 | anchor.style.font = '1em/1 anchorjs-icons';
160 |
161 | // We set lineHeight = 1 here because the `anchorjs-icons` font family could otherwise affect the
162 | // height of the heading. This isn't the case for icons with `placement: left`, so we restore
163 | // line-height: inherit in that case, ensuring they remain positioned correctly. For more info,
164 | // see https://github.com/bryanbraun/anchorjs/issues/39.
165 | if (this.options.placement === 'left') {
166 | anchor.style.lineHeight = 'inherit';
167 | }
168 | }
169 |
170 | if (this.options.placement === 'left') {
171 | anchor.style.position = 'absolute';
172 | anchor.style.marginLeft = '-1em';
173 | anchor.style.paddingRight = '0.5em';
174 | elements[i].insertBefore(anchor, elements[i].firstChild);
175 | } else {
176 | // if the option provided is `right` (or anything else).
177 | anchor.style.paddingLeft = '0.375em';
178 | elements[i].appendChild(anchor);
179 | }
180 | }
181 |
182 | for (i = 0; i < indexesToDrop.length; i++) {
183 | elements.splice(indexesToDrop[i] - i, 1);
184 | }
185 | this.elements = this.elements.concat(elements);
186 |
187 | return this;
188 | };
189 |
190 | /**
191 | * Removes all anchorjs-links from elements targed by the selector.
192 | * @param {String|Array|Nodelist} selector - A CSS selector string targeting elements with anchor links,
193 | * OR a nodeList / array containing the DOM elements.
194 | * @returns {this} - The AnchorJS object
195 | */
196 | this.remove = function(selector) {
197 | var index,
198 | domAnchor,
199 | elements = _getElements(selector);
200 |
201 | for (var i = 0; i < elements.length; i++) {
202 | domAnchor = elements[i].querySelector('.anchorjs-link');
203 | if (domAnchor) {
204 | // Drop the element from our main list, if it's in there.
205 | index = this.elements.indexOf(elements[i]);
206 | if (index !== -1) {
207 | this.elements.splice(index, 1);
208 | }
209 | // Remove the anchor from the DOM.
210 | elements[i].removeChild(domAnchor);
211 | }
212 | }
213 | return this;
214 | };
215 |
216 | /**
217 | * Removes all anchorjs links. Mostly used for tests.
218 | */
219 | this.removeAll = function() {
220 | this.remove(this.elements);
221 | };
222 |
223 | /**
224 | * Urlify - Refine text so it makes a good ID.
225 | *
226 | * To do this, we remove apostrophes, replace nonsafe characters with hyphens,
227 | * remove extra hyphens, truncate, trim hyphens, and make lowercase.
228 | *
229 | * @param {String} text - Any text. Usually pulled from the webpage element we are linking to.
230 | * @returns {String} - hyphen-delimited text for use in IDs and URLs.
231 | */
232 | this.urlify = function(text) {
233 | // Regex for finding the nonsafe URL characters (many need escaping): & +$,:;=?@"#{}|^~[`%!'<>]./()*\
234 | var nonsafeChars = /[& +$,:;=?@"#{}|^~[`%!'<>\]\.\/\(\)\*\\]/g,
235 | urlText;
236 |
237 | // The reason we include this _applyRemainingDefaultOptions is so urlify can be called independently,
238 | // even after setting options. This can be useful for tests or other applications.
239 | if (!this.options.truncate) {
240 | _applyRemainingDefaultOptions(this.options);
241 | }
242 |
243 | // Note: we trim hyphens after truncating because truncating can cause dangling hyphens.
244 | // Example string: // " ⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
245 | urlText = text
246 | .trim() // "⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
247 | .replace(/\'/gi, '') // "⚡⚡ Dont forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
248 | .replace(nonsafeChars, '-') // "⚡⚡-Dont-forget--URL-fragments-should-be-i18n-friendly--hyphenated--short--and-clean-"
249 | .replace(/-{2,}/g, '-') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-short-and-clean-"
250 | .substring(0, this.options.truncate) // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-"
251 | .replace(/^-+|-+$/gm, '') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated"
252 | .toLowerCase(); // "⚡⚡-dont-forget-url-fragments-should-be-i18n-friendly-hyphenated"
253 |
254 | return urlText;
255 | };
256 |
257 | /**
258 | * Determines if this element already has an AnchorJS link on it.
259 | * Uses this technique: http://stackoverflow.com/a/5898748/1154642
260 | * @param {HTMLElemnt} el - a DOM node
261 | * @returns {Boolean} true/false
262 | */
263 | this.hasAnchorJSLink = function(el) {
264 | var hasLeftAnchor =
265 | el.firstChild &&
266 | (' ' + el.firstChild.className + ' ').indexOf(' anchorjs-link ') > -1,
267 | hasRightAnchor =
268 | el.lastChild &&
269 | (' ' + el.lastChild.className + ' ').indexOf(' anchorjs-link ') > -1;
270 |
271 | return hasLeftAnchor || hasRightAnchor || false;
272 | };
273 |
274 | /**
275 | * Turns a selector, nodeList, or array of elements into an array of elements (so we can use array methods).
276 | * It also throws errors on any other inputs. Used to handle inputs to .add and .remove.
277 | * @param {String|Array|Nodelist} input - A CSS selector string targeting elements with anchor links,
278 | * OR a nodeList / array containing the DOM elements.
279 | * @returns {Array} - An array containing the elements we want.
280 | */
281 | function _getElements(input) {
282 | var elements;
283 | if (typeof input === 'string' || input instanceof String) {
284 | // See https://davidwalsh.name/nodelist-array for the technique transforming nodeList -> Array.
285 | elements = [].slice.call(document.querySelectorAll(input));
286 | // I checked the 'input instanceof NodeList' test in IE9 and modern browsers and it worked for me.
287 | } else if (Array.isArray(input) || input instanceof NodeList) {
288 | elements = [].slice.call(input);
289 | } else {
290 | throw new Error('The selector provided to AnchorJS was invalid.');
291 | }
292 | return elements;
293 | }
294 |
295 | /**
296 | * _addBaselineStyles
297 | * Adds baseline styles to the page, used by all AnchorJS links irregardless of configuration.
298 | */
299 | function _addBaselineStyles() {
300 | // We don't want to add global baseline styles if they've been added before.
301 | if (document.head.querySelector('style.anchorjs') !== null) {
302 | return;
303 | }
304 |
305 | var style = document.createElement('style'),
306 | linkRule =
307 | ' .anchorjs-link {' +
308 | ' opacity: 0;' +
309 | ' text-decoration: none;' +
310 | ' -webkit-font-smoothing: antialiased;' +
311 | ' -moz-osx-font-smoothing: grayscale;' +
312 | ' }',
313 | hoverRule =
314 | ' *:hover > .anchorjs-link,' +
315 | ' .anchorjs-link:focus {' +
316 | ' opacity: 1;' +
317 | ' }',
318 | anchorjsLinkFontFace =
319 | ' @font-face {' +
320 | ' font-family: "anchorjs-icons";' + // Icon from icomoon; 10px wide & 10px tall; 2 empty below & 4 above
321 | ' src: url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype");' +
322 | ' }',
323 | pseudoElContent =
324 | ' [data-anchorjs-icon]::after {' +
325 | ' content: attr(data-anchorjs-icon);' +
326 | ' }',
327 | firstStyleEl;
328 |
329 | style.className = 'anchorjs';
330 | style.appendChild(document.createTextNode('')); // Necessary for Webkit.
331 |
332 | // We place it in the head with the other style tags, if possible, so as to
333 | // not look out of place. We insert before the others so these styles can be
334 | // overridden if necessary.
335 | firstStyleEl = document.head.querySelector('[rel="stylesheet"], style');
336 | if (firstStyleEl === undefined) {
337 | document.head.appendChild(style);
338 | } else {
339 | document.head.insertBefore(style, firstStyleEl);
340 | }
341 |
342 | style.sheet.insertRule(linkRule, style.sheet.cssRules.length);
343 | style.sheet.insertRule(hoverRule, style.sheet.cssRules.length);
344 | style.sheet.insertRule(pseudoElContent, style.sheet.cssRules.length);
345 | style.sheet.insertRule(anchorjsLinkFontFace, style.sheet.cssRules.length);
346 | }
347 | }
348 |
349 | return AnchorJS;
350 | });
351 |
--------------------------------------------------------------------------------
/docs/assets/bass-addons.css:
--------------------------------------------------------------------------------
1 | .input {
2 | font-family: inherit;
3 | display: block;
4 | width: 100%;
5 | height: 2rem;
6 | padding: .5rem;
7 | margin-bottom: 1rem;
8 | border: 1px solid #ccc;
9 | font-size: .875rem;
10 | border-radius: 3px;
11 | box-sizing: border-box;
12 | }
13 |
--------------------------------------------------------------------------------
/docs/assets/bass.css:
--------------------------------------------------------------------------------
1 | /*! Basscss | http://basscss.com | MIT License */
2 |
3 | .h1{ font-size: 2rem }
4 | .h2{ font-size: 1.5rem }
5 | .h3{ font-size: 1.25rem }
6 | .h4{ font-size: 1rem }
7 | .h5{ font-size: .875rem }
8 | .h6{ font-size: .75rem }
9 |
10 | .font-family-inherit{ font-family:inherit }
11 | .font-size-inherit{ font-size:inherit }
12 | .text-decoration-none{ text-decoration:none }
13 |
14 | .bold{ font-weight: bold; font-weight: bold }
15 | .regular{ font-weight:normal }
16 | .italic{ font-style:italic }
17 | .caps{ text-transform:uppercase; letter-spacing: .2em; }
18 |
19 | .left-align{ text-align:left }
20 | .center{ text-align:center }
21 | .right-align{ text-align:right }
22 | .justify{ text-align:justify }
23 |
24 | .nowrap{ white-space:nowrap }
25 | .break-word{ word-wrap:break-word }
26 |
27 | .line-height-1{ line-height: 1 }
28 | .line-height-2{ line-height: 1.125 }
29 | .line-height-3{ line-height: 1.25 }
30 | .line-height-4{ line-height: 1.5 }
31 |
32 | .list-style-none{ list-style:none }
33 | .underline{ text-decoration:underline }
34 |
35 | .truncate{
36 | max-width:100%;
37 | overflow:hidden;
38 | text-overflow:ellipsis;
39 | white-space:nowrap;
40 | }
41 |
42 | .list-reset{
43 | list-style:none;
44 | padding-left:0;
45 | }
46 |
47 | .inline{ display:inline }
48 | .block{ display:block }
49 | .inline-block{ display:inline-block }
50 | .table{ display:table }
51 | .table-cell{ display:table-cell }
52 |
53 | .overflow-hidden{ overflow:hidden }
54 | .overflow-scroll{ overflow:scroll }
55 | .overflow-auto{ overflow:auto }
56 |
57 | .clearfix:before,
58 | .clearfix:after{
59 | content:" ";
60 | display:table
61 | }
62 | .clearfix:after{ clear:both }
63 |
64 | .left{ float:left }
65 | .right{ float:right }
66 |
67 | .fit{ max-width:100% }
68 |
69 | .max-width-1{ max-width: 24rem }
70 | .max-width-2{ max-width: 32rem }
71 | .max-width-3{ max-width: 48rem }
72 | .max-width-4{ max-width: 64rem }
73 |
74 | .border-box{ box-sizing:border-box }
75 |
76 | .align-baseline{ vertical-align:baseline }
77 | .align-top{ vertical-align:top }
78 | .align-middle{ vertical-align:middle }
79 | .align-bottom{ vertical-align:bottom }
80 |
81 | .m0{ margin:0 }
82 | .mt0{ margin-top:0 }
83 | .mr0{ margin-right:0 }
84 | .mb0{ margin-bottom:0 }
85 | .ml0{ margin-left:0 }
86 | .mx0{ margin-left:0; margin-right:0 }
87 | .my0{ margin-top:0; margin-bottom:0 }
88 |
89 | .m1{ margin: .5rem }
90 | .mt1{ margin-top: .5rem }
91 | .mr1{ margin-right: .5rem }
92 | .mb1{ margin-bottom: .5rem }
93 | .ml1{ margin-left: .5rem }
94 | .mx1{ margin-left: .5rem; margin-right: .5rem }
95 | .my1{ margin-top: .5rem; margin-bottom: .5rem }
96 |
97 | .m2{ margin: 1rem }
98 | .mt2{ margin-top: 1rem }
99 | .mr2{ margin-right: 1rem }
100 | .mb2{ margin-bottom: 1rem }
101 | .ml2{ margin-left: 1rem }
102 | .mx2{ margin-left: 1rem; margin-right: 1rem }
103 | .my2{ margin-top: 1rem; margin-bottom: 1rem }
104 |
105 | .m3{ margin: 2rem }
106 | .mt3{ margin-top: 2rem }
107 | .mr3{ margin-right: 2rem }
108 | .mb3{ margin-bottom: 2rem }
109 | .ml3{ margin-left: 2rem }
110 | .mx3{ margin-left: 2rem; margin-right: 2rem }
111 | .my3{ margin-top: 2rem; margin-bottom: 2rem }
112 |
113 | .m4{ margin: 4rem }
114 | .mt4{ margin-top: 4rem }
115 | .mr4{ margin-right: 4rem }
116 | .mb4{ margin-bottom: 4rem }
117 | .ml4{ margin-left: 4rem }
118 | .mx4{ margin-left: 4rem; margin-right: 4rem }
119 | .my4{ margin-top: 4rem; margin-bottom: 4rem }
120 |
121 | .mxn1{ margin-left: -.5rem; margin-right: -.5rem; }
122 | .mxn2{ margin-left: -1rem; margin-right: -1rem; }
123 | .mxn3{ margin-left: -2rem; margin-right: -2rem; }
124 | .mxn4{ margin-left: -4rem; margin-right: -4rem; }
125 |
126 | .ml-auto{ margin-left:auto }
127 | .mr-auto{ margin-right:auto }
128 | .mx-auto{ margin-left:auto; margin-right:auto; }
129 |
130 | .p0{ padding:0 }
131 | .pt0{ padding-top:0 }
132 | .pr0{ padding-right:0 }
133 | .pb0{ padding-bottom:0 }
134 | .pl0{ padding-left:0 }
135 | .px0{ padding-left:0; padding-right:0 }
136 | .py0{ padding-top:0; padding-bottom:0 }
137 |
138 | .p1{ padding: .5rem }
139 | .pt1{ padding-top: .5rem }
140 | .pr1{ padding-right: .5rem }
141 | .pb1{ padding-bottom: .5rem }
142 | .pl1{ padding-left: .5rem }
143 | .py1{ padding-top: .5rem; padding-bottom: .5rem }
144 | .px1{ padding-left: .5rem; padding-right: .5rem }
145 |
146 | .p2{ padding: 1rem }
147 | .pt2{ padding-top: 1rem }
148 | .pr2{ padding-right: 1rem }
149 | .pb2{ padding-bottom: 1rem }
150 | .pl2{ padding-left: 1rem }
151 | .py2{ padding-top: 1rem; padding-bottom: 1rem }
152 | .px2{ padding-left: 1rem; padding-right: 1rem }
153 |
154 | .p3{ padding: 2rem }
155 | .pt3{ padding-top: 2rem }
156 | .pr3{ padding-right: 2rem }
157 | .pb3{ padding-bottom: 2rem }
158 | .pl3{ padding-left: 2rem }
159 | .py3{ padding-top: 2rem; padding-bottom: 2rem }
160 | .px3{ padding-left: 2rem; padding-right: 2rem }
161 |
162 | .p4{ padding: 4rem }
163 | .pt4{ padding-top: 4rem }
164 | .pr4{ padding-right: 4rem }
165 | .pb4{ padding-bottom: 4rem }
166 | .pl4{ padding-left: 4rem }
167 | .py4{ padding-top: 4rem; padding-bottom: 4rem }
168 | .px4{ padding-left: 4rem; padding-right: 4rem }
169 |
170 | .col{
171 | float:left;
172 | box-sizing:border-box;
173 | }
174 |
175 | .col-right{
176 | float:right;
177 | box-sizing:border-box;
178 | }
179 |
180 | .col-1{
181 | width:8.33333%;
182 | }
183 |
184 | .col-2{
185 | width:16.66667%;
186 | }
187 |
188 | .col-3{
189 | width:25%;
190 | }
191 |
192 | .col-4{
193 | width:33.33333%;
194 | }
195 |
196 | .col-5{
197 | width:41.66667%;
198 | }
199 |
200 | .col-6{
201 | width:50%;
202 | }
203 |
204 | .col-7{
205 | width:58.33333%;
206 | }
207 |
208 | .col-8{
209 | width:66.66667%;
210 | }
211 |
212 | .col-9{
213 | width:75%;
214 | }
215 |
216 | .col-10{
217 | width:83.33333%;
218 | }
219 |
220 | .col-11{
221 | width:91.66667%;
222 | }
223 |
224 | .col-12{
225 | width:100%;
226 | }
227 | @media (min-width: 40em){
228 |
229 | .sm-col{
230 | float:left;
231 | box-sizing:border-box;
232 | }
233 |
234 | .sm-col-right{
235 | float:right;
236 | box-sizing:border-box;
237 | }
238 |
239 | .sm-col-1{
240 | width:8.33333%;
241 | }
242 |
243 | .sm-col-2{
244 | width:16.66667%;
245 | }
246 |
247 | .sm-col-3{
248 | width:25%;
249 | }
250 |
251 | .sm-col-4{
252 | width:33.33333%;
253 | }
254 |
255 | .sm-col-5{
256 | width:41.66667%;
257 | }
258 |
259 | .sm-col-6{
260 | width:50%;
261 | }
262 |
263 | .sm-col-7{
264 | width:58.33333%;
265 | }
266 |
267 | .sm-col-8{
268 | width:66.66667%;
269 | }
270 |
271 | .sm-col-9{
272 | width:75%;
273 | }
274 |
275 | .sm-col-10{
276 | width:83.33333%;
277 | }
278 |
279 | .sm-col-11{
280 | width:91.66667%;
281 | }
282 |
283 | .sm-col-12{
284 | width:100%;
285 | }
286 |
287 | }
288 | @media (min-width: 52em){
289 |
290 | .md-col{
291 | float:left;
292 | box-sizing:border-box;
293 | }
294 |
295 | .md-col-right{
296 | float:right;
297 | box-sizing:border-box;
298 | }
299 |
300 | .md-col-1{
301 | width:8.33333%;
302 | }
303 |
304 | .md-col-2{
305 | width:16.66667%;
306 | }
307 |
308 | .md-col-3{
309 | width:25%;
310 | }
311 |
312 | .md-col-4{
313 | width:33.33333%;
314 | }
315 |
316 | .md-col-5{
317 | width:41.66667%;
318 | }
319 |
320 | .md-col-6{
321 | width:50%;
322 | }
323 |
324 | .md-col-7{
325 | width:58.33333%;
326 | }
327 |
328 | .md-col-8{
329 | width:66.66667%;
330 | }
331 |
332 | .md-col-9{
333 | width:75%;
334 | }
335 |
336 | .md-col-10{
337 | width:83.33333%;
338 | }
339 |
340 | .md-col-11{
341 | width:91.66667%;
342 | }
343 |
344 | .md-col-12{
345 | width:100%;
346 | }
347 |
348 | }
349 | @media (min-width: 64em){
350 |
351 | .lg-col{
352 | float:left;
353 | box-sizing:border-box;
354 | }
355 |
356 | .lg-col-right{
357 | float:right;
358 | box-sizing:border-box;
359 | }
360 |
361 | .lg-col-1{
362 | width:8.33333%;
363 | }
364 |
365 | .lg-col-2{
366 | width:16.66667%;
367 | }
368 |
369 | .lg-col-3{
370 | width:25%;
371 | }
372 |
373 | .lg-col-4{
374 | width:33.33333%;
375 | }
376 |
377 | .lg-col-5{
378 | width:41.66667%;
379 | }
380 |
381 | .lg-col-6{
382 | width:50%;
383 | }
384 |
385 | .lg-col-7{
386 | width:58.33333%;
387 | }
388 |
389 | .lg-col-8{
390 | width:66.66667%;
391 | }
392 |
393 | .lg-col-9{
394 | width:75%;
395 | }
396 |
397 | .lg-col-10{
398 | width:83.33333%;
399 | }
400 |
401 | .lg-col-11{
402 | width:91.66667%;
403 | }
404 |
405 | .lg-col-12{
406 | width:100%;
407 | }
408 |
409 | }
410 | .flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
411 |
412 | @media (min-width: 40em){
413 | .sm-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
414 | }
415 |
416 | @media (min-width: 52em){
417 | .md-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
418 | }
419 |
420 | @media (min-width: 64em){
421 | .lg-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
422 | }
423 |
424 | .flex-column{ -webkit-box-orient:vertical; -webkit-box-direction:normal; -webkit-flex-direction:column; -ms-flex-direction:column; flex-direction:column }
425 | .flex-wrap{ -webkit-flex-wrap:wrap; -ms-flex-wrap:wrap; flex-wrap:wrap }
426 |
427 | .items-start{ -webkit-box-align:start; -webkit-align-items:flex-start; -ms-flex-align:start; -ms-grid-row-align:flex-start; align-items:flex-start }
428 | .items-end{ -webkit-box-align:end; -webkit-align-items:flex-end; -ms-flex-align:end; -ms-grid-row-align:flex-end; align-items:flex-end }
429 | .items-center{ -webkit-box-align:center; -webkit-align-items:center; -ms-flex-align:center; -ms-grid-row-align:center; align-items:center }
430 | .items-baseline{ -webkit-box-align:baseline; -webkit-align-items:baseline; -ms-flex-align:baseline; -ms-grid-row-align:baseline; align-items:baseline }
431 | .items-stretch{ -webkit-box-align:stretch; -webkit-align-items:stretch; -ms-flex-align:stretch; -ms-grid-row-align:stretch; align-items:stretch }
432 |
433 | .self-start{ -webkit-align-self:flex-start; -ms-flex-item-align:start; align-self:flex-start }
434 | .self-end{ -webkit-align-self:flex-end; -ms-flex-item-align:end; align-self:flex-end }
435 | .self-center{ -webkit-align-self:center; -ms-flex-item-align:center; align-self:center }
436 | .self-baseline{ -webkit-align-self:baseline; -ms-flex-item-align:baseline; align-self:baseline }
437 | .self-stretch{ -webkit-align-self:stretch; -ms-flex-item-align:stretch; align-self:stretch }
438 |
439 | .justify-start{ -webkit-box-pack:start; -webkit-justify-content:flex-start; -ms-flex-pack:start; justify-content:flex-start }
440 | .justify-end{ -webkit-box-pack:end; -webkit-justify-content:flex-end; -ms-flex-pack:end; justify-content:flex-end }
441 | .justify-center{ -webkit-box-pack:center; -webkit-justify-content:center; -ms-flex-pack:center; justify-content:center }
442 | .justify-between{ -webkit-box-pack:justify; -webkit-justify-content:space-between; -ms-flex-pack:justify; justify-content:space-between }
443 | .justify-around{ -webkit-justify-content:space-around; -ms-flex-pack:distribute; justify-content:space-around }
444 |
445 | .content-start{ -webkit-align-content:flex-start; -ms-flex-line-pack:start; align-content:flex-start }
446 | .content-end{ -webkit-align-content:flex-end; -ms-flex-line-pack:end; align-content:flex-end }
447 | .content-center{ -webkit-align-content:center; -ms-flex-line-pack:center; align-content:center }
448 | .content-between{ -webkit-align-content:space-between; -ms-flex-line-pack:justify; align-content:space-between }
449 | .content-around{ -webkit-align-content:space-around; -ms-flex-line-pack:distribute; align-content:space-around }
450 | .content-stretch{ -webkit-align-content:stretch; -ms-flex-line-pack:stretch; align-content:stretch }
451 | .flex-auto{
452 | -webkit-box-flex:1;
453 | -webkit-flex:1 1 auto;
454 | -ms-flex:1 1 auto;
455 | flex:1 1 auto;
456 | min-width:0;
457 | min-height:0;
458 | }
459 | .flex-none{ -webkit-box-flex:0; -webkit-flex:none; -ms-flex:none; flex:none }
460 | .fs0{ flex-shrink: 0 }
461 |
462 | .order-0{ -webkit-box-ordinal-group:1; -webkit-order:0; -ms-flex-order:0; order:0 }
463 | .order-1{ -webkit-box-ordinal-group:2; -webkit-order:1; -ms-flex-order:1; order:1 }
464 | .order-2{ -webkit-box-ordinal-group:3; -webkit-order:2; -ms-flex-order:2; order:2 }
465 | .order-3{ -webkit-box-ordinal-group:4; -webkit-order:3; -ms-flex-order:3; order:3 }
466 | .order-last{ -webkit-box-ordinal-group:100000; -webkit-order:99999; -ms-flex-order:99999; order:99999 }
467 |
468 | .relative{ position:relative }
469 | .absolute{ position:absolute }
470 | .fixed{ position:fixed }
471 |
472 | .top-0{ top:0 }
473 | .right-0{ right:0 }
474 | .bottom-0{ bottom:0 }
475 | .left-0{ left:0 }
476 |
477 | .z1{ z-index: 1 }
478 | .z2{ z-index: 2 }
479 | .z3{ z-index: 3 }
480 | .z4{ z-index: 4 }
481 |
482 | .border{
483 | border-style:solid;
484 | border-width: 1px;
485 | }
486 |
487 | .border-top{
488 | border-top-style:solid;
489 | border-top-width: 1px;
490 | }
491 |
492 | .border-right{
493 | border-right-style:solid;
494 | border-right-width: 1px;
495 | }
496 |
497 | .border-bottom{
498 | border-bottom-style:solid;
499 | border-bottom-width: 1px;
500 | }
501 |
502 | .border-left{
503 | border-left-style:solid;
504 | border-left-width: 1px;
505 | }
506 |
507 | .border-none{ border:0 }
508 |
509 | .rounded{ border-radius: 3px }
510 | .circle{ border-radius:50% }
511 |
512 | .rounded-top{ border-radius: 3px 3px 0 0 }
513 | .rounded-right{ border-radius: 0 3px 3px 0 }
514 | .rounded-bottom{ border-radius: 0 0 3px 3px }
515 | .rounded-left{ border-radius: 3px 0 0 3px }
516 |
517 | .not-rounded{ border-radius:0 }
518 |
519 | .hide{
520 | position:absolute !important;
521 | height:1px;
522 | width:1px;
523 | overflow:hidden;
524 | clip:rect(1px, 1px, 1px, 1px);
525 | }
526 |
527 | @media (max-width: 40em){
528 | .xs-hide{ display:none !important }
529 | }
530 |
531 | @media (min-width: 40em) and (max-width: 52em){
532 | .sm-hide{ display:none !important }
533 | }
534 |
535 | @media (min-width: 52em) and (max-width: 64em){
536 | .md-hide{ display:none !important }
537 | }
538 |
539 | @media (min-width: 64em){
540 | .lg-hide{ display:none !important }
541 | }
542 |
543 | .display-none{ display:none !important }
544 |
545 |
--------------------------------------------------------------------------------
/docs/assets/fonts/EOT/SourceCodePro-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/EOT/SourceCodePro-Bold.eot
--------------------------------------------------------------------------------
/docs/assets/fonts/EOT/SourceCodePro-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/EOT/SourceCodePro-Regular.eot
--------------------------------------------------------------------------------
/docs/assets/fonts/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 |
5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/docs/assets/fonts/OTF/SourceCodePro-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/OTF/SourceCodePro-Bold.otf
--------------------------------------------------------------------------------
/docs/assets/fonts/OTF/SourceCodePro-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/OTF/SourceCodePro-Regular.otf
--------------------------------------------------------------------------------
/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/source-code-pro.css:
--------------------------------------------------------------------------------
1 | @font-face{
2 | font-family: 'Source Code Pro';
3 | font-weight: 400;
4 | font-style: normal;
5 | font-stretch: normal;
6 | src: url('EOT/SourceCodePro-Regular.eot') format('embedded-opentype'),
7 | url('WOFF2/TTF/SourceCodePro-Regular.ttf.woff2') format('woff2'),
8 | url('WOFF/OTF/SourceCodePro-Regular.otf.woff') format('woff'),
9 | url('OTF/SourceCodePro-Regular.otf') format('opentype'),
10 | url('TTF/SourceCodePro-Regular.ttf') format('truetype');
11 | }
12 |
13 | @font-face{
14 | font-family: 'Source Code Pro';
15 | font-weight: 700;
16 | font-style: normal;
17 | font-stretch: normal;
18 | src: url('EOT/SourceCodePro-Bold.eot') format('embedded-opentype'),
19 | url('WOFF2/TTF/SourceCodePro-Bold.ttf.woff2') format('woff2'),
20 | url('WOFF/OTF/SourceCodePro-Bold.otf.woff') format('woff'),
21 | url('OTF/SourceCodePro-Bold.otf') format('opentype'),
22 | url('TTF/SourceCodePro-Bold.ttf') format('truetype');
23 | }
24 |
--------------------------------------------------------------------------------
/docs/assets/github.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | github.com style (c) Vasily Polovnyov
4 |
5 | */
6 |
7 | .hljs {
8 | display: block;
9 | overflow-x: auto;
10 | padding: 0.5em;
11 | color: #333;
12 | background: #f8f8f8;
13 | -webkit-text-size-adjust: none;
14 | }
15 |
16 | .hljs-comment,
17 | .diff .hljs-header,
18 | .hljs-javadoc {
19 | color: #998;
20 | font-style: italic;
21 | }
22 |
23 | .hljs-keyword,
24 | .css .rule .hljs-keyword,
25 | .hljs-winutils,
26 | .nginx .hljs-title,
27 | .hljs-subst,
28 | .hljs-request,
29 | .hljs-status {
30 | color: #1184CE;
31 | }
32 |
33 | .hljs-number,
34 | .hljs-hexcolor,
35 | .ruby .hljs-constant {
36 | color: #ed225d;
37 | }
38 |
39 | .hljs-string,
40 | .hljs-tag .hljs-value,
41 | .hljs-phpdoc,
42 | .hljs-dartdoc,
43 | .tex .hljs-formula {
44 | color: #ed225d;
45 | }
46 |
47 | .hljs-title,
48 | .hljs-id,
49 | .scss .hljs-preprocessor {
50 | color: #900;
51 | font-weight: bold;
52 | }
53 |
54 | .hljs-list .hljs-keyword,
55 | .hljs-subst {
56 | font-weight: normal;
57 | }
58 |
59 | .hljs-class .hljs-title,
60 | .hljs-type,
61 | .vhdl .hljs-literal,
62 | .tex .hljs-command {
63 | color: #458;
64 | font-weight: bold;
65 | }
66 |
67 | .hljs-tag,
68 | .hljs-tag .hljs-title,
69 | .hljs-rules .hljs-property,
70 | .django .hljs-tag .hljs-keyword {
71 | color: #000080;
72 | font-weight: normal;
73 | }
74 |
75 | .hljs-attribute,
76 | .hljs-variable,
77 | .lisp .hljs-body {
78 | color: #008080;
79 | }
80 |
81 | .hljs-regexp {
82 | color: #009926;
83 | }
84 |
85 | .hljs-symbol,
86 | .ruby .hljs-symbol .hljs-string,
87 | .lisp .hljs-keyword,
88 | .clojure .hljs-keyword,
89 | .scheme .hljs-keyword,
90 | .tex .hljs-special,
91 | .hljs-prompt {
92 | color: #990073;
93 | }
94 |
95 | .hljs-built_in {
96 | color: #0086b3;
97 | }
98 |
99 | .hljs-preprocessor,
100 | .hljs-pragma,
101 | .hljs-pi,
102 | .hljs-doctype,
103 | .hljs-shebang,
104 | .hljs-cdata {
105 | color: #999;
106 | font-weight: bold;
107 | }
108 |
109 | .hljs-deletion {
110 | background: #fdd;
111 | }
112 |
113 | .hljs-addition {
114 | background: #dfd;
115 | }
116 |
117 | .diff .hljs-change {
118 | background: #0086b3;
119 | }
120 |
121 | .hljs-chunk {
122 | color: #aaa;
123 | }
124 |
--------------------------------------------------------------------------------
/docs/assets/site.js:
--------------------------------------------------------------------------------
1 | /* global anchors */
2 |
3 | // add anchor links to headers
4 | anchors.options.placement = 'left';
5 | anchors.add('h3');
6 |
7 | // Filter UI
8 | var tocElements = document.getElementById('toc').getElementsByTagName('li');
9 |
10 | document.getElementById('filter-input').addEventListener('keyup', function(e) {
11 | var i, element, children;
12 |
13 | // enter key
14 | if (e.keyCode === 13) {
15 | // go to the first displayed item in the toc
16 | for (i = 0; i < tocElements.length; i++) {
17 | element = tocElements[i];
18 | if (!element.classList.contains('display-none')) {
19 | location.replace(element.firstChild.href);
20 | return e.preventDefault();
21 | }
22 | }
23 | }
24 |
25 | var match = function() {
26 | return true;
27 | };
28 |
29 | var value = this.value.toLowerCase();
30 |
31 | if (!value.match(/^\s*$/)) {
32 | match = function(element) {
33 | var html = element.firstChild.innerHTML;
34 | return html && html.toLowerCase().indexOf(value) !== -1;
35 | };
36 | }
37 |
38 | for (i = 0; i < tocElements.length; i++) {
39 | element = tocElements[i];
40 | children = Array.from(element.getElementsByTagName('li'));
41 | if (match(element) || children.some(match)) {
42 | element.classList.remove('display-none');
43 | } else {
44 | element.classList.add('display-none');
45 | }
46 | }
47 | });
48 |
49 | var items = document.getElementsByClassName('toggle-sibling');
50 | for (var j = 0; j < items.length; j++) {
51 | items[j].addEventListener('click', toggleSibling);
52 | }
53 |
54 | function toggleSibling() {
55 | var stepSibling = this.parentNode.getElementsByClassName('toggle-target')[0];
56 | var icon = this.getElementsByClassName('icon')[0];
57 | var klass = 'display-none';
58 | if (stepSibling.classList.contains(klass)) {
59 | stepSibling.classList.remove(klass);
60 | icon.innerHTML = '▾';
61 | } else {
62 | stepSibling.classList.add(klass);
63 | icon.innerHTML = '▸';
64 | }
65 | }
66 |
67 | function showHashTarget(targetId) {
68 | if (targetId) {
69 | var hashTarget = document.getElementById(targetId);
70 | // new target is hidden
71 | if (
72 | hashTarget &&
73 | hashTarget.offsetHeight === 0 &&
74 | hashTarget.parentNode.parentNode.classList.contains('display-none')
75 | ) {
76 | hashTarget.parentNode.parentNode.classList.remove('display-none');
77 | }
78 | }
79 | }
80 |
81 | function scrollIntoView(targetId) {
82 | // Only scroll to element if we don't have a stored scroll position.
83 | if (targetId && !history.state) {
84 | var hashTarget = document.getElementById(targetId);
85 | if (hashTarget) {
86 | hashTarget.scrollIntoView();
87 | }
88 | }
89 | }
90 |
91 | function gotoCurrentTarget() {
92 | showHashTarget(location.hash.substring(1));
93 | scrollIntoView(location.hash.substring(1));
94 | }
95 |
96 | window.addEventListener('hashchange', gotoCurrentTarget);
97 | gotoCurrentTarget();
98 |
99 | var toclinks = document.getElementsByClassName('pre-open');
100 | for (var k = 0; k < toclinks.length; k++) {
101 | toclinks[k].addEventListener('mousedown', preOpen, false);
102 | }
103 |
104 | function preOpen() {
105 | showHashTarget(this.hash.substring(1));
106 | }
107 |
108 | var split_left = document.querySelector('#split-left');
109 | var split_right = document.querySelector('#split-right');
110 | var split_parent = split_left.parentNode;
111 | var cw_with_sb = split_left.clientWidth;
112 | split_left.style.overflow = 'hidden';
113 | var cw_without_sb = split_left.clientWidth;
114 | split_left.style.overflow = '';
115 |
116 | Split(['#split-left', '#split-right'], {
117 | elementStyle: function(dimension, size, gutterSize) {
118 | return {
119 | 'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
120 | };
121 | },
122 | gutterStyle: function(dimension, gutterSize) {
123 | return {
124 | 'flex-basis': gutterSize + 'px'
125 | };
126 | },
127 | gutterSize: 20,
128 | sizes: [33, 67]
129 | });
130 |
131 | // Chrome doesn't remember scroll position properly so do it ourselves.
132 | // Also works on Firefox and Edge.
133 |
134 | function updateState() {
135 | history.replaceState(
136 | {
137 | left_top: split_left.scrollTop,
138 | right_top: split_right.scrollTop
139 | },
140 | document.title
141 | );
142 | }
143 |
144 | function loadState(ev) {
145 | if (ev) {
146 | // Edge doesn't replace change history.state on popstate.
147 | history.replaceState(ev.state, document.title);
148 | }
149 | if (history.state) {
150 | split_left.scrollTop = history.state.left_top;
151 | split_right.scrollTop = history.state.right_top;
152 | }
153 | }
154 |
155 | window.addEventListener('load', function() {
156 | // Restore after Firefox scrolls to hash.
157 | setTimeout(function() {
158 | loadState();
159 | // Update with initial scroll position.
160 | updateState();
161 | // Update scroll positions only after we've loaded because Firefox
162 | // emits an initial scroll event with 0.
163 | split_left.addEventListener('scroll', updateState);
164 | split_right.addEventListener('scroll', updateState);
165 | }, 1);
166 | });
167 |
168 | window.addEventListener('popstate', loadState);
169 |
--------------------------------------------------------------------------------
/docs/assets/split.css:
--------------------------------------------------------------------------------
1 | .gutter {
2 | background-color: #f5f5f5;
3 | background-repeat: no-repeat;
4 | background-position: 50%;
5 | }
6 |
7 | .gutter.gutter-vertical {
8 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
9 | cursor: ns-resize;
10 | }
11 |
12 | .gutter.gutter-horizontal {
13 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
14 | cursor: ew-resize;
15 | }
16 |
--------------------------------------------------------------------------------
/docs/assets/split.js:
--------------------------------------------------------------------------------
1 | /*! Split.js - v1.3.5 */
2 | // https://github.com/nathancahill/Split.js
3 | // Copyright (c) 2017 Nathan Cahill; Licensed MIT
4 |
5 | (function(global, factory) {
6 | typeof exports === 'object' && typeof module !== 'undefined'
7 | ? (module.exports = factory())
8 | : typeof define === 'function' && define.amd
9 | ? define(factory)
10 | : (global.Split = factory());
11 | })(this, function() {
12 | 'use strict';
13 | // The programming goals of Split.js are to deliver readable, understandable and
14 | // maintainable code, while at the same time manually optimizing for tiny minified file size,
15 | // browser compatibility without additional requirements, graceful fallback (IE8 is supported)
16 | // and very few assumptions about the user's page layout.
17 | var global = window;
18 | var document = global.document;
19 |
20 | // Save a couple long function names that are used frequently.
21 | // This optimization saves around 400 bytes.
22 | var addEventListener = 'addEventListener';
23 | var removeEventListener = 'removeEventListener';
24 | var getBoundingClientRect = 'getBoundingClientRect';
25 | var NOOP = function() {
26 | return false;
27 | };
28 |
29 | // Figure out if we're in IE8 or not. IE8 will still render correctly,
30 | // but will be static instead of draggable.
31 | var isIE8 = global.attachEvent && !global[addEventListener];
32 |
33 | // This library only needs two helper functions:
34 | //
35 | // The first determines which prefixes of CSS calc we need.
36 | // We only need to do this once on startup, when this anonymous function is called.
37 | //
38 | // Tests -webkit, -moz and -o prefixes. Modified from StackOverflow:
39 | // http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167
40 | var calc =
41 | ['', '-webkit-', '-moz-', '-o-']
42 | .filter(function(prefix) {
43 | var el = document.createElement('div');
44 | el.style.cssText = 'width:' + prefix + 'calc(9px)';
45 |
46 | return !!el.style.length;
47 | })
48 | .shift() + 'calc';
49 |
50 | // The second helper function allows elements and string selectors to be used
51 | // interchangeably. In either case an element is returned. This allows us to
52 | // do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`.
53 | var elementOrSelector = function(el) {
54 | if (typeof el === 'string' || el instanceof String) {
55 | return document.querySelector(el);
56 | }
57 |
58 | return el;
59 | };
60 |
61 | // The main function to initialize a split. Split.js thinks about each pair
62 | // of elements as an independant pair. Dragging the gutter between two elements
63 | // only changes the dimensions of elements in that pair. This is key to understanding
64 | // how the following functions operate, since each function is bound to a pair.
65 | //
66 | // A pair object is shaped like this:
67 | //
68 | // {
69 | // a: DOM element,
70 | // b: DOM element,
71 | // aMin: Number,
72 | // bMin: Number,
73 | // dragging: Boolean,
74 | // parent: DOM element,
75 | // isFirst: Boolean,
76 | // isLast: Boolean,
77 | // direction: 'horizontal' | 'vertical'
78 | // }
79 | //
80 | // The basic sequence:
81 | //
82 | // 1. Set defaults to something sane. `options` doesn't have to be passed at all.
83 | // 2. Initialize a bunch of strings based on the direction we're splitting.
84 | // A lot of the behavior in the rest of the library is paramatized down to
85 | // rely on CSS strings and classes.
86 | // 3. Define the dragging helper functions, and a few helpers to go with them.
87 | // 4. Loop through the elements while pairing them off. Every pair gets an
88 | // `pair` object, a gutter, and special isFirst/isLast properties.
89 | // 5. Actually size the pair elements, insert gutters and attach event listeners.
90 | var Split = function(ids, options) {
91 | if (options === void 0) options = {};
92 |
93 | var dimension;
94 | var clientDimension;
95 | var clientAxis;
96 | var position;
97 | var paddingA;
98 | var paddingB;
99 | var elements;
100 |
101 | // All DOM elements in the split should have a common parent. We can grab
102 | // the first elements parent and hope users read the docs because the
103 | // behavior will be whacky otherwise.
104 | var parent = elementOrSelector(ids[0]).parentNode;
105 | var parentFlexDirection = global.getComputedStyle(parent).flexDirection;
106 |
107 | // Set default options.sizes to equal percentages of the parent element.
108 | var sizes =
109 | options.sizes ||
110 | ids.map(function() {
111 | return 100 / ids.length;
112 | });
113 |
114 | // Standardize minSize to an array if it isn't already. This allows minSize
115 | // to be passed as a number.
116 | var minSize = options.minSize !== undefined ? options.minSize : 100;
117 | var minSizes = Array.isArray(minSize)
118 | ? minSize
119 | : ids.map(function() {
120 | return minSize;
121 | });
122 | var gutterSize = options.gutterSize !== undefined ? options.gutterSize : 10;
123 | var snapOffset = options.snapOffset !== undefined ? options.snapOffset : 30;
124 | var direction = options.direction || 'horizontal';
125 | var cursor =
126 | options.cursor ||
127 | (direction === 'horizontal' ? 'ew-resize' : 'ns-resize');
128 | var gutter =
129 | options.gutter ||
130 | function(i, gutterDirection) {
131 | var gut = document.createElement('div');
132 | gut.className = 'gutter gutter-' + gutterDirection;
133 | return gut;
134 | };
135 | var elementStyle =
136 | options.elementStyle ||
137 | function(dim, size, gutSize) {
138 | var style = {};
139 |
140 | if (typeof size !== 'string' && !(size instanceof String)) {
141 | if (!isIE8) {
142 | style[dim] = calc + '(' + size + '% - ' + gutSize + 'px)';
143 | } else {
144 | style[dim] = size + '%';
145 | }
146 | } else {
147 | style[dim] = size;
148 | }
149 |
150 | return style;
151 | };
152 | var gutterStyle =
153 | options.gutterStyle ||
154 | function(dim, gutSize) {
155 | return (obj = {}), (obj[dim] = gutSize + 'px'), obj;
156 | var obj;
157 | };
158 |
159 | // 2. Initialize a bunch of strings based on the direction we're splitting.
160 | // A lot of the behavior in the rest of the library is paramatized down to
161 | // rely on CSS strings and classes.
162 | if (direction === 'horizontal') {
163 | dimension = 'width';
164 | clientDimension = 'clientWidth';
165 | clientAxis = 'clientX';
166 | position = 'left';
167 | paddingA = 'paddingLeft';
168 | paddingB = 'paddingRight';
169 | } else if (direction === 'vertical') {
170 | dimension = 'height';
171 | clientDimension = 'clientHeight';
172 | clientAxis = 'clientY';
173 | position = 'top';
174 | paddingA = 'paddingTop';
175 | paddingB = 'paddingBottom';
176 | }
177 |
178 | // 3. Define the dragging helper functions, and a few helpers to go with them.
179 | // Each helper is bound to a pair object that contains it's metadata. This
180 | // also makes it easy to store references to listeners that that will be
181 | // added and removed.
182 | //
183 | // Even though there are no other functions contained in them, aliasing
184 | // this to self saves 50 bytes or so since it's used so frequently.
185 | //
186 | // The pair object saves metadata like dragging state, position and
187 | // event listener references.
188 |
189 | function setElementSize(el, size, gutSize) {
190 | // Split.js allows setting sizes via numbers (ideally), or if you must,
191 | // by string, like '300px'. This is less than ideal, because it breaks
192 | // the fluid layout that `calc(% - px)` provides. You're on your own if you do that,
193 | // make sure you calculate the gutter size by hand.
194 | var style = elementStyle(dimension, size, gutSize);
195 |
196 | // eslint-disable-next-line no-param-reassign
197 | Object.keys(style).forEach(function(prop) {
198 | return (el.style[prop] = style[prop]);
199 | });
200 | }
201 |
202 | function setGutterSize(gutterElement, gutSize) {
203 | var style = gutterStyle(dimension, gutSize);
204 |
205 | // eslint-disable-next-line no-param-reassign
206 | Object.keys(style).forEach(function(prop) {
207 | return (gutterElement.style[prop] = style[prop]);
208 | });
209 | }
210 |
211 | // Actually adjust the size of elements `a` and `b` to `offset` while dragging.
212 | // calc is used to allow calc(percentage + gutterpx) on the whole split instance,
213 | // which allows the viewport to be resized without additional logic.
214 | // Element a's size is the same as offset. b's size is total size - a size.
215 | // Both sizes are calculated from the initial parent percentage,
216 | // then the gutter size is subtracted.
217 | function adjust(offset) {
218 | var a = elements[this.a];
219 | var b = elements[this.b];
220 | var percentage = a.size + b.size;
221 |
222 | a.size = offset / this.size * percentage;
223 | b.size = percentage - offset / this.size * percentage;
224 |
225 | setElementSize(a.element, a.size, this.aGutterSize);
226 | setElementSize(b.element, b.size, this.bGutterSize);
227 | }
228 |
229 | // drag, where all the magic happens. The logic is really quite simple:
230 | //
231 | // 1. Ignore if the pair is not dragging.
232 | // 2. Get the offset of the event.
233 | // 3. Snap offset to min if within snappable range (within min + snapOffset).
234 | // 4. Actually adjust each element in the pair to offset.
235 | //
236 | // ---------------------------------------------------------------------
237 | // | | <- a.minSize || b.minSize -> | |
238 | // | | | <- this.snapOffset || this.snapOffset -> | | |
239 | // | | | || | | |
240 | // | | | || | | |
241 | // ---------------------------------------------------------------------
242 | // | <- this.start this.size -> |
243 | function drag(e) {
244 | var offset;
245 |
246 | if (!this.dragging) {
247 | return;
248 | }
249 |
250 | // Get the offset of the event from the first side of the
251 | // pair `this.start`. Supports touch events, but not multitouch, so only the first
252 | // finger `touches[0]` is counted.
253 | if ('touches' in e) {
254 | offset = e.touches[0][clientAxis] - this.start;
255 | } else {
256 | offset = e[clientAxis] - this.start;
257 | }
258 |
259 | // If within snapOffset of min or max, set offset to min or max.
260 | // snapOffset buffers a.minSize and b.minSize, so logic is opposite for both.
261 | // Include the appropriate gutter sizes to prevent overflows.
262 | if (offset <= elements[this.a].minSize + snapOffset + this.aGutterSize) {
263 | offset = elements[this.a].minSize + this.aGutterSize;
264 | } else if (
265 | offset >=
266 | this.size - (elements[this.b].minSize + snapOffset + this.bGutterSize)
267 | ) {
268 | offset = this.size - (elements[this.b].minSize + this.bGutterSize);
269 | }
270 |
271 | // Actually adjust the size.
272 | adjust.call(this, offset);
273 |
274 | // Call the drag callback continously. Don't do anything too intensive
275 | // in this callback.
276 | if (options.onDrag) {
277 | options.onDrag();
278 | }
279 | }
280 |
281 | // Cache some important sizes when drag starts, so we don't have to do that
282 | // continously:
283 | //
284 | // `size`: The total size of the pair. First + second + first gutter + second gutter.
285 | // `start`: The leading side of the first element.
286 | //
287 | // ------------------------------------------------
288 | // | aGutterSize -> ||| |
289 | // | ||| |
290 | // | ||| |
291 | // | ||| <- bGutterSize |
292 | // ------------------------------------------------
293 | // | <- start size -> |
294 | function calculateSizes() {
295 | // Figure out the parent size minus padding.
296 | var a = elements[this.a].element;
297 | var b = elements[this.b].element;
298 |
299 | this.size =
300 | a[getBoundingClientRect]()[dimension] +
301 | b[getBoundingClientRect]()[dimension] +
302 | this.aGutterSize +
303 | this.bGutterSize;
304 | this.start = a[getBoundingClientRect]()[position];
305 | }
306 |
307 | // stopDragging is very similar to startDragging in reverse.
308 | function stopDragging() {
309 | var self = this;
310 | var a = elements[self.a].element;
311 | var b = elements[self.b].element;
312 |
313 | if (self.dragging && options.onDragEnd) {
314 | options.onDragEnd();
315 | }
316 |
317 | self.dragging = false;
318 |
319 | // Remove the stored event listeners. This is why we store them.
320 | global[removeEventListener]('mouseup', self.stop);
321 | global[removeEventListener]('touchend', self.stop);
322 | global[removeEventListener]('touchcancel', self.stop);
323 |
324 | self.parent[removeEventListener]('mousemove', self.move);
325 | self.parent[removeEventListener]('touchmove', self.move);
326 |
327 | // Delete them once they are removed. I think this makes a difference
328 | // in memory usage with a lot of splits on one page. But I don't know for sure.
329 | delete self.stop;
330 | delete self.move;
331 |
332 | a[removeEventListener]('selectstart', NOOP);
333 | a[removeEventListener]('dragstart', NOOP);
334 | b[removeEventListener]('selectstart', NOOP);
335 | b[removeEventListener]('dragstart', NOOP);
336 |
337 | a.style.userSelect = '';
338 | a.style.webkitUserSelect = '';
339 | a.style.MozUserSelect = '';
340 | a.style.pointerEvents = '';
341 |
342 | b.style.userSelect = '';
343 | b.style.webkitUserSelect = '';
344 | b.style.MozUserSelect = '';
345 | b.style.pointerEvents = '';
346 |
347 | self.gutter.style.cursor = '';
348 | self.parent.style.cursor = '';
349 | }
350 |
351 | // startDragging calls `calculateSizes` to store the inital size in the pair object.
352 | // It also adds event listeners for mouse/touch events,
353 | // and prevents selection while dragging so avoid the selecting text.
354 | function startDragging(e) {
355 | // Alias frequently used variables to save space. 200 bytes.
356 | var self = this;
357 | var a = elements[self.a].element;
358 | var b = elements[self.b].element;
359 |
360 | // Call the onDragStart callback.
361 | if (!self.dragging && options.onDragStart) {
362 | options.onDragStart();
363 | }
364 |
365 | // Don't actually drag the element. We emulate that in the drag function.
366 | e.preventDefault();
367 |
368 | // Set the dragging property of the pair object.
369 | self.dragging = true;
370 |
371 | // Create two event listeners bound to the same pair object and store
372 | // them in the pair object.
373 | self.move = drag.bind(self);
374 | self.stop = stopDragging.bind(self);
375 |
376 | // All the binding. `window` gets the stop events in case we drag out of the elements.
377 | global[addEventListener]('mouseup', self.stop);
378 | global[addEventListener]('touchend', self.stop);
379 | global[addEventListener]('touchcancel', self.stop);
380 |
381 | self.parent[addEventListener]('mousemove', self.move);
382 | self.parent[addEventListener]('touchmove', self.move);
383 |
384 | // Disable selection. Disable!
385 | a[addEventListener]('selectstart', NOOP);
386 | a[addEventListener]('dragstart', NOOP);
387 | b[addEventListener]('selectstart', NOOP);
388 | b[addEventListener]('dragstart', NOOP);
389 |
390 | a.style.userSelect = 'none';
391 | a.style.webkitUserSelect = 'none';
392 | a.style.MozUserSelect = 'none';
393 | a.style.pointerEvents = 'none';
394 |
395 | b.style.userSelect = 'none';
396 | b.style.webkitUserSelect = 'none';
397 | b.style.MozUserSelect = 'none';
398 | b.style.pointerEvents = 'none';
399 |
400 | // Set the cursor, both on the gutter and the parent element.
401 | // Doing only a, b and gutter causes flickering.
402 | self.gutter.style.cursor = cursor;
403 | self.parent.style.cursor = cursor;
404 |
405 | // Cache the initial sizes of the pair.
406 | calculateSizes.call(self);
407 | }
408 |
409 | // 5. Create pair and element objects. Each pair has an index reference to
410 | // elements `a` and `b` of the pair (first and second elements).
411 | // Loop through the elements while pairing them off. Every pair gets a
412 | // `pair` object, a gutter, and isFirst/isLast properties.
413 | //
414 | // Basic logic:
415 | //
416 | // - Starting with the second element `i > 0`, create `pair` objects with
417 | // `a = i - 1` and `b = i`
418 | // - Set gutter sizes based on the _pair_ being first/last. The first and last
419 | // pair have gutterSize / 2, since they only have one half gutter, and not two.
420 | // - Create gutter elements and add event listeners.
421 | // - Set the size of the elements, minus the gutter sizes.
422 | //
423 | // -----------------------------------------------------------------------
424 | // | i=0 | i=1 | i=2 | i=3 |
425 | // | | isFirst | | isLast |
426 | // | pair 0 pair 1 pair 2 |
427 | // | | | | |
428 | // -----------------------------------------------------------------------
429 | var pairs = [];
430 | elements = ids.map(function(id, i) {
431 | // Create the element object.
432 | var element = {
433 | element: elementOrSelector(id),
434 | size: sizes[i],
435 | minSize: minSizes[i]
436 | };
437 |
438 | var pair;
439 |
440 | if (i > 0) {
441 | // Create the pair object with it's metadata.
442 | pair = {
443 | a: i - 1,
444 | b: i,
445 | dragging: false,
446 | isFirst: i === 1,
447 | isLast: i === ids.length - 1,
448 | direction: direction,
449 | parent: parent
450 | };
451 |
452 | // For first and last pairs, first and last gutter width is half.
453 | pair.aGutterSize = gutterSize;
454 | pair.bGutterSize = gutterSize;
455 |
456 | if (pair.isFirst) {
457 | pair.aGutterSize = gutterSize / 2;
458 | }
459 |
460 | if (pair.isLast) {
461 | pair.bGutterSize = gutterSize / 2;
462 | }
463 |
464 | // if the parent has a reverse flex-direction, switch the pair elements.
465 | if (
466 | parentFlexDirection === 'row-reverse' ||
467 | parentFlexDirection === 'column-reverse'
468 | ) {
469 | var temp = pair.a;
470 | pair.a = pair.b;
471 | pair.b = temp;
472 | }
473 | }
474 |
475 | // Determine the size of the current element. IE8 is supported by
476 | // staticly assigning sizes without draggable gutters. Assigns a string
477 | // to `size`.
478 | //
479 | // IE9 and above
480 | if (!isIE8) {
481 | // Create gutter elements for each pair.
482 | if (i > 0) {
483 | var gutterElement = gutter(i, direction);
484 | setGutterSize(gutterElement, gutterSize);
485 |
486 | gutterElement[addEventListener](
487 | 'mousedown',
488 | startDragging.bind(pair)
489 | );
490 | gutterElement[addEventListener](
491 | 'touchstart',
492 | startDragging.bind(pair)
493 | );
494 |
495 | parent.insertBefore(gutterElement, element.element);
496 |
497 | pair.gutter = gutterElement;
498 | }
499 | }
500 |
501 | // Set the element size to our determined size.
502 | // Half-size gutters for first and last elements.
503 | if (i === 0 || i === ids.length - 1) {
504 | setElementSize(element.element, element.size, gutterSize / 2);
505 | } else {
506 | setElementSize(element.element, element.size, gutterSize);
507 | }
508 |
509 | var computedSize = element.element[getBoundingClientRect]()[dimension];
510 |
511 | if (computedSize < element.minSize) {
512 | element.minSize = computedSize;
513 | }
514 |
515 | // After the first iteration, and we have a pair object, append it to the
516 | // list of pairs.
517 | if (i > 0) {
518 | pairs.push(pair);
519 | }
520 |
521 | return element;
522 | });
523 |
524 | function setSizes(newSizes) {
525 | newSizes.forEach(function(newSize, i) {
526 | if (i > 0) {
527 | var pair = pairs[i - 1];
528 | var a = elements[pair.a];
529 | var b = elements[pair.b];
530 |
531 | a.size = newSizes[i - 1];
532 | b.size = newSize;
533 |
534 | setElementSize(a.element, a.size, pair.aGutterSize);
535 | setElementSize(b.element, b.size, pair.bGutterSize);
536 | }
537 | });
538 | }
539 |
540 | function destroy() {
541 | pairs.forEach(function(pair) {
542 | pair.parent.removeChild(pair.gutter);
543 | elements[pair.a].element.style[dimension] = '';
544 | elements[pair.b].element.style[dimension] = '';
545 | });
546 | }
547 |
548 | if (isIE8) {
549 | return {
550 | setSizes: setSizes,
551 | destroy: destroy
552 | };
553 | }
554 |
555 | return {
556 | setSizes: setSizes,
557 | getSizes: function getSizes() {
558 | return elements.map(function(element) {
559 | return element.size;
560 | });
561 | },
562 | collapse: function collapse(i) {
563 | if (i === pairs.length) {
564 | var pair = pairs[i - 1];
565 |
566 | calculateSizes.call(pair);
567 |
568 | if (!isIE8) {
569 | adjust.call(pair, pair.size - pair.bGutterSize);
570 | }
571 | } else {
572 | var pair$1 = pairs[i];
573 |
574 | calculateSizes.call(pair$1);
575 |
576 | if (!isIE8) {
577 | adjust.call(pair$1, pair$1.aGutterSize);
578 | }
579 | }
580 | },
581 | destroy: destroy
582 | };
583 | };
584 |
585 | return Split;
586 | });
587 |
--------------------------------------------------------------------------------
/docs/assets/style.css:
--------------------------------------------------------------------------------
1 | .documentation {
2 | font-family: Helvetica, sans-serif;
3 | color: #666;
4 | line-height: 1.5;
5 | background: #f5f5f5;
6 | }
7 |
8 | .black {
9 | color: #666;
10 | }
11 |
12 | .bg-white {
13 | background-color: #fff;
14 | }
15 |
16 | h4 {
17 | margin: 20px 0 10px 0;
18 | }
19 |
20 | .documentation h3 {
21 | color: #000;
22 | }
23 |
24 | .border-bottom {
25 | border-color: #ddd;
26 | }
27 |
28 | a {
29 | color: #1184CE;
30 | text-decoration: none;
31 | }
32 |
33 | .documentation a[href]:hover {
34 | text-decoration: underline;
35 | }
36 |
37 | a:hover {
38 | cursor: pointer;
39 | }
40 |
41 | .py1-ul li {
42 | padding: 5px 0;
43 | }
44 |
45 | .max-height-100 {
46 | max-height: 100%;
47 | }
48 |
49 | .height-viewport-100 {
50 | height: 100vh;
51 | }
52 |
53 | section:target h3 {
54 | font-weight:700;
55 | }
56 |
57 | .documentation td,
58 | .documentation th {
59 | padding: .25rem .25rem;
60 | }
61 |
62 | h1:hover .anchorjs-link,
63 | h2:hover .anchorjs-link,
64 | h3:hover .anchorjs-link,
65 | h4:hover .anchorjs-link {
66 | opacity: 1;
67 | }
68 |
69 | .fix-3 {
70 | width: 25%;
71 | max-width: 244px;
72 | }
73 |
74 | .fix-3 {
75 | width: 25%;
76 | max-width: 244px;
77 | }
78 |
79 | @media (min-width: 52em) {
80 | .fix-margin-3 {
81 | margin-left: 25%;
82 | }
83 | }
84 |
85 | .pre, pre, code, .code {
86 | font-family: Source Code Pro,Menlo,Consolas,Liberation Mono,monospace;
87 | font-size: 14px;
88 | }
89 |
90 | .fill-light {
91 | background: #F9F9F9;
92 | }
93 |
94 | .width2 {
95 | width: 1rem;
96 | }
97 |
98 | .input {
99 | font-family: inherit;
100 | display: block;
101 | width: 100%;
102 | height: 2rem;
103 | padding: .5rem;
104 | margin-bottom: 1rem;
105 | border: 1px solid #ccc;
106 | font-size: .875rem;
107 | border-radius: 3px;
108 | box-sizing: border-box;
109 | }
110 |
111 | table {
112 | border-collapse: collapse;
113 | }
114 |
115 | .prose table th,
116 | .prose table td {
117 | text-align: left;
118 | padding:8px;
119 | border:1px solid #ddd;
120 | }
121 |
122 | .prose table th:nth-child(1) { border-right: none; }
123 | .prose table th:nth-child(2) { border-left: none; }
124 |
125 | .prose table {
126 | border:1px solid #ddd;
127 | }
128 |
129 | .prose-big {
130 | font-size: 18px;
131 | line-height: 30px;
132 | }
133 |
134 | .quiet {
135 | opacity: 0.7;
136 | }
137 |
138 | .minishadow {
139 | box-shadow: 2px 2px 10px #f3f3f3;
140 | }
141 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "final-education-app",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "server": "cd server && NODE_ENV=development nodemon server.js --exec babel-node --presets es2015,stage-0",
6 | "client": "cd client && npm run start",
7 | "dev": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\"",
8 | "start": "NODE_ENV=production node server/server.js",
9 | "heroku-postbuild": "cd client && npm install && npm run build"
10 | },
11 | "main": "index.js",
12 | "dependencies": {
13 | "@google-cloud/storage": "^1.7.0",
14 | "babel-cli": "^6.26.0",
15 | "babel-preset-es2015": "^6.24.1",
16 | "babel-preset-stage-0": "^6.24.1",
17 | "bcrypt": "^3.0.0",
18 | "body-parser": "^1.18.3",
19 | "express": "^4.16.3",
20 | "formidable": "^1.2.1",
21 | "fs": "0.0.1-security",
22 | "jsonwebtoken": "^8.3.0",
23 | "mongoose": "^5.1.5",
24 | "morgan": "^1.9.0",
25 | "nodemon": "^1.17.5",
26 | "react-redux": "^5.0.7",
27 | "redux": "^4.0.0",
28 | "redux-thunk": "^2.3.0",
29 | "save": "^2.3.2"
30 | },
31 | "devDependencies": {
32 | "concurrently": "^3.5.1"
33 | },
34 | "scriptsComments": {
35 | "Ref": "Reference for starting server simultaneously -- https://goo.gl/mqyLU5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/auth/adminAuth.js:
--------------------------------------------------------------------------------
1 | /**
2 | * adminAuth module - Ref: Example seen from https://goo.gl/HAeXNA - Checks for authorization in server side
3 | * @module adminAuth
4 | */
5 |
6 | var jwt = require('jsonwebtoken');
7 | const User = require('./../models/User')
8 |
9 | var adminAuth = (req, res, next) => {
10 | const token = req.headers['token'];
11 |
12 | if (token) {
13 | jwt.verify(token, 'something', (error, data) => {
14 | if (error) {
15 | return res.status(401).send({ error: 'Not authorized' });
16 | }
17 | let userId = data._id
18 | User.findById(userId)
19 | .then(userData => {
20 |
21 | let role = userData.role
22 | if (role !== 1) {
23 | res.status(404).send({ error: 'Not authorized' });
24 | }else{
25 | next()
26 | }
27 |
28 | })
29 | .catch(() => res.status(400).json({err: 'User does not exist'}))
30 |
31 | });
32 | }else{
33 | res.status(403).send({ error: 'Not authorized'});
34 | }
35 | }
36 | module.exports = adminAuth
37 |
--------------------------------------------------------------------------------
/server/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * config module - google storage
3 | * @module config
4 | */
5 |
6 | var path = require('path')
7 |
8 | const bucketName = '' // Put here your bucketName
9 | const Storage = require('@google-cloud/storage');
10 | const projectId = ''; // Put here your id
11 |
12 | const storage = new Storage({
13 | keyFilename: path.join(__dirname, '/../storage.json'),
14 | projectId: projectId
15 | });
16 |
17 |
18 | module.exports = {
19 | storage,
20 | bucketName
21 | }
22 |
--------------------------------------------------------------------------------
/server/configDB.js:
--------------------------------------------------------------------------------
1 | /**
2 | * configDB database - Database config
3 | * @module configDB
4 | */
5 |
6 |
7 | const mongoose = require('mongoose');
8 |
9 | mongoose.connect(''); // Here put the database you want to connect to.
10 |
11 |
12 | var db = mongoose.connection;
13 |
14 | db.on('error', () => console.log('Error in connection'));
15 | db.once('open', () => { console.log('connected'); });
16 |
17 |
18 | module.exports = db
19 |
--------------------------------------------------------------------------------
/server/models/Course.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Course module - Ref: From mongoose docs https://goo.gl/szRMTi
3 | * @module Course
4 | */
5 |
6 |
7 | var mongoose = require('mongoose')
8 | var Schema = mongoose.Schema;
9 |
10 | var courseSchema = new Schema({
11 | name: String,
12 | dateFrom: String,
13 | dateTo: String,
14 | description: String,
15 | courseStatus: String,
16 | filePaths: [{
17 | fileName: String,
18 | path: String,
19 | clicks: [String]
20 | }],
21 | videos: [{
22 | url: String,
23 | thumbnail: String,
24 | title: String,
25 | clicks: [String],
26 | youtubeId: String
27 | }],
28 | members: [{
29 | userId: String
30 | }]
31 |
32 |
33 | }, { timestamps: true, collection: 'courses' });
34 |
35 | module.exports = mongoose.model('Course', courseSchema);
36 |
--------------------------------------------------------------------------------
/server/models/User.js:
--------------------------------------------------------------------------------
1 | /**
2 | * User module - Ref: From mongoose docs https://goo.gl/szRMTi
3 | * @module User
4 | */
5 |
6 | var mongoose = require('mongoose')
7 | var Schema = mongoose.Schema;
8 |
9 |
10 | let userSchema = new Schema({
11 | firstName: String,
12 | lastName: String,
13 | email: String,
14 | password: String,
15 | role: Number
16 | }, { timestamps: true, collection: 'users' });
17 |
18 | module.exports = mongoose.model('User', userSchema);
19 |
--------------------------------------------------------------------------------
/server/routes/course.js:
--------------------------------------------------------------------------------
1 | /**
2 | * course module - REST CALLS
3 | * @module course
4 | */
5 |
6 |
7 | var express = require('express');
8 | var router = express.Router();
9 | var formidable = require('formidable');
10 | var mongoose = require('mongoose')
11 | var path = require('path')
12 | var googleStorage = require('./../config')
13 |
14 | var adminAuth = require('./../auth/adminAuth')
15 |
16 |
17 | const Course = require('./../models/Course')
18 | const User = require('./../models/User')
19 |
20 |
21 |
22 | // GET COURSES
23 | router.get('/', (req, res) => {
24 | Course.find()
25 | .then(data => res.json(data))
26 | .catch(error => res.status(500).res.json({error: 'No courses found'}))
27 | });
28 |
29 |
30 | // GET SINGLE COURSE
31 | router.get('/:courseId', (req, res) => {
32 | let courseId = req.params.courseId;
33 |
34 | Course.findById(courseId)
35 | .then(data => res.json(data))
36 | .catch(err => res.status(500).json({error: 'Something failed'}))
37 |
38 | });
39 |
40 |
41 | // CREATE COURSE -- SECURITY
42 | router.post('/', adminAuth, (req, res) => {
43 |
44 | const course = new Course();
45 | const obj = req.body;
46 |
47 | Object.keys(obj).forEach(key => {
48 | let value = obj[key]
49 | course[key] = value
50 | });
51 |
52 | course.save()
53 | .then(data => res.send(data))
54 | .catch(error => res.status(500).res.json({error: 'Could not create course '}))
55 |
56 |
57 | });
58 |
59 |
60 | // DELETE COURSE -- SECURITY
61 | router.delete('/:courseId', adminAuth, (req, res) => {
62 | let courseId = req.params.courseId;
63 |
64 | Course.findOneAndDelete({ _id: courseId })
65 | .then(data => res.json(data))
66 | .catch(error => res.status(500).res.json({error: 'Could not delete course'}))
67 |
68 | });
69 |
70 |
71 | // UPDATE COURSE -- SECURITY
72 | router.put("/:courseId", adminAuth, (req, res) => {
73 |
74 | let obj = req.body;
75 | let courseId = req.params.courseId
76 |
77 | Course.findByIdAndUpdate(courseId,{
78 | name: obj.name,
79 | dateFrom: obj.dateFrom,
80 | dateTo: obj.dateTo,
81 | description: obj.description,
82 | courseStatus: obj.courseStatus
83 | }, {new: true})
84 | .then(data => res.send(data))
85 | .catch(error => res.status(500).res.json({error: 'Could not upload file'}))
86 |
87 | });
88 |
89 |
90 |
91 | // DONWLOAD FILE
92 | router.get('/file/:downloadName/:courseId/:fileId', (req, res) => {
93 |
94 | let fileId = req.params.fileId
95 | let courseId = req.params.courseId
96 | let ip = req.headers['x-forwarded-for']
97 |
98 | /*Check IP for counting */
99 | Course.findOne({ "_id": courseId, "filePaths._id": fileId }).then( val => {
100 | let index = val.filePaths.findIndex(el => el._id == fileId)
101 | let isIp = val.filePaths[index].clicks.findIndex(el => ip)
102 |
103 | if (isIp === -1) {
104 | Course.findOneAndUpdate({ "_id": courseId, "filePaths._id": fileId },{"$push": { "filePaths.$.clicks": ip}})
105 | .then(val => { console.log('')})
106 | .catch(err => console.log(err))
107 | }
108 |
109 | })
110 |
111 | let downloadName = req.params.downloadName
112 | let publicPath = `${__dirname}/../public/${downloadName}`
113 | let options = { destination: publicPath}
114 |
115 | /*Get file first from Google to download */
116 | googleStorage.storage.bucket(googleStorage.bucketName).file(downloadName).download(options)
117 | .then(() => res.download(publicPath, downloadName))
118 | .catch(err => console.error('ERROR:', err));
119 |
120 | });
121 |
122 |
123 |
124 |
125 | // UPLOAD FILE -- SECURITY
126 | router.post("/file", adminAuth, (req, res) => {
127 |
128 | var form = new formidable.IncomingForm();
129 | form.parse(req, (error,fields,files) => {
130 | let oldPath = files.file.path;
131 | let courseId = fields.courseId
132 | let newName = `${courseId}_${files.file.name}`
133 |
134 |
135 | Course.findById(courseId)
136 | .then(courseObj => {
137 | let filePaths = courseObj.filePaths
138 | let findName = filePaths.findIndex(element => element.path == newName)
139 | if (findName == -1) {
140 |
141 | googleStorage.storage.bucket(googleStorage.bucketName).upload(oldPath).then(data => {
142 | let fileName = data[0].name
143 | googleStorage.storage.bucket(googleStorage.bucketName).file(fileName).move(newName).then(() => {
144 |
145 | Course.findByIdAndUpdate(courseId, {"$push": { "filePaths": {fileName: files.file.name, path: newName}}} , {new: true})
146 | .then(data => res.json(data))
147 | .catch(error => res.status(500).res.json({error: 'Could not update file'}))
148 |
149 | })
150 | }).catch(err => console.error('ERROR:', err))
151 |
152 | }
153 |
154 | })
155 | .catch(error => res.status(500).res.json({error: 'Could not upload file'}))
156 |
157 | })
158 |
159 | });
160 |
161 |
162 |
163 | // DELETE FILE -- SECURITY
164 | router.delete('/file/:courseId/:fileId', adminAuth, (req, res) => {
165 | let courseId = req.params.courseId
166 | let fileId = req.params.fileId;
167 |
168 | Course.findById(courseId)
169 | .then(courseData => {
170 | let files = courseData.filePaths
171 | let path = files.find(element => element._id == fileId).path
172 |
173 | googleStorage.storage.bucket(googleStorage.bucketName).file(path).delete().then(() => {
174 | Course.findByIdAndUpdate( courseId, { $pull: { "filePaths": { _id: fileId } } },{new: true})
175 | .then(data => res.json(data))
176 | .catch(error => res.status(500).res.json({error: 'Could not update Course file'}))
177 | })
178 | .catch(err => console.log(err))
179 | })
180 | .catch(error => res.status(500).res.json({error: 'Could not delete file'}))
181 |
182 |
183 | });
184 |
185 |
186 |
187 | // DELETE VIDEO -- SECURITY
188 | router.delete('/video/:courseId/:videoId', adminAuth, (req, res) => {
189 | let courseId = req.params.courseId
190 | let videoId = req.params.videoId
191 | let youtubeId = req.params.youtubeId
192 |
193 |
194 | Course.findById(courseId)
195 | .then(courseData => {
196 |
197 | Course.findByIdAndUpdate( courseId, { $pull: { "videos": { _id: videoId } } },{new: true})
198 | .then(data => res.send(data))
199 | .catch(error => res.status(500).res.json({error: 'Could not update video file'}))
200 |
201 | })
202 | .catch(error => res.status(500).res.json({error: 'Could not delete video'}))
203 |
204 | });
205 |
206 |
207 |
208 |
209 | // UPLOAD VIDEO -- SECURITY
210 | router.post('/video', adminAuth, (req, res) => {
211 | let courseId = req.body.courseId
212 | let url = req.body.url
213 | let title = req.body.title
214 | let youtubeId = req.body.youtubeId
215 | let thumbnail = req.body.thumbnail
216 |
217 | Course.findById(courseId)
218 | .then(courseObj => {
219 | let videos = courseObj.videos
220 | let findUrl = videos.findIndex(element => element.title === title)
221 |
222 | if (findUrl == -1) {
223 | Course.findByIdAndUpdate(courseId, {"$push": { "videos": {title, url, youtubeId, thumbnail, clicks: 0}}} , {new: true})
224 | .then(data => res.send(data))
225 | .catch(error => res.status(500).res.json({error: 'Could not update video data'}))
226 | }
227 |
228 | })
229 | .catch(error => res.status(500).res.json({error: 'Could not upload video'}))
230 |
231 | });
232 |
233 |
234 |
235 | // WATCH VIDEO
236 | router.get('/video/:courseId/:videoId', (req, res) => {
237 | let courseId = req.params.courseId
238 | let videoId = req.params.videoId
239 | let ip = req.headers['x-forwarded-for']
240 |
241 |
242 | Course.findOne({ "_id": courseId, "videos._id": videoId }).then( val => {
243 |
244 | let index = val.videos.findIndex(el => el._id == videoId)
245 | let isIp = val.videos[index].clicks.findIndex(el => ip)
246 |
247 | if (isIp === -1) {
248 | Course.findOneAndUpdate({ "_id": courseId, "videos._id": videoId },{"$push": { "videos.$.clicks": ip}}, {new: true})
249 | .then(data => res.send(data))
250 | .catch(err => console.log(err))
251 | }else{
252 | res.send(val)
253 | }
254 |
255 | })
256 |
257 | });
258 |
259 |
260 |
261 |
262 | // ENROLL COURSE
263 | router.post('/enrollCourse', (req, res) => {
264 | let courseId = req.body.courseId
265 | let userId = req.body.userId
266 |
267 | Course.findById(courseId)
268 | .then(courseObj => {
269 | let members = courseObj.members
270 | let findMember = members.findIndex(element => element.userId === userId)
271 |
272 | if (findMember == -1) {
273 | Course.findByIdAndUpdate(courseId, {"$push": { "members": {userId: userId}}} , {new: true})
274 | .then(data => res.send(data))
275 | .catch(error => res.status(500).res.json({error: 'Could not update course '}))
276 | }
277 |
278 | })
279 | .catch(error => res.status(500).res.json({error: 'Could not enroll in course '}))
280 |
281 |
282 | });
283 |
284 |
285 |
286 |
287 |
288 | // LEAVE COURSE
289 | router.delete('/leaveCourse/:courseId/:enrollId', (req, res) => {
290 | let courseId = req.params.courseId
291 | let enrollId = req.params.enrollId
292 |
293 |
294 | Course.findById(courseId)
295 | .then(courseData => {
296 | Course.findByIdAndUpdate( courseId, { $pull: { "members": { _id: enrollId } } },{new: true})
297 | .then(data => res.send(data))
298 | .catch(error => res.status(500).json({error: 'Could not update '}))
299 |
300 | })
301 | .catch(error => res.status(500).json({error: 'Could not find course '}))
302 |
303 |
304 | });
305 |
306 |
307 |
308 |
309 |
310 | module.exports = router
311 |
--------------------------------------------------------------------------------
/server/routes/user.js:
--------------------------------------------------------------------------------
1 | /**
2 | * user module - REST CALLS
3 | * @module user
4 | */
5 |
6 |
7 | var express = require('express');
8 | var router = express.Router();
9 | var bcrypt = require('bcrypt')
10 | var jwt = require('jsonwebtoken');
11 |
12 | const User = require('./../models/User')
13 | const Course = require('./../models/Course')
14 | const secretKey = 'something'
15 |
16 | var adminAuth = require('./../auth/adminAuth')
17 |
18 |
19 | // GET ALL USERS
20 | router.get('/', (req, res) => {
21 | User.find()
22 | .then(data => res.json(data))
23 | .catch(err => res.status(400).send({err: 'Could not get all users'}))
24 | });
25 |
26 |
27 | // CREATE USER
28 | router.post('/', adminAuth, (req, res) => {
29 |
30 | let user = new User();
31 | const userObj = req.body;
32 | let { firstName, lastName, email, password, role } = userObj
33 | user.firstName = firstName
34 | user.lastName = lastName
35 | user.email = email
36 | user.role = role
37 |
38 | User.findOne({ email }).then(userData => {
39 | if (userData) {
40 | return res.status(400).json({err: 'Email exsists'});
41 | }else {
42 |
43 | // Ref: taken from bcrypt example https://goo.gl/4EMHbe
44 | bcrypt.hash(password, 2).then(hash => {
45 | user.password = hash
46 | user.save()
47 | .then(data => res.send(data))
48 | .catch(err => res.status(400).send({err: 'Could not create user'}))
49 | })
50 | }
51 | });
52 | });
53 |
54 |
55 | // GET SINGLE USER
56 | router.get('/:userId', (req, res) => {
57 | let userId = req.params.userId
58 |
59 | User.findById(userId)
60 | .then(userData => res.send(userData))
61 | .catch(() => res.status(400).json({err: 'User does not exist'}))
62 |
63 | });
64 |
65 |
66 | // SINGLE LOGIN
67 | router.post('/login', (req, res) => {
68 |
69 | let { email, password } = req.body
70 |
71 | // find user and if everything is found decrypt password
72 | User.findOne({email: email}).then(userData => {
73 | if (userData == null) {
74 | res.status(400).json({err: 'Email does not exist'});
75 | }else {
76 | let dbPassword = userData.password
77 | let {_id, firstName, lastName, email, role} = userData
78 |
79 | bcrypt.compare(password, dbPassword).then(result => {
80 | if (result === false) {
81 | return res.status(400).json({err: 'This is the wrong password'});
82 | }
83 |
84 |
85 | let jwtData = {};
86 | jwtData._id = _id
87 | jwtData.firstName = firstName
88 | jwtData.lastName = lastName
89 | jwtData.email = email
90 | jwtData.role = role
91 |
92 | let token = jwt.sign(jwtData, secretKey)
93 | res.json({token})
94 |
95 | })
96 | }
97 | })
98 |
99 | });
100 |
101 |
102 |
103 |
104 |
105 | // DELETE USER
106 | router.delete('/:userId', (req, res) => {
107 |
108 | let userId = req.params.userId;
109 |
110 | Course.find().then(courseData => {
111 | courseData.forEach(val => {
112 | if (val.members.length > 0) {
113 | val.members.forEach(memberVal => {
114 | if (memberVal.userId == userId) {
115 | Course.findByIdAndUpdate( val._id,
116 | { $pull: { "members": { userId: userId } } },{new: true},(err,data) => {
117 | console.log(data);
118 | });
119 | }
120 | })
121 | }
122 | })
123 | })
124 |
125 |
126 | User.findOneAndDelete({ _id: userId })
127 | .then(data => res.json(data))
128 | .catch(err => res.status(400).send({err: 'Somthing went wrong'}))
129 |
130 |
131 | });
132 |
133 |
134 | module.exports = router
135 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | /**
2 | * server
3 | * @module server
4 | */
5 |
6 |
7 | const express = require('express');
8 | const bodyParser = require('body-parser');
9 | const logger = require('morgan');
10 | const path = require('path')
11 | const app = express();
12 | const db = require('./configDB')
13 |
14 |
15 |
16 | var course = require('./routes/course')
17 | var user = require('./routes/user')
18 |
19 |
20 |
21 |
22 | // Examples taken from https://goo.gl/ZGizec
23 | app.use(bodyParser.urlencoded({ extended: true }));
24 | app.use(bodyParser.json());
25 | app.use(logger('dev'));
26 |
27 |
28 |
29 | app.use('/api/course', course);
30 | app.use('/api/user', user);
31 |
32 | // Examples taken from Express.js file https://goo.gl/VYU9fs
33 | if (process.env.NODE_ENV == 'production') {
34 | app.use(express.static(path.join(__dirname, '/../client/build')));
35 | app.get('*', (req, res) => {
36 | res.sendFile(path.join(__dirname+'/../client/build', 'index.html'));
37 | });
38 | }
39 |
40 |
41 |
42 |
43 | app.listen(process.env.PORT || 3001);
44 |
--------------------------------------------------------------------------------
/storage.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/awesomedev08/React-Education-App/9355a10e496df5be48e7ae7470a9b0a021e81187/storage.json
--------------------------------------------------------------------------------