├── README.md ├── babel.config.js ├── src ├── jest.config.js ├── client │ ├── model │ │ ├── index.js │ │ ├── metrics.js │ │ ├── points.js │ │ └── user.js │ ├── components │ │ ├── metrics │ │ │ ├── MetricsContainer.jsx │ │ │ └── TaskBar.jsx │ │ ├── home │ │ │ ├── HomePage.jsx │ │ │ ├── MetricTile.jsx │ │ │ ├── DataPopUp.jsx │ │ │ ├── MetricList.jsx │ │ │ └── MetricTile.test.js │ │ ├── goals │ │ │ ├── GoalPage.jsx │ │ │ ├── GoalsList.jsx │ │ │ └── GoalEditor.jsx │ │ ├── auth │ │ │ ├── AuthProvider.jsx │ │ │ ├── Login.test.js │ │ │ ├── Login.jsx │ │ │ └── Signup.jsx │ │ └── NavBar.jsx │ ├── index.html │ ├── services │ │ ├── metricService.js │ │ ├── pointService.js │ │ └── authService.js │ ├── index.js │ ├── App.jsx │ ├── oldcomponents │ │ ├── Graph.js │ │ ├── PrivateRoute.js │ │ ├── NavBar.js │ │ ├── MainComponent.js │ │ ├── home │ │ │ ├── DataSetTile.js │ │ │ └── HomePage.jsx │ │ ├── Metrics.js │ │ ├── SetEditor.js │ │ └── Login.js │ ├── utils │ │ └── mockData.js │ └── utils.js ├── jest.setup.js └── server │ ├── constants │ ├── colors.js │ └── errors.js │ ├── routes │ └── api │ │ ├── index.js │ │ ├── auth.js │ │ └── datasets.js │ ├── models │ ├── User.js │ ├── DataSet.js │ └── Point.js │ ├── utils.js │ ├── server.js │ └── controllers │ ├── auth.js │ ├── dataset.js │ └── point.js ├── .prettierrc ├── .gitignore ├── .eslintrc ├── LICENSE ├── webpack.config.js ├── docs ├── data-model-example.js ├── front-end-tree.text └── Planning.md └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # my-life-in-data -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/jest.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | setupFilesAfterEnv: ['./jest.setup.js'], 5 | }; 6 | -------------------------------------------------------------------------------- /src/client/model/index.js: -------------------------------------------------------------------------------- 1 | import user from './user'; 2 | import metrics from './metrics'; 3 | 4 | export default { 5 | user, 6 | metrics, 7 | }; 8 | -------------------------------------------------------------------------------- /src/jest.setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "bracketSpacing": true, 6 | "semi": true, 7 | "arrowParens": "always", 8 | "trailingComma": "es5" 9 | } 10 | -------------------------------------------------------------------------------- /src/server/constants/colors.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'pink', 3 | 'red', 4 | 'orange', 5 | 'yellow', 6 | 'green', 7 | 'blue', 8 | 'purple', 9 | 'brown', 10 | 'gray', 11 | 'black', 12 | ]; 13 | -------------------------------------------------------------------------------- /src/client/components/metrics/MetricsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TaskBar from './TaskBar'; 3 | 4 | export default function MetricsContainer() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/client/components/home/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from '../NavBar'; 3 | import MetricList from './MetricList'; 4 | 5 | export default function HomePage() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/server/routes/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const authController = require('../../controllers/auth'); 4 | 5 | router.use('/datasets', authController.authenticate, require('./datasets')); 6 | router.use(require('./auth')); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | My Life In Data 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules/ 8 | 9 | # Optional npm cache directory 10 | .npm 11 | 12 | # Build directory 13 | dist/ 14 | 15 | # VSCode 16 | .vscode 17 | 18 | # Optional eslint cache 19 | .eslintcache 20 | 21 | # Optional REPL history 22 | .node_repl_history 23 | 24 | # macOS meta 25 | .DS_Store 26 | 27 | # Env 28 | .env -------------------------------------------------------------------------------- /src/client/services/metricService.js: -------------------------------------------------------------------------------- 1 | import { fetchJSON } from '../utils'; 2 | 3 | export default { 4 | /** Gets all the metrics for the logged in user */ 5 | async getAll() { 6 | return fetchJSON('/api/datasets', { method: 'GET' }); 7 | }, 8 | 9 | /** Gets a metric by id */ 10 | async getOne({ id }) { 11 | return fetchJSON(`/api/datasets/${id}`, { method: 'GET' }); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/server/routes/api/auth.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const { 4 | signUp, 5 | login, 6 | authenticate, 7 | issueToken, 8 | } = require('../../controllers/auth'); 9 | 10 | router.post('/signup', signUp, issueToken); 11 | router.post('/login', login, issueToken); 12 | /* Convenience route to check if a token is valid */ 13 | router.post('/validateToken', authenticate, (req, res) => 14 | res.status(200).json({ ok: true }) 15 | ); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["problems", "plugin:react/recommended", "prettier"], 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2020, 10 | "sourceType": "module", 11 | "ecmaFeatures": { "jsx": true } 12 | }, 13 | "plugins": ["react-hooks"], 14 | "rules": { 15 | "react-hooks/rules-of-hooks": "error", 16 | "react-hooks/exhaustive-deps": "warn", 17 | "react/prop-types": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/services/pointService.js: -------------------------------------------------------------------------------- 1 | import { fetchJSON } from '../utils'; 2 | 3 | export default { 4 | /** Gets all the points for a metric */ 5 | async getAll({ metric }) { 6 | return fetchJSON(`/api/datasets/${metric._id}/points`, { method: 'GET' }); 7 | }, 8 | 9 | /** Records a new point on a metric */ 10 | async record({ metric, value }) { 11 | return fetchJSON(`/api/datasets/${metric._id}/points`, { 12 | method: 'POST', 13 | body: { metric, value }, 14 | }); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { StoreProvider, createStore } from 'easy-peasy'; 5 | 6 | import App from './App'; 7 | import model from './model'; 8 | import AuthProvider from './components/auth/AuthProvider'; 9 | 10 | import 'semantic-ui-css/semantic.min.css'; 11 | 12 | const store = createStore(model); 13 | 14 | render( 15 | 16 | 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById('app') 23 | ); 24 | -------------------------------------------------------------------------------- /src/client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | 4 | import NavBar from './components/NavBar'; 5 | import MetricsContainer from './components/metrics/MetricsContainer'; 6 | import HomePage from './components/home/HomePage'; 7 | 8 | const App = () => ( 9 |
10 | 11 | 12 | 13 |

goals 🎯

14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | ); 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/server/constants/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Constants for error messages 3 | * @module errors 4 | */ 5 | 6 | const createError = (status, message) => { 7 | function error() { 8 | this.status = status; 9 | this.message = message; 10 | } 11 | error.prototype = Object.create(Error.prototype); 12 | return error; 13 | }; 14 | 15 | module.exports = { 16 | ErrorDataSetNotFound: createError( 17 | 404, 18 | "the dataset with the given id doesn't exist" 19 | ), 20 | ErrorUserNotFound: createError( 21 | 404, 22 | "the user with the given user name doesn't exist" 23 | ), 24 | ErrorPasswordIncorrect: createError(401, 'password incorrect'), 25 | }; 26 | -------------------------------------------------------------------------------- /src/client/components/goals/GoalPage.jsx: -------------------------------------------------------------------------------- 1 | // import all of our settings 2 | import React from 'react'; 3 | import { Modal } from 'semantic-ui-react'; 4 | // import React, { useState } from 'react'; 5 | import GoalsList from './GoalsList' 6 | import GoalEditor from './GoalEditor' 7 | import { Header, Button, Grid, Segment } from 'semantic-ui-react'; 8 | 9 | // create react function hook 10 | export default function GoalPage() { 11 | // this component holds state 12 | 13 | // renders components, GoalList and GoalEditor 14 | 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/client/oldcomponents/Graph.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Plot from 'react-plotly.js'; 3 | import { mapListToGraphData } from '../utils'; 4 | 5 | // can either take the point list in as a prop or access the store directly here... 6 | 7 | function Graph({ pointList, setName }) { 8 | const { x, y } = mapListToGraphData(pointList, 'average'); 9 | 10 | return ( 11 | 23 | ); 24 | } 25 | 26 | export default Graph; 27 | -------------------------------------------------------------------------------- /src/client/oldcomponents/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Route, Redirect } from 'react-router-dom'; 3 | import { useStoreState } from 'easy-peasy'; 4 | 5 | function PrivateRoute({ children, ...rest }) { 6 | const isLoggedIn = useStoreState((state) => state.user.isLoggedIn); 7 | 8 | return ( 9 | 12 | isLoggedIn ? ( 13 | children 14 | ) : ( 15 | 21 | ) 22 | } 23 | /> 24 | ); 25 | } 26 | 27 | export default PrivateRoute; 28 | -------------------------------------------------------------------------------- /src/server/models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcrypt'); 3 | const saltRounds = 10; 4 | const { Schema } = mongoose; 5 | 6 | const RequiredString = { type: String, required: true }; 7 | 8 | const userSchema = new Schema({ 9 | username: { 10 | ...RequiredString, 11 | index: true, 12 | unique: true, 13 | }, 14 | password: RequiredString, 15 | firstName: RequiredString, 16 | lastName: RequiredString, 17 | // age: { type: Number, required: true }, 18 | }); 19 | 20 | userSchema.pre('save', async function () { 21 | this.password = await bcrypt.hash(this.password, saltRounds); 22 | }); 23 | 24 | const User = mongoose.model('User', userSchema); 25 | 26 | module.exports = User; 27 | -------------------------------------------------------------------------------- /src/server/routes/api/datasets.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const dataSetController = require('../../controllers/dataset'); 4 | const pointController = require('../../controllers/point'); 5 | 6 | /* /api/datasets */ 7 | router.post('', dataSetController.create); 8 | router.get('', dataSetController.getAll); 9 | 10 | router 11 | .route('/:dataset$') 12 | .put(dataSetController.update) 13 | .get(dataSetController.getOne) 14 | .delete(dataSetController.delete); 15 | 16 | router 17 | .route('/:dataset/points$') 18 | .post(pointController.create) 19 | .get(pointController.getAll) 20 | .delete(pointController.deleteAll); 21 | 22 | router 23 | .route('/:dataset/points/:point') 24 | .put(pointController.update) 25 | .get(pointController.getOne) 26 | .delete(pointController.deleteOne); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /src/client/model/metrics.js: -------------------------------------------------------------------------------- 1 | import { action, thunk } from 'easy-peasy'; 2 | 3 | import metricService from '../services/metricService'; 4 | 5 | /* 6 | { 7 | "_id": "5f7f76c8b5fa8588c76bddfc", 8 | "name": "Water Intake", 9 | "graphColor": "blue", 10 | "type": "number", 11 | "aggregateFunc": "sum", 12 | "owner": "5f7f6bf69a10a183f88464f3", 13 | "createdAt": "2020-10-08T20:30:00.325Z", 14 | "__v": 0 15 | }, 16 | 17 | */ 18 | 19 | export default { 20 | // State here 21 | items: [], 22 | 23 | getAll: thunk(async (actions) => { 24 | // get all the user's metrics from the metricService 25 | const metrics = await metricService.getAll(); 26 | // dispatch an action to set the metrics into our local state 27 | actions.getAllComplete(metrics); 28 | }), 29 | 30 | getAllComplete: action((state, metrics) => { 31 | state.items = metrics; 32 | }), 33 | }; 34 | -------------------------------------------------------------------------------- /src/client/model/points.js: -------------------------------------------------------------------------------- 1 | import { action, thunk } from 'easy-peasy'; 2 | import pointService from '../services/pointService'; 3 | 4 | const pointsModel = { 5 | metricPoints: [], 6 | 7 | getPoints: thunk(async (actions, { metric }) => { 8 | const metricPoints = await pointService.getAll(metric); 9 | actions.getPointsComplete(metricPoints); 10 | }), 11 | 12 | getPointsComplete: action((state, metricPoints) => { 13 | state.metricPoints = metricPoints; 14 | }), 15 | 16 | record: thunk(async (actions, { metric, value }) => { 17 | try { 18 | const newPoint = await pointService.record({ metric, value }); 19 | actions.recordPoint(newPoint); 20 | } catch { 21 | alert(`Couldn't record point.`); 22 | } 23 | }), 24 | 25 | recordPoint: action((state, newPoint) => { 26 | state.metricPoints.push(newPoint); 27 | }), 28 | }; 29 | 30 | export default pointsModel; 31 | -------------------------------------------------------------------------------- /src/server/models/DataSet.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema, ObjectId } = mongoose; 4 | 5 | const RequiredString = { type: String, required: true }; 6 | 7 | const dataSetSchema = new Schema({ 8 | owner: { type: ObjectId, required: true }, 9 | name: RequiredString, 10 | graphColor: { 11 | ...RequiredString, 12 | enum: require('../constants/colors'), 13 | }, 14 | createdAt: { type: Date, default: Date.now }, 15 | type: { 16 | ...RequiredString, 17 | enum: ['number', 'boolean'], 18 | }, 19 | aggregateFunc: { 20 | ...RequiredString, 21 | enum: ['sum', 'count', 'average'], 22 | }, 23 | // NOTE: ⚠️ might want to include 10-20 most recent data points here 24 | // for convenience... 25 | }); 26 | 27 | dataSetSchema.index({ owner: 1 }); 28 | 29 | const DataSet = mongoose.model('DataSet', dataSetSchema); 30 | 31 | module.exports = DataSet; 32 | -------------------------------------------------------------------------------- /src/client/components/home/MetricTile.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal, Card, Button, Icon, Progress } from 'semantic-ui-react'; 3 | 4 | import DataPopUp from './DataPopUp'; 5 | 6 | // MetricTile takes in props --> { metric, onClick } 7 | export default function MetricTile({ metric, onClick }) { 8 | const { name, graphColor, pointsToday } = metric; 9 | 10 | const pointsTodayString = `${pointsToday} point${ 11 | pointsToday > 1 ? 's' : '' 12 | } recorded today.`; 13 | 14 | return ( 15 | 16 | 17 | {name} 18 | {pointsTodayString} 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/client/components/home/DataPopUp.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Modal, Input, Button, Icon } from 'semantic-ui-react'; 3 | 4 | export default function DataPopUp({ metric = {}, isOpen, onSubmit }) { 5 | const [value, setValue] = useState(0); 6 | 7 | return ( 8 | 9 | {metric.name} 10 | 11 | 12 | onSubmit(value)} 16 | icon 17 | color={metric.graphColor} 18 | labelPosition="right" 19 | > 20 | Record 21 | 22 | 23 | } 24 | type="number" 25 | onChange={(e) => setValue(e.target.value)} 26 | > 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/client/components/auth/AuthProvider.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Container } from 'semantic-ui-react'; 3 | import { useStoreState, useStoreActions } from 'easy-peasy'; 4 | 5 | import Login from './Login'; 6 | import SignUp from './Signup'; 7 | 8 | export default function AuthProvider({ children }) { 9 | const isLoggedIn = useStoreState((state) => state.user.isLoggedIn); 10 | const { authenticate, login, signup } = useStoreActions((actions) => ({ 11 | authenticate: actions.user.authenticate, 12 | login: actions.user.login, 13 | signup: actions.user.signup, 14 | })); 15 | 16 | const [showSignUp, setShowSignUp] = useState(false); 17 | 18 | useEffect(() => { 19 | authenticate(); 20 | }, [authenticate]); 21 | 22 | if (isLoggedIn) { 23 | return children; 24 | } 25 | 26 | return ( 27 | 28 | {showSignUp ? ( 29 | 30 | ) : ( 31 | setShowSignUp(true)} onSubmit={login} /> 32 | )} 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/server/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Takes an object and returns an object including only the keys specified 3 | * 4 | * @param {object} obj 5 | * @param {string[]} keys 6 | * 7 | * @returns {object} 8 | */ 9 | const filterProperties = (obj, keys) => 10 | Object.entries(obj) 11 | .filter(([k]) => keys.includes(k)) 12 | .reduce((newObj, [key, value]) => ({ ...newObj, [key]: value }), {}); 13 | 14 | const removeProperties = (obj, keys) => 15 | Object.entries(obj) 16 | .filter(([k]) => !keys.includes(k)) 17 | .reduce((newObj, [key, value]) => ({ ...newObj, [key]: value }), {}); 18 | 19 | const validateProperties = (obj, validation) => { 20 | const keys = Object.keys(validation); 21 | const filteredObj = filterProperties(obj, keys); 22 | Object.entries(filteredObj).every(([key, value]) => { 23 | const validate = validation[key]; 24 | if (!validate(value)) { 25 | throw new Error(`property ${key} is invalid`); 26 | } 27 | return true; 28 | }); 29 | return filteredObj; 30 | }; 31 | 32 | module.exports = { 33 | filterProperties, 34 | removeProperties, 35 | validateProperties, 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FireAxolotl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/client/components/goals/GoalsList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal, Header, Button, Grid, Segment } from 'semantic-ui-react'; 3 | // import React, { useState } from 'react'; 4 | 5 | 6 | const handleSubmit = (event) => { 7 | console.log('button clicked!'); 8 | event.preventDefault(); 9 | } 10 | 11 | export default function GoalsList({handleClick, goals}) { 12 | // each time the button is clicked add a new element to the list 13 | // map through goals 14 | return ( 15 | 16 |
21 | 22 | 23 | My goal 1 24 | 25 | 26 | 27 | My goal 2 28 | 29 | 30 | My goal 3 31 | 32 | 33 | My goal 3 34 | 35 | 36 | ) 37 | }; 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/client/oldcomponents/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, Menu, Dropdown } from 'semantic-ui-react'; 3 | import Graph from './Graph'; 4 | import { useStoreState } from 'easy-peasy'; 5 | import { useLocation, Link } from 'react-router-dom'; 6 | 7 | function NavBar() { 8 | const { pathname } = useLocation(); 9 | 10 | const username = useStoreState((state) => state.user.username); 11 | 12 | return ( 13 | 14 | 15 | Points 16 | 17 | 18 | Sets 19 | 20 | 21 | Metrics 22 | 23 | 24 | 25 | 26 | console.log('Signing out!')}> 27 | Sign Out 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default NavBar; 37 | -------------------------------------------------------------------------------- /src/client/utils/mockData.js: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | 3 | /** Returns an array of n mock data points */ 4 | export function fetchMockPoints(n = 15) { 5 | const points = []; 6 | 7 | let date = Date.now() - ms('2d'); 8 | for (let i = 0; i < n; i++) { 9 | points.push({ 10 | timestamp: date, 11 | value: ~~(Math.random() * 10), 12 | }); 13 | 14 | date += ms(`${~~(Math.random() * 5)}h`); 15 | } 16 | 17 | return points; 18 | } 19 | 20 | /** Returns a list of mock metric models (the same models you'd get from fetching the our backend) */ 21 | export function fetchMockMetrics() { 22 | const metrics = []; 23 | for (let i = 0; i < 5; i++) { 24 | metrics.push({ 25 | _id: ~~(Math.random() * 10 ** 9), 26 | name: [ 27 | 'Water Intake', 28 | 'Miles Ran', 29 | 'Tacos Eaten', 30 | 'Hours Slept', 31 | 'Time In Car', 32 | ][~~(Math.random() * 5)], 33 | graphColor: ['blue', 'green', 'orange', 'purple', 'red'][ 34 | ~~(Math.random() * 5) 35 | ], 36 | type: 'number', 37 | aggregateFunc: 'average', 38 | pointsToday: ~~(Math.random() * 10), 39 | }); 40 | } 41 | 42 | return metrics; 43 | } 44 | -------------------------------------------------------------------------------- /src/client/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Menu, Dropdown } from 'semantic-ui-react'; 4 | import { useStoreState, useStoreActions } from 'easy-peasy'; 5 | 6 | export default function NavBar() { 7 | const user = useStoreState((state) => state.user); 8 | const logout = useStoreActions((actions) => actions.user.logout); 9 | 10 | return ( 11 | 12 | {/* Each of these Menu Items will redirect to a specific path. React Router will then 13 | render the correct page using this path */} 14 | 15 | 16 | Points 17 | 18 | 19 | 20 | Goals 21 | 22 | 23 | 24 | Metrics 25 | 26 | 27 | 28 | 29 | 30 | Sign Out 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/server/models/Point.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const { Schema, ObjectId } = mongoose; 4 | 5 | const pointSchema = new Schema({ 6 | owner: { type: ObjectId, required: true }, 7 | dataset: { type: ObjectId, required: true }, 8 | timestamp: { 9 | type: Date, 10 | default: Date.now, 11 | }, 12 | value: { type: Number, required: true }, 13 | }); 14 | 15 | /* 16 | Create an index on the Point schema to aid performance when retrieving the points for a data set. 17 | We create a compound index first on the `dataset` ObjectId, then for the timestamps in descending order. 18 | This helps MongoDB know that Points will likely be accessed in such a way that groups the points by their data set. 19 | We compound that index with the `timestamp` to make retrieval of more recent points faster than older points. 20 | 21 | - https://docs.mongodb.com/manual/core/index-compound/ 22 | - https://mongoosejs.com/docs/guide.html#indexes 23 | - https://stackoverflow.com/questions/12573753/creating-multifield-indexes-in-mongoose-mongodb 24 | */ 25 | pointSchema.index({ dataset: 1, timestamp: -1 }); 26 | 27 | const Point = mongoose.model('Point', pointSchema); 28 | 29 | module.exports = Point; 30 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const mongoose = require('mongoose'); 3 | const path = require('path'); 4 | 5 | require('dotenv').config(); 6 | 7 | // Connect to MongoDB 8 | mongoose.connect(process.env.MONGO_URL, { 9 | auth: { 10 | user: 'fire', 11 | password: 'fYzgID9ajNNj3K3j', 12 | }, 13 | useNewUrlParser: true, 14 | }); 15 | mongoose.connection.once('open', () => { 16 | console.log('Connected to Database'); 17 | }); 18 | 19 | const app = express(); 20 | 21 | // middleware for json body parsing 22 | app.use('/api', express.json()); 23 | 24 | // routes for api 25 | app.use('/api', require('./routes/api')); 26 | 27 | // serve the index.html statically 28 | app.get('/', (req, res) => 29 | res.sendFile(path.join(__dirname, '../template.html')) 30 | ); 31 | 32 | app.use((err, req, res, next) => { 33 | next; // unused 34 | const defaultError = { 35 | log: '⚠️ Express error handler caught unknown middleware error.', 36 | status: 500, 37 | message: { err: 'An error occurred' }, 38 | }; 39 | const errorObj = Object.assign(defaultError, err); 40 | console.log(errorObj.log); 41 | res.status(errorObj.status).json(errorObj.message); 42 | }); 43 | 44 | app.listen(3000, () => { 45 | console.log('⚡️ prepare your body'); 46 | }); 47 | -------------------------------------------------------------------------------- /src/client/oldcomponents/MainComponent.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; 3 | import { useStoreActions, useStoreState } from 'easy-peasy'; 4 | 5 | import PrivateRoute from './PrivateRoute'; 6 | import NavBar from './NavBar'; 7 | import Login from './Login'; 8 | import HomePage from './home/HomePage'; 9 | import SetEditor from './SetEditor'; 10 | import Metrics from './Metrics'; 11 | 12 | const MainComponent = () => { 13 | const isLoggedIn = useStoreState((state) => state.user.isLoggedIn); 14 | const initialize = useStoreActions((actions) => actions.initialize); 15 | // const login = useStoreActions(actions => actions.user.authenticate); 16 | 17 | useEffect(() => { 18 | console.log('ran useEffect...'); 19 | initialize(); 20 | }, []); 21 | 22 | if (!isLoggedIn) return ; 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default MainComponent; 43 | -------------------------------------------------------------------------------- /src/client/components/auth/Login.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, configure } from 'enzyme'; 3 | import { Form } from 'semantic-ui-react'; 4 | import Login from './Login.jsx'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | describe('Login Component', () => { 10 | describe('Login Component Characteristics', () => { 11 | let shallowWrapper; 12 | 13 | const mockSignup = jest.fn(); 14 | const mockSubmit = jest.fn(); 15 | 16 | const props = { 17 | onSignupClick: mockSignup, 18 | onSubmit: mockSubmit, 19 | }; 20 | 21 | beforeAll(() => { 22 | shallowWrapper = shallow(); 23 | }); 24 | 25 | it('Should have two inputs', () => { 26 | expect(shallowWrapper.find('input').length).toEqual(2); 27 | }); 28 | it('Should have two inputs', () => { 29 | expect(shallowWrapper.find('input').length).not.toEqual(1); 30 | }); 31 | it('Should have a form', () => { 32 | expect(shallowWrapper.find(Form).length).toEqual(1); 33 | }); 34 | 35 | it('Should have an input with a type of text', () => { 36 | expect( 37 | shallowWrapper.find('input').filterWhere((item) => { 38 | return item.prop('type') === 'text'; 39 | }).length 40 | ).toEqual(1); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/client/oldcomponents/home/DataSetTile.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Button, Icon, Card } from 'semantic-ui-react'; 4 | 5 | function DataSetTile({ dataset, openAddPointModal, viewMetrics }) { 6 | const cardExtra = ( 7 |
8 |
15 | 22 |
23 |
24 | 25 | 40 | 41 |
42 |
43 | ); 44 | 45 | return ( 46 | 47 | ); 48 | } 49 | 50 | export default DataSetTile; 51 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/client/index.js', 5 | output: { 6 | path: path.join(__dirname, 'dist'), 7 | filename: 'bundle.js', 8 | }, 9 | mode: process.env.NODE_ENV || 'development', 10 | optimization: { 11 | usedExports: true, 12 | }, 13 | devtool: 'source-map', 14 | devServer: { 15 | contentBase: path.join(__dirname, '/src/client'), 16 | historyApiFallback: { 17 | index: 'index.html', 18 | }, 19 | proxy: { 20 | '/api': 'http://localhost:3000', 21 | }, 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.jsx?/, 27 | exclude: /node_modules/, 28 | use: [ 29 | { 30 | loader: 'babel-loader', 31 | options: { 32 | presets: ['@babel/preset-env', '@babel/preset-react'], 33 | plugins: [ 34 | '@babel/plugin-transform-runtime', 35 | '@babel/plugin-transform-async-to-generator', 36 | ], 37 | }, 38 | }, 39 | ], 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }], 44 | }, 45 | { 46 | test: /\.(woff|woff2|eot|ttf|otf|png|jpe?g|gif|svg)$/, 47 | use: ['file-loader'], 48 | }, 49 | ], 50 | }, 51 | resolve: { 52 | extensions: ['.jsx', '.js'], 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /src/client/model/user.js: -------------------------------------------------------------------------------- 1 | import { action, thunk, computed } from 'easy-peasy'; 2 | 3 | import authService from '../services/authService'; 4 | 5 | export default { 6 | username: '', 7 | firstName: '', 8 | lastName: '', 9 | 10 | isLoggedIn: computed(({ username }) => username !== ''), 11 | 12 | authenticate: thunk(async (actions) => { 13 | const { user } = await authService.authenticate(); 14 | if (!user) throw new Error('no user logged in'); 15 | console.log(user); 16 | actions.loginComplete(user); 17 | }), 18 | 19 | login: thunk(async (actions, { username, password }) => { 20 | const { user } = await authService.login({ username, password }); 21 | actions.loginComplete(user); 22 | }), 23 | 24 | signup: thunk( 25 | async (actions, { username, password, firstName, lastName }) => { 26 | const { user } = await authService.signup({ 27 | username, 28 | password, 29 | firstName, 30 | lastName, 31 | }); 32 | actions.loginComplete(user); 33 | } 34 | ), 35 | 36 | loginComplete: action((state, user) => { 37 | state.username = user.username; 38 | state.firstName = user.firstName; 39 | state.lastName = user.lastName; 40 | }), 41 | 42 | logout: thunk(async (actions) => { 43 | await authService.logout(); 44 | actions.logoutComplete(); 45 | }), 46 | 47 | logoutComplete: action((state) => { 48 | state.username = ''; 49 | state.firstName = ''; 50 | state.lastName = ''; 51 | }), 52 | }; 53 | -------------------------------------------------------------------------------- /src/client/components/home/MetricList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Grid } from 'semantic-ui-react'; 3 | import { useStoreState, useStoreActions } from 'easy-peasy'; 4 | 5 | import MetricTile from './MetricTile'; 6 | import DataPopUp from './DataPopUp'; 7 | 8 | export default function MetricList() { 9 | const metrics = useStoreState((state) => state.metrics.items); 10 | const { getAllMetrics } = useStoreActions((actions) => ({ 11 | getAllMetrics: actions.metrics.getAll, 12 | })); 13 | 14 | const [modalIsOpen, setModalIsOpen] = useState(false); 15 | const [modalMetric, setModalMetric] = useState(); 16 | 17 | useEffect(() => { 18 | getAllMetrics(); 19 | }, [getAllMetrics]); 20 | 21 | const openDataModal = (metric) => { 22 | setModalMetric(metric); 23 | setModalIsOpen(true); 24 | }; 25 | 26 | const recordModalValue = (value) => { 27 | setModalIsOpen(false); 28 | if (value !== undefined) { 29 | console.log('recording value: ', value); 30 | } 31 | }; 32 | 33 | return ( 34 | <> 35 | 40 | 41 | {metrics.map((metric) => ( 42 | 43 | openDataModal(metric)} /> 44 | 45 | ))} 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/client/utils.js: -------------------------------------------------------------------------------- 1 | import ms from 'ms'; 2 | 3 | /** 4 | * Sends an authenticated fetch request using the JWT in localStorage (if there is one). 5 | * - Automatically calls `JSON.stringify()` on the body parameter 6 | * - Automatically deserializes the response to JSON 7 | * 8 | * @param {string} url the url of the fetch request 9 | * @param {string} method 'POST', 'GET', etc... 10 | * @param {object} body the JSON body to send with the request 11 | * @param {?object} options extra options to pass to `fetch` 12 | * 13 | * @returns {object} the response json 14 | */ 15 | export async function fetchJSON(url, { method, body }, options) { 16 | const token = localStorage.getItem('jwt'); 17 | 18 | const response = await fetch(url, { 19 | method, 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | Authorization: `Bearer ${token}`, 23 | }, 24 | body: body ? JSON.stringify(body) : undefined, 25 | ...options, 26 | }); 27 | 28 | if (response.status !== 200) 29 | throw new Error(`recieved status code: ${response.status}`); 30 | 31 | return response.json(); 32 | } 33 | 34 | /** Extracts a JSON payload from a JWT. 35 | * @param {string} jwt the encoded json web token 36 | * @returns {object} the json payload 37 | */ 38 | export function extractPayload(jwt) { 39 | const [header, payload, signature] = jwt.split('.'); 40 | if (!header || !payload || !signature) 41 | throw new Error( 42 | `${LOGTAG} provided string is not a jwt. Expected 3 parts after splitting on '.'` 43 | ); 44 | return JSON.parse(atob(payload)); 45 | } 46 | -------------------------------------------------------------------------------- /docs/data-model-example.js: -------------------------------------------------------------------------------- 1 | 2 | // Water 3 | { 4 | meta: { 5 | name: 'water', 6 | graphColor: 'blue', 7 | createdAt: 321901293875 8 | type: 'number', 9 | aggregateFunc: 'sum' 10 | }, 11 | points: { 12 | 1237950189: 2 13 | 1237950189: 1 14 | 1237950189: 6 15 | 1237950189: 2 16 | 1237950189: 8 17 | } 18 | } 19 | 20 | 21 | // Got Up On Time 22 | { 23 | meta: { 24 | name: 'Got up on time', 25 | graphColor: 'green', 26 | createdAt: 321901293875 27 | type: 'boolean', 28 | aggregateFunc: 'sum' 29 | }, 30 | points: { 31 | 1237950189: 1 32 | 1237950189: 1 33 | 1237950189: 1 34 | 1237950189: 1 35 | 1237950189: 1 36 | } 37 | } 38 | 39 | // said "fuck" 40 | { 41 | meta: { 42 | name: 'said "fuck"', 43 | graphColor: 'red', 44 | createdAt: 321901293875 45 | type: 'event' 46 | aggregateFunc: 'sum' 47 | }, 48 | points: { 49 | 1237950189: 1, 50 | 1237950189: 1, 51 | 1237950189: 1, 52 | 1237950189: 1, 53 | 1237950189: 1, 54 | 1237950189: 1, 55 | 1237950189: 1, 56 | 1237950189: 1, 57 | 1237950189: 1 58 | } 59 | } 60 | 61 | // Mood 62 | { 63 | meta: { 64 | name: 'Mood', 65 | graphColor: 'blue', 66 | createdAt: 321901293875 67 | // scale: { 68 | // 1: 'Sad', 69 | // 2: 'Neutral', 70 | // 3: 'Happy' 71 | // }, 72 | type: 'event', //irrelevant 73 | aggregateFunc: 'sum, count, average' // whats in the dropdown when creating form 74 | }, 75 | points: { 76 | 1237950189: 2 77 | 1237950189: 1 78 | 1237950189: 3 79 | 1237950189: 1 80 | 1237950189: 2 81 | } 82 | } -------------------------------------------------------------------------------- /docs/front-end-tree.text: -------------------------------------------------------------------------------- 1 | Tasks: 2 | figure out router arrangement and nesting w/ auth 3 | build out homepage component 4 | set tile components 5 | add point modal component 6 | some kind of nav 7 | build out set editor component 8 | display of sets (list maybe) can delete the sets 9 | add set form modal 10 | build out metrics component 11 | graph 12 | list of different sets to switch visualization 13 | sidebar or dropdown 14 | 15 | 16 | 17 | index.js 18 | | 19 | APP = (wrapper router, easy-peasy, react-query) 20 | | 21 | main component 22 | Home Page (auth layer) ('/') 23 | (tiles to add points to a set) 24 | button on tile to take you to metrics 25 | (click on tiles, opens modal to add point) 26 | 27 | 28 | Login (/login) 29 | (sign-up modal) 30 | 31 | set editor (auth layer)(/sets) 32 | (stretch) tiles for each set 33 | button to add a set 34 | open modal form to define new set 35 | 36 | metrics (auth layer) (/metrics) 37 | graph displaying a single set 38 | defaults to past week (stretch feat. to display different time scales) 39 | some kind of navigation (tiles, list, etc) 40 | changes which set the graph renders 41 | 42 | 43 | 44 | 45 | 46 | 47 | // React auth 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/client/components/goals/GoalEditor.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Dropdown, Card, Button } from 'semantic-ui-react'; 3 | 4 | export default function GoalEditor(props) { 5 | 6 | const [saveOptionSelected, saveOptions] = useState(''); 7 | 8 | const goalOptions = [ 9 | { key: 1, text: 'drink more water', value: 1}, 10 | { key: 2, text: 'run more', value: 2 }, 11 | { key: 3, text: 'walk more', value: 3 }, 12 | { key: 4, text: 'eat less', value: 4 }, 13 | { key: 5, text: 'eat more', value: 5 }, 14 | { key: 6, text: 'sleep more', value: 6 }, 15 | { key: 7, text: 'sleep less', value: 7 }, 16 | { key: 8, text: 'smoke less', value: 8 }, 17 | ]; 18 | 19 | 20 | const saveData = () => { 21 | console.log(saveOptionSelected) 22 | // when Button 'Save' is clicked we need to save the data selected and have access to it in GoalsList.jsx 23 | } 24 | 25 | return ( 26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 | saveOptions(e.target.placeholder)} 39 | /> 40 |
41 |
42 | 48 |
49 |
50 | 51 | 54 | 55 |
56 |
57 |
58 |
59 | ); 60 | } -------------------------------------------------------------------------------- /src/client/components/home/MetricTile.test.js: -------------------------------------------------------------------------------- 1 | import { shallow, configure } from 'enzyme'; 2 | import MetricTile from './MetricTile.jsx'; 3 | import { Card, Button, Progress } from 'semantic-ui-react'; 4 | import React from 'react'; 5 | import Adapter from 'enzyme-adapter-react-16'; 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | describe('Metric Tile Component', () => { 10 | let shallowWrapper; 11 | const mockClick = jest.fn(); 12 | 13 | const props = { 14 | metric: { name: 'Andrew', graphColor: 'pink', pointsToday: 5 }, 15 | onClick: mockClick, 16 | }; 17 | 18 | beforeAll(() => { 19 | shallowWrapper = shallow(); 20 | }); 21 | 22 | afterEach(() => { 23 | mockClick.mockClear(); 24 | }); 25 | 26 | it('Should be a Card', () => { 27 | expect(shallowWrapper.type()).toEqual(Card); 28 | }); 29 | it('Should render a Progress component', () => { 30 | expect(shallowWrapper.find(Progress).length).toEqual(1); 31 | }); 32 | it('Should render a Button component', () => { 33 | expect(shallowWrapper.find(Button).length).toEqual(1); 34 | }); 35 | it("Should have an Icon whose text is 'Record", () => { 36 | expect(shallowWrapper.find(Button).render().text()).toMatch('Record'); 37 | }); 38 | it('Clicking the button should activate a callback', () => { 39 | shallowWrapper.find(Button).simulate('click'); 40 | expect(mockClick.mock.calls.length).toEqual(1); 41 | }); 42 | it('Hovering over the button should not activate callback', () => { 43 | shallowWrapper.find(Button).simulate('mouseover'); 44 | expect(mockClick.mock.calls.length).toEqual(0); 45 | }); 46 | it('Should render the color contained in the prop', () => { 47 | expect(shallowWrapper.prop('color')).toEqual(props.metric.graphColor); 48 | }); 49 | it('Should render a Card Header', () => { 50 | expect(shallowWrapper.render().find('.header').text()).toEqual( 51 | props.metric.name 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/client/oldcomponents/Metrics.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Grid, Menu } from 'semantic-ui-react'; 3 | import Graph from './Graph'; 4 | import ms from 'ms'; 5 | import { useStoreState } from 'easy-peasy'; 6 | 7 | const dummyPointList = [ 8 | { 9 | timestamp: new Date(Date.now() - ms('6 days')), 10 | value: 9, 11 | dataset: 1, // data set id 12 | owner: 3, // user id 13 | }, 14 | { 15 | timestamp: new Date(Date.now() - ms('3 days')), 16 | value: 3, 17 | dataset: 1, // data set id 18 | owner: 3, // user id 19 | }, 20 | { 21 | timestamp: new Date(Date.now() - ms('2.8 days')), 22 | value: 3, 23 | dataset: 1, // data set id 24 | owner: 3, // user id 25 | }, 26 | { 27 | timestamp: new Date(Date.now() - ms('0.3 days')), 28 | value: 1, 29 | dataset: 1, // data set id 30 | owner: 3, // user id 31 | }, 32 | ]; 33 | 34 | /* 35 | I'm thinking that we'll have a useEffect AJAX request in here for 36 | the respective data set's point list - we'll have a store state variable of the current 37 | data set's id and can use that in the fetch. When that response comes in we'll set 38 | some state variable - most likely in the store - that will hold the list of points to be graphed, 39 | substituted here by dummyPointList 40 | */ 41 | 42 | const Metrics = () => { 43 | const [activeItem, setActiveItem] = useState('points'); 44 | const dataSets = useStoreState((state) => state.datasets.items); 45 | 46 | const setList = dataSets.map((dataset) => { 47 | return ( 48 | setActiveItem(dataset.name)} 54 | > 55 | {dataset.name} 56 | 57 | ); 58 | }); 59 | 60 | return ( 61 | 62 | 63 | 64 | {setList} 65 | 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | }; 73 | 74 | export default Metrics; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-life-in-data", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "jest", 7 | "start": "NODE_ENV=development nodemon ./src/server/server.js", 8 | "dev": "NODE_ENV=development webpack-dev-server --open & nodemon server/server.js", 9 | "build": "NODE_ENV=production webpack" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/samkcarlile/my-life-in-data.git" 14 | }, 15 | "author": "Sam Carlile ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/samkcarlile/my-life-in-data/issues" 19 | }, 20 | "homepage": "https://github.com/samkcarlile/my-life-in-data#readme", 21 | "devDependencies": { 22 | "@babel/core": "^7.11.6", 23 | "@babel/plugin-transform-async-to-generator": "^7.10.4", 24 | "@babel/plugin-transform-runtime": "^7.11.5", 25 | "@babel/preset-env": "^7.11.5", 26 | "@babel/preset-react": "^7.10.4", 27 | "@babel/runtime": "^7.11.2", 28 | "babel-loader": "^8.1.0", 29 | "concurrently": "^5.3.0", 30 | "css-loader": "^4.3.0", 31 | "enzyme": "^3.11.0", 32 | "enzyme-adapter-react-16": "^1.15.5", 33 | "eslint": "^7.10.0", 34 | "eslint-config-prettier": "^6.12.0", 35 | "eslint-config-problems": "^5.0.0", 36 | "eslint-plugin-react": "^7.21.3", 37 | "eslint-plugin-react-hooks": "^4.1.2", 38 | "file-loader": "^6.1.0", 39 | "html-webpack-plugin": "^4.5.0", 40 | "nodemon": "^2.0.4", 41 | "prettier": "^2.1.2", 42 | "style-loader": "^1.2.1", 43 | "webpack": "^4.44.2", 44 | "webpack-cli": "^3.3.12", 45 | "webpack-dev-server": "^3.11.0" 46 | }, 47 | "dependencies": { 48 | "babel-jest": "^26.5.2", 49 | "bcrypt": "^5.0.0", 50 | "dotenv": "^8.2.0", 51 | "easy-peasy": "^3.3.1", 52 | "express": "^4.17.1", 53 | "jest": "^26.5.2", 54 | "jsonwebtoken": "^8.5.1", 55 | "mongoose": "^5.10.7", 56 | "ms": "^2.1.2", 57 | "plotly.js": "^1.56.0", 58 | "react": "^16.13.1", 59 | "react-dom": "^16.13.1", 60 | "react-plotly.js": "^2.5.0", 61 | "react-router-dom": "^5.2.0", 62 | "react-test-renderer": "^16.13.1", 63 | "semantic-ui-css": "^2.4.1", 64 | "semantic-ui-react": "^2.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/client/oldcomponents/home/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useStoreState } from 'easy-peasy'; 3 | import { Input, Modal, Button, Card } from 'semantic-ui-react'; 4 | import DataSetTile from './DataSetTile'; 5 | 6 | export default function HomePage() { 7 | // const [modalIsOpen, setModalOpen] = useState(false); 8 | // const [modalName, setModalName] = useState(); 9 | // const [modalDataSetId, setModalDataSetId] = useState(); 10 | // const [modalInputValue, setModalInputValue] = useState(); 11 | 12 | // const datasets = useStoreState((state) => state.datasets.items); 13 | 14 | // const openAddPointModal = (dataset) => { 15 | // setModalDataSetId(dataset._id); 16 | // setModalName(dataset.name); 17 | // setModalInputValue(''); 18 | // setModalOpen(true); 19 | // }; 20 | 21 | const datasets = [ 22 | { 23 | _id: 12345678910, 24 | name: 'Water Intake', 25 | graphColor: 'blue', 26 | type: 'number', 27 | aggregateFunc: 'average', 28 | }, 29 | ]; 30 | 31 | return ( 32 |
33 |
36 | 37 | {datasets.map((dataset) => ( 38 | openAddPointModal(dataset)} 42 | viewMetrics={() => console.log(dataset._id)} 43 | /> 44 | ))} 45 | 46 |
47 | 48 | {/* setModalOpen(false)} 52 | open={modalIsOpen} 53 | > 54 | Create a new point in {modalName} 55 | 58 | setModalInputValue(e.target.value)} 62 | type="number" 63 | placeholder="Input Num" 64 | > 65 | 66 | 67 | */} 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/client/components/auth/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Form, Button, Card, Icon } from 'semantic-ui-react'; 3 | 4 | export default function Login({ onSignUpClick, onSubmit }) { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | 8 | /* 9 | const login = (e) => { 10 | fetch('/api/login', { 11 | method: 'POST', 12 | body: JSON.stringify({ username, password }), 13 | headers: { 14 | 'Content-type': 'Application/json', 15 | }, 16 | }) 17 | .then((data) => data.json()) 18 | .then((data) => { 19 | if (data.message) { 20 | //alert(data.message); 21 | } else { 22 | props.logInUser(data); 23 | //alert('logged in'); 24 | } 25 | }) 26 | .catch((e) => { 27 | console.log(e); 28 | }); 29 | }; 30 | */ 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 |   Login to an existing account 38 | 39 | 40 |
41 | 42 | 43 | setUsername(e.target.value)} 47 | /> 48 | 49 | 50 | 51 | setPassword(e.target.value)} 55 | /> 56 | 57 |
58 | 59 | 62 | 69 | 70 |
71 |
72 |
73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/client/services/authService.js: -------------------------------------------------------------------------------- 1 | import { fetchJSON, extractPayload } from '../utils'; 2 | const LOGTAG = `⚠️ [authService] -`; 3 | 4 | export default { 5 | /** Checks for a saved token in localStorage, and returns parsed token payload (the user object) if there is one */ 6 | async authenticate() { 7 | // if we have a saved token, we're already logged in 8 | const savedToken = await this.getSavedToken(); 9 | if (savedToken) { 10 | return extractPayload(savedToken); 11 | } 12 | return undefined; 13 | }, 14 | 15 | /** Logs a user in using either a username and password and stores the JWT in localStorage. */ 16 | async login({ username, password }) { 17 | if (!username || !password) 18 | throw new Error( 19 | `${LOGTAG} - must call login() with a username and password` 20 | ); 21 | 22 | try { 23 | const response = await fetchJSON( 24 | '/api/login', 25 | { 26 | method: 'POST', 27 | body: { username, password }, 28 | }, 29 | { cache: 'no-cache' } 30 | ); 31 | const { token } = response; 32 | saveToken(token); 33 | return extractPayload(token); 34 | } catch (err) { 35 | throw new Error(`${LOGTAG} - error logging in: ${err}`); 36 | } 37 | }, 38 | 39 | /** Deletes the saved JWT from localStorage (if there is one) */ 40 | async logout() { 41 | localStorage.removeItem('jwt'); 42 | }, 43 | 44 | /** Signs up a user, logs the user in, and store the JWT in localStorage */ 45 | async signup({ username, password, firstName, lastName }) { 46 | const newUserForm = { username, password, firstName, lastName }; 47 | 48 | // Send a request to sign the user up 49 | try { 50 | const response = await fetchJSON( 51 | '/api/signup', 52 | { 53 | method: 'POST', 54 | body: newUserForm, 55 | }, 56 | { cache: 'no-cache' } 57 | ); 58 | const { token } = response; 59 | saveToken(token); 60 | return extractPayload(token); 61 | } catch (err) { 62 | throw new Error(`${LOGTAG} - error signing up: ${err}`); 63 | } 64 | }, 65 | 66 | /** Returns the JWT saved in localStorage, or undefined if there is not a JWT present. */ 67 | async getSavedToken() { 68 | return localStorage.getItem('jwt') || undefined; 69 | }, 70 | }; 71 | 72 | //////////////// UTILITY FUNCTIONS //////////////// 73 | 74 | /** Saves a JWT to the localStoraage */ 75 | async function saveToken(jwt) { 76 | // NOTE: no it doesn't need to be async, but it keeps the interfaces standard. 77 | localStorage.setItem('jwt', jwt); 78 | } 79 | -------------------------------------------------------------------------------- /src/client/components/auth/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Form, Button, Card, Icon } from 'semantic-ui-react'; 3 | 4 | export default function Signup({ onSubmit }) { 5 | const [firstName, setFirstName] = useState(''); 6 | const [lastName, setLastName] = useState(''); 7 | const [username, setUsername] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 |   Sign Up 16 | 17 | 18 |
19 | 20 | 21 | setFirstName(e.target.value)} 26 | /> 27 | 28 | 29 | 30 | setLastName(e.target.value)} 35 | /> 36 | 37 | 38 | 39 | setUsername(e.target.value)} 43 | /> 44 | 45 | 46 | 47 | setPassword(e.target.value)} 51 | /> 52 | 53 |
54 | 55 | 72 | 73 |
74 |
75 |
76 |
77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/client/oldcomponents/SetEditor.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useStoreState } from 'easy-peasy'; 3 | import { List, Input, Dropdown, Modal, Button } from 'semantic-ui-react'; 4 | 5 | const SetEditor = () => { 6 | const setList = useStoreState((state) => state.datasets.items); 7 | 8 | const [modalIsOpen, setModalOpen] = useState(false); 9 | const [modalInputValue, setModalInputValue] = useState(''); 10 | const [modalDropdownValue, setModalDropdownValue] = useState(); 11 | 12 | const aggregateFuncOptions = [ 13 | { key: 1, text: 'Sum', value: 'Sum' }, 14 | { key: 2, text: 'Count', value: 'Count' }, 15 | { key: 3, text: 'Average', value: 'Average' }, 16 | ]; 17 | 18 | const dataSets = setList.map((set) => { 19 | return ( 20 | 21 | 22 | 25 | 26 | {set.name} 27 | 28 | ); 29 | }); 30 | 31 | return ( 32 |
33 | 34 | {dataSets} 35 | 36 | 37 | 45 | 46 | 47 | 48 | setModalOpen(false)} 53 | // onOpen={() => setModalOpen(true)} 54 | open={modalIsOpen} 55 | // trigger={} 56 | > 57 | Create a New Set 58 | 59 | setModalInputValue(e.target.value)} 63 | placeholder="Set Name" 64 | > 65 | 66 | 67 | setModalDropdownValue(value)} 73 | /> 74 | 75 | 76 | {/* function here that submit the contents of the modal to Db */} 77 | 78 | 79 | 80 |
81 | ); 82 | }; 83 | 84 | export default SetEditor; 85 | -------------------------------------------------------------------------------- /src/client/oldcomponents/Login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { List, Input, Modal, Button } from 'semantic-ui-react'; 3 | 4 | const Login = () => { 5 | const [modalIsOpen, setModalOpen] = useState(false); 6 | const [modalInputValue, setModalInputValue] = useState(''); 7 | 8 | const authenticate = (username, password) => {}; 9 | 10 | const createNewUser = (newUsername, newPassword) => {}; 11 | return ( 12 |
13 |

Log In

14 | e.target.value.username} 18 | placeholder="Username" 19 | type="text" 20 | > 21 | 22 | e.target.value.password} 26 | placeholder="Password" 27 | > 28 | 29 | 35 | 36 | 39 | 40 | setModalOpen(false)} 45 | // onOpen={() => setModalOpen(true)} 46 | open={modalIsOpen} 47 | // trigger={} 48 | > 49 | Create New Account 50 | 51 | setModalInputValue(e.target.value.username)} 55 | placeholder="Create Username" 56 | > 57 | 58 | 59 | setModalInputValue(e.target.value.password)} 63 | placeholder="Create Password" 64 | > 65 | 66 | 67 | setModalInputValue(e.target.value.firstName)} 71 | placeholder="First Name" 72 | > 73 | 74 | 75 | setModalInputValue(e.target.value.lastName)} 79 | placeholder="Last Name" 80 | > 81 | 82 | 83 | 84 | {/* function here that submits new user&pass to DB */} 85 | 88 | 89 | 90 |
91 | ); 92 | }; 93 | 94 | export default Login; 95 | -------------------------------------------------------------------------------- /src/server/controllers/auth.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const bcrypt = require('bcrypt'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | const { filterProperties, removeProperties } = require('../utils'); 6 | const { 7 | ErrorUserNotFound, 8 | ErrorPasswordIncorrect, 9 | } = require('../constants/errors'); 10 | 11 | const authController = {}; 12 | 13 | authController.signUp = async (req, res, next) => { 14 | const newUserForm = filterProperties(req.body, [ 15 | 'username', 16 | 'password', 17 | 'firstName', 18 | 'lastName', 19 | 'age', 20 | ]); 21 | 22 | try { 23 | const newUser = await User.create(newUserForm); 24 | res.locals.user = newUser.toObject(); 25 | next(); 26 | } catch (err) { 27 | next({ 28 | log: `⚠️ [ERROR] authController.signUp - ${err}`, 29 | message: { error: "Some shit went down I'm sorry." }, 30 | }); 31 | } 32 | }; 33 | 34 | authController.login = async (req, res, next) => { 35 | const { username, password } = filterProperties(req.body, [ 36 | 'username', 37 | 'password', 38 | ]); 39 | 40 | try { 41 | const user = await User.findOne({ username }) 42 | .lean() 43 | .orFail(new ErrorUserNotFound()) 44 | .exec(); 45 | 46 | if (await bcrypt.compare(password, user.password)) { 47 | res.locals.user = user; 48 | next(); 49 | } else throw new ErrorPasswordIncorrect(); 50 | } catch (err) { 51 | next({ 52 | log: `⚠️ [ERROR] authController.login - ${err}`, 53 | message: { 54 | error: err.message, 55 | status: err.status || 500, 56 | }, 57 | }); 58 | } 59 | }; 60 | 61 | authController.issueToken = async (req, res, next) => { 62 | if (!res.locals.user) 63 | return next({ 64 | log: `⚠️ [ERROR] authController.issueToken - expected res.locals.user`, 65 | message: { error: 'Something went wrong.' }, 66 | }); 67 | 68 | const userPayload = removeProperties(res.locals.user, ['password']); 69 | 70 | // generate the token 71 | const token = jwt.sign({ user: userPayload }, 'secret', { 72 | expiresIn: '1y', 73 | }); 74 | 75 | // send the token back 76 | res.status(200).json({ token }); 77 | }; 78 | 79 | /** Checks if the jwt is on the header and it is valid */ 80 | authController.authenticate = async (req, res, next) => { 81 | try { 82 | const authHeader = req.get('Authorization'); 83 | const token = authHeader.split(' ')[1]; // it looks like this: 'Bearer: ' 84 | 85 | const payload = jwt.verify(token, 'secret'); 86 | req.user = payload.user; 87 | next(); 88 | } catch (err) { 89 | // token is bad 90 | next({ 91 | log: `⚠️ [ERROR] authController.authenticate - ${err}`, 92 | message: { 93 | error: 'Authentication failed', 94 | status: 401, 95 | }, 96 | }); 97 | } 98 | }; 99 | 100 | module.exports = authController; 101 | -------------------------------------------------------------------------------- /src/server/controllers/dataset.js: -------------------------------------------------------------------------------- 1 | const DataSet = require('../models/DataSet.js'); 2 | 3 | const { filterProperties } = require('../utils'); 4 | const { ErrorDataSetNotFound } = require('../constants/errors'); 5 | 6 | const dataSetController = {}; 7 | 8 | dataSetController.create = async (req, res, next) => { 9 | const dataSetForm = filterProperties(req.body, [ 10 | 'name', 11 | 'graphColor', 12 | 'type', 13 | 'aggregateFunc', 14 | ]); 15 | 16 | dataSetForm.owner = req.user._id; 17 | 18 | try { 19 | const dataset = await DataSet.create(dataSetForm); 20 | res.status(200).json(dataset); 21 | } catch (err) { 22 | next({ 23 | log: `⚠️ [ERROR] dataSetController.create - ${err}`, 24 | message: { error: "Some shit went down I'm sorry." }, 25 | }); 26 | } 27 | }; 28 | 29 | dataSetController.update = async (req, res, next) => { 30 | const query = { _id: req.params.dataset, owner: req.user._id }; 31 | const dataSetForm = filterProperties(req.body, [ 32 | 'name', 33 | 'graphColor', 34 | 'type', 35 | 'aggregateFunc', 36 | ]); 37 | const options = { new: true, runValidators: true }; 38 | 39 | try { 40 | const updatedDataSet = await DataSet.findOneAndUpdate( 41 | query, 42 | dataSetForm, 43 | options 44 | ).exec(); 45 | 46 | res.status(200).json(updatedDataSet); 47 | } catch (err) { 48 | next({ 49 | log: `⚠️ [ERROR] dataSetController.update - ${err}`, 50 | message: { error: "Some shit went down I'm sorry." }, 51 | }); 52 | } 53 | }; 54 | 55 | dataSetController.getAll = async (req, res, next) => { 56 | try { 57 | const dataSets = await DataSet.find({ owner: req.user._id }).exec(); 58 | res.status(200).json(dataSets); 59 | } catch (err) { 60 | next({ 61 | log: `⚠️ [ERROR] dataSetController.getAll - ${err}`, 62 | message: { error: "Some shit went down I'm sorry." }, 63 | }); 64 | } 65 | }; 66 | 67 | dataSetController.getOne = async (req, res, next) => { 68 | const { dataset: id } = req.params; 69 | 70 | try { 71 | const dataSet = await DataSet.findById({ 72 | _id: id, 73 | owner: req.user._id, 74 | }).exec(); 75 | res.status(200).json(dataSet); 76 | } catch (err) { 77 | next({ 78 | log: `⚠️ [ERROR] dataSetController.getOne - ${err}`, 79 | message: { error: "Some shit went down I'm sorry." }, 80 | }); 81 | } 82 | }; 83 | 84 | dataSetController.delete = async (req, res, next) => { 85 | const { dataset: id } = req.params; 86 | 87 | try { 88 | await DataSet.deleteOne({ _id: id, owner: req.user._id }).orFail( 89 | new ErrorDataSetNotFound() 90 | ); 91 | res.sendStatus(200); 92 | } catch (err) { 93 | next({ 94 | log: `⚠️ [ERROR] dataSetController.delete - ${err}`, 95 | // https://stackoverflow.com/questions/4088350/is-rest-delete-really-idempotent 96 | status: err.status || 500, 97 | message: { error: 'error deleting dataset' }, 98 | }); 99 | } 100 | }; 101 | 102 | module.exports = dataSetController; 103 | -------------------------------------------------------------------------------- /src/client/components/metrics/TaskBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header, Icon, Image, Menu, Segment, Sidebar, Progress, Button } from 'semantic-ui-react'; 3 | 4 | import { fetchMockPoints, fetchMockMetrics } from '../../utils/mockData'; 5 | 6 | export default function TaskBar() { 7 | const dummyData = ['Water', 'Hours Slept', 'Tacos Eaten']; 8 | 9 | // Create an array of menu items. We want one for each task 10 | const menuItems = dummyData.map((task, idx) => ( 11 | 12 | {task} 13 | 14 | )); 15 | 16 | // create a new array using a map over dummydata 17 | const myDailyPointsData = dummyData.map((task, idx) => ( 18 |
19 |
20 | {task} 21 | 22 | 25 |
26 |
27 | )); 28 | /* 29 | myDailyPointsData = [
task + progress bar
,
task
,
task
] 30 | */ 31 | 32 | const pointsWithGraph = myDailyPointsData.map((taskProgress, idx) => 'hello'); 33 | // map over the myDailyPointsData array 34 | // for each element in the array 35 | // we can add a progress bar 36 | 37 | // This function will take in an array of data points and group them by day. The returned output 38 | // will be an array of arrays. Each "inner array" represents 1 day. For example, it may return 39 | // [[X, Y], [A, B]] (note X, Y, A, and B would be objects). This means that X and Y are on the same 40 | // day and A and B are on the same day. But, X and Y are on a different day than A and B. 41 | const aggregateData = (data) => { 42 | // This is a function that receives a datapoint looking like this: 43 | // {timestamp: 1602001174963, value: 9} and returns a string in this format: "Oct062020" 44 | const convertToDateString = (dataPoint) => { 45 | const date = new Date(dataPoint.timestamp); 46 | const dateString = date.toDateString(); 47 | const dateArrayWithoutDayOfWeek = dateString.split(' ').slice(1); 48 | const finalDateString = dateArrayWithoutDayOfWeek.join(''); 49 | return finalDateString; 50 | }; 51 | 52 | const dataGroupedByDay = {}; 53 | 54 | data.forEach((dataPoint) => { 55 | const str = convertToDateString(dataPoint); // Convert current date to a string 56 | 57 | // If we do not have a key for the current date, make one and set its value as an empty array 58 | if (!dataGroupedByDay[str]) { 59 | dataGroupedByDay[str] = []; 60 | } 61 | 62 | // Push the current dataPoint into the corresponding array within our dataGroupedByDay object 63 | dataGroupedByDay[str].push(dataPoint); 64 | }); 65 | 66 | return dataGroupedByDay; 67 | }; 68 | 69 | return ( 70 | 71 | {console.log(aggregateData(fetchMockPoints()))} 72 | 73 | {menuItems} 74 | 75 | 76 | 77 | 78 |
My Daily Points
79 | {/* */} 80 |
{myDailyPointsData}
81 |
82 |
83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/server/controllers/point.js: -------------------------------------------------------------------------------- 1 | const Point = require('../models/Point.js'); 2 | 3 | const pointController = {}; 4 | 5 | pointController.create = async (req, res, next) => { 6 | const newPointForm = { 7 | dataset: req.params.dataset, 8 | owner: req.user._id, 9 | value: req.body.value, 10 | }; 11 | 12 | try { 13 | const newPoint = await Point.create(newPointForm); 14 | res.status(200).json(newPoint); 15 | } catch (err) { 16 | next({ 17 | log: `⚠️ [ERROR] pointController.create - ${err}`, 18 | message: { error: "Some shit went down I'm sorry." }, 19 | }); 20 | } 21 | }; 22 | 23 | pointController.getAll = async (req, res, next) => { 24 | const query = { 25 | dataset: req.params.dataset, 26 | owner: req.user._id, 27 | }; 28 | 29 | try { 30 | const allPoints = await Point.find(query).exec(); 31 | res.status(200).json(allPoints); 32 | } catch (err) { 33 | next({ 34 | log: `⚠️ [ERROR] pointController.getAll - ${err}`, 35 | message: { error: "Some shit went down I'm sorry." }, 36 | }); 37 | } 38 | }; 39 | 40 | pointController.getOne = async (req, res, next) => { 41 | const query = { 42 | _id: req.params.point, 43 | owner: req.user._id, 44 | }; 45 | 46 | try { 47 | const onePoint = await Point.findOne(query).lean().exec(); 48 | res.status(200).json(onePoint); 49 | } catch (err) { 50 | next({ 51 | log: `⚠️ [ERROR] pointController.getOne - ${err}`, 52 | message: { error: "Some shit went down I'm sorry." }, 53 | }); 54 | } 55 | }; 56 | 57 | pointController.update = async (req, res, next) => { 58 | const query = { _id: req.params.point, owner: req.user._id }; 59 | const pointUpdateForm = { value: req.body.value }; 60 | const options = { new: true, runValidators: true }; 61 | 62 | try { 63 | const updatedPoint = await Point.findOneAndUpdate( 64 | query, 65 | pointUpdateForm, 66 | options 67 | ).exec(); 68 | res.status(200).json(updatedPoint); 69 | } catch (err) { 70 | next({ 71 | log: `⚠️ [ERROR] pointController.update - ${err}`, 72 | message: { error: "Some shit went down I'm sorry." }, 73 | }); 74 | } 75 | }; 76 | 77 | pointController.deleteOne = async (req, res, next) => { 78 | const query = { _id: req.params.point, owner: req.user._id }; 79 | 80 | try { 81 | await Point.deleteOne(query).orFail('nothing to delete'); 82 | res.sendStatus(200); 83 | } catch (err) { 84 | next({ 85 | log: `⚠️ [ERROR] pointController.delete - ${err}`, 86 | message: { error: "Some shit went down I'm sorry." }, 87 | }); 88 | } 89 | }; 90 | 91 | pointController.deleteAll = async (req, res, next) => { 92 | const query = { dataset: req.params.dataset, owner: req.user._id }; 93 | 94 | try { 95 | await Point.deleteMany(query).orFail('nothing to delete'); 96 | res.sendStatus(200); 97 | } catch (err) { 98 | next({ 99 | log: `⚠️ [ERROR] pointController.delete - ${err}`, 100 | message: { error: "Some shit went down I'm sorry." }, 101 | }); 102 | } 103 | }; 104 | 105 | module.exports = pointController; 106 | 107 | /* 108 | Point: { 109 | owner: { type: ObjectId required: true }, 110 | dataset: { type: ObjectId, required: true }, 111 | timestamp: { 112 | type: Date, 113 | default: Date.now, 114 | }, 115 | value: { type: Number, required: true }, 116 | }, 117 | */ 118 | -------------------------------------------------------------------------------- /docs/Planning.md: -------------------------------------------------------------------------------- 1 | # Routes 2 | 3 | ## Auth & User Info 4 | 5 | | Method | Route | Purpose | Request Body | Response Body | Status | 6 | | :------- | :------------------ | :------------------------------------ | :----------------------------------------- | :-------------------------------- | :------ | 7 | | **POST** | `/api/signup` | Creates a new user account | `{username:, password:}` | `{jwt: }` | 200,409 | 8 | | **POST** | `/api/login` | Logs in with username and password | `{username: , password: }` | `{jwt: }` | 200,403 | 9 | | **POST** | `/api/authenticate` | Authenticates an existing login token | `{jwt: }` | `{ok: , [error: ]}` | 200,403 | 10 | 11 | > **Note**: May want to include some sort of `/api/settings` to store things like the user's theme, layout, etc...Not sure yet, implementation will inform if this exists or not. 12 | 13 | --- 14 | 15 | ## Data Set Meta 16 | 17 | | Method | Route | Purpose | Request Body | Response Body | Status | 18 | | :--------- | :----------------- | :----------------------- | :----------------- | :---------------- | :------ | 19 | | **POST** | `/api/dataset` | Create new data set | `` | `` | 20 | | **PUT** | `/api/dataset` | Update existing data set | `` | `` | 200,403 | 21 | | **GET** | `/api/dataset` | Get all user data sets | -- | `` | 200,403 | 22 | | **GET** | `/api/dataset/:id` | Get data set by ID | -- | `` | 200,403 | 23 | | **DELETE** | `/api/dataset/:id` | Delete existing data set | -- | -- | 200,403 | 24 | 25 | --- 26 | 27 | ## Data Set Points 28 | 29 | | Method | Route | Purpose | Request Body | Response Body | Status | 30 | | :--------- | :---------------------------------------------------------- | :----------------------------------------------------------- | :-------------- | :-------------- | :-------------- | 31 | | **POST** | `/api/dataset/:id/point` | Create a new point in the data set `id` | `` | `` | 200,400 | 32 | | **GET** | `/api/dataset/:id/point/?start=&end=` | Get points from the data set `id` within the specified range | -- | `` | 200,403 | 33 | | **GET** | `/api/dataset/:id/point/:timestamp` | Get a point by `timestamp` in the data set `id` | | -- | `` | 200,403 | 34 | | **PUT** | `/api/dataset:id/point/:timestamp` | Update existing point by `timestamp` in the data set `id` | `` | `` | 200,403 | 35 | | **DELETE** | `/api/points` | Delete existing data set | `` | -- | 200,403 | 36 | 37 | --- 38 | 39 | ## Request Body Types 40 | 41 | ### `NewDataSetForm` 42 | 43 | ```js 44 | { 45 | 'name': '' 46 | } 47 | ``` 48 | 49 | ## Response Body Types 50 | 51 | ### `DataSetMeta` 52 | 53 | ```js 54 | meta: { 55 | // the name of the data set 56 | name: 'water', 57 | // the graph/icon color 58 | graphColor: 'blue', 59 | // timestamp 60 | createdAt: 321901293875, 61 | // type of the data points 62 | type: 'number', 63 | // the aggregate function that dictates how to group data points 64 | aggregateFunc: 'sum', 65 | }, 66 | ``` 67 | --------------------------------------------------------------------------------