diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..5171c54083337f0b87926da2e8f52abefe19d70f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3ef213587d0dd9a7c3f63877f7eb5450c96b889e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:10.15.3-alpine as build +WORKDIR /usr/src/app +ENV PATH /usr/src/app/node_modules/.bin:$PATH +COPY package*.json ./ +RUN npm install +RUN npm install react-scripts@3.0.1 -g +COPY . . +RUN npm run build + +# production environment +FROM nginx:1.16.0-alpine +COPY --from=build /usr/src/app/build /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/package-lock.json b/package-lock.json index 3ff0ebd163c596887a34d9dd893b06b2912fe8ff..a5848437d7f6b2b08a9cbd23acc1605d3eaa8322 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1310,6 +1310,20 @@ "@babel/types": "^7.3.0" } }, + "@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1337,11 +1351,30 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.3.tgz", "integrity": "sha512-zkOxCS/fA+3SsdA+9Yun0iANxzhQRiNwTvJSr6N95JhuJ/x27z9G2URx1Jpt3zYFfCGUXZGL5UDxt5eyLE7wgw==" }, + "@types/object-assign": { + "version": "4.0.30", + "resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz", + "integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI=" + }, + "@types/prop-types": { + "version": "15.7.1", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.1.tgz", + "integrity": "sha512-CFzn9idOEpHrgdw8JsoTkaDDyRWk1jrzIV8djzcgpq0y9tG4B4lFT+Nxh52DVpDXV+n4+NPNv7M1Dj5uMp6XFg==" + }, "@types/q": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==" }, + "@types/react": { + "version": "16.8.19", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.8.19.tgz", + "integrity": "sha512-QzEzjrd1zFzY9cDlbIiFvdr+YUmefuuRYrPxmkwG0UQv5XF35gFIi7a95m1bNVcFU0VimxSZ5QVGSiBmlggQXQ==", + "requires": { + "@types/prop-types": "*", + "csstype": "^2.2.0" + } + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -3889,6 +3922,11 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.5.tgz", + "integrity": "sha512-JsTaiksRsel5n7XwqPAfB0l3TFKdpjW/kgAELf9vrb5adGA7UCPLajKK5s3nFrcFm3Rkyp/Qkgl73ENc1UY3cA==" + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -10012,6 +10050,16 @@ "whatwg-fetch": "3.0.0" } }, + "react-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.0.tgz", + "integrity": "sha512-om/HB4MBHt4k+moR8Mb5h1kmKxcmOxK2U6aaQZ8Y+f+igICcE5bpng7yCiAo3kKN0btFpzvQ70XnpONOC0xkdA==", + "requires": { + "@types/hoist-non-react-statics": "^3.0.1", + "hoist-non-react-statics": "^3.0.0", + "universal-cookie": "^4.0.0" + } + }, "react-dev-utils": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.0.1.tgz", @@ -11897,6 +11945,24 @@ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==" }, + "universal-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.0.tgz", + "integrity": "sha512-6JVx+3oGPjslGqFhQ8YSIBHmYTx8HbyAEH++2/b6SKNXsbsdQ7lU7wRG2bYcRB5JVCz8GYgQ+Ixew91hn3Dy9w==", + "requires": { + "@types/cookie": "^0.3.1", + "@types/object-assign": "^4.0.30", + "cookie": "^0.3.1", + "object-assign": "^4.1.0" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } + } + }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", diff --git a/package.json b/package.json index b7199d6fe609e71c8541b53f702c70e67934b9a5..af5b9cd24a2375a6c95fc5b8708d9bc55264571f 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,13 @@ "leaflet": "^1.5.1", "leaflet-draw": "^1.0.4", "react": "^16.8.6", + "react-cookie": "^4.0.0", "react-dom": "^16.8.6", "react-leaflet": "^2.3.0", "react-leaflet-draw": "0.19.0", + "universal-cookie": "^4.0.0" "react-scripts": "3.0.1" + }, "scripts": { "start": "react-scripts start", diff --git a/src/App.css b/src/App.css index eda5bc7d5065ea9ffbf56b3af657ed3129c55bd0..b0f08dddfc38b5c27752c025dbfe8113491b93d2 100644 --- a/src/App.css +++ b/src/App.css @@ -1,4 +1,140 @@ +body { + margin: 0; + padding: 0; + overflow: hidden; +} + +.hidden { + display: none; +} + +/* UserMap */ .map { + position: absolute; + margin-top: 50px; + height: 95vh; + width: 100vw; +} + +/* Header */ +div.header { + position: absolute; + top: 0; + height: 50px; + width: 100%; + background: white; + z-index: 1000; +} + +div.header button { + transition-property: background-color; + transition-duration: 0.5s; + transition-timing-function: ease; + background-color: #279fd9; + color: white; + font-weight: bold; + border: 0px; + padding: 0.5em; + margin: 0em 1em; + width: 10%; + height: 100%; + font-size: 18px; + float: right; +} + +div.header button:hover { + background-color: darkblue; + cursor: pointer; +} + +/* Login&RegisterForm */ +div.fade-main { + position: fixed; + top: 0; + z-index: 1000; height: 100vh; width: 100vw; -} \ No newline at end of file + margin: auto; + text-align: center; + background-color: rgba(0, 0, 0, 0.85); +} + +div.sticky { + position: fixed; + top: 0px; + right: 0px; + height: 100px; + width: 100px; + margin-right: 150px; +} + +.close { + position: fixed; + color: #f1f1f1; + height: 85px; + font-size: 100px; + font-weight: bold; + transition: transform 0.4s ease-in-out; + line-height: 70%; +} + +.close:hover, +.close:focus { + color: #bbb; + text-decoration: none; + cursor: pointer; + transform: rotate(90deg); +} + +div.login { + height: 60vh; + width: 40vw; + margin: auto; + margin-top: 5%; + padding-top: 25px; +} + +div.login h1 { + color: rgb(0, 200, 255); + margin-bottom: 0.5em; + font-size: 3em; +} + +div.login h2 { + color: rgb(255, 0, 0); + margin-top: 2em; + font-size: 1.5em; +} + +div.login input { + background: none; + border: none; + outline: none; + width: 60%; + padding: 12px 20px; + margin-bottom: 1em; + display: inline-block; + box-sizing: border-box; + font-size: 180%; + color: white; + border-bottom: 3px solid rgb(0, 200, 255); +} + +div.login button { + transition-property: background-color; + transition-duration: 0.5s; + transition-timing-function: ease; + background-color: #279fd9; + color: white; + font-weight: bold; + border: 0px; + padding: 0.5em; + margin: 0em 2em; + width: 20%; + font-size: 20px; +} + +div.login button:hover { + background-color: darkblue; + cursor: pointer; +} diff --git a/src/App.js b/src/App.js index 7f1245d010b90dbc1da6ea71e6eadce955877590..b5ee0cf4c7ef5a434a32cde764f4c1b03cc231ee 100644 --- a/src/App.js +++ b/src/App.js @@ -4,6 +4,9 @@ import './App.css'; import UserMap from './components/UserMap.js' +import styles from './App.css'; +import UserMap from './components/UserMap'; +import Header from './components/Header'; class App extends Component { constructor() { @@ -12,15 +15,17 @@ class App extends Component { this.state = { lat: 62.2416479, lng: 25.7597186, - zoom: 13, - } + zoom: 13 + }; } - render(){ - const initialPosition = [this.state.lat, this.state.lng]; + + render() { + const initialPosition = [this.state.lat, this.state.lng]; return ( - <div> - <UserMap position={initialPosition} zoom={this.state.zoom}/> - </div> + <div> + <UserMap position={initialPosition} zoom={this.state.zoom} />, + <Header /> + </div> ); } } diff --git a/src/components/Header.js b/src/components/Header.js new file mode 100644 index 0000000000000000000000000000000000000000..4406f82407fb761209ea19f44e63f55c23ca5ff0 --- /dev/null +++ b/src/components/Header.js @@ -0,0 +1,100 @@ +import React from 'react'; + +import LoginForm from './LoginForm'; +import RegisterForm from './RegisterForm'; + +class Header extends React.Component { + state = { + login: false, + register: false, + username: null, + token: null + }; + + // toggles the login/register view + toggleView = view => { + this.setState(prevState => { + return { + [view]: view === 'login' ? !prevState.login : !prevState.register + }; + }); + }; + + handleState = data => { + sessionStorage.setItem('name', data.name); + sessionStorage.setItem('token', data.token); + this.setState({ username: data.name, token: data.token }); + }; + + handleLogout = () => { + this.setState({ username: null, token: null }); + sessionStorage.removeItem('token'); + }; + + // verifies the token (if it exists) on element mount with backend server + componentDidMount() { + let token = sessionStorage.getItem('token'); + if (token) { + fetch('http://localhost:5000/user/verify', { + headers: { + Authorization: 'Bearer ' + token + } + }) + .then(res => res.json()) + .then( + result => { + // if token is still valid, login user + if (result === true) { + this.setState({ + username: sessionStorage.getItem('name'), + token: token + }); + // logout user if token has expired / is invalid + } else { + this.handleLogout(); + } + }, + error => { + console.log(error); + } + ); + } + } + + render() { + return ( + <div> + <div className='header'> + {!this.state.username && ( + <button onClick={() => this.toggleView('register')}> + register + </button> + )} + {!this.state.username && ( + <button onClick={() => this.toggleView('login')}>login</button> + )} + {this.state.username && ( + <button onClick={this.handleLogout}>logout</button> + )} + {this.state.username && <button>{this.state.username}</button>} + </div> + {this.state.register && ( + <RegisterForm + view='register' + handleState={this.handleState} + toggleView={this.toggleView} + /> + )} + {this.state.login && ( + <LoginForm + view='login' + handleState={this.handleState} + toggleView={this.toggleView} + /> + )} + </div> + ); + } +} + +export default Header; diff --git a/src/components/LoginForm.js b/src/components/LoginForm.js new file mode 100644 index 0000000000000000000000000000000000000000..7893bb8c967509d8589d31665e1f1a57c4b34fcb --- /dev/null +++ b/src/components/LoginForm.js @@ -0,0 +1,117 @@ +import React from 'react'; + +export class LoginForm extends React.Component { + constructor(props) { + super(props); + + this.state = { + errorMsg: '', + username: '', + password: '' + }; + } + + handleError = error => { + this.setState({ errorMsg: error }); + }; + + handleChange = e => { + const { name, value } = e.target; + this.setState({ [name]: value }); + }; + + // show/hide this form + handleView = e => { + this.props.toggleView(this.props.view); + }; + + // remove login view with ESC + handleEsc = e => { + if (e.keyCode === 27) { + this.handleView(); + } + }; + + handleLogin = e => { + const name = this.state.username; + const password = this.state.password; + e.preventDefault(); + + // Send login info to the server + fetch('http://localhost:5000/user/login', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + password: password + }) + }) + .then(res => res.json()) + .then( + result => { + if (result.name) { + this.props.handleState(result); + this.handleView(); + } else { + this.handleError(result.errorResponse.message); + } + }, + // Note: it's important to handle errors here + // instead of a catch() block so that we don't swallow + // exceptions from actual bugs in components. + error => { + console.log(error); + } + ); + }; + + componentDidMount() { + document.addEventListener('keyup', this.handleEsc); + } + + componentWillUnmount() { + document.removeEventListener('keyup', this.handleEsc); + } + + render() { + return ( + <div className='fade-main'> + <div className='sticky'> + <span className='close' onClick={this.handleView}> + × + </span> + </div> + <div className='login'> + <form onSubmit={this.handleLogin}> + <h1>demo login</h1> + <br /> + <input + placeholder='Enter Username' + name='username' + value={this.state.username} + onChange={this.handleChange} + required + /> + <br /> + <input + placeholder='Enter password' + type='password' + name='password' + value={this.state.password} + onChange={this.handleChange} + required + /> + <br /> + <button type='submit'>login</button> + <h2>{this.state.errorMsg}</h2> + </form> + </div> + </div> + ); + } +} + +export default LoginForm; diff --git a/src/components/RegisterForm.js b/src/components/RegisterForm.js new file mode 100644 index 0000000000000000000000000000000000000000..f0ead02cce19af21937d132a54443f593d8ad9d4 --- /dev/null +++ b/src/components/RegisterForm.js @@ -0,0 +1,133 @@ +import React from 'react'; + +export class RegisterForm extends React.Component { + constructor(props) { + super(props); + + this.state = { + errorMsg: '', + username: '', + password: '', + password2: '' + }; + } + + // shows error messages associated with registering + handleError = error => { + this.setState({ errorMsg: error }); + }; + + // updates state with input values + handleChange = e => { + const { name, value } = e.target; + this.setState({ [name]: value }); + }; + + // show/hide this form + handleView = e => { + this.props.toggleView(this.props.view); + }; + + // remove register view with ESC + handleEsc = e => { + if (e.keyCode === 27) { + this.handleView(); + } + }; + + handleRegister = e => { + const name = this.state.username; + const password = this.state.password; + e.preventDefault(); + + if (this.state.password !== this.state.password2) { + this.handleError('Passwords do not match'); + } else { + // Send register info to the server + fetch('http://localhost:5000/user/register', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + password: password + }) + }) + .then(res => res.json()) + .then( + result => { + if (result.name) { + this.props.handleState(result); + this.handleView(); + } else { + this.handleError(result.errorResponse.message); + } + }, + // Note: it's important to handle errors here + // instead of a catch() block so that we don't swallow + // exceptions from actual bugs in components. + error => { + console.log(error); + } + ); + } + }; + + componentDidMount() { + document.addEventListener('keyup', this.handleEsc); + } + + componentWillUnmount() { + document.removeEventListener('keyup', this.handleEsc); + } + + render() { + return ( + <div className='fade-main'> + <div className='sticky'> + <span className='close' onClick={this.handleView}> + × + </span> + </div> + <div className='login'> + <form onSubmit={this.handleRegister}> + <h1>register new user</h1> + <br /> + <input + placeholder='Enter Username' + name='username' + value={this.state.username} + onChange={this.handleChange} + required + /> + <br /> + <input + placeholder='Enter password' + type='password' + name='password' + value={this.state.password} + onChange={this.handleChange} + required + /> + <br /> + <input + placeholder='Verify password' + type='password' + name='password2' + value={this.state.password2} + onChange={this.handleChange} + required + /> + <br /> + <button type='submit'>register</button> + <h2>{this.state.errorMsg}</h2> + </form> + </div> + </div> + ); + } +} + +export default RegisterForm; diff --git a/src/components/UserMap.js b/src/components/UserMap.js index 40bc4232c3f2f51e99d9dac09cc70b51594ce1af..cb64d1aeadbed37593062118c46849b28c37b1bc 100644 --- a/src/components/UserMap.js +++ b/src/components/UserMap.js @@ -26,5 +26,4 @@ class UserMap extends Component { } } - export default UserMap; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 4a1df4db71cdb32ede8a8f6cf33da4539cbf0920..ec2585e8c0bb8188184ed1e0703c4c8f2a8419b0 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,13 @@ body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/src/index.js b/src/index.js index b22efa548d7cc11ed18f9fc56ea9e99cc6373d70..0b8e65249e5cf647a221551fe14c46fbdeb8a31a 100644 --- a/src/index.js +++ b/src/index.js @@ -11,13 +11,11 @@ import * as serviceWorker from './serviceWorker'; // make the default marker work with react (dunno if this is a weird hack) delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ - iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), - iconUrl: require('leaflet/dist/images/marker-icon.png'), - shadowUrl: require('leaflet/dist/images/marker-shadow.png') + iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), + iconUrl: require('leaflet/dist/images/marker-icon.png'), + shadowUrl: require('leaflet/dist/images/marker-shadow.png') }); - - ReactDOM.render(<App />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change