├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── Components │ ├── Announcements │ │ ├── AddAnnouncement │ │ │ ├── Body.js │ │ │ └── index.js │ │ └── AnnouncementsPanel.js │ ├── Courses │ │ ├── AddCourses │ │ │ ├── Body.js │ │ │ └── index.js │ │ ├── AllCourses │ │ │ ├── Body.js │ │ │ └── index.js │ │ ├── Context │ │ │ ├── context.js │ │ │ ├── index.js │ │ │ └── withCourse.js │ │ ├── CourseBar.js │ │ ├── CoursePage │ │ │ ├── Assignment │ │ │ │ ├── AdminSubmission │ │ │ │ │ ├── Body.js │ │ │ │ │ └── index.js │ │ │ │ ├── AssignmentModal.js │ │ │ │ ├── AssignmentPage.js │ │ │ │ └── withAssignment.js │ │ │ ├── Body.js │ │ │ ├── Curriculum │ │ │ │ ├── CurriculumModal.js │ │ │ │ ├── CurriculumPage.js │ │ │ │ └── withCurriculum.js │ │ │ └── index.js │ │ ├── CourseShowcase.js │ │ └── EditCourses │ │ │ ├── Body.js │ │ │ └── index.js │ ├── Dashboard │ │ ├── MainPalette │ │ │ ├── AdminCoursePalette.js │ │ │ ├── CoursePanel.js │ │ │ └── StudentCoursePalette.js │ │ └── index.js │ ├── Firebase │ │ ├── context.js │ │ ├── firebase.js │ │ └── index.js │ ├── LandingPage │ │ └── HomePage.js │ ├── Login │ │ ├── LoginForm.js │ │ └── index.js │ ├── Navbar │ │ ├── SideNavigation.js │ │ └── TopNavBar.js │ ├── Session │ │ ├── context.js │ │ ├── index.js │ │ ├── withAuthentication.js │ │ └── withAuthorization.js │ ├── Signout │ │ └── SignOutButton.js │ ├── Signup │ │ ├── SignupForm.js │ │ └── index.js │ ├── Templates │ │ └── MainTemplate.js │ └── TextField.js ├── assets │ ├── 006023-Free-vector_-Macbook-Ipad-and-Iphone-by-Frexy-Dribbble-removebg-preview (1).png │ ├── logo.svg │ └── lp.png ├── index.js └── serviceWorker.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # netlify 15 | /.netlify 16 | 17 | # test 18 | /test 19 | 20 | # misc 21 | .DS_Store 22 | .env 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Demo: https://webcademy.netlify.app/ 2 | 3 | Built with Firebase and React. 4 | 5 | Test built with Puppeteer framework and Mocha. 6 | 7 | 8 | ### Note: This is a quick prototype of LMS built with React. Please expect some bugs and most stylings were done inline for quick testing. PR request is accepted! 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eduwebfyp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ckeditor/ckeditor5-build-balloon": "^17.0.0", 7 | "@ckeditor/ckeditor5-react": "^2.1.0", 8 | "@react-firebase/auth": "^0.2.10", 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.3.2", 11 | "@testing-library/user-event": "^7.1.2", 12 | "Dante2": "^0.5.0-rc50", 13 | "alloyeditor": "^2.11.5", 14 | "draft-js": "^0.11.4", 15 | "express": "^4.17.1", 16 | "firebase": "^7.8.2", 17 | "mocha": "^7.1.1", 18 | "prism": "^4.1.2", 19 | "prismjs": "^1.19.0", 20 | "puppeteer-core": "^3.0.0", 21 | "react": "^16.12.0", 22 | "react-dom": "^16.12.0", 23 | "react-draft-wysiwyg": "^1.14.4", 24 | "react-html-parser": "^2.0.2", 25 | "react-router-dom": "^5.1.2", 26 | "react-scripts": "3.4.0", 27 | "recompose": "^0.30.0", 28 | "rsuite": "^4.2.1", 29 | "suneditor-react": "^2.6.7" 30 | }, 31 | "scripts": { 32 | "test": "mocha --timeout 0 account.test.js", 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezeburger/React-Learning-Management-System/4f2fdc4a222a445af1854d0d606d780ce5e92b88/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 20 | 21 | 30 | Webcademy 31 | 32 | 33 | 34 |
35 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezeburger/React-Learning-Management-System/4f2fdc4a222a445af1854d0d606d780ce5e92b88/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezeburger/React-Learning-Management-System/4f2fdc4a222a445af1854d0d606d780ce5e92b88/public/logo512.png -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import 'rsuite/dist/styles/rsuite-default.css'; 2 | import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'; 3 | 4 | import React from 'react'; 5 | import { Route, BrowserRouter as Router } from 'react-router-dom'; 6 | 7 | import Login from './Components/Login/'; 8 | import HomePage from './Components/LandingPage/HomePage.js'; 9 | import SignUp from './Components/Signup'; 10 | import Dashboard from './Components/Dashboard'; 11 | 12 | import { withAuthentication } from './Components/Session'; 13 | import AddCourses from './Components/Courses/AddCourses'; 14 | import EditCourses from './Components/Courses/EditCourses'; 15 | import CoursePage from './Components/Courses/CoursePage'; 16 | import CurriculumPage from './Components/Courses/CoursePage/Curriculum/CurriculumPage'; 17 | import Announcement from './Components/Announcements/AddAnnouncement'; 18 | import AllCourses from './Components/Courses/AllCourses'; 19 | import AssignmentPage from './Components/Courses/CoursePage/Assignment/AssignmentPage'; 20 | import AdminSubmission from './Components/Courses/CoursePage/Assignment/AdminSubmission'; 21 | 22 | const App = () => ( 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 40 | 45 |
46 |
47 | ); 48 | 49 | export default withAuthentication(App); 50 | -------------------------------------------------------------------------------- /src/Components/Announcements/AddAnnouncement/Body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | FormGroup, 5 | Form, 6 | FormControl, 7 | ControlLabel, 8 | SelectPicker, 9 | Button, 10 | ButtonToolbar, 11 | Alert, 12 | } from "rsuite"; 13 | 14 | class Body extends React.Component{ 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | showError: false, 19 | errorPlacement: "bottomStart", 20 | formValue: { 21 | courseId: '', 22 | announcement: '', 23 | }, 24 | userCreatedCourses: null, 25 | 26 | }; 27 | this.formSubmit = this.formSubmit.bind(this); 28 | this.getCreatedCourses = this.getCreatedCourses.bind(this); 29 | } 30 | 31 | formSubmit() { 32 | const { courseId, announcement } = this.state.formValue; 33 | 34 | if (!courseId ||!announcement) { 35 | Alert.error("Please fill in all fields.", 5000); 36 | } else { 37 | this.props.firebase.course(courseId).child('announcements').push( 38 | { 39 | createdOn: Date.now(), 40 | createdBy: this.props.authUser.name, 41 | content: announcement, 42 | }, 43 | err => { 44 | if (err) { 45 | Alert.error( 46 | "An error occurred while creating announcement. Please try again.", 47 | 5000 48 | ); 49 | } 50 | } 51 | ).then(rsp => { 52 | Alert.success('Announcement created successfully.'); 53 | this.props.history.push(`/dashboard`); 54 | }) 55 | } 56 | } 57 | 58 | getCreatedCourses(){ 59 | let userCreatedCourses = this.props.coursesList; 60 | const uid = this.props.authUser.uid; 61 | 62 | userCreatedCourses = userCreatedCourses.filter(course=> { 63 | return course.createdBy === uid 64 | }) 65 | 66 | userCreatedCourses = userCreatedCourses.map(course=> { 67 | return{ 68 | label: course.title, 69 | value: course.uid, 70 | }; 71 | }) 72 | 73 | this.setState({userCreatedCourses}); 74 | } 75 | 76 | componentDidMount(){ 77 | this.getCreatedCourses(); 78 | } 79 | render() { 80 | return ( 81 |
82 | 83 | 84 | 85 |
this.setState({ formValue })}> 86 | 87 | Add announcement to course: 88 | 97 | 98 | 99 | 100 | Announcement Content 101 | 106 | 107 | 108 | 109 | 110 | 117 | 118 | 119 |
120 |
121 |
122 |
123 | ); 124 | } 125 | } 126 | 127 | export default Body; -------------------------------------------------------------------------------- /src/Components/Announcements/AddAnnouncement/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {compose} from 'recompose'; 3 | import mainTemplate from '../../Templates/MainTemplate' 4 | import {withFirebase} from '../../Firebase'; 5 | import {withAuthorization} from '../../Session' 6 | import withCourse from '../../Courses/Context/withCourse' 7 | import {NavLink} from 'react-router-dom'; 8 | import AnnouncementsPanel from '../../Announcements/AnnouncementsPanel'; 9 | import { Breadcrumb } from 'rsuite'; 10 | import Body from './Body'; 11 | 12 | const Header = (props) => ( 13 |
14 |
15 | 16 |
17 |
18 | ) 19 | 20 | const BreadCrumb = () => ( 21 | 22 | Dashboard 23 | Courses 24 | Add New Announcement 25 | 26 | ) 27 | 28 | const condition = authUser => authUser && !!(authUser.roles.userRole === 'admin'); 29 | 30 | let AddAnnouncement = mainTemplate(Header, BreadCrumb, Body); 31 | 32 | export default compose( 33 | withAuthorization(condition), 34 | withCourse, 35 | withFirebase, 36 | )(AddAnnouncement); 37 | 38 | -------------------------------------------------------------------------------- /src/Components/Announcements/AnnouncementsPanel.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Avatar, Badge, Icon, IconButton, Drawer, Tag } from 'rsuite'; 3 | import withCourse from '../Courses/Context/withCourse'; 4 | 5 | class AnnouncementsPanel extends Component{ 6 | constructor(props){ 7 | super(props); 8 | 9 | this.state = { 10 | drawerShow: false 11 | }; 12 | 13 | this.toggleDrawer = this.toggleDrawer.bind(this); 14 | this.renderAdmin = this.renderAdmin.bind(this); 15 | } 16 | 17 | toggleDrawer(){ 18 | this.setState({drawerShow: !this.state.drawerShow}); 19 | } 20 | 21 | renderAdmin(){ 22 | const uid = this.props.authUser.uid; 23 | let userCourses = !!(this.props.authUser.coursesCreated)? true: false; 24 | 25 | //User created courses 26 | if(userCourses){ 27 | userCourses = this.props.coursesList.filter(course => { 28 | return course.createdBy === uid && course.announcements; 29 | }) 30 | let announcements = []; 31 | 32 | userCourses.forEach(course => { 33 | const announcementKey = Object.keys(course.announcements); 34 | 35 | announcementKey.forEach(aKey => { 36 | const date = new Date(course.announcements[aKey].createdOn).toLocaleString(); 37 | announcements.push({ 38 | courseTitle: course.title, 39 | content: course.announcements[aKey].content, 40 | createdOn: date, 41 | createdBy: course.announcements[aKey].createdBy, 42 | }) 43 | }) 44 | }) 45 | announcements = announcements.sort((a, b) => (a.createdOn < b.createdOn) ? 1 : ((b.createdOn < a.createdOn) ? -1 : 0)); 46 | 47 | return( 48 |
49 | {announcements.length? 50 | announcements.map((a, i) => { 51 | return( 52 | <> 53 | {a.courseTitle} 54 | {a.createdOn} - {a.createdBy? a.createdBy: 'Unknown'} 55 |

{a.content}

56 |
57 | 58 | ) 59 | }) 60 | :

No announcements have been created...

61 | } 62 |
63 | 64 | ) 65 | } 66 | else{ 67 | return( 68 |

Nothing to display...

69 | ) 70 | } 71 | 72 | } 73 | 74 | renderStudent(){ 75 | let enrolledCourses = this.props.authUser.enrolledCourses; 76 | let announcements = []; 77 | 78 | if(enrolledCourses){ 79 | enrolledCourses = Object.keys(enrolledCourses).map(key => { 80 | return enrolledCourses[key].courseId; 81 | }) 82 | 83 | enrolledCourses = this.props.coursesList.filter(course => { 84 | return enrolledCourses.includes(course.uid.substr(1)); 85 | }) 86 | 87 | enrolledCourses.forEach(course => { 88 | if(course.announcements){ 89 | Object.keys(course.announcements).forEach(aKey => { 90 | const date = new Date(course.announcements[aKey].createdOn).toLocaleString(); 91 | announcements.push({ 92 | courseTitle: course.title, 93 | content: course.announcements[aKey].content, 94 | createdOn: date, 95 | createdBy: course.announcements[aKey].createdBy, 96 | }) 97 | }) 98 | } 99 | }) 100 | 101 | announcements = announcements.sort((a, b) => (a.createdOn < b.createdOn) ? 1 : ((b.createdOn < a.createdOn) ? -1 : 0)); 102 | return( 103 |
104 | {announcements.length? 105 | announcements.map((a, i) => { 106 | return( 107 |
108 | {a.courseTitle} 109 | {a.createdOn} - {a.createdBy? a.createdBy: 'Unknown'} 110 |

{a.content}

111 |
112 |
113 | ) 114 | }) 115 | :

No announcements have been created...

116 | } 117 |
118 | 119 | ) 120 | } 121 | } 122 | 123 | render(){ 124 | return( 125 |
126 | 127 | 128 | } 131 | onClick= {this.toggleDrawer} 132 | /> 133 | 134 | 135 | 136 | 137 | 138 | Announcements 139 | 140 | { 141 | this.props.authUser.roles.userRole === 'admin'? 142 | this.renderAdmin() 143 | : this.renderStudent() 144 | 145 | } 146 | 147 | 148 | 149 |
150 | ) 151 | } 152 | } 153 | 154 | export default withCourse(AnnouncementsPanel); -------------------------------------------------------------------------------- /src/Components/Courses/AddCourses/Body.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Container, 4 | FormGroup, 5 | Form, 6 | Input, 7 | FormControl, 8 | ControlLabel, 9 | SelectPicker, 10 | Button, 11 | ButtonToolbar, 12 | Alert, 13 | Uploader, 14 | Icon 15 | } from "rsuite"; 16 | 17 | const qualificationData = [ 18 | { label: "Primary School", value: "primary-school" }, 19 | { label: "High School", value: "high-school" }, 20 | { label: "College", value: "college" }, 21 | { label: "Degree", value: "degree" }, 22 | { label: "Masters", value: "master" }, 23 | { label: "Other", value: "other" } 24 | ]; 25 | 26 | const difficultyLevel = [ 27 | { label: "Fundamental", value: "fundamental" }, 28 | { label: "Intermediate", value: "intermediate" }, 29 | { label: "Advanced", value: "advanced" }, 30 | { label: "Fundamental-Intermediate", value: "fundamental-intermediate" }, 31 | { label: "Fundamental-Advanced", value: "fundamental-advanced" }, 32 | { label: "Other", value: "other" } 33 | ]; 34 | 35 | const durationAcceptor = ({ onChange }) => { 36 | return ( 37 | { 40 | onChange(e); 41 | }} 42 | style={{ width: "90px" }} 43 | /> 44 | ); 45 | }; 46 | 47 | class Body extends React.Component { 48 | constructor(props) { 49 | super(props); 50 | this.state = { 51 | showError: false, 52 | errorPlacement: "bottomStart", 53 | formValue: { 54 | title: "", 55 | qualification: "", 56 | levelOfStudy: "", 57 | duration: 0, 58 | description: "", 59 | prerequisite: "" 60 | }, 61 | featureImage: null, 62 | uploading: false, 63 | createCourseBtnShow: false, 64 | }; 65 | this.formSubmit = this.formSubmit.bind(this); 66 | } 67 | 68 | formSubmit() { 69 | this.setState({createCourseBtnShow: true}); 70 | 71 | const { 72 | title, 73 | qualification, 74 | levelOfStudy, 75 | duration, 76 | description, 77 | prerequisite 78 | } = this.state.formValue; 79 | const uid = this.props.firebase.auth.currentUser.uid; 80 | if ( 81 | !title || 82 | !qualification || 83 | !levelOfStudy || 84 | !duration || 85 | !description || 86 | !prerequisite || 87 | !this.state.setFileInfo 88 | ) { 89 | Alert.error("Please fill in all fields.", 5000); 90 | this.setState({createCourseBtnShow: false}); 91 | } else { 92 | this.props.firebase 93 | .courses() 94 | .push( 95 | { 96 | createdBy: uid, 97 | title, 98 | qualification, 99 | levelOfStudy, 100 | duration, 101 | description, 102 | prerequisite, 103 | image: this.state.setFileInfo 104 | }, 105 | err => { 106 | if (err) { 107 | Alert.error( 108 | "An error occurred while creating course. Please try again.", 109 | 5000 110 | ); 111 | this.setState({createCourseBtnShow: false}); 112 | } 113 | } 114 | ) 115 | .then(rsp => { 116 | const courseId = rsp.key.substr(1, rsp.key.length); 117 | this.props.firebase 118 | .user(uid) 119 | .child("coursesCreated") 120 | .push({ courseId }); 121 | this.props.history.push(`/courses/${courseId}`); 122 | }) 123 | .catch(err =>{ 124 | console.log(err); 125 | }) 126 | } 127 | } 128 | 129 | previewFile(file, callback) { 130 | const reader = new FileReader(); 131 | reader.onloadend = () => { 132 | callback(reader.result); 133 | }; 134 | reader.readAsDataURL(file); 135 | } 136 | 137 | render() { 138 | return ( 139 |
140 | 141 | 142 | { 148 | this.setState({ uploading: true }); 149 | this.previewFile(file.blobFile, value => { 150 | this.setState({ setFileInfo: value }); 151 | }); 152 | }} 153 | onError={() => { 154 | // Alert.error("Upload Failed. Please try again."); 155 | }} 156 | > 157 | 169 | 170 | 171 | 172 |
this.setState({ formValue })}> 173 | 174 | Course Title 175 | 176 | 177 | 178 | Qualification 179 | 185 | 186 | 187 | Level 188 | 194 | 195 | 196 | Duration (Hours) 197 | 202 | 203 | 204 | Course Description 205 | 210 | 211 | 212 | Prerequisites 213 | 219 | 220 | 221 | 222 | 225 | 226 | 227 |
228 |
229 |
230 |
231 | ); 232 | } 233 | } 234 | 235 | export default Body; -------------------------------------------------------------------------------- /src/Components/Courses/AddCourses/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {compose} from 'recompose'; 3 | import mainTemplate from '../../Templates/MainTemplate' 4 | import {withFirebase} from '../../Firebase'; 5 | import {withAuthorization} from '../../Session' 6 | import {NavLink} from 'react-router-dom'; 7 | import AnnouncementsPanel from '../../Announcements/AnnouncementsPanel'; 8 | import { Breadcrumb } from 'rsuite'; 9 | import Body from './Body'; 10 | 11 | const Header = (props) => ( 12 |
13 |
14 | 15 |
16 |
17 | ) 18 | 19 | const BreadCrumb = () => ( 20 | 21 | Dashboard 22 | Courses 23 | Add New Course 24 | 25 | ) 26 | 27 | const condition = authUser => authUser && !!(authUser.roles['userRole'] === 'admin'); 28 | 29 | let AddCoursePage = mainTemplate(Header, BreadCrumb, Body); 30 | 31 | export default compose( 32 | withAuthorization(condition), 33 | withFirebase, 34 | )(AddCoursePage); 35 | 36 | -------------------------------------------------------------------------------- /src/Components/Courses/AllCourses/Body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from 'rsuite'; 3 | import CourseShowcase from '../CourseShowcase'; 4 | 5 | export default class Body extends React.Component{ 6 | 7 | render(){ 8 | const coursesList = this.props.coursesList; 9 | 10 | return( 11 | 12 | { 13 | coursesList && coursesList.length? 14 | Array.from(coursesList).reverse().map(course => { 15 | return ( 16 | 17 | ) 18 | }) 19 | :

No courses found...

20 | } 21 |
22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /src/Components/Courses/AllCourses/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AnnouncementsPanel from '../../Announcements/AnnouncementsPanel'; 3 | import mainTemplate from '../../Templates/MainTemplate'; 4 | import { Breadcrumb } from 'rsuite'; 5 | import Body from './Body'; 6 | import withCourse from '../Context/withCourse'; 7 | import { NavLink } from 'react-router-dom'; 8 | 9 | const Header = (props) => ( 10 |
11 |
12 | 13 |
14 |
15 | ) 16 | 17 | const BreadCrumb = (props) => ( 18 | 19 | Dashboard 20 | All Courses 21 | 22 | ) 23 | 24 | const body = (props) => { 25 | return( 26 |
27 |
28 | 29 |
30 |
31 | ) 32 | } 33 | 34 | 35 | 36 | const WrappedComponents = mainTemplate(Header, BreadCrumb, body); 37 | 38 | export default withCourse(WrappedComponents); 39 | -------------------------------------------------------------------------------- /src/Components/Courses/Context/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CourseContext = React.createContext(null); 4 | 5 | export default CourseContext; -------------------------------------------------------------------------------- /src/Components/Courses/Context/index.js: -------------------------------------------------------------------------------- 1 | import CourseContext from './context'; 2 | import withCourse from './withCourse'; 3 | 4 | export default {CourseContext, withCourse}; -------------------------------------------------------------------------------- /src/Components/Courses/Context/withCourse.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withFirebase } from '../../Firebase'; 3 | import { Loader } from 'rsuite'; 4 | 5 | const withCourse = Component => { 6 | class WithCourse extends React.Component{ 7 | constructor(props){ 8 | super(props); 9 | 10 | this.state = { 11 | coursesList: null, 12 | course: null, 13 | loading: true, 14 | } 15 | 16 | this.getCourse = this.getCourse.bind(this); 17 | } 18 | 19 | componentDidMount(){ 20 | this.listener = this.props.firebase.onAuthCourseListener((coursesList)=>{ 21 | this.setState({ coursesList, loading: false }) 22 | }, 23 | ()=>{ 24 | this.setState({ coursesList: null, loading: false }) 25 | } 26 | ) 27 | } 28 | 29 | async getCourse(id){ 30 | const {coursesList} = this.state; 31 | let foundCourse = null; 32 | 33 | await new Promise(rs => { 34 | for(let i = 0; i< coursesList.length; i++){ 35 | if(coursesList.uid === id){ 36 | foundCourse = coursesList[i]; 37 | rs(); 38 | break; 39 | } 40 | } 41 | }) 42 | return foundCourse; 43 | } 44 | 45 | render(){ 46 | return( 47 | <> 48 | { 49 | this.state.loading? 50 | 51 | : 52 | ( 53 | this.state.coursesList? 54 | 55 | : 56 | ) 57 | } 58 | 59 | ) 60 | } 61 | } 62 | 63 | return withFirebase(WithCourse); 64 | } 65 | 66 | export default withCourse; -------------------------------------------------------------------------------- /src/Components/Courses/CourseBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Container, Progress, Button } from 'rsuite'; 3 | const { Line } = Progress; 4 | 5 | export default class CourseBar extends Component{ 6 | render(){ 7 | return( 8 |
11 | 29 |
30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Assignment/AdminSubmission/Body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | Panel, 5 | Table, 6 | Button, 7 | Modal, 8 | Form, 9 | FormGroup, 10 | ControlLabel, 11 | FormControl, 12 | InputNumber, 13 | Divider, 14 | Alert 15 | } from 'rsuite'; 16 | import ReactHtmlParser from 'react-html-parser'; 17 | 18 | export default class Body extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | show: false, 24 | modalData: {}, 25 | formValue: {} 26 | }; 27 | } 28 | 29 | reviewAssignment = rowData => { 30 | this.props.firebase 31 | .user(rowData.studentId) 32 | .child('enrolledCourses') 33 | .on('value', snapshot => { 34 | let match = []; 35 | let submissions = []; 36 | let coursesList = this.props.coursesList; 37 | 38 | snapshot.forEach(item => { 39 | let course = item.val(); 40 | course.enrolledCourseKey = item.key; 41 | 42 | if (course.submissions) { 43 | submissions.push(course); 44 | } 45 | }); 46 | 47 | //Get courseId and submissions of the course 48 | submissions = submissions 49 | .map(item => { 50 | const innerKeys = Object.keys(item.submissions); 51 | 52 | const s = innerKeys.map(submissionKey => { 53 | return { 54 | submissionKey, 55 | data: item.submissions[submissionKey] 56 | }; 57 | }); 58 | 59 | return { 60 | courseId: item.courseId, 61 | enrolledCourseKey: item.enrolledCourseKey, 62 | submissions: s 63 | }; 64 | }) 65 | .flat(); 66 | 67 | //Get matching course with matching rowData.snapshotKey 68 | match = submissions.filter(item => { 69 | const temp = Object.keys(item.submissions).map(key => { 70 | return item.submissions[key].submissionKey; 71 | }); 72 | 73 | return temp.includes(rowData.snapshotKey); 74 | }); 75 | 76 | //Get only the submission that match the rowData.snapshotKey 77 | match = match.map(item => { 78 | item.submissions = item.submissions.filter(inner => { 79 | return inner.submissionKey === rowData.snapshotKey; 80 | }); 81 | 82 | item.submissions = item.submissions[0]; 83 | return item; 84 | }); 85 | 86 | let assId = match.map(item => { 87 | return `-${item.submissions.data.assId}`; 88 | })[0]; 89 | 90 | //Extracting assignments to obtain original questions 91 | let assignments = coursesList.map(item => { 92 | return item.assignments; 93 | }); 94 | 95 | assignments = assignments.filter((course, i) => { 96 | if(course){ 97 | const keys = Object.keys(course); 98 | return keys.includes(assId); 99 | } 100 | }); 101 | 102 | assignments = assignments[0][assId].questions; 103 | 104 | this.setState({ 105 | show: true, 106 | modalData: { 107 | questions: assignments, 108 | submissionData: match[0], 109 | rowData 110 | } 111 | }); 112 | }); 113 | }; 114 | 115 | close = () => { 116 | this.setState({ show: false }); 117 | }; 118 | 119 | onReviewSubmit = modalData => { 120 | let { marks, comments } = this.state.formValue; 121 | const uid = this.props.firebase.auth.currentUser.uid; 122 | let { rowData, submissionData } = modalData; 123 | marks = marks ? marks : rowData.marks ? rowData.marks : null; 124 | 125 | if (!marks) { 126 | Alert.error('Marks cannot be empty.'); 127 | } else { 128 | const date = Date.now(); 129 | 130 | comments = comments 131 | ? comments 132 | : rowData.comments 133 | ? rowData.comments 134 | : null; 135 | this.props.firebase 136 | .course(`-${submissionData.courseId}`) 137 | .child('submissions') 138 | .child(rowData.key) 139 | .update({ 140 | status: 'reviewed', 141 | comments, 142 | reviewedOn: date, 143 | reviewedBy: uid, 144 | marks: marks 145 | }) 146 | .then(() => { 147 | this.props.firebase 148 | .user(rowData.studentId) 149 | .child('enrolledCourses') 150 | .child(submissionData.enrolledCourseKey) 151 | .child('submissions') 152 | .child(submissionData.submissions.submissionKey) 153 | .update({ 154 | status: 'reviewed', 155 | reviewedOn: date, 156 | reviewer: this.props.authUser.name, 157 | reviewerId: uid, 158 | comments, 159 | marks 160 | }) 161 | .then(() => { 162 | Alert.success('Successfully reviewed.'); 163 | this.setState({ show: false }); 164 | }); 165 | }); 166 | } 167 | }; 168 | 169 | renderModal = modalData => { 170 | let { submissionData, questions, rowData } = modalData; 171 | let answer = submissionData.submissions.data.answers; 172 | 173 | answer = Object.keys(answer).map(key => { 174 | return answer[key]; 175 | }); 176 | 177 | let qna = []; 178 | 179 | for (let i = 0; i < questions.length; i++) { 180 | qna.push({ 181 | question: questions[i], 182 | answer: answer[i] 183 | }); 184 | } 185 | 186 | return ( 187 | 188 | 189 | Assignment Review 190 | 191 | Student Submission 192 | 193 |
{ 196 | this.setState({ formValue }); 197 | }} 198 | formDefaultValue={{ 199 | comments: rowData.comments 200 | ? rowData.comments 201 | : null, 202 | marks: rowData.marks ? rowData.marks : null 203 | }}> 204 | {qna.map((item, i) => { 205 | return ( 206 |
207 | 208 | Q{i + 1} 209 | {ReactHtmlParser(item.question)} 210 | 222 | 223 |
224 | ); 225 | })} 226 | Marks allocation and Comments 227 | 228 | 229 | 230 | Instructor comment (Optional): 231 | 232 | 233 | 239 | 240 | 241 | 242 | 243 | 244 | Marks{' '} 245 | {rowData.marks ? ( 246 | 247 | {rowData.marks} 248 | 249 | ) : null} 250 | / 100% : 251 | 252 | 253 | 254 | 255 | 256 | 257 | {rowData.marks ? ( 258 | 266 | ) : ( 267 | 276 | )} 277 | 278 |
279 |
280 |
281 | ); 282 | }; 283 | 284 | render() { 285 | const modalData = 286 | this.state.modalData && 287 | this.state.modalData.questions && 288 | this.state.modalData.submissionData 289 | ? this.state.modalData 290 | : null; 291 | let coursesList = this.props.coursesList; 292 | let uid = this.props.authUser ? this.props.authUser.uid : null; 293 | let submissions = []; 294 | let coursesCreated = 295 | this.props.authUser && this.props.authUser.coursesCreated 296 | ? this.props.authUser.coursesCreated 297 | : null; 298 | 299 | if (coursesCreated) { 300 | coursesCreated = Object.keys(coursesCreated).map(key => { 301 | return coursesCreated[key].courseId; 302 | }); 303 | 304 | coursesList = coursesList.filter(item => { 305 | return item.createdBy === uid; 306 | }); 307 | 308 | submissions = coursesList.map(course => { 309 | return { 310 | title: course.title, 311 | submissions: course.submissions 312 | }; 313 | }); 314 | 315 | submissions = submissions.map(item => { 316 | if (item.submissions) { 317 | item.submissions = Object.keys(item.submissions).map( 318 | key => { 319 | item.submissions[key].key = key; 320 | return item.submissions[key]; 321 | } 322 | ); 323 | } 324 | 325 | return item; 326 | }); 327 | if (submissions.submissions) { 328 | submissions.submissions.reverse(); 329 | } 330 | } 331 | 332 | return ( 333 | 335 | {modalData ? this.renderModal(modalData) : null} 336 | 337 | {submissions.map(item => { 338 | return ( 339 | <> 340 |

{item.title}

341 | 342 | 343 | 344 | 345 | Submitted On 346 | 347 | 348 | {rowData => { 349 | const date = new Date( 350 | rowData.submittedOn 351 | ).toLocaleString(); 352 | 353 | return

{date}

; 354 | }} 355 |
356 |
357 | 358 | 362 | 363 | Reviewed On 364 | 365 | 366 | {rowData => { 367 | const reviewedOn = rowData 368 | .reviewedOn 369 | ? new Date( 370 | rowData.reviewedOn 371 | ).toLocaleString() 372 | : null; 373 | 374 | return rowData 375 | .reviewedOn ? ( 376 |

{reviewedOn}

377 | ) : ( 378 |

382 | - 383 |

384 | ); 385 | }} 386 |
387 |
388 | 389 | 390 | 391 | Student Name 392 | 393 | 397 | 398 | 399 | 400 | 401 | Status 402 | 403 | 404 | {rowData => { 405 | return rowData.status === 406 | 'new' ? ( 407 |

408 | Pending Review 409 |

410 | ) : ( 411 |

416 | Reviewed 417 |

418 | ); 419 | }} 420 |
421 |
422 | 423 | 427 | 428 | Action 429 | 430 | 431 | {rowData => { 432 | return ( 433 | 442 | ); 443 | }} 444 | 445 | 446 |
447 |
448 | 449 | ); 450 | })} 451 |
452 | ); 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Assignment/AdminSubmission/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AnnouncementsPanel from '../../../../Announcements/AnnouncementsPanel'; 3 | import mainTemplate from '../../../../Templates/MainTemplate'; 4 | import { Breadcrumb } from 'rsuite'; 5 | import Body from './Body'; 6 | import withCourse from '../../../Context/withCourse'; 7 | import { NavLink } from 'react-router-dom'; 8 | import { withAuthorization } from '../../../../Session'; 9 | import { compose } from 'recompose'; 10 | 11 | const Header = (props) => ( 12 |
13 |
14 | 15 |
16 |
17 | ) 18 | 19 | const BreadCrumb = (props) => ( 20 | 21 | Dashboard 22 | Student Submissions 23 | 24 | ) 25 | 26 | const body = (props) => { 27 | return( 28 |
29 |
30 | 31 |
32 |
33 | ) 34 | } 35 | 36 | 37 | 38 | const WrappedComponents = mainTemplate(Header, BreadCrumb, body); 39 | const condition = authUser => authUser && !!(authUser.roles.userRole === 'admin'); 40 | 41 | export default compose(withCourse, withAuthorization(condition))(WrappedComponents); 42 | -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Assignment/AssignmentModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Modal, 4 | Container, 5 | Uploader, 6 | Form, 7 | FormGroup, 8 | FormControl, 9 | ControlLabel, 10 | SelectPicker, 11 | Button, 12 | ButtonToolbar, 13 | Alert, 14 | Progress 15 | } from 'rsuite'; 16 | 17 | const { Line } = Progress; 18 | 19 | export default class AssignmentModal extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | curriculum: [], 24 | formValue: { 25 | title: '', 26 | curriculum: '', 27 | type: '', 28 | uploaded: false 29 | }, 30 | assFile: null, 31 | uploaderDisabled: false, 32 | uploadProgressShow: false, 33 | uploadProgress: 0 34 | }; 35 | 36 | this.onUploadChange = this.onUploadChange.bind(this); 37 | this.getCurriculums = this.getCurriculums.bind(this); 38 | this.formSubmit = this.formSubmit.bind(this); 39 | this.formNext = this.formNext.bind(this); 40 | } 41 | 42 | getCurriculums() { 43 | //Get curriculums 44 | this.props.firebase 45 | .courseCurriculums('-' + this.props.match.params.id) 46 | .once('value') 47 | .then(snapshot => { 48 | const curriculums = snapshot.val(); 49 | 50 | if (curriculums) { 51 | const objKeys = Object.keys(curriculums); 52 | let curriculum = []; 53 | 54 | objKeys.forEach(item => { 55 | const obj = { 56 | value: item, 57 | label: curriculums[item].title.trim() 58 | }; 59 | curriculum.push(obj); 60 | this.setState({ curriculum }); 61 | }); 62 | } 63 | }); 64 | } 65 | 66 | componentDidMount() { 67 | this.getCurriculums(); 68 | } 69 | 70 | onUploadChange(file) { 71 | if (this.state.assFile) { 72 | this.setState({ 73 | assFile: null 74 | }); 75 | } else { 76 | this.setState({ 77 | assFile: file 78 | }); 79 | } 80 | } 81 | 82 | formSubmit() { 83 | const { title, curriculum, type } = this.state.formValue; 84 | const courseId = '-' + this.props.match.params.id; 85 | const { assFile } = this.state; 86 | 87 | const timestamp = Date.now(); 88 | if (!title || !assFile || !curriculum) { 89 | Alert.error('Please fill in all fields.'); 90 | } else { 91 | const upload = this.props.firebase 92 | .assignmentRef() 93 | .child( 94 | `${this.props.courseTitle}/${timestamp}_${assFile[0].blobFile.name}` 95 | ) 96 | .put(assFile[0].blobFile); 97 | 98 | upload.on( 99 | 'state_changed', 100 | snapshot => { 101 | let uploadProgress = 102 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 103 | this.setState({ uploadProgressShow: true, uploadProgress }); 104 | }, 105 | () => { 106 | Alert.error('Upload failed...Please try again...'); 107 | }, 108 | 109 | async () => { 110 | //Uploaded Successfully... 111 | Alert.success('Created successfully'); 112 | this.setState({ uploaded: true }); 113 | const url = await upload.snapshot.ref 114 | .getDownloadURL() 115 | .then(downloadURL => { 116 | return downloadURL; 117 | }); 118 | 119 | //Store to realtime db 120 | this.props.firebase 121 | .course(courseId) 122 | .child('assignments') 123 | .push( 124 | { 125 | curriId: curriculum.substr(1), 126 | title, 127 | type, 128 | attachmentUrl: url 129 | }, 130 | err => { 131 | if (err) { 132 | Alert.error( 133 | 'An error occured while creating assignment. Please try again.', 134 | 5000 135 | ); 136 | } 137 | } 138 | ) 139 | .then(rsp => { 140 | window.location.reload(false); 141 | }); 142 | } 143 | ); 144 | } 145 | } 146 | 147 | formNext() { 148 | const { title, curriculum, type } = this.state.formValue; 149 | const courseId = '-' + this.props.match.params.id; 150 | 151 | if (!title || !curriculum) { 152 | Alert.error('Please fill in all fields.'); 153 | } else { 154 | this.props.firebase 155 | .course(courseId) 156 | .child('assignments') 157 | .push( 158 | { 159 | curriId: curriculum, 160 | title, 161 | type 162 | }, 163 | err => { 164 | if (err) { 165 | Alert.error( 166 | 'An error occured while creating assignment. Please try again.', 167 | 5000 168 | ); 169 | } 170 | } 171 | ) 172 | .then(rsp => { 173 | const assKey = rsp.key.substr(1); 174 | 175 | this.props.history.push( 176 | `/courses/${courseId.substr(1)}/assignment/${assKey}` 177 | ); 178 | }); 179 | } 180 | } 181 | 182 | render() { 183 | const { curriculum } = this.state; 184 | const { assFile } = this.state; 185 | const uploaderDisabled = assFile ? true : false; 186 | 187 | return ( 188 |
189 | 190 | Create an Assignment 191 | 192 | 193 | 194 |
197 | this.setState({ formValue }) 198 | }> 199 | 200 | 201 | Assignment Title 202 | 203 | 204 | 205 | 206 | 207 | 208 | Assign assignment to curriculum:{' '} 209 | 210 | 216 | 217 | 218 | 219 | 220 | Assignment type:{' '} 221 | 222 | 228 | 229 | 230 | {this.state.formValue.type === 'file' ? ( 231 | 232 | 233 | Assignment File (Optional):{' '} 234 | 235 | 246 | {uploaderDisabled ? ( 247 |
252 | Max one(1) file uploaded. 253 |
254 | ) : ( 255 |
260 | Click or Drag assignment to 261 | this area to upload 262 |
263 | )} 264 |
265 | {this.state.uploadProgressShow ? ( 266 | 272 | ) : ( 273 | '' 274 | )} 275 |
276 | ) : null} 277 | 278 | 279 | {this.state.formValue.type ? ( 280 | 282 | {this.state.formValue.type === 283 | 'file' ? ( 284 | 293 | ) : ( 294 | 300 | )} 301 | 302 | ) : ( 303 | 309 | )} 310 | 311 |
312 |
313 |
314 |
315 |
316 | ); 317 | } 318 | } 319 | 320 | const assignmentType = [ 321 | { 322 | value: 'file', 323 | label: 'Attachment only' 324 | }, 325 | { 326 | value: 'subjective', 327 | label: 'Subjective' 328 | } 329 | ]; 330 | -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Assignment/AssignmentPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SunEditor from "suneditor-react"; 3 | import { compose } from "recompose"; 4 | 5 | import "suneditor/dist/css/suneditor.min.css"; 6 | 7 | import { 8 | Container, 9 | Nav, 10 | Navbar, 11 | Icon, 12 | Button, 13 | Alert, 14 | Form, 15 | ControlLabel, 16 | FormGroup, 17 | FormControl, 18 | ButtonGroup, 19 | Modal, 20 | } from "rsuite"; 21 | import ReactHtmlParser from "react-html-parser"; 22 | import { withAuthentication } from "../../../Session"; 23 | import withAssignment from "./withAssignment"; 24 | 25 | class AssignmentPage extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | 29 | this.state = { 30 | contentState: "", 31 | assignmentsQ: [], 32 | show: false, 33 | }; 34 | } 35 | 36 | componentDidMount() { 37 | const assignmentId = this.props.match.params.id; 38 | const courseId = this.props.match.url 39 | .replace(/\/.*?\//g, "") 40 | .replace(assignmentId, ""); 41 | 42 | this.props.firebase 43 | .assignments(`-${courseId}`) 44 | .child(`-${assignmentId}`) 45 | .child("questions") 46 | .on("value", (snapshot) => { 47 | this.setState({ 48 | assignmentsQ: snapshot.val() ? snapshot.val() : [], 49 | }); 50 | }); 51 | } 52 | 53 | studentHasAttempted = (assignmentId) => { 54 | const uid = this.props.authUser? this.props.firebase.auth.currentUser.uid: null; 55 | let submissions = []; 56 | if(uid){ 57 | this.props.firebase.user(uid).child('enrolledCourses').on('value', snapshot => { 58 | snapshot.forEach(child => { 59 | const submission = child.val().submissions; 60 | if(submission){ 61 | submissions.push(submission) 62 | } 63 | }) 64 | }) 65 | } 66 | 67 | if(submissions.length){ 68 | submissions = submissions.map(item => { 69 | const keys = Object.keys(item); 70 | // return item[key].assId; 71 | return keys.map(key => { 72 | return item[key].assId; 73 | }) 74 | }) 75 | 76 | submissions = submissions.flat(); 77 | } 78 | 79 | return submissions.includes(assignmentId)? true : false; 80 | } 81 | 82 | editorOnChange = (contentState) => { 83 | this.setState({ contentState }); 84 | }; 85 | 86 | saveQuestions = () => { 87 | const assignmentId = this.props.match.params.id; 88 | const courseId = this.props.match.url 89 | .replace(/\/.*?\//g, "") 90 | .replace(assignmentId, ""); 91 | this.props.updateAssignment( 92 | "-" + courseId, 93 | "-" + assignmentId, 94 | this.state.assignmentsQ 95 | ); 96 | this.closeModal(); 97 | }; 98 | 99 | addQuestion = () => { 100 | if (this.state.contentState) { 101 | let assignmentsQ = this.state.assignmentsQ 102 | ? this.state.assignmentsQ 103 | : []; 104 | assignmentsQ.push(this.state.contentState); 105 | this.setState({ assignmentsQ }); 106 | } else { 107 | Alert.error("Content cannot be blank."); 108 | } 109 | 110 | this.setState({ contentState: "" }); 111 | }; 112 | 113 | removeQ = (i) => { 114 | let assignmentsQ = this.state.assignmentsQ; 115 | assignmentsQ = assignmentsQ.filter((ass, index) => { 116 | return index != i; 117 | }); 118 | 119 | this.setState({ assignmentsQ }); 120 | }; 121 | 122 | renderQuestions = (stud = false) => { 123 | const assignmentsQ = this.state.assignmentsQ; 124 | const assignmentId = this.props.match.params.id; 125 | 126 | const attempted = (this.studentHasAttempted(assignmentId)); 127 | const disabledAdmin = this.props.authUser? (this.props.authUser.roles.userRole === 'admin'? true : null) : false; 128 | 129 | return ( 130 | 131 |
132 |

Assignment

133 |
134 | 135 |
{ 137 | this.setState({formValue}) 138 | }} 139 | style={{ marginBottom: "20px" }} 140 | > 141 | {assignmentsQ.map((question, i) => { 142 | return ( 143 | <> 144 | 145 | 146 | Question {i + 1} 147 | 148 | {ReactHtmlParser(question)} 149 | 150 | {!stud ? ( 151 | 157 | ) : null} 158 | 159 | 160 | ); 161 | })} 162 | { 163 | stud? 164 | 165 | {attempted? 166 | 176 | : 185 | } 186 | 187 | 188 | :null 189 | } 190 |
191 |
192 | ); 193 | }; 194 | 195 | closeModal = () => { 196 | this.setState({ show: false }); 197 | }; 198 | 199 | openModal = () => { 200 | this.setState({ show: true }); 201 | }; 202 | 203 | studentSubmitAnswer = () => { 204 | const uid = this.props.firebase.auth.currentUser.uid; 205 | const assId = this.props.match.params.id; 206 | const courseId = this.props.match.url.replace(/\/.*?\//g, '').replace(assId, ''); 207 | 208 | const formValue = this.state.formValue; 209 | if(formValue){ 210 | const authUser = this.props.authUser; 211 | 212 | let enrolledCourses = authUser.enrolledCourses; 213 | const enrolledCoursesKey = Object.keys(authUser.enrolledCourses); 214 | 215 | enrolledCourses = enrolledCoursesKey.map(key => { 216 | return { 217 | snapshotKey: key, 218 | courseId: enrolledCourses[key].courseId 219 | } 220 | }) 221 | 222 | enrolledCourses = enrolledCourses.filter(item =>{ 223 | return item.courseId === courseId; 224 | }) 225 | 226 | const dateNow = Date.now(); 227 | 228 | this.props.firebase.user(uid).child('enrolledCourses').child(enrolledCourses[0].snapshotKey).child('submissions').push({ 229 | status: 'new', 230 | assId, 231 | answers: formValue, 232 | submittedOn: dateNow, 233 | }).then(rsp => { 234 | Alert.success('Successfully submitted'); 235 | 236 | this.props.firebase.course(`-${courseId}`).child('submissions').push({ 237 | status: 'new', 238 | studentId: uid, 239 | studentName: authUser.name, 240 | submittedOn: dateNow, 241 | snapshotKey: rsp.key, 242 | }) 243 | }) 244 | } 245 | else{ 246 | Alert.error('Please answer all questions.'); 247 | } 248 | } 249 | 250 | renderForAdmin = () => { 251 | return ( 252 | <> 253 | 254 | 264 | 265 | 266 | 273 | {this.renderQuestions()} 274 | 275 | 288 | 289 | 290 | 297 | 305 | 311 | 312 | {" "} 319 | Confirm saving/overwriting changes? 320 | 321 | 322 | 328 | 334 | 335 | 336 | 337 | 338 | 339 | ); 340 | }; 341 | 342 | renderForStudent = () => { 343 | return ( 344 | <> 345 | 346 | 356 | 357 | 358 | 366 | {this.renderQuestions(true)} 367 | 368 | 369 | ); 370 | }; 371 | 372 | render() { 373 | const authUser = this.props.authUser; 374 | const hasEditAuthority = authUser && authUser.roles.userRole === "admin"; 375 | let coursesCreated = null; 376 | let isCourseCreator = null; 377 | 378 | if (hasEditAuthority) { 379 | const coursesCreatedKey = authUser.coursesCreated? Object.keys(authUser.coursesCreated) : null; 380 | 381 | if(coursesCreatedKey){ 382 | coursesCreated = Object.keys( 383 | authUser.coursesCreated 384 | ).map((key) => ({ 385 | ...authUser.coursesCreated[key], 386 | uid: key, 387 | })); 388 | coursesCreated = coursesCreated.map((item) => { 389 | return item.courseId; 390 | }); 391 | 392 | const curriId = this.props.match.params.id; 393 | const courseId = this.props.match.url 394 | .replace(/\/.*?\//g, "") 395 | .replace(curriId, ""); 396 | 397 | isCourseCreator = coursesCreated.includes(courseId); 398 | } 399 | } 400 | 401 | return ( 402 | 403 | {isCourseCreator 404 | ? this.renderForAdmin() 405 | : this.renderForStudent()} 406 | 407 | ); 408 | } 409 | } 410 | 411 | export default compose( 412 | withAuthentication, 413 | withAssignment) 414 | (AssignmentPage); 415 | -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Assignment/withAssignment.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withFirebase } from '../../../Firebase'; 3 | import { Alert } from 'rsuite'; 4 | 5 | const withAssignment = Component => { 6 | class WithAssignment extends React.Component{ 7 | updateAssignment = (courseId, assignmentId, questions) => { 8 | questions = questions? questions: []; 9 | this.props.firebase.assignments(courseId).child(assignmentId).update({questions}).then(()=>{ 10 | Alert.success('Successfully saved'); 11 | }).catch(err => {Alert.error('Save failed... Please try again.')}) 12 | } 13 | 14 | render(){ 15 | return( 16 | 20 | ) 21 | } 22 | } 23 | 24 | return withFirebase(WithAssignment); 25 | } 26 | 27 | export default withAssignment; -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Body.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Container, 4 | Button, 5 | Icon, 6 | Panel, 7 | PanelGroup, 8 | Modal, 9 | Alert, 10 | Loader, 11 | Table 12 | } from 'rsuite'; 13 | import CurriculumModal from './Curriculum/CurriculumModal'; 14 | import { NavLink, Link } from 'react-router-dom'; 15 | import AssignmentModal from './Assignment/AssignmentModal'; 16 | 17 | export default class Body extends React.Component { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | course : null, 22 | loading : true, 23 | authUser : null, 24 | panelNavigate : '', 25 | showCurriculumModal : false, 26 | showAssignmentModal : false, 27 | }; 28 | 29 | this.panelSelect = this.panelSelect.bind(this); 30 | this.enrollCourse = this.enrollCourse.bind(this); 31 | this.renderCurriculum = this.renderCurriculum.bind(this); 32 | this.retrievePageCourse = this.retrievePageCourse.bind(this); 33 | this.curriculumModalToggle = this.curriculumModalToggle.bind(this); 34 | this.assignmentModalToggle = this.assignmentModalToggle.bind(this); 35 | } 36 | componentWillMount() { 37 | this.retrievePageCourse(this.props.match.params.id); 38 | window.scrollTo(0, 0); 39 | } 40 | 41 | retrievePageCourse(id) { 42 | const coursesList = this.props.coursesList 43 | ? this.props.coursesList 44 | : []; 45 | 46 | if (coursesList) { 47 | let foundCourse = null; 48 | 49 | for (let i = 0; i < coursesList.length; i++) { 50 | if (coursesList[i].uid === `-${id}`) { 51 | foundCourse = coursesList[i]; 52 | break; 53 | } 54 | } 55 | 56 | this.setState({ course: foundCourse }); 57 | } 58 | } 59 | 60 | panelSelect(key) { 61 | this.setState({ panelNavigate: key }); 62 | } 63 | 64 | curriculumModalToggle() { 65 | this.setState({ showCurriculumModal: !this.state.showCurriculumModal }); 66 | } 67 | 68 | assignmentModalToggle() { 69 | this.setState({ showAssignmentModal: !this.state.showAssignmentModal }); 70 | } 71 | 72 | renderEnrollButton(isCourseCreator, authUser, enrolledToCourse, courseId) { 73 | if (isCourseCreator) { 74 | return ( 75 | <> 76 | 85 | 94 | 95 | 96 | ); 97 | } else if ( 98 | (authUser && authUser.roles.userRole === 'admin') || enrolledToCourse) { 99 | return ( 100 | 109 | ); 110 | } else { 111 | return ( 112 | 122 | ); 123 | } 124 | } 125 | 126 | enrollCourse() { 127 | const authUser = this.state.authUser; 128 | 129 | if (!authUser) { 130 | Alert.error('Login first') 131 | this.props.history.push('/login'); 132 | } else { 133 | this.props.firebase 134 | .user(authUser.uid) 135 | .child('enrolledCourses') 136 | .push({ courseId: this.props.match.params.id }, err => { 137 | if (err) { 138 | Alert.error( 139 | 'There was an error registering to courses...' 140 | ); 141 | } 142 | }) 143 | .then(rsp => { 144 | const date = Date.now(); 145 | 146 | Alert.success('Successfully enrolled to course.'); 147 | this.props.firebase.course(`-${this.props.match.params.id}`).child('students').push({ 148 | studId: authUser.uid, 149 | joinedOn: date, 150 | }) 151 | }); 152 | } 153 | } 154 | 155 | renderCurriculum( allowPreview, isCourseCreator, item, enrolledToCourse) { 156 | if (allowPreview || isCourseCreator || enrolledToCourse) { 157 | return ( 158 | 160 | {item.shortDescription} 161 | 162 | ); 163 | } else { 164 | return

{item.shortDescription}

; 165 | } 166 | } 167 | 168 | renderAssignments = (assignment) => { 169 | return( 170 | 172 | {assignment.title} 173 | 174 | ) 175 | } 176 | 177 | componentDidMount() { 178 | this.listener = this.props.firebase.onAuthUserListener( 179 | authUser => { 180 | return this.setState({ authUser, loading: false }); 181 | }, 182 | () => { 183 | this.setState({ authUser: null, loading: false }); 184 | } 185 | ); 186 | } 187 | 188 | render() { 189 | let coursesCreated = null; 190 | let enrolledToCourse = null; 191 | let isCourseCreator = null; 192 | const courseId = this.props.match.params.id; 193 | const authUser = this.state.authUser ? this.state.authUser : null; 194 | console.log(this.state.course) 195 | let { 196 | levelOfStudy, 197 | description, 198 | image, 199 | duration, 200 | prerequisite, 201 | title, 202 | curriculum, 203 | assignments, 204 | qualification 205 | } = this.state.course ? this.state.course : {}; 206 | 207 | if (this.state.course) { 208 | if (curriculum) { 209 | curriculum = Object.keys(curriculum).map(key => ({ 210 | ...curriculum[key], 211 | curriculumId: key.substr(1) 212 | })); 213 | } 214 | 215 | if(assignments){ 216 | assignments = Object.keys(assignments).map(key => ({ 217 | ...assignments[key], 218 | assignmentId: key 219 | })); 220 | } 221 | 222 | if (authUser && !!authUser.coursesCreated) { 223 | coursesCreated = Object.keys(authUser.coursesCreated).map( 224 | key => ({ 225 | ...authUser.coursesCreated[key], 226 | uid: key 227 | }) 228 | ); 229 | 230 | coursesCreated = coursesCreated.map(item => { 231 | return item.courseId; 232 | }); 233 | 234 | isCourseCreator = coursesCreated.includes( 235 | this.props.match.params.id 236 | ); 237 | } 238 | } 239 | 240 | if (authUser && authUser.enrolledCourses) { 241 | enrolledToCourse = authUser.enrolledCourses; 242 | const keys = Object.keys(enrolledToCourse); 243 | 244 | enrolledToCourse = keys.map(key => { 245 | return enrolledToCourse[key]; 246 | }); 247 | 248 | enrolledToCourse = enrolledToCourse.map(item => { 249 | return item.courseId; 250 | }); 251 | 252 | enrolledToCourse = enrolledToCourse.includes(courseId); 253 | } 254 | return ( 255 |
256 |
257 | 263 | 264 | 265 | 266 | 272 | 273 | 274 |
275 | 276 | 281 | 286 | 291 |

{title}

292 |

297 | {description} 298 |

299 |
300 | 301 | 306 |

Duration

307 |

312 | {duration} Hours 313 |

314 |
315 | 316 | 321 |

Academic Qualification

322 |

327 | {qualification[0].toUpperCase() + qualification.substr(1)} 328 |

329 |
330 | 331 | 336 |

337 | Requirements 338 |

339 |

344 | {prerequisite} 345 |

346 |
347 | 348 | {this.state.loading ? ( 349 | 350 | ) : ( 351 | <> 352 | 353 |

358 | Curriculum 359 |

360 | 367 | {curriculum ? ( 368 | curriculum.map((item, i) => { 369 | const allowPreview = !!item.allowPreview; 370 | return ( 371 | 377 | {this.renderCurriculum( 378 | allowPreview, 379 | isCourseCreator, 380 | item, 381 | enrolledToCourse, 382 | )} 383 | 384 | ); 385 | }) 386 | ) : ( 387 | 388 | 389 | 390 | )} 391 | {authUser && isCourseCreator ? ( 392 | 397 | ) : null} 398 | 399 |
400 | 401 |

406 | Assignments 407 |

408 | { (enrolledToCourse || (authUser && authUser.roles.userRole) === 'admin')? 409 | 410 | 411 | 412 | Id 413 | 414 | 415 | 416 | 417 | Assignment Title 418 | 419 | 420 | 421 | 422 | Type 423 | 424 | 425 | 426 | 427 | Action 428 | 429 | {rowData => { 430 | if(rowData.type === 'subjective'){ 431 | return( 432 | 433 | 434 | 435 | ) 436 | } 437 | else{ 438 | return( 439 | 440 | 441 | 442 | ) 443 | } 444 | }} 445 | 446 | 447 |
448 | {authUser && isCourseCreator ? ( 449 | 456 | ) : null} 457 |
458 | :

Enroll to view assignments...

459 | } 460 | {/* 465 | {assignments ? ( 466 | assignments.map((item, i) => { 467 | let curi = curriculum.filter(curri => { 468 | return curri.curriculumId === item.curriId.substr(1); 469 | }) 470 | 471 | return ( 472 | 476 | {this.renderAssignments(item)} 477 | 478 | ); 479 | }) 480 | ) : ( 481 | 482 | 483 | 484 | )} 485 | {authUser && isCourseCreator ? ( 486 | 493 | ) : null} 494 | */} 495 |
496 | 497 | )} 498 |
499 |
500 | 501 | 507 |
508 | 514 |
515 | 516 | 523 | 527 |

528 | {levelOfStudy} 529 |

530 |
531 | 532 | {this.renderEnrollButton( 533 | isCourseCreator, 534 | authUser, 535 | enrolledToCourse, 536 | courseId 537 | )} 538 | 539 |
540 |
541 |
542 |
543 | ); 544 | } 545 | } -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Curriculum/CurriculumModal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Modal, 4 | Form, 5 | FormGroup, 6 | ControlLabel, 7 | FormControl, 8 | Container, 9 | Button, 10 | ButtonToolbar, 11 | Alert, 12 | Toggle, 13 | } from "rsuite"; 14 | 15 | export default class CurriculumModal extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | editorContext: null, 20 | content: null, 21 | formValue: { 22 | title: "", 23 | shortDescription: "", 24 | allowPreview: false, 25 | } 26 | }; 27 | this.formSubmit = this.formSubmit.bind(this); 28 | } 29 | 30 | close() { 31 | this.setState({ show: false }); 32 | } 33 | open(size) { 34 | this.setState({ show: true }); 35 | } 36 | 37 | onDanteSaveHandler(editorContext, content) { 38 | this.setState({ editorContext, content }); 39 | } 40 | 41 | formSubmit() { 42 | let { title, shortDescription, allowPreview } = this.state.formValue; 43 | const courseId = this.props.match.params.id; 44 | allowPreview = !!(allowPreview); 45 | 46 | if (!title || !shortDescription) { 47 | Alert.error("Title and description cannot be empty.", 5000); 48 | } else { 49 | this.props.firebase 50 | .courseCurriculums(`-${courseId}`) 51 | .push({ title, shortDescription, allowPreview }, err => { 52 | if (err) { 53 | Alert.error( 54 | "An error occurred while creating curriculum. Please try again.", 55 | 5000 56 | ); 57 | } 58 | }) 59 | .then(rsp => { 60 | const curriculumId = rsp.key.substr(1, rsp.key.length); 61 | this.props.history.push( 62 | `/courses/${courseId}/curriculum/${curriculumId}` 63 | ); 64 | }); 65 | } 66 | } 67 | 68 | render() { 69 | return ( 70 |
71 | 72 | Add Curriculum 73 | 74 | 75 | 76 | 77 |
this.setState({ formValue })}> 78 | 79 | Title 80 | 81 | 82 | 83 | 84 | Short Description 85 | 86 | 87 | 88 | 89 | Allow Preview? 90 |

Enabling this option will allow non-login users to access the curriculum.

91 | 96 |
97 | 98 | 99 | 100 | 107 | 108 | 109 |
110 |
111 |
112 |
113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Curriculum/CurriculumPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SunEditor from 'suneditor-react'; 3 | import { compose } from 'recompose'; 4 | 5 | import 'suneditor/dist/css/suneditor.min.css'; 6 | 7 | import { Container, Nav, Navbar, Icon } from 'rsuite'; 8 | import withCurriculum from './withCurriculum'; 9 | import { AuthUserContext } from '../../../Session'; 10 | 11 | class CurriculumPage extends React.Component{ 12 | constructor(props){ 13 | super(props); 14 | 15 | this.state = { 16 | contentState: '', 17 | sunEditor: { 18 | editing: false, 19 | resizingBar: false, 20 | enable: false, 21 | showToolbar: false, 22 | disable: true, 23 | } 24 | } 25 | 26 | this.editorOnChange = this.editorOnChange.bind(this); 27 | this.editorSave = this.editorSave.bind(this); 28 | this.editToggle = this.editToggle.bind(this); 29 | } 30 | 31 | componentDidMount(){ 32 | const curriId = this.props.match.params.id; 33 | const courseId = this.props.match.url.replace(/\/.*?\//g, '').replace(curriId, ''); 34 | this.props.getCurriculum(courseId, curriId); 35 | 36 | this.props.firebase.courseCurriculums(`-${courseId}`) 37 | .child(`-${curriId}`) 38 | .child('curriculumContent') 39 | .on('value', snapshot=> { 40 | this.setState({contentState: snapshot.val()}) 41 | } 42 | ) 43 | } 44 | 45 | editToggle(){ 46 | this.setState({ 47 | sunEditor:{ 48 | editing: !this.state.sunEditor.editing, 49 | resizingBar: !this.state.sunEditor.resizingBar, 50 | enable: !this.state.sunEditor.enable, 51 | showToolbar: !this.state.sunEditor.showToolbar, 52 | disable: !this.state.sunEditor.disable, 53 | } 54 | }) 55 | } 56 | 57 | editorOnChange(content){ 58 | this.setState({contentState: content}); 59 | } 60 | 61 | editorSave(){ 62 | const curriId = this.props.match.params.id; 63 | const courseId = this.props.match.url.replace(/\/.*?\//g, '').replace(curriId, ''); 64 | this.props.updateCurriculum('-'+courseId, '-'+curriId, this.state.contentState); 65 | } 66 | 67 | render(){ 68 | return( 69 | 70 | {authUser => { 71 | const hasEditAuthority = authUser && authUser.roles.userRole === 'admin'; 72 | let coursesCreated = null; 73 | let isCourseCreator = null; 74 | 75 | if(hasEditAuthority){ 76 | coursesCreated = Object.keys(authUser.coursesCreated).map( 77 | key => ({ 78 | ...authUser.coursesCreated[key], 79 | uid: key 80 | }) 81 | ); 82 | coursesCreated = coursesCreated.map(item => { 83 | return item.courseId; 84 | }); 85 | 86 | const curriId = this.props.match.params.id; 87 | const courseId = this.props.match.url.replace(/\/.*?\//g, '').replace(curriId, ''); 88 | 89 | isCourseCreator = coursesCreated.includes( 90 | courseId 91 | ); 92 | } 93 | 94 | return( 95 | 96 | 97 | 100 | { 101 | isCourseCreator? 102 | (this.state.sunEditor.editing? 103 |
104 | 107 | 110 |
111 | : 112 |
113 | 116 |
117 | ) 118 | : 119 | ( 120 | '' 121 | ) 122 | 123 | } 124 | 125 |
126 | 127 | 128 | 159 | 160 | 161 |
162 | ) 163 | }} 164 | 165 | 166 |
167 | ) 168 | } 169 | } 170 | 171 | // const condition = authUser => !!authUser; 172 | 173 | export default compose( 174 | withCurriculum, 175 | )(CurriculumPage); 176 | -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/Curriculum/withCurriculum.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withFirebase } from '../../../Firebase'; 3 | import { Alert } from 'rsuite'; 4 | 5 | const withCurriculum = Component => { 6 | class WithCurriculum extends React.Component{ 7 | constructor(props){ 8 | super(props); 9 | 10 | this.state = { 11 | content: null, 12 | } 13 | this.getCurriculum = this.getCurriculum.bind(this); 14 | this.updateCurriculum = this.updateCurriculum.bind(this); 15 | 16 | } 17 | 18 | getCurriculum(courseId, curriId){ 19 | this.props.firebase.courseCurriculums(courseId).child(curriId).child('curriculumContent').once('value').then(snapshot => { 20 | this.setState({content: snapshot.val()}); 21 | }) 22 | } 23 | 24 | updateCurriculum(courseId, curriId, content){ 25 | this.props.firebase.courseCurriculums(courseId).child(curriId).update({curriculumContent: content}).then(()=>{ 26 | Alert.success('Successfully saved'); 27 | }).catch(err => {Alert.error('Save failed... Please try again.')}) 28 | } 29 | 30 | render(){ 31 | return( 32 | 37 | ) 38 | } 39 | } 40 | 41 | return withFirebase(WithCurriculum); 42 | } 43 | 44 | export default withCurriculum; -------------------------------------------------------------------------------- /src/Components/Courses/CoursePage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AnnouncementsPanel from '../../Announcements/AnnouncementsPanel'; 3 | import mainTemplate from '../../Templates/MainTemplate'; 4 | import { Breadcrumb } from 'rsuite'; 5 | import Body from './Body'; 6 | import withCourse from '../Context/withCourse'; 7 | import { NavLink } from 'react-router-dom'; 8 | 9 | const Header = (props) => ( 10 |
11 |
12 | 13 |
14 |
15 | ) 16 | 17 | const BreadCrumb = (props) => ( 18 | 19 | Dashboard 20 | Courses 21 | 22 | ) 23 | 24 | const body = (props) => { 25 | return( 26 |
27 |
28 | 29 |
30 |
31 | ) 32 | } 33 | 34 | 35 | 36 | const WrappedComponents = mainTemplate(Header, BreadCrumb, body); 37 | 38 | export default withCourse(WrappedComponents); 39 | -------------------------------------------------------------------------------- /src/Components/Courses/CourseShowcase.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Panel } from 'rsuite'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | export default class CourseShowcase extends Component{ 6 | 7 | render(){ 8 | const course = this.props.course; 9 | return( 10 |
11 | 12 | 13 | 18 | 19 |
20 |

{course.title}

21 |
22 |

23 | {course.description} 24 |

25 |
26 |
27 |
28 |
29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /src/Components/Courses/EditCourses/Body.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Container, 4 | FormGroup, 5 | Form, 6 | FormControl, 7 | ControlLabel, 8 | SelectPicker, 9 | Button, 10 | ButtonToolbar, 11 | Alert, 12 | Uploader, 13 | InputNumber, 14 | } from "rsuite"; 15 | 16 | const qualificationData = [ 17 | { label: "Primary School", value: "primary-school" }, 18 | { label: "High School", value: "high-school" }, 19 | { label: "College", value: "college" }, 20 | { label: "Degree", value: "degree" }, 21 | { label: "Masters", value: "master" }, 22 | { label: "Other", value: "other" } 23 | ]; 24 | 25 | const difficultyLevel = [ 26 | { label: "Fundamental", value: "fundamental" }, 27 | { label: "Intermediate", value: "intermediate" }, 28 | { label: "Advanced", value: "advanced" }, 29 | { label: "Fundamental-Intermediate", value: "fundamental-intermediate" }, 30 | { label: "Fundamental-Advanced", value: "fundamental-advanced" }, 31 | { label: "Other", value: "other" } 32 | ]; 33 | 34 | class Body extends React.Component { 35 | constructor(props) { 36 | super(props); 37 | this.state = { 38 | course: null, 39 | showError: false, 40 | errorPlacement: "bottomStart", 41 | formValue: { 42 | title: "", 43 | qualification: "", 44 | levelOfStudy: "", 45 | description: "", 46 | prerequisite: "" 47 | }, 48 | featureImage: null, 49 | uploading: false, 50 | setFileInfo: null, 51 | }; 52 | this.formSubmit = this.formSubmit.bind(this); 53 | this.retrievePageCourse = this.retrievePageCourse.bind(this); 54 | } 55 | 56 | retrievePageCourse(id) { 57 | const coursesList = this.props.coursesList? this.props.coursesList : []; 58 | if(coursesList){ 59 | let foundCourse = null; 60 | for (let i = 0; i < coursesList.length; i++) { 61 | if (coursesList[i].uid === `-${id}`) { 62 | foundCourse = coursesList[i]; 63 | break; 64 | } 65 | } 66 | this.setState({ course: foundCourse }); 67 | } 68 | } 69 | 70 | formSubmit() { 71 | const formValue = this.state.formValue; 72 | const course = this.state.course; 73 | 74 | const formValueKeys = Object.keys(formValue); 75 | 76 | formValueKeys.forEach(key=> { 77 | if(formValue[key]){ 78 | course[key] = formValue[key] 79 | } 80 | }) 81 | 82 | this.props.firebase 83 | .course(`-${this.props.match.params.id}`) 84 | .update( 85 | { 86 | title: course.title, 87 | qualification: course.qualification, 88 | levelOfStudy: course.levelOfStudy, 89 | description: course.description, 90 | prerequisite: course.prerequisite, 91 | image: this.state.setFileInfo? this.state.setFileInfo : course.image 92 | }, 93 | err => { 94 | if (err) { 95 | Alert.error( 96 | "An error occurred while creating course. Please try again.", 97 | 5000 98 | ); 99 | } 100 | } 101 | ) 102 | .then(rsp => { 103 | this.props.history.push(`/courses/${this.props.match.params.id}`); 104 | }); 105 | 106 | } 107 | 108 | previewFile(file, callback) { 109 | const reader = new FileReader(); 110 | reader.onloadend = () => { 111 | callback(reader.result); 112 | }; 113 | reader.readAsDataURL(file); 114 | } 115 | 116 | componentDidMount(){ 117 | this.retrievePageCourse(this.props.match.params.id); 118 | } 119 | 120 | render() { 121 | const course = this.state.course? this.state.course: null; 122 | 123 | return ( 124 |
125 | {course? 126 | 127 | 128 | { 134 | this.setState({ uploading: true }); 135 | this.previewFile(file.blobFile, value => { 136 | this.setState({ setFileInfo: value}); 137 | }); 138 | }} 139 | > 140 | 152 | 153 | 154 | 155 | 156 |
this.setState({ formValue })}> 157 | 158 | Course Title 159 | 162 | 163 | 164 | Qualification 165 | 172 | 173 | 174 | Level 175 | 183 | 184 | 185 | Duration (Hours) 186 | 192 | 193 | 194 | Course Description 195 | 201 | 202 | 203 | Prerequisites 204 | 211 | 212 | 213 | 214 | 217 | 218 | 219 |
220 |
221 |
222 | :

Loading...

223 | } 224 |
225 | ); 226 | } 227 | } 228 | 229 | export default Body; -------------------------------------------------------------------------------- /src/Components/Courses/EditCourses/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {compose} from 'recompose'; 3 | import mainTemplate from '../../Templates/MainTemplate' 4 | import {withFirebase} from '../../Firebase'; 5 | import {withAuthorization} from '../../Session' 6 | import {NavLink} from 'react-router-dom'; 7 | import AnnouncementsPanel from '../../Announcements/AnnouncementsPanel'; 8 | import { Breadcrumb } from 'rsuite'; 9 | import Body from './Body'; 10 | import withCourse from '../Context/withCourse'; 11 | 12 | const Header = (props) => ( 13 |
14 |
15 | 16 |
17 |
18 | ) 19 | 20 | const BreadCrumb = () => ( 21 | 22 | Dashboard 23 | Courses 24 | Edit Course 25 | 26 | ) 27 | 28 | const condition = authUser => authUser && !!(authUser.roles['userRole'] === 'admin'); 29 | 30 | let EditCoursePage = mainTemplate(Header, BreadCrumb, Body); 31 | 32 | export default compose( 33 | withAuthorization(condition), 34 | withCourse, 35 | withFirebase, 36 | )(EditCoursePage); 37 | 38 | -------------------------------------------------------------------------------- /src/Components/Dashboard/MainPalette/AdminCoursePalette.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withFirebase } from '../../Firebase'; 3 | import { FlexboxGrid, List, Button } from 'rsuite'; 4 | import { NavLink, Link } from 'react-router-dom'; 5 | 6 | class AdminCoursePalette extends React.Component{ 7 | constructor(props){ 8 | super(props); 9 | 10 | this.state = { 11 | courses: [], 12 | } 13 | } 14 | async componentDidMount(){ 15 | const createdCourses = this.props.authUser.coursesCreated; 16 | let coursesList = []; 17 | let courses = null; 18 | 19 | if(createdCourses){ 20 | coursesList = Object.keys(createdCourses).map(key => 21 | createdCourses[key].courseId 22 | ); 23 | 24 | coursesList = coursesList.map(id => { 25 | return new Promise(rs => { 26 | this.props.firebase.course(`-${id}`) 27 | .once('value') 28 | .then(snapshot => { 29 | const course = snapshot.val(); 30 | rs({course, id}); 31 | }) 32 | }) 33 | }) 34 | 35 | courses = await Promise.all(coursesList).then(courses =>{ 36 | return courses; 37 | }) 38 | 39 | this.setState({courses}); 40 | } 41 | 42 | 43 | } 44 | 45 | render(){ 46 | const courses = this.state.courses ? this.state.courses: []; 47 | return( 48 |
49 |
50 |

My Created Courses

51 |
52 |
53 | { 54 | courses? 55 | 56 | { 57 | courses.map((course, i)=> { 58 | let curriculum = course.course.curriculum || {}; 59 | curriculum = curriculum? Object.keys(curriculum).length: 0; 60 | let students = course.course.students? Object.keys(course.course.students).length : 0; 61 | return( 62 | <> 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 76 |
77 | {course.course.title} 78 |
79 |
80 | 81 | 82 |
83 |
Students
84 |
{students}
85 |
86 |
87 | 88 | 89 |
90 |
Curriculums
91 |
{curriculum}
92 |
93 |
94 | 95 | 98 | 104 | 107 | 108 | 109 |
110 |
111 |
112 | 113 | ) 114 | }) 115 | } 116 |
117 | :

No courses created...

118 | } 119 |
120 |
121 | ) 122 | } 123 | } 124 | 125 | const styleCenter = { 126 | display: 'flex', 127 | justifyContent: 'center', 128 | alignItems: 'center', 129 | height: '60px' 130 | }; 131 | 132 | const slimText = { 133 | fontSize: '0.666em', 134 | color: '#97969B', 135 | fontWeight: 'lighter', 136 | paddingBottom: 5 137 | }; 138 | 139 | const titleStyle = { 140 | padding: 5, 141 | whiteSpace: 'nowrap', 142 | fontWeight: 500, 143 | fontSize: '14px' 144 | }; 145 | 146 | const dataStyle = { 147 | fontSize: '1.2em', 148 | fontWeight: 500 149 | }; 150 | export default withFirebase(AdminCoursePalette); -------------------------------------------------------------------------------- /src/Components/Dashboard/MainPalette/CoursePanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AdminCoursePalette from './AdminCoursePalette'; 3 | import StudentCoursePalette from './StudentCoursePalette'; 4 | 5 | class CoursePanel extends React.Component{ 6 | render(){ 7 | const authUser = this.props.authUser; 8 | return( 9 | authUser.roles.userRole === 'admin'? 10 | 11 | : 12 | ) 13 | } 14 | } 15 | 16 | export default CoursePanel; -------------------------------------------------------------------------------- /src/Components/Dashboard/MainPalette/StudentCoursePalette.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withFirebase } from '../../Firebase'; 3 | import { Button, Panel, Table, Modal } from 'rsuite'; 4 | import { compose } from 'recompose'; 5 | import withCourse from '../../Courses/Context/withCourse'; 6 | 7 | class StudentCoursePalette extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | courses: [], 13 | show: false 14 | }; 15 | } 16 | async componentDidMount() { 17 | const enrolledCourses = this.props.authUser.enrolledCourses; 18 | const coursesList = this.props.coursesList; 19 | 20 | let submissions = []; 21 | 22 | if (enrolledCourses) { 23 | submissions = Object.keys(enrolledCourses).map(key => { 24 | const courseId = enrolledCourses[key].courseId; 25 | 26 | const course = coursesList.filter(item => { 27 | return item.uid === `-${courseId}`; 28 | }); 29 | 30 | return { 31 | courseId: enrolledCourses[key].courseId, 32 | courseTitle: course[0].title, 33 | submissions: enrolledCourses[key].submissions 34 | }; 35 | }); 36 | 37 | //Extract submissions innerkey 38 | submissions = submissions.map(item => { 39 | if (item.submissions) { 40 | const key = Object.keys(item.submissions); 41 | 42 | const submission = key.map(key => { 43 | return item.submissions[key]; 44 | }); 45 | 46 | return { 47 | courseId: item.courseId, 48 | courseTitle: item.courseTitle, 49 | submissions: submission 50 | }; 51 | } 52 | 53 | return { 54 | courseId: item.courseId, 55 | courseTitle: item.courseTitle 56 | }; 57 | }); 58 | this.setState({ submissions }); 59 | 60 | submissions = submissions 61 | .map(item => { 62 | if (item.submissions) { 63 | item.submissions = item.submissions.map(inner => { 64 | const assId = '-' + inner.assId; 65 | 66 | const course = coursesList.filter(course => { 67 | return course.uid === `-${item.courseId}`; 68 | }); 69 | 70 | let assignment = course[0].assignments; 71 | const keys = Object.keys(assignment); 72 | 73 | assignment = keys.map(key => { 74 | return { 75 | key, 76 | data: assignment[key] 77 | }; 78 | }); 79 | 80 | assignment = assignment.filter(ass => { 81 | return ass.key === assId; 82 | }); 83 | 84 | assignment = assignment[0].data; 85 | 86 | return { 87 | ...inner, 88 | assTitle: assignment.title 89 | }; 90 | }); 91 | return; 92 | } 93 | }) 94 | .flat(); 95 | } 96 | } 97 | 98 | open = () => { 99 | this.setState({ show: true }); 100 | }; 101 | 102 | close = () => { 103 | this.setState({ show: false }); 104 | }; 105 | 106 | render() { 107 | const submissions = this.state.submissions 108 | ? this.state.submissions 109 | : []; 110 | 111 | return ( 112 |
113 |
114 |

My Submissions

115 |
116 |
117 | {submissions 118 | ? submissions.map(submission => { 119 | return ( 120 | 123 | 126 | 127 | 128 | Submitted On 129 | 130 | 131 | {rowData => { 132 | const date = new Date( 133 | rowData.submittedOn 134 | ).toLocaleString(); 135 | 136 | return

{date}

; 137 | }} 138 |
139 |
140 | 141 | 142 | Assignment Title 143 | 144 | 148 | 149 | 150 | 151 | Status 152 | 153 | 154 | {rowData => { 155 | return rowData.status === 156 | 'new' ? ( 157 |

161 | Pending Review 162 |

163 | ) : ( 164 |

168 | Reviewed 169 |

170 | ); 171 | }} 172 |
173 |
174 | 178 | 179 | Reviewed On 180 | 181 | 182 | {rowData => { 183 | const reviewedOn = rowData.reviewedOn 184 | ? new Date( 185 | rowData.reviewedOn 186 | ).toLocaleString() 187 | : null; 188 | 189 | return rowData.reviewedOn ? ( 190 |

{reviewedOn}

191 | ) : ( 192 |

196 | - 197 |

198 | ); 199 | }} 200 |
201 |
202 | 206 | 207 | Marks 208 | 209 | 210 | {rowData => { 211 | return rowData.marks ? ( 212 |

217 | {rowData.marks}% 218 |

219 | ) : ( 220 |

224 | - 225 |

226 | ); 227 | }} 228 |
229 |
230 | 234 | 235 | Instructor Comments 236 | 237 | 238 | {rowData => { 239 | return rowData.comments ? ( 240 | <> 241 | 248 | 258 | 259 | <> 260 |

261 | Instructor 262 | comments 263 |

264 |

{ 265 | rowData.comments 266 | }

267 | 268 |
269 | 270 | 278 | 279 |
280 | 281 | ) : ( 282 |

286 | - 287 |

288 | ); 289 | }} 290 |
291 |
{' '} 292 | */} 293 |
294 |
295 | ); 296 | }) 297 | : null} 298 |
299 |
300 | ); 301 | } 302 | } 303 | 304 | export default compose(withFirebase, withCourse)(StudentCoursePalette); 305 | -------------------------------------------------------------------------------- /src/Components/Dashboard/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withAuthorization } from '../Session'; 3 | 4 | import AnnouncementsPanel from '../Announcements/AnnouncementsPanel'; 5 | import mainTemplate from '../Templates/MainTemplate'; 6 | import { Breadcrumb } from 'rsuite'; 7 | import CoursePanel from './MainPalette/CoursePanel'; 8 | 9 | const Header = (props) => ( 10 |
11 |
12 | 13 |
14 |
15 | ) 16 | 17 | const BreadCrumb = () => ( 18 | 19 | Dashboard 20 | 21 | ) 22 | 23 | const Body = (props) => ( 24 |
25 |
26 | 27 |
28 |
29 | ) 30 | 31 | const condition = authUser => !!authUser; 32 | 33 | const WrappedComponents = mainTemplate(Header, BreadCrumb, Body); 34 | 35 | export default withAuthorization(condition)(WrappedComponents); -------------------------------------------------------------------------------- /src/Components/Firebase/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FirebaseContext = React.createContext(null); 4 | 5 | export const withFirebase = Component => props => ( 6 | 7 | {firebase => } 8 | 9 | ); 10 | 11 | export default FirebaseContext; 12 | -------------------------------------------------------------------------------- /src/Components/Firebase/firebase.js: -------------------------------------------------------------------------------- 1 | import app from "firebase/app"; 2 | import "firebase/auth"; 3 | import "firebase/database"; 4 | import 'firebase/storage'; 5 | import { Alert } from "rsuite"; 6 | 7 | const config = { 8 | apiKey: process.env.REACT_APP_API_KEY, 9 | authDomain: process.env.REACT_APP_AUTH_DOMAIN, 10 | databaseURL: process.env.REACT_APP_DATABASE_URL, 11 | projectId: process.env.REACT_APP_PROJECT_ID, 12 | storageBucket: process.env.REACT_APP_STORAGE_BUCKET, 13 | messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, 14 | appId: process.env.REACT_APP_APP_ID 15 | }; 16 | 17 | class Firebase { 18 | constructor() { 19 | app.initializeApp(config); 20 | this.auth = app.auth(); 21 | this.db = app.database(); 22 | this.storage = app.storage(); 23 | } 24 | 25 | doCreateUserWithEmailAndPassword = (email, password) => { 26 | return this.auth.createUserWithEmailAndPassword(email, password); 27 | }; 28 | doSignInWithEmailAndPassword = (email, password) => { 29 | return this.auth.signInWithEmailAndPassword(email, password); 30 | }; 31 | 32 | doSignOut = () => { 33 | this.auth.signOut(); 34 | Alert.success('Successfully signed out...') 35 | }; 36 | 37 | doPasswordReset = email => { 38 | return this.auth.sendPasswordResetEmail(email); 39 | }; 40 | 41 | doPasswordUpdate = password => { 42 | return this.auth.currentUser.updatePassword(password); 43 | }; 44 | 45 | user = uid => this.db.ref(`users/${uid}`); 46 | users = () => this.db.ref("users"); 47 | 48 | course = cid => this.db.ref(`courses/${cid}`); 49 | courses = () => this.db.ref("courses"); 50 | courseCurriculums = cid => this.db.ref(`courses/${cid}/curriculum`); 51 | 52 | assignments = cid => this.db.ref(`courses/${cid}/assignments`) 53 | // Database ref 54 | assignmentRef = () => this.storage.ref().child('assignments'); 55 | 56 | onAuthUserListener = (next, fallback) =>{ 57 | this.auth.onAuthStateChanged(authUser => { 58 | if (authUser) { 59 | this.user(authUser.uid).on('value', snapshot => { 60 | const dbUser = snapshot.val(); 61 | if (dbUser) { 62 | if (!dbUser.roles) { 63 | dbUser.roles = {}; 64 | } 65 | 66 | authUser = { 67 | uid: authUser.uid, 68 | email: authUser.email, 69 | ...dbUser 70 | }; 71 | 72 | next(authUser); 73 | } 74 | }); 75 | } 76 | else { 77 | fallback(); 78 | } 79 | }); 80 | } 81 | 82 | onAuthCourseListener = (next, fallback) => { 83 | this.auth.onAuthStateChanged(() => { 84 | this.courses().on("value", (snapshot, prevChildKey) => { 85 | const coursesObject = snapshot.val(); 86 | 87 | if(coursesObject){ 88 | const coursesList = Object.keys(coursesObject).map(key => ({ 89 | ...coursesObject[key], 90 | uid: key, 91 | })); 92 | next(coursesList); 93 | } 94 | else{ 95 | fallback(); 96 | } 97 | }); 98 | }); 99 | }; 100 | } 101 | 102 | export default Firebase; 103 | -------------------------------------------------------------------------------- /src/Components/Firebase/index.js: -------------------------------------------------------------------------------- 1 | import FirebaseContext, { withFirebase } from './context'; 2 | import Firebase from './firebase'; 3 | 4 | export default Firebase; 5 | export { withFirebase, FirebaseContext }; -------------------------------------------------------------------------------- /src/Components/LandingPage/HomePage.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import TopNavBar from '../Navbar/TopNavBar'; 3 | import { Container, Button } from 'rsuite'; 4 | import headerImg from '../../assets/lp.png' 5 | import CourseShowcase from '../Courses/CourseShowcase'; 6 | import withCourse from '../Courses/Context/withCourse'; 7 | import { NavLink } from 'react-router-dom'; 8 | class HomePage extends Component { 9 | render(){ 10 | const coursesList = this.props.coursesList; 11 | 12 | return( 13 |
14 | 15 | 16 | 17 |

The Most Trusted
Simplified LMS

18 |

The #1 featured product on ProductHunt and a featured LMS on People's Choice Magazine.

19 | 20 |
21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 |

Recently Added

29 |
30 | 31 | 32 | { 33 | coursesList && coursesList.length? 34 | Array.from(coursesList).reverse().map((course, i) => { 35 | if(i < 4){ 36 | return ( 37 | 38 | ) 39 | }else{ 40 | return null; 41 | } 42 | }) 43 | :null 44 | } 45 | 46 |
47 |
48 | ) 49 | } 50 | } 51 | 52 | export default withCourse(HomePage); 53 | 54 | -------------------------------------------------------------------------------- /src/Components/Login/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { 3 | Form, 4 | FormGroup, 5 | FormControl, 6 | ControlLabel, 7 | Button, 8 | ButtonToolbar, 9 | Alert, 10 | Message 11 | } from 'rsuite'; 12 | 13 | const INITIAL_STATE = { 14 | email: '', 15 | password: '', 16 | error: '', 17 | }; 18 | 19 | export default class LoginForm extends Component{ 20 | constructor(props){ 21 | super(props); 22 | 23 | this.state = { 24 | errorPlacement: 'bottomStart', 25 | showEmailError: false, 26 | showPasswordError: false, 27 | isFormSubmit: false, 28 | 29 | ...INITIAL_STATE, 30 | } 31 | this.formSubmit = this.formSubmit.bind(this); 32 | } 33 | 34 | formSubmit(e) { 35 | const { email, password } = this.state; 36 | 37 | this.props.firebase.doSignInWithEmailAndPassword(email, password).then(() => { 38 | this.setState({ ...INITIAL_STATE }); 39 | Alert.success('Successfully logged in. Redirecting...', 5000) 40 | this.props.history.push('/dashboard'); 41 | }).catch(error => { 42 | let message = error.message; 43 | this.setState({ error: message }); 44 | }); 45 | 46 | e.preventDefault(); 47 | }; 48 | 49 | render(){ 50 | const { showEmailError, showPasswordError, errorPlacement } = this.state; 51 | const emailErrorMessage = showEmailError ? 'This field is required' : null; 52 | const passwordErrorMessage = showPasswordError ? 'This field is required' : null; 53 | 54 | const isInvalid = 55 | !this.state.email || 56 | !this.state.password 57 | 58 | return( 59 |
60 |
{ 62 | this.setState({ 63 | email: formValue.email, 64 | password: formValue.password, 65 | }); 66 | }}> 67 | 68 | 69 | Email 70 | 75 | 76 | 77 | 80 | Password 81 | 87 | 88 | 89 | {this.state.error? 90 |
91 | 92 |
93 | : ''} 94 | 95 | 96 | 97 |
98 | 99 | {/* */} 100 |
101 |
102 |
103 |
104 |
105 | ) 106 | } 107 | } -------------------------------------------------------------------------------- /src/Components/Login/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { withFirebase } from '../Firebase'; 3 | import { withRouter } from 'react-router-dom'; 4 | import LoginForm from './LoginForm'; 5 | import TopNavBar from '../Navbar/TopNavBar'; 6 | import {Container, Content, FlexboxGrid, Panel, Header, Footer} from 'rsuite'; 7 | import { compose } from 'recompose'; 8 | import { AuthUserContext } from '../Session'; 9 | 10 | class LoginPage extends Component { 11 | componentDidMount(){ 12 | this.listener = this.props.firebase.auth.onAuthStateChanged(authUser => { 13 | if(authUser){ 14 | this.props.history.push('/dashboard'); 15 | } 16 | }) 17 | } 18 | 19 | componentWillUnmount(){ 20 | this.listener(); 21 | } 22 | 23 | render(){ 24 | return( 25 | {authUser => { 26 | const View = !authUser? ( 27 |
28 | 29 |
30 | 31 |
32 | 33 | 34 | 35 | Login} bordered> 36 | 37 | 38 | 39 | 40 | 41 |
ShaneLMS
42 |
43 |
44 | ): '' 45 | 46 | return View; 47 | }} 48 |
49 | 50 | 51 | ) 52 | } 53 | } 54 | 55 | const Login = compose( 56 | withRouter, 57 | withFirebase, 58 | )(LoginPage); 59 | 60 | export default (Login); -------------------------------------------------------------------------------- /src/Components/Navbar/SideNavigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Sidenav, Icon, Nav, Avatar } from 'rsuite'; 3 | import SignOutButton from '../Signout/SignOutButton'; 4 | import { NavLink } from 'react-router-dom'; 5 | 6 | export default class SideNavigation extends Component { 7 | constructor() { 8 | super(); 9 | this.state = { 10 | expanded: true 11 | }; 12 | } 13 | 14 | render() { 15 | const { expanded } = this.state; 16 | let role = this.props.roles.userRole; 17 | role = role[0].toUpperCase() + role.substr(1); 18 | 19 | return ( 20 |
21 | 33 | 34 |
41 | 47 | 51 | Webcademy 52 | 53 |
54 |
55 | 56 | 57 | 68 |

77 | {role} 78 |

79 | 80 | 108 | 109 | {role === 'Admin' ? ( 110 | 137 | ) : null} 138 | 139 | 147 |
148 |
149 |
150 | ); 151 | } 152 | } 153 | 154 | const NavLinkStyle = { 155 | textDecoration: 'none', 156 | color: '#8e8e93' 157 | }; 158 | -------------------------------------------------------------------------------- /src/Components/Navbar/TopNavBar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Icon, Navbar, Nav} from 'rsuite'; 3 | import {NavLink} from 'react-router-dom'; 4 | import AuthUserContext from '../Session/context'; 5 | import { withFirebase } from '../Firebase'; 6 | 7 | class TopNavBar extends Component { 8 | render(){ 9 | return( 10 | 11 | { 12 | authUser => { 13 | return( 14 |
15 | 16 | 17 |
18 | 19 | Webcademy 20 |
21 |
22 | 23 | 46 |
47 |
48 | ) 49 | } 50 | } 51 |
52 | ) 53 | } 54 | } 55 | 56 | export default withFirebase(TopNavBar); -------------------------------------------------------------------------------- /src/Components/Session/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AuthUserContext = React.createContext(null); 4 | 5 | export default AuthUserContext; -------------------------------------------------------------------------------- /src/Components/Session/index.js: -------------------------------------------------------------------------------- 1 | import AuthUserContext from './context'; 2 | import withAuthentication from './withAuthentication'; 3 | import withAuthorization from './withAuthorization'; 4 | 5 | export {AuthUserContext, withAuthentication, withAuthorization}; -------------------------------------------------------------------------------- /src/Components/Session/withAuthentication.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AuthUserContext from './context'; 3 | import { withFirebase } from '../Firebase'; 4 | 5 | const withAuthentication = Component => { 6 | class WithAuthentication extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | authUser: null 11 | }; 12 | } 13 | 14 | componentDidMount() { 15 | this.listener = this.props.firebase.onAuthUserListener( 16 | authUser => { 17 | this.setState({ authUser }); 18 | }, 19 | () => { 20 | this.setState({ authUser: null }); 21 | } 22 | ); 23 | } 24 | 25 | render() { 26 | return ( 27 | 28 | {this.state.authUser? : } 29 | 30 | 31 | ); 32 | } 33 | } 34 | return withFirebase(WithAuthentication); 35 | }; 36 | export default withAuthentication; 37 | -------------------------------------------------------------------------------- /src/Components/Session/withAuthorization.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { compose } from 'recompose'; 3 | import { withFirebase } from '../Firebase'; 4 | import { withRouter } from 'react-router-dom'; 5 | import AuthUserContext from './context'; 6 | import { Loader } from 'rsuite'; 7 | 8 | const withAuthorization = condition => Component => { 9 | class WithAuthorization extends React.Component { 10 | componentDidMount() { 11 | this.listener = this.props.firebase.onAuthUserListener( 12 | authUser => { 13 | if (!condition(authUser)) { 14 | this.props.history.push('/login'); 15 | } 16 | }, 17 | () => this.props.history.push('/login') 18 | ); 19 | } 20 | 21 | render() { 22 | return ( 23 | 24 | {authUser => 25 | condition(authUser) ? ( 26 | 27 | ) : 28 | } 29 | 30 | ); 31 | } 32 | } 33 | return compose(withRouter, withFirebase)(WithAuthorization); 34 | }; 35 | 36 | export default withAuthorization; 37 | -------------------------------------------------------------------------------- /src/Components/Signout/SignOutButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withFirebase } from '../Firebase'; 3 | import {Nav, Icon} from 'rsuite'; 4 | 5 | const SignOutButton = ({ firebase }) => ( 6 | }> 7 |

Sign out

8 |
9 | ); 10 | 11 | export default withFirebase(SignOutButton); -------------------------------------------------------------------------------- /src/Components/Signup/SignupForm.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { 3 | Form, 4 | FormGroup, 5 | FormControl, 6 | ControlLabel, 7 | Button, 8 | ButtonToolbar, 9 | Message, 10 | Alert, 11 | } from 'rsuite'; 12 | 13 | const INITIAL_STATE = { 14 | name: '', 15 | email: '', 16 | password: '', 17 | isAdmin: false, 18 | error: '', 19 | }; 20 | 21 | export default class SignupForm extends Component{ 22 | constructor(props){ 23 | super(props); 24 | 25 | this.state = { 26 | errorPlacement: 'bottomStart', 27 | showEmailError: false, 28 | isFormSubmit: false, 29 | referrer: '', 30 | ...INITIAL_STATE 31 | } 32 | 33 | this.formSubmit = this.formSubmit.bind(this); 34 | } 35 | 36 | formSubmit(e) { 37 | let { name, email, password, isAdmin, referrer } = this.state; 38 | const roles = {}; 39 | 40 | isAdmin = referrer === 'INSTR' ? true : false; 41 | 42 | if(isAdmin){ 43 | roles['userRole'] = 'admin'; 44 | } 45 | else{ 46 | roles['userRole'] = 'student'; 47 | } 48 | 49 | this.props.firebase.doCreateUserWithEmailAndPassword(email, password).then(authUser => { 50 | return this.props.firebase.user(authUser.user.uid).set({name, email, roles}) 51 | }).then(() => { 52 | this.setState({ ...INITIAL_STATE }); 53 | Alert.success('Successfully registered. Redirecting...', 5000) 54 | this.props.history.push('/dashboard'); 55 | 56 | }).catch(error => { 57 | let message = error.message; 58 | this.setState({error: message}); 59 | }); 60 | 61 | e.preventDefault(); 62 | }; 63 | 64 | render(){ 65 | const { showEmailError, errorPlacement } = this.state; 66 | const emailErrorMessage = showEmailError ? 'This field is required' : null; 67 | const password = this.state.password; 68 | const passwordErrorMessage = (password && password.length > 2 && password.length < 10) ? 'Password must contain at least 10 characters' : ''; 69 | 70 | const isInvalid = 71 | !this.state.name || 72 | !this.state.email || 73 | !(this.state.password && this.state.password.length >= 10); 74 | 75 | return( 76 |
77 |
{ 79 | this.setState({ 80 | name: formValue.name, 81 | email: formValue.email, 82 | password: formValue.password, 83 | referrer: formValue.referrer, 84 | }); 85 | }} 86 | > 87 | 88 | Full Name 89 | 94 | 95 | 96 | Email 97 | 102 | 103 | 104 | 107 | Password 108 | 114 | 115 | 116 | 119 | Referrer (Optional) 120 | 123 | 124 | 125 | {this.state.error? 126 |
127 | 128 |
129 | : ''} 130 | 131 | 132 | 133 | 134 | 135 | 136 |
137 |
138 | ) 139 | } 140 | } -------------------------------------------------------------------------------- /src/Components/Signup/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import { withFirebase } from '../Firebase'; 3 | import SignupForm from './SignupForm'; 4 | import TopNavBar from '../Navbar/TopNavBar'; 5 | import { withRouter } from 'react-router-dom'; 6 | 7 | import {Container, Content, FlexboxGrid, Panel, Header, Footer} from 'rsuite'; 8 | import { compose } from 'recompose'; 9 | import { AuthUserContext } from '../Session'; 10 | 11 | class SignUpPage extends Component { 12 | componentDidMount(){ 13 | this.listener = this.props.firebase.auth.onAuthStateChanged(authUser => { 14 | if(authUser){ 15 | this.props.history.push('/dashboard'); 16 | } 17 | }) 18 | } 19 | 20 | componentWillUnmount(){ 21 | this.listener(); 22 | } 23 | 24 | render(){ 25 | return( 26 | {authUser => { 27 | const View = !authUser? ( 28 |
29 | 30 |
31 | 32 |
33 | 34 | 35 | 36 | Signup} bordered> 37 | 38 | 39 | 40 | 41 | 42 |
ShaneLMS
43 |
44 |
45 | ): 46 | '' 47 | return View 48 | } 49 | } 50 |
51 | 52 | 53 | 54 | ) 55 | } 56 | } 57 | 58 | const SignUp = compose( 59 | withRouter, 60 | withFirebase, 61 | )(SignUpPage); 62 | 63 | export default (SignUp); 64 | -------------------------------------------------------------------------------- /src/Components/Templates/MainTemplate.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SideNavigation from '../Navbar/SideNavigation'; 3 | import TopNavBar from '../Navbar/TopNavBar' 4 | import { Container} from 'rsuite'; 5 | import { AuthUserContext, } from '../Session'; 6 | 7 | const mainTemplate = (Header, BreadCrumb, Body) =>{ 8 | class MainTemplate extends React.Component{ 9 | 10 | render(){ 11 | return( 12 | 13 | {authUser => { 14 | const flexDir = authUser? 'row': 'column'; 15 | 16 | return( 17 | 18 | { 19 | authUser? 20 | 21 | : 22 | } 23 | 24 | 25 | {authUser? 26 | 27 |
28 | 29 | : null 30 | } 31 | 32 | 33 | 34 | { 35 | authUser? 36 | 37 | 38 | 39 | : null 40 | } 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | }} 50 | 51 | ) 52 | } 53 | } 54 | return MainTemplate; 55 | } 56 | 57 | export default mainTemplate; -------------------------------------------------------------------------------- /src/Components/TextField.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PureComponent} from 'react'; 2 | import {FormGroup, ControlLabel, FormControl} from 'rsuite'; 3 | 4 | export default class TextField extends PureComponent { 5 | render() { 6 | const { name, label, accepter, ...props } = this.props; 7 | return ( 8 | 9 | {label} 10 | 11 | 12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /src/assets/006023-Free-vector_-Macbook-Ipad-and-Iphone-by-Frexy-Dribbble-removebg-preview (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezeburger/React-Learning-Management-System/4f2fdc4a222a445af1854d0d606d780ce5e92b88/src/assets/006023-Free-vector_-Macbook-Ipad-and-Iphone-by-Frexy-Dribbble-removebg-preview (1).png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | image/svg+xml -------------------------------------------------------------------------------- /src/assets/lp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cheezeburger/React-Learning-Management-System/4f2fdc4a222a445af1854d0d606d780ce5e92b88/src/assets/lp.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import Firebase, { FirebaseContext } from './Components/Firebase'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root'), 12 | ); 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' } 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready.then(registration => { 134 | registration.unregister(); 135 | }); 136 | } 137 | } 138 | --------------------------------------------------------------------------------