├── README.md ├── server ├── calModel.js ├── api.js ├── server.js └── calController.js ├── src ├── main.js ├── js │ └── components │ │ ├── CalEntry.jsx │ │ ├── CalBox.jsx │ │ ├── CalContainer.jsx │ │ ├── Form.jsx │ │ └── App.jsx ├── index.html └── style.css ├── webpack.config.js ├── package.json └── dist └── bundle.js /README.md: -------------------------------------------------------------------------------- 1 | # Covid Calendar 2 | -------------------------------------------------------------------------------- /server/calModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const PG_URI = // 'put ElephantSQL URL string here'; 4 | 5 | const pool = new Pool({ connectionString: PG_URI }); 6 | 7 | module.exports = { 8 | query: (text, params, callback) => { 9 | console.log('*SQL Query*', text); 10 | return pool.query(text, params, callback); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-filename-extension */ 2 | /* eslint-disable import/extensions */ 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import App from './js/components/App.jsx'; 6 | 7 | // eslint-disable-next-line no-unused-vars 8 | import styles from './style.css'; 9 | 10 | render(, document.getElementById('app')); 11 | -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const calController = require('./calController.js'); 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', 8 | calController.getEntries, 9 | (req, res) => res.status(200).json(res.locals.entries)); 10 | 11 | router.post('/entry', 12 | calController.postEntry, 13 | (req, res) => res.status(200).json(res.locals.newEntry)); 14 | 15 | router.post('/delete', 16 | calController.deleteEntry, 17 | (req, res) => res.status(200).json(res.locals.delete)); 18 | 19 | module.exports = router; 20 | -------------------------------------------------------------------------------- /src/js/components/CalEntry.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from 'react'; 3 | 4 | // each entry will be written with time, location, and people from the props.entry object 5 | const CalEntry = (props) => { 6 | const { entry } = props; 7 | const { 8 | time, location, people, 9 | } = entry; 10 | return ( 11 |
12 |

13 | Time:  14 | {time} 15 |
16 | Location:  17 | {location} 18 |
19 | People:  20 | {people} 21 |

22 |
23 | ); 24 | }; 25 | 26 | export default CalEntry; 27 | -------------------------------------------------------------------------------- /src/js/components/CalBox.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | /* eslint-disable import/extensions */ 3 | /* eslint-disable react/prop-types */ 4 | import React from 'react'; 5 | 6 | import CalEntry from './CalEntry.jsx'; 7 | 8 | const CalBox = (props) => { 9 | // each calendar box will hold entries that match the date of the box 10 | const { title, entry, date } = props; 11 | const CalEntryArr = []; 12 | for (let i = 0; i < entry.length; i++) { 13 | if (entry[i].date === date) { 14 | CalEntryArr.push( 15 | , 19 | ); 20 | } 21 | } 22 | return ( 23 |
24 |

{title}

25 |

{date}

26 | {CalEntryArr} 27 |
28 | ); 29 | }; 30 | 31 | export default CalBox; 32 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // const { webpack } = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: './src/main.js', 6 | mode: process.env.NODE_ENV, 7 | output: { 8 | filename: 'bundle.js', 9 | path: path.resolve(__dirname, './dist'), 10 | publicPath: '/dist/', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/i, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: [ 21 | '@babel/preset-env', 22 | '@babel/preset-react', 23 | ], 24 | }, 25 | }, 26 | }, 27 | { 28 | test: /\.s?[ac]ss$/i, 29 | use: [ 30 | 'style-loader', 31 | 'css-loader', 32 | 'sass-loader', 33 | ], 34 | }, 35 | ], 36 | }, 37 | devServer: { 38 | // contentBase: '/dist/', 39 | publicPath: '/dist/', 40 | port: 8080, 41 | compress: true, 42 | proxy: { 43 | '/dist': 'http://localhost:3000', 44 | '/': 'http://localhost:3000', 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | const path = require('path'); 5 | const bodyParser = require('body-parser'); 6 | const apiRouter = require('./api.js'); 7 | 8 | const port = 3000; 9 | 10 | // parse through the body request in order to add entries 11 | app.use(bodyParser.json()); 12 | 13 | // serves webpack bundle 14 | app.use('/dist', express.static(path.resolve(__dirname, '../dist'))); 15 | 16 | // use route handlers to access database 17 | app.use('/api', apiRouter); 18 | 19 | // responds with main app 20 | app.get('/', (req, res) => { 21 | res.status(200).sendFile(path.resolve(__dirname, '../src/index.html')); 22 | }); 23 | 24 | // unknown request error handler 25 | app.use((req, res) => res.sendStatus(404)); 26 | 27 | // global error handler 28 | app.use((err, req, res) => { 29 | const defaultErr = { 30 | log: 'Express error handler caught unknown middleware error', 31 | status: 400, 32 | message: { err: 'An error occurred' }, 33 | }; 34 | const errObj = { ...defaultErr, err }; // Object.assign({}, defaultErr, err); 35 | return res.status(errObj.status).json(errObj.message); 36 | }); 37 | 38 | app.listen(port); 39 | -------------------------------------------------------------------------------- /src/js/components/CalContainer.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | /* eslint-disable react/prop-types */ 3 | /* eslint-disable no-plusplus */ 4 | import React from 'react'; 5 | 6 | import CalBox from './CalBox.jsx'; 7 | 8 | const CalContainer = (props) => { 9 | // determine unique dates so that we only add Calendar Boxes for unique dates 10 | const { entry } = props; 11 | let dateArr = []; 12 | for (let i = 0; i < entry.length; i++) { 13 | if (!dateArr.includes(entry[i].date)) { 14 | dateArr.push(entry[i].date); 15 | } 16 | } 17 | // sort array of dates so that most recent date appears first 18 | dateArr = dateArr.sort((a, b) => b - a); 19 | 20 | // create date object to compare with entry dates to determine how long ago entry is 21 | const dateObj = new Date(); 22 | const today = dateObj.getDate(); 23 | 24 | const CalBoxArr = []; 25 | for (let i = 0; i < dateArr.length; i++) { 26 | // determine how many days agao entry date is compared to today's date 27 | const entryDate = new Date(dateArr[i]); 28 | const entryDay = entryDate.getDate() + 1; 29 | const title = today - entryDay; 30 | // push calendar boxes with unique dates 31 | CalBoxArr.push( 32 | , 38 | ); 39 | } 40 | return ( 41 |
42 | {CalBoxArr} 43 |
44 | ); 45 | }; 46 | 47 | export default CalContainer; 48 | -------------------------------------------------------------------------------- /server/calController.js: -------------------------------------------------------------------------------- 1 | const db = require('./calModel.js'); 2 | 3 | const calController = {}; 4 | 5 | calController.getEntries = (req, res, next) => { 6 | const entries = ` 7 | SELECT * FROM entries 8 | WHERE userId = 1; 9 | `; 10 | db.query(entries, (err, result) => { 11 | if (err) throw err; 12 | res.locals.entries = result.rows; 13 | return next(); 14 | }); 15 | }; 16 | 17 | calController.postEntry = (req, res, next) => { 18 | const { 19 | date, time, location, people, 20 | } = req.body; 21 | const values = [date.toString(), time.toString(), location, people]; 22 | const newEntry = ` 23 | INSERT INTO entries 24 | VALUES (1, $1, $2, $3, $4); 25 | `; 26 | db.query(newEntry, values, (err, result) => { 27 | if (err) throw err; 28 | res.locals.newEntry = result.rows; 29 | return next(); 30 | }); 31 | }; 32 | 33 | calController.deleteEntry = (req, res, next) => { 34 | const { 35 | date, time, location, people, 36 | } = req.body; 37 | const values = [date.toString(), time.toString(), location, people]; 38 | const deleteEntry = ` 39 | DELETE FROM entries 40 | WHERE userId = 1 41 | AND date = $1 42 | AND time = $2 43 | AND location = $3 44 | AND people = $4; 45 | `; 46 | db.query(deleteEntry, values, (err, result) => { 47 | if (err) throw err; 48 | res.locals.delete = result.rows; 49 | return next(); 50 | }); 51 | }; 52 | 53 | calController.newUser = (req, res, next) => { 54 | // something 55 | }; 56 | 57 | module.exports = calController; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solo-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/main.js", 6 | "scripts": { 7 | "start": "nodemon server/server.js --open", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "NODE_ENV=production webpack & npm start", 10 | "dev": "NODE_ENV=development npm start & webpack-dev-server --open" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/bhayashi/solo-project.git" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/bhayashi/solo-project/issues" 21 | }, 22 | "homepage": "https://github.com/bhayashi/solo-project#readme", 23 | "devDependencies": { 24 | "@babel/core": "^7.11.6", 25 | "@babel/preset-env": "^7.11.5", 26 | "@babel/preset-react": "^7.10.4", 27 | "babel-loader": "^8.1.0", 28 | "css-loader": "^4.3.0", 29 | "eslint": "^7.8.1", 30 | "eslint-config-airbnb": "^18.2.0", 31 | "eslint-plugin-import": "^2.22.0", 32 | "eslint-plugin-jsx-a11y": "^6.3.1", 33 | "eslint-plugin-react": "^7.20.6", 34 | "eslint-plugin-react-hooks": "^4.1.0", 35 | "nodemon": "^2.0.4", 36 | "sass": "^1.26.10", 37 | "sass-loader": "^10.0.2", 38 | "style-loader": "^1.2.1", 39 | "webpack": "^4.44.1", 40 | "webpack-cli": "^3.3.12", 41 | "webpack-dev-server": "^3.11.0" 42 | }, 43 | "dependencies": { 44 | "body-parser": "^1.19.0", 45 | "express": "^4.17.1", 46 | "pg": "^8.3.3", 47 | "react": "^16.13.1", 48 | "react-dom": "^16.13.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | COVID Calendar 9 | 10 | 11 |
12 |

COVID Calendar

13 |
14 |
15 | 16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 |

Today

34 |

09/08/2020

35 | 36 |
37 |

Time: 11:00AM
Location: Los Angeles
People Visited: N/A

38 |
39 | 40 |
41 |

Time: 1:00PM
Location: DTLA
People Visited: George

42 |
43 |
44 | 45 |
46 |

1 Day Ago

47 |
48 | 49 |
50 |

2 Days Ago

51 |
52 |
53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/js/components/Form.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React from 'react'; 3 | 4 | // this form contains everything that will change state 5 | // every input handles an event or click 6 | const Form = (props) => { 7 | const { 8 | handleDate, handleTime, handleLoc, handlePeople, clickSubmit, 9 | clickUndo, date, time, location, people, 10 | } = props; 11 | return ( 12 |
13 | 23 |
24 | 34 |
35 | 45 |
46 | 56 |
57 | 58 | 64 | 70 | 71 |
72 | ); 73 | }; 74 | 75 | export default Form; 76 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: lightgray; 3 | } 4 | 5 | #main-title { 6 | font-family: 'Fira Code', monospace; 7 | text-align: center; 8 | color: rgb(33, 51, 51); 9 | } 10 | 11 | #entry-form { 12 | display: flex; 13 | flex-direction: column; 14 | width: 50%; 15 | margin-left: auto; 16 | margin-right: auto; 17 | font-family: 'Fira Code', monospace; 18 | max-width: 15rem; 19 | } 20 | 21 | .entry-form { 22 | display: flex; 23 | flex-direction: column; 24 | width: 50%; 25 | margin-left: auto; 26 | margin-right: auto; 27 | font-family: 'Fira Code', monospace; 28 | max-width: 15rem; 29 | color: rgb(33, 51, 51); 30 | } 31 | 32 | #form-buttons-container { 33 | display: inline; 34 | } 35 | 36 | .form-button { 37 | width: 45%; 38 | height: 2rem; 39 | background-color: rgb(43, 156, 109); 40 | color: white; 41 | font-family: 'Fira Code', monospace; 42 | border-radius: 0.3rem; 43 | border: 0.1rem solid lightgray; 44 | } 45 | 46 | .form-button:hover { 47 | color: rgb(43, 156, 109); 48 | border: 0.1rem solid rgb(43, 156, 109); 49 | background: white; 50 | } 51 | 52 | #cal-container { 53 | display: flex; 54 | flex-direction: column; 55 | width: 70%; 56 | margin: 3rem auto 0rem auto; 57 | font-family: 'Fira Code', monospace; 58 | max-width: 50rem; 59 | } 60 | 61 | .cal-container { 62 | display: flex; 63 | flex-direction: column; 64 | width: 70%; 65 | margin: 3rem auto 0rem auto; 66 | font-family: 'Fira Code', monospace; 67 | max-width: 50rem; 68 | } 69 | 70 | .cal-box { 71 | text-align: center; 72 | color: white; 73 | width: 100%; 74 | border: 0.1rem solid lightgray; 75 | border-radius: 0.3rem; 76 | margin: 0.5rem auto 0.5rem auto; 77 | background: rgb(43, 156, 109); 78 | padding-bottom: 1rem; 79 | } 80 | 81 | .cal-entry { 82 | text-align: start; 83 | width: 50%; 84 | padding: 0rem 0.5rem 0rem 0.5rem; 85 | margin: 0.3rem auto 0.3rem auto; 86 | border: 0.1rem solid darkslategray; 87 | border-radius: 0.3rem; 88 | background: rgb(236, 236, 236); 89 | color: darkslategray; 90 | } 91 | -------------------------------------------------------------------------------- /src/js/components/App.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable import/extensions */ 3 | import React from 'react'; 4 | 5 | import Form from './Form.jsx'; 6 | import CalContainer from './CalContainer.jsx'; 7 | 8 | class App extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | // state will keep track of input fields as well as an array of entry submissions 12 | this.state = { 13 | date: '', 14 | time: '', 15 | location: '', 16 | people: '', 17 | entry: [], 18 | }; 19 | this.handleDate = this.handleDate.bind(this); 20 | this.handleTime = this.handleTime.bind(this); 21 | this.handleLoc = this.handleLoc.bind(this); 22 | this.handlePeople = this.handlePeople.bind(this); 23 | this.clickSubmit = this.clickSubmit.bind(this); 24 | this.clickUndo = this.clickUndo.bind(this); 25 | } 26 | 27 | componentDidMount() { 28 | fetch('/api/') 29 | .then((res) => res.json()) 30 | .then((entry) => { 31 | // eslint-disable-next-line no-param-reassign 32 | if (!Array.isArray(entry)) entry = []; 33 | return this.setState({ entry }); 34 | }) 35 | .catch((err) => console.log('App.jsx componentDidMount: get entries error', err)); 36 | } 37 | 38 | handleDate(e) { 39 | this.setState({ date: e.target.value }); 40 | } 41 | 42 | handleTime(e) { 43 | this.setState({ time: e.target.value }); 44 | } 45 | 46 | handleLoc(e) { 47 | this.setState({ location: e.target.value }); 48 | } 49 | 50 | handlePeople(e) { 51 | this.setState({ people: e.target.value }); 52 | } 53 | 54 | // when submit button is clicked 55 | // all the state properties will be added into the entry array as an object 56 | clickSubmit() { 57 | this.setState((state) => { 58 | const entryObj = { 59 | date: state.date, 60 | time: state.time, 61 | location: state.location, 62 | people: state.people, 63 | }; 64 | // use a fetch request to post new entry into the database 65 | fetch('/api/entry', { 66 | method: 'POST', 67 | headers: { 'Content-Type': 'application/json' }, 68 | body: JSON.stringify(entryObj), 69 | }) 70 | .then((response) => response.json()) 71 | .then((data) => console.log('Successful Entry Submitted', data)) 72 | .catch((err) => console.log('App.jsx clickSubmit: entry submit error', err)); 73 | // entry array of objects is updated with new entry object 74 | const entry = [...state.entry]; 75 | entry.push(entryObj); 76 | return { entry }; 77 | }); 78 | } 79 | 80 | // when undo button is clicked, the last entry in the array will be removed 81 | clickUndo() { 82 | this.setState((state) => { 83 | const entry = [...state.entry]; 84 | const deletedEntry = entry.pop(); 85 | 86 | // use a fetch request to delete entry in the database 87 | fetch('/api/delete', { 88 | method: 'POST', 89 | headers: { 'Content-Type': 'application/json' }, 90 | body: JSON.stringify(deletedEntry), 91 | }) 92 | .then((response) => response.json()) 93 | .then((data) => console.log('Successful Entry Submitted', data)) 94 | .catch((err) => console.log('App.jsx clickSubmit: entry submit error', err)); 95 | 96 | return { entry }; 97 | }); 98 | } 99 | 100 | render() { 101 | const { 102 | date, time, location, people, entry, 103 | } = this.state; 104 | return ( 105 |
106 |
118 | 121 |
122 | ); 123 | } 124 | } 125 | 126 | export default App; 127 | -------------------------------------------------------------------------------- /dist/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var l=t[r]={i:r,l:!1,exports:{}};return e[r].call(l.exports,l,l.exports,n),l.l=!0,l.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var l in e)n.d(r,l,function(t){return e[t]}.bind(null,l));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist/",n(n.s=11)}([function(e,t,n){"use strict";e.exports=n(4)},function(e,t,n){"use strict"; 2 | /* 3 | object-assign 4 | (c) Sindre Sorhus 5 | @license MIT 6 | */var r=Object.getOwnPropertySymbols,l=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;function a(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,o,u=a(e),c=1;cO.length&&O.push(e)}function R(e,t,n){return null==e?0:function e(t,n,r,l){var o=typeof t;"undefined"!==o&&"boolean"!==o||(t=null);var u=!1;if(null===t)u=!0;else switch(o){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case i:case a:u=!0}}if(u)return r(l,t,""===n?"."+I(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var c=0;c