├── .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 |
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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------