├── .env ├── .prettierrc.json ├── Procfile ├── client ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── src │ ├── context │ │ └── userContext.js │ ├── components │ │ ├── index.js │ │ ├── Main │ │ │ ├── StatisticChart │ │ │ │ ├── CategoryChart │ │ │ │ │ ├── GlobalChart.js │ │ │ │ │ ├── helpers │ │ │ │ │ │ └── getDataSet.js │ │ │ │ │ ├── StateChart.js │ │ │ │ │ └── CountryChart.js │ │ │ │ ├── StatisticChart.js │ │ │ │ └── index.js │ │ │ ├── RequestDialog.js │ │ │ └── Home.js │ │ ├── SnackbarError.js │ │ └── Auth │ │ │ ├── Login.js │ │ │ └── Signup.js │ ├── index.css │ ├── index.js │ ├── themes │ │ └── theme.js │ ├── App.js │ ├── routes.js │ └── serviceWorker.js ├── package.json └── README.md ├── server ├── db │ ├── index.js │ ├── models │ │ ├── index.js │ │ ├── statistic.js │ │ └── user.js │ ├── db.js │ └── seed.js ├── routes │ ├── api │ │ ├── request │ │ │ ├── helpers │ │ │ │ ├── calcPCT │ │ │ │ │ ├── index.js │ │ │ │ │ ├── getPCTByFemale.js │ │ │ │ │ ├── getPCTByName.js │ │ │ │ │ └── getPCTByAge.js │ │ │ │ ├── index.js │ │ │ │ ├── getFreshNumber.js │ │ │ │ ├── getCSVForm.js │ │ │ │ └── statisticCalculator.js │ │ │ └── requestHandler.js │ │ ├── index.js │ │ └── user │ │ │ └── userHandler.js │ └── auth │ │ └── index.js ├── middlewares │ └── auth.js ├── package.json ├── app.js └── bin │ └── www ├── .gitignore ├── .gitpod.yml ├── package.json └── README.md /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server/bin/www -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dragon1207/pop-analyzer/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/context/userContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default React.createContext(); 4 | -------------------------------------------------------------------------------- /server/db/index.js: -------------------------------------------------------------------------------- 1 | const db = require("./db"); 2 | 3 | require("./models"); 4 | 5 | module.exports = db; 6 | -------------------------------------------------------------------------------- /client/src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Home } from "./Main/Home"; 2 | export { default as SnackbarError } from "./SnackbarError"; 3 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Messenger", 3 | "name": "Messenger", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#3A8DFF", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/calcPCT/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getPCTByAge: require("./getPCTByAge"), 3 | getPCTByFemale: require("./getPCTByFemale"), 4 | getPCTByName: require("./getPCTByName"), 5 | }; 6 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/index.js: -------------------------------------------------------------------------------- 1 | const StatisticCalculator = require("./statisticCalculator"); 2 | const getCSVForm = require("./getCSVForm"); 3 | 4 | module.exports = { 5 | StatisticCalculator, 6 | getCSVForm, 7 | ...require("./calcPCT"), 8 | }; 9 | -------------------------------------------------------------------------------- /server/routes/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | 3 | router.use("/users", require("./user/userHandler")); 4 | router.use("/requests", require("./request/requestHandler")); 5 | 6 | router.use((req, res, next) => { 7 | const error = new Error("Not Found"); 8 | error.status = 404; 9 | next(error); 10 | }); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/getFreshNumber.js: -------------------------------------------------------------------------------- 1 | // 2.9999999999999 | 3.0000000000004 => 3 2 | module.exports = function (value) { 3 | const freshValue = (value + Number.EPSILON).toFixed(10).split(""); 4 | 5 | while (!freshValue.at(-1) === "0") freshValue.pop(); 6 | if (freshValue.at(-1) === ".") freshValue.pop(); 7 | 8 | return Number(freshValue.join("")); 9 | }; 10 | -------------------------------------------------------------------------------- /server/db/models/index.js: -------------------------------------------------------------------------------- 1 | const User = require("./user"); 2 | const Statistic = require("./statistic"); 3 | 4 | // associations 5 | 6 | Statistic.belongsTo(User, { 7 | as: "user", 8 | onDelete: "cascade", 9 | foreignKey: "user_id", 10 | }); 11 | 12 | User.hasMany(Statistic, { 13 | as: "statistics", 14 | onDelete: "cascade", 15 | foreignKey: "user_id", 16 | }); 17 | 18 | module.exports = { 19 | User, 20 | Statistic, 21 | }; 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | 7 | # production 8 | /build/ 9 | 10 | # misc 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | server/.env 22 | client/cypress/videos 23 | client/cypress/screenshots 24 | -------------------------------------------------------------------------------- /client/src/components/Main/StatisticChart/CategoryChart/GlobalChart.js: -------------------------------------------------------------------------------- 1 | import { Box } from "@material-ui/core"; 2 | import { Doughnut } from "react-chartjs-2"; 3 | import getDataSet from "./helpers/getDataSet"; 4 | 5 | export default function ({ data }) { 6 | return ( 7 | <> 8 |

Global Chart

9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | } 18 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/calcPCT/getPCTByFemale.js: -------------------------------------------------------------------------------- 1 | const getFreshNumber = require("../getFreshNumber"); 2 | 3 | module.exports = function (users) { 4 | // Split users by gender 5 | const femalePercentage = 6 | (users.filter((user) => user.gender === "female").length / users.length) * 7 | 100; 8 | 9 | // Calculate PCT for each category 10 | return { 11 | Female: getFreshNumber(femalePercentage), 12 | Other: getFreshNumber(100 - femalePercentage), 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /server/db/models/statistic.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require("sequelize"); 2 | const { DataTypes } = Sequelize; 3 | const db = require("../db"); 4 | 5 | const Statistic = db.define( 6 | "Statistic", 7 | { 8 | statistic: { type: DataTypes.TEXT }, 9 | }, 10 | { 11 | createdAt: "created_at", 12 | updatedAt: "updated_at", 13 | deletedAt: "deleted_at", 14 | paranoid: true, 15 | underscored: true, 16 | tableName: "statistic", 17 | } 18 | ); 19 | 20 | module.exports = Statistic; 21 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/calcPCT/getPCTByName.js: -------------------------------------------------------------------------------- 1 | const getFreshNumber = require("../getFreshNumber"); 2 | 3 | module.exports = function (users) { 4 | // Split users by name 5 | const lastNameNToZPercentage = 6 | (users.filter((user) => /^[N-Z]/.test(user.name.last)).length / 7 | users.length) * 8 | 100; 9 | 10 | // Calculate PCT for each category 11 | return { 12 | "N-Z": getFreshNumber(lastNameNToZPercentage), 13 | "A-M": getFreshNumber(100 - lastNameNToZPercentage), 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /server/db/db.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require("sequelize"); 2 | 3 | const db = new Sequelize( 4 | process.env.DATABASE_URL || 5 | // "postgres://postgres:postgres@localhost:5432/population", 6 | "postgres://ggasnuadbwhkrp:bf430ef220df4299010645b8984c0721373de79bc5d379b6b99e1d277faae634@ec2-34-225-159-178.compute-1.amazonaws.com:5432/dbm88fkif79epo", 7 | 8 | { 9 | logging: false, 10 | dialectOptions: { 11 | ssl: { 12 | rejectUnauthorized: false 13 | } 14 | } 15 | } 16 | ); 17 | 18 | module.exports = db; 19 | -------------------------------------------------------------------------------- /client/src/themes/theme.js: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@material-ui/core"; 2 | 3 | export const theme = createTheme({ 4 | typography: { 5 | fontFamily: "Open Sans, sans-serif", 6 | fontSize: 14, 7 | button: { 8 | textTransform: "none", 9 | letterSpacing: 0, 10 | fontWeight: "bold", 11 | }, 12 | }, 13 | overrides: { 14 | MuiInput: { 15 | input: { 16 | fontWeight: "bold", 17 | }, 18 | }, 19 | }, 20 | palette: { 21 | primary: { main: "#3A8DFF" }, 22 | secondary: { main: "#B0B0B0" }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: gitpod/workspace-postgres 2 | tasks: 3 | - name: Backend 4 | before: cd server 5 | env: 6 | SESSION_SECRET: oiwnewerJlw70238974 7 | DATABASE_URL: postgres://localhost/postgres 8 | init: | 9 | npm install 10 | npm run seed 11 | command: npm run dev 12 | - name: Frontend 13 | before: cd client 14 | init: | 15 | npm install 16 | command: DANGEROUSLY_DISABLE_HOST_CHECK=true npm start 17 | ports: 18 | - port: 3000 19 | onOpen: open-browser 20 | visibility: public 21 | - port: 3001 22 | onOpen: notify 23 | visibility: public 24 | - port: 5432 25 | onOpen: ignore 26 | visibility: private 27 | -------------------------------------------------------------------------------- /server/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const createError = require("http-errors"); 2 | const jwt = require("jsonwebtoken"); 3 | const { User } = require("../db/models"); 4 | 5 | module.exports = function (req, res, next) { 6 | const token = req.headers["x-access-token"]; 7 | if (token) { 8 | jwt.verify(token, process.env.SESSION_SECRET, (err, decoded) => { 9 | if (err) { 10 | return next(); 11 | } 12 | User.findOne({ 13 | where: { id: decoded.id }, 14 | }).then((user) => { 15 | if (user && typeof user !== "string") req.user = user.toJSON(); 16 | return next(); 17 | }); 18 | }); 19 | } else { 20 | next(createError(401)); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /client/src/components/Main/StatisticChart/StatisticChart.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Box } from "@material-ui/core"; 3 | import GlobalChart from "./CategoryChart/GlobalChart"; 4 | import CountryChart from "./CategoryChart/CountryChart"; 5 | import StateChart from "./CategoryChart/StateChart"; 6 | 7 | export default function ({ category, statistic }) { 8 | return ( 9 | <> 10 |

Chart by {category}

11 | 12 | {statistic && ( 13 | <> 14 | 15 | 16 | 17 | 18 | )} 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MuiThemeProvider } from "@material-ui/core"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import { Box } from "@material-ui/core"; 5 | 6 | import { theme } from "./themes/theme"; 7 | import Routes from "./routes"; 8 | import axios from "axios"; 9 | 10 | axios.interceptors.request.use(async function (config) { 11 | const token = await localStorage.getItem("statistic-token"); 12 | config.headers["x-access-token"] = token; 13 | 14 | return config; 15 | }); 16 | 17 | function App() { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /server/routes/api/user/userHandler.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { User } = require("../../../db/models"); 3 | const { Op } = require("sequelize"); 4 | 5 | // find users by username 6 | router.get("/:username", async (req, res, next) => { 7 | try { 8 | const { username } = req.params; 9 | 10 | const users = await User.findAll({ 11 | where: { 12 | username: { 13 | [Op.substring]: username, 14 | }, 15 | id: { 16 | [Op.not]: req.user.id, 17 | }, 18 | }, 19 | }); 20 | 21 | for (let i = 0; i < users.length; i++) { 22 | const userJSON = users[i].toJSON(); 23 | users[i] = userJSON; 24 | } 25 | res.json(users); 26 | } catch (error) { 27 | next(error); 28 | } 29 | }); 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /client/src/components/Main/StatisticChart/CategoryChart/helpers/getDataSet.js: -------------------------------------------------------------------------------- 1 | export default function (data) { 2 | return { 3 | labels: Object.keys(data), 4 | datasets: [ 5 | { 6 | data: Object.values(data), 7 | backgroundColor: [ 8 | "rgba(255, 99, 132, 1)", 9 | "rgba(54, 162, 235, 1)", 10 | "rgba(255, 206, 86, 1)", 11 | "rgba(75, 192, 192, 1)", 12 | "rgba(153, 102, 255, 1)", 13 | "rgba(255, 159, 64, 1)", 14 | ], 15 | hoverBackgroundColor: [ 16 | "rgba(255, 99, 132, 0.5)", 17 | "rgba(54, 162, 235, 0.5)", 18 | "rgba(255, 206, 86, 0.5)", 19 | "rgba(75, 192, 192, 0.5)", 20 | "rgba(153, 102, 255, 0.5)", 21 | "rgba(255, 159, 64, 0.5)", 22 | ], 23 | }, 24 | ], 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /client/src/components/Main/StatisticChart/CategoryChart/StateChart.js: -------------------------------------------------------------------------------- 1 | import { Box } from "@material-ui/core"; 2 | import { Doughnut } from "react-chartjs-2"; 3 | import getDataSet from "./helpers/getDataSet"; 4 | 5 | export default function ({ data }) { 6 | return ( 7 | <> 8 |

State Chart

9 | 15 | {Object.entries(data).map(([state, data], index) => ( 16 | 22 | {state} 23 | 24 | 25 | ))} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/Main/StatisticChart/CategoryChart/CountryChart.js: -------------------------------------------------------------------------------- 1 | import { Box } from "@material-ui/core"; 2 | import { Doughnut } from "react-chartjs-2"; 3 | import getDataSet from "./helpers/getDataSet"; 4 | 5 | export default function ({ data }) { 6 | return ( 7 | <> 8 |

Country Chart

9 | 15 | {Object.entries(data).map(([country, data], index) => ( 16 | 22 | {country} 23 | 24 | 25 | ))} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/Main/StatisticChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import StatisticChart from "./StatisticChart"; 3 | 4 | const renderChart = (type, data, index) => { 5 | switch (type) { 6 | case "byFemale": 7 | return ; 8 | case "byLastName": 9 | return ( 10 | 11 | ); 12 | case "byAge": 13 | return ; 14 | } 15 | 16 | return ""; 17 | }; 18 | 19 | export default function ({ statistic }) { 20 | return ( 21 | <> 22 | {statistic && 23 | Object.entries(statistic).map(([key, data], index) => 24 | renderChart(key, data, index) 25 | )} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "nodemon ./bin/www", 7 | "debug": "nodemon --inspect ./bin/www", 8 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --exit --timeout 30000", 9 | "seed": "node db/seed.js" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.27.2", 13 | "connect-session-sequelize": "^7.0.4", 14 | "cookie-parser": "^1.4.5", 15 | "dotenv": "^8.2.0", 16 | "express": "^4.17.1", 17 | "express-session": "^1.17.1", 18 | "http-errors": "~1.8.0", 19 | "jsonwebtoken": "^8.5.1", 20 | "morgan": "^1.10.0", 21 | "nodemon": "^2.0.6", 22 | "pg": "^8.5.1", 23 | "sequelize": "^6.3.5", 24 | "socket.io": "^3.0.4" 25 | }, 26 | "devDependencies": { 27 | "chai": "^4.2.0", 28 | "chai-http": "^4.3.0", 29 | "mocha": "^8.2.1" 30 | }, 31 | "engines": { 32 | "node": "16.x" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "server/app.js", 3 | "scripts": { 4 | "frontend": "cd client && npm start", 5 | "api": "cd server && nodemon app.js", 6 | "dev": "concurrently --kill-others-on-fail \"npm run api\" \"npm run frontend\"", 7 | "heroku-postbuild": "cd client && npm install && npm run build" 8 | }, 9 | "dependencies": { 10 | "axios": "^0.27.2", 11 | "connect-session-sequelize": "^7.0.4", 12 | "cookie-parser": "^1.4.5", 13 | "dotenv": "^8.2.0", 14 | "express": "^4.17.1", 15 | "express-session": "^1.17.1", 16 | "http-errors": "~1.8.0", 17 | "jsonwebtoken": "^8.5.1", 18 | "morgan": "^1.10.0", 19 | "nodemon": "^2.0.6", 20 | "pg": "^8.5.1", 21 | "sequelize": "^6.3.5", 22 | "socket.io": "^3.0.4" 23 | }, 24 | "devDependencies": { 25 | "chai": "^4.2.0", 26 | "chai-http": "^4.3.0", 27 | "mocha": "^8.2.1" 28 | }, 29 | "engines": { 30 | "node": "16.x" 31 | } 32 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.0", 7 | "@material-ui/icons": "^4.11.2", 8 | "axios": "^0.21.0", 9 | "chart.js": "^3.8.0", 10 | "moment": "^2.29.1", 11 | "react": "^17.0.1", 12 | "react-chartjs-2": "^4.2.0", 13 | "react-dom": "^17.0.1", 14 | "react-router-dom": "^5.2.0", 15 | "react-scripts": "^4.0.0", 16 | "socket.io-client": "^3.0.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "proxy": "http://localhost:3001", 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/calcPCT/getPCTByAge.js: -------------------------------------------------------------------------------- 1 | const getFreshNumber = require("../getFreshNumber"); 2 | 3 | function getLevel(number) { 4 | const levelLimit = [16, 26, 46, 66, 86]; 5 | if (number < 0) throw new Error("Age can't be negative"); 6 | 7 | for (let i = 0; i < levelLimit.length; i++) 8 | if (number < levelLimit[i]) return i; 9 | 10 | return levelLimit.length; 11 | } 12 | 13 | module.exports = function (users) { 14 | const keyPerLevel = ["< 16", "16-25", "26-45", "46-65", "66-85", "86 <"]; 15 | // Split users by age 16 | const agePercentage = users.reduce( 17 | (totCount, user) => { 18 | const userKey = keyPerLevel[getLevel(user.dob.age)]; 19 | return { ...totCount, [userKey]: totCount[userKey] + 1 }; 20 | }, 21 | keyPerLevel.reduce((totLevel, key) => { 22 | return { ...totLevel, [key]: 0 }; 23 | }, {}) 24 | ); 25 | 26 | // Calculate PCT for each category 27 | for (const key of Object.keys(agePercentage)) 28 | agePercentage[key] = getFreshNumber( 29 | (agePercentage[key] / users.length) * 100 30 | ); 31 | 32 | return agePercentage; 33 | }; 34 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/getCSVForm.js: -------------------------------------------------------------------------------- 1 | // There are lots of modules that converts json to csv format 2 | // But as our data is complex, we should have our own converter 3 | module.exports = function getCSVFormat(data, category) { 4 | const maxWidth = Object.entries(data.global).length + 1; 5 | const result = []; 6 | const addSplitterAndHeader = () => { 7 | result.push(new Array(maxWidth - 1).fill(",").join("")); // Splitter 8 | result.push(`,${Object.keys(data.global).join(",")}`); // Header 9 | }; 10 | 11 | result.push(`${category}${new Array(maxWidth - 1).fill(",").join("")}`); 12 | 13 | // Global Statistic 14 | addSplitterAndHeader(); 15 | result.push(`Global,${Object.values(data.global).join(",")}`); 16 | 17 | // Statistic By Countires 18 | addSplitterAndHeader(); 19 | for (const [country, statistic] of Object.entries(data.country)) 20 | result.push(`${country},${Object.values(statistic).join(",")}`); 21 | 22 | // Statistic By States 23 | addSplitterAndHeader(); 24 | for (const [state, statistic] of Object.entries(data.state)) 25 | result.push(`${state},${Object.values(statistic).join(",")}`); 26 | 27 | return result; 28 | }; 29 | -------------------------------------------------------------------------------- /client/src/components/SnackbarError.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, Snackbar } from "@material-ui/core"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import Close from "@material-ui/icons/Close"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | snackbar: { 8 | backgroundColor: "red", 9 | fontWeight: "bold", 10 | }, 11 | icon: { 12 | color: "white", 13 | }, 14 | })); 15 | 16 | const SnackbarError = ({ setSnackBarOpen, errorMessage, snackBarOpen }) => { 17 | const classes = useStyles(); 18 | return ( 19 | setSnackBarOpen(false)} 22 | message={errorMessage || "Sorry, an error occured. Please try again"} 23 | action={ 24 | 25 | 34 | 35 | } 36 | ContentProps={{ 37 | classes: { 38 | root: classes.snackbar, 39 | }, 40 | }} 41 | /> 42 | ); 43 | }; 44 | 45 | export default SnackbarError; 46 | -------------------------------------------------------------------------------- /server/db/seed.js: -------------------------------------------------------------------------------- 1 | const db = require("./db"); 2 | const User = require("./models/user"); 3 | const Statistic = require("./models/statistic"); 4 | const StatisticRequest = require("./models/statisticRequest"); 5 | 6 | async function seed() { 7 | await db.sync({ force: true }); 8 | console.log("db synced!"); 9 | 10 | // const thomas = await User.create({ 11 | // username: "thomas", 12 | // email: "thomas@email.com", 13 | // password: "123456", 14 | // photoUrl: 15 | // "https://res.cloudinary.com/dmlvthmqr/image/upload/v1607914467/messenger/thomas_kwzerk.png", 16 | // }); 17 | 18 | // const santiago = await User.create({ 19 | // username: "santiago", 20 | // email: "santiago@email.com", 21 | // password: "123456", 22 | // photoUrl: 23 | // "https://res.cloudinary.com/dmlvthmqr/image/upload/v1607914466/messenger/775db5e79c5294846949f1f55059b53317f51e30_s3back.png", 24 | // }); 25 | 26 | console.log(`seeding ended`); 27 | } 28 | 29 | async function runSeed() { 30 | console.log("seeding..."); 31 | try { 32 | await seed(); 33 | } catch (err) { 34 | console.error(err); 35 | process.exitCode = 1; 36 | } finally { 37 | console.log("closing db connection"); 38 | await db.close(); 39 | console.log("db connection closed"); 40 | } 41 | } 42 | 43 | if (module === require.main) { 44 | runSeed(); 45 | } 46 | -------------------------------------------------------------------------------- /server/db/models/user.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require("sequelize"); 2 | const db = require("../db"); 3 | const crypto = require("crypto"); 4 | 5 | const User = db.define("user", { 6 | username: { 7 | type: Sequelize.STRING, 8 | unique: true, 9 | allowNull: false, 10 | }, 11 | email: { 12 | type: Sequelize.STRING, 13 | unique: true, 14 | alloWNull: false, 15 | validate: { 16 | isEmail: true, 17 | }, 18 | }, 19 | photoUrl: { 20 | type: Sequelize.STRING, 21 | }, 22 | password: { 23 | type: Sequelize.STRING, 24 | validate: { 25 | min: 6, 26 | }, 27 | allowNull: false, 28 | get() { 29 | return () => this.getDataValue("password"); 30 | }, 31 | }, 32 | salt: { 33 | type: Sequelize.STRING, 34 | get() { 35 | return () => this.getDataValue("salt"); 36 | }, 37 | }, 38 | }); 39 | 40 | User.prototype.correctPassword = function (password) { 41 | return User.encryptPassword(password, this.salt()) === this.password(); 42 | }; 43 | 44 | User.createSalt = function () { 45 | return crypto.randomBytes(16).toString("base64"); 46 | }; 47 | 48 | User.encryptPassword = function (plainPassword, salt) { 49 | return crypto 50 | .createHash("RSA-SHA256") 51 | .update(plainPassword) 52 | .update(salt) 53 | .digest("hex"); 54 | }; 55 | 56 | const setSaltAndPassword = (user) => { 57 | if (user.changed("password")) { 58 | user.salt = User.createSalt(); 59 | user.password = User.encryptPassword(user.password(), user.salt()); 60 | } 61 | }; 62 | 63 | User.beforeCreate(setSaltAndPassword); 64 | User.beforeUpdate(setSaltAndPassword); 65 | User.beforeBulkCreate((users) => { 66 | users.forEach(setSaltAndPassword); 67 | }); 68 | 69 | module.exports = User; 70 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 17 | 26 | Messenger 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const createError = require("http-errors"); 2 | const express = require("express"); 3 | const { join } = require("path"); 4 | const logger = require("morgan"); 5 | const session = require("express-session"); 6 | const SequelizeStore = require("connect-session-sequelize")(session.Store); 7 | const db = require("./db"); 8 | const AuthMiddleware = require("./middlewares/auth"); 9 | 10 | // create store for sessions to persist in database 11 | const sessionStore = new SequelizeStore({ db }); 12 | 13 | const { json, urlencoded } = express; 14 | 15 | const app = express(); 16 | 17 | app.use(logger("dev")); 18 | app.use(json()); 19 | app.use(urlencoded({ extended: false })); 20 | const path = require('path') 21 | 22 | // Serve static files from the React frontend app 23 | app.use(express.static(path.join(__dirname, '../client/build'))) 24 | 25 | // require api routes here after I create them 26 | app.use("/auth", require("./routes/auth")); 27 | app.use("/api", AuthMiddleware, require("./routes/api")); 28 | 29 | // catch 404 and forward to error handler 30 | app.use(function (req, res, next) { 31 | next(createError(404)); 32 | }); 33 | 34 | // error handler 35 | app.use(function (err, req, res, next) { 36 | // set locals, only providing error in development 37 | console.log(err); 38 | res.locals.message = err.message; 39 | res.locals.error = req.app.get("env") === "development" ? err : {}; 40 | 41 | // render the error page 42 | res.status(err.status || 500); 43 | res.json({ error: err }); 44 | }); 45 | 46 | // AFTER defining routes: Anything that doesn't match what's above, send back index.html; (the beginning slash ('/') in the string is important!) 47 | app.get('*', (req, res) => { 48 | res.sendFile(path.join(__dirname + '/../client/build/index.html')) 49 | }) 50 | 51 | module.exports = { app, sessionStore }; 52 | -------------------------------------------------------------------------------- /server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* Sets up the environment variables from your .env file*/ 4 | require("dotenv").config(); 5 | 6 | /** 7 | * Module dependencies. 8 | */ 9 | 10 | const { app, sessionStore } = require("../app"); 11 | const http = require("http"); 12 | const db = require("../db"); 13 | 14 | /** 15 | * Get port from environment and store in Express. 16 | */ 17 | 18 | const port = normalizePort(process.env.PORT || "3001"); 19 | app.set("port", port); 20 | 21 | /** 22 | * Create HTTP server. 23 | */ 24 | 25 | const server = http.createServer(app); 26 | 27 | /** 28 | * Listen on provided port, on all network interfaces, and sync database. 29 | */ 30 | 31 | sessionStore 32 | .sync() 33 | .then(() => db.sync()) 34 | .then(() => { 35 | server.listen(port); 36 | server.on("error", onError); 37 | server.on("listening", onListening); 38 | }); 39 | 40 | /** 41 | * Normalize a port into a number, string, or false. 42 | */ 43 | 44 | function normalizePort(val) { 45 | const port = parseInt(val, 10); 46 | 47 | if (isNaN(port)) { 48 | // named pipe 49 | return val; 50 | } 51 | 52 | if (port >= 0) { 53 | // port number 54 | return port; 55 | } 56 | 57 | return false; 58 | } 59 | 60 | /** 61 | * Event listener for HTTP server "error" event. 62 | */ 63 | 64 | function onError(error) { 65 | if (error.syscall !== "listen") { 66 | throw error; 67 | } 68 | 69 | const bind = typeof port === "string" ? "Pipe " + port : "Port " + port; 70 | 71 | // handle specific listen errors with friendly messages 72 | switch (error.code) { 73 | case "EACCES": 74 | console.error(bind + " requires elevated privileges"); 75 | process.exit(1); 76 | break; 77 | case "EADDRINUSE": 78 | console.error(bind + " is already in use"); 79 | process.exit(1); 80 | break; 81 | default: 82 | throw error; 83 | } 84 | } 85 | 86 | /** 87 | * Event listener for HTTP server "listening" event. 88 | */ 89 | 90 | function onListening() { 91 | const addr = server.address(); 92 | const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; 93 | 94 | console.log("Listening on " + bind); 95 | } 96 | -------------------------------------------------------------------------------- /server/routes/api/request/helpers/statisticCalculator.js: -------------------------------------------------------------------------------- 1 | module.exports = class StatisticCalculator { 2 | constructor(PCTengine) { 3 | this._calcPCT = PCTengine; 4 | } 5 | 6 | _getGlobalStatistic(fakeUsers) { 7 | return this._calcPCT(fakeUsers); 8 | } 9 | 10 | _getStatisticByCountry(fakeUsers) { 11 | return Object.entries( 12 | // Group by country 13 | fakeUsers.reduce( 14 | (totCount, user) => 15 | user.location.country in totCount 16 | ? { 17 | ...totCount, 18 | [user.location.country]: [ 19 | ...totCount[user.location.country], 20 | user, 21 | ], 22 | } 23 | : { ...totCount, [user.location.country]: [user] }, 24 | {} 25 | ) 26 | ) 27 | .sort((a, b) => b[1].length - a[1].length) // Sort by population 28 | .slice(0, 5) // Choose top 5 29 | .reduce((totData, usersInOneCountry) => { 30 | // Calculate PCT 31 | return { 32 | ...totData, 33 | [usersInOneCountry[0]]: this._calcPCT(usersInOneCountry[1]), 34 | }; 35 | }, {}); 36 | } 37 | 38 | _getStatisticByState(fakeUsers) { 39 | return Object.entries( 40 | // Group by state 41 | fakeUsers.reduce( 42 | (totCount, user) => 43 | user.location.state in totCount 44 | ? { 45 | ...totCount, 46 | [user.location.state]: [...totCount[user.location.state], user], 47 | } 48 | : { ...totCount, [user.location.state]: [user] }, 49 | {} 50 | ) 51 | ) 52 | .sort((a, b) => b[1].length - a[1].length) // Sort by population 53 | .slice(0, 5) // Choose top 5 54 | .reduce((totData, usersInOneState) => { 55 | // Calculate PCT 56 | return { 57 | ...totData, 58 | [usersInOneState[0]]: this._calcPCT(usersInOneState[1]), 59 | }; 60 | }, {}); 61 | } 62 | 63 | calc(fakeUsers) { 64 | return { 65 | global: this._getGlobalStatistic(fakeUsers), 66 | country: this._getStatisticByCountry(fakeUsers), 67 | state: this._getStatisticByState(fakeUsers), 68 | }; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /client/src/components/Auth/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Link, useHistory } from "react-router-dom"; 3 | import { 4 | Grid, 5 | Box, 6 | Typography, 7 | Button, 8 | FormControl, 9 | TextField, 10 | } from "@material-ui/core"; 11 | 12 | const Login = ({ user, login }) => { 13 | const history = useHistory(); 14 | 15 | const handleLogin = async (event) => { 16 | event.preventDefault(); 17 | const form = event.currentTarget; 18 | const formElements = form.elements; 19 | const username = formElements.username.value; 20 | const password = formElements.password.value; 21 | 22 | await login({ username, password }); 23 | }; 24 | 25 | useEffect(() => { 26 | if (user && user.id) history.push("/"); 27 | }, [user, history]); 28 | 29 | return ( 30 | 31 | 32 | 33 | Need to register? 34 | 35 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 59 | 60 | 61 | 69 | 70 | 71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default Login; 78 | -------------------------------------------------------------------------------- /server/routes/auth/index.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { User } = require("../../db/models"); 3 | const jwt = require("jsonwebtoken"); 4 | const AuthMiddleware = require("../../middlewares/auth"); 5 | 6 | router.post("/register", async (req, res, next) => { 7 | try { 8 | // expects {username, email, password} in req.body 9 | const { username, password, email } = req.body; 10 | 11 | if (!username || !password || !email) { 12 | return res 13 | .status(400) 14 | .json({ error: "Username, password, and email required" }); 15 | } 16 | 17 | if (password.length < 6) { 18 | return res 19 | .status(400) 20 | .json({ error: "Password must be at least 6 characters" }); 21 | } 22 | 23 | const user = await User.create(req.body); 24 | 25 | const token = jwt.sign( 26 | { id: user.dataValues.id }, 27 | process.env.SESSION_SECRET, 28 | { expiresIn: 86400 } 29 | ); 30 | res.json({ 31 | ...user.dataValues, 32 | token, 33 | }); 34 | } catch (error) { 35 | if (error.name === "SequelizeUniqueConstraintError") { 36 | return res.status(401).json({ error: "User already exists" }); 37 | } else if (error.name === "SequelizeValidationError") { 38 | return res.status(401).json({ error: "Validation error" }); 39 | } else next(error); 40 | } 41 | }); 42 | 43 | router.post("/login", async (req, res, next) => { 44 | try { 45 | // expects username and password in req.body 46 | const { username, password } = req.body; 47 | if (!username || !password) 48 | return res.status(400).json({ error: "Username and password required" }); 49 | 50 | const user = await User.findOne({ 51 | where: { 52 | username: req.body.username, 53 | }, 54 | }); 55 | 56 | if (!user) { 57 | console.log({ error: `No user found for username: ${username}` }); 58 | res.status(401).json({ error: "Wrong username and/or password" }); 59 | } else if (!user.correctPassword(password)) { 60 | console.log({ error: "Wrong username and/or password" }); 61 | res.status(401).json({ error: "Wrong username and/or password" }); 62 | } else { 63 | const token = jwt.sign( 64 | { id: user.dataValues.id }, 65 | process.env.SESSION_SECRET, 66 | { expiresIn: 86400 } 67 | ); 68 | res.json({ 69 | ...user.dataValues, 70 | token, 71 | }); 72 | } 73 | } catch (error) { 74 | next(error); 75 | } 76 | }); 77 | 78 | router.delete("/logout", AuthMiddleware, (req, res, next) => { 79 | res.sendStatus(204); 80 | }); 81 | 82 | router.get("/user", AuthMiddleware, (req, res, next) => { 83 | if (req.user) { 84 | return res.json(req.user); 85 | } else { 86 | return res.json({}); 87 | } 88 | }); 89 | 90 | module.exports = router; 91 | -------------------------------------------------------------------------------- /server/routes/api/request/requestHandler.js: -------------------------------------------------------------------------------- 1 | const router = require("express").Router(); 2 | const { Statistic } = require("../../../db/models"); 3 | const { Op } = require("sequelize"); 4 | const axios = require("axios"); 5 | const { 6 | StatisticCalculator, 7 | getCSVForm, 8 | getPCTByAge, 9 | getPCTByName, 10 | getPCTByFemale, 11 | } = require("./helpers"); 12 | 13 | const PCTengine = { 14 | byFemale: getPCTByFemale, 15 | byLastName: getPCTByName, 16 | byAge: getPCTByAge, 17 | }; 18 | 19 | // find users by username 20 | router.get("/", async (req, res, next) => { 21 | try { 22 | const userId = req.user.id; 23 | 24 | const statistics = await Statistic.findAll({ 25 | where: { 26 | user_id: userId, 27 | }, 28 | attributes: ["id", "created_at"], 29 | order: [["created_at", "DESC"]], 30 | }); 31 | 32 | res.json(statistics); 33 | } catch (error) { 34 | next(error); 35 | } 36 | }); 37 | 38 | // Get the new user data from randomuser api and calc statistic 39 | router.post("/new", async (req, res, next) => { 40 | try { 41 | const { user } = req; 42 | const { desiredTypes } = req.body; 43 | const fakeUsers = (await axios("https://randomuser.me/api/?results=1000")) 44 | .data.results; 45 | const result = {}; 46 | 47 | for (const [key, flag] of Object.entries(desiredTypes)) 48 | if (flag) 49 | result[key] = new StatisticCalculator(PCTengine[key]).calc(fakeUsers); 50 | 51 | const newStatistic = ( 52 | await Statistic.create({ 53 | user_id: user.id, 54 | statistic: JSON.stringify(result), 55 | }) 56 | ).toJSON(); 57 | newStatistic.statistic = JSON.parse(newStatistic.statistic); 58 | 59 | res.json(newStatistic); 60 | } catch (error) { 61 | next(error); 62 | } 63 | }); 64 | 65 | // Return json data for json format 66 | // Return csv data for csv format 67 | router.get("/:id", async (req, res, next) => { 68 | try { 69 | const { id } = req.params; 70 | const { format } = req.query; 71 | const statistic = ( 72 | await Statistic.findOne({ 73 | where: { id }, 74 | }) 75 | ).toJSON(); 76 | statistic.statistic = JSON.parse(statistic.statistic); 77 | 78 | if (format === "json") { 79 | res.json(statistic); 80 | } else { 81 | const result = []; 82 | 83 | for (const [category, statistics] of Object.entries( 84 | statistic.statistic 85 | )) { 86 | const csvForm = getCSVForm(statistics, `By ${category.slice(2)}`); 87 | if (!result.length) result.push(...csvForm); 88 | else { 89 | for (let i = 0; i < csvForm.length; i++) 90 | result[i] += `,,,${csvForm[i]}`; 91 | } 92 | } 93 | 94 | res.json(result); 95 | } 96 | } catch (error) { 97 | next(error); 98 | } 99 | }); 100 | 101 | module.exports = router; 102 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Population analyzer. 2 | 3 | Population analyzer 4 | 5 | ## Working inside a Cloud Environment 6 | 7 | ### Environment Setup 8 | 9 | This repository supports Gitpod, so you can quickly setup your dev environment in the cloud by opening `https://gitpod.io/#https://github.com//` in your browser. A Gitpod workspace looks almost identical to Visual Studio Code. 10 | ![gitpod-demo](https://user-images.githubusercontent.com/8978815/154584026-8d252528-869a-4587-8387-5db0fb6213b6.png) 11 | When you open a Gitpod page, 12 | 13 | - Setup scripts will be automatically run in two integrated terminals - one for backend and the other for frontend. 14 | - You can find Remote Explorer extension on the left edge. This extension shows the list of open ports and lets you connect to those ports by clicking the browser icon. 15 | 16 | All changes you make inside this Gitpod workspace remains in the cloud even after you close the tab. You can access your existing workspaces on [Gitpod dashboard](https://gitpod.io/workspaces), and resume your work by opening the workspace again. 17 | 18 | ### Using Git inside Gitpod 19 | 20 | You have full access to `git` CLI within Gitpod terminal, and Visual Studio Code's git integration is available too if your prefer GUI. You would need to create a new branch and push it in order to open a PR on GitHub. Some common commands are listed below: 21 | 22 | - `git checkout `: switch to a specific branch. 23 | - `git checkout -b `: create a new branch. 24 | - `git push`: push changes to the remote repository (GitHub in this case). 25 | 26 | ## Local Setup 27 | 28 | **Note:** these setup steps are not necessary when running the code on GitPod. They are only necessary when running the project on your local machine. 29 | 30 | Create the PostgreSQL database (these instructions may need to be adapted for your operating system): 31 | 32 | ``` 33 | psql 34 | CREATE DATABASE statistic; 35 | \q 36 | ``` 37 | 38 | Alternatively, if you have docker installed, you can use it to spawn a postgres instance on your machine: 39 | 40 | ``` 41 | docker run -it -p 5432:5432 -e POSTGRES_DB= -e POSTGRES_USER= -e POSTGRES_PASSWORD= postgres -c log_statement=all 42 | ``` 43 | 44 | Update db.js to connect with your local PostgreSQL set up. The [Sequelize documentation](https://sequelize.org/master/manual/getting-started.html) can help with this. 45 | 46 | Create a .env file in the server directory and add your session secret (this can be any string): 47 | 48 | ``` 49 | SESSION_SECRET = "your session secret" 50 | ``` 51 | 52 | In the server folder, install dependencies and then seed the database: 53 | 54 | ``` 55 | cd server 56 | npm install 57 | npm run seed 58 | ``` 59 | 60 | In the client folder, install dependencies: 61 | 62 | ``` 63 | cd client 64 | npm install 65 | ``` 66 | 67 | ### Running the Application Locally 68 | 69 | In one terminal, start the front end: 70 | 71 | ``` 72 | cd client 73 | npm start 74 | ``` 75 | 76 | In a separate terminal, start the back end: 77 | 78 | ``` 79 | cd server 80 | npm run dev 81 | ``` 82 | 83 | ## How to Run E2E Tests 84 | 85 | 1. Seed the database with `npm run seed` in `server` directory. 86 | 1. Start the backend server with `npm run dev` in `server` directory. 87 | 1. Start the frontend server with `npm start` in `client` directory. 88 | -------------------------------------------------------------------------------- /client/src/routes.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import axios from "axios"; 3 | import { Route, Switch, withRouter } from "react-router-dom"; 4 | 5 | import Signup from "./components/Auth/Signup.js"; 6 | import Login from "./components/Auth/Login.js"; 7 | import { SnackbarError, Home } from "./components"; 8 | import UserContext from "./context/userContext"; 9 | 10 | const Routes = (props) => { 11 | const [user, setUser] = useState({ 12 | isFetching: true, 13 | }); 14 | 15 | const [errorMessage, setErrorMessage] = useState(""); 16 | const [snackBarOpen, setSnackBarOpen] = useState(false); 17 | 18 | const login = async (credentials) => { 19 | try { 20 | const { data } = await axios.post("/auth/login", credentials); 21 | await localStorage.setItem("statistic-token", data.token); 22 | setUser(data); 23 | } catch (error) { 24 | console.error(error); 25 | setUser({ error: error.response.data.error || "Server Error" }); 26 | } 27 | }; 28 | 29 | const register = async (credentials) => { 30 | try { 31 | const { data } = await axios.post("/auth/register", credentials); 32 | await localStorage.setItem("statistic-token", data.token); 33 | setUser(data); 34 | } catch (error) { 35 | console.error(error); 36 | setUser({ error: error.response.data.error || "Server Error" }); 37 | } 38 | }; 39 | 40 | const logout = async (id) => { 41 | try { 42 | await axios.delete("/auth/logout"); 43 | await localStorage.removeItem("statistic-token"); 44 | setUser({}); 45 | } catch (error) { 46 | console.error(error); 47 | } 48 | }; 49 | 50 | // Lifecycle 51 | 52 | useEffect(() => { 53 | const fetchUser = async () => { 54 | setUser((prev) => ({ ...prev, isFetching: true })); 55 | try { 56 | const { data } = await axios.get("/auth/user"); 57 | setUser(data); 58 | } catch (error) { 59 | console.error(error); 60 | } finally { 61 | setUser((prev) => ({ ...prev, isFetching: false })); 62 | } 63 | }; 64 | 65 | fetchUser(); 66 | }, []); 67 | 68 | useEffect(() => { 69 | if (user?.error) { 70 | // check to make sure error is what we expect, in case we get an unexpected server error object 71 | if (typeof user.error === "string") { 72 | setErrorMessage(user.error); 73 | } else { 74 | setErrorMessage("Internal Server Error. Please try again"); 75 | } 76 | setSnackBarOpen(true); 77 | } 78 | }, [user?.error]); 79 | 80 | if (user?.isFetching) { 81 | return
Loading...
; 82 | } 83 | 84 | return ( 85 | 86 | {snackBarOpen && ( 87 | 92 | )} 93 | 94 | } 97 | /> 98 | } 101 | /> 102 | 105 | user?.id ? ( 106 | 107 | ) : ( 108 | 109 | ) 110 | } 111 | /> 112 | 113 | 114 | ); 115 | }; 116 | 117 | export default withRouter(Routes); 118 | -------------------------------------------------------------------------------- /client/src/components/Main/RequestDialog.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | Button, 4 | Dialog, 5 | DialogActions, 6 | DialogContent, 7 | DialogContentText, 8 | DialogTitle, 9 | FormControlLabel, 10 | Checkbox, 11 | FormGroup, 12 | RadioGroup, 13 | Radio, 14 | Box, 15 | } from "@material-ui/core"; 16 | 17 | export default function FormDialog({ requestData }) { 18 | const [open, setOpen] = useState(false); 19 | const [desiredTypes, setDesiredTypes] = useState({ 20 | byFemale: true, 21 | byLastName: true, 22 | byAge: true, 23 | }); 24 | const [requestButtonStatus, setRequestButtonStatus] = useState(true); 25 | 26 | useEffect(() => { 27 | setRequestButtonStatus( 28 | desiredTypes.byAge | desiredTypes.byFemale | desiredTypes.byLastName 29 | ); 30 | }, [desiredTypes.byAge, desiredTypes.byFemale, desiredTypes.byLastName]); 31 | 32 | const handleClickOpen = () => { 33 | setOpen(true); 34 | }; 35 | 36 | const handleClose = () => { 37 | setOpen(false); 38 | }; 39 | 40 | const handleChange = (event) => { 41 | setDesiredTypes({ 42 | ...desiredTypes, 43 | [event.target.name]: event.target.checked, 44 | }); 45 | }; 46 | 47 | const handleRequest = () => { 48 | setOpen(false); 49 | requestData({ desiredTypes }); 50 | }; 51 | 52 | return ( 53 |
54 | 57 | 62 | 63 | Request Form 64 | 65 | 66 | Select necessary statistic types 67 | 68 | 69 | 70 | 78 | } 79 | label="By female" 80 | /> 81 | 89 | } 90 | label="By last name" 91 | /> 92 | 100 | } 101 | label="By age" 102 | /> 103 | 104 | 105 | 106 | 107 | 110 | 117 | 118 | 119 | 120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /client/src/components/Auth/Signup.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Link, useHistory } from "react-router-dom"; 3 | import { 4 | Grid, 5 | Box, 6 | Typography, 7 | Button, 8 | FormControl, 9 | TextField, 10 | FormHelperText, 11 | } from "@material-ui/core"; 12 | 13 | const Signup = ({ user, register }) => { 14 | const history = useHistory(); 15 | 16 | const [formErrorMessage, setFormErrorMessage] = useState({}); 17 | 18 | const handleRegister = async (event) => { 19 | event.preventDefault(); 20 | const form = event.currentTarget; 21 | const formElements = form.elements; 22 | const username = formElements.username.value; 23 | const email = formElements.email.value; 24 | const password = formElements.password.value; 25 | const confirmPassword = formElements.confirmPassword.value; 26 | 27 | if (password !== confirmPassword) { 28 | setFormErrorMessage({ confirmPassword: "Passwords must match" }); 29 | return; 30 | } 31 | await register({ username, email, password }); 32 | }; 33 | 34 | useEffect(() => { 35 | if (user && user.id) history.push("/"); 36 | }, [user, history]); 37 | 38 | return ( 39 | 40 | 41 | 42 | Need to log in? 43 | 44 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | 60 | 61 | 62 | 63 | 64 | 71 | 72 | 73 | 74 | 75 | 83 | 84 | {formErrorMessage.confirmPassword} 85 | 86 | 87 | 88 | 89 | 90 | 98 | 99 | {formErrorMessage.confirmPassword} 100 | 101 | 102 | 103 | 111 | 112 |
113 |
114 |
115 | ); 116 | }; 117 | 118 | export default Signup; 119 | -------------------------------------------------------------------------------- /client/src/components/Main/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext } from "react"; 2 | import axios from "axios"; 3 | import { useHistory } from "react-router-dom"; 4 | import { 5 | Grid, 6 | Button, 7 | Select, 8 | MenuItem, 9 | FormControl, 10 | InputLabel, 11 | Container, 12 | Box, 13 | } from "@material-ui/core"; 14 | import { makeStyles } from "@material-ui/core/styles"; 15 | import StatisticChart from "./StatisticChart"; 16 | 17 | import RequestDialog from "./RequestDialog"; 18 | import UserContext from "../../context/userContext"; 19 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js"; 20 | 21 | ChartJS.register(ArcElement, Tooltip, Legend); 22 | 23 | const useStyles = makeStyles((theme) => ({ 24 | root: { 25 | display: "flex", 26 | flexDirection: "column", 27 | }, 28 | formElement: { 29 | width: "30rem", 30 | margin: "1rem 2rem", 31 | }, 32 | })); 33 | 34 | const Home = ({ user, logout }) => { 35 | const history = useHistory(); 36 | const User = useContext(UserContext); 37 | 38 | const classes = useStyles(); 39 | const [isLoggedIn, setIsLoggedIn] = useState(false); 40 | const [requests, setRequests] = useState([]); 41 | const [statistic, setStatistic] = useState(null); 42 | const [currentRequestId, setCurrentRequestId] = useState(""); 43 | 44 | // Lifecycle 45 | 46 | useEffect(() => { 47 | // when fetching, prevent redirect 48 | if (user?.isFetching) return; 49 | 50 | if (user && user.id) { 51 | setIsLoggedIn(true); 52 | } else { 53 | // If we were previously logged in, redirect to login instead of register 54 | if (isLoggedIn) history.push("/login"); 55 | else history.push("/register"); 56 | } 57 | }, [user, history, isLoggedIn]); 58 | 59 | useEffect(() => { 60 | const fetchPastRequests = async () => { 61 | try { 62 | const { data } = await axios.get("/api/requests"); 63 | setCurrentRequestId(data[0].id); 64 | setRequests(data); 65 | } catch (error) { 66 | console.error(error); 67 | } 68 | }; 69 | if (!user.isFetching) { 70 | fetchPastRequests(); 71 | } 72 | }, [user]); 73 | 74 | useEffect(async () => { 75 | const { data } = await axios.get( 76 | `/api/requests/${currentRequestId}?format=json` 77 | ); 78 | setStatistic(data.statistic); 79 | }, [currentRequestId]); 80 | 81 | const handleLogout = async () => { 82 | if (user && user.id) { 83 | await logout(user.id); 84 | } 85 | }; 86 | 87 | const handleChange = (e) => { 88 | setCurrentRequestId(e.target.value); 89 | }; 90 | 91 | const requestData = async ({ desiredTypes }) => { 92 | const { data } = await axios.post("/api/requests/new", { desiredTypes }); 93 | setRequests([data, ...requests]); 94 | setCurrentRequestId(data.id); 95 | }; 96 | 97 | function downloadCsv(csvString, filename) { 98 | var blob = new Blob([csvString]); 99 | if (window.navigator.msSaveOrOpenBlob) { 100 | window.navigator.msSaveBlob(blob, "filename.csv"); 101 | } else { 102 | var a = window.document.createElement("a"); 103 | 104 | a.href = window.URL.createObjectURL(blob, { 105 | type: "text/plain", 106 | }); 107 | a.download = filename; 108 | document.body.appendChild(a); 109 | a.click(); 110 | document.body.removeChild(a); 111 | } 112 | } 113 | 114 | const download = async (id) => { 115 | const { data } = await axios.get(`/api/requests/${id}?format=csv`); 116 | const createdAt = requests.find( 117 | (req) => req.id === currentRequestId 118 | ).created_at; 119 | 120 | downloadCsv( 121 | data.join("\n"), 122 | `report-${createdAt.slice(0, 10)} ${createdAt.slice(11, 19)}.csv` 123 | ); 124 | }; 125 | 126 | return ( 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 142 | 143 | 144 | 145 | 148 | 149 | 150 | 151 | Request Date 152 | 166 | 167 | 168 | 169 | 170 | ); 171 | }; 172 | 173 | export default Home; 174 | -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener("load", () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | "This web app is being served cache-first by a service " + 46 | "worker. To learn more, visit https://bit.ly/CRA-PWA" 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === "installed") { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | "New content is available and will be used when all " + 74 | "tabs for this page are closed. See https://bit.ly/CRA-PWA." 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log("Content is cached for offline use."); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error("Error during service worker registration:", error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then((response) => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get("content-type"); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf("javascript") === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then((registration) => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | "No internet connection found. App is running in offline mode." 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ("serviceWorker" in navigator) { 131 | navigator.serviceWorker.ready.then((registration) => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | --------------------------------------------------------------------------------