)
78 | }
79 |
80 | return (
81 |
82 |
83 |
84 | Sign In
85 |
86 |
87 |
88 |
{
89 | values.error && (
90 | error
91 | {values.error}
92 | )
93 | }
94 |
95 |
96 |
97 |
98 |
99 | )
100 | }
101 |
--------------------------------------------------------------------------------
/client/course/MyCourses.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import { makeStyles } from '@material-ui/core/styles'
3 | import Paper from '@material-ui/core/Paper'
4 | import List from '@material-ui/core/List'
5 | import ListItem from '@material-ui/core/ListItem'
6 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'
7 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
8 | import ListItemText from '@material-ui/core/ListItemText'
9 | import Avatar from '@material-ui/core/Avatar'
10 | import Icon from '@material-ui/core/Icon'
11 | import Button from '@material-ui/core/Button'
12 | import Typography from '@material-ui/core/Typography'
13 | import Divider from '@material-ui/core/Divider'
14 | import auth from './../auth/auth-helper'
15 | import {listByInstructor} from './api-course.js'
16 | import {Redirect, Link} from 'react-router-dom'
17 |
18 | const useStyles = makeStyles(theme => ({
19 | root: theme.mixins.gutters({
20 | maxWidth: 600,
21 | margin: 'auto',
22 | padding: theme.spacing(3),
23 | marginTop: theme.spacing(12)
24 | }),
25 | title: {
26 | margin: `${theme.spacing(3)}px 0 ${theme.spacing(3)}px ${theme.spacing(1)}px` ,
27 | color: theme.palette.protectedTitle,
28 | fontSize: '1.2em'
29 | },
30 | addButton:{
31 | float:'right'
32 | },
33 | leftIcon: {
34 | marginRight: "8px"
35 | },
36 | avatar: {
37 | borderRadius: 0,
38 | width:65,
39 | height: 40
40 | },
41 | listText: {
42 | marginLeft: 16
43 | }
44 | }))
45 |
46 | export default function MyCourses(){
47 | const classes = useStyles()
48 | const [courses, setCourses] = useState([])
49 | const [redirectToSignin, setRedirectToSignin] = useState(false)
50 | const jwt = auth.isAuthenticated()
51 |
52 | useEffect(() => {
53 | const abortController = new AbortController()
54 | const signal = abortController.signal
55 | listByInstructor({
56 | userId: jwt.user._id
57 | }, {t: jwt.token}, signal).then((data) => {
58 | if (data.error) {
59 | setRedirectToSignin(true)
60 | } else {
61 | setCourses(data)
62 | }
63 | })
64 | return function cleanup(){
65 | abortController.abort()
66 | }
67 | }, [])
68 |
69 | if (redirectToSignin) {
70 | return
71 | }
72 | return (
73 |
74 |
75 |
76 | Your Courses
77 |
78 |
79 |
82 |
83 |
84 |
85 |
86 | {courses.map((course, i) => {
87 | return
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | })}
96 |
97 |
98 |
)
99 | }
--------------------------------------------------------------------------------
/client/course/NewLesson.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 | import PropTypes from 'prop-types'
3 | import Button from '@material-ui/core/Button'
4 | import TextField from '@material-ui/core/TextField'
5 | import Dialog from '@material-ui/core/Dialog'
6 | import DialogActions from '@material-ui/core/DialogActions'
7 | import DialogContent from '@material-ui/core/DialogContent'
8 | import DialogTitle from '@material-ui/core/DialogTitle'
9 | import Add from '@material-ui/icons/AddBox'
10 | import {makeStyles} from '@material-ui/core/styles'
11 | import {newLesson} from './api-course'
12 | import auth from './../auth/auth-helper'
13 |
14 | const useStyles = makeStyles(theme => ({
15 | form: {
16 | minWidth: 500
17 | }
18 | }))
19 |
20 | export default function NewLesson(props) {
21 | const classes = useStyles()
22 | const [open, setOpen] = useState(false)
23 | const [values, setValues] = useState({
24 | title: '',
25 | content: '',
26 | resource_url: ''
27 | })
28 |
29 | const handleChange = name => event => {
30 | setValues({ ...values, [name]: event.target.value })
31 | }
32 | const clickSubmit = () => {
33 | const jwt = auth.isAuthenticated()
34 | const lesson = {
35 | title: values.title || undefined,
36 | content: values.content || undefined,
37 | resource_url: values.resource_url || undefined
38 | }
39 | newLesson({
40 | courseId: props.courseId
41 | }, {
42 | t: jwt.token
43 | }, lesson).then((data) => {
44 | if (data && data.error) {
45 | setValues({...values, error: data.error})
46 | } else {
47 | props.addLesson(data)
48 | setValues({...values, title: '',
49 | content: '',
50 | resource_url: ''})
51 | setOpen(false)
52 | }
53 | })
54 | }
55 | const handleClickOpen = () => {
56 | setOpen(true)
57 | }
58 |
59 | const handleClose = () => {
60 | setOpen(false)
61 | }
62 |
63 | return (
64 |
65 |
68 |
109 |
110 | )
111 | }
112 | NewLesson.propTypes = {
113 | courseId: PropTypes.string.isRequired,
114 | addLesson: PropTypes.func.isRequired
115 | }
--------------------------------------------------------------------------------
/client/core/Home.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import { makeStyles } from '@material-ui/core/styles'
3 | import Card from '@material-ui/core/Card'
4 | import Divider from '@material-ui/core/Divider'
5 | import {listPublished} from './../course/api-course'
6 | import {listEnrolled, listCompleted} from './../enrollment/api-enrollment'
7 | import Typography from '@material-ui/core/Typography'
8 | import auth from './../auth/auth-helper'
9 | import Courses from './../course/Courses'
10 | import Enrollments from '../enrollment/Enrollments'
11 |
12 |
13 | const useStyles = makeStyles(theme => ({
14 | card: {
15 | width:'90%',
16 | margin: 'auto',
17 | marginTop: 20,
18 | marginBottom: theme.spacing(2),
19 | padding: 20,
20 | backgroundColor: '#ffffff'
21 | },
22 | extraTop: {
23 | marginTop: theme.spacing(12)
24 | },
25 | title: {
26 | padding:`${theme.spacing(3)}px ${theme.spacing(2.5)}px ${theme.spacing(2)}px`,
27 | color: theme.palette.openTitle
28 | },
29 | media: {
30 | minHeight: 400
31 | },
32 | gridList: {
33 | width: '100%',
34 | minHeight: 200,
35 | padding: '16px 0 10px'
36 | },
37 | tile: {
38 | textAlign: 'center'
39 | },
40 | image: {
41 | height: '100%'
42 | },
43 | tileBar: {
44 | backgroundColor: 'rgba(0, 0, 0, 0.72)',
45 | textAlign: 'left'
46 | },
47 | enrolledTitle: {
48 | color:'#efefef',
49 | marginBottom: 5
50 | },
51 | action:{
52 | margin: '0 10px'
53 | },
54 | enrolledCard: {
55 | backgroundColor: '#616161',
56 | },
57 | divider: {
58 | marginBottom: 16,
59 | backgroundColor: 'rgb(157, 157, 157)'
60 | },
61 | noTitle: {
62 | color: 'lightgrey',
63 | marginBottom: 12,
64 | marginLeft: 8
65 | }
66 | }))
67 |
68 | export default function Home(){
69 | const classes = useStyles()
70 | const jwt = auth.isAuthenticated()
71 | const [courses, setCourses] = useState([])
72 | const [enrolled, setEnrolled] = useState([])
73 | useEffect(() => {
74 | const abortController = new AbortController()
75 | const signal = abortController.signal
76 | listEnrolled({t: jwt.token}, signal).then((data) => {
77 | if (data.error) {
78 | console.log(data.error)
79 | } else {
80 | setEnrolled(data)
81 | }
82 | })
83 | return function cleanup(){
84 | abortController.abort()
85 | }
86 | }, [])
87 | useEffect(() => {
88 | const abortController = new AbortController()
89 | const signal = abortController.signal
90 | listPublished(signal).then((data) => {
91 | if (data.error) {
92 | console.log(data.error)
93 | } else {
94 | setCourses(data)
95 | }
96 | })
97 | return function cleanup(){
98 | abortController.abort()
99 | }
100 | }, [])
101 | return (
102 | {auth.isAuthenticated().user && (
103 |
104 |
105 | Courses you are enrolled in
106 |
107 | {enrolled.length != 0 ? ()
108 | : (No courses.)
109 | }
110 |
111 | )}
112 |
113 |
114 | All Courses
115 |
116 | {(courses.length != 0 && courses.length != enrolled.length) ? ()
117 | : (No new courses.)
118 | }
119 |
120 |
121 | )
122 | }
123 |
124 |
--------------------------------------------------------------------------------
/client/course/api-course.js:
--------------------------------------------------------------------------------
1 | const create = async (params, credentials, course) => {
2 | try {
3 | let response = await fetch('/api/courses/by/'+ params.userId, {
4 | method: 'POST',
5 | headers: {
6 | 'Accept': 'application/json',
7 | 'Authorization': 'Bearer ' + credentials.t
8 | },
9 | body: course
10 | })
11 | return response.json()
12 | } catch(err) {
13 | console.log(err)
14 | }
15 | }
16 |
17 | const list = async (signal) => {
18 | try {
19 | let response = await fetch('/api/courses/', {
20 | method: 'GET',
21 | signal: signal,
22 | })
23 | return await response.json()
24 | } catch(err) {
25 | console.log(err)
26 | }
27 | }
28 |
29 | const read = async (params, signal) => {
30 | try {
31 | let response = await fetch('/api/courses/' + params.courseId, {
32 | method: 'GET',
33 | signal: signal,
34 | headers: {
35 | 'Accept': 'application/json',
36 | 'Content-Type': 'application/json',
37 | }
38 | })
39 | return await response.json()
40 | } catch(err) {
41 | console.log(err)
42 | }
43 | }
44 |
45 | const update = async (params, credentials, course) => {
46 | try {
47 | let response = await fetch('/api/courses/' + params.courseId, {
48 | method: 'PUT',
49 | headers: {
50 | 'Accept': 'application/json',
51 | 'Authorization': 'Bearer ' + credentials.t
52 | },
53 | body: course
54 | })
55 | return await response.json()
56 | } catch(err) {
57 | console.log(err)
58 | }
59 | }
60 |
61 | const remove = async (params, credentials) => {
62 | try {
63 | let response = await fetch('/api/courses/' + params.courseId, {
64 | method: 'DELETE',
65 | headers: {
66 | 'Accept': 'application/json',
67 | 'Content-Type': 'application/json',
68 | 'Authorization': 'Bearer ' + credentials.t
69 | }
70 | })
71 | return await response.json()
72 | } catch(err) {
73 | console.log(err)
74 | }
75 | }
76 |
77 | const listByInstructor = async (params, credentials, signal) => {
78 | try {
79 | let response = await fetch('/api/courses/by/'+params.userId, {
80 | method: 'GET',
81 | signal: signal,
82 | headers: {
83 | 'Accept': 'application/json',
84 | 'Authorization': 'Bearer ' + credentials.t
85 | }
86 | })
87 | return response.json()
88 | } catch(err){
89 | console.log(err)
90 | }
91 | }
92 |
93 | const newLesson = async (params, credentials, lesson) => {
94 | try {
95 | let response = await fetch('/api/courses/'+params.courseId+'/lesson/new', {
96 | method: 'PUT',
97 | headers: {
98 | 'Accept': 'application/json',
99 | 'Content-Type': 'application/json',
100 | 'Authorization': 'Bearer ' + credentials.t
101 | },
102 | body: JSON.stringify({lesson:lesson})
103 | })
104 | return response.json()
105 | } catch(err){
106 | console.log(err)
107 | }
108 | }
109 | const listPublished = async (signal) => {
110 | try {
111 | let response = await fetch('/api/courses/published', {
112 | method: 'GET',
113 | signal: signal,
114 | headers: {
115 | 'Accept': 'application/json',
116 | 'Content-Type': 'application/json',
117 | }
118 | })
119 | return await response.json()
120 | } catch(err) {
121 | console.log(err)
122 | }
123 | }
124 | export {
125 | create,
126 | list,
127 | read,
128 | update,
129 | remove,
130 | listByInstructor,
131 | newLesson,
132 | listPublished
133 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MERN Classroom
2 |
3 | A simple web-based classroom application that allows instructors to add courses with lessons, while students can enroll in these courses and track their progress. - developed using React, Node, Express and MongoDB.
4 |
5 | 
6 |
7 | ### [Live Demo](http://classroom.mernbook.com/ "MERN Classroom")
8 |
9 | #### What you need to run this code
10 | 1. Node (13.12.0)
11 | 2. NPM (6.14.4) or Yarn (1.22.4)
12 | 3. MongoDB (4.2.0)
13 |
14 | #### How to run this code
15 | 1. Make sure MongoDB is running on your system
16 | 2. Clone this repository
17 | 3. Open command line in the cloned folder,
18 | - To install dependencies, run ``` npm install ``` or ``` yarn ```
19 | - To run the application for development, run ``` npm run development ``` or ``` yarn development ```
20 | 4. Open [localhost:3000](http://localhost:3000/) in the browser
21 | ----
22 | ### More applications built using this stack
23 |
24 | * [MERN Skeleton](https://github.com/shamahoque/mern-social/tree/second-edition)
25 | * [MERN Social](https://github.com/shamahoque/mern-social/tree/second-edition)
26 | * [MERN Marketplace](https://github.com/shamahoque/mern-marketplace/tree/second-edition)
27 | * [MERN Expense Tracker](https://github.com/shamahoque/mern-expense-tracker)
28 | * [MERN Mediastream](https://github.com/shamahoque/mern-mediastream/tree/second-edition)
29 | * [MERN VR Game](https://github.com/shamahoque/mern-vrgame/tree/second-edition)
30 |
31 | Learn more at [mernbook.com](http://www.mernbook.com/)
32 |
33 | ----
34 | ## Get the book
35 | #### [Full-Stack React Projects - Second Edition](https://www.packtpub.com/web-development/full-stack-react-projects-second-edition)
36 | *Learn MERN stack development by building modern web apps using MongoDB, Express, React, and Node.js*
37 |
38 |
39 |
40 | React combined with industry-tested, server-side technologies, such as Node, Express, and MongoDB, enables you to develop and deploy robust real-world full-stack web apps. This updated second edition focuses on the latest versions and conventions of the technologies in this stack, along with their new features such as Hooks in React and async/await in JavaScript. The book also explores advanced topics such as implementing real-time bidding, a web-based classroom app, and data visualization in an expense tracking app.
41 |
42 | Full-Stack React Projects will take you through the process of preparing the development environment for MERN stack-based web development, creating a basic skeleton app, and extending it to build six different web apps. You'll build apps for social media, classrooms, media streaming, online marketplaces with real-time bidding, and web-based games with virtual reality features. Throughout the book, you'll learn how MERN stack web development works, extend its capabilities for complex features, and gain actionable insights into creating MERN-based apps, along with exploring industry best practices to meet the ever-increasing demands of the real world.
43 |
44 | Things you'll learn in this book:
45 |
46 | - Extend a MERN-based application to build a variety of applications
47 | - Add real-time communication capabilities with Socket.IO
48 | - Implement data visualization features for React applications using Victory
49 | - Develop media streaming applications using MongoDB GridFS
50 | - Improve SEO for your MERN apps by implementing server-side rendering with data
51 | - Implement user authentication and authorization using JSON web tokens
52 | - Set up and use React 360 to develop user interfaces with VR capabilities
53 | - Make your MERN stack applications reliable and scalable with industry best practices
54 |
55 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1839215410) today!
56 |
57 | ---
58 |
--------------------------------------------------------------------------------
/client/user/Signup.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 | import Card from '@material-ui/core/Card'
3 | import CardActions from '@material-ui/core/CardActions'
4 | import CardContent from '@material-ui/core/CardContent'
5 | import Button from '@material-ui/core/Button'
6 | import TextField from '@material-ui/core/TextField'
7 | import Typography from '@material-ui/core/Typography'
8 | import Icon from '@material-ui/core/Icon'
9 | import { makeStyles } from '@material-ui/core/styles'
10 | import {create} from './api-user.js'
11 | import Dialog from '@material-ui/core/Dialog'
12 | import DialogActions from '@material-ui/core/DialogActions'
13 | import DialogContent from '@material-ui/core/DialogContent'
14 | import DialogContentText from '@material-ui/core/DialogContentText'
15 | import DialogTitle from '@material-ui/core/DialogTitle'
16 | import {Link} from 'react-router-dom'
17 |
18 | const useStyles = makeStyles(theme => ({
19 | card: {
20 | maxWidth: 600,
21 | margin: 'auto',
22 | textAlign: 'center',
23 | marginTop: theme.spacing(12),
24 | paddingBottom: theme.spacing(2)
25 | },
26 | error: {
27 | verticalAlign: 'middle'
28 | },
29 | title: {
30 | marginTop: theme.spacing(2),
31 | color: theme.palette.openTitle
32 | },
33 | textField: {
34 | marginLeft: theme.spacing(1),
35 | marginRight: theme.spacing(1),
36 | width: 300
37 | },
38 | submit: {
39 | margin: 'auto',
40 | marginBottom: theme.spacing(2)
41 | }
42 | }))
43 |
44 | export default function Signup() {
45 | const classes = useStyles()
46 | const [values, setValues] = useState({
47 | name: '',
48 | password: '',
49 | email: '',
50 | open: false,
51 | error: ''
52 | })
53 |
54 | const handleChange = name => event => {
55 | setValues({ ...values, [name]: event.target.value })
56 | }
57 |
58 | const clickSubmit = () => {
59 | const user = {
60 | name: values.name || undefined,
61 | email: values.email || undefined,
62 | password: values.password || undefined
63 | }
64 | create(user).then((data) => {
65 | if (data.error) {
66 | setValues({ ...values, error: data.error})
67 | } else {
68 | setValues({ ...values, error: '', open: true})
69 | }
70 | })
71 | }
72 |
73 | return (
74 |
75 |
76 |
77 | Sign Up
78 |
79 |
80 |
81 |
82 |
{
83 | values.error && (
84 | error
85 | {values.error})
86 | }
87 |
88 |
89 |
90 |
91 |
92 |
107 |
108 | )
109 | }
--------------------------------------------------------------------------------
/server/controllers/enrollment.controller.js:
--------------------------------------------------------------------------------
1 | import Enrollment from '../models/enrollment.model'
2 | import errorHandler from './../helpers/dbErrorHandler'
3 |
4 | const create = async (req, res) => {
5 | let newEnrollment = {
6 | course: req.course,
7 | student: req.auth,
8 | }
9 | newEnrollment.lessonStatus = req.course.lessons.map((lesson)=>{
10 | return {lesson: lesson, complete:false}
11 | })
12 | const enrollment = new Enrollment(newEnrollment)
13 | try {
14 | let result = await enrollment.save()
15 | return res.status(200).json(result)
16 | } catch (err) {
17 | return res.status(400).json({
18 | error: errorHandler.getErrorMessage(err)
19 | })
20 | }
21 | }
22 |
23 | /**
24 | * Load enrollment and append to req.
25 | */
26 | const enrollmentByID = async (req, res, next, id) => {
27 | try {
28 | let enrollment = await Enrollment.findById(id)
29 | .populate({path: 'course', populate:{ path: 'instructor'}})
30 | .populate('student', '_id name')
31 | if (!enrollment)
32 | return res.status('400').json({
33 | error: "Enrollment not found"
34 | })
35 | req.enrollment = enrollment
36 | next()
37 | } catch (err) {
38 | return res.status('400').json({
39 | error: "Could not retrieve enrollment"
40 | })
41 | }
42 | }
43 |
44 | const read = (req, res) => {
45 | return res.json(req.enrollment)
46 | }
47 |
48 | const complete = async (req, res) => {
49 | let updatedData = {}
50 | updatedData['lessonStatus.$.complete']= req.body.complete
51 | updatedData.updated = Date.now()
52 | if(req.body.courseCompleted)
53 | updatedData.completed = req.body.courseCompleted
54 |
55 | try {
56 | let enrollment = await Enrollment.updateOne({'lessonStatus._id':req.body.lessonStatusId}, {'$set': updatedData})
57 | res.json(enrollment)
58 | } catch (err) {
59 | return res.status(400).json({
60 | error: errorHandler.getErrorMessage(err)
61 | })
62 | }
63 | }
64 |
65 | const remove = async (req, res) => {
66 | try {
67 | let enrollment = req.enrollment
68 | let deletedEnrollment = await enrollment.remove()
69 | res.json(deletedEnrollment)
70 | } catch (err) {
71 | return res.status(400).json({
72 | error: errorHandler.getErrorMessage(err)
73 | })
74 | }
75 | }
76 |
77 | const isStudent = (req, res, next) => {
78 | const isStudent = req.auth && req.auth._id == req.enrollment.student._id
79 | if (!isStudent) {
80 | return res.status('403').json({
81 | error: "User is not enrolled"
82 | })
83 | }
84 | next()
85 | }
86 |
87 | const listEnrolled = async (req, res) => {
88 | try {
89 | let enrollments = await Enrollment.find({student: req.auth._id}).sort({'completed': 1}).populate('course', '_id name category')
90 | res.json(enrollments)
91 | } catch (err) {
92 | console.log(err)
93 | return res.status(400).json({
94 | error: errorHandler.getErrorMessage(err)
95 | })
96 | }
97 | }
98 |
99 | const findEnrollment = async (req, res, next) => {
100 | try {
101 | let enrollments = await Enrollment.find({course:req.course._id, student: req.auth._id})
102 | if(enrollments.length == 0){
103 | next()
104 | }else{
105 | res.json(enrollments[0])
106 | }
107 | } catch (err) {
108 | return res.status(400).json({
109 | error: errorHandler.getErrorMessage(err)
110 | })
111 | }
112 | }
113 |
114 | const enrollmentStats = async (req, res) => {
115 | try {
116 | let stats = {}
117 | stats.totalEnrolled = await Enrollment.find({course:req.course._id}).countDocuments()
118 | stats.totalCompleted = await Enrollment.find({course:req.course._id}).exists('completed', true).countDocuments()
119 | res.json(stats)
120 | } catch (err) {
121 | return res.status(400).json({
122 | error: errorHandler.getErrorMessage(err)
123 | })
124 | }
125 | }
126 |
127 | export default {
128 | create,
129 | enrollmentByID,
130 | read,
131 | remove,
132 | complete,
133 | isStudent,
134 | listEnrolled,
135 | findEnrollment,
136 | enrollmentStats
137 | }
138 |
--------------------------------------------------------------------------------
/client/course/NewCourse.js:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react'
2 | import Card from '@material-ui/core/Card'
3 | import CardActions from '@material-ui/core/CardActions'
4 | import CardContent from '@material-ui/core/CardContent'
5 | import Button from '@material-ui/core/Button'
6 | import FileUpload from '@material-ui/icons/AddPhotoAlternate'
7 | import auth from './../auth/auth-helper'
8 | import TextField from '@material-ui/core/TextField'
9 | import Typography from '@material-ui/core/Typography'
10 | import Icon from '@material-ui/core/Icon'
11 | import { makeStyles } from '@material-ui/core/styles'
12 | import {create} from './api-course.js'
13 | import {Link, Redirect} from 'react-router-dom'
14 |
15 | const useStyles = makeStyles(theme => ({
16 | card: {
17 | maxWidth: 600,
18 | margin: 'auto',
19 | textAlign: 'center',
20 | marginTop: theme.spacing(12),
21 | paddingBottom: theme.spacing(2)
22 | },
23 | error: {
24 | verticalAlign: 'middle'
25 | },
26 | title: {
27 | marginTop: theme.spacing(2),
28 | color: theme.palette.openTitle
29 | },
30 | textField: {
31 | marginLeft: theme.spacing(1),
32 | marginRight: theme.spacing(1),
33 | width: 300
34 | },
35 | submit: {
36 | margin: 'auto',
37 | marginBottom: theme.spacing(2)
38 | },
39 | input: {
40 | display: 'none'
41 | },
42 | filename:{
43 | marginLeft:'10px'
44 | }
45 | }))
46 |
47 | export default function NewCourse() {
48 | const classes = useStyles()
49 | const [values, setValues] = useState({
50 | name: '',
51 | description: '',
52 | image: '',
53 | category: '',
54 | redirect: false,
55 | error: ''
56 | })
57 | const jwt = auth.isAuthenticated()
58 |
59 | const handleChange = name => event => {
60 | const value = name === 'image'
61 | ? event.target.files[0]
62 | : event.target.value
63 | setValues({...values, [name]: value })
64 | }
65 | const clickSubmit = () => {
66 | let courseData = new FormData()
67 | values.name && courseData.append('name', values.name)
68 | values.description && courseData.append('description', values.description)
69 | values.image && courseData.append('image', values.image)
70 | values.category && courseData.append('category', values.category)
71 | create({
72 | userId: jwt.user._id
73 | }, {
74 | t: jwt.token
75 | }, courseData).then((data) => {
76 | if (data.error) {
77 | setValues({...values, error: data.error})
78 | } else {
79 | setValues({...values, error: '', redirect: true})
80 | }
81 | })
82 | }
83 |
84 | if (values.redirect) {
85 | return ()
86 | }
87 | return (
88 |
89 |
90 |
91 | New Course
92 |
93 |
94 |
95 | {values.image ? values.image.name : ''}
101 |
102 |
112 |
113 | {
114 | values.error && (
115 | error
116 | {values.error})
117 | }
118 |
119 |
120 |
121 |
122 |
123 |
124 |
)
125 | }
126 |
--------------------------------------------------------------------------------
/client/user/EditProfile.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import Card from '@material-ui/core/Card'
3 | import CardActions from '@material-ui/core/CardActions'
4 | import CardContent from '@material-ui/core/CardContent'
5 | import Button from '@material-ui/core/Button'
6 | import TextField from '@material-ui/core/TextField'
7 | import Typography from '@material-ui/core/Typography'
8 | import Icon from '@material-ui/core/Icon'
9 | import FormControlLabel from '@material-ui/core/FormControlLabel'
10 | import Switch from '@material-ui/core/Switch'
11 | import { makeStyles } from '@material-ui/core/styles'
12 | import auth from './../auth/auth-helper'
13 | import {read, update} from './api-user.js'
14 | import {Redirect} from 'react-router-dom'
15 |
16 | const useStyles = makeStyles(theme => ({
17 | card: {
18 | maxWidth: 600,
19 | margin: 'auto',
20 | textAlign: 'center',
21 | marginTop: theme.spacing(12),
22 | paddingBottom: theme.spacing(2)
23 | },
24 | title: {
25 | margin: theme.spacing(2),
26 | color: theme.palette.protectedTitle
27 | },
28 | error: {
29 | verticalAlign: 'middle'
30 | },
31 | textField: {
32 | marginLeft: theme.spacing(1),
33 | marginRight: theme.spacing(1),
34 | width: 300
35 | },
36 | submit: {
37 | margin: 'auto',
38 | marginBottom: theme.spacing(2)
39 | }
40 | }))
41 |
42 | export default function EditProfile({ match }) {
43 | const classes = useStyles()
44 | const [values, setValues] = useState({
45 | name: '',
46 | password: '',
47 | email: '',
48 | open: false,
49 | error: '',
50 | redirectToProfile: false,
51 | educator: false
52 | })
53 | const jwt = auth.isAuthenticated()
54 |
55 | useEffect(() => {
56 | const abortController = new AbortController()
57 | const signal = abortController.signal
58 |
59 | read({
60 | userId: match.params.userId
61 | }, {t: jwt.token}, signal).then((data) => {
62 | if (data && data.error) {
63 | setValues({...values, error: data.error})
64 | } else {
65 | setValues({...values, name: data.name, email: data.email, educator: data.educator})
66 | }
67 | })
68 | return function cleanup(){
69 | abortController.abort()
70 | }
71 |
72 | }, [match.params.userId])
73 |
74 | const clickSubmit = () => {
75 | const user = {
76 | name: values.name || undefined,
77 | email: values.email || undefined,
78 | password: values.password || undefined,
79 | educator: values.educator
80 | }
81 | update({
82 | userId: match.params.userId
83 | }, {
84 | t: jwt.token
85 | }, user).then((data) => {
86 | if (data && data.error) {
87 | setValues({...values, error: data.error})
88 | } else {
89 | auth.updateUser(data, ()=>{
90 | setValues({...values, userId: data._id, redirectToProfile: true})
91 | })
92 | }
93 | })
94 | }
95 | const handleChange = name => event => {
96 | setValues({...values, [name]: event.target.value})
97 | }
98 | const handleCheck = (event, checked) => {
99 | setValues({...values, educator: checked})
100 | }
101 |
102 | if (values.redirectToProfile) {
103 | return ()
104 | }
105 | return (
106 |
107 |
108 |
109 | Edit Profile
110 |
111 |
112 |
113 |
114 |
115 |
116 | I am an Educator
117 |
118 | }
127 | label={values.educator? 'Yes' : 'No'}
128 | />
129 |
{
130 | values.error && (
131 | error
132 | {values.error}
133 | )
134 | }
135 |
136 |
137 |
138 |
139 |
140 | )
141 | }
142 |
143 |
--------------------------------------------------------------------------------
/server/controllers/course.controller.js:
--------------------------------------------------------------------------------
1 | import Course from '../models/course.model'
2 | import extend from 'lodash/extend'
3 | import fs from 'fs'
4 | import errorHandler from './../helpers/dbErrorHandler'
5 | import formidable from 'formidable'
6 | import defaultImage from './../../client/assets/images/default.png'
7 |
8 | const create = (req, res) => {
9 | let form = new formidable.IncomingForm()
10 | form.keepExtensions = true
11 | form.parse(req, async (err, fields, files) => {
12 | if (err) {
13 | return res.status(400).json({
14 | error: "Image could not be uploaded"
15 | })
16 | }
17 | let course = new Course(fields)
18 | course.instructor= req.profile
19 | if(files.image){
20 | course.image.data = fs.readFileSync(files.image.path)
21 | course.image.contentType = files.image.type
22 | }
23 | try {
24 | let result = await course.save()
25 | res.json(result)
26 | }catch (err){
27 | return res.status(400).json({
28 | error: errorHandler.getErrorMessage(err)
29 | })
30 | }
31 | })
32 | }
33 |
34 | /**
35 | * Load course and append to req.
36 | */
37 | const courseByID = async (req, res, next, id) => {
38 | try {
39 | let course = await Course.findById(id).populate('instructor', '_id name')
40 | if (!course)
41 | return res.status('400').json({
42 | error: "Course not found"
43 | })
44 | req.course = course
45 | next()
46 | } catch (err) {
47 | return res.status('400').json({
48 | error: "Could not retrieve course"
49 | })
50 | }
51 | }
52 |
53 | const read = (req, res) => {
54 | req.course.image = undefined
55 | return res.json(req.course)
56 | }
57 |
58 | const list = async (req, res) => {
59 | try {
60 | let courses = await Course.find().select('name email updated created')
61 | res.json(courses)
62 | } catch (err) {
63 | return res.status(400).json({
64 | error: errorHandler.getErrorMessage(err)
65 | })
66 | }
67 | }
68 |
69 | const update = async (req, res) => {
70 | let form = new formidable.IncomingForm()
71 | form.keepExtensions = true
72 | form.parse(req, async (err, fields, files) => {
73 | if (err) {
74 | return res.status(400).json({
75 | error: "Photo could not be uploaded"
76 | })
77 | }
78 | let course = req.course
79 | course = extend(course, fields)
80 | if(fields.lessons){
81 | course.lessons = JSON.parse(fields.lessons)
82 | }
83 | course.updated = Date.now()
84 | if(files.image){
85 | course.image.data = fs.readFileSync(files.image.path)
86 | course.image.contentType = files.image.type
87 | }
88 | try {
89 | await course.save()
90 | res.json(course)
91 | } catch (err) {
92 | return res.status(400).json({
93 | error: errorHandler.getErrorMessage(err)
94 | })
95 | }
96 | })
97 | }
98 |
99 | const newLesson = async (req, res) => {
100 | try {
101 | let lesson = req.body.lesson
102 | let result = await Course.findByIdAndUpdate(req.course._id, {$push: {lessons: lesson}, updated: Date.now()}, {new: true})
103 | .populate('instructor', '_id name')
104 | .exec()
105 | res.json(result)
106 | } catch (err) {
107 | return res.status(400).json({
108 | error: errorHandler.getErrorMessage(err)
109 | })
110 | }
111 | }
112 |
113 | const remove = async (req, res) => {
114 | try {
115 | let course = req.course
116 | let deleteCourse = await course.remove()
117 | res.json(deleteCourse)
118 | } catch (err) {
119 | return res.status(400).json({
120 | error: errorHandler.getErrorMessage(err)
121 | })
122 | }
123 | }
124 |
125 | const isInstructor = (req, res, next) => {
126 | const isInstructor = req.course && req.auth && req.course.instructor._id == req.auth._id
127 | if(!isInstructor){
128 | return res.status('403').json({
129 | error: "User is not authorized"
130 | })
131 | }
132 | next()
133 | }
134 |
135 | const listByInstructor = (req, res) => {
136 | Course.find({instructor: req.profile._id}, (err, courses) => {
137 | if (err) {
138 | return res.status(400).json({
139 | error: errorHandler.getErrorMessage(err)
140 | })
141 | }
142 | res.json(courses)
143 | }).populate('instructor', '_id name')
144 | }
145 |
146 | const listPublished = (req, res) => {
147 | Course.find({published: true}, (err, courses) => {
148 | if (err) {
149 | return res.status(400).json({
150 | error: errorHandler.getErrorMessage(err)
151 | })
152 | }
153 | res.json(courses)
154 | }).populate('instructor', '_id name')
155 | }
156 |
157 | const photo = (req, res, next) => {
158 | if(req.course.image.data){
159 | res.set("Content-Type", req.course.image.contentType)
160 | return res.send(req.course.image.data)
161 | }
162 | next()
163 | }
164 | const defaultPhoto = (req, res) => {
165 | return res.sendFile(process.cwd()+defaultImage)
166 | }
167 |
168 |
169 | export default {
170 | create,
171 | courseByID,
172 | read,
173 | list,
174 | remove,
175 | update,
176 | isInstructor,
177 | listByInstructor,
178 | photo,
179 | defaultPhoto,
180 | newLesson,
181 | listPublished
182 | }
183 |
--------------------------------------------------------------------------------
/client/course/Course.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import Card from '@material-ui/core/Card'
3 | import CardHeader from '@material-ui/core/CardHeader'
4 | import CardMedia from '@material-ui/core/CardMedia'
5 | import Typography from '@material-ui/core/Typography'
6 | import IconButton from '@material-ui/core/IconButton'
7 | import Edit from '@material-ui/icons/Edit'
8 | import PeopleIcon from '@material-ui/icons/Group'
9 | import CompletedIcon from '@material-ui/icons/VerifiedUser'
10 | import Button from '@material-ui/core/Button'
11 | import {makeStyles} from '@material-ui/core/styles'
12 | import List from '@material-ui/core/List'
13 | import ListItem from '@material-ui/core/ListItem'
14 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'
15 | import Avatar from '@material-ui/core/Avatar'
16 | import ListItemText from '@material-ui/core/ListItemText'
17 | import {read, update} from './api-course.js'
18 | import {enrollmentStats} from './../enrollment/api-enrollment'
19 | import {Link, Redirect} from 'react-router-dom'
20 | import auth from './../auth/auth-helper'
21 | import DeleteCourse from './DeleteCourse'
22 | import Divider from '@material-ui/core/Divider'
23 | import NewLesson from './NewLesson'
24 | import Dialog from '@material-ui/core/Dialog'
25 | import DialogActions from '@material-ui/core/DialogActions'
26 | import DialogContent from '@material-ui/core/DialogContent'
27 | import DialogTitle from '@material-ui/core/DialogTitle'
28 | import Enroll from './../enrollment/Enroll'
29 |
30 | const useStyles = makeStyles(theme => ({
31 | root: theme.mixins.gutters({
32 | maxWidth: 800,
33 | margin: 'auto',
34 | padding: theme.spacing(3),
35 | marginTop: theme.spacing(12)
36 | }),
37 | flex:{
38 | display:'flex',
39 | marginBottom: 20
40 | },
41 | card: {
42 | padding:'24px 40px 40px'
43 | },
44 | subheading: {
45 | margin: '10px',
46 | color: theme.palette.openTitle
47 | },
48 | details: {
49 | margin: '16px',
50 | },
51 | sub: {
52 | display: 'block',
53 | margin: '3px 0px 5px 0px',
54 | fontSize: '0.9em'
55 | },
56 | media: {
57 | height: 190,
58 | display: 'inline-block',
59 | width: '100%',
60 | marginLeft: '16px'
61 | },
62 | icon: {
63 | verticalAlign: 'sub'
64 | },
65 | category:{
66 | color: '#5c5c5c',
67 | fontSize: '0.9em',
68 | padding: '3px 5px',
69 | backgroundColor: '#dbdbdb',
70 | borderRadius: '0.2em',
71 | marginTop: 5
72 | },
73 | action: {
74 | margin: '10px 0px',
75 | display: 'flex',
76 | justifyContent: 'flex-end'
77 | },
78 | statSpan: {
79 | margin: '7px 10px 0 10px',
80 | alignItems: 'center',
81 | color: '#616161',
82 | display: 'inline-flex',
83 | '& svg': {
84 | marginRight: 10,
85 | color: '#b6ab9a'
86 | }
87 | },
88 | enroll:{
89 | float: 'right'
90 | }
91 | }))
92 |
93 | export default function Course ({match}) {
94 | const classes = useStyles()
95 | const [stats, setStats] = useState({})
96 | const [course, setCourse] = useState({instructor:{}})
97 | const [values, setValues] = useState({
98 | redirect: false,
99 | error: ''
100 | })
101 | const [open, setOpen] = useState(false)
102 | const jwt = auth.isAuthenticated()
103 | useEffect(() => {
104 | const abortController = new AbortController()
105 | const signal = abortController.signal
106 |
107 | read({courseId: match.params.courseId}, signal).then((data) => {
108 | if (data.error) {
109 | setValues({...values, error: data.error})
110 | } else {
111 | setCourse(data)
112 | }
113 | })
114 | return function cleanup(){
115 | abortController.abort()
116 | }
117 | }, [match.params.courseId])
118 | useEffect(() => {
119 | const abortController = new AbortController()
120 | const signal = abortController.signal
121 |
122 | enrollmentStats({courseId: match.params.courseId}, {t:jwt.token}, signal).then((data) => {
123 | if (data.error) {
124 | setValues({...values, error: data.error})
125 | } else {
126 | setStats(data)
127 | }
128 | })
129 | return function cleanup(){
130 | abortController.abort()
131 | }
132 | }, [match.params.courseId])
133 | const removeCourse = (course) => {
134 | setValues({...values, redirect:true})
135 | }
136 | const addLesson = (course) => {
137 | setCourse(course)
138 | }
139 | const clickPublish = () => {
140 | if(course.lessons.length > 0){
141 | setOpen(true)
142 | }
143 | }
144 | const publish = () => {
145 | let courseData = new FormData()
146 | courseData.append('published', true)
147 | update({
148 | courseId: match.params.courseId
149 | }, {
150 | t: jwt.token
151 | }, courseData).then((data) => {
152 | if (data && data.error) {
153 | setValues({...values, error: data.error})
154 | } else {
155 | setCourse({...course, published: true})
156 | setOpen(false)
157 | }
158 | })
159 | }
160 | const handleClose = () => {
161 | setOpen(false)
162 | }
163 | if (values.redirect) {
164 | return ()
165 | }
166 | const imageUrl = course._id
167 | ? `/api/courses/photo/${course._id}?${new Date().getTime()}`
168 | : '/api/courses/defaultphoto'
169 | return (
170 |
171 |
172 |
175 | By {course.instructor.name}
176 | {course.category}
177 |
178 | }
179 | action={<>
180 | {auth.isAuthenticated().user && auth.isAuthenticated().user._id == course.instructor._id &&
181 | (
182 |
183 |
184 |
185 |
186 |
187 | {!course.published ? (<>
188 |
189 |
190 | >) : (
191 |
192 | )}
193 | )
194 | }
195 | {course.published && (
196 |
{stats.totalEnrolled} enrolled
197 |
{stats.totalCompleted} completed
198 |
199 | )}
200 |
201 | >
202 | }
203 | />
204 |
205 |
210 |
211 |
212 | {course.description}
213 |
214 |
215 | {course.published &&
}
216 |
217 |
218 |
219 |
220 |
221 |
222 |
Lessons
224 | }
225 | subheader={{course.lessons && course.lessons.length} lessons}
226 | action={
227 | auth.isAuthenticated().user && auth.isAuthenticated().user._id == course.instructor._id && !course.published &&
228 | (
229 |
230 | )
231 | }
232 | />
233 |
234 | {course.lessons && course.lessons.map((lesson, index) => {
235 | return(
236 |
237 |
238 |
239 | {index+1}
240 |
241 |
242 |
245 |
246 |
247 | )
248 | }
249 | )}
250 |
251 |
252 |
253 |
266 | )
267 | }
268 |
--------------------------------------------------------------------------------
/client/course/EditCourse.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import Card from '@material-ui/core/Card'
3 | import CardHeader from '@material-ui/core/CardHeader'
4 | import CardMedia from '@material-ui/core/CardMedia'
5 | import Typography from '@material-ui/core/Typography'
6 | import IconButton from '@material-ui/core/IconButton'
7 | import DeleteIcon from '@material-ui/icons/Delete'
8 | import FileUpload from '@material-ui/icons/AddPhotoAlternate'
9 | import ArrowUp from '@material-ui/icons/ArrowUpward'
10 | import Button from '@material-ui/core/Button'
11 | import {makeStyles} from '@material-ui/core/styles'
12 | import List from '@material-ui/core/List'
13 | import ListItem from '@material-ui/core/ListItem'
14 | import TextField from '@material-ui/core/TextField'
15 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'
16 | import Avatar from '@material-ui/core/Avatar'
17 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
18 | import ListItemText from '@material-ui/core/ListItemText'
19 | import {read, update} from './api-course.js'
20 | import {Link, Redirect} from 'react-router-dom'
21 | import auth from './../auth/auth-helper'
22 | import Divider from '@material-ui/core/Divider'
23 |
24 | const useStyles = makeStyles(theme => ({
25 | root: theme.mixins.gutters({
26 | maxWidth: 800,
27 | margin: 'auto',
28 | padding: theme.spacing(3),
29 | marginTop: theme.spacing(12)
30 | }),
31 | flex:{
32 | display:'flex',
33 | marginBottom: 20
34 | },
35 | card: {
36 | padding:'24px 40px 40px'
37 | },
38 | subheading: {
39 | margin: '10px',
40 | color: theme.palette.openTitle
41 | },
42 | details: {
43 | margin: '16px',
44 | },
45 | upArrow: {
46 | border: '2px solid #f57c00',
47 | marginLeft: 3,
48 | marginTop: 10,
49 | padding:4
50 | },
51 | sub: {
52 | display: 'block',
53 | margin: '3px 0px 5px 0px',
54 | fontSize: '0.9em'
55 | },
56 | media: {
57 | height: 250,
58 | display: 'inline-block',
59 | width: '50%',
60 | marginLeft: '16px'
61 | },
62 | icon: {
63 | verticalAlign: 'sub'
64 | },
65 | textfield:{
66 | width: 350
67 | },
68 | action: {
69 | margin: '8px 24px',
70 | display: 'inline-block'
71 | }, input: {
72 | display: 'none'
73 | },
74 | filename:{
75 | marginLeft:'10px'
76 | },
77 | list: {
78 | backgroundColor: '#f3f3f3'
79 | }
80 | }))
81 |
82 | export default function EditCourse ({match}) {
83 | const classes = useStyles()
84 | const [course, setCourse] = useState({
85 | name: '',
86 | description: '',
87 | image:'',
88 | category:'',
89 | instructor:{},
90 | lessons: []
91 | })
92 | const [values, setValues] = useState({
93 | redirect: false,
94 | error: ''
95 | })
96 | useEffect(() => {
97 | const abortController = new AbortController()
98 | const signal = abortController.signal
99 |
100 | read({courseId: match.params.courseId}, signal).then((data) => {
101 | if (data.error) {
102 | setValues({...values, error: data.error})
103 | } else {
104 | data.image = ''
105 | setCourse(data)
106 | }
107 | })
108 | return function cleanup(){
109 | abortController.abort()
110 | }
111 | }, [match.params.courseId])
112 | const jwt = auth.isAuthenticated()
113 | const handleChange = name => event => {
114 | const value = name === 'image'
115 | ? event.target.files[0]
116 | : event.target.value
117 | setCourse({ ...course, [name]: value })
118 | }
119 | const handleLessonChange = (name, index) => event => {
120 | const lessons = course.lessons
121 | lessons[index][name] = event.target.value
122 | setCourse({ ...course, lessons: lessons })
123 | }
124 | const deleteLesson = index => event => {
125 | const lessons = course.lessons
126 | lessons.splice(index, 1)
127 | setCourse({...course, lessons:lessons})
128 | }
129 | const moveUp = index => event => {
130 | const lessons = course.lessons
131 | const moveUp = lessons[index]
132 | lessons[index] = lessons[index-1]
133 | lessons[index-1] = moveUp
134 | setCourse({ ...course, lessons: lessons })
135 | }
136 | const clickSubmit = () => {
137 | let courseData = new FormData()
138 | course.name && courseData.append('name', course.name)
139 | course.description && courseData.append('description', course.description)
140 | course.image && courseData.append('image', course.image)
141 | course.category && courseData.append('category', course.category)
142 | courseData.append('lessons', JSON.stringify(course.lessons))
143 | update({
144 | courseId: match.params.courseId
145 | }, {
146 | t: jwt.token
147 | }, courseData).then((data) => {
148 | if (data && data.error) {
149 | console.log(data.error)
150 | setValues({...values, error: data.error})
151 | } else {
152 | setValues({...values, redirect: true})
153 | }
154 | })
155 | }
156 | if (values.redirect) {
157 | return ()
158 | }
159 | const imageUrl = course._id
160 | ? `/api/courses/photo/${course._id}?${new Date().getTime()}`
161 | : '/api/courses/defaultphoto'
162 | return (
163 |
164 |
165 | }
173 | subheader={
174 | By {course.instructor.name}
175 | {}
182 |
183 | }
184 | action={
185 | auth.isAuthenticated().user && auth.isAuthenticated().user._id == course.instructor._id &&
186 | (
187 | )
188 | }
189 | />
190 |
191 |
196 |
197 |
206 |
207 | {course.image ? course.image.name : ''}
213 |
214 |
215 |
216 |
217 |
218 |
219 |
Lessons - Edit and Rearrange
221 | }
222 | subheader={{course.lessons && course.lessons.length} lessons}
223 | />
224 |
225 | {course.lessons && course.lessons.map((lesson, index) => {
226 | return(
227 |
228 |
229 | <>
230 |
231 | {index+1}
232 |
233 | { index != 0 &&
234 |
235 |
236 |
237 | }
238 | >
239 |
240 |
248 |
257 |
>}
264 | />
265 | {!course.published &&
266 |
267 |
268 |
269 | }
270 |
271 |
272 | )
273 | }
274 | )}
275 |
276 |
277 |
278 |
)
279 | }
280 |
--------------------------------------------------------------------------------
/client/enrollment/Enrollment.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from 'react'
2 | import Card from '@material-ui/core/Card'
3 | import CardHeader from '@material-ui/core/CardHeader'
4 | import CardMedia from '@material-ui/core/CardMedia'
5 | import CardActions from '@material-ui/core/CardActions'
6 | import Typography from '@material-ui/core/Typography'
7 | import Button from '@material-ui/core/Button'
8 | import {makeStyles} from '@material-ui/core/styles'
9 | import List from '@material-ui/core/List'
10 | import ListItem from '@material-ui/core/ListItem'
11 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'
12 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'
13 | import ListSubheader from '@material-ui/core/ListSubheader'
14 | import Avatar from '@material-ui/core/Avatar'
15 | import ListItemIcon from '@material-ui/core/ListItemIcon'
16 | import ListItemText from '@material-ui/core/ListItemText'
17 | import {read, complete} from './api-enrollment.js'
18 | import {Link} from 'react-router-dom'
19 | import auth from './../auth/auth-helper'
20 | import Divider from '@material-ui/core/Divider'
21 | import Drawer from '@material-ui/core/Drawer'
22 | import Info from '@material-ui/icons/Info'
23 | import CheckCircle from '@material-ui/icons/CheckCircle'
24 | import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUnchecked'
25 | import { CardContent } from '@material-ui/core'
26 |
27 |
28 | const useStyles = makeStyles(theme => ({
29 | root: theme.mixins.gutters({
30 | maxWidth: 800,
31 | margin: 'auto',
32 | marginTop: theme.spacing(12),
33 | marginLeft: 250
34 | }),
35 | heading: {
36 | marginBottom: theme.spacing(3),
37 | fontWeight: 200
38 | },
39 | flex:{
40 | display:'flex',
41 | marginBottom: 20
42 | },
43 | card: {
44 | padding:'24px 40px 20px'
45 | },
46 | subheading: {
47 | margin: '10px',
48 | color: theme.palette.openTitle
49 | },
50 | details: {
51 | margin: '16px',
52 | },
53 | sub: {
54 | display: 'block',
55 | margin: '3px 0px 5px 0px',
56 | fontSize: '0.9em'
57 | },
58 | avatar: {
59 | color: '#9b9b9b',
60 | border: '1px solid #bdbdbd',
61 | background: 'none'
62 | },
63 | media: {
64 | height: 180,
65 | display: 'inline-block',
66 | width: '100%',
67 | marginLeft: '16px'
68 | },
69 | icon: {
70 | verticalAlign: 'sub'
71 | },
72 | category:{
73 | color: '#5c5c5c',
74 | fontSize: '0.9em',
75 | padding: '3px 5px',
76 | backgroundColor: '#dbdbdb',
77 | borderRadius: '0.2em',
78 | marginTop: 5
79 | },
80 | action: {
81 | margin: '8px 24px',
82 | display: 'inline-block'
83 | },
84 | drawer: {
85 | width: 240,
86 | flexShrink: 0,
87 | },
88 | drawerPaper: {
89 | width: 240,
90 | backgroundColor: '#616161'
91 | },
92 | content: {
93 | flexGrow: 1,
94 | padding: theme.spacing(3),
95 | },
96 | toolbar: theme.mixins.toolbar,
97 | selectedDrawer: {
98 | backgroundColor: '#e9e3df'
99 | },
100 | unselected: {
101 | backgroundColor: '#ffffff'
102 | },
103 | check: {
104 | color:'#38cc38'
105 | },
106 | subhead: {
107 | fontSize: '1.2em'
108 | },
109 | progress: {
110 | textAlign: 'center',
111 | color: '#dfdfdf',
112 | '& span':{
113 | color: '#fffde7',
114 | fontSize: '1.15em'
115 | }
116 | },
117 | para: {
118 | whiteSpace: 'pre-wrap'
119 | }
120 | }))
121 |
122 | export default function Enrollment ({match}) {
123 | const classes = useStyles()
124 | const [enrollment, setEnrollment] = useState({course:{instructor:[]}, lessonStatus: []})
125 | const [values, setValues] = useState({
126 | error: '',
127 | drawer: -1
128 | })
129 | const [totalComplete, setTotalComplete] = useState(0)
130 | const jwt = auth.isAuthenticated()
131 | useEffect(() => {
132 | const abortController = new AbortController()
133 | const signal = abortController.signal
134 |
135 | read({enrollmentId: match.params.enrollmentId}, {t: jwt.token}, signal).then((data) => {
136 | if (data.error) {
137 | setValues({...values, error: data.error})
138 | } else {
139 | totalCompleted(data.lessonStatus)
140 | setEnrollment(data)
141 | }
142 | })
143 | return function cleanup(){
144 | abortController.abort()
145 | }
146 | }, [match.params.enrollmentId])
147 | const totalCompleted = (lessons) => {
148 | let count = lessons.reduce((total, lessonStatus) => {return total + (lessonStatus.complete ? 1 : 0)}, 0)
149 | setTotalComplete(count)
150 | return count
151 | }
152 | const selectDrawer = (index) => event => {
153 | setValues({...values, drawer:index})
154 | }
155 | const markComplete = () => {
156 | if(!enrollment.lessonStatus[values.drawer].complete){
157 | const lessonStatus = enrollment.lessonStatus
158 | lessonStatus[values.drawer].complete = true
159 | let count = totalCompleted(lessonStatus)
160 |
161 | let updatedData = {}
162 | updatedData.lessonStatusId = lessonStatus[values.drawer]._id
163 | updatedData.complete = true
164 |
165 | if(count == lessonStatus.length){
166 | updatedData.courseCompleted = Date.now()
167 | }
168 |
169 | complete({
170 | enrollmentId: match.params.enrollmentId
171 | }, {
172 | t: jwt.token
173 | }, updatedData).then((data) => {
174 | if (data && data.error) {
175 | setValues({...values, error: data.error})
176 | } else {
177 | setEnrollment({...enrollment, lessonStatus: lessonStatus})
178 | }
179 | })
180 | }
181 | }
182 | const imageUrl = enrollment.course._id
183 | ? `/api/courses/photo/${enrollment.course._id}?${new Date().getTime()}`
184 | : '/api/courses/defaultphoto'
185 | return (
186 |
187 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | Lessons
204 |
205 | {enrollment.lessonStatus.map((lesson, index) => (
206 |
207 |
208 |
209 | {index+1}
210 |
211 |
212 |
213 |
214 | { lesson.complete ? : }
215 |
216 |
217 | ))}
218 |
219 |
220 |
221 |
222 | {totalComplete} out of {enrollment.lessonStatus.length} completed
} />
223 |
224 |
225 |
226 | {values.drawer == - 1 &&
227 |
228 |
231 | By {enrollment.course.instructor.name}
232 | {enrollment.course.category}
233 |
234 | }
235 | action={
236 | totalComplete == enrollment.lessonStatus.length &&
237 | (
238 |
241 | )
242 | }
243 | />
244 |
245 |
250 |
251 |
252 | {enrollment.course.description}
253 |
254 |
255 |
256 |
257 |
258 |
Lessons
260 | }
261 | subheader={{enrollment.course.lessons && enrollment.course.lessons.length} lessons}
262 | action={
263 | auth.isAuthenticated().user && auth.isAuthenticated().user._id == enrollment.course.instructor._id &&
264 | (
265 |
266 | )
267 | }
268 | />
269 |
270 | {enrollment.course.lessons && enrollment.course.lessons.map((lesson, i) => {
271 | return(
272 |
273 |
274 |
275 | {i+1}
276 |
277 |
278 |
281 |
282 |
283 | )
284 | }
285 | )}
286 |
287 |
288 | }
289 | {values.drawer != -1 && (<>
290 | {enrollment.course.name}
291 |
292 | {enrollment.lessonStatus[values.drawer].complete? "Completed" : "Mark as complete"}} />
295 |
296 | {enrollment.course.lessons[values.drawer].content}
297 |
298 |
299 |
300 |
301 | >)}
302 | )
303 | }
--------------------------------------------------------------------------------