├── .gitignore ├── README.md ├── bin └── examples ├── package-lock.json ├── package.json ├── public ├── favicon.jpg ├── index.html ├── manifest.json └── url.js └── src ├── App.js ├── Components ├── ClassNotificationBar.js ├── Classroom_Components │ ├── ClassMenu.js │ ├── Drawer.js │ ├── FindComponent.js │ ├── GroupStatus.js │ ├── GroupWhiteboard.js │ ├── GroupingMenu │ │ ├── App.js │ │ ├── App.scss │ │ ├── App.test.js │ │ ├── Card │ │ │ ├── Card.js │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── CardLayer │ │ │ ├── CardLayer.js │ │ │ └── index.js │ │ ├── CardWall │ │ │ ├── CardWall.js │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── ManageGroup.js │ │ ├── index.css │ │ ├── index.js │ │ └── logo.svg │ ├── JoinedLayoutStudent.js │ ├── JoinedLayoutTeacher.js │ ├── LocalStream.js │ ├── ParticipantList.js │ ├── RemoteStream.js │ ├── RndContainer.js │ ├── ShareBtn.js │ ├── UserCardMenu.js │ ├── ViewButton.js │ ├── WebcamPermissionStatus.js │ ├── Whiteboard.js │ ├── Whiteboard_Components │ │ ├── CanvasInsideGroupWhiteboard.js │ │ ├── CanvasInsideWhiteboard.js │ │ ├── Portal.js │ │ ├── Rectangle.js │ │ └── TransformerComponent.js │ └── thumbnail │ │ ├── .DS_Store │ │ ├── background.png │ │ ├── clear.png │ │ ├── download.png │ │ ├── eraser.png │ │ ├── graph-grid-icon.png │ │ ├── load-image.png │ │ ├── pen.png │ │ ├── redo.png │ │ ├── undo.png │ │ └── upload.png ├── Content_Components │ ├── UserCard.js │ ├── UserCardSmall.js │ └── tools │ │ ├── Pencil.js │ │ └── index.js ├── DrawerLeft.js ├── DrawerRight.js ├── Drawer_Components │ ├── Classroom.js │ ├── DrawerLeftList.js │ ├── DrawerRightList.js │ ├── History.js │ ├── Locker.js │ ├── ScrollableTabs.js │ ├── Sketch.js │ ├── UserSettings.js │ └── index.js ├── LeftPanel.js ├── Loading.js ├── Locations │ ├── ClassList.js │ ├── ClassListStudent.js │ ├── ClassListTeacher.js │ ├── Classroom.js │ ├── Content.js │ ├── Mailbox.js │ ├── Notebooks.js │ ├── PopupFunction │ │ ├── ChangeUserInfo.js │ │ ├── CreateClass.js │ │ ├── EnrollClass.js │ │ └── ViewClassStudentsInfo.js │ └── Upload.js ├── NestedListInFind.js ├── NotificationBar.js ├── Popover.js ├── PopoverNarrow.js ├── PopoverWithBtn.js ├── RegisterBtn.js └── index.js ├── Login.js ├── css ├── App.css ├── ask_camera_permission.jpeg ├── ask_camera_permission.jpg ├── ask_camera_permission.jpg.sb-e2fe0202-Ek4Ixv ├── ask_camera_permission.jpg.sb-e2fe0202-rASvL6 ├── classroom.jpg ├── classroom.jpg.bk ├── classroom.png ├── index.css ├── whiteboard-no-cut.png ├── whiteboard.png └── yoda.jpg ├── index.js ├── interface ├── connection.js └── util.js └── registerServiceWorker.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | compile 4 | .idea 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Realistic and Interactive Online Classroom (Frontend) 2 | 3 | Demo: [https://overcoded.tk](https://overcoded.tk "Realistic and Interactive Online Classroom") 4 | 5 | Elixir backend Github repo: [https://github.com/herbert1228/online-classroom-elixir-server](https://github.com/herbert1228/online-classroom-elixir-server "Elixir backend") 6 | 7 | ## Update: 8 | 9 | - Grouping 10 | - Teacher can assign students into groups 11 | - Students in the same group can edit a shared whiteboard (work together) 12 | 13 | - Class Management 14 | - Teacher can control the webcam permission of students 15 | - enable/disable audio 16 | - enable/disable video 17 | 18 | ## First stable version with the followings major features: 19 | 20 | - Create classroom session letting students to enroll and join. 21 | - Enter a classroom as Teacher (owner of the classroom). 22 | - Enter a classroom as Student (after enrolling with Teacher's username 23 | and classroom name). 24 | - Desktop like classroom, components are draggable and some are 25 | resizable. 26 | - Classroom components: 27 | - Webcam of teacher and all students 28 | - Peer to Peer connection 29 | - Mute, Stop, Re-call 30 | 31 | - Sync and Shared Whiteboard 32 | - Self-whiteboard is editable 33 | - Other whiteboards are view only 34 | - Sync instantly 35 | - Can view teacher's whiteboard as student Only 36 | - Can view all students' whiteaboard as Teacher 37 | - Edit with: 38 | - Add/remove/drag/resize/rotate: images and texts 39 | - Draw with Pen 40 | shared whiteboard 41 | 42 | - Drawer (File system) 43 | - Upload by selecting / dragging 44 | - View image with popup window component 45 | - Delete a file 46 | - Download a file 47 | - Share a file with teacher or students 48 | 49 | 50 | - Connection driver for communicating with an elixir backend hosted at AWS ec2 server 51 | - 3 protocols 52 | - simple call / cast (call is synchronous which returns the value, cast is asynchronous) 53 | - signaling server call / cast 54 | - whiteboard server call / cast 55 | - Websocket secure wss (major) 56 | - Https (upload/download file) 57 | - Also support: ws and http 58 | - STUN/TURN (hosted on our AWS ec2 server) config 59 | 60 | ## Screenshot 61 | 62 | ![screenshot](https://github.com/herbert1228/online-classroom-react.js-client/blob/master/src/css/classroom.png) 63 | -------------------------------------------------------------------------------- /bin/examples: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/bin/examples -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myWebsite", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^1.5.1", 7 | "@material-ui/icons": "^1.1.1", 8 | "ajv": "^6.5.3", 9 | "axios": "^0.18.0", 10 | "classnames": "^2.2.6", 11 | "install": "^0.12.2", 12 | "jshint": "^2.9.7", 13 | "konva": "^2.6.0", 14 | "loadash": "^1.0.0", 15 | "lodash": "^4.17.11", 16 | "npm": "^6.4.1", 17 | "prop-types": "^15.6.2", 18 | "rc-color-picker": "^1.2.6", 19 | "react": "^16.8.1", 20 | "react-color": "^2.17.0", 21 | "react-cookie": "^3.0.4", 22 | "react-dnd": "5.0.0", 23 | "react-dnd-html5-backend": "5.0.1", 24 | "react-dom": "^16.8.2", 25 | "react-dropzone": "^8.0.3", 26 | "react-jss": "^8.6.1", 27 | "react-konva": "^16.8.0", 28 | "react-rnd": "^9.1.1", 29 | "react-router-dom": "^4.3.1", 30 | "react-scripts": "3.0.0", 31 | "redux": "^4.0.1", 32 | "simple-peer": "^9.1.2", 33 | "typeface-roboto": "0.0.54", 34 | "use-image": "^1.0.3", 35 | "uuid": "^3.3.2", 36 | "webrtc-adapter": "^6.4.8" 37 | }, 38 | "scripts": { 39 | "start": "PORT=3000 react-scripts start", 40 | "test": "react-scripts test --env=jsdom", 41 | "eject": "react-scripts eject", 42 | "build": "react-scripts build", 43 | "deploy": "react-scripts build && rsync -avh ./build/* overcoded.tk:overcoded.tk", 44 | "upload": "rsync -avh ./build/* overcoded.tk:overcoded.tk" 45 | }, 46 | "devDependencies": { 47 | "babel-plugin-transform-decorators": "^6.24.1", 48 | "node-sass": "^4.11.0", 49 | "react-redux": "^6.0.0", 50 | "sass-loader": "^7.1.0", 51 | "ws": "^6.1.2" 52 | }, 53 | "browserslist": { 54 | "production": [ 55 | ">0.2%", 56 | "not dead", 57 | "not op_mini all" 58 | ], 59 | "development": [ 60 | "last 1 chrome version", 61 | "last 1 firefox version", 62 | "last 1 safari version" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/public/favicon.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 23 | Classroom 24 | 31 | 32 | 33 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Classroom", 3 | "name": "Create Classroom", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/url.js: -------------------------------------------------------------------------------- 1 | // Version 3 (First stable beta version) 2 | // textarea -> overflow hidden 3 | 4 | // url @overcoded 5 | // SERVER_URL = `wss://${window.location.hostname}/socket` 6 | // UPLOAD_URL = `https://${window.location.hostname}` 7 | 8 | //newline 9 | 10 | // @localhost 11 | 12 | // SERVER_URL = "ws://overcoded.tk:8500" 13 | // UPLOAD_URL = "http://overcoded.tk:8600" 14 | 15 | SERVER_URL = `ws://${window.location.hostname}:8500/` 16 | UPLOAD_URL = `http://${window.location.hostname}:8600` -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react' 2 | import {compose} from 'redux' 3 | import {Content, Classroom, Notebooks, Mailbox, NotificationBar, ClassNotificationBar} from './Components' 4 | import PropTypes, {instanceOf} from 'prop-types' 5 | import {Cookies, withCookies} from 'react-cookie' 6 | import {withStyles} from '@material-ui/core' 7 | import DrawerLeft from './Components/DrawerLeft' 8 | import './css/App.css' 9 | import Login from './Login' 10 | import { connect } from 'react-redux' 11 | import {store} from './index' 12 | import { connection as conn } from './interface/connection' 13 | import {drawerWidth} from './Components/index' 14 | import ClassListStudent from './Components/Locations/ClassListStudent'; 15 | import ClassListTeacher from './Components/Locations/ClassListTeacher'; 16 | 17 | const styles = theme => ({ 18 | root: { 19 | height: "100vh", //100% after adding more components 20 | zIndex: 1, 21 | overflow: 'hidden', 22 | position: 'relative', 23 | display: 'flex', 24 | }, 25 | content: { 26 | backgroundColor: theme.palette.background.default, 27 | marginLeft: drawerWidth, 28 | width: `calc(100% - ${drawerWidth}px)`, 29 | minWidth: 0, // So the Typography noWrap works 30 | display: 'relative' 31 | } 32 | }) 33 | 34 | class App extends React.Component { 35 | static propTypes = { 36 | cookies: instanceOf(Cookies).isRequired 37 | } 38 | 39 | state = { 40 | showNotification: false, 41 | notificationMessage: "", 42 | showClassNotification: false, 43 | classNotificationMessage: "", 44 | otherId: null, 45 | } 46 | 47 | notificationQueue = [] 48 | classNotificationQueue = [] 49 | 50 | changeScene = async (target) => { 51 | if (this.props.joined && this.props.location === 1) { 52 | const response = await conn.call("leave_class") 53 | if (response.type === "ok") { 54 | store.dispatch({type: "leaveClass"}) 55 | // this.handleNotification("leave_class success") 56 | } else throw new Error("invalid action: leave class") 57 | } 58 | store.dispatch({type: "changeLocation", target}) 59 | this.props.cookies.set("location", target) 60 | } 61 | 62 | handleNotification = (message) => { 63 | this.notificationQueue.push(message) 64 | if (this.state.showNotification) { 65 | this.setState({ showNotification: false }); 66 | } else { 67 | this.processQueue(); 68 | } 69 | } 70 | 71 | processQueue = () => { 72 | if (this.notificationQueue.length > 0) { 73 | this.setState({ 74 | notificationMessage: this.notificationQueue.shift(), 75 | showNotification: true, 76 | }) 77 | } 78 | } 79 | 80 | handleDismissNotification = (event, reason) => { 81 | if (reason === 'clickaway') { 82 | return; 83 | } 84 | this.setState({ showNotification: false }); 85 | } 86 | 87 | // In-class notification 88 | 89 | handleClassNotification = (message) => { 90 | this.classNotificationQueue.push(message) 91 | if (this.state.showClassNotification) { 92 | this.setState({ showClassNotification: false }); 93 | } else { 94 | this.classProcessQueue(); 95 | } 96 | } 97 | 98 | classProcessQueue = () => { 99 | if (this.classNotificationQueue.length > 0) { 100 | this.setState({ 101 | classNotificationMessage: this.classNotificationQueue.shift(), 102 | showClassNotification: true, 103 | }) 104 | } 105 | } 106 | 107 | handleDismissClassNotification = (event, reason) => { 108 | if (reason === 'clickaway') { 109 | return; 110 | } 111 | this.setState({ showClassNotification: false }); 112 | } 113 | 114 | render() { 115 | const {classes, ...others} = this.props 116 | return ( 117 | 118 | 123 | 128 | {(this.props.self) && 129 |
130 | {this.props.drawerOpen && 131 | 138 | } 139 |
140 | {this.props.location === 0 &&} 141 | {this.props.location === 1 &&} 142 | {/* {this.props.location === 2 &&} */} 143 | {this.props.location === 2.1 &&} 144 | {this.props.location === 2.2 &&} 145 | {this.props.location === 3 &&} 146 | {this.props.location === 4 &&} 147 |
148 |
149 | } 150 | {(!this.props.self) && 151 | 152 | } 153 |
154 | //TODO add loading animation here 155 | ) 156 | } 157 | } 158 | 159 | App.propTypes = { 160 | classes: PropTypes.object.isRequired, 161 | theme: PropTypes.object.isRequired, 162 | } 163 | 164 | function mapStateToProps(state) { 165 | return { 166 | createdClass: state.createdClass, 167 | enrolledClass: state.enrolledClass, 168 | startedClass: state.startedClass, 169 | session_user: state.session_user, 170 | location: state.location, 171 | self: state.self, 172 | joined: state.joined, 173 | peerConn: state.peerConn, 174 | drawerOpen: state.drawerOpen, 175 | lastJoin: state.lastJoin, 176 | groupCards: state.groupCards, 177 | group: state.group, 178 | webcamPermission: state.webcamPermission 179 | } 180 | } 181 | 182 | export default compose( 183 | withCookies, 184 | withStyles(styles, {withTheme: true}), 185 | connect(mapStateToProps) 186 | )(App) -------------------------------------------------------------------------------- /src/Components/ClassNotificationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | // import Button from '@material-ui/core/Button'; 5 | import Snackbar from '@material-ui/core/Snackbar'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import CloseIcon from '@material-ui/icons/Close'; 8 | import Slide from '@material-ui/core/Slide'; 9 | 10 | const styles = theme => ({ 11 | close: { 12 | padding: theme.spacing.unit / 2, 13 | }, 14 | }) 15 | 16 | // function TransitionLeft() { 17 | // return 18 | // } 19 | 20 | class ClassNotificationBar extends React.Component { 21 | state = { 22 | // Transition: null 23 | } 24 | // componentDidMount() { 25 | // this.setState({open: true, TransitionLeft}) 26 | // } 27 | render() { 28 | const { classes } = this.props; 29 | return ( 30 |
31 | {this.props.message}} 45 | action={[ 46 | //, 49 | 56 | 57 | , 58 | ]} 59 | /> 60 |
61 | ); 62 | } 63 | } 64 | 65 | ClassNotificationBar.propTypes = { 66 | classes: PropTypes.object.isRequired, 67 | }; 68 | 69 | export default withStyles(styles)(ClassNotificationBar); -------------------------------------------------------------------------------- /src/Components/Classroom_Components/ClassMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button, Menu, MenuItem} from '@material-ui/core' 3 | import Leave from '@material-ui/icons/DirectionsRun' 4 | import {withStyles} from '@material-ui/core/styles' 5 | import { connection as conn } from '../../interface/connection'; 6 | import { store } from '../..'; 7 | 8 | const styles = theme => ({}) 9 | 10 | class ClassMenu extends React.Component { 11 | state = { 12 | anchorEl: null, 13 | } 14 | 15 | async leave() { 16 | this.handleClose() 17 | const response = await conn.call("leave_class") 18 | if (response.type === "ok") { 19 | store.dispatch({type: "leaveClass"}) 20 | this.props.changeScene(1) 21 | // this.props.handleNotification("leave_class success") 22 | // this.setState({session_user: [], joined: null}) //TODO 23 | } else throw new Error("invalid action: leave class") 24 | } 25 | 26 | handleClick = event => { 27 | this.setState({ anchorEl: event.currentTarget }) 28 | } 29 | 30 | handleClose = () => { 31 | this.setState({ anchorEl: null }) 32 | } 33 | 34 | render() { 35 | const { anchorEl } = this.state; 36 | // const { classes } = this.props; 37 | 38 | return ( 39 |
40 | 49 | 55 | 56 | 57 | 58 | {/*My account*/} 59 | {/*Logout*/} 60 | 61 |
62 | ) 63 | } 64 | } 65 | 66 | export default withStyles(styles)(ClassMenu); -------------------------------------------------------------------------------- /src/Components/Classroom_Components/Drawer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import PropTypes from 'prop-types' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import {Card, CardHeader, Divider, Grid} from '@material-ui/core' 5 | import List from '@material-ui/core/List'; 6 | import ListItem from '@material-ui/core/ListItem'; 7 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'; 8 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 9 | import ListItemText from '@material-ui/core/ListItemText'; 10 | import Avatar from '@material-ui/core/Avatar'; 11 | import IconButton from '@material-ui/core/IconButton'; 12 | import FolderIcon from '@material-ui/icons/Folder'; 13 | import DeleteIcon from '@material-ui/icons/Delete'; 14 | import Dropzone from 'react-dropzone' 15 | import classNames from 'classnames' 16 | import {connection as conn, uploadURL} from '../../interface/connection' 17 | import {withCookies} from 'react-cookie' 18 | import {compose} from 'redux' 19 | import RndContainer from './RndContainer'; 20 | import { FileDownload, PinDrop } from '@material-ui/icons'; 21 | import ShareBrn from './ShareBtn' 22 | import ViewButton from './ViewButton' 23 | 24 | const styles = theme => ({ 25 | card: { 26 | width: 450, 27 | height: 550, 28 | }, 29 | griditems: { 30 | display: 'flex', 31 | justifyContent: 'space-between' 32 | }, 33 | root: { 34 | flexGrow: 1, 35 | maxWidth: 752, 36 | }, 37 | demo: { 38 | backgroundColor: theme.palette.background.paper, 39 | }, 40 | title: { 41 | margin: `${theme.spacing.unit * 4}px 0 ${theme.spacing.unit * 2}px`, 42 | }, 43 | infolist: { 44 | width: 450, 45 | height: 150 46 | }, 47 | listItem: { 48 | width: 285, // 270 49 | overflow: "hidden", 50 | }, 51 | dropzone: { 52 | 53 | }, 54 | }) 55 | 56 | class Drawer extends React.Component { 57 | state = { 58 | files: [], 59 | } 60 | 61 | async componentDidMount() { 62 | const response = await conn.call("get_filenames_in_drawer") 63 | if (response.result) this.setState({files: response.result}) 64 | conn.addListener('drawer_item_change', this.handleDrawerChange) 65 | } 66 | 67 | handleDrawerChange = e => { 68 | this.setState({files: e.result}, () => { 69 | this.props.handleNotification(`Received file from ${e.from}`) 70 | }) 71 | } 72 | 73 | // handleDrag = (filename) => { 74 | // const username = this.props.cookies.get("name") 75 | // const password = this.props.cookies.get("password") 76 | // fetch(uploadURL+`/download/${username}/${password}/${filename}`) 77 | // .then(response => response.text()) 78 | // .then(data => console.log(data)) 79 | // .catch(e => {this.props.handleNotification(`${e}`)}) 80 | // } 81 | 82 | handleDownload = filename => { 83 | const username = this.props.cookies.get("name") 84 | const password = this.props.cookies.get("password") 85 | window.open(uploadURL+`/download/${username}/${password}/${filename}`) 86 | } 87 | 88 | handleDelete = async filename => { 89 | const response = await conn.call("file_delete", {filename}) 90 | if (response) { 91 | if (response.result === "ok") { 92 | this.setState({files: response.files}) 93 | this.props.handleNotification(`Delete file ${filename} success`) 94 | } else {this.props.handleNotification(`Delete file error: Invalid response`)} 95 | } else {this.props.handleNotification(`Internal server error: Delete file failed`)} 96 | } 97 | 98 | handleDrop = (acceptedFiles, rejectedFiles) => { 99 | if (acceptedFiles.length > 0){ 100 | let formdata = new FormData() 101 | formdata.append("data", acceptedFiles[0], acceptedFiles[0].name) //3rd arg refer to filename 102 | formdata.append("timestamp", (new Date()).toISOString()) 103 | formdata.append("username", this.props.cookies.get("name")) 104 | formdata.append("password", this.props.cookies.get("password")) 105 | fetch(uploadURL+'/upload', { 106 | method: "POST", 107 | body: formdata 108 | }) 109 | .then(response => response.text()) 110 | .then(data => this.props.handleNotification(data)) 111 | .catch(e => {this.props.handleNotification(`${e}`)}) 112 | } 113 | } 114 | 115 | breakFilenameToLines(filename) { 116 | const len = 21 117 | if (filename.length > len) { 118 | return filename.substr(0, len) + ' ' + filename.substr(len) 119 | } 120 | return filename 121 | } 122 | 123 | render() { 124 | const {classes, ...other} = this.props; 125 | return ( 126 | 129 | 130 | Personal Drawer} 133 | style={{ 134 | height: 18, 135 | paddingTop: 7, 136 | backgroundColor: "#e9e7e74d", 137 | overflow: "hidden", 138 | whiteSpace: "nowrap", 139 | textOverflow: "ellipsis" 140 | }} 141 | action={ 142 | this.props.pinTop}> 143 | 144 | 145 | } 146 | /> 147 | 148 | 154 | 155 |
156 | 157 | {this.state.files && 158 | this.state.files.map(filename => 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 173 | 174 | this.handleDownload(filename)}> 175 | 176 | 177 | this.handleDelete(filename)}> 178 | 179 | 180 | 181 | 182 | )} 183 | 184 | 185 | {({ getRootProps, getInputProps, isDragActive }) => { 186 | return ( 187 |
191 | 192 | { 193 | isDragActive ? 194 |

Drop files here... (50MB max)

: 195 |

Drop files here, or click to select files to upload (50MB max)

196 | } 197 |
198 | ) 199 | }} 200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 | ) 209 | } 210 | } 211 | 212 | export default compose( 213 | withCookies, 214 | withStyles(styles, {withTheme: true}), 215 | )(Drawer) -------------------------------------------------------------------------------- /src/Components/Classroom_Components/FindComponent.js: -------------------------------------------------------------------------------- 1 | import PopoverNarrow from '../PopoverNarrow' 2 | import React, { Fragment } from 'react'; 3 | import {withStyles} from '@material-ui/core/styles' 4 | import { DialogContent, IconButton, Button, ListItem, ListItemText, Collapse, ListItemIcon } from "@material-ui/core"; 5 | import { FindInPage, List, ExpandLess, ExpandMore, StarBorder } from '@material-ui/icons'; 6 | import NestedListInFind from '../NestedListInFind'; 7 | 8 | const styles = theme => ({ 9 | listRoot: { 10 | width: '100%', 11 | // maxWidth: , 12 | backgroundColor: theme.palette.background.paper, 13 | }, 14 | nested: { 15 | paddingLeft: theme.spacing.unit * 4, 16 | } 17 | }) 18 | 19 | class FindComponent extends React.Component { 20 | state = { 21 | open: false, 22 | listOpen: false 23 | } 24 | 25 | handleClose = () => { 26 | this.setState({open: false}) 27 | } 28 | 29 | handleOpen = () => { 30 | this.setState({open: true}) 31 | } 32 | 33 | bringTop = component => { 34 | this.props.bringTop(component) 35 | this.handleClose() 36 | } 37 | 38 | render() { 39 | const { classes, components, ...others } = this.props 40 | return ( 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | ); 52 | } 53 | } 54 | 55 | function hasGroup(groupId, props) { 56 | return props.groupCards.filter(group => group.status === groupId).length > 0 57 | } 58 | 59 | export default withStyles(styles, {withTheme: true})(FindComponent) -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupStatus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Tooltip from '@material-ui/core/Tooltip'; 4 | import { Typography } from '@material-ui/core'; 5 | 6 | class GroupStatus extends React.Component { 7 | state = { 8 | open: false, 9 | }; 10 | 11 | handleTooltipClose = () => { 12 | this.setState({ open: false }); 13 | }; 14 | 15 | handleTooltipOpen = () => { 16 | this.setState({ open: true }); 17 | }; 18 | 19 | render() { 20 | return ( 21 | 27 | Groupmates: 28 | {(this.props.group.members || []).map(member => 29 | {member} 30 | )} 31 | 32 | } 33 | > 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | } 41 | 42 | export default GroupStatus; -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { DragDropContext } from 'react-dnd' 3 | import HTML5Backend from 'react-dnd-html5-backend' 4 | 5 | import Card from './Card' 6 | import CardWall from './CardWall' 7 | import CardLayer from './CardLayer' 8 | import {ClassStatusChannel} from '../../../interface/connection' 9 | 10 | import './App.scss' 11 | 12 | class App extends Component { 13 | constructor(props) { 14 | super(props) 15 | this.state = { 16 | groups: ['All', 'Group1', 'Group2', 'Group3'] 17 | } 18 | this.updateCardStatus = this.updateCardStatus.bind(this) 19 | } 20 | 21 | updateCardStatus(cardId, targetStatus) { 22 | const cards = this.props.groupCards 23 | const targetIndex = cards.findIndex(c => (cardId === c.id)) 24 | cards[targetIndex].status = targetStatus // update card status 25 | 26 | const targetCard = cards.splice(targetIndex, 1)[0] // delete old card 27 | cards.push(targetCard) // insert target card to last position of the array 28 | 29 | const result = ClassStatusChannel.changeGroup(cardId, targetStatus) 30 | this.props.handleNotification(`${cardId} is grouped to ${targetStatus}`) 31 | } 32 | 33 | groupOfCards() { 34 | const cards = this.props.groupCards 35 | const cardsGroup = {} 36 | 37 | cards.forEach((card) => { 38 | if (Array.isArray(cardsGroup[card.status])) { 39 | cardsGroup[card.status].push(card) 40 | } else { 41 | cardsGroup[card.status] = [card] 42 | } 43 | }) 44 | return cardsGroup 45 | } 46 | 47 | render() { 48 | const cards = this.groupOfCards() 49 | 50 | return ( 51 |
52 |
53 | 54 | { 55 | this.state.groups.map(status => ( 56 | 61 | { 62 | (cards[status] || []).map(card => ( 63 | 69 | )) 70 | } 71 | 72 | )) 73 | } 74 |
75 |
76 | ); 77 | } 78 | } 79 | 80 | export default DragDropContext(HTML5Backend)(App) 81 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | height: 100%; 3 | background-color: rgba(126, 196, 243, 0.068); 4 | } 5 | 6 | .board { 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | white-space: nowrap; 12 | margin-bottom: 8px; 13 | margin-top: 45px; 14 | overflow-x: auto; 15 | overflow-y: auto; 16 | padding: 16px; 17 | position: absolute; 18 | top: 10px; 19 | right: 4%; 20 | bottom: 4%; 21 | left: 4%; 22 | } 23 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/Card/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { DragSource } from 'react-dnd' 4 | import { getEmptyImage } from 'react-dnd-html5-backend' 5 | 6 | const dragSource = { 7 | beginDrag(props) { 8 | return { 9 | ...props, 10 | } 11 | } 12 | } 13 | 14 | function dragCollect(connect, monitor) { 15 | return { 16 | connectDragSource: connect.dragSource(), 17 | connectDragPreview: connect.dragPreview(), 18 | isDragging: monitor.isDragging() 19 | } 20 | } 21 | 22 | class Card extends React.Component { 23 | componentDidMount() { 24 | const { connectDragPreview } = this.props 25 | if (connectDragPreview) { 26 | // Use empty image as a drag preview so browsers don't draw it 27 | // and we can draw whatever we want on the custom drag layer instead. 28 | connectDragPreview(getEmptyImage(), { 29 | // IE fallback: specify that we'd rather screenshot the node 30 | // when it already knows it's being dragged so we can hide it with CSS. 31 | captureDraggingState: true, 32 | }) 33 | } 34 | } 35 | 36 | render() { 37 | const { 38 | name, 39 | empty, 40 | isDragging, // Injected by React DnD 41 | connectDragPreview, // Injected by React DnD 42 | connectDragSource, // Injected by React DnD 43 | } = this.props 44 | 45 | 46 | return connectDragSource( 47 |
54 | { name } 55 |
56 | ) 57 | } 58 | } 59 | 60 | Card.propTypes = { 61 | name: PropTypes.string, 62 | empty: PropTypes.bool, 63 | 64 | // Injected by React DnD 65 | isDragging: PropTypes.bool.isRequired, 66 | connectDragSource: PropTypes.func.isRequired, 67 | } 68 | 69 | Card.defaultProps = { 70 | } 71 | 72 | export default DragSource('CONNECT_CARD', dragSource, dragCollect)(Card) 73 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/Card/index.js: -------------------------------------------------------------------------------- 1 | import Card from './Card' 2 | import "./style.scss" 3 | 4 | export default Card 5 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/Card/style.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: #fff; 3 | border-radius: 3px; 4 | box-shadow: 0 1px 0 rgba(9,45,66,.25); 5 | cursor: pointer; 6 | display: block; 7 | width: 250px; 8 | height: 60px; 9 | position: relative; 10 | text-decoration: none; 11 | margin: 8px; 12 | 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | 17 | &.empty-card { 18 | > * { display: none; } 19 | background-color: #c9c9c9; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/CardLayer/CardLayer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | // import PropTypes from 'prop-types' 3 | import { DragLayer } from 'react-dnd' 4 | 5 | const layerStyles = { 6 | position: 'fixed', 7 | pointerEvents: 'none', 8 | zIndex: 100, 9 | left: 0, 10 | top: 0, 11 | width: '100%', 12 | height: '100%', 13 | } 14 | 15 | const snapToGrid = (x, y) => { 16 | const snappedX = Math.round(x / 32) * 32 17 | const snappedY = Math.round(y / 32) * 32 18 | return [snappedX, snappedY] 19 | } 20 | 21 | const getItemStyles = (props) => { 22 | const { initialOffset, currentOffset } = props 23 | if (!initialOffset || !currentOffset) { 24 | return { 25 | display: 'none', 26 | } 27 | } 28 | 29 | let { x, y } = currentOffset 30 | 31 | if (props.snapToGrid) { 32 | x -= initialOffset.x 33 | y -= initialOffset.y 34 | [x, y] = snapToGrid(x, y) 35 | x += initialOffset.x 36 | y += initialOffset.y 37 | } 38 | 39 | const transform = `translate(${x}px, ${y}px) rotate(5deg)` 40 | return { 41 | transform, 42 | WebkitTransform: transform, 43 | } 44 | } 45 | 46 | const LayerCollect = monitor => ({ 47 | item: monitor.getItem(), 48 | itemType: monitor.getItemType(), 49 | initialOffset: monitor.getInitialSourceClientOffset(), 50 | currentOffset: monitor.getSourceClientOffset(), 51 | isDragging: monitor.isDragging() 52 | }) 53 | 54 | const CardLayer = (props) => { 55 | const { item, itemType, isDragging } = props 56 | if (!isDragging) { 57 | return null 58 | } 59 | 60 | return ( 61 |
62 |
{ item.name }
63 |
64 | ) 65 | } 66 | 67 | 68 | CardLayer.propTypes = { 69 | } 70 | 71 | CardLayer.defaultProps = { 72 | } 73 | 74 | export default DragLayer(LayerCollect)(CardLayer) 75 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/CardLayer/index.js: -------------------------------------------------------------------------------- 1 | import CardLayer from './CardLayer' 2 | 3 | export default CardLayer 4 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/CardWall/CardWall.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { findDOMNode } from 'react-dom' 4 | import { DropTarget } from 'react-dnd' 5 | 6 | import Card from '../Card' 7 | 8 | const dropTarget = { 9 | canDrop(props, monitor) { 10 | // You can disallow drop based on props or item 11 | const item = monitor.getItem() 12 | const { status: wallStatus } = props 13 | const { status: cardStatus } = item 14 | 15 | return wallStatus !== cardStatus 16 | }, 17 | 18 | hover(props, monitor, component) { 19 | // This is fired very often and lets you perform side effects 20 | // in response to the hover. You can't handle enter and leave 21 | // here—if you need them, put monitor.isOver() into collect() so you 22 | // can just use componentWillReceiveProps() to handle enter/leave. 23 | 24 | // You can access the coordinates if you need them 25 | const clientOffset = monitor.getClientOffset() 26 | const componentRect = findDOMNode(component).getBoundingClientRect() 27 | 28 | // You can check whether we're over a nested drop target 29 | const isJustOverThisOne = monitor.isOver({ shallow: true }) 30 | 31 | // You will receive hover() even for items for which canDrop() is false 32 | const canDrop = monitor.canDrop() 33 | }, 34 | drop(props, monitor, component) { 35 | if (monitor.didDrop()) { 36 | // If you want, you can check whether some nested 37 | // target already handled drop 38 | return true 39 | } 40 | 41 | // Obtain the dragged item 42 | // You can do something with it 43 | const item = monitor.getItem() 44 | console.log('dropCard:', item) 45 | console.log('dropWall', props) 46 | const { id } = item 47 | const { updateCardStatus, status: targetStatus } = props 48 | updateCardStatus(id, targetStatus) 49 | 50 | // You can also do nothing and return a drop result, 51 | // which will be available as monitor.getDropResult() 52 | // in the drag source's endDrag() method 53 | return { moved: true } 54 | }, 55 | } 56 | 57 | const dropCollect = (connect, monitor) => ({ 58 | // Call this function inside render() 59 | // to let React DnD handle the drag events: 60 | connectDropTarget: connect.dropTarget(), 61 | // You can ask the monitor about the current drag state: 62 | isOver: monitor.isOver(), 63 | isOverCurrent: monitor.isOver({ shallow: true }), 64 | canDrop: monitor.canDrop(), 65 | itemType: monitor.getItemType(), 66 | }) 67 | 68 | class CardWall extends React.Component { 69 | componentWillReceiveProps(nextProps) { 70 | const { isOver, isOverCurrent } = this.props 71 | if (!isOver && nextProps.isOver) { 72 | // You can use this as enter handler 73 | } 74 | 75 | if (isOver && !nextProps.isOver) { 76 | // You can use this as leave handler 77 | } 78 | 79 | if (isOverCurrent && !nextProps.isOverCurrent) { 80 | // You can be more specific and track enter/leave 81 | // shallowly, not including nested targets 82 | } 83 | } 84 | 85 | render() { 86 | const { 87 | children, 88 | status, 89 | isOver, // Injected by React DnD 90 | canDrop, // Injected by React DnD 91 | connectDropTarget, // Injected by React DnD 92 | } = this.props 93 | 94 | return connectDropTarget( 95 |
96 |
97 |

{ status }

98 |
99 | { children } 100 | { isOver && canDrop && } 101 |
102 |
103 |
104 | ) 105 | } 106 | } 107 | 108 | CardWall.propTypes = { 109 | children: PropTypes.node, 110 | status: PropTypes.string.isRequired, 111 | 112 | // Injected by React DnD 113 | connectDropTarget: PropTypes.func, 114 | isOver: PropTypes.bool, 115 | isOverCurrent: PropTypes.bool, 116 | canDrop: PropTypes.bool, 117 | itemType: PropTypes.string, 118 | } 119 | 120 | CardWall.defaultProps = { 121 | } 122 | 123 | export default DropTarget('CONNECT_CARD', dropTarget, dropCollect)(CardWall) 124 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/CardWall/index.js: -------------------------------------------------------------------------------- 1 | import CardWall from './CardWall' 2 | import './style.scss' 3 | 4 | export default CardWall 5 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/CardWall/style.scss: -------------------------------------------------------------------------------- 1 | .card-wall { 2 | width: 272px; 3 | margin: 0 4px; 4 | height: 100%; 5 | box-sizing: border-box; 6 | display: inline-block; 7 | vertical-align: top; 8 | white-space: nowrap; 9 | 10 | .card-wall-wrapper { 11 | width: 100%; 12 | min-height: 40px; 13 | background-color: #DFE3E6; 14 | border-radius: 3px; 15 | 16 | > p { 17 | font-size: 20px; 18 | line-height: 24px; 19 | margin: 8px; 20 | color: #17394d; 21 | border-bottom: 1px solid #000 22 | } 23 | } 24 | 25 | .card-wall-content { 26 | width: 272px; 27 | box-sizing: border-box; 28 | display: flex; 29 | flex-direction: column; 30 | max-height: 100%; 31 | position: relative; 32 | white-space: normal; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/ManageGroup.js: -------------------------------------------------------------------------------- 1 | import Popover from '../../Popover' 2 | import React, { Fragment } from 'react'; 3 | import { DialogContent, Button, withStyles, Typography, Grid} from '@material-ui/core' 4 | import {Stage, Layer} from 'react-konva' 5 | import {connection as conn, genid} from '../../../interface/connection' 6 | import Rectangle from '../Whiteboard_Components/Rectangle'; 7 | import Card from './Card' 8 | import App from './App'; 9 | 10 | const styles = theme => ({ 11 | }) 12 | 13 | const generalAttrs = {rotation: 0, scaleX: 1, scaleY: 1} 14 | 15 | function defaultRect() { 16 | return {type: "group", x: 800/2 - 50, y: 600/2 - 50, width: 100, height: 30, fill: '#ffff0080', name: genid(), ...generalAttrs} 17 | } 18 | 19 | class ManageGroup extends React.Component { 20 | stageRef = null 21 | state = { 22 | groups: [] 23 | } 24 | 25 | addGroup = () => { 26 | const newGroup = defaultRect() 27 | this.setState({objects: [...this.state.groups, newGroup]}) 28 | console.log(this.props.session_user) 29 | } 30 | 31 | render() { 32 | const {classes, ...others} = this.props 33 | return ( 34 | 35 | 38 | 39 | 40 | {/* 41 | 42 | 43 | 44 |
45 | 46 |
47 |
48 |
*/} 49 |
50 |
51 |
52 | ) 53 | } 54 | } 55 | 56 | export default withStyles(styles, {withTheme: true})(ManageGroup); 57 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | html, body, #root { 8 | height: 100%; 9 | } 10 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/GroupingMenu/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/JoinedLayoutStudent.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { AppBar, Toolbar } from '@material-ui/core' 3 | import UserCard from '../Content_Components/UserCard' 4 | import ClassMenu from '../Classroom_Components/ClassMenu' 5 | import Drawer from '../Classroom_Components/Drawer' 6 | import { withStyles } from '@material-ui/core/styles' 7 | import ParticipantList from './ParticipantList' 8 | import Whiteboard from './Whiteboard'; 9 | import GroupWhiteboard from './GroupWhiteboard'; 10 | import _ from 'lodash' 11 | import {ClassStatusChannel} from '../../interface/connection' 12 | import {store} from '../../index' 13 | import GroupStatus from './GroupStatus'; 14 | import WebcamPermissionStatus from './WebcamPermissionStatus'; 15 | import FindComponent from './FindComponent'; 16 | 17 | const styles = theme => ({ 18 | container: { 19 | display: 'relative', 20 | height: '900', 21 | weight: '900', 22 | } 23 | }) 24 | 25 | class JoinedLayoutStudent extends Component { 26 | ref={} 27 | state = { 28 | webcam: { 29 | selfWebcam: { id: "selfWebcam", zIndex: 2, position: {x: 1312, y: 5}, size: {width: 460 / 1.5, height: (345+41) / 1.5 + 14.5} }, 30 | teacherWebcam: { id: "teacherWebcam", zIndex: 2, position: {x: 10, y: 5}, size: {width: 460, height: 345+41} }, 31 | }, 32 | whiteboard: { 33 | selfWhiteboard: { id: "selfWhiteboard", user: this.props.self, zIndex: 3, position: {x: 820, y: 150}, size: {width: 800, height: 718} }, 34 | teacherWhiteboard: { id: "teacherWhiteboard", user: this.props.joined.owner, zIndex: 3, position: {x: 10, y: 150}, size: {width: 800, height: 718} }, 35 | }, 36 | drawer: { 37 | selfDrawer: { id: "Personal Drawer", zIndex: 0, position: {x: 660, y: 5}, size: {width: 450, height: 550} }, 38 | // classDrawer: { id: "Class Resources", zIndex: 0, position: {x: 10, y: 5} },// to distribute/receive files class esources 39 | }, 40 | other: { 41 | StudentList: { id: "StudentList", zIndex: 1, position: {x: 480, y: 5}, size: {width: 0, height: 0} }, 42 | } 43 | } 44 | componentDidMount() { 45 | ClassStatusChannel.onGroupStatusChange(this.handleGroupStatusChange) 46 | ClassStatusChannel.onWebcamPermissionChanged(this.handleWebcamPermissionChanged) 47 | } 48 | componentWillUnmount() { 49 | ClassStatusChannel.removeListener(this.handleGroupStatusChange, this.handleWebcamPermissionChanged) 50 | } 51 | componentDidUpdate(prevProps) { 52 | const {session_user} = this.props 53 | if (!session_user) return 54 | if (prevProps.session_user === session_user) return 55 | const diff = _.xor(prevProps.session_user, this.props.session_user) 56 | if (diff === this.props.self) return 57 | 58 | for (let each of diff) { 59 | if (each === this.props.self) continue 60 | if (prevProps.session_user.length > this.props.session_user.length) { 61 | this.props.handleNotification(each + " left") 62 | } else { 63 | this.props.handleNotification(each + " joined") 64 | } 65 | } 66 | } 67 | handleGroupStatusChange = group => { 68 | // {members, group} 69 | if (this.props.group.group !== group.group) { 70 | // const prevGroupIndex = this.state.whiteboard.findIndex(w => (w.type === 'group')) 71 | // this.state.whiteboard.splice(prevGroupIndex, 1) // delete old group whiteboard 72 | delete this.state.whiteboard[this.props.group.group+"Whiteboard"] 73 | if (group.group !== null) { 74 | this.setState({whiteboard: { 75 | ...this.state.whiteboard, 76 | [group.group+"Whiteboard"]: { type: 'group', id: group.group+"Whiteboard", user: group.group, zIndex: 2, position: {x: 850, y: 150}, size: {width: 800, height: 718} } 77 | }}, () => this.bringTop(group.group+"Whiteboard")) 78 | } 79 | this.props.handleClassNotification(`Assigned to ${group.group}`) 80 | } 81 | store.dispatch({type: "updateGroup", group}) 82 | } 83 | handleWebcamPermissionChanged = webcamPermission => { 84 | store.dispatch({type: "updateWebcamPermission", webcamPermission}) 85 | } 86 | bringTop = (target) => { // target: selfWebcam/etc... 87 | let maxZ = -1 88 | Object.values(this.state).forEach(outer => { 89 | Object.values(outer).forEach(inner => { 90 | if (inner.zIndex > maxZ) maxZ = inner.zIndex 91 | }) 92 | }) 93 | var outer = findKeyInNestedObject(target, this.state) 94 | this.setState({ 95 | [outer]: { 96 | ...this.state[outer], 97 | [target]: {...this.state[outer][target], zIndex: maxZ + 1} 98 | } 99 | }) 100 | } 101 | testfunc = () => { 102 | this.ref["StudentList"].updatePosition({x: 825, y: 230}) 103 | this.bringTop("StudentList") 104 | } 105 | render() { 106 | const { classes, ...other } = this.props 107 | return ( 108 | 109 | 110 | 111 | 112 | 113 | {/* */} 114 | 115 | 116 | 117 | 118 |
119 | this.bringTop('StudentList')} 122 | size={this.state.other["StudentList"].size} 123 | position={this.state.other["StudentList"].position} 124 | zIndex={this.state.other["StudentList"].zIndex} 125 | inputRef={(id, el) => this.ref[id] = el} 126 | enableResizing={false} 127 | minWidth={0} 128 | {...other}/> 129 | {Object.values(this.state.webcam).map((webcam) => ( 130 | (webcam.id !== 'teacherWebcam' || 131 | (webcam.id === 'teacherWebcam' && 132 | this.props.session_user.includes(this.props.joined.owner))) && 133 | this.bringTop(webcam.id)} 137 | size={webcam.size} 138 | position={webcam.position} 139 | zIndex={webcam.zIndex} 140 | inputRef={(id, el) => this.ref[id] = el} // delete id field (modifly RndContainer as well) 141 | lockAspectRatio={4/3} 142 | lockAspectRatioExtraHeight={41} // Change with card header and teacher's layout 143 | {...other} 144 | user={(webcam.id === "teacherWebcam") ? 145 | this.props.joined.owner : this.props.self} 146 | /> 147 | ))} 148 | {Object.values(this.state.whiteboard).map((whiteboard) => { 149 | if (whiteboard.type !== 'group' && 150 | (whiteboard.id !== 'teacherWhiteboard' || 151 | (whiteboard.id === 'teacherWhiteboard' && 152 | this.props.session_user.includes(this.props.joined.owner)))) { 153 | return ( 154 | this.bringTop(whiteboard.id)} 158 | inputRef={(id, el) => this.ref[id] = el} 159 | lockAspectRatio={4/3} 160 | lockAspectRatioExtraHeight={72} 161 | enableResizing={false} 162 | {...other} 163 | /> 164 | ) 165 | } 166 | else if (whiteboard.type === 'group') 167 | return ( 168 | this.bringTop(whiteboard.id)} 171 | inputRef={(id, el) => this.ref[id] = el} 172 | lockAspectRatio={4/3} 173 | lockAspectRatioExtraHeight={72} 174 | enableResizing={false} 175 | {...whiteboard} 176 | {...other} 177 | /> 178 | ) 179 | })} 180 | this.bringTop('selfDrawer')} 183 | size={this.state.drawer["selfDrawer"].size} 184 | position={this.state.drawer["selfDrawer"].position} 185 | zIndex={this.state.drawer["selfDrawer"].zIndex} 186 | inputRef={(id, el) => this.ref[id] = el} 187 | enableResizing={false} 188 | {...other} /> 189 |
190 |
191 | ) 192 | } 193 | } 194 | 195 | function findKeyInNestedObject(key, nestedObj) { 196 | let result 197 | Object.keys(nestedObj).forEach(k => { 198 | if (Object.keys(nestedObj[k]).includes(key)) { 199 | result = k 200 | } 201 | }) 202 | return result 203 | } 204 | 205 | export default withStyles(styles)(JoinedLayoutStudent) -------------------------------------------------------------------------------- /src/Components/Classroom_Components/ParticipantList.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { withStyles } from '@material-ui/core' 3 | import UserCardSmall from '../Content_Components/UserCardSmall' 4 | import { Grid, Button } from '@material-ui/core' 5 | import RndContainer from './RndContainer'; 6 | import ManageGroup from './GroupingMenu/ManageGroup' 7 | import {SignalingChannel} from '../../interface/connection' 8 | 9 | const styles = theme => ({ 10 | grid_item: { 11 | marginBottom: 25, 12 | }, 13 | participantList : { 14 | padding: 20, 15 | backgroundColor: "rgba(15,25,30,0.15)", 16 | borderRadius: 25, 17 | width: 137 18 | } 19 | }) 20 | 21 | class ParticipantList extends Component { 22 | state = { 23 | open: false, 24 | } 25 | 26 | handleOpenGroupingMenu = async () => { 27 | this.setState({open: true}) 28 | // const response = await conn.call("get_student_names_of_a_class", c) 29 | // if (response.result) { 30 | // if (response.result.subed.length <= 1) { 31 | // this.props.handleNotification(`No one enrolled ${c.class_name} yet`) 32 | // return 33 | // } 34 | // this.setState({ 35 | // open: true, 36 | // subed: response.result.subed, 37 | // joined: response.result.joined}) 38 | // } 39 | } 40 | 41 | render() { 42 | const { classes, ...other } = this.props 43 | return ( 44 | 45 | this.setState({open: false})}/> 46 | 47 | 54 | {isTeacher(this.props) && 55 | 56 | 59 | 60 | } 61 | 62 | Students: 63 | 64 | {this.props.session_user && 65 | this.props.session_user.map(user => ( 66 | (user !== this.props.self) && 67 | (user !== this.props.joined.owner) && 68 | 69 | 70 | 71 | ))} 72 | 73 | 74 | 75 | ) 76 | } 77 | } 78 | 79 | function isTeacher(props) { 80 | return props.joined.owner === props.self 81 | } 82 | 83 | export default withStyles(styles)(ParticipantList) -------------------------------------------------------------------------------- /src/Components/Classroom_Components/RndContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Rnd} from 'react-rnd' 3 | import {withStyles} from '@material-ui/core/styles' 4 | 5 | const styles = theme => ({}) 6 | 7 | class RndContainer extends React.Component { 8 | render() { 9 | const { children, zIndex, id, 10 | position, size, enableResizing, minWidth, 11 | lockAspectRatio, lockAspectRatioExtraHeight 12 | } = this.props 13 | return ( 14 | this.props.bringTop()} 18 | onDragStart={() => this.props.bringTop()} 19 | // TODO need physically click the card header to handle blur problem 20 | // onDragStop={() => { 21 | // setTimeout(() => { 22 | // let {x, y} = document.getElementById(`draggable${id}`).getBoundingClientRect() 23 | // x += 10 24 | // y += 10 25 | // console.log("clicking", x, y) 26 | // const ev = new MouseEvent('click', { 27 | // 'view': window, 28 | // 'bubbles': true, 29 | // 'cancelable': true, 30 | // screenX: x, 31 | // screenY: y 32 | // }) 33 | // const el = document.elementFromPoint(x, y) 34 | // el.dispatchEventev 35 | // }, 2000) 36 | // }} 37 | lockAspectRatio={lockAspectRatio || false} 38 | enableResizing={(enableResizing === false)? {} : { 39 | bottom: true, bottomLeft: true, bottomRight: true, 40 | left: true, right: true, 41 | top: true, topLeft: true, topRight: true 42 | }} 43 | lockAspectRatioExtraHeight={lockAspectRatioExtraHeight || 0} 44 | bounds="window" 45 | minWidth={minWidth || 200} 46 | dragHandleClassName={ 47 | document.getElementById(`draggable${id}`)? 48 | document.getElementById(`draggable${id}`).className : null 49 | } 50 | ref={el => this.props.inputRef(id, el)} 51 | > 52 | {children} 53 | 54 | ) 55 | } 56 | } 57 | 58 | export default withStyles(styles)(RndContainer); -------------------------------------------------------------------------------- /src/Components/Classroom_Components/ShareBtn.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import PopoverWithBtn from '../PopoverWithBtn' 3 | import {Button, DialogActions, DialogContent, RadioGroup, FormControlLabel, IconButton, Radio} from "@material-ui/core"; 4 | import {connection as conn} from '../../interface/connection' 5 | import { Share } from '@material-ui/icons'; 6 | 7 | export default class ShareBtn extends React.Component { 8 | state = { 9 | open: false, 10 | value: null, 11 | joined: [] 12 | } 13 | 14 | async getStudentsInfo(c) { 15 | 16 | } 17 | 18 | handleOpenOrClose() { 19 | this.setState({open: !this.state.open}) 20 | } 21 | 22 | handleShare = async filename => { 23 | if (this.state.value) { 24 | const response = await conn.call("file_share", {share_target: this.state.value, filename}) 25 | if (response) { 26 | if (response.result === "ok") this.props.handleNotification(`Shared ${filename} to ${this.state.value}`) 27 | } 28 | } 29 | this.handleOpenOrClose() 30 | } 31 | 32 | handleEntering = () => { 33 | this.radioGroupRef.focus() 34 | } 35 | 36 | handleSelect = (event, value) => { 37 | this.setState({value}) 38 | } 39 | 40 | render() { 41 | const {filename} = this.props 42 | return ( 43 | 44 | this.handleOpenOrClose()}> 45 | 46 | 47 | 48 | 49 | { 51 | this.radioGroupRef = ref 52 | }} 53 | value={this.state.value} 54 | onChange={this.handleSelect} 55 | > 56 | {this.props.session_user && 57 | this.props.session_user.map(user => ( 58 | (user !== this.props.self) && 59 | } 63 | label={user}> 64 | 65 | ))} 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | } -------------------------------------------------------------------------------- /src/Components/Classroom_Components/UserCardMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Menu, MenuItem, IconButton} from '@material-ui/core' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import {MoreVert} from '@material-ui/icons' 5 | 6 | const styles = theme => ({ 7 | 8 | }) 9 | 10 | class ClassMenu extends React.Component { 11 | state = { 12 | anchorEl: null, 13 | } 14 | 15 | handleClick = event => { 16 | this.setState({ anchorEl: event.currentTarget }) 17 | } 18 | 19 | handleClose = () => { 20 | this.setState({ anchorEl: null }) 21 | } 22 | 23 | render() { 24 | const { anchorEl } = this.state; 25 | 26 | return ( 27 |
28 | 32 | 33 | 34 | 40 | this.props.disableWebcam()}> 41 | Disable Webcam 42 | 43 | {/*My account*/} 44 | {/*Logout*/} 45 | 46 |
47 | ) 48 | } 49 | } 50 | 51 | export default withStyles(styles)(ClassMenu); -------------------------------------------------------------------------------- /src/Components/Classroom_Components/ViewButton.js: -------------------------------------------------------------------------------- 1 | import Popover from '../Popover' 2 | import React, { Fragment } from 'react'; 3 | import {withStyles} from '@material-ui/core/styles' 4 | import { DialogContent, IconButton } from "@material-ui/core"; 5 | import {uploadURL} from '../../interface/connection' 6 | import View from '@material-ui/icons/Pageview'; 7 | 8 | const styles = theme => ({ 9 | 10 | }) 11 | 12 | class ViewButton extends React.Component { 13 | state = { 14 | open: false, 15 | data: null, 16 | } 17 | 18 | handleClose = () => { 19 | this.setState({open: false}) 20 | } 21 | 22 | handleOpen() { 23 | this.setState({open: true}) 24 | } 25 | 26 | handleView = (filename) => { 27 | const {username, password} = this.props 28 | fetch(uploadURL+`/download/${username}/${password}/${filename}`) 29 | .then(response => response.text()) 30 | // .then(response => response.blob()) 31 | // .then(blob => { 32 | // const reader = new FileReader() 33 | // reader.addEventListener("load", () => { 34 | // this.setState({data: reader.result}) 35 | // }, false) 36 | 37 | // reader.readAsDataURL(blob); 38 | // }) 39 | .then(() => this.handleOpen()) 40 | .catch(e => {this.props.handleNotification(`${e}`)}) 41 | } 42 | 43 | cannotView(filename) { 44 | if (filename.substr(-4) === ".jpg" 45 | || filename.substr(-4) === ".png" 46 | || filename.substr(-4) === ".PNG" 47 | || filename.substr(-4) === ".gif" 48 | || filename.substr(-4) === ".svg" 49 | || filename.substr(-4) === ".bmp" 50 | // || filename.substr(-4) === ".pdf" 51 | || filename.substr(-5) === ".jpeg" 52 | || filename.substr(-5) === ".apng"){ 53 | return false 54 | } 55 | return true 56 | } 57 | 58 | render() { 59 | const { username, password, filename} = this.props 60 | return ( 61 | 62 | this.handleView(filename)}> 66 | 67 | 68 | 69 | 70 | View Selected File 76 | 77 | 78 | 79 | ); 80 | } 81 | } 82 | 83 | export default withStyles(styles, {withTheme: true})(ViewButton) -------------------------------------------------------------------------------- /src/Components/Classroom_Components/WebcamPermissionStatus.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import Button from '@material-ui/core/Button'; 4 | import Tooltip from '@material-ui/core/Tooltip'; 5 | import { Typography } from '@material-ui/core'; 6 | 7 | class WebcamPermissionStatus extends React.Component { 8 | state = { 9 | open: false, 10 | }; 11 | 12 | handleTooltipClose = () => { 13 | this.setState({ open: false }); 14 | }; 15 | 16 | handleTooltipOpen = () => { 17 | this.setState({ open: true }); 18 | }; 19 | 20 | render() { 21 | return ( 22 | 28 | Webcam 29 | Video: {this.props.webcamPermission.video? 'Yes' : 'No'} 30 | Audio: {this.props.webcamPermission.audio? 'Yes' : 'No'} 31 | 32 | } 33 | > 34 | 35 | Permission 36 | 37 | 38 | ) 39 | } 40 | } 41 | 42 | export default WebcamPermissionStatus; -------------------------------------------------------------------------------- /src/Components/Classroom_Components/Whiteboard_Components/CanvasInsideGroupWhiteboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {withStyles} from '@material-ui/core/styles' 3 | import {Image} from 'react-konva' 4 | import { GroupWhiteboardChannel } from '../../../interface/connection' 5 | // import _ from 'lodash' 6 | 7 | const styles = theme => ({ 8 | }) 9 | 10 | class CanvasInsideGroupWhiteboard extends React.Component { 11 | ctx = null 12 | state = { 13 | canvas: null, 14 | bbox: null, 15 | mode: this.props.mode, 16 | isMouseDown: false, 17 | lineWidth: this.props.lineWidth, 18 | lineColor: this.props.lineColor, 19 | point_record: {}, 20 | undoStack:[], 21 | redoStack: [], 22 | lines: [] // [{line: [], type: {color, width}}] 23 | } 24 | 25 | componentDidMount() { 26 | const canvas = document.createElement('canvas') 27 | canvas.width = 800 28 | canvas.height = 600 29 | this.ctx = canvas.getContext('2d') 30 | this.setState({canvas}) 31 | // const bbox = canvas.getBoundingClientRect() 32 | const stage = this.image.parent.parent 33 | this.setState({stage}) 34 | requestAnimationFrame(this.refresh) 35 | } 36 | 37 | componentDidUpdate(prevProps){ 38 | const { linesReceived, erasedLines } = this.props 39 | this.drawReceivedLines(linesReceived, prevProps.linesReceived) 40 | this.removeErasedLines(erasedLines, prevProps.erasedLines) 41 | } 42 | 43 | // sync new lines 44 | drawReceivedLines = (linesReceived, prevLinesReceived) => { 45 | if (!linesReceived) return 46 | if (linesReceived.length === 0) return 47 | if (prevLinesReceived === linesReceived) return 48 | // for (let data of linesReceived) 49 | this.setState({ 50 | lines: [ 51 | ...this.state.lines, 52 | ...linesReceived 53 | ] 54 | }) 55 | } 56 | 57 | //sync deleted lines 58 | removeErasedLines = (erasedLines, prevErasedLines) => { 59 | if (!erasedLines) return 60 | if (erasedLines.length === 0) return 61 | if (prevErasedLines === erasedLines) return 62 | 63 | // console.log(compareArrOfObj(erasedLines, this.state.lines)) 64 | this.setState({ 65 | lines: this.state.lines.filter( 66 | line => !erasedLines.some(other => JSON.stringify(line) === JSON.stringify(other)) 67 | ) 68 | }) 69 | } 70 | 71 | handleMouseUp = () => { 72 | if (!this.state.isMouseDown) return 73 | const {lines} = this.state 74 | this.setState({isMouseDown: false}) 75 | if (this.props.mode === "draw") { 76 | 77 | // discard empty line 78 | if (lines[lines.length - 1].line.length === 0) { 79 | lines.splice(lines.length - 1, 1) 80 | return 81 | } 82 | 83 | // send to server 84 | GroupWhiteboardChannel.draw( 85 | this.props.user, 86 | { 87 | type: "canvasDraw", 88 | lines: { 89 | line: lines[lines.length - 1].line, 90 | type: { 91 | color: this.props.lineColor, 92 | width: this.props.lineWidth 93 | } 94 | } 95 | } 96 | ) 97 | } 98 | } 99 | 100 | handleMouseDown = (e) => { //e = {evt: MouseEvent, target, currentTarget, type} 101 | this.setState({isMouseDown: true}) 102 | this.commonMoveOrDownHandler(e, true) 103 | } 104 | 105 | handleMouseMove = e => { 106 | if (!this.state.isMouseDown) return 107 | this.commonMoveOrDownHandler(e, false) 108 | } 109 | 110 | commonMoveOrDownHandler = (e, mousedown) => { 111 | const {lines} = this.state 112 | let [x, y] = getLocalCoord(this.state.stage, e.evt) 113 | if (this.props.mode === "eraser") { 114 | const radius = 10 115 | const newLines = lines.filter(line => !eraserInRange(line, x, y, radius)) 116 | const erasedLines = lines.filter(line => eraserInRange(line, x, y, radius)) 117 | 118 | if ((!lines) || (lines.length === newLines.length)) return 119 | this.setState({lines: newLines}) 120 | GroupWhiteboardChannel.draw( 121 | this.props.user, 122 | { type: "canvas erase", erasedLines } 123 | ) 124 | 125 | } else if (this.props.mode === "draw") { 126 | if (mousedown) { 127 | this.setState({ 128 | lines: [ 129 | ...this.state.lines, 130 | {line: [[x, y]], type: {color: this.props.lineColor, width: this.props.lineWidth}} 131 | ] 132 | }) 133 | } else { 134 | var line = lines[lines.length - 1] 135 | line.line.push([x, y]) 136 | } 137 | } 138 | } 139 | 140 | refresh= () => { 141 | const {lines} = this.state 142 | this.ctx.clearRect(0, 0, this.state.canvas.width, this.state.canvas.height) 143 | 144 | for (let line of lines) { 145 | drawQuadraticCurve(this.ctx, line) 146 | } 147 | 148 | if (this.image) this.image.getLayer().draw() // source from where? 149 | 150 | // console.log(`mode: ${this.props.mode}, active: ${this.state.isMouseDown}, lines: ${JSON.stringify(lines)}`) 151 | requestAnimationFrame(this.refresh) 152 | } 153 | 154 | render() { 155 | return ( 156 | (this.image = node)} 161 | shadowBlur={1} 162 | onMouseDown={this.handleMouseDown} 163 | onMouseUp={this.handleMouseUp} 164 | onMouseMove={this.handleMouseMove} 165 | onMouseOut={this.handleMouseUp} 166 | /> 167 | ) 168 | } 169 | } 170 | 171 | function eraserInRange(line, ex, ey, radius) { 172 | for (let [px, py] of line.line) { 173 | if (intersectCircle(px, py, ex, ey, radius)) { 174 | return true 175 | } 176 | } 177 | return false 178 | } 179 | 180 | function intersectCircle(px, py, ex, ey, radius) { 181 | return (px - ex) * (px - ex) + (py - ey) * (py - ey) <= radius * radius 182 | } 183 | 184 | // eslint-disable-next-line 185 | function drawStraightLine(ctx, line) { 186 | if (line.line.length === 0) return 187 | let [fx, fy] = line.line[0] 188 | 189 | ctx.beginPath() 190 | ctx.lineJoin = "round" 191 | ctx.strokeStyle = line.type.color 192 | ctx.lineWidth = line.type.width 193 | ctx.moveTo(fx, fy) 194 | for (let [x, y] of line.line) { 195 | ctx.lineTo(x, y) 196 | } 197 | ctx.stroke() 198 | } 199 | 200 | function drawDot(ctx, line) { 201 | let [fx, fy] = line.line[0] 202 | ctx.beginPath() 203 | ctx.arc(fx, fy, line.type.width/2 , 0, 2 * Math.PI) 204 | ctx.fill() 205 | } 206 | 207 | function drawQuadraticCurve(ctx, line) { 208 | const ppts = line.line 209 | if (ppts.length < 3) { 210 | drawDot(ctx, line) 211 | return 212 | } 213 | 214 | ctx.beginPath() 215 | ctx.lineJoin = "round" 216 | ctx.strokeStyle = line.type.color 217 | ctx.lineWidth = line.type.width 218 | ctx.moveTo(ppts[0][0], ppts[0][1]) 219 | 220 | let i 221 | for (i = 1; i < ppts.length - 2; i ++) { 222 | var xc = (ppts[i][0] + ppts[i + 1][0]) / 2 223 | var yc = (ppts[i][1] + ppts[i + 1][1]) / 2 224 | ctx.quadraticCurveTo(ppts[i][0], ppts[i][1], xc, yc) 225 | } 226 | 227 | // curve through the last two points 228 | ctx.quadraticCurveTo(ppts[i][0], ppts[i][1], ppts[i+1][0], ppts[i+1][1]) 229 | ctx.stroke() 230 | } 231 | 232 | // eslint-disable-next-line 233 | function drawStraightLineWithHightlight(ctx, line) { 234 | if (line.length === 0) return 235 | let [fx, fy] = line[0] 236 | 237 | ctx.beginPath() 238 | ctx.moveTo(fx, fy) 239 | for (let [x, y] of line) { 240 | ctx.lineTo(x, y) 241 | } 242 | ctx.stroke() 243 | 244 | var radius = 3 245 | for (let [x, y] of line) { 246 | ctx.beginPath() 247 | ctx.arc(x, y, radius, 0, 2 * Math.PI) 248 | ctx.fill() 249 | } 250 | } 251 | 252 | function getLocalCoord(stage, {clientX, clientY}) { 253 | const bbox = stage.getPointerPosition() 254 | // return [clientX - bbox.x, clientY - bbox.y] 255 | return [bbox.x, bbox.y] 256 | } 257 | 258 | export default withStyles(styles)(CanvasInsideGroupWhiteboard); 259 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/Whiteboard_Components/CanvasInsideWhiteboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {withStyles} from '@material-ui/core/styles' 3 | import {Image} from 'react-konva' 4 | import { WhiteboardChannel } from '../../../interface/connection' 5 | // import _ from 'lodash' 6 | 7 | const styles = theme => ({ 8 | }) 9 | 10 | class CanvasInsideWhiteboard extends React.Component { 11 | ctx = null 12 | state = { 13 | canvas: null, 14 | bbox: null, 15 | mode: this.props.mode, 16 | isMouseDown: false, 17 | lineWidth: this.props.lineWidth, 18 | lineColor: this.props.lineColor, 19 | point_record: {}, 20 | undoStack:[], 21 | redoStack: [], 22 | lines: [] // [{line: [], type: {color, width}}] 23 | } 24 | 25 | componentDidMount() { 26 | const canvas = document.createElement('canvas') 27 | canvas.width = 800 28 | canvas.height = 600 29 | this.ctx = canvas.getContext('2d') 30 | this.setState({canvas}) 31 | // const bbox = canvas.getBoundingClientRect() 32 | const stage = this.image.parent.parent 33 | this.setState({stage}) 34 | requestAnimationFrame(this.refresh) 35 | } 36 | 37 | componentDidUpdate(prevProps){ 38 | const { linesReceived, erasedLines } = this.props 39 | this.drawReceivedLines(linesReceived, prevProps.linesReceived) 40 | this.removeErasedLines(erasedLines, prevProps.erasedLines) 41 | } 42 | 43 | // sync new lines 44 | drawReceivedLines = (linesReceived, prevLinesReceived) => { 45 | if (!linesReceived) return 46 | if (linesReceived.length === 0) return 47 | if (prevLinesReceived === linesReceived) return 48 | // for (let data of linesReceived) 49 | this.setState({ 50 | lines: [ 51 | ...this.state.lines, 52 | ...linesReceived 53 | ] 54 | }) 55 | } 56 | 57 | //sync deleted lines 58 | removeErasedLines = (erasedLines, prevErasedLines) => { 59 | if (!erasedLines) return 60 | if (erasedLines.length === 0) return 61 | if (prevErasedLines === erasedLines) return 62 | 63 | // console.log(compareArrOfObj(erasedLines, this.state.lines)) 64 | this.setState({ 65 | lines: this.state.lines.filter( 66 | line => !erasedLines.some(other => JSON.stringify(line) === JSON.stringify(other)) 67 | ) 68 | }) 69 | } 70 | 71 | handleMouseUp = () => { 72 | if (!this.state.isMouseDown) return 73 | const {lines} = this.state 74 | this.setState({isMouseDown: false}) 75 | if (this.props.mode === "draw") { 76 | 77 | // discard empty line 78 | if (lines[lines.length - 1].line.length === 0) { 79 | lines.splice(lines.length - 1, 1) 80 | return 81 | } 82 | 83 | // send to server 84 | WhiteboardChannel.draw( 85 | this.props.user, 86 | { 87 | type: "canvasDraw", 88 | lines: { 89 | line: lines[lines.length - 1].line, 90 | type: { 91 | color: this.props.lineColor, 92 | width: this.props.lineWidth 93 | } 94 | } 95 | } 96 | ) 97 | } 98 | } 99 | 100 | handleMouseDown = (e) => { //e = {evt: MouseEvent, target, currentTarget, type} 101 | this.setState({isMouseDown: true}) 102 | this.commonMoveOrDownHandler(e, true) 103 | } 104 | 105 | handleMouseMove = e => { 106 | if (!this.state.isMouseDown) return 107 | this.commonMoveOrDownHandler(e, false) 108 | } 109 | 110 | commonMoveOrDownHandler = (e, mousedown) => { 111 | const {lines} = this.state 112 | let [x, y] = getLocalCoord(this.state.stage, e.evt) 113 | if (this.props.mode === "eraser") { 114 | const radius = 10 115 | const newLines = lines.filter(line => !eraserInRange(line, x, y, radius)) 116 | const erasedLines = lines.filter(line => eraserInRange(line, x, y, radius)) 117 | 118 | if ((!lines) || (lines.length === newLines.length)) return 119 | this.setState({lines: newLines}) 120 | WhiteboardChannel.draw( 121 | this.props.user, 122 | { type: "canvas erase", erasedLines } 123 | ) 124 | 125 | } else if (this.props.mode === "draw") { 126 | if (mousedown) { 127 | this.setState({ 128 | lines: [ 129 | ...this.state.lines, 130 | {line: [[x, y]], type: {color: this.props.lineColor, width: this.props.lineWidth}} 131 | ] 132 | }) 133 | } else { 134 | var line = lines[lines.length - 1] 135 | line.line.push([x, y]) 136 | } 137 | } 138 | } 139 | 140 | refresh= () => { 141 | const {lines} = this.state 142 | this.ctx.clearRect(0, 0, this.state.canvas.width, this.state.canvas.height) 143 | 144 | for (let line of lines) { 145 | drawQuadraticCurve(this.ctx, line) 146 | } 147 | 148 | if (this.image) this.image.getLayer().draw() // source from where? 149 | 150 | // console.log(`mode: ${this.props.mode}, active: ${this.state.isMouseDown}, lines: ${JSON.stringify(lines)}`) 151 | requestAnimationFrame(this.refresh) 152 | } 153 | 154 | render() { 155 | return ( 156 | (this.image = node)} 161 | shadowBlur={1} 162 | onMouseDown={this.handleMouseDown} 163 | onMouseUp={this.handleMouseUp} 164 | onMouseMove={this.handleMouseMove} 165 | onMouseOut={this.handleMouseUp} 166 | /> 167 | ) 168 | } 169 | } 170 | 171 | function eraserInRange(line, ex, ey, radius) { 172 | for (let [px, py] of line.line) { 173 | if (intersectCircle(px, py, ex, ey, radius)) { 174 | return true 175 | } 176 | } 177 | return false 178 | } 179 | 180 | function intersectCircle(px, py, ex, ey, radius) { 181 | return (px - ex) * (px - ex) + (py - ey) * (py - ey) <= radius * radius 182 | } 183 | 184 | // eslint-disable-next-line 185 | function drawStraightLine(ctx, line) { 186 | if (line.line.length === 0) return 187 | let [fx, fy] = line.line[0] 188 | 189 | ctx.beginPath() 190 | ctx.lineJoin = "round" 191 | ctx.strokeStyle = line.type.color 192 | ctx.lineWidth = line.type.width 193 | ctx.moveTo(fx, fy) 194 | for (let [x, y] of line.line) { 195 | ctx.lineTo(x, y) 196 | } 197 | ctx.stroke() 198 | } 199 | 200 | function drawDot(ctx, line) { 201 | let [fx, fy] = line.line[0] 202 | ctx.beginPath() 203 | ctx.arc(fx, fy, line.type.width/2 , 0, 2 * Math.PI) 204 | ctx.fill() 205 | } 206 | 207 | function drawQuadraticCurve(ctx, line) { 208 | const ppts = line.line 209 | if (ppts.length < 3) { 210 | drawDot(ctx, line) 211 | return 212 | } 213 | 214 | ctx.beginPath() 215 | ctx.lineJoin = "round" 216 | ctx.strokeStyle = line.type.color 217 | ctx.lineWidth = line.type.width 218 | ctx.moveTo(ppts[0][0], ppts[0][1]) 219 | 220 | let i 221 | for (i = 1; i < ppts.length - 2; i ++) { 222 | var xc = (ppts[i][0] + ppts[i + 1][0]) / 2 223 | var yc = (ppts[i][1] + ppts[i + 1][1]) / 2 224 | ctx.quadraticCurveTo(ppts[i][0], ppts[i][1], xc, yc) 225 | } 226 | 227 | // curve through the last two points 228 | ctx.quadraticCurveTo(ppts[i][0], ppts[i][1], ppts[i+1][0], ppts[i+1][1]) 229 | ctx.stroke() 230 | } 231 | 232 | // eslint-disable-next-line 233 | function drawStraightLineWithHightlight(ctx, line) { 234 | if (line.length === 0) return 235 | let [fx, fy] = line[0] 236 | 237 | ctx.beginPath() 238 | ctx.moveTo(fx, fy) 239 | for (let [x, y] of line) { 240 | ctx.lineTo(x, y) 241 | } 242 | ctx.stroke() 243 | 244 | var radius = 3 245 | for (let [x, y] of line) { 246 | ctx.beginPath() 247 | ctx.arc(x, y, radius, 0, 2 * Math.PI) 248 | ctx.fill() 249 | } 250 | } 251 | 252 | function getLocalCoord(stage, {clientX, clientY}) { 253 | const bbox = stage.getPointerPosition() 254 | // return [clientX - bbox.x, clientY - bbox.y] 255 | return [bbox.x, bbox.y] 256 | } 257 | 258 | export default withStyles(styles)(CanvasInsideWhiteboard); 259 | -------------------------------------------------------------------------------- /src/Components/Classroom_Components/Whiteboard_Components/Portal.js: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/tajo/react-portal/blob/55ed77ab823b03d1d4c45b950ba26ea5d687e85c/src/LegacyPortal.js 2 | 3 | // This file is a fallback for a consumer who is not yet on React 16 4 | // as createPortal was introduced in React 16 5 | 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import PropTypes from 'prop-types'; 9 | 10 | export default class Portal extends React.Component { 11 | componentDidMount() { 12 | this.renderPortal(); 13 | } 14 | 15 | componentDidUpdate(props) { 16 | this.renderPortal(); 17 | } 18 | 19 | componentWillUnmount() { 20 | ReactDOM.unmountComponentAtNode(this.defaultNode || this.props.node); 21 | if (this.defaultNode) { 22 | document.body.removeChild(this.defaultNode); 23 | } 24 | this.defaultNode = null; 25 | this.portal = null; 26 | } 27 | 28 | renderPortal(props) { 29 | if (!this.props.node && !this.defaultNode) { 30 | this.defaultNode = document.createElement('div'); 31 | document.body.appendChild(this.defaultNode); 32 | } 33 | 34 | let children = this.props.children; 35 | // https://gist.github.com/jimfb/d99e0678e9da715ccf6454961ef04d1b 36 | if (typeof this.props.children.type === 'function') { 37 | children = React.cloneElement(this.props.children); 38 | } 39 | 40 | this.portal = ReactDOM.unstable_renderSubtreeIntoContainer( 41 | this, 42 | children, 43 | this.props.node || this.defaultNode 44 | ); 45 | } 46 | 47 | render() { 48 | return null; 49 | } 50 | } 51 | 52 | Portal.propTypes = { 53 | children: PropTypes.node.isRequired, 54 | node: PropTypes.any 55 | }; -------------------------------------------------------------------------------- /src/Components/Classroom_Components/Whiteboard_Components/Rectangle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Rect } from 'react-konva' 3 | 4 | export default function Rectangle(props) { 5 | return ( 6 | props.onDragEnd(e)} 17 | onTransformEnd={e => props.onTransformEnd(e)} 18 | draggable 19 | /> 20 | ) 21 | } -------------------------------------------------------------------------------- /src/Components/Classroom_Components/Whiteboard_Components/TransformerComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Transformer } from 'react-konva' 3 | 4 | export default class TransformerComponent extends React.Component { 5 | componentDidMount() { 6 | this.checkNode() 7 | } 8 | 9 | componentDidUpdate() { 10 | this.checkNode() 11 | } 12 | 13 | checkNode() { 14 | //here we need to manually attach or detach Transformer node 15 | const stage = this.transformer.getStage() 16 | const {selectedShapeName} = this.props 17 | 18 | const selectedNode = stage.findOne('.' + selectedShapeName) 19 | 20 | //do nothing if already attached 21 | if (selectedNode === this.transformer.node()) return 22 | 23 | if (selectedNode) { 24 | //attach to another node 25 | this.transformer.attachTo(selectedNode) 26 | } else { 27 | //remove transformer 28 | this.transformer.detach() 29 | } 30 | this.transformer.getLayer().batchDraw() 31 | } 32 | 33 | render() { 34 | return ( 35 | {this.transformer = node}} 37 | /> 38 | ) 39 | } 40 | } -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/.DS_Store -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/background.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/clear.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/download.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/eraser.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/graph-grid-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/graph-grid-icon.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/load-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/load-image.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/pen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/pen.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/redo.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/undo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/undo.png -------------------------------------------------------------------------------- /src/Components/Classroom_Components/thumbnail/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/Components/Classroom_Components/thumbnail/upload.png -------------------------------------------------------------------------------- /src/Components/Content_Components/UserCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import PropTypes from 'prop-types' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import {Card, CardHeader, Divider} from '@material-ui/core' 5 | import LocalStream from '../Classroom_Components/LocalStream' 6 | import RemoteStream from '../Classroom_Components/RemoteStream' 7 | // import UserCardMenu from '../Classroom_Components/UserCardMenu' 8 | import RndContainer from '../Classroom_Components/RndContainer' 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import { PinDrop } from '@material-ui/icons'; 11 | 12 | 13 | const styles = theme => ({ 14 | card: { 15 | width: '100%', 16 | height: '100%' 17 | }, 18 | avatar: { 19 | backgroundColor: "#769da8" 20 | }, 21 | normalTitle: {color: "#484747", fontSize: 22}, 22 | teacherTitle: {color: "#ff4500e6", fontSize: 22} 23 | }) 24 | 25 | class UserCard extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | this.state = { 29 | drawRight: 'Read Only', 30 | camOpen: true 31 | } 32 | } 33 | 34 | disableWebcam() { 35 | this.setState({camOpen: false}) 36 | } 37 | 38 | render() { 39 | const {user} = this.props; 40 | const {classes, ...other} = this.props; 41 | return ( 42 | 43 | 44 | {user}} 47 | style={{ 48 | height: 18, 49 | backgroundColor: "#e9e7e74d", 50 | paddingTop: 7, 51 | overflow: "hidden", 52 | whiteSpace: "nowrap", 53 | textOverflow: "ellipsis" 54 | }} 55 | classes={{title: isTeacher(this.props)? classes.teacherTitle : classes.normalTitle}} 56 | // avatar={ 57 | // 58 | // {user.substring(0, 3)} 59 | // 60 | // } 61 | // action={ 62 | // 63 | // } 64 | action={ 65 | this.props.pinTop}> 66 | 67 | 68 | } 69 | /> 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | } 77 | 78 | function Webcam(props) { 79 | const {self, user} = props 80 | if (user === self) { 81 | return ( 82 | 85 | ) 86 | } else { 87 | return ( 88 | 91 | ) 92 | } 93 | } 94 | 95 | function isTeacher(props) { 96 | return props.joined.owner === props.user 97 | } 98 | 99 | export default withStyles(styles)(UserCard); -------------------------------------------------------------------------------- /src/Components/Content_Components/UserCardSmall.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import { Card } from '@material-ui/core' 5 | import LocalStream from '../Classroom_Components/LocalStream' 6 | import RemoteStream from '../Classroom_Components/RemoteStream' 7 | 8 | const styles = theme => ({ 9 | card: { 10 | // width: 92, 11 | // height: 80, 12 | width: 460 / 5, 13 | height: 300 / 5 + 9 14 | }, 15 | container: { 16 | display: 'flex', 17 | flexDirection: 'column', 18 | justifyContent: 'center', 19 | justifyItems: 'center', 20 | } 21 | }) 22 | 23 | class UserCardSmall extends React.Component { 24 | constructor(props) { 25 | super(props); 26 | this.state = { 27 | animate: true, 28 | self: null, 29 | drawRight: 'Read Only', 30 | camOpen: true 31 | }; 32 | } 33 | 34 | disableWebcam() { 35 | this.setState({ camOpen: false }) 36 | } 37 | 38 | render() { 39 | const { user } = this.props; 40 | const { classes, ...other } = this.props; 41 | return ( 42 |
43 | {user} 44 | 45 | 46 | 47 |
48 | ) 49 | } 50 | } 51 | 52 | function Webcam(props) { 53 | const { self, user } = props 54 | if (user === self) { 55 | return ( 56 | 61 | ) 62 | } else { 63 | return ( 64 | 71 | ) 72 | } 73 | } 74 | 75 | export default withStyles(styles)(UserCardSmall); -------------------------------------------------------------------------------- /src/Components/Content_Components/tools/Pencil.js: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | 3 | export const TOOL_PENCIL = 'pencil'; 4 | 5 | export default (props) => { 6 | let stroke = null; 7 | 8 | const onMouseDown = (x, y, color, size) => { 9 | stroke = { 10 | id: v4(), 11 | tool: TOOL_PENCIL, 12 | color, 13 | size, 14 | points: [{ x, y }] 15 | }; 16 | return stroke; 17 | }; 18 | 19 | const drawLine = (item, start, { x, y }) => { 20 | props.lineJoin = 'round'; 21 | props.lineCap = 'round'; 22 | props.beginPath(); 23 | props.lineWidth = item.size; 24 | props.strokeStyle = item.color; 25 | props.globalCompositeOperation = 'source-over'; //or 'destination-over' 26 | props.moveTo(start.x, start.y); 27 | props.lineTo(x, y); 28 | props.closePath(); 29 | props.stroke(); 30 | }; 31 | 32 | const onMouseMove = (x, y) => { 33 | if (!stroke) return []; 34 | const newPoint = { x, y }; 35 | const start = stroke.points.slice(-1)[0]; 36 | drawLine(stroke, start, newPoint); 37 | stroke.points.push(newPoint); 38 | // return [stroke]; 39 | }; 40 | 41 | const onMouseUp = (x, y) => { 42 | if (!stroke) return; 43 | onMouseMove(x, y); 44 | const item = stroke; 45 | stroke = null; 46 | return item; 47 | }; 48 | 49 | const draw = (item, animated) => { 50 | let time = 0; 51 | let i = 0; 52 | const j = item.points.length; 53 | for (i, j; i < j; i++) { 54 | if (!item.points[i - 1]) continue; 55 | if (animated) { 56 | setTimeout(drawLine.bind(null, item, item.points[i - 1], item.points[i]), time); 57 | time += 10; 58 | } else { 59 | drawLine(item, item.points[i - 1], item.points[i]); 60 | } 61 | } 62 | }; 63 | 64 | return { 65 | onMouseDown, 66 | onMouseMove, 67 | onMouseUp, 68 | draw, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/Components/Content_Components/tools/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | default as Pencil, 3 | TOOL_PENCIL 4 | } from './Pencil'; -------------------------------------------------------------------------------- /src/Components/DrawerLeft.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {Divider, Drawer, withStyles} from '@material-ui/core' 4 | import {ListItems} from './Drawer_Components/DrawerLeftList'; 5 | import classNames from 'classnames' 6 | import {drawerWidth} from "./index"; 7 | import { connection as conn } from '../interface/connection' 8 | import {store} from '../index' 9 | import {withCookies} from 'react-cookie' 10 | import {compose} from 'redux' 11 | import UserSettings from './Drawer_Components/UserSettings'; 12 | 13 | const styles = theme => ({ 14 | drawerPaperLeft: { 15 | position: 'fixed', 16 | whiteSpace: 'nowrap', 17 | width: drawerWidth, 18 | background: "rgba(30,30,30,0.15)" 19 | }, 20 | toolbar: { 21 | display: 'flex', 22 | alignItems: 'left', 23 | justifyContent: 'flex-start', 24 | padding: '0 8px', 25 | ...theme.mixins.toolbar, 26 | }, 27 | avatar: { 28 | backgroundColor: "rgba(50,70,60,0.5)" 29 | }, 30 | }) 31 | 32 | class DrawerLeft extends React.Component { 33 | state = { 34 | anchorEl: null, 35 | selectedIndex: 1, 36 | } 37 | 38 | handleClickListItem = event => { 39 | this.setState({ anchorEl: event.currentTarget }) 40 | } 41 | 42 | handleMenuItemClick = (event, index) => { 43 | this.setState({ selectedIndex: index, anchorEl: null }) 44 | } 45 | 46 | handleClose = () => { 47 | this.setState({ anchorEl: null }) 48 | } 49 | 50 | handleLogout = async () => { 51 | const result = await conn.call("logout") 52 | if (result.type !== "ok") throw new Error("invalid_logout") 53 | this.props.cookies.remove("name") 54 | this.props.cookies.remove("password") 55 | store.dispatch({type: "logout"}) 56 | } 57 | 58 | render() { 59 | const {classes, open, ...other} = this.props //, ...others 60 | 61 | return ( 62 | 69 | 70 | 71 | 74 | 75 | ) 76 | } 77 | } 78 | 79 | DrawerLeft.propTypes = { 80 | classes: PropTypes.object.isRequired, 81 | } 82 | 83 | export default compose( 84 | withCookies, 85 | withStyles(styles, {withTheme: true}), 86 | )(DrawerLeft) -------------------------------------------------------------------------------- /src/Components/DrawerRight.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {Tabpage} from "./Drawer_Components"; 4 | import {Drawer} from '@material-ui/core' 5 | import {withStyles} from '@material-ui/core/styles' 6 | import {drawerWidth} from "./index"; 7 | 8 | const styles = theme => ({ 9 | drawerPaperRight: { 10 | position: 'fixed', 11 | right: 0, 12 | width: drawerWidth, 13 | }, 14 | toolbar: { 15 | display: 'flex', 16 | alignItems: 'center', 17 | justifyContent: 'flex-end', 18 | padding: '0 8px', 19 | ...theme.mixins.toolbar, 20 | }, 21 | }); 22 | 23 | class DrawerRight extends React.Component { 24 | render() { 25 | const {classes, ...others} = this.props; 26 | return ( 27 | 34 |
35 | 36 | 37 | ) 38 | } 39 | } 40 | 41 | DrawerRight.propTypes = { 42 | classes: PropTypes.object.isRequired, 43 | }; 44 | 45 | export default withStyles(styles)(DrawerRight); -------------------------------------------------------------------------------- /src/Components/Drawer_Components/Classroom.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import {ListItem, ListItemIcon, ListItemText} from "@material-ui/core"; 3 | import InboxIcon from '@material-ui/icons/MoveToInbox'; 4 | import Popover from '../Popover' 5 | 6 | export default class extends React.Component { 7 | state = {open: false}; 8 | 9 | handle() { 10 | this.setState({open: !this.state.open}); 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | this.handle()}> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Testing Whiteboard 25 | 26 | 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /src/Components/Drawer_Components/DrawerLeftList.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import {Divider, List} from '@material-ui/core' 3 | import SuperUserIcon from '@material-ui/icons/SupervisorAccount'; 4 | import ListIcon from '@material-ui/icons/Subject'; 5 | import NoteIcon from '@material-ui/icons/BookmarkBorder'; 6 | import MailIcon from '@material-ui/icons/MailOutline'; 7 | import InputIcon from '@material-ui/icons/Input'; 8 | import ReportIcon from '@material-ui/icons/Report'; 9 | import HomeIcon from '@material-ui/icons/Home'; 10 | import Redirect from 'react-router-dom/Redirect' 11 | // import {History, Locker, Sketch, Classroom} from './index' 12 | 13 | import {ListItem, ListItemIcon, ListItemText} from "@material-ui/core"; 14 | 15 | export class ListItems extends React.Component { 16 | state = {redirect: false} 17 | render() { 18 | if (this.state.redirect) return ( 19 | ) 20 | return ( 21 | 22 |
23 | this.props.changeScene(0)} disabled={this.props.location === 0}> 24 | 25 | 26 | 27 | 28 | 29 | this.props.changeScene(1)} disabled={this.props.location === 1}> 30 | 31 | 32 | 33 | 34 | 35 | {/* this.props.changeScene(2)} disabled={this.props.location === 2}> 36 | 37 | 38 | 39 | 40 | */} 41 | this.props.changeScene(2.2)} disabled={this.props.location === 2.2}> 42 | 43 | 44 | 45 | 46 | 47 | this.props.changeScene(2.1)} disabled={this.props.location === 2.1}> 48 | 49 | 50 | 51 | 52 | 53 | {/* this.props.changeScene(3)} disabled={this.props.location === 3}> 54 | 55 | 56 | 57 | 58 | 59 | this.props.changeScene(4)} disabled={this.props.location === 4}> 60 | 61 | 62 | 63 | 64 | */} 65 |
66 | 67 | 68 | this.setState({redirect: true})}> 69 | 70 | 71 | 72 | 73 | 74 | {/* 75 | 76 | 77 | 78 | 79 | */} 80 | {/* 81 | 82 | 83 | 84 | 85 | */} 86 | 87 |
88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Components/Drawer_Components/DrawerRightList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {ListItem, ListItemIcon, ListItemText} from '@material-ui/core' 3 | import {Inbox} from '@material-ui/icons' 4 | 5 | let InboxIcon = Inbox; 6 | 7 | export const mainList = ( 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | ); -------------------------------------------------------------------------------- /src/Components/Drawer_Components/History.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import {ListItem, ListItemIcon, ListItemText} from "@material-ui/core"; 3 | import DraftsIcon from '@material-ui/icons/Drafts'; 4 | import Popover from '../Popover' 5 | 6 | export default class extends React.Component { 7 | state = {open: false}; 8 | 9 | handle() { 10 | this.setState({open: !this.state.open}); 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | this.handle()}> 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {/*Testing History*/} 25 | 26 | 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Components/Drawer_Components/Locker.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import {ListItem, ListItemIcon, ListItemText} from "@material-ui/core"; 3 | import SendIcon from '@material-ui/icons/Send'; 4 | import Popover from '../Popover' 5 | 6 | export default class extends React.Component { 7 | state = {open: false}; 8 | 9 | handle() { 10 | this.setState({open: !this.state.open}); 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | this.handle()}> 17 | 18 | 19 | 20 | 21 | 22 | 23 | Testing Locker 24 | 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Components/Drawer_Components/ScrollableTabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {withStyles} from '@material-ui/core/styles'; 4 | import {AppBar, Avatar, Chip, Grid, Tab, Tabs, TextField, Typography} from '@material-ui/core'; 5 | import 'whatwg-fetch' 6 | import {drawerWidth} from "../index"; 7 | 8 | function TabContainer(props) { 9 | return ( 10 | 11 | {props.children} 12 | 13 | ); 14 | } 15 | 16 | TabContainer.propTypes = { 17 | children: PropTypes.node.isRequired, 18 | }; 19 | 20 | const styles = theme => ({ 21 | root: { 22 | flexGrow: 1, 23 | width: '100%', 24 | backgroundColor: theme.palette.background.paper, 25 | }, 26 | appbar: { 27 | position: "fixed", 28 | top: 65, 29 | right: 0, 30 | width: drawerWidth, 31 | }, 32 | tab: { 33 | // fontSize: 5, 34 | }, 35 | grid: { 36 | paddingTop: 60, 37 | padding: 18, 38 | height: "100%" 39 | }, 40 | chip: { 41 | marginTop: 18, 42 | }, 43 | textField: { 44 | padding: 10, 45 | paddingTop: 0, 46 | position: "fixed", 47 | bottom: '0', 48 | background: "white" 49 | }, 50 | span: { 51 | fontSize: 10, 52 | display: "block" 53 | } 54 | }); 55 | 56 | class ScrollableTabsButtonAuto extends React.Component { 57 | constructor(props){ 58 | super(props); 59 | this.state = { 60 | isLoading: true, 61 | resources: [], 62 | value: 0, 63 | comment: '' 64 | } 65 | } 66 | 67 | handleChange = (event, value) => { 68 | this.setState({ value }); 69 | }; 70 | 71 | handleComment(e) { 72 | this.setState({ comment: e.target.value }); 73 | }; 74 | 75 | // async keyPressPassword(e) { 76 | keyPress(e) { 77 | // console.log(e.cancelable); 78 | if(e.keyCode === 13){ 79 | e.preventDefault(); 80 | // await this.uploadChatFromApi(); 81 | // await this.getChatFromApi(); 82 | // console.log("fetched") 83 | // console.log("uploaded") 84 | this.setState({comment: ''}); 85 | if (this.props.self === null){ return} 86 | let message = { 87 | name: 'user chat', 88 | data: { 89 | // id: '1', 90 | user: this.props.self, 91 | content: this.state.comment, 92 | time: this.getTime() 93 | } 94 | }; 95 | this.props.ws.send(JSON.stringify(message)); 96 | } 97 | } 98 | 99 | getTime() { 100 | const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 101 | let today = new Date(); 102 | let dd = today.getDate(); 103 | let MM = monthNames[today.getMonth()]; 104 | let hh = today.getHours(); 105 | let mm = today.getMinutes(); 106 | // if (dd < 10){dd="0"+dd} 107 | if (hh < 10){hh="0"+hh} 108 | if (mm < 10){mm="0"+mm} 109 | return hh + ":" + mm + " " + dd + "/" + MM; 110 | } 111 | 112 | render() { 113 | const { classes } = this.props; 114 | const { value } = this.state; 115 | 116 | return ( 117 |
118 | 119 | 127 | Chat} /> 128 | 129 | 130 | 131 | 132 | 139 | { 140 | (this.props.chat != null) ? 141 | value === 0 && 142 | this.props.chat.map((res, index) => ( 143 |
144 | {res.user.substring(0,3)}} 146 | // label={res.content} 147 | label={} 148 | // className={classes.chip} 149 | /> 150 |
151 | )) : 152 | value === 0 && Comment Something Here 153 | } 154 | {value === 1 && Item Two} 155 | {value === 2 && Item Three} 156 |
157 |
158 | 165 | 166 |
167 | ); 168 | } 169 | } 170 | 171 | function ChipContent(props) { 172 | const {res} = props; 173 | return ( 174 |
175 |
{res.content}
176 |
177 | {res.time} 178 |
179 |
180 | ) 181 | } 182 | 183 | 184 | ScrollableTabsButtonAuto.propTypes = { 185 | classes: PropTypes.object.isRequired, 186 | }; 187 | 188 | export default withStyles(styles)(ScrollableTabsButtonAuto); 189 | 190 | // async getChatFromApi() { 191 | // const request = fetch('http://localhost:8080/api/chat') 192 | // const repsonse = await request 193 | // const json = await repsonse.json(); 194 | // 195 | // this.setState({ 196 | // resources: json, 197 | // isLoading: false 198 | // }); 199 | // } 200 | 201 | // uploadChatFromApi() { 202 | // // const url = 'http://herbert.dynu.net:8080/api/chat'; 203 | // const url = 'http://localhost:8080/api/chat'; 204 | // return fetch(url, { 205 | // method: 'POST', 206 | // headers: { 207 | // 'Content-Type': 'application/json', 208 | // }, 209 | // body: JSON.stringify({ 210 | // // id: '1', 211 | // user: 'Dev', 212 | // content: this.state.comment, 213 | // time: this.getTime() 214 | // }) 215 | // }) 216 | // .then(function (response) { 217 | // console.log("finished upload") 218 | // console.log(response.url); 219 | // }) 220 | // .catch(function (error) { 221 | // console.log(error.message) 222 | // }); 223 | // 224 | // } 225 | // 226 | // componentWillMount() { 227 | // this.getChatFromApi(); 228 | // } -------------------------------------------------------------------------------- /src/Components/Drawer_Components/Sketch.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import {ListItem, ListItemIcon, ListItemText} from "@material-ui/core"; 3 | import StarIcon from '@material-ui/icons/Star'; 4 | import Popover from '../Popover' 5 | 6 | export default class extends React.Component { 7 | state = {open: false}; 8 | 9 | handle() { 10 | this.setState({open: !this.state.open}); 11 | } 12 | 13 | render() { 14 | return ( 15 | 16 | this.handle()} selected={this.props.location === 2}> 17 | 18 | 19 | 20 | 21 | 22 | 23 | ClassList 24 | 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Components/Drawer_Components/UserSettings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connection as conn } from '../../interface/connection' 3 | import {store} from '../../index' 4 | import {withCookies} from 'react-cookie' 5 | import {compose} from 'redux' 6 | import {Avatar, withStyles, List, ListItem, ListItemText, Menu, MenuItem} from '@material-ui/core' 7 | import ChangeUserInfo from '../Locations/PopupFunction/ChangeUserInfo' 8 | 9 | const styles = theme => ({ 10 | avatar: { 11 | backgroundColor: "rgba(50,70,60,0.5)" 12 | }, 13 | }) 14 | 15 | class UserSettings extends Component { 16 | state = { 17 | anchorEl: null, 18 | } 19 | 20 | handleClickListItem = event => { 21 | this.setState({ anchorEl: event.currentTarget }) 22 | } 23 | 24 | handleClose = () => { 25 | this.setState({ anchorEl: null }) 26 | } 27 | 28 | handleLogout = async () => { 29 | const result = await conn.call("logout") 30 | if (result.type !== "ok") throw new Error("invalid_logout") 31 | this.props.cookies.remove("name") 32 | this.props.cookies.remove("password") 33 | store.dispatch({type: "logout"}) 34 | } 35 | 36 | render() { 37 | const {classes} = this.props 38 | return ( 39 |
40 | 41 | 48 | 49 | {this.props.self.substring(0, 3)} 50 | 51 | 56 | 57 | 58 | 65 | 66 | 67 | Logout 68 | 69 | 70 |
71 | ) 72 | } 73 | } 74 | 75 | export default compose( 76 | withCookies, 77 | withStyles(styles, {withTheme: true}), 78 | )(UserSettings) -------------------------------------------------------------------------------- /src/Components/Drawer_Components/index.js: -------------------------------------------------------------------------------- 1 | import Locker from './Locker' 2 | import History from './History' 3 | import Sketch from './Sketch' 4 | import Classroom from './Classroom' 5 | import Tabpage from './ScrollableTabs' 6 | import UserSettings from './UserSettings' 7 | 8 | export { 9 | Locker, History, Sketch, Classroom, Tabpage, UserSettings 10 | } -------------------------------------------------------------------------------- /src/Components/LeftPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Paper} from '@material-ui/core' 3 | 4 | export default props => 5 | 6 | Left 7 | -------------------------------------------------------------------------------- /src/Components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CircularProgress from '@material-ui/core/CircularProgress'; 3 | 4 | 5 | const loadingStyle = { 6 | position: 'absolute', 7 | top: '47%', 8 | left: '48%', 9 | } 10 | 11 | function CircularUnderLoad() { 12 | return ; 13 | } 14 | 15 | export default CircularUnderLoad; -------------------------------------------------------------------------------- /src/Components/Locations/ClassListStudent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | import {Grid, Card, CardHeader, 6 | Divider, Button, ExpansionPanel, 7 | ExpansionPanelDetails, ExpansionPanelSummary} from '@material-ui/core' 8 | import {Settings, ExpandMore} from '@material-ui/icons' 9 | import EnrollClass from './PopupFunction/EnrollClass' 10 | import {connection as conn} from '../../interface/connection' 11 | import {store} from '../../index' 12 | import ViewClassStudentsInfo from './PopupFunction/ViewClassStudentsInfo'; 13 | 14 | const styles = theme => ({ 15 | card: { 16 | minWidth: 800, 17 | minHeight: 500, 18 | marginTop: 50, 19 | marginBottom: 10, 20 | marginLeft: 50, 21 | marginRight: 50, 22 | paddingBottom: 50, 23 | overflowY: "auto" //TODO 'auto' || 'scroll' 24 | }, 25 | inner_grid: { 26 | paddingLeft: 20, 27 | paddingTop: 30 28 | }, 29 | list: { 30 | paddingLeft: 20, 31 | paddingTop: 5 32 | }, 33 | divider: { 34 | paddingLeft: 20, 35 | marginBottom: 10, 36 | width: "100%" 37 | }, 38 | ex_root: { 39 | width: "90%", 40 | paddingLeft: "5%" 41 | }, 42 | expand_heading: { 43 | fontSize: theme.typography.pxToRem(16), 44 | flexBasis: '30%', 45 | flexShrink: 0, 46 | overflow: "hidden", 47 | marginTop: 10 48 | }, 49 | expand_secondaryHeading: { 50 | fontSize: theme.typography.pxToRem(15), 51 | color: theme.palette.text.secondary, 52 | }, 53 | expand_secondaryHeading_green: { 54 | fontSize: theme.typography.pxToRem(15), 55 | color: 'rgba(99, 214, 70)', 56 | } 57 | }); 58 | 59 | class ClassListStudent extends React.Component { 60 | state = {expanded: null, expandedEnroll: null, pollingID: null} 61 | 62 | componentDidMount() { 63 | const pollingID = setInterval(async () => { 64 | const enrolled = await conn.call("get_enrolled_class") 65 | store.dispatch({ 66 | type: "get_enrolled_class", 67 | result: enrolled.result 68 | }) 69 | }, 3000) 70 | this.setState({pollingID}) 71 | } 72 | 73 | componentWillUnmount() { 74 | clearInterval(this.state.pollingID) 75 | } 76 | 77 | handleChange = panel => (event, expanded) => { 78 | this.setState({ 79 | expanded: expanded ? panel : false, 80 | }) 81 | } 82 | 83 | handleChangeEnroll = panel => (event, expanded) => { 84 | this.setState({ 85 | expandedEnroll: expanded ? panel : false, 86 | }) 87 | } 88 | 89 | async joinClass(owner, class_name) { 90 | const response = await conn.call("join_class", {owner, class_name}) 91 | if (response.type === "ok") { 92 | const s_user = await conn.call("get_session_user") 93 | store.dispatch({ 94 | type: "get_session_user", 95 | result: s_user.result 96 | }) 97 | store.dispatch({type: "joinClass", owner, class_name: class_name}) 98 | // this.props.handleNotification(`join ${owner}'s class: ${class_name} success`) 99 | this.props.changeScene(1) 100 | } 101 | if (response.type === "reject") { 102 | this.props.handleNotification(`join class failed, reason: ${response.reason}`) 103 | } 104 | } 105 | 106 | isStarting(owner, class_name) { 107 | return this.props.startedClass.find(obj => (obj.owner === owner && obj.class_name === class_name)) !== undefined 108 | } 109 | 110 | render() { 111 | const { classes, self, ...other } = this.props 112 | const { expandedEnroll } = this.state 113 | return ( 114 | 115 | 119 | 124 | 125 | 126 | 127 | 128 | 129 | {/*Enrolled Class*/} 130 | 131 | 132 | Enrolled Class 133 | 134 | 135 | 136 |
137 | { 138 | (this.props.enrolledClass) && 139 | (this.props.enrolledClass.length > 0) ? 140 | this.props.enrolledClass.map((en, index) => ( 141 | (en.owner !== self) && 142 | 143 | }> 144 | {en.class_name} (teacher: {en.owner}) 145 |
146 | {this.isStarting(en.owner, en.class_name) ? 147 | 148 | Status: Starting 149 | : 150 | 151 | Status: Not started 152 | 153 | } 154 | 155 | Participants Number: {en.joined_number}/{en.sub_number} 156 | 157 |
158 |
159 | 160 | {/*Should not join a not started class*/} 161 | 165 | 167 | 168 | 169 |
170 | )) : 171 | No Enrolled Class 172 | } 173 |
174 |
175 | ) 176 | } 177 | } 178 | 179 | ClassListStudent.propTypes = { 180 | classes: PropTypes.object.isRequired, 181 | }; 182 | 183 | export default withStyles(styles)(ClassListStudent); -------------------------------------------------------------------------------- /src/Components/Locations/ClassListTeacher.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | import {Grid, Card, CardHeader, 6 | Divider, Button, ExpansionPanel, 7 | ExpansionPanelDetails, ExpansionPanelSummary} from '@material-ui/core' 8 | import {Settings, ExpandMore} from '@material-ui/icons' 9 | import CreateClass from './PopupFunction/CreateClass' 10 | import {connection as conn} from '../../interface/connection' 11 | import {store} from '../../index' 12 | import ViewClassStudentsInfo from './PopupFunction/ViewClassStudentsInfo'; 13 | 14 | const styles = theme => ({ 15 | card: { 16 | minWidth: 800, 17 | minHeight: 500, 18 | marginTop: 50, 19 | marginBottom: 10, 20 | marginLeft: 50, 21 | marginRight: 50, 22 | paddingBottom: 50, 23 | overflowY: "auto" //TODO 'auto' || 'scroll' 24 | }, 25 | inner_grid: { 26 | paddingLeft: 20, 27 | paddingTop: 30 28 | }, 29 | list: { 30 | paddingLeft: 20, 31 | paddingTop: 5 32 | }, 33 | divider: { 34 | paddingLeft: 20, 35 | marginBottom: 10, 36 | width: "100%" 37 | }, 38 | ex_root: { 39 | width: "90%", 40 | paddingLeft: "5%" 41 | }, 42 | expand_heading: { 43 | fontSize: theme.typography.pxToRem(16), 44 | flexBasis: '30%', 45 | flexShrink: 0, 46 | overflow: "hidden", 47 | marginTop: 10 48 | }, 49 | expand_secondaryHeading: { 50 | fontSize: theme.typography.pxToRem(15), 51 | color: theme.palette.text.secondary, 52 | }, 53 | expand_secondaryHeading_green: { 54 | fontSize: theme.typography.pxToRem(15), 55 | color: 'rgba(99, 214, 70)', 56 | } 57 | }); 58 | 59 | class ClassListTeacher extends React.Component { 60 | state = {expanded: null, expandedEnroll: null, pollingID: null} 61 | 62 | componentDidMount() { 63 | const pollingID = setInterval(async () => { 64 | const enrolled = await conn.call("get_enrolled_class") 65 | store.dispatch({ 66 | type: "get_enrolled_class", 67 | result: enrolled.result 68 | }) 69 | }, 3000) 70 | this.setState({pollingID}) 71 | } 72 | 73 | componentWillUnmount() { 74 | clearInterval(this.state.pollingID) 75 | } 76 | 77 | handleChange = panel => (event, expanded) => { 78 | this.setState({ 79 | expanded: expanded ? panel : false, 80 | }) 81 | } 82 | 83 | handleChangeEnroll = panel => (event, expanded) => { 84 | this.setState({ 85 | expandedEnroll: expanded ? panel : false, 86 | }) 87 | } 88 | 89 | async joinClass(owner, class_name) { 90 | const response = await conn.call("join_class", {owner, class_name}) 91 | if (response.type === "ok") { 92 | const s_user = await conn.call("get_session_user") 93 | store.dispatch({ 94 | type: "get_session_user", 95 | result: s_user.result 96 | }) 97 | store.dispatch({type: "joinClass", owner, class_name: class_name}) 98 | // this.props.handleNotification(`join ${owner}'s class: ${class_name} success`) 99 | this.props.changeScene(1) 100 | } 101 | if (response.type === "reject") { 102 | this.props.handleNotification(`join class failed, reason: ${response.reason}`) 103 | } 104 | } 105 | 106 | async startClass(class_name) { 107 | const response = await conn.call("start_class", {class_name}) 108 | if (response.type === "ok") { 109 | this.props.handleNotification("start class success") 110 | } 111 | if (response.type === "reject") { 112 | this.props.handleNotification(response.reason) 113 | } 114 | } 115 | 116 | isStarting(owner, class_name) { 117 | return this.props.startedClass.find(obj => (obj.owner === owner && obj.class_name === class_name)) !== undefined 118 | } 119 | 120 | 121 | findStatus = (target) => { 122 | for (let en of this.props.enrolledClass) { 123 | if (en.owner === this.props.self && en.class_name === target) { 124 | return en.joined_number + '/' + en.sub_number 125 | } 126 | } 127 | return "error" 128 | } 129 | 130 | render() { 131 | const { classes, self, ...other } = this.props 132 | const { expanded } = this.state 133 | return ( 134 | 135 | 139 | 144 | 145 | 146 | 147 | 148 | 149 | {/*Created Class*/} 150 | 151 | 152 | Created Class 153 | 154 | 155 | 156 |
157 | {(this.props.createdClass) && //TODO createdClass sometimes undefined 158 | (this.props.createdClass.length > 0) ? 159 | this.props.createdClass.map((c, index) => ( 160 | 165 | }> 166 | {c} 167 |
168 | {this.isStarting(self, c) ? 169 | 170 | Status: Starting 171 | : 172 | 173 | Status: Not started 174 | 175 | } 176 | 177 | Participants Number: {this.findStatus(c)} 178 | 179 |
180 |
181 | 182 | 186 | 190 | 192 | 193 | 194 |
195 | )) : 196 | No Created Class 197 | } 198 |
199 |
200 | ) 201 | } 202 | } 203 | 204 | ClassListTeacher.propTypes = { 205 | classes: PropTypes.object.isRequired, 206 | } 207 | 208 | export default withStyles(styles)(ClassListTeacher); -------------------------------------------------------------------------------- /src/Components/Locations/Classroom.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | import { Button } from '@material-ui/core' 6 | import JoinedLayoutStudent from '../Classroom_Components/JoinedLayoutStudent' 7 | import JoinedLayoutTeacher from '../Classroom_Components/JoinedLayoutTeacher' 8 | import {connection as conn} from '../../interface/connection' 9 | import {store} from '../../index' 10 | 11 | const styles = theme => ({ 12 | notJoined: { 13 | position: 'absolute', 14 | top: '50%', 15 | left: '50%', 16 | transform: 'translateX(-55%) translateY(-60%)', 17 | textAlign: 'center', 18 | display: 'flex', 19 | justifyContent: 'space-around', 20 | flexDirection: 'column', 21 | }, 22 | notJoinHeading: { 23 | // marginBottom: '20px' 24 | } 25 | }) 26 | 27 | class Classroom extends React.Component { 28 | async joinClass(owner, class_name) { 29 | const response = await conn.call("join_class", {owner, class_name}) 30 | if (response.type === "ok") { 31 | const s_user = await conn.call("get_session_user") 32 | store.dispatch({ 33 | type: "get_session_user", 34 | result: s_user.result 35 | }) 36 | store.dispatch({type: "joinClass", owner, class_name: class_name}) 37 | // this.props.handleNotification(`join ${owner}'s class: ${class_name} success`) 38 | } 39 | if (response.type === "reject") { 40 | this.props.handleNotification(`join class failed, reason: ${response.reason}`) 41 | } 42 | } 43 | 44 | render() { 45 | const { classes, ...other } = this.props 46 | return ( 47 | 48 | {(this.props.joined === null) && 49 |
50 | 51 | Not joined a classroom yet 52 | 53 |
54 | 59 | 64 |
65 | {this.props.lastJoin && 66 |
67 | } 68 | {this.props.lastJoin && 69 | Or join where you left last time: 70 | } 71 | {this.props.lastJoin && 72 | 73 | 78 | 79 | } 80 |
81 | } 82 | {(this.props.joined) && (this.props.joined.owner !== this.props.self) && 83 | 84 | } 85 | {(this.props.joined) && (this.props.joined.owner === this.props.self) && 86 | 87 | } 88 |
89 | ) 90 | } 91 | } 92 | 93 | Classroom.propTypes = { 94 | classes: PropTypes.object.isRequired, 95 | } 96 | 97 | export default withStyles(styles)(Classroom) -------------------------------------------------------------------------------- /src/Components/Locations/Content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | 6 | const styles = theme => ({ 7 | root: { 8 | height: '100%', 9 | width: '100%', 10 | display: 'flex', 11 | justifyContent: 'center', 12 | alignItems: 'center', 13 | }, 14 | grid_item: { 15 | marginBottom: 50, 16 | }, 17 | grid: { 18 | paddingTop: 30, 19 | } 20 | }) 21 | 22 | class Content extends React.Component { 23 | render() { 24 | const { classes } = this.props 25 | return ( 26 |
27 |
28 | 29 | Welcome to the classroom ! 30 | 31 | 32 | Currently only Classroom Page (First Icon) and ClassList Page (Second Icon) is not empty. 33 | 34 | 35 | Safari cannot render react/vue correctly, so please browse with chrome/firefox, maybe we will figure it out later. 36 | 37 | 38 | All layouts are temporary, for testing only. 39 | 40 | 41 | Copyright © 2019 Overcoded All rights reserved 42 | 43 |
44 |
45 | ) 46 | } 47 | } 48 | 49 | Content.propTypes = { 50 | classes: PropTypes.object.isRequired, 51 | }; 52 | 53 | export default withStyles(styles)(Content); -------------------------------------------------------------------------------- /src/Components/Locations/Mailbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | import {Grid} from '@material-ui/core' 6 | import 'whatwg-fetch' 7 | import UserCard from '../Content_Components/UserCard' 8 | 9 | 10 | const styles = theme => ({ 11 | toolbar: { 12 | display: 'flex', 13 | alignItems: 'center', 14 | justifyContent: 'flex-end', 15 | padding: '0 8px', 16 | ...theme.mixins.toolbar, 17 | }, 18 | content: { 19 | flexGrow: 1, 20 | backgroundColor: theme.palette.background.default, 21 | padding: theme.spacing.unit * 5, 22 | minWidth: 0, // So the Typography noWrap works 23 | paddingRight: 250, 24 | paddingLeft: 120, 25 | }, 26 | grid_item: { 27 | marginBottom: 50, 28 | }, 29 | grid: { 30 | paddingTop: 30, 31 | } 32 | }); 33 | 34 | class Content extends React.Component { 35 | render() { 36 | const { classes, ...other } = this.props; 37 | return ( 38 |
39 |
40 | {'Mailbox'} 41 | 48 | {(this.props.users != null) && this.props.users.map(user => ( 49 | 50 | 51 | 52 | ))} 53 | 54 |
55 | ) 56 | } 57 | } 58 | 59 | Content.propTypes = { 60 | classes: PropTypes.object.isRequired, 61 | }; 62 | 63 | export default withStyles(styles)(Content); -------------------------------------------------------------------------------- /src/Components/Locations/Notebooks.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types' 3 | import {withStyles} from '@material-ui/core/styles' 4 | import Typography from '@material-ui/core/Typography' 5 | import {Grid} from '@material-ui/core' 6 | import 'whatwg-fetch' 7 | import UserCard from '../Content_Components/UserCard' 8 | 9 | 10 | const styles = theme => ({ 11 | toolbar: { 12 | display: 'flex', 13 | alignItems: 'center', 14 | justifyContent: 'flex-end', 15 | padding: '0 8px', 16 | ...theme.mixins.toolbar, 17 | }, 18 | content: { 19 | flexGrow: 1, 20 | backgroundColor: theme.palette.background.default, 21 | padding: theme.spacing.unit * 5, 22 | minWidth: 0, // So the Typography noWrap works 23 | paddingRight: 250, 24 | paddingLeft: 120, 25 | }, 26 | grid_item: { 27 | marginBottom: 50, 28 | }, 29 | grid: { 30 | paddingTop: 30, 31 | } 32 | }); 33 | 34 | class Content extends React.Component { 35 | render() { 36 | const { classes, ...other } = this.props; 37 | return ( 38 |
39 |
40 | {'Notebooks'} 41 | 48 | {(this.props.users != null) && this.props.users.map(user => ( 49 | 50 | 51 | 52 | ))} 53 | 54 |
55 | ) 56 | } 57 | } 58 | 59 | Content.propTypes = { 60 | classes: PropTypes.object.isRequired, 61 | }; 62 | 63 | export default withStyles(styles)(Content); -------------------------------------------------------------------------------- /src/Components/Locations/PopupFunction/ChangeUserInfo.js: -------------------------------------------------------------------------------- 1 | import Popover from '../../Popover' 2 | import React, { Fragment } from 'react'; 3 | import {MenuItem, Grid, DialogContent, TextField, DialogContentText, Button, DialogActions, withStyles} from '@material-ui/core' 4 | 5 | const styles = theme => ({ 6 | textField: { 7 | marginLeft: theme.spacing.unit, 8 | marginRight: theme.spacing.unit, 9 | width: 200, 10 | } 11 | }) 12 | 13 | class ChangeUserInfo extends React.Component { 14 | state = { 15 | open: false, 16 | name: this.props.self, 17 | age: '', 18 | course: '', 19 | loginName: '', 20 | loginPassword: '', 21 | showPassword: '' 22 | } 23 | 24 | handleChangeUserInfo = () => { 25 | this.setState({open: true}) 26 | } 27 | 28 | handleSave = () => { 29 | this.setState({open: false}) 30 | } 31 | 32 | handleChangeName = name => event => { 33 | this.setState({ [name]: event.target.value }); 34 | }; 35 | 36 | handleChangeAge = age => event => { 37 | this.setState({ [age]: event.target.value }); 38 | }; 39 | 40 | handleChangeCourse = course => event => { 41 | this.setState({ [course]: event.target.value }); 42 | }; 43 | 44 | render() { 45 | const { classes } = this.props 46 | return ( 47 | 48 | 49 | Change UserInfo 50 | 51 | 52 | 53 | 54 | 60 | 61 | 71 | 72 | 73 | 83 | 84 | 85 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | ); 106 | } 107 | } 108 | 109 | export default withStyles(styles, {withTheme: true})(ChangeUserInfo); 110 | -------------------------------------------------------------------------------- /src/Components/Locations/PopupFunction/CreateClass.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import PopoverWithBtn from '../../PopoverWithBtn' 3 | import {Button, DialogActions, DialogContent, DialogContentText, TextField} from "@material-ui/core"; 4 | import { connection as conn } from '../../../interface/connection' 5 | import {store} from '../../../index' 6 | 7 | export default class CreateClass extends React.Component { 8 | state = { 9 | open: false, 10 | class_name: "" 11 | } 12 | 13 | handle() { 14 | this.setState({open: !this.state.open}); 15 | } 16 | 17 | handleClassName(e) { 18 | this.setState({class_name: e.target.value}); 19 | }; 20 | 21 | keyPress(e) { 22 | if (e.keyCode === 13) { 23 | e.preventDefault(); 24 | this.handleSubmit() 25 | } 26 | } 27 | 28 | async handleSubmit() { 29 | const response = await conn.call("create_class", {class_name: this.state.class_name}) 30 | if (response.type === "ok") { 31 | // [created, enrolled] = Promise.all(conn.call(), conn.call()) 32 | const created = await conn.call("get_created_class") 33 | const enrolled = await conn.call("get_enrolled_class") 34 | store.dispatch({ 35 | type:"get_created_class", 36 | result: created.result 37 | }) 38 | store.dispatch({ 39 | type: "get_enrolled_class", 40 | result: enrolled.result 41 | }) 42 | this.props.handleNotification("created class success") 43 | } 44 | if (response.type === "reject") { 45 | this.props.handleNotification(`created class failed, reason: ${response.reason}`) 46 | } 47 | this.setState({open: false}) 48 | } 49 | 50 | render() { 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | 58 | {/*{children}*/} 59 | 60 |
61 | 68 | 69 |
70 | 71 | 72 | 73 |
74 |
75 | ) 76 | } 77 | } -------------------------------------------------------------------------------- /src/Components/Locations/PopupFunction/EnrollClass.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import PopoverWithBtn from '../../PopoverWithBtn' 3 | import {Button, DialogActions, DialogContent, DialogContentText, TextField} from "@material-ui/core"; 4 | import { connection as conn } from '../../../interface/connection' 5 | import {store} from '../../../index' 6 | 7 | export default class CreateClass extends React.Component { 8 | state = { 9 | open: false, 10 | t_name: "", 11 | class_name: "" 12 | } 13 | 14 | handle() { 15 | this.setState({open: !this.state.open}); 16 | } 17 | 18 | handleT(e) { 19 | this.setState({t_name: e.target.value}); 20 | } 21 | 22 | handleClassName(e) { 23 | this.setState({class_name: e.target.value}); 24 | } 25 | 26 | keyPress(e) { 27 | if (e.keyCode === 13) { 28 | e.preventDefault(); 29 | this.handleSubmit() 30 | } 31 | } 32 | 33 | handleSubmit() { 34 | conn.call("enroll_class", {owner: this.state.t_name, class_name: this.state.class_name}) 35 | .then(async (response) => { 36 | if (response.type === "ok") { 37 | // Promise.all(conn.call("get_enrolled_class"), conn.call("get_started_class")) 38 | // .then(([enrolled, started]) => { 39 | // store.dispatch({ 40 | // type: "get_enrolled_class", 41 | // result: enrolled.result 42 | // }) 43 | // store.dispatch({ 44 | // type: "get_started_class", 45 | // result: started.result 46 | // }) 47 | // }) 48 | const enrolled = await conn.call("get_enrolled_class") 49 | const started = await conn.call("get_started_class") 50 | store.dispatch({ 51 | type: "get_enrolled_class", 52 | result: enrolled.result 53 | }) 54 | store.dispatch({ 55 | type: "get_started_class", 56 | result: started.result 57 | }) 58 | this.props.handleNotification(`Enrolled ${this.state.t_name}'s class: ${this.state.class_name}`) 59 | } 60 | if (response.type === "reject") this.props.handleNotification("Enrolled class FAILED") 61 | }) 62 | this.setState({open: false}) 63 | } 64 | 65 | render() { 66 | return ( 67 | 68 | 69 | 70 | 71 | 72 | 73 | {/*{children}*/} 74 | 75 |
76 | 83 | 84 |
85 | 91 | 92 |
93 | 94 | 95 | 96 |
97 |
98 | ) 99 | } 100 | } -------------------------------------------------------------------------------- /src/Components/Locations/PopupFunction/ViewClassStudentsInfo.js: -------------------------------------------------------------------------------- 1 | import Popover from '../../PopoverNarrow' 2 | import React, { Fragment } from 'react'; 3 | import { Grid, DialogContent, Button, withStyles, Typography} from '@material-ui/core' 4 | import {connection as conn} from '../../../interface/connection' 5 | 6 | const styles = theme => ({ 7 | textField: { 8 | marginLeft: theme.spacing.unit, 9 | marginRight: theme.spacing.unit, 10 | width: 200, 11 | }, 12 | item: { 13 | fontSize: theme.typography.pxToRem(15), 14 | color: theme.palette.text.secondary, 15 | }, 16 | item_green: { 17 | fontSize: theme.typography.pxToRem(15), 18 | color: 'rgba(99, 214, 70)', 19 | } 20 | }) 21 | 22 | class ViewClassStudentsInfo extends React.Component { 23 | state = { 24 | open: false, 25 | subed: null, 26 | joined: null 27 | } 28 | 29 | getStudentsInfo = async (c) => { 30 | const response = await conn.call("get_student_names_of_a_class", c) 31 | if (response.result) { 32 | if (response.result.subed.length <= 1) { 33 | this.props.handleNotification(`No one enrolled ${c.class_name} yet`) 34 | return 35 | } 36 | this.setState({ 37 | open: true, 38 | subed: response.result.subed, 39 | joined: response.result.joined}) 40 | } 41 | } 42 | 43 | render() { 44 | const {classes} = this.props 45 | return ( 46 | 47 | 50 | this.setState({open: false})}> 53 | 54 | 60 | { 61 | this.state.subed && 62 | this.state.subed.map(sname => ( 63 | (sname !== this.props.class.owner) && 64 | 65 | {(this.state.joined.includes(sname)) ? 66 | 67 | {sname} 68 | : 69 | 70 | {sname} 71 | 72 | } 73 | 74 | ))} 75 | 76 | 77 | 78 | 79 | ) 80 | } 81 | } 82 | 83 | export default withStyles(styles, {withTheme: true})(ViewClassStudentsInfo); 84 | -------------------------------------------------------------------------------- /src/Components/Locations/Upload.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { compose } from 'redux' 3 | import { NotificationBar } from '../../Components' 4 | import { TextField, Grid, withStyles } from "@material-ui/core" 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import InputAdornment from '@material-ui/core/InputAdornment'; 7 | import Visibility from '@material-ui/icons/Visibility'; 8 | import VisibilityOff from '@material-ui/icons/VisibilityOff'; 9 | import { withCookies } from 'react-cookie' 10 | import '../../css/App.css' 11 | import Dropzone from 'react-dropzone' 12 | import classNames from 'classnames' 13 | import Link from 'react-router-dom/Link' 14 | import {uploadURL} from '../../interface/connection' 15 | 16 | // import axios from 'axios' 17 | 18 | const styles = theme => ({ 19 | dropzone: { 20 | position: 'absolute', 21 | top: '65%', 22 | left: '50%', 23 | transform: 'translateX(-50%) translateY(-50%)' 24 | }, 25 | form: { 26 | position: 'absolute', 27 | top: '40%', 28 | left: '50%', 29 | transform: 'translateX(-50%) translateY(-50%)' 30 | } 31 | }) 32 | 33 | class Upload extends React.Component { 34 | state = { 35 | showNotification: false, 36 | notificationMessage: "", 37 | connected: false, 38 | showPassword: false, 39 | username: "", 40 | password: "" 41 | } 42 | 43 | componentDidMount() { 44 | const {cookies} = this.props 45 | if (cookies.get("name")) { 46 | this.setState({username: cookies.get("name"), password: cookies.get("password")}) 47 | } 48 | } 49 | 50 | notificationQueue = [] 51 | 52 | handleNotification = (message) => { 53 | this.notificationQueue.push(message) 54 | if (this.state.showNotification) { 55 | this.setState({ showNotification: false }); 56 | } else { 57 | this.processQueue(); 58 | } 59 | } 60 | 61 | processQueue = () => { 62 | if (this.notificationQueue.length > 0) { 63 | this.setState({ 64 | notificationMessage: this.notificationQueue.shift(), 65 | showNotification: true, 66 | }) 67 | } 68 | } 69 | 70 | handleDismissNotification = (event, reason) => { 71 | if (reason === 'clickaway') { 72 | return; 73 | } 74 | this.setState({ showNotification: false }); 75 | } 76 | 77 | handleName = e => { 78 | this.setState({ username: e.target.value }); 79 | } 80 | 81 | handlePassword = (e) => { 82 | this.setState({password: e.target.value}); 83 | } 84 | 85 | keyPress = (e) => { 86 | if (e.keyCode === 13) { 87 | e.preventDefault() 88 | } 89 | } 90 | 91 | handleClickShowPassword = () => { 92 | this.setState(state => ({ showPassword: !state.showPassword })); 93 | } 94 | 95 | handleDrop = (acceptedFiles, rejectedFiles) => { 96 | console.log({ acceptedFiles, rejectedFiles }) 97 | if (acceptedFiles.length > 0){ 98 | let formdata = new FormData() 99 | formdata.append("data", acceptedFiles[0], acceptedFiles[0].name) //3rd arg refer to filename 100 | formdata.append("timestamp", (new Date()).toISOString()) 101 | formdata.append("username", this.state.username) 102 | formdata.append("password", this.state.password) 103 | fetch(uploadURL+'/upload', { 104 | method: "POST", 105 | body: formdata 106 | }) 107 | .then(response => response.text()) 108 | .then(data => this.handleNotification(data)) 109 | .catch(e => {this.handleNotification(`${e}`)}) 110 | } 111 | } 112 | 113 | render() { 114 | const { classes } = this.props 115 | return ( 116 |
117 | Back to Classroom 118 | 125 | 126 | 135 | 136 | 137 | 138 | 149 | 153 | {this.state.showPassword ? : } 154 | 155 | 156 | ), 157 | }} 158 | /> 159 | 160 | 161 |
162 | 163 | {({ getRootProps, getInputProps, isDragActive }) => { 164 | return ( 165 |
169 | 170 | { 171 | isDragActive ? 172 |

Drop files here... (50MB max)

: 173 |

Drop files here, or click to select files to upload (50MB max)

174 | } 175 |
176 | ) 177 | }} 178 |
179 |
180 | 186 |
187 | ) 188 | } 189 | } 190 | 191 | export default compose( 192 | withCookies, 193 | withStyles(styles, { withTheme: true }) 194 | )(Upload); -------------------------------------------------------------------------------- /src/Components/NestedListInFind.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import ListSubheader from '@material-ui/core/ListSubheader'; 5 | import List from '@material-ui/core/List'; 6 | import ListItem from '@material-ui/core/ListItem'; 7 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 8 | import ListItemText from '@material-ui/core/ListItemText'; 9 | import Collapse from '@material-ui/core/Collapse'; 10 | import InboxIcon from '@material-ui/icons/MoveToInbox'; 11 | import DraftsIcon from '@material-ui/icons/Drafts'; 12 | import SendIcon from '@material-ui/icons/Send'; 13 | import ExpandLess from '@material-ui/icons/ExpandLess'; 14 | import ExpandMore from '@material-ui/icons/ExpandMore'; 15 | import StarBorder from '@material-ui/icons/StarBorder'; 16 | 17 | const styles = theme => ({ 18 | root: { 19 | width: '100%', 20 | // maxWidth: 360, 21 | backgroundColor: theme.palette.background.paper, 22 | }, 23 | nested: { 24 | paddingLeft: theme.spacing.unit * 4, 25 | }, 26 | }); 27 | 28 | class NestedList extends React.Component { 29 | state = { 30 | open: true, 31 | } 32 | 33 | handleClick = () => { 34 | this.setState(state => ({ open: !state.open })); 35 | } 36 | 37 | render() { 38 | const { classes, components } = this.props 39 | 40 | return ( 41 | Classroom Components} 44 | className={classes.root} 45 | > 46 | {Object.keys(components).map( 47 | (outer) => ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | {this.state.open ? : } 55 | 56 | {Object.keys(components[outer]).map((inner) => ( 57 | 58 | 59 | 60 | this.props.bringTop(inner)}> 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ))} 70 | ) 71 | )} 72 | 73 | ) 74 | } 75 | } 76 | 77 | function getInnerName(inner, outer) { 78 | if (outer === 'webcam') return inner.substring(0, inner.length - 6) 79 | if (outer === 'whiteboard') return inner.substring(0, inner.length - 10) 80 | if (outer === 'drawer') return inner.substring(0, inner.length - 6) 81 | // if (outer === 'other') return inner.substring(0, inner.length - 4) 82 | return inner 83 | } 84 | 85 | NestedList.propTypes = { 86 | classes: PropTypes.object.isRequired, 87 | }; 88 | 89 | export default withStyles(styles)(NestedList); -------------------------------------------------------------------------------- /src/Components/NotificationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | // import Button from '@material-ui/core/Button'; 5 | import Snackbar from '@material-ui/core/Snackbar'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import CloseIcon from '@material-ui/icons/Close'; 8 | 9 | const styles = theme => ({ 10 | close: { 11 | padding: theme.spacing.unit / 2, 12 | }, 13 | }); 14 | 15 | class NotificationBar extends React.Component { 16 | render() { 17 | const { classes } = this.props; 18 | return ( 19 |
20 | {this.props.message}} 33 | action={[ 34 | //, 37 | 44 | 45 | , 46 | ]} 47 | /> 48 |
49 | ); 50 | } 51 | } 52 | 53 | NotificationBar.propTypes = { 54 | classes: PropTypes.object.isRequired, 55 | }; 56 | 57 | export default withStyles(styles)(NotificationBar); -------------------------------------------------------------------------------- /src/Components/Popover.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Dialog, 3 | // DialogContent, DialogContentText, 4 | DialogTitle, withStyles, 5 | // Button, DialogActions 6 | } from "@material-ui/core"; 7 | 8 | const styles = theme => ({ 9 | dialog: { 10 | width: '70%', 11 | height: '90%', 12 | maxWidth: '90%', 13 | // maxHeight: 435 14 | } 15 | }); 16 | 17 | class Popover extends React.Component { 18 | 19 | render() { 20 | const {title, children, classes, open} = this.props; 21 | return ( 22 | this.props.onClose? this.props.onClose() : null} 25 | aria-labelledby="form-dialog-title" 26 | classes={{paper: classes.dialog}} 27 | > 28 | {title} 29 | {/* 30 | 31 | {children} 32 | 33 | 34 | 35 | 36 | 37 | */} 38 | {children} 39 | 40 | ) 41 | } 42 | } 43 | 44 | export default withStyles(styles)(Popover); -------------------------------------------------------------------------------- /src/Components/PopoverNarrow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Dialog, 3 | // DialogContent, DialogContentText, 4 | DialogTitle, withStyles, 5 | // Button, DialogActions 6 | } from "@material-ui/core"; 7 | 8 | const styles = theme => ({ 9 | dialog: { 10 | width: '25%', 11 | height: '90%', 12 | maxWidth: '90%', 13 | // maxHeight: 435 14 | } 15 | }); 16 | 17 | class PopoverNarrow extends React.Component { 18 | 19 | render() { 20 | const {title, children, classes, open} = this.props; 21 | return ( 22 | this.props.onClose? this.props.onClose() : null} 25 | aria-labelledby="form-dialog-title" 26 | classes={{paper: classes.dialog}} 27 | > 28 | {title} 29 | {/* 30 | 31 | {children} 32 | 33 | 34 | 35 | 36 | 37 | */} 38 | {children} 39 | 40 | ) 41 | } 42 | } 43 | 44 | export default withStyles(styles)(PopoverNarrow); -------------------------------------------------------------------------------- /src/Components/PopoverWithBtn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Dialog, DialogTitle, withStyles} from "@material-ui/core"; 3 | 4 | const styles = theme => ({ 5 | dialog: { 6 | // width: '50%', 7 | // height: '80%', 8 | maxHeight: '30%', 9 | maxWidth: '30%', 10 | // maxHeight: 435 11 | } 12 | }); 13 | 14 | class PopoverWithBtn extends React.Component { 15 | 16 | render() { 17 | const {title, children, classes, open} = this.props; 18 | return ( 19 | this.props.onClose()} 22 | aria-labelledby="form-dialog-title" 23 | classes={{paper: classes.dialog}} 24 | > 25 | {title} 26 | {children} 27 | {/**/} 28 | {/**/} 29 | {/*{children}*/} 30 | {/**/} 31 | {/**/} 32 | {/**/} 33 | {/**/} 34 | {/**/} 35 | 36 | ) 37 | } 38 | } 39 | 40 | export default withStyles(styles)(PopoverWithBtn); -------------------------------------------------------------------------------- /src/Components/RegisterBtn.js: -------------------------------------------------------------------------------- 1 | import React, {Fragment} from 'react'; 2 | import PopoverWithBtn from './PopoverWithBtn' 3 | import {Button, DialogActions, DialogContent, DialogContentText, TextField} from "@material-ui/core"; 4 | import {connection as conn} from '../interface/connection' 5 | 6 | export default class RegisterBtn extends React.Component { 7 | state = { 8 | open: false, 9 | name: "", 10 | password: "" 11 | } 12 | 13 | handle() { 14 | this.setState({open: !this.state.open}); 15 | } 16 | 17 | handleName(e) { 18 | this.setState({name: e.target.value}); 19 | }; 20 | 21 | handlePassword(e) { 22 | this.setState({password: e.target.value}); 23 | }; 24 | 25 | keyPress(e) { 26 | if (e.keyCode === 13) { 27 | e.preventDefault(); 28 | this.handleSubmit() 29 | } 30 | } 31 | 32 | async handleSubmit() { 33 | const result = await conn.call("register", {username: this.state.name, password: this.state.password}) 34 | if (result.type === "ok") { 35 | this.props.handleNotification("registered") 36 | } else if (result.type === "reject") { 37 | this.props.handleNotification(result.reason) 38 | } 39 | this.setState({open: false}) 40 | } 41 | 42 | render() { 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | {/*{children}*/} 51 | 52 |
53 | 60 | 61 |
62 | 68 | 69 |
70 | 71 | 72 | 73 |
74 |
75 | ) 76 | } 77 | } -------------------------------------------------------------------------------- /src/Components/index.js: -------------------------------------------------------------------------------- 1 | import Content from './Locations/Content' 2 | import Classroom from './Locations/Classroom' 3 | import ClassList from './Locations/ClassList' 4 | import Mailbox from './Locations/Mailbox' 5 | import Notebooks from './Locations/Notebooks' 6 | import NotificationBar from './NotificationBar' 7 | import ClassNotificationBar from './ClassNotificationBar' 8 | 9 | const drawerWidth = 220; 10 | 11 | export { 12 | Content, drawerWidth, Classroom, ClassList, Mailbox, Notebooks, NotificationBar, ClassNotificationBar 13 | } -------------------------------------------------------------------------------- /src/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import {Button, TextField, Grid, withStyles, Avatar, Fade} from "@material-ui/core" 3 | import RegisterBtn from './Components/RegisterBtn' 4 | import Background from './css/classroom.jpg' 5 | import {withCookies} from 'react-cookie' 6 | import {connection as conn} from './interface/connection' 7 | import {compose} from 'redux' 8 | import {store} from './index' 9 | import Loading from './Components/Loading' 10 | import IconButton from '@material-ui/core/IconButton'; 11 | import InputAdornment from '@material-ui/core/InputAdornment'; 12 | import Visibility from '@material-ui/icons/Visibility'; 13 | import VisibilityOff from '@material-ui/icons/VisibilityOff'; 14 | 15 | const styles = theme => ({ 16 | loginBg: { 17 | backgroundImage: 'url(' + Background + ')', 18 | filter: 'blur(5px)', 19 | opacity: 0.8, 20 | backgroundSize: 'cover', 21 | overflow: 'hidden', 22 | height: "100vh" 23 | }, 24 | bigAvatar: { 25 | margin: 30, 26 | width: 90, 27 | height: 90, 28 | }, 29 | loginForm: { 30 | position: 'absolute', 31 | top: '50%', 32 | left: '50%', 33 | transform: 'translateX(-50%) translateY(-50%)' 34 | } 35 | }) 36 | 37 | class Login extends React.Component { 38 | // static propTypes = { 39 | // cookies: instanceOf(Cookies).isRequired 40 | // } 41 | 42 | state = { 43 | connected: false, 44 | loginName: "dev", 45 | loginPassword: "dev", 46 | showPassword: false, 47 | fade: false, 48 | refreshIntervalId: null 49 | } 50 | 51 | componentDidMount() { 52 | const refreshIntervalId = setInterval(() => this.setState({fade: true}), 500) 53 | this.setState({refreshIntervalId}) 54 | conn.addListener("socketopen", this.handleSocketOpen) 55 | } 56 | 57 | componentWillUnmount() { 58 | clearInterval(this.state.refreshIntervalId) 59 | } 60 | 61 | handleSocketOpen = () => { 62 | conn.removeListener("socketopen", this.handleSocketOpen) 63 | const {cookies} = this.props 64 | this.setState({connected: true}) 65 | if (cookies.get("name")) { 66 | this.setState({loginName: cookies.get("name"), loginPassword: cookies.get("password")}) 67 | this.handleLogin() 68 | if (cookies.get("location")) { 69 | // TODO parseInt in production 70 | store.dispatch({type: "changeLocation", target: parseFloat(cookies.get("location"), 10)}) 71 | } 72 | } else { 73 | this.setState({connected: true}) 74 | } 75 | } 76 | 77 | handleLogin = async () => { 78 | const result = await conn.call("login", {username: this.state.loginName, password: this.state.loginPassword}) 79 | if (result["type"] === "ok") { 80 | const {cookies} = this.props 81 | this.setState({connected: true}) 82 | cookies.set("name", this.state.loginName) // option: {path: "/"} 83 | cookies.set("password", this.state.loginPassword) 84 | this.setState({loginPassword: ""}) 85 | store.dispatch({type:"login", loginName: this.state.loginName}) 86 | //TODO listen the following events 87 | const created = await conn.call("get_created_class") 88 | const enrolled = await conn.call("get_enrolled_class") 89 | const started = await conn.call("get_started_class") 90 | store.dispatch({ 91 | type:"get_created_class", 92 | result: created.result 93 | }) 94 | store.dispatch({ 95 | type: "get_enrolled_class", 96 | result: enrolled.result 97 | }) 98 | store.dispatch({ 99 | type: "get_started_class", 100 | result: started.result 101 | }) 102 | this.props.handleNotification(`Welcome ${this.state.loginName}`) 103 | } else { 104 | if (result["type"] === "reject") { 105 | if (result["reason"] === "invalid_params") this.props.handleNotification("Invalid username or password") 106 | //TODO prompt and force login 107 | if (result["reason"] === "already_logged_in") this.props.handleNotification("Already logged in") 108 | } 109 | } 110 | } 111 | 112 | handleName = (e) => { 113 | this.setState({loginName: e.target.value}); 114 | } 115 | 116 | handlePassword = (e) => { 117 | this.setState({loginPassword: e.target.value}); 118 | } 119 | 120 | keyPress = (e) => { 121 | if (e.keyCode === 13) { 122 | e.preventDefault(); 123 | this.handleLogin() 124 | } 125 | } 126 | 127 | handleClickShowPassword = () => { 128 | this.setState(state => ({ showPassword: !state.showPassword })); 129 | } 130 | 131 | render() { 132 | const {classes} = this.props 133 | return ( 134 | 135 | {conn.connected() && 136 |
} 137 | {conn.connected() && 138 | 139 | 146 | 147 | A 148 | 149 | 150 | Name 151 | 152 | 153 | 162 | 163 | 164 | 165 | 176 | 180 | {this.state.showPassword ? : } 181 | 182 | 183 | ), 184 | }} 185 | /> 186 | 187 | 188 | 189 | 198 | 199 | 200 | 201 | 202 | 203 | } 204 | {!conn.connected() && 205 | 206 | } 207 |
208 | ) 209 | } 210 | } 211 | 212 | export default compose( 213 | withCookies, 214 | withStyles(styles, {withTheme: true}), 215 | )(Login) -------------------------------------------------------------------------------- /src/css/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | /*font: 28px "Century Gothic", Futura, sans-serif;*/ 3 | /*margin: 40px;*/ 4 | background-color: #fafafa; 5 | } 6 | 7 | /*.container {*/ 8 | /*height: 1000px;*/ 9 | /*background-color: #ececec;*/ 10 | /*margin: auto;*/ 11 | /*width: 60%;*/ 12 | /*}*/ 13 | 14 | /*.classroom {*/ 15 | /*display: flex;*/ 16 | /*flex-direction: column;*/ 17 | /*align-items: left;*/ 18 | /*font: 28px "Century Gothic", Futura, sans-serif;*/ 19 | /*padding-left: 40px;*/ 20 | /*padding-top: 40px;*/ 21 | /*}*/ 22 | 23 | /*.control_panel {*/ 24 | /*margin-left: 40px;*/ 25 | /*}*/ -------------------------------------------------------------------------------- /src/css/ask_camera_permission.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/ask_camera_permission.jpeg -------------------------------------------------------------------------------- /src/css/ask_camera_permission.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/ask_camera_permission.jpg -------------------------------------------------------------------------------- /src/css/ask_camera_permission.jpg.sb-e2fe0202-Ek4Ixv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/ask_camera_permission.jpg.sb-e2fe0202-Ek4Ixv -------------------------------------------------------------------------------- /src/css/ask_camera_permission.jpg.sb-e2fe0202-rASvL6: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/ask_camera_permission.jpg.sb-e2fe0202-rASvL6 -------------------------------------------------------------------------------- /src/css/classroom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/classroom.jpg -------------------------------------------------------------------------------- /src/css/classroom.jpg.bk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/classroom.jpg.bk -------------------------------------------------------------------------------- /src/css/classroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/classroom.png -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /src/css/whiteboard-no-cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/whiteboard-no-cut.png -------------------------------------------------------------------------------- /src/css/whiteboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/whiteboard.png -------------------------------------------------------------------------------- /src/css/yoda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herbert1228/online-classroom-react.js-client/dc2fc3bc23ea27574f1b026d814721aaa0306189/src/css/yoda.jpg -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App' 3 | import Upload from './Components/Locations/Upload' 4 | // import registerServiceWorker from './registerServiceWorker'; 5 | import {createMuiTheme, MuiThemeProvider} from '@material-ui/core/styles' 6 | import {amber, blueGrey} from '@material-ui/core/colors' 7 | import {BrowserRouter as Router, Route} from "react-router-dom" 8 | import { Provider } from 'react-redux' 9 | import { createStore } from 'redux' 10 | import ReactDOM from 'react-dom' 11 | import { connection as conn } from './interface/connection' 12 | 13 | let themeType = 'light'; 14 | 15 | const theme = createMuiTheme({ 16 | palette: { 17 | primary: {main: blueGrey[800]}, 18 | secondary: amber, 19 | type: themeType 20 | } 21 | }) 22 | 23 | const initialState = { 24 | createdClass: [], 25 | enrolledClass: [], // class_name, owner, online and offline students number of every class 26 | startedClass: [], 27 | session_user: [], // user currently in the same class 28 | peerConn: [], // session_user who is ready for peer connection (as webcam turned on) 29 | location: 0, 30 | self: null, 31 | joined: null, 32 | lastJoin: null, 33 | drawerOpen: true, 34 | groupCards: [], 35 | group: {group: null, members: []}, 36 | webcamPermission: {video: true, audio: true} 37 | } 38 | 39 | function reducer(state = initialState, action) { 40 | // console.log(action) 41 | switch(action.type) { 42 | case "get_created_class": 43 | return {...state, createdClass: action.result} 44 | case "get_enrolled_class": 45 | return {...state, enrolledClass: action.result} 46 | case "get_started_class": 47 | return {...state, startedClass: action.result} 48 | case "get_session_user": 49 | return {...state, session_user: action.result} 50 | case "get_exist_peer_conn": 51 | return {...state, peerConn: action.result} 52 | case "changeLocation": 53 | return {...state, location: action.target} 54 | case "logout": 55 | return {...state, self: null, joined: null} 56 | case "login": 57 | return {...state, self: action.loginName} 58 | case "joinClass": 59 | const joined = {owner: action.owner, class_name: action.class_name} 60 | return {...state, joined, lastJoin: joined} 61 | case "leaveClass": 62 | return {...state, joined: null, session_user: []} 63 | case "drawerOpen": 64 | return {...state, drawerOpen: action.drawerOpen} 65 | case "updateGroupCards": 66 | return {...state, groupCards: action.groupCards} 67 | case "updateGroup": 68 | return {...state, group: action.group} 69 | case "updateWebcamPermission": 70 | return {...state, webcamPermission: action.webcamPermission} 71 | default: 72 | return state 73 | } 74 | } 75 | 76 | export const store = createStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) 77 | 78 | class AppContainer extends React.Component { 79 | componentDidMount() { 80 | conn.connect() 81 | conn.addListener("socketclose", this.handleSocketClose) 82 | conn.addListener("get_started_class", (e) => this.dispatch("get_started_class", e.result)) 83 | conn.addListener("broadcast_message", (e) => console.log(e.result)) 84 | conn.addListener("get_session_user", (e) => this.dispatch("get_session_user", e.result)) 85 | conn.addListener("get_exist_peer_conn", (e) => this.dispatch("get_exist_peer_conn", e.result)) 86 | } 87 | 88 | componentWillUnmount() { 89 | conn.removeListener("socketclose", this.handleSocketClose) 90 | conn.removeListener("get_started_class", (e) => this.dispatch("get_started_class", e.result)) 91 | conn.removeListener("broadcast_message", (e) => console.log(e.result)) 92 | conn.removeListener("get_session_user", (e) => this.dispatch("get_session_user", e.result)) 93 | conn.removeListener("get_exist_peer_conn", (e) => this.dispatch("get_exist_peer_conn", e.result)) 94 | } 95 | 96 | dispatch = (type, result) => { 97 | store.dispatch({type, result}) 98 | } 99 | 100 | handleSocketClose = () => { 101 | //TODO relogin here 102 | console.log("attempting to reconnect...") 103 | setTimeout(() => { 104 | store.dispatch({type: "logout"}) //TODO error: cannot set self to null 105 | conn.connect() 106 | }, 2000) 107 | this.setState({joined: null}) 108 | } 109 | 110 | render() { 111 | return ( 112 | 113 | ) 114 | } 115 | } 116 | 117 | function Index() { 118 | return ( 119 | 120 | 121 | 122 |
123 | }/> 124 | }/> 125 |
126 |
127 |
128 |
129 | ) 130 | } 131 | 132 | ReactDOM.render(, document.getElementById('root')); 133 | 134 | // registerServiceWorker(); 135 | -------------------------------------------------------------------------------- /src/interface/util.js: -------------------------------------------------------------------------------- 1 | const notification = { 2 | 3 | } 4 | 5 | export { 6 | 7 | } -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------