├── 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 |
22 |
23 | Record
24 |
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 | Add To List
22 |
23 | My goal 1
24 | Goal Stats
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 | openAddPointModal(dataset)}
21 | >
22 |
23 |
24 |
25 | {
34 | viewMetrics(dataset._id);
35 | }}
36 | >
37 |
38 | View Metrics
39 |
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 | Title
31 |
32 |
33 | saveOptions(e.target.placeholder)}
39 | />
40 |
41 |
42 |
48 |
49 |
50 |
51 |
52 | Save
53 |
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 | setModalOpen(false)}>OK
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 |
42 | Username
43 | setUsername(e.target.value)}
47 | />
48 |
49 |
50 | Password
51 | setPassword(e.target.value)}
55 | />
56 |
57 |
58 |
59 |
60 | Sign up
61 |
62 | onSubmit({ username, password })}
66 | >
67 | Log In
68 |
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 |
20 | First Name
21 | setFirstName(e.target.value)}
26 | />
27 |
28 |
29 | Last Name
30 | setLastName(e.target.value)}
35 | />
36 |
37 |
38 | Username
39 | setUsername(e.target.value)}
43 | />
44 |
45 |
46 | Password
47 | setPassword(e.target.value)}
51 | />
52 |
53 |
54 |
55 |
58 | username.length &&
59 | password.length &&
60 | firstName.length &&
61 | lastName.length &&
62 | onSubmit({
63 | username,
64 | password,
65 | firstName,
66 | lastName,
67 | })
68 | }
69 | >
70 | Sign Up
71 |
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 |
23 | Delete
24 |
25 |
26 | {set.name}
27 |
28 | );
29 | });
30 |
31 | return (
32 |
33 |
34 | {dataSets}
35 |
36 |
37 | setModalOpen(true)}
42 | >
43 | Add New Set
44 |
45 |
46 |
47 |
48 | setModalOpen(false)}
53 | // onOpen={() => setModalOpen(true)}
54 | open={modalIsOpen}
55 | // trigger={Show Modal }
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 | setModalOpen(false)}>OK
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 | authenticate(e.target.value.username, e.target.value.password)}
32 | >
33 | Login
34 |
35 |
36 | setModalOpen(true)}>
37 | Sign Up
38 |
39 |
40 | setModalOpen(false)}
45 | // onOpen={() => setModalOpen(true)}
46 | open={modalIsOpen}
47 | // trigger={Show Modal }
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 | setModalOpen(false)}>
86 | Create New Account
87 |
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 |
console.log('this button feels good')}>
23 | Increment
24 |
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 |
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 |
--------------------------------------------------------------------------------