diff --git a/README.md b/README.md index 197b5d19..eab94aff 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ Runs npm install in both the client and server package after npm install is run You will need to setup two sets of environment variables, one for the client and one for the server. You can do this using .env files within the client and server folders of this project. Refer to the readmes in the client and server folders to setup your environment variables -Once you've done this, from the root directory run the following script +Once you've done this, from the root directory run the following scripts + npm install npm run dev This will start both the client and server projects in dev mode on your machine diff --git a/client/package-lock.json b/client/package-lock.json index 51335697..d979f358 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,6 +14,7 @@ "cwg": "^0.2.1", "feathers-redux": "^3.0.0", "file-saver": "^2.0.0", + "html2canvas": "^1.4.1", "jspdf": "^2.3.1", "prop-types": "^15.7.2", "random-words": "^1.1.0", @@ -26,7 +27,7 @@ "react-dnd-touch-backend": "^0.8.1", "react-dom": "^16.8.4", "react-google-recaptcha": "^2.0.1", - "react-lineto": "^3.1.3", + "react-keyboard-event-handler": "^1.5.4", "react-loading-overlay": "^1.0.1", "react-password-strength": "^2.4.0", "react-redux": "^6.0.0", @@ -34,6 +35,7 @@ "react-router-dom": "^4.3.1", "react-router-redux": "^4.0.8", "react-scripts": "^3.2.0", + "react-xarrows": "^2.0.2", "react-youtube-playlist": "^2.2.8", "reactstrap": "^7.1.0", "redux": "^4.0.1", @@ -5420,21 +5422,11 @@ } }, "node_modules/css-line-break": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", - "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", - "optional": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "dependencies": { - "base64-arraybuffer": "^0.2.0" - } - }, - "node_modules/css-line-break/node_modules/base64-arraybuffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", - "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", - "optional": true, - "engines": { - "node": ">= 0.6.0" + "utrie": "^1.0.2" } }, "node_modules/css-loader": { @@ -8508,12 +8500,12 @@ } }, "node_modules/html2canvas": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz", - "integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==", - "optional": true, + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "dependencies": { - "css-line-break": "1.1.1" + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" }, "engines": { "node": ">=8.0.0" @@ -13972,6 +13964,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-keyboard-event-handler": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/react-keyboard-event-handler/-/react-keyboard-event-handler-1.5.4.tgz", + "integrity": "sha512-MSOxU/sQ5q9XWNHhXAJxzh4xVLZjKORGNC2Pzvx3qUo24TQeztGB0tq8oSArwX6vfKSIVijiw8wBmkN5pJOB4w==", + "peerDependencies": { + "prop-types": "15.x || 16.x", + "react": "15.x || 16.x" + } + }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -14333,6 +14334,23 @@ "react-dom": ">=15.0.0" } }, + "node_modules/react-xarrows": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-xarrows/-/react-xarrows-2.0.2.tgz", + "integrity": "sha512-tDlAqaxHNmy0vegW/6NdhoWyXJq1LANX/WUAlHyzoHe9BwFVnJPPDghmDjYeVr7XWFmBrVTUrHsrW7GKYI6HtQ==", + "dependencies": { + "@types/prop-types": "^15.7.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.com/donate?hosted_button_id=CRQ343F9VTRS8" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-youtube-playlist": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/react-youtube-playlist/-/react-youtube-playlist-2.2.8.tgz", @@ -16854,6 +16872,14 @@ "node": ">=6" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -17487,6 +17513,22 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, + "node_modules/utrie/node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -22686,20 +22728,11 @@ } }, "css-line-break": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", - "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", - "optional": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", "requires": { - "base64-arraybuffer": "^0.2.0" - }, - "dependencies": { - "base64-arraybuffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", - "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", - "optional": true - } + "utrie": "^1.0.2" } }, "css-loader": { @@ -25129,12 +25162,12 @@ } }, "html2canvas": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.7.tgz", - "integrity": "sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==", - "optional": true, + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", "requires": { - "css-line-break": "1.1.1" + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" } }, "htmlparser2": { @@ -29477,6 +29510,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-keyboard-event-handler": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/react-keyboard-event-handler/-/react-keyboard-event-handler-1.5.4.tgz", + "integrity": "sha512-MSOxU/sQ5q9XWNHhXAJxzh4xVLZjKORGNC2Pzvx3qUo24TQeztGB0tq8oSArwX6vfKSIVijiw8wBmkN5pJOB4w==", + "requires": {} + }, "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", @@ -29772,6 +29811,16 @@ "react-lifecycles-compat": "^3.0.4" } }, + "react-xarrows": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-xarrows/-/react-xarrows-2.0.2.tgz", + "integrity": "sha512-tDlAqaxHNmy0vegW/6NdhoWyXJq1LANX/WUAlHyzoHe9BwFVnJPPDghmDjYeVr7XWFmBrVTUrHsrW7GKYI6HtQ==", + "requires": { + "@types/prop-types": "^15.7.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + } + }, "react-youtube-playlist": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/react-youtube-playlist/-/react-youtube-playlist-2.2.8.tgz", @@ -31796,6 +31845,14 @@ } } }, + "text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "requires": { + "utrie": "^1.0.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -32287,6 +32344,21 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "requires": { + "base64-arraybuffer": "^1.0.2" + }, + "dependencies": { + "base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" + } + } + }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", diff --git a/client/package.json b/client/package.json index 5c30509c..4e5c5750 100644 --- a/client/package.json +++ b/client/package.json @@ -9,6 +9,7 @@ "cwg": "^0.2.1", "feathers-redux": "^3.0.0", "file-saver": "^2.0.0", + "html2canvas": "^1.4.1", "jspdf": "^2.3.1", "prop-types": "^15.7.2", "random-words": "^1.1.0", @@ -21,7 +22,7 @@ "react-dnd-touch-backend": "^0.8.1", "react-dom": "^16.8.4", "react-google-recaptcha": "^2.0.1", - "react-lineto": "^3.1.3", + "react-keyboard-event-handler": "^1.5.4", "react-loading-overlay": "^1.0.1", "react-password-strength": "^2.4.0", "react-redux": "^6.0.0", @@ -29,6 +30,7 @@ "react-router-dom": "^4.3.1", "react-router-redux": "^4.0.8", "react-scripts": "^3.2.0", + "react-xarrows": "^2.0.2", "react-youtube-playlist": "^2.2.8", "reactstrap": "^7.1.0", "redux": "^4.0.1", diff --git a/client/public/index.html b/client/public/index.html index c755b701..08b55932 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -27,8 +27,6 @@
-
- diff --git a/client/public/sounds/hint.wav b/client/public/sounds/hint.wav new file mode 100644 index 00000000..e451c5f2 Binary files /dev/null and b/client/public/sounds/hint.wav differ diff --git a/client/src/components/Accessibility.js b/client/src/components/Accessibility.js index 7ea24207..fc736a26 100644 --- a/client/src/components/Accessibility.js +++ b/client/src/components/Accessibility.js @@ -16,7 +16,7 @@ class Accessibility extends Component { */ handleVisualChange = (event) => { var state = {...this.props.accessibility}; - state.visual[event.target.id] = event.target.value===true?false:true; + state.visual[event.target.id] = event.target.checked; this.props.updateAccessibility(state); } @@ -27,7 +27,7 @@ class Accessibility extends Component { */ handlePhysicalChange = (event) => { var state = {...this.props.accessibility}; - state.physical[event.target.id] = event.target.value===true?false:true; + state.physical[event.target.id] = event.target.checked; this.props.updateAccessibility(state); } @@ -64,46 +64,46 @@ class Accessibility extends Component {
Red-Green Colour Blindness
- + Protanomaly: In males with protanomaly, the red cone photopigment is abnormal. Red, orange, and yellow appear greener and colors are not as bright. This condition is mild and doesn’t usually interfere with daily living. Protanomaly is an X-linked disorder estimated to affect 1 percent of males. - + Protanopia: In males with protanopia, there are no working red cone cells. Red appears as black. Certain shades of orange, yellow, and green all appear as yellow. Protanopia is an X-linked disorder that is estimated to affect 1 percent of males. - + Deuteranomaly: In males with deuteranomaly, the green cone photopigment is abnormal. Yellow and green appear redder and it is difficult to tell violet from blue. This condition is mild and doesn’t interfere with daily living. Deuteranomaly is the most common form of color blindness and is an X-linked disorder affecting 5 percent of males. - + Deuteranopia: In males with deuteranopia, there are no working green cone cells. They tend to see reds as brownish-yellow and greens as beige. Deuteranopia is an X-linked disorder that affects about 1 percent of males.
Blue-Yellow Colour Blindness
- + Tritanomaly: People with tritanomaly have functionally limited blue cone cells. Blue appears greener and it can be difficult to tell yellow and red from pink. Tritanomaly is extremely rare. It is an autosomal dominant disorder affecting males and females equally. - + Tritanopia: People with tritanopia, also known as blue-yellow color blindness, lack blue cone cells. Blue appears green and yellow appears violet or light grey. Tritanopia is an extremely rare autosomal recessive disorder affecting males and females equally.
Complete Colour Blindness
- + Cone Monochromacy: This rare form of color blindness results from a failure of two of the three cone cell photopigments to work. There is red cone monochromacy, green cone monochromacy, and blue cone monochromacy. People with cone monochromacy have trouble distinguishing colors because the brain needs to compare the signals from different types of cones in order to see color. When only one type of cone works, this comparison isn’t possible. People with blue cone monochromacy, may also have reduced visual acuity, near-sightedness, and uncontrollable eye movements, a condition known as nystagmus. Cone monochromacy is an autosomal recessive disorder. - + Rod Monochromacy: This type of monochromacy is rare and is the most severe form of color blindness. It is present at birth. None of the cone cells have functional photopigments. Lacking all cone vision, people with rod monochromacy see the world in black, white, and gray. And since rods respond to dim light, people with rod monochromacy tend to be photophobic – very uncomfortable in bright environments. They also experience nystagmus. Rod monochromacy is an autosomal recessive disorder. @@ -112,12 +112,12 @@ class Accessibility extends Component { - + If your design requires larger fonts as you are expecting players with issues with short sightedness, this is a suitable option to select - + If your design requires a contrasting colour scheme to be used to make text more readable, select this option @@ -142,12 +142,12 @@ class Accessibility extends Component { - + If your design is required to be wheelchair accessible select this option - + If your design is required to be wheelchair accessible select this option diff --git a/client/src/components/AccessibilityWarning.js b/client/src/components/AccessibilityWarning.js new file mode 100644 index 00000000..d8612e87 --- /dev/null +++ b/client/src/components/AccessibilityWarning.js @@ -0,0 +1,55 @@ +import React, {Component} from 'react'; +import { Container, Row, Col, UncontrolledTooltip } from 'reactstrap'; +import PropTypes from 'prop-types'; +import Accessibility from '../models/Accessibility'; + +/** + * Class for Not Found + * @extends Component + * @author Alistair Quinn + */ +class AccessibilityWarning extends Component { + /** + * Generates Warning from Checked Accessibility + */ + generateWarning = (accessibility) => { + let selectedKeys = []; + Object.keys(accessibility).forEach((key)=>{ + if(accessibility[key]===true) + selectedKeys.push(key); + }); + return selectedKeys.join(',') + } + + /** + * React lifecycle method + * Renders Layout + * @returns {JSX} + */ + render() { + return ( + + + +

+ + You selected: {this.generateWarning(this.props.accessibility.visual)} be careful with colour choices + + + +

+ + You selected: {this.generateWarning(this.props.accessibility.physical)} ensure plenty of room around puzzles and check how difficult puzzle is to handle + + +
+
+ ); + } +}; + +AccessibilityWarning.propTypes = { + accessibility: PropTypes.instanceOf(Accessibility), +} + +export default AccessibilityWarning; \ No newline at end of file diff --git a/client/src/components/AreaDnDSource.js b/client/src/components/AreaDnDSource.js index 2b86d585..6a74aa6c 100644 --- a/client/src/components/AreaDnDSource.js +++ b/client/src/components/AreaDnDSource.js @@ -107,7 +107,7 @@ class AreaDnDSource extends Component{ * Removes a component * @function */ - removeComponent = ()=>{ + removeComponent = () => { this.props.removeComponent(this.props.component._id); } @@ -117,21 +117,11 @@ class AreaDnDSource extends Component{ * @param {Component} component * @param {string} parentId */ - addComponent = (component,parentId)=>{ + addComponent = (component,parentId) => { this.props.addComponent(component,this.props.component._id); this.props.addRelationship(component._id,parentId); } - /** - * React Lifecycle called when updated - * @function - * @param {object} prevProps - */ - componentDidUpdate(prevProps){ - if(this.props.renderTrigger!==prevProps.renderTrigger) - this.forceUpdate(); - } - /** * React Lifecycle Method * Renders Layout @@ -161,7 +151,6 @@ AreaDnDSource.propTypes = { removeComponent: PropTypes.func, addComponent: PropTypes.func, addRelationship: PropTypes.func, - renderTrigger: PropTypes.string, findComponent: PropTypes.func, handleComponentClick: PropTypes.func, outputComponents: PropTypes.array, diff --git a/client/src/components/AreaDnDTarget.js b/client/src/components/AreaDnDTarget.js index c437f0fc..77c19e6f 100644 --- a/client/src/components/AreaDnDTarget.js +++ b/client/src/components/AreaDnDTarget.js @@ -62,7 +62,7 @@ class AreaDnDTarget extends Component { /** Creates AreaDnDTarget */ constructor(){ super(); - this.state = { width: 0, height: 0}; + this.state={width:window.innerWidth}; } /** @@ -83,17 +83,16 @@ class AreaDnDTarget extends Component { } updateScreenDimensions = () => { - this.setState({ width: window.innerWidth, height: window.innerHeight }); + this.setState({ width: window.innerWidth }); } /** * Adds new components and updates existing * @param {Component} item - * @param {bool} isInput */ - handleComponentDrop(item,isInput=false){ - var component = null; - if (item.id!==undefined && item._id === undefined){ + handleComponentDrop(item){ + let component = null; + if (item.id !== undefined){ switch(item.id){ case 'Puzzle': component = new Puzzle(); @@ -146,7 +145,7 @@ class AreaDnDTarget extends Component { {title} {this.props.outputComponents.map((component,i)=>{ component = this.props.findComponent(component._id); - return() + return() })}
diff --git a/client/src/components/BusinessLogic.js b/client/src/components/BusinessLogic.js index e281df94..1bd6e5bc 100644 --- a/client/src/components/BusinessLogic.js +++ b/client/src/components/BusinessLogic.js @@ -1,7 +1,7 @@ import React, {Component} from 'react'; import { Route, Switch } from 'react-router'; import { Redirect } from 'react-router-dom'; -import { Dashboard, EscapeRoomDesigner, Login, Signup, Tutorials, About, ConditionalRoute, NotFound, VerifyToken, SendVerify, ResetToken, SendReset } from './index.js'; +import { Dashboard, EscapeRoomDesigner, EscapeRoomRunner, Login, Signup, Tutorials, About, ConditionalRoute, NotFound, VerifyToken, SendVerify, ResetToken, SendReset, TermsOfService } from './index.js'; import EscapeRoom from '../models/EscapeRoom.js'; import PropTypes from 'prop-types'; @@ -49,17 +49,29 @@ class BusinessLogic extends Component { this.props.redux.actions.escapeRoom.setSelectedEscapeRoom(escapeRoom); this.props.history.push('/designer'); } + + /** + * Opens escape room designer + * @param {EscapeRoom} escapeRoom + */ + runEscapeRoom = (escapeRoom) => { + this.props.redux.actions.escapeRoom.setSelectedEscapeRoom(escapeRoom); + this.props.history.push('/runner'); + } /** * Creates a new escape room * then opens designer */ - newEscapeRoom = async() => { + newEscapeRoom = async(newEscapeRoom) => { const userId = this.props.redux.state.user._id; - const newEscapeRoom = new EscapeRoom(userId); + if(newEscapeRoom == null) + newEscapeRoom = new EscapeRoom(userId); + else + newEscapeRoom.userId = userId; let response = await this.props.services['escape-rooms'].create(newEscapeRoom); if(response.action.type.includes('FULFILLED')){ - const escapeRoom = response.value; + const escapeRoom = EscapeRoom.convert(response.value); if (escapeRoom!==null){ this.props.redux.actions.escapeRooms.addEscapeRoom(escapeRoom); this.props.redux.actions.escapeRoom.setSelectedEscapeRoom(escapeRoom); @@ -152,8 +164,9 @@ class BusinessLogic extends Component { return ( - ()}/> + ()}/> 0 && escapeRoom!==undefined && loggedIn} redirect={'/'} render={(routeProps) =>()}/> + 0 && escapeRoom!==undefined && loggedIn} redirect={'/'} render={(routeProps) =>()}/> ()}/> ()}/> @@ -162,6 +175,7 @@ class BusinessLogic extends Component { ()}/> ()}/> ()}/> + ) diff --git a/client/src/components/ComponentArranger.js b/client/src/components/ComponentArranger.js index 15d54b8a..fce18843 100644 --- a/client/src/components/ComponentArranger.js +++ b/client/src/components/ComponentArranger.js @@ -2,10 +2,10 @@ import React, {Component} from 'react'; import { Row, Col } from 'reactstrap'; import '../styles/Component.css'; import '../styles/ComponentArranger.css'; -import Area from './AreaDnDSource'; +import AreaDnDSource from './AreaDnDSource'; import AreaModel from '../../../client/src/models/Area'; import { DropTarget } from 'react-dnd'; -import LineTo from 'react-lineto'; +import Xarrow from "react-xarrows"; import PropTypes from 'prop-types'; /** @@ -56,12 +56,6 @@ function collect(connect, monitor) { * @author Alistair Quinn */ class ComponentArranger extends Component { - /** Creates ComponentArranger */ - constructor(){ - super() - this.state = {lines:[]} - } - /** * Maps Area to a Row * @function @@ -77,7 +71,7 @@ class ComponentArranger extends Component { return ( - + ) @@ -85,32 +79,12 @@ class ComponentArranger extends Component { } /** - * React Lifecycle called on Update - * @param {object} props - * @param {object} state + * Finds a component by ID + * @param {string} id + * @returns {Component} */ - componentDidUpdate(props,state) { - if(JSON.stringify(this.props.components)!==JSON.stringify(props.components)){ - this.forceUpdate(); - } - } - - /** Calls force update */ - update = () => this.forceUpdate() - - /** React Lifecycle called when component Mounts */ - componentDidMount() { - setTimeout(()=>{ - this.forceUpdate(); - },100) - window.addEventListener('scroll', this.update, true); - window.addEventListener('resize', this.update); - } - - /** React Lifecycle called when component unmounts */ - componentWillUnmount() { - window.removeEventListener('scroll', this.update); - window.removeEventListener('resize', this.update); + findComponent = (id) => { + return this.props.components.find(component=>component._id===id); } /** @@ -130,41 +104,40 @@ class ComponentArranger extends Component { let inputComponents = component.inputComponents; let outputComponents = component.outputComponents; for(let inputComponent of inputComponents){ - inputComponent = this.props.findComponent(inputComponent); - lines.push(); + inputComponent = this.findComponent(inputComponent); + lines.push(); }; for(let outputComponent of outputComponents){ - outputComponent = this.props.findComponent(outputComponent); - lines.push() + outputComponent = this.findComponent(outputComponent); + lines.push(); }; } }; return this.props.connectDropTarget(
+ + + +

Components

+ +
+ + + {this.props.components.map(this.mapAreas)} + + + {lines} - - - -

Components

- -
- - - {this.props.components.map(this.mapAreas)} - - -
) } }; ComponentArranger.propTypes = { - renderTrigger: PropTypes.string, + selected: PropTypes.instanceOf(Component), components: PropTypes.array, isOver: PropTypes.bool, canDrop: PropTypes.bool, - findComponent: PropTypes.func, showModal: PropTypes.func, handleComponentClick: PropTypes.func, updateComponent: PropTypes.func, diff --git a/client/src/components/ComponentDetails.js b/client/src/components/ComponentDetails.js index 78294fc4..dd14ed19 100644 --- a/client/src/components/ComponentDetails.js +++ b/client/src/components/ComponentDetails.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; -import { Container, Row, Col, Input, Label, UncontrolledTooltip, ListGroupItem, Button, ListGroup } from 'reactstrap'; -import { LockGenerator, PuzzleGenerator } from '../../../client/src/components/index'; +import { Container, Row, Col } from 'reactstrap'; +import { AccessibilityWarning, Properties, Relationships } from './index'; import PropTypes from 'prop-types'; import '../styles/ComponentDetails.css'; import Accessibility from './Accessibility'; @@ -11,210 +11,6 @@ import Accessibility from './Accessibility'; * @author Alistair Quinn */ class ComponentDetails extends Component { - /** Creates ComponentDetails */ - constructor(){ - super(); - this.state={visualWarning:false,physicalWarning:false} - } - - /** - * Converts a string to Camel Case - * @param {string} string - * @returns {string} - */ - convertCamelCase(string){ - if(typeof string !== 'string') - string = string.toString(); - return string.replace(/([A-Z])/g, ' $1').replace(/^./,(str)=>{ return str.toUpperCase(); }) - } - - /** - * Handles input change - * @param {Event} event - * */ - handleChange = (event) => { - let state = {}; - state[event.target.id] = event.target.value; - if(event.target.id==="puzzleType") - state.puzzle = {} - if(event.target.id==="lockType") - state.output = "" - state._id = this.props.selected._id; - this.props.updateComponent(state); - } - - /** - * Handles Output change - * @param {string} event - */ - handleOutputChange = (output)=>{ - this.props.updateComponent({output}); - } - - /** - * Handles Puzzle change - * @param {Puzzle} puzzle - */ - handlePuzzleChange = (puzzle)=>{ - if(puzzle.output===undefined){ - puzzle.output = ""; - } - this.props.updateComponent({_id:this.props.selected._id,puzzle,output:puzzle.output}); - this.forceUpdate(); - } - - /** Generates Output from Inputs */ - generateFromInputs=()=>{ - this.props.updateComponent({_id:this.props.selected._id,output:this.props.calculateOutput(this.props.selected._id)}) - } - - /** - * Maps Details to Inputs - * @param {string} key - * @param {int} i index - * @returns {JSX} - */ - mapDetailToInput = (key,i) => { - if(key==='output') { - let generator; - if(this.props.selected.type==='Lock') - generator = - else if(this.props.selected.type==='Puzzle') { - generator = - } - return ( - - - - -
- {generator} -
- -
) - } else if (key==='lockType'){ - return ( - - - - - - - - - - - - - ) - } else if (key==='eventType'){ - return ( - - - - - - - - - - - - ) - } else if (key==='puzzleType'){ - return ( - - - - - - - - - - - - - - - ) - } else if (key==='puzzle'){ - let component = this.props.selected; - let details = Object.keys(component[key]).map((property,index,array)=>{ - let detail; - if(property.includes('DATA')){ - detail = "Export as PDF to View"; - }else if(typeof component[key][property] === 'object') - detail = JSON.stringify(component[key][property]); - else{ - detail = component[key][property]; - } - return( - - -

{" " + this.convertCamelCase(property) + ": "+this.convertCamelCase(detail)}

- -
- ) - }); - if(Object.keys(component[key]).length>0){ - return ( - - - - {details} - - - ) - }else{ - return null; - } - }else if(typeof this.props.selected[key] === "string" && key!=="_id" && key!=="type" && key!=="version"){ - return ( - - - - - - ) - } - } - - /** - * Deletes a relationship - * @param {string} id - * @param {bool} isInput - */ - handleOnClick = (id,isInput)=> (e) => { - let component = {...this.props.selected}; - let state = {}; - state._id = component._id; - if(isInput){ - state.inputComponents = component.inputComponents.filter(oldId => oldId!==id); - } else { - state.outputComponents = component.outputComponents.filter(oldId => oldId!==id); - } - this.props.updateComponent(state); - } - - /** - * Maps Relationship to List Group Item - * @param {string} id - * @param {int} i - * @param {bool} isInput - * @returns {JSX} - */ - mapIDToP = (id,i,isInput) => { - return ( - - {id} - - - ) - } - /** * React Lifecycle Render * @returns {JSX} @@ -223,91 +19,34 @@ class ComponentDetails extends Component { let component = this.props.selected; let id=""; let type=""; - let properties; - let inputs; - let outputs; - let inputRelationships; - let outputRelationships; - let visualWarning; - let physicalWarning; - if(component!==undefined || component!==null){ + if(component!==undefined && component!==null){ + // Properties if(component._id!==undefined) id = " ("+component._id+")"; - else - id=""; - properties = Object.keys(component).map(this.mapDetailToInput) type = component.type||""; - if(component.type!=='Area'&&component.inputComponents!==undefined&&component.outputComponents!==undefined&&(component.inputComponents.length>0||component.outputComponents.length>0)){ - inputs = component.inputComponents.map((id,i)=>this.mapIDToP(id,i,true)); - outputs = component.outputComponents.map((id,i)=>this.mapIDToP(id,i,false)); - inputRelationships = ( - - -

Inputs

- - {inputs} - - -
- ); - outputRelationships = ( - - -

Outputs

- - {outputs} - - -
- ); - } - //Accessibility - let visualKeys = []; - Object.keys(this.props.accessibility.visual).forEach((key)=>{ - if(this.props.accessibility.visual[key]===true) - visualKeys.push(key); - }); - if(visualKeys.length>0) - visualWarning = ( - -

- - You selected: {visualKeys.join(',')} be careful with colour choices - - - ) - let physicalKeys = []; - Object.keys(this.props.accessibility.physical).forEach((key)=>{ - if(this.props.accessibility.physical[key]===true) - physicalKeys.push(key); - }); - if(physicalKeys.length>0) - physicalWarning = ( - -

- - You selected: {physicalKeys.join(',')} ensure plenty of room around puzzles and check how difficult puzzle is to handle - - - ) } - return ( - - + if(this.props.selected===null) + return ( +

Details

-

{type + id}

-
- - {visualWarning} - {physicalWarning} - - {properties} - {inputRelationships} - {outputRelationships} -
- ) + + ) + else + return ( + + + +

Details

+

{type + id}

+ +
+ + + +
+ ) } }; diff --git a/client/src/components/ComponentDnDSource.js b/client/src/components/ComponentDnDSource.js index 7fb675ba..459e01cc 100644 --- a/client/src/components/ComponentDnDSource.js +++ b/client/src/components/ComponentDnDSource.js @@ -65,7 +65,7 @@ const componentSource = { endDrag(props, monitor, component) { if (!monitor.didDrop()) { if((props.component!==undefined||props.component!==null)&&props.showModal) - props.showModal(new Modal("Warning", "Are you sure you want to delete this component?","Yes",component.removeComponent,"No",()=>{})); + props.showModal(new Modal("Warning", "Are you sure you want to delete this component?","Yes",()=>(component.props.removeComponent(props.component._id)),"No",()=>{})); return; } } @@ -93,63 +93,12 @@ function collect(connect, monitor) { * @author Alistair Quinn */ class ComponentDnDSource extends Component{ - /** Removes a component */ - removeComponent = ()=>{ - this.props.removeComponent(this.props.component._id); - } - - /** - * React Lifecycle called after component udpates - * @param {object} prevProps - */ - componentDidUpdate(prevProps){ - if(this.props.renderTrigger!==prevProps.renderTrigger) - this.forceUpdate(); - } - - /** - * Finds a component - * @param {Component} component - */ - findComponent(component){ - if(this.props.findComponent!==undefined){ - return this.props.findComponent(component); - } else { - return null; - } - } - - /** Calls forceUpdate */ - update = () => this.forceUpdate() - - /** React Lifecycle called when Component did Mount */ - componentDidMount() { - window.addEventListener('click', this.update, true); - window.addEventListener('scroll', this.update, true); - window.addEventListener('resize', this.update); - } - - /** React Lifecycle called when Component did Mount */ - componentWillUnmount() { - window.removeEventListener('click', this.update); - window.removeEventListener('scroll', this.update); - window.removeEventListener('resize', this.update) - } - /** * React Lifecycle Render * @returns {JSX} */ render() { - var target; - if (this.props.isTarget){ - target = ( - - - - - ); - } + let id=`${this.props.component._id}`; var style = {}; let classNames = "component container-fluid"; if(this.props.component!==undefined){ @@ -158,7 +107,7 @@ class ComponentDnDSource extends Component{ classNames += " " + this.props.component.type + " " + this.props.component._id; } return this.props.connectDragSource( -
+

{this.props.component.type}

@@ -170,7 +119,10 @@ class ComponentDnDSource extends Component{

{this.props.component.name}

- {target} + + + +
) } @@ -178,10 +130,7 @@ class ComponentDnDSource extends Component{ ComponentDnDSource.propTypes = { removeComponent: PropTypes.func, - renderTrigger: PropTypes.string, component: PropTypes.instanceOf(Component), - findComponent: PropTypes.func, - isTarget: PropTypes.bool, handleComponentClick: PropTypes.func, showModal: PropTypes.func, addComponent: PropTypes.func, diff --git a/client/src/components/ComponentDnDTarget.js b/client/src/components/ComponentDnDTarget.js index 483e37ba..4fdd34c8 100644 --- a/client/src/components/ComponentDnDTarget.js +++ b/client/src/components/ComponentDnDTarget.js @@ -1,7 +1,6 @@ import React, {Component} from 'react'; import { Card, CardBody, UncontrolledTooltip } from 'reactstrap'; import { DropTarget } from 'react-dnd'; -import { Area, Puzzle, Event, Music, Lock, Prop } from '../../../client/src/models/index'; import PropTypes from 'prop-types'; import '../styles/Component.css'; @@ -59,33 +58,8 @@ class ComponentDnDTarget extends Component { * @param {bool} isInput */ handleComponentDrop(item,isInput=true){ - var component = null; - if (item.id!==undefined){ - switch(item.id){ - case 'Puzzle': - component = new Puzzle(); - break; - case 'Lock': - component = new Lock(); - break; - case 'Event': - component = new Event(); - break; - case 'Music': - component = new Music(); - break; - case 'Prop': - component = new Prop(); - break; - default: - return; - } - this.props.addComponent(component,this.props.component._id); - }else { - component = item; - if(component._id!==this.props.component._id) - this.props.addRelationship(component._id,this.props.component._id,isInput); - } + if(item._id!==this.props.component._id) + this.props.addRelationship(item._id,this.props.component._id,isInput); } /** @@ -93,7 +67,7 @@ class ComponentDnDTarget extends Component { * @returns {JSX} */ render() { - let id=this.props.component._id; + let id=`${this.props.component._id}-${this.props.isInput}-Target`; var classNames = "hide-border"; if(this.props.isOver && this.props.canDrop){ classNames+=" canDrop"; @@ -114,7 +88,7 @@ class ComponentDnDTarget extends Component { tooltip = "Drag another component to this green square to add it as an output of this component" } return this.props.connectDropTarget( -
+
@@ -128,8 +102,7 @@ class ComponentDnDTarget extends Component { ComponentDnDTarget.propTypes = { addComponent: PropTypes.func, - component: PropTypes.instanceOf(Area), - addRelationship: PropTypes.func, + component: PropTypes.instanceOf(Component), isOver: PropTypes.bool, canDrop: PropTypes.bool, isInput: PropTypes.bool, diff --git a/client/src/components/Dashboard.js b/client/src/components/Dashboard.js index bb1d71c4..85158c03 100644 --- a/client/src/components/Dashboard.js +++ b/client/src/components/Dashboard.js @@ -1,7 +1,9 @@ import React, {Component} from 'react'; import { Container, Row, Col, Dropdown, DropdownToggle, DropdownMenu, DropdownItem , ListGroup, ListGroupItem , Button } from 'reactstrap'; +import ComponentArranger from "./ComponentArranger"; import { saveAs } from 'file-saver'; import Modal from '../../../client/src/models/Modal'; +import html2canvas from 'html2canvas'; import {escapeRoomToPDF} from '../../../client/src/pdf/pdf'; import PropTypes from 'prop-types'; @@ -15,8 +17,11 @@ class Dashboard extends Component { constructor(){ super(); this.state = { - dropdownOpen: [false,false] + dropdownOpen: [false,false], + escapeRoom: {components:[]}, } + this.InputFile = React.createRef(); + this.ComponentArranger = React.createRef(); } /** @@ -37,8 +42,17 @@ class Dashboard extends Component { * @param {Event} e */ handleClick = async (e) => { - if(this.props.newEscapeRoom) - this.props.newEscapeRoom(); + switch(e.target.id){ + case "newButton": + if(this.props.newEscapeRoom) + this.props.newEscapeRoom(); + break; + case "importButton": + this.InputFile.current.click(); + break; + default: + return; + } } /** @@ -54,8 +68,17 @@ class Dashboard extends Component { * Saves an Escape Room as PDF * @param {EscapeRoom} escapeRoom */ - savePDF(escapeRoom) { - escapeRoomToPDF(escapeRoom); + async savePDF(escapeRoom) { + let components = await this.convertComponentsToDataURL(); + escapeRoomToPDF(escapeRoom, components); + } + + async convertComponentsToDataURL() { + let canvas; + this.ComponentArranger.current.style.display = "block"; + canvas = await html2canvas(this.ComponentArranger.current); + this.ComponentArranger.current.style.display = "none"; + return canvas.toDataURL("image/png"); } /** @@ -67,26 +90,61 @@ class Dashboard extends Component { */ handleItemClick = (i, action) => (e) => { const escapeRoom = this.props.escapeRooms[i]; - switch(action){ - case 'EDIT': - if(this.props.editEscapeRoom) - this.props.editEscapeRoom(escapeRoom); - break; - case 'JSON': - this.saveJSON(escapeRoom); - break; - case 'PDF': - this.savePDF(escapeRoom); - break; - case 'DELETE': - if(this.props.deleteEscapeRoom) - this.props.showModal(new Modal("Warning","Are you sure you want to delete "+escapeRoom.details.name+"?","Yes",()=>{this.props.deleteEscapeRoom(escapeRoom)},"No",()=>{})); - break; - default: - return; + this.setState({escapeRoom},() => { + switch(action){ + case 'EDIT': + if(this.props.editEscapeRoom) + this.props.editEscapeRoom(escapeRoom); + break; + case 'RUN': + if(this.props.runEscapeRoom) + this.props.runEscapeRoom(escapeRoom); + break; + case 'JSON': + this.saveJSON(escapeRoom); + break; + case 'PDF': + this.savePDF(escapeRoom); + break; + case 'JSONANDPDF': + this.saveJSON(escapeRoom); + this.savePDF(escapeRoom); + break; + case 'DELETE': + if(this.props.deleteEscapeRoom) + this.props.showModal(new Modal("Warning","Are you sure you want to delete "+escapeRoom.details.name+"?","Yes",()=>{this.props.deleteEscapeRoom(escapeRoom)},"No",()=>{})); + break; + default: + return; + } + }); + } + + /** + * Handles File Input Change + * @param {Event} e + */ + handleChange = async(e) => { + const { files } = e.target; + if (files && files.length === 1) { + let escapeRoom = await this.convertJSONFileToEscapeRoom(files[0]); + if(this.props.newEscapeRoom != null) + this.props.newEscapeRoom(escapeRoom); } } + /** + * Reads JSON File and Converts to Object + * @param {File} file + * @returns {EscapeRoom} + */ + convertJSONFileToEscapeRoom = async(file) => { + let text = await file.text(); + let escapeRoom = JSON.parse(text); + delete escapeRoom._id; // Delete existing id so a new one is generated + return escapeRoom; + } + /** * Maps Escape Rooms to List Item * @function @@ -99,9 +157,11 @@ class Dashboard extends Component { - Edit + Edit + Run Export as JSON Export as PDF + Export as JSON and PDF Delete @@ -114,13 +174,19 @@ class Dashboard extends Component { */ render() { const escapeRooms = this.props.escapeRooms; + const empty = function(){} return ( - + + + + + +
@@ -128,17 +194,30 @@ class Dashboard extends Component { + + +
+ +
+ +
) } }; Dashboard.propTypes = { - newEscapeRoom: PropTypes.func, escapeRooms: PropTypes.array, + showModal: PropTypes.func, editEscapeRoom: PropTypes.func, + newEscapeRoom: PropTypes.func, deleteEscapeRoom: PropTypes.func, - showModal: PropTypes.func, + runEscapeRoom: PropTypes.func, } export default Dashboard; \ No newline at end of file diff --git a/client/src/components/Design.js b/client/src/components/Design.js index bcd2607b..a0ec06eb 100644 --- a/client/src/components/Design.js +++ b/client/src/components/Design.js @@ -13,7 +13,7 @@ class Design extends Component { /** Creates Design */ constructor(){ super(); - this.state = {selected: {}}; + this.state = {selected: null}; } /** @@ -37,15 +37,6 @@ class Design extends Component { this.setState({selected:{...this.state.selected,...component}}); } - /** - * Finds a component by ID - * @param {string} id - * @returns {Component} - */ - findComponent = (id) => { - return this.props.components.components.find(component=>component._id===id); - } - /** * React Lifecycle Render * @returns {JSX} @@ -58,7 +49,7 @@ class Design extends Component { - + @@ -72,7 +63,6 @@ class Design extends Component { Design.propTypes = { updateComponent: PropTypes.func, components: PropTypes.array, - findComponent: PropTypes.func, showModal: PropTypes.func, handleComponentClick: PropTypes.func, addComponent: PropTypes.func, diff --git a/client/src/components/Details.js b/client/src/components/Details.js index 4fdd0228..e7245151 100644 --- a/client/src/components/Details.js +++ b/client/src/components/Details.js @@ -45,14 +45,14 @@ class Details extends Component { - - + + The maximum and minimum amount of players your room is designed for - - Can be the time players have to try and escape by, or the estimated time it will take players to complete the room + + Target Time for the room in whole mintues. So a 1 hour room would be 60 @@ -63,6 +63,21 @@ class Details extends Component {
+ + + + The objective of the room, for example: retrieve the diamond, foil the mad scientists evil plans ect. + + + + + URL of an image that represents the room. + + + + + URL of background music that accompanies the room. + diff --git a/client/src/components/EscapeRoomDesigner.js b/client/src/components/EscapeRoomDesigner.js index a6cc0f90..8b1adc4a 100644 --- a/client/src/components/EscapeRoomDesigner.js +++ b/client/src/components/EscapeRoomDesigner.js @@ -1,10 +1,8 @@ import React, {Component} from 'react'; -import { Container, Dropdown, DropdownToggle , DropdownMenu , DropdownItem , Row, Col, Nav, NavItem, NavLink, TabContent, TabPane , Button } from 'reactstrap'; -import { Details, Accessibility, Design } from '../../../client/src/components/index'; +import { Container, Row, Col, Nav, NavItem, NavLink, TabContent, TabPane , Button } from 'reactstrap'; +import { Details, Accessibility, Design } from './index'; import classnames from 'classnames'; -import { saveAs } from 'file-saver'; -import {escapeRoomToPDF} from '../../../client/src/pdf/pdf'; -import EscapeRoom from '../../../client/src/models/EscapeRoom'; +import EscapeRoom from '../models/EscapeRoom'; import PropTypes from 'prop-types'; import '../styles/EscapeRoomDesigner.css'; @@ -17,24 +15,7 @@ class EscapeRoomDesigner extends Component { /** Creates EscapeRoom Designer */ constructor(){ super() - this.state = {activeTab:'design', dropdownOpen: false}; - } - - /** - * Saves an EscapeRoom as JSON - * @param {EscapeRoom} escapeRoom - */ - saveJSON(escapeRoom) { - const blob = new Blob([JSON.stringify(escapeRoom)],{type:'text/plain;charset=utf-8'}); - saveAs(blob, escapeRoom.details.name+".json"); - } - - /** - * Saves Escape Room as PDF - * @param {EscapeRoom} escapeRoom - */ - savePDF(escapeRoom) { - escapeRoomToPDF(escapeRoom); + this.state = {activeTab:'design'}; } /** @@ -44,32 +25,8 @@ class EscapeRoomDesigner extends Component { * @param {Event} e */ handleClick = (action) => (e) => { - switch(action){ - case 'EXIT': - if(this.props.saveEscapeRoom) - this.props.saveEscapeRoom(this.props.escapeRoom); - break; - case 'JSON': - if(this.props.saveEscapeRoom) - this.props.saveEscapeRoom(this.props.escapeRoom); - this.saveJSON(this.props.escapeRoom); - break; - case 'PDF': - if(this.props.saveEscapeRoom) - this.props.saveEscapeRoom(this.props.escapeRoom); - this.savePDF(this.props.escapeRoom); - break; - default: - return; - } - } - - /** - * Toggles Bool - * @param {Event} e - */ - handleToggle = (e) => { - this.setState({dropdownOpen: !this.state.dropdownOpen}); + if(this.props.saveEscapeRoom) + this.props.saveEscapeRoom(this.props.escapeRoom); } /** @@ -83,39 +40,7 @@ class EscapeRoomDesigner extends Component { activeTab: tab }) } - } - - /** React Lifecycle Called when Component Mounts */ - componentDidMount(){ - const escapeRoom = this.props.escapeRoom; - if(escapeRoom===undefined){ - this.props.history.push('/'); - } - } - - /** React Lifecycle Called when Components Updates */ - componentDidUpdate(prevProps,prevState){ - if(prevState.activeTab!==this.state.activeTab){ - this.toggleSvgs(); - } - } - - /** - * Hides SVGs when design not current tab - * @function - */ - toggleSvgs=()=>{ - let lines = document.querySelectorAll("body > div:not(#root)"); - if(this.state.activeTab!=="design"){ - for (let i = 0; i < lines.length;i++){ - lines[i].style.display = 'none'; - } - } else { - for (let i = 0; i < lines.length;i++){ - lines[i].style.display = 'block'; - } - } - } + } /** * Calculates Output of Component @@ -132,17 +57,9 @@ class EscapeRoomDesigner extends Component { return ( - - - - Save and Export - - Export as JSON - Export as PDF - - + + - @@ -184,7 +101,7 @@ class EscapeRoomDesigner extends Component { - + @@ -197,7 +114,6 @@ class EscapeRoomDesigner extends Component { EscapeRoomDesigner.propTypes = { saveEscapeRoom: PropTypes.func, escapeRoom: PropTypes.instanceOf(EscapeRoom), - history: PropTypes.object, updateDetails: PropTypes.func, updateAccessibility: PropTypes.func, addComponent: PropTypes.func, diff --git a/client/src/components/EscapeRoomRunner.js b/client/src/components/EscapeRoomRunner.js new file mode 100644 index 00000000..72f4a7b9 --- /dev/null +++ b/client/src/components/EscapeRoomRunner.js @@ -0,0 +1,126 @@ +import React, {Component} from 'react'; +import { Container, Row, Col, Input } from 'reactstrap'; +import KeyboardEventHandler from 'react-keyboard-event-handler'; +import PropTypes from 'prop-types'; + +/** + * Class for Pallet + * @extends Component + * @author Alistair Quinn + */ +class EscapeRoomRunner extends Component { + constructor(props){ + super(); + this.state = {paused:true, time:props.time * 60}; // Change time from minutes to seconds + this.music = new Audio(props.music); + this.music.loop = true; + this.hintSound = new Audio(props.hint); + } + + /** + * React Lifecycle Methods + */ + componentWillUnmount(){ + this.music.pause(); + } + + /** + * Handles Timer Toggle + */ + handleTimerToggle = ()=>{ + if(this.state.paused){ + this.timerInterval = setInterval(this.timer, 1000); + this.music.play(); + } + else { + clearInterval(this.timerInterval); + this.music.pause(); + } + this.setState({paused:!this.state.paused}); + } + + /** + * Decrements timer by 1 second + */ + timer = () => { + if(this.state.time>0) + this.setState({time:this.state.time-1}); + else + this.setState({paused:true}); + } + + /** + * Handles Timer Toggle + */ + handleHintSound = ()=>{ + this.hintSound.play(); + } + + /**M + * Handles Timer Toggle + */ + handleMusicMute = ()=>{ + this.music.muted = !this.music.muted; + } + + /** + * React lifecycle method + * Renders layout + * @returns {JSX} + */ + render() { + // Get Time in a better format for display + let date = new Date(null); + date.setSeconds(this.state.time); + return ( + + + +

{this.props.name}

+ +
+ + + + {date.toISOString().substr(11, 8)} + + + + + + + + + this.handleTimerToggle()}/> + this.handleHintSound()}/> + this.handleMusicMute()}/> +
+ ) + } +}; + +EscapeRoomRunner.propTypes = { + name: PropTypes.string, + time: PropTypes.number, + background: PropTypes.string, + music: PropTypes.string, + hint: PropTypes.string, +} + +EscapeRoomRunner.defaultProps = { + name:"Unnamed", + time: 60, + background:"/images/backgrounds/main.jpg", + music:"https://cdn.bensound.com/bensound-wildwildtown.mp3", + hint:"/sounds/hint.wav" +} + +export default EscapeRoomRunner; \ No newline at end of file diff --git a/client/src/components/ImageManager.js b/client/src/components/ImageManager.js new file mode 100644 index 00000000..f9d4e3f5 --- /dev/null +++ b/client/src/components/ImageManager.js @@ -0,0 +1,128 @@ +import React, {Component} from 'react'; +import { Container, Row, Col, Button, ListGroup, ListGroupItem, Label } from 'reactstrap'; +import PropTypes from 'prop-types'; + +/** + * Class for Not Found + * @extends Component + * @author Alistair Quinn + */ +class ImageManager extends Component { + constructor(props){ + super(props); + this.state={images:props.images}; + this.InputFile = React.createRef(); + } + + /** + * Adds a string to items + * @function + * @param {Event} e + */ + addImage = (image) => { + let images = [...this.state.images]; + images.push(image); + this.setState({images}); + this.props.handleChange(images); + } + + /** + * Handles Button Click + */ + handleClick = (e) => { + this.InputFile.current.click(); + } + + /** + * Removes an item from the + * @param {Event} e + */ + removeImage = (index) => (e) => { + let images = [...this.state.images]; + images = [...images.slice(0,index),...images.slice(index+1)]; + this.setState({images}); + this.props.handleChange(images); + } + + /** + * Maps items to Inputs + * @param {string} item + * @param {int} index + * @param {Array} array + * @returns {JSX} + */ + mapImageToListItem = (item,index,array)=>{ + console.log(item); + return ( + + + + + ) + } + + /** + * Handles File Input Change + * @param {Event} e + */ + handleChange = async(e) => { + const { files } = e.target; + if (files && files.length === 1) { + try{ + let fileUri = await this.imageToBase64(files[0]); + this.addImage(fileUri); + } catch(error){ + + } + } + } + + /** + * Converts Image to Base 64 URI + * https://stackoverflow.com/questions/36280818/how-to-convert-file-to-base64-in-javascript + * @param {File} file + * @returns + */ + imageToBase64 = (file) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => resolve(reader.result); + reader.onerror = error => reject(error); + }); + + /** + * React lifecycle method + * Renders Layout + * @returns {JSX} + */ + render() { + return ( + + + + + + + + + + + + + {this.state.images.map(this.mapImageToListItem)} + + + + + ) + } +}; + +ImageManager.propTypes = { + images:PropTypes.array, + handleChange:PropTypes.func, +} + +export default ImageManager; \ No newline at end of file diff --git a/client/src/components/Main.js b/client/src/components/Main.js index 88080bfe..4b0f12bf 100644 --- a/client/src/components/Main.js +++ b/client/src/components/Main.js @@ -4,6 +4,7 @@ import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Navbar, NavbarBrand import LoadingOverlay from 'react-loading-overlay'; import Profile from '../components/Profile'; import BusinessLogic from './BusinessLogic.js'; +import EscapeRoom from '../models/EscapeRoom'; import PropTypes from 'prop-types'; import '../styles/Main.css'; @@ -76,6 +77,8 @@ class Main extends Component { let result = await this.props.services['escape-rooms'].find({query:{userId:userId}}); if(result.action.type.includes('FULFILLED')){ const escapeRooms = result.value.data; + for(let i = 0; i < escapeRooms.length; i++) // Convert objects to class + escapeRooms[i] = EscapeRoom.convert(escapeRooms[i]); if (escapeRooms!==null && escapeRooms!==undefined){ this.props.redux.actions.escapeRooms.updateEscapeRooms(escapeRooms); } @@ -196,6 +199,9 @@ class Main extends Component { Tutorials + + ToS + {profile} diff --git a/client/src/components/Properties.js b/client/src/components/Properties.js new file mode 100644 index 00000000..132b24f6 --- /dev/null +++ b/client/src/components/Properties.js @@ -0,0 +1,212 @@ +import React, {Component} from 'react'; +import { Container, Row, Col, Label, Input, Button } from 'reactstrap'; +import { LockGenerator, PuzzleGenerator, ImageManager } from './index' +import PropTypes from 'prop-types'; + +/** + * Class for Not Found + * @extends Component + * @author Alistair Quinn + */ +class Properties extends Component { + /** + * Converts a string to Camel Case + * @param {string} string + * @returns {string} + */ + convertCamelCase(string){ + if(typeof string !== 'string') + string = string.toString(); + return string.replace(/([A-Z])/g, ' $1').replace(/^./,(str)=>{ return str.toUpperCase(); }) + } + + /** Generates Output from Inputs */ + generateFromInputs=()=>{ + this.props.updateComponent({_id:this.props.selected._id,output:this.props.calculateOutput(this.props.selected._id)}) + } + + /** + * Handles Puzzle change + * @param {Puzzle} puzzle + */ + handlePuzzleChange = (puzzle)=>{ + if(puzzle.output===undefined){ + puzzle.output = ""; + } + this.props.updateComponent({_id:this.props.selected._id,puzzle,output:puzzle.output}); + } + + /** + * Handles Output change + * @param {string} event + */ + handleOutputChange = (output)=>{ + this.props.updateComponent({output}); + } + + /** + * Handles Images Change + * @param {Array} images + */ + handleImagesChange = (images)=>{ + this.props.updateComponent({images}); + } + + /** + * Handles input change + * @param {Event} event + * */ + handleChange = (event) => { + let state = {}; + state[event.target.id] = event.target.value; + if(event.target.id==="puzzleType") + state.puzzle = {} + if(event.target.id==="lockType") + state.output = "" + state._id = this.props.selected._id; + this.props.updateComponent(state); + } + + /** + * Maps Details to Inputs + * @param {string} key + * @param {int} i index + * @returns {JSX} + */ + mapPropertyToInput = (key,i) => { + if(key==='output') { + let generator; + if(this.props.selected.type==='Lock') + generator = + else if(this.props.selected.type==='Puzzle') { + generator = + } + return ( + + + + +
+ {generator} +
+ +
) + } else if(key==='lockType'){ + return ( + + + + + + + + + + + + + ) + } else if(key==='eventType'){ + return ( + + + + + + + + + + + + ) + } else if(key==='puzzleType'){ + return ( + + + + + + + + + + + + + + + ) + } else if(key==='puzzle'){ + let component = this.props.selected; + let details = Object.keys(component[key]).map((property,index,array)=>{ + let detail; + if(property.includes('DATA')){ + detail = "Export as PDF to View"; + }else if(typeof component[key][property] === 'object') + detail = JSON.stringify(component[key][property]); + else{ + detail = component[key][property]; + } + return( + + +

{" " + this.convertCamelCase(property) + ": "+this.convertCamelCase(detail)}

+ +
+ ) + }); + if(Object.keys(component[key]).length>0){ + return ( + + + + {details} + + + ) + }else{ + return null; + } + } else if(key==='estimatedCost'){ + return ( + + + + + + ) + } else if(key==='images'){ + return() + } else if(typeof this.props.selected[key] === "string" && key!=="_id" && key!=="type" && key!=="version"){ + return ( + + + + + + ) + } + } + + /** + * React lifecycle method + * Renders Layout + * @returns {JSX} + */ + render() { + return ( + + {Object.keys(this.props.selected).map(this.mapPropertyToInput)} + + ) + } +}; + +Properties.propTypes = { + selected: PropTypes.instanceOf(Component), + updateComponent: PropTypes.func, + calculateOutput: PropTypes.func, +} + +export default Properties; \ No newline at end of file diff --git a/client/src/components/Relationships.js b/client/src/components/Relationships.js new file mode 100644 index 00000000..296b3b4b --- /dev/null +++ b/client/src/components/Relationships.js @@ -0,0 +1,104 @@ +import React, {Component} from 'react'; +import { Container, Row, Col, ListGroup, ListGroupItem, Button } from 'reactstrap'; +import PropTypes from 'prop-types'; + +/** + * Class for Not Found + * @extends Component + * @author Alistair Quinn + */ +class Relationships extends Component { + /** + * Deletes a relationship + * @param {string} id + * @param {bool} isInput + */ + handleClick = (id,isInput) => (e) => { + let component = {...this.props.selected}; + let state = {}; + state._id = component._id; + if(isInput){ + state.inputComponents = component.inputComponents.filter(oldId => oldId!==id); + } else { + state.outputComponents = component.outputComponents.filter(oldId => oldId!==id); + } + this.props.updateComponent(state); + } + + /** + * Maps Relationship to List Group Item + * @param {string} id + * @param {int} i + * @param {bool} isInput + * @returns {JSX} + */ + mapRelationshipToListGroup = (id,i,isInput,deletable=true) => { + if(deletable) + return ( + + {id} + + + ) + else + return ( + + {id} + + ) + } + + /** + * React lifecycle method + * Renders Layout + * @returns {JSX} + */ + render() { + if(this.props.selected.type !== "Area") + return ( + + +
Relationships
+
+ + +
Inputs
+ + {this.props.selected.inputComponents.map((id,i)=>this.mapRelationshipToListGroup(id,i,true))} + + +
+ + +
Outputs
+ + {this.props.selected.outputComponents.map((id,i)=>this.mapRelationshipToListGroup(id,i,false))} + + +
+
+ ) + else + return ( + + + +
Relationships
+ + {this.props.selected.outputComponents.map((id,i)=>this.mapRelationshipToListGroup(id,i,false,false))} + + +
+
+ ) + } +}; + +Relationships.propTypes = { + selected: PropTypes.instanceOf(Component), + updateComponent: PropTypes.func, +} + +export default Relationships; \ No newline at end of file diff --git a/client/src/components/SignUp.js b/client/src/components/SignUp.js index 2452de94..0661baae 100644 --- a/client/src/components/SignUp.js +++ b/client/src/components/SignUp.js @@ -1,6 +1,6 @@ import React, {Component} from 'react'; import { Link } from 'react-router-dom'; -import { Container, Row, Col, Alert, Button, Form, FormGroup, Label, Input, FormText} from 'reactstrap'; +import { Container, Row, Col, Alert, Button, Form, FormGroup, Label, Input, FormText } from 'reactstrap'; import PasswordStrengthMeter from './PasswordStrengthMeter' import zxcvbn from 'zxcvbn'; import ReCAPTCHA from "react-google-recaptcha"; @@ -20,6 +20,7 @@ class Signup extends Component { password:"", password2:"", message: "", + tos:false, testResult: {} } } @@ -47,9 +48,10 @@ class Signup extends Component { handleChange = (e) => { if(e.target.id === "password") this.setState({testResult: zxcvbn(e.target.value)}); - this.setState({ - [e.target.id]: e.target.value, - },()=>this.setState({color:"danger",message:this.composeErrorMessage()})); + if(e.target.id === "tos") + this.setState({tos:e.target.checked},()=>this.setState({color:"danger",message:this.composeErrorMessage()})); + else + this.setState({[e.target.id]: e.target.value},()=>this.setState({color:"danger",message:this.composeErrorMessage()})); } /** @@ -67,6 +69,8 @@ class Signup extends Component { messages.push("Password Too Short"); if(this.state.email.includes(" ") || this.state.email.includes("$") || !this.state.email.includes("@") || !this.state.email.includes(".")) messages.push("Invalid Email"); + if(!this.state.tos) + messages.push("You must agree to the terms of service"); return messages.join(", "); } @@ -120,8 +124,12 @@ class Signup extends Component { />
+ + + + - + Passwords must be strong and 8 characters in length or more diff --git a/client/src/components/TermsOfService.js b/client/src/components/TermsOfService.js new file mode 100644 index 00000000..edc30533 --- /dev/null +++ b/client/src/components/TermsOfService.js @@ -0,0 +1,34 @@ +import React, {Component} from 'react'; +import { Container, Row, Col } from 'reactstrap'; +import PropTypes from 'prop-types'; + +/** + * Class for Verify + * @extends Component + * @author Alistair Quinn + */ +class TermsOfService extends Component { + /** + * React Lifecycle Method + * Renders Layout + * @returns {JSX} + */ + render() { + return ( + + + +

Terms of Service

+

BLAH BLAH BLAH BLAH

+ +
+
+ ) + } +}; + +TermsOfService.propTypes = { + email: PropTypes.string, +} + +export default TermsOfService; \ No newline at end of file diff --git a/client/src/components/index.js b/client/src/components/index.js index 9ad163b8..d37b0533 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -7,6 +7,7 @@ import Tutorials from './Tutorials'; // Escape Room import Dashboard from './Dashboard'; import EscapeRoomDesigner from './EscapeRoomDesigner'; +import EscapeRoomRunner from './EscapeRoomRunner'; import Details from './Details'; import Design from './Design'; import Pallet from './Pallet'; @@ -18,6 +19,9 @@ import ComponentDnDSource from './ComponentDnDSource'; import ComponentDnDTarget from './ComponentDnDTarget'; import ComponentDetails from './ComponentDetails'; import Accessibility from './Accessibility'; +import AccessibilityWarning from './AccessibilityWarning'; +import Relationships from './Relationships'; +import Properties from './Properties'; import LockGenerator from './LockGenerator'; import PuzzleGenerator from './PuzzleGenerator'; import PalletItem from './PalletItem'; @@ -28,11 +32,13 @@ import Signup from './SignUp'; import ConditionalRoute from './ConditionalRoute'; import NotFound from './NotFound'; import ListCreator from './ListCreator'; +import ImageManager from './ImageManager'; import PasswordStrengthMeter from './PasswordStrengthMeter'; import VerifyToken from './VerifyToken'; import SendVerify from './SendVerify'; import ResetToken from './ResetToken'; import SendReset from './SendReset'; +import TermsOfService from './TermsOfService'; /** * Escape Room Generate Application Components @@ -45,7 +51,7 @@ export { // Escape Room Generator App, Main, BusinessLogic, About, Tutorials, // Escape Room - Accessibility, AreaDnDSource, AreaDnDTarget, AreaPalletItem, ComponentArranger, ComponentDetails, ComponentDnDSource, ComponentDnDTarget, Dashboard, Design, Details, EscapeRoomDesigner, LockGenerator, Pallet, PalletItem, PuzzleGenerator, + Accessibility, AccessibilityWarning, Relationships, Properties, AreaDnDSource, AreaDnDTarget, AreaPalletItem, ComponentArranger, ComponentDetails, ComponentDnDSource, ComponentDnDTarget, Dashboard, Design, Details, EscapeRoomDesigner, EscapeRoomRunner, LockGenerator, Pallet, PalletItem, PuzzleGenerator, // Web - Profile, Login, Signup, ConditionalRoute, NotFound, ListCreator, PasswordStrengthMeter, VerifyToken, SendVerify, ResetToken, SendReset + Profile, Login, Signup, ConditionalRoute, NotFound, ListCreator, ImageManager, PasswordStrengthMeter, VerifyToken, SendVerify, ResetToken, SendReset, TermsOfService } \ No newline at end of file diff --git a/client/src/models/Component.js b/client/src/models/Component.js index c422b9fe..69ecc174 100644 --- a/client/src/models/Component.js +++ b/client/src/models/Component.js @@ -7,7 +7,6 @@ import uniqid from 'uniqid'; class Component { constructor(){ this._id = uniqid(); - this.version = "1"; this.name = ""; this.description = ""; this.output = ""; @@ -15,8 +14,8 @@ class Component { this.outputComponents = []; this.type = "Component"; this.position = {top:0,left:0}; - this.estimatedCost = ""; - this.resources = []; + this.estimatedCost = 0; + this.images = []; } } diff --git a/client/src/models/Details.js b/client/src/models/Details.js index 99fea4da..25a53415 100644 --- a/client/src/models/Details.js +++ b/client/src/models/Details.js @@ -7,13 +7,15 @@ class Details { this.name = "Unnamed"; this.designers= ""; this.theme = ""; - this.minPlayers = ""; - this.maxPlayers = ""; - this.targetTime = ""; - this.difficulty = "3"; + this.minPlayers = 0; + this.maxPlayers = 0; + this.targetTime = 0; + this.difficulty = 0; + this.image = ""; + this.music = ""; this.objective = ""; this.description = ""; - this.estimatedCost = ""; + this.estimatedCost = 0; } } diff --git a/client/src/models/EscapeRoom.js b/client/src/models/EscapeRoom.js index aa6ca809..36236457 100644 --- a/client/src/models/EscapeRoom.js +++ b/client/src/models/EscapeRoom.js @@ -51,6 +51,11 @@ class EscapeRoom { } } } + + static convert(obj){ + let escapeRoom = new EscapeRoom(); + return Object.assign(escapeRoom, obj); + } } export default EscapeRoom; \ No newline at end of file diff --git a/client/src/pdf/pdf.js b/client/src/pdf/pdf.js index 299fd24e..04db1db2 100644 --- a/client/src/pdf/pdf.js +++ b/client/src/pdf/pdf.js @@ -17,12 +17,13 @@ import images from '../data/images.json'; * Converts an Escape Room to PDF * @param {EscapeRoom} escapeRoom */ -function escapeRoomToPDF(escapeRoom){ +function escapeRoomToPDF(escapeRoom, components){ let doc = jsPDF('p','mm','a4'); let x = startIndent; let y = startHeight; doc.addImage(images.logo,'PNG',x + 55,y); - y+=newLineHeight; let convertedObject = convertObject(escapeRoom,doc,x,y); + doc.addPage(); + doc.addImage(components, 'PNG',0,0); convertedObject.doc.save(escapeRoom.details.name+".pdf"); } @@ -35,7 +36,6 @@ function escapeRoomToPDF(escapeRoom){ * @returns {jsPDF} doc */ function convertObject(escapeRoom,doc,x,y){ - console.log(escapeRoom); for(let key of Object.keys(escapeRoom)){ // Page Control // Blocked Keys diff --git a/client/src/reducers/escapeRoom.js b/client/src/reducers/escapeRoom.js index 9c58f2c8..893d2457 100644 --- a/client/src/reducers/escapeRoom.js +++ b/client/src/reducers/escapeRoom.js @@ -34,6 +34,20 @@ function escapeRoom(state={},action){ case 'REMOVE_COMPONENT': newState = {...state}; i = newState.components.findIndex(component=>component._id===action.componentId); + let comp = {...newState.components[i]}; + // If Area Remove Containing Components + if(comp.type === "Area"){ + comp.outputComponents.forEach(oldComponent => { + i = newState.components.findIndex(component=>component._id===oldComponent); + newState.components = [...newState.components.slice(0,i),...newState.components.slice(i+1)] + newState.components.forEach((component,index,components)=>{ + components[index].inputComponents = component.inputComponents.filter(inputId=>inputId!==oldComponent); + components[index].outputComponents = component.outputComponents.filter(outputId=>outputId!==oldComponent); + }); + + }); + } + i = newState.components.findIndex(component=>component._id===action.componentId); newState.components = [...newState.components.slice(0,i),...newState.components.slice(i+1)] newState.components.forEach((component,index,components)=>{ components[index].inputComponents = component.inputComponents.filter(inputId=>inputId!==action.componentId); diff --git a/client/src/styles/Component.css b/client/src/styles/Component.css index a4971536..7b220a2d 100644 --- a/client/src/styles/Component.css +++ b/client/src/styles/Component.css @@ -10,7 +10,7 @@ width: 100%; min-width: 100%; min-height: 50%; - height: 83vh; + height: 70vh !important; overflow: hidden; }