├── readme_images ├── demo.gif ├── logo.png ├── light_dark1.gif ├── light_dark2.gif ├── final_signin.png ├── final_portfolio.png ├── final_register.png ├── wireframe_signin.jpg ├── final_transactions.png ├── wireframe_register.jpg ├── wireframe_portfolio.jpg └── wireframe_transactions.jpg ├── frontend ├── dist │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── postcss.config.js ├── src │ ├── reducers │ │ ├── ui_reducer.js │ │ ├── entities_reducer.js │ │ ├── root_reducer.js │ │ ├── errors_reducer.js │ │ ├── modal_reducer.js │ │ ├── trades_reducer.js │ │ ├── stock_error_reducer.js │ │ ├── session_errors_reducer.js │ │ ├── chart_error_reducer.js │ │ ├── stocks_reducer.js │ │ └── session_reducer.js │ ├── middleware │ │ └── thunk.js │ ├── util │ │ ├── trade_api_util.js │ │ ├── stock_api_util.js │ │ ├── session_api_util.js │ │ └── route_util.js │ ├── actions │ │ ├── modal_actions.js │ │ ├── trade_actions.js │ │ ├── session_actions.js │ │ └── stock_actions.js │ ├── components │ │ ├── root.js │ │ ├── splash │ │ │ ├── splash_container.js │ │ │ └── splash.js │ │ ├── header │ │ │ ├── header_container.js │ │ │ └── header.js │ │ ├── session │ │ │ ├── sign_in_container.js │ │ │ ├── register_container.js │ │ │ ├── sign_in_form.js │ │ │ └── register_form.js │ │ ├── portfolio │ │ │ ├── portfolio_container.js │ │ │ ├── portfolio.js │ │ │ └── portfolio_item.js │ │ ├── purchase │ │ │ ├── purchase_container.js │ │ │ └── purchase_form.js │ │ ├── transactions │ │ │ └── transaction_item.js │ │ ├── theme_switch │ │ │ └── theme_switch.js │ │ ├── modal │ │ │ └── modal.jsx │ │ ├── app.js │ │ ├── footer │ │ │ └── footer.js │ │ └── stock_chart │ │ │ ├── stock_chart_util.js │ │ │ └── stock_chart.js │ ├── store │ │ └── store.js │ └── index.js ├── webpack.prod.js ├── styles │ ├── theme_switch.scss │ ├── modal.scss │ ├── index.scss │ ├── loading_spinner.scss │ ├── color_variables.scss │ ├── footer.scss │ ├── reset.scss │ ├── header.scss │ ├── splash.scss │ ├── session.scss │ ├── chart.scss │ ├── purchase_form.scss │ └── portfolio.scss ├── webpack.dev.js ├── public │ └── index.html ├── package.json └── webpack.common.js ├── .gitignore ├── validation ├── valid-text.js ├── signin.js ├── purchase.js └── register.js ├── config ├── keys.js ├── keys_prod.js └── passport.js ├── .dockerignore ├── models ├── User.js └── Trade.js ├── Dockerfile ├── app.js ├── package.json ├── routes └── api │ ├── trades.js │ ├── stocks.js │ └── users.js └── README.md /readme_images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/demo.gif -------------------------------------------------------------------------------- /readme_images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/logo.png -------------------------------------------------------------------------------- /frontend/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/frontend/dist/favicon.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config/keys_dev.js 2 | /node_modules 3 | /frontend/node_modules 4 | .DS_Store 5 | bundle.js 6 | bundle.css -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("autoprefixer") 4 | ] 5 | }; -------------------------------------------------------------------------------- /readme_images/light_dark1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/light_dark1.gif -------------------------------------------------------------------------------- /readme_images/light_dark2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/light_dark2.gif -------------------------------------------------------------------------------- /frontend/dist/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/frontend/dist/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/frontend/dist/favicon-32x32.png -------------------------------------------------------------------------------- /readme_images/final_signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/final_signin.png -------------------------------------------------------------------------------- /frontend/dist/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/frontend/dist/apple-touch-icon.png -------------------------------------------------------------------------------- /readme_images/final_portfolio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/final_portfolio.png -------------------------------------------------------------------------------- /readme_images/final_register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/final_register.png -------------------------------------------------------------------------------- /readme_images/wireframe_signin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/wireframe_signin.jpg -------------------------------------------------------------------------------- /readme_images/final_transactions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/final_transactions.png -------------------------------------------------------------------------------- /readme_images/wireframe_register.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/wireframe_register.jpg -------------------------------------------------------------------------------- /readme_images/wireframe_portfolio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/wireframe_portfolio.jpg -------------------------------------------------------------------------------- /frontend/dist/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/frontend/dist/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/dist/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/frontend/dist/android-chrome-512x512.png -------------------------------------------------------------------------------- /readme_images/wireframe_transactions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekwolpert/InvestChest/HEAD/readme_images/wireframe_transactions.jpg -------------------------------------------------------------------------------- /validation/valid-text.js: -------------------------------------------------------------------------------- 1 | const validText = str => { 2 | return typeof str === "string" && str.trim().length > 0; 3 | }; 4 | 5 | module.exports = validText; -------------------------------------------------------------------------------- /config/keys.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === "production") { 2 | module.exports = require("./keys_prod"); 3 | } else { 4 | module.exports = require("./keys_dev"); 5 | } -------------------------------------------------------------------------------- /frontend/src/reducers/ui_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import modal from "./modal_reducer"; 4 | 5 | export default combineReducers({ 6 | modal 7 | }); 8 | -------------------------------------------------------------------------------- /frontend/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | 4 | module.exports = merge(common, { 5 | mode: "production" 6 | }); -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /config/keys_dev.js 2 | /node_modules 3 | /frontend/node_modules 4 | .DS_Store 5 | bundle.js 6 | bundle.css 7 | /reademe_images 8 | .gitignore 9 | Dockerfile 10 | .dockerignore 11 | README.md -------------------------------------------------------------------------------- /frontend/styles/theme_switch.scss: -------------------------------------------------------------------------------- 1 | .fa-lightbulb { 2 | font-size: 28px; 3 | margin: auto; 4 | cursor: pointer; 5 | color: $blue; 6 | } 7 | 8 | .fa-lightbulb:hover { 9 | color: $yellow; 10 | } -------------------------------------------------------------------------------- /config/keys_prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongoURI: process.env.MONGO_URI, 3 | secretOrKey: process.env.SECRET_OR_KEY, 4 | iexApiToken: process.env.IEX_API_TOKEN, 5 | iexSandboxToken: process.env.IEX_SANDBOX_TOKEN 6 | }; -------------------------------------------------------------------------------- /frontend/src/middleware/thunk.js: -------------------------------------------------------------------------------- 1 | const thunk = store => next => action => { 2 | if (typeof action === "function") { 3 | return action(store.dispatch, store.getState); 4 | } 5 | return next(action); 6 | }; 7 | 8 | export default thunk; -------------------------------------------------------------------------------- /frontend/src/util/trade_api_util.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getTrades = () => { 4 | return axios.get("/api/trades/history"); 5 | }; 6 | 7 | export const createTrade = data => { 8 | return axios.post("/api/trades/purchase", data); 9 | }; -------------------------------------------------------------------------------- /frontend/src/reducers/entities_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import tradesReducer from "./trades_reducer"; 3 | import stocksReducer from "./stocks_reducer"; 4 | 5 | const entitiesReducer = combineReducers({ 6 | stocks: stocksReducer, 7 | trades: tradesReducer 8 | }); 9 | 10 | export default entitiesReducer; -------------------------------------------------------------------------------- /frontend/src/actions/modal_actions.js: -------------------------------------------------------------------------------- 1 | export const OPEN_MODAL = "OPEN_MODAL"; 2 | export const CLOSE_MODAL = "CLOSE_MODAL"; 3 | 4 | export const openModal = modal => { 5 | return { 6 | type: OPEN_MODAL, 7 | modal 8 | }; 9 | }; 10 | 11 | export const closeModal = () => { 12 | return { 13 | type: CLOSE_MODAL 14 | }; 15 | }; -------------------------------------------------------------------------------- /frontend/src/components/root.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { HashRouter } from "react-router-dom"; 4 | import App from "./app"; 5 | 6 | const Root = ({ store }) => ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | 14 | export default Root; 15 | -------------------------------------------------------------------------------- /frontend/src/reducers/root_reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import session from "./session_reducer"; 3 | import ui from "./ui_reducer"; 4 | import errors from "./errors_reducer"; 5 | import entities from "./entities_reducer"; 6 | 7 | const rootReducer = combineReducers({ 8 | entities, 9 | session, 10 | ui, 11 | errors 12 | }); 13 | 14 | export default rootReducer; -------------------------------------------------------------------------------- /frontend/src/util/stock_api_util.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getStocks = symbols => { 4 | return axios.get(`/api/stocks/batch/${symbols}`); 5 | }; 6 | 7 | export const getStock = symbol => { 8 | return axios.get(`/api/stocks/lookup/${symbol}`); 9 | }; 10 | 11 | export const getChart = (symbol, range) => { 12 | return axios.get(`/api/stocks/chart/${symbol}/${range}`); 13 | }; -------------------------------------------------------------------------------- /frontend/src/components/splash/splash_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import Splash from "./splash"; 3 | import { openModal } from "../../actions/modal_actions"; 4 | import { withRouter } from "react-router-dom"; 5 | 6 | const mapDispatchToProps = dispatch => ({ 7 | openModal: modal => dispatch(openModal(modal)) 8 | }); 9 | 10 | export default withRouter(connect(null, mapDispatchToProps)(Splash)); -------------------------------------------------------------------------------- /frontend/src/reducers/errors_reducer.js: -------------------------------------------------------------------------------- 1 | 2 | import { combineReducers } from "redux"; 3 | 4 | import SessionErrorsReducer from "./session_errors_reducer"; 5 | import StockErrorReducer from "./stock_error_reducer"; 6 | import ChartErrorReducer from "./chart_error_reducer"; 7 | 8 | export default combineReducers({ 9 | session: SessionErrorsReducer, 10 | stock: StockErrorReducer, 11 | chart: ChartErrorReducer 12 | }); -------------------------------------------------------------------------------- /frontend/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require("webpack-merge"); 2 | const common = require("./webpack.common.js"); 3 | const path = require("path"); 4 | 5 | module.exports = merge(common, { 6 | mode: "development", 7 | devtool: "inline-source-map", 8 | watch: true, 9 | devServer: { 10 | contentBase: path.resolve(__dirname, "public"), 11 | publicPath: "/", 12 | proxy: { "/api": "http://localhost:3000" }, 13 | compress: true, 14 | watchContentBase: true 15 | } 16 | }); -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const UserSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true 8 | }, 9 | email: { 10 | type: String, 11 | required: true 12 | }, 13 | password: { 14 | type: String, 15 | required: true 16 | }, 17 | cash: { 18 | type: Number, 19 | default: 5000 20 | }, 21 | date: { 22 | type: Date, 23 | default: Date.now 24 | } 25 | }); 26 | 27 | module.exports = User = mongoose.model("users", UserSchema); -------------------------------------------------------------------------------- /frontend/src/reducers/modal_reducer.js: -------------------------------------------------------------------------------- 1 | import { OPEN_MODAL, CLOSE_MODAL } from "../actions/modal_actions"; 2 | import { RECEIVE_CURRENT_USER } from "../actions/session_actions"; 3 | 4 | export default function modalReducer(state = null, action) { 5 | switch (action.type) { 6 | case OPEN_MODAL: 7 | return action.modal; 8 | case CLOSE_MODAL: 9 | return null; 10 | case RECEIVE_CURRENT_USER: 11 | return null; 12 | default: 13 | return state; 14 | } 15 | } -------------------------------------------------------------------------------- /frontend/dist/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Investchest", 3 | "short_name": "Investchest", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /frontend/styles/modal.scss: -------------------------------------------------------------------------------- 1 | .modal-background { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | right: 0; 6 | left: 0; 7 | background: rgba(var(--opacitybg), 0.8); 8 | z-index: 200; 9 | cursor: zoom-out; 10 | } 11 | 12 | .modal-child { 13 | position: absolute; 14 | top: 50%; 15 | left: 50%; 16 | width: 50%; 17 | min-width: 360px; 18 | max-width: 520px; 19 | transform: translate(-50%, -50%); 20 | background: var(--bg2); 21 | padding: 16px; 22 | border-radius: 4px; 23 | border: var(--text4) solid 4px; 24 | cursor: default; 25 | } -------------------------------------------------------------------------------- /frontend/src/reducers/trades_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_ALL_TRADES, 3 | RECEIVE_TRADE 4 | } from "../actions/trade_actions"; 5 | 6 | import { RECEIVE_USER_LOGOUT } from "../actions/session_actions"; 7 | 8 | export default function(state = null, action) { 9 | switch (action.type) { 10 | case RECEIVE_ALL_TRADES: 11 | return action.trades; 12 | case RECEIVE_TRADE: 13 | return [action.trade, ...state]; 14 | case RECEIVE_USER_LOGOUT: 15 | return null; 16 | default: 17 | return state; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/store/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import thunk from "../middleware/thunk"; 3 | import rootReducer from "../reducers/root_reducer"; 4 | 5 | const middlewares = [thunk]; 6 | 7 | if (process.env.NODE_ENV !== "production") { 8 | const { logger } = require("redux-logger"); 9 | middlewares.push(logger); 10 | } 11 | 12 | const configureStore = (preloadedState = {}) => ( 13 | createStore( 14 | rootReducer, 15 | preloadedState, 16 | applyMiddleware(...middlewares) 17 | ) 18 | ); 19 | 20 | export default configureStore; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine as frontend 2 | 3 | WORKDIR /app 4 | COPY /frontend/package*.json ./ 5 | RUN npm install 6 | COPY /frontend ./ 7 | RUN npm run build 8 | 9 | FROM node:10-alpine 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=frontend /app/dist ./frontend/dist 14 | COPY --from=frontend /app/public ./frontend/public 15 | 16 | COPY package*.json ./ 17 | RUN npm install 18 | COPY /config ./config 19 | COPY /models ./models 20 | COPY /routes ./routes 21 | COPY /validation ./validation 22 | COPY app.js ./ 23 | 24 | ENV PORT 3000 25 | ENV NODE_ENV production 26 | 27 | EXPOSE 3000 28 | 29 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /frontend/src/reducers/stock_error_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_STOCK, RECEIVE_STOCK_ERROR, REMOVE_STOCK_ERROR } from "../actions/stock_actions"; 2 | 3 | const _nullErrors = []; 4 | 5 | const StockErrorReducer = (state = _nullErrors, action) => { 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_STOCK_ERROR: 9 | return action.error; 10 | case RECEIVE_STOCK: 11 | return _nullErrors; 12 | case REMOVE_STOCK_ERROR: 13 | return _nullErrors; 14 | default: 15 | return state; 16 | } 17 | }; 18 | 19 | export default StockErrorReducer; -------------------------------------------------------------------------------- /frontend/src/util/session_api_util.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const setAuthToken = token => { 4 | if (token) { 5 | axios.defaults.headers.common["Authorization"] = token; 6 | } else { 7 | delete axios.defaults.headers.common["Authorization"]; 8 | } 9 | }; 10 | 11 | export const register = (userData) => { 12 | return axios.post("/api/users/register", userData); 13 | }; 14 | 15 | export const signIn = (userData) => { 16 | return axios.post("/api/users/signin", userData); 17 | }; 18 | 19 | export const getCurrentUser = () => { 20 | return axios.post("/api/users/current"); 21 | }; -------------------------------------------------------------------------------- /frontend/src/components/header/header_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { logout } from "../../actions/session_actions"; 3 | import Header from "./header"; 4 | import { openModal } from "../../actions/modal_actions"; 5 | import { withRouter } from "react-router-dom"; 6 | 7 | const mapStateToProps = state => ({ 8 | loggedIn: state.session.isAuthenticated 9 | }); 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | logout: () => dispatch(logout()), 13 | openModal: modal => dispatch(openModal(modal)), 14 | }); 15 | 16 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Header)); -------------------------------------------------------------------------------- /models/Trade.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const TradeSchema = new Schema({ 5 | user: { 6 | type: Schema.Types.ObjectId, 7 | ref: "users" 8 | }, 9 | symbol: { 10 | type: String, 11 | required: true 12 | }, 13 | purchasePrice: { 14 | type: Number, 15 | required: true 16 | }, 17 | numberOfShares: { 18 | type: Number, 19 | required: true 20 | }, 21 | date: { 22 | type: Date, 23 | default: Date.now 24 | } 25 | }); 26 | 27 | module.exports = Trade = mongoose.model("trades", TradeSchema); 28 | -------------------------------------------------------------------------------- /frontend/src/reducers/session_errors_reducer.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | RECEIVE_SESSION_ERRORS, 4 | RECEIVE_CURRENT_USER, 5 | REMOVE_SESSION_ERRORS 6 | } from "../actions/session_actions"; 7 | import { CLOSE_MODAL } from "../actions/modal_actions"; 8 | 9 | const _nullErrors = []; 10 | 11 | const SessionErrorsReducer = (state = _nullErrors, action) => { 12 | Object.freeze(state); 13 | switch (action.type) { 14 | case RECEIVE_SESSION_ERRORS: 15 | return action.errors; 16 | case RECEIVE_CURRENT_USER: 17 | return _nullErrors; 18 | case REMOVE_SESSION_ERRORS: 19 | return _nullErrors; 20 | default: 21 | return state; 22 | } 23 | }; 24 | 25 | export default SessionErrorsReducer; -------------------------------------------------------------------------------- /frontend/src/actions/trade_actions.js: -------------------------------------------------------------------------------- 1 | import * as APIUtil from "../util/trade_api_util"; 2 | 3 | export const RECEIVE_ALL_TRADES = "RECEIVE_ALL_TRADES"; 4 | export const RECEIVE_TRADE = "RECEIVE_TRADE"; 5 | 6 | const receiveAllTrades = trades => ({ 7 | type: RECEIVE_ALL_TRADES, 8 | trades: trades.data 9 | }); 10 | 11 | const receiveTrade = trade => ({ 12 | type: RECEIVE_TRADE, 13 | trade: trade.data.trade, 14 | user: trade.data.user 15 | }); 16 | 17 | export const getTrades = () => dispatch => APIUtil.getTrades() 18 | .then(trades => 19 | dispatch(receiveAllTrades(trades)) 20 | ); 21 | 22 | export const createTrade = trade => dispatch => APIUtil.createTrade(trade) 23 | .then(trade => 24 | dispatch(receiveTrade(trade)) 25 | ); -------------------------------------------------------------------------------- /validation/signin.js: -------------------------------------------------------------------------------- 1 | const Validator = require("validator"); 2 | const validText = require("./valid-text"); 3 | 4 | module.exports = function validateSignInInput(data) { 5 | 6 | let errors = {}; 7 | 8 | data.email = validText(data.email) ? data.email : ""; 9 | data.password = validText(data.password) ? data.password : ""; 10 | 11 | if (!Validator.isEmail(data.email)) { 12 | errors.email = "Email is invalid"; 13 | } 14 | 15 | if (Validator.isEmpty(data.email)) { 16 | errors.email = "Email field is required"; 17 | } 18 | 19 | if (Validator.isEmpty(data.password)) { 20 | errors.password = "Password field is required"; 21 | } 22 | 23 | return { 24 | errors, 25 | isValid: Object.keys(errors).length === 0 26 | }; 27 | }; -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const JwtStrategy = require("passport-jwt").Strategy; 2 | const ExtractJwt = require("passport-jwt").ExtractJwt; 3 | const mongoose = require("mongoose"); 4 | const secretOrKey = require("./keys").secretOrKey; 5 | 6 | const User = mongoose.model("users"); 7 | 8 | const options = {}; 9 | options.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); 10 | options.secretOrKey = secretOrKey; 11 | 12 | module.exports = passport => { 13 | passport.use(new JwtStrategy(options, (jwt_payload, done) => { 14 | User.findById(jwt_payload.id) 15 | .then(user => { 16 | if (user) { 17 | return done(null, user); 18 | } 19 | return done(null, false); 20 | }) 21 | .catch(err => console.log(err)); 22 | })); 23 | }; -------------------------------------------------------------------------------- /frontend/src/reducers/chart_error_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_CHART, RECEIVE_CHART_ERROR, REMOVE_CHART_ERROR, REMOVE_STOCK_ERROR, RECEIVE_STOCK } from "../actions/stock_actions"; 2 | 3 | const _nullErrors = []; 4 | 5 | const ChartErrorReducer = (state = _nullErrors, action) => { 6 | Object.freeze(state); 7 | switch (action.type) { 8 | case RECEIVE_CHART_ERROR: 9 | return action.error; 10 | case RECEIVE_CHART: 11 | return _nullErrors; 12 | case REMOVE_CHART_ERROR: 13 | return _nullErrors; 14 | case RECEIVE_STOCK: 15 | return _nullErrors; 16 | case REMOVE_STOCK_ERROR: 17 | return _nullErrors; 18 | default: 19 | return state; 20 | } 21 | }; 22 | 23 | export default ChartErrorReducer; -------------------------------------------------------------------------------- /frontend/src/components/session/sign_in_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { signIn, removeSessionErrors } from "../../actions/session_actions"; 3 | import { openModal, closeModal } from "../../actions/modal_actions"; 4 | import SignInForm from "./sign_in_form"; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | errors: state.errors.session 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return { 14 | signIn: user => dispatch(signIn(user)), 15 | otherForm: () => dispatch(openModal("register")), 16 | closeModal: () => dispatch(closeModal()), 17 | removeSessionErrors: () => dispatch(removeSessionErrors()) 18 | }; 19 | } 20 | 21 | export default connect(mapStateToProps, mapDispatchToProps)(SignInForm); -------------------------------------------------------------------------------- /frontend/src/reducers/stocks_reducer.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_BATCH_STOCKS, RECEIVE_STOCK, RECEIVE_CHART } from "../actions/stock_actions"; 2 | import { RECEIVE_USER_LOGOUT } from "../actions/session_actions"; 3 | 4 | export default function(state = null, action) { 5 | Object.freeze(state); 6 | switch (action.type) { 7 | case RECEIVE_BATCH_STOCKS: 8 | return action.stocks; 9 | case RECEIVE_STOCK: 10 | return {...state, [action.stock.symbol]: { quote: action.stock } }; 11 | case RECEIVE_CHART: 12 | return { ...state, [action.symbol]: { ...state[action.symbol], chart: { ...state[action.symbol].chart, [action.range]: action.chart } } }; 13 | case RECEIVE_USER_LOGOUT: 14 | return null; 15 | default: 16 | return state; 17 | } 18 | } -------------------------------------------------------------------------------- /frontend/src/components/portfolio/portfolio_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import Portfolio from "./portfolio"; 3 | import { getTrades } from "../../actions/trade_actions"; 4 | import { getStocks } from "../../actions/stock_actions"; 5 | import { withRouter } from "react-router-dom"; 6 | 7 | const mapStateToProps = state => { 8 | return { 9 | user: state.session.user, 10 | trades: state.entities.trades ? state.entities.trades : null, 11 | stocks: state.entities.stocks ? state.entities.stocks : null 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = dispatch => { 16 | return { 17 | getTrades: () => dispatch(getTrades()), 18 | getStocks: (stocks) => dispatch(getStocks(stocks)) 19 | }; 20 | }; 21 | 22 | export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Portfolio)); -------------------------------------------------------------------------------- /frontend/src/components/session/register_container.js: -------------------------------------------------------------------------------- 1 | 2 | import { connect } from "react-redux"; 3 | import { register, signIn, removeSessionErrors} from "../../actions/session_actions"; 4 | import { openModal, closeModal } from "../../actions/modal_actions"; 5 | import RegisterForm from "./register_form"; 6 | 7 | const mapStateToProps = (state) => { 8 | return { 9 | signedIn: state.session.isSignedIn, 10 | errors: state.errors.session 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return { 16 | register: user => dispatch(register(user)), 17 | signIn: user => dispatch(signIn(user)), 18 | otherForm: () => dispatch(openModal("signIn")), 19 | closeModal: () => dispatch(closeModal()), 20 | removeSessionErrors: () => dispatch(removeSessionErrors()) 21 | }; 22 | } 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(RegisterForm); -------------------------------------------------------------------------------- /frontend/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Nunito:400,600,700&display=swap"); 2 | @import "reset"; 3 | @import "color_variables"; 4 | 5 | body { 6 | background: var(--bg3); 7 | color: var(--text3); 8 | 9 | * { 10 | font-family: "Nunito", ; 11 | } 12 | } 13 | 14 | html, body { 15 | height: 100%; 16 | } 17 | 18 | input { 19 | line-height: normal; 20 | -webkit-appearance: none; 21 | -moz-appearance: none; 22 | appearance: none; 23 | } 24 | 25 | .body-container { 26 | max-width: 1280px; 27 | margin: auto; 28 | background: var(--bg2); 29 | border-left: var(--text4) solid 2px; 30 | border-right: var(--text4) solid 2px; 31 | height: calc(100%); 32 | } 33 | 34 | @import "modal"; 35 | @import "loading_spinner"; 36 | @import "session"; 37 | @import "header"; 38 | @import "theme_switch"; 39 | @import "splash"; 40 | @import "portfolio"; 41 | @import "purchase_form"; 42 | @import "chart"; 43 | @import "footer"; -------------------------------------------------------------------------------- /frontend/src/components/purchase/purchase_container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { getStock, getChart, removeStockError } from "../../actions/stock_actions"; 3 | import { createTrade } from "../../actions/trade_actions"; 4 | import PurchaseForm from "./purchase_form"; 5 | 6 | 7 | const mapStateToProps = state => { 8 | return { 9 | user: state.session.user, 10 | stocks: state.entities.stocks ? state.entities.stocks : {}, 11 | stockError: state.errors.stock, 12 | chartError: state.errors.chart 13 | }; 14 | }; 15 | 16 | 17 | const mapDispatchToProps = dispatch => { 18 | return { 19 | getStock: (stock) => dispatch(getStock(stock)), 20 | getChart: (symbol, range) => dispatch(getChart(symbol, range)), 21 | removeStockError: () => dispatch(removeStockError()), 22 | createTrade: (trade) => dispatch(createTrade(trade)) 23 | }; 24 | }; 25 | 26 | export default connect(mapStateToProps, mapDispatchToProps)(PurchaseForm); -------------------------------------------------------------------------------- /frontend/src/components/transactions/transaction_item.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as moment from "moment"; 3 | 4 | const TransactionItem = (props) => { 5 | return ( 6 | <> 7 |
  • 8 |
    9 | BUY – {`${props.trade.symbol} (${ 10 | props.companyName 11 | }) – ${props.trade.numberOfShares} Share${ 12 | props.trade.numberOfShares > 1 ? "s" : "" 13 | } @ $${props.trade.purchasePrice.toFixed(2)}`} 14 |
    15 | 16 | Purchase Date:{" "} 17 | {moment( 18 | props.trade.date 19 | ).format("MMMM Do YYYY, h:mm:ss A")} 20 | 21 |
  • 22 | 23 | 24 | ) 25 | }; 26 | 27 | export default TransactionItem; -------------------------------------------------------------------------------- /frontend/src/util/route_util.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Route, Redirect, withRouter } from "react-router-dom"; 4 | 5 | const Auth = ({ component: Component, path, loggedIn, exact }) => ( 6 | 10 | !loggedIn ? ( 11 | 12 | ) : ( 13 | 14 | ) 15 | } 16 | /> 17 | ); 18 | 19 | const Protected = ({ component: Component, loggedIn, ...rest }) => ( 20 | 23 | loggedIn ? ( 24 | 25 | ) : ( 26 | 27 | ) 28 | } 29 | /> 30 | ); 31 | 32 | const mapStateToProps = state => ( 33 | { loggedIn: state.session.isAuthenticated } 34 | ); 35 | 36 | export const AuthRoute = withRouter(connect(mapStateToProps)(Auth)); 37 | export const ProtectedRoute = withRouter(connect(mapStateToProps)(Protected)); 38 | -------------------------------------------------------------------------------- /frontend/styles/loading_spinner.scss: -------------------------------------------------------------------------------- 1 | .center-spinner { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 100%; 6 | } 7 | 8 | .lds-ring { 9 | display: inline-block; 10 | position: relative; 11 | width: 160px; 12 | height: 160px; 13 | align-items: center; 14 | 15 | div { 16 | box-sizing: border-box; 17 | display: block; 18 | position: absolute; 19 | width: 128px; 20 | height: 128px; 21 | margin: 16px; 22 | border: 16px solid var(--text4); 23 | border-radius: 50%; 24 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 25 | border-color: var(--text4) transparent transparent transparent; 26 | } 27 | 28 | div:nth-child(1) { 29 | animation-delay: -0.45s; 30 | } 31 | 32 | div:nth-child(2) { 33 | animation-delay: -0.3s; 34 | } 35 | 36 | 37 | div:nth-child(3) { 38 | animation-delay: -0.15s; 39 | } 40 | } 41 | 42 | @keyframes lds-ring { 43 | 0% { 44 | transform: rotate(0deg); 45 | } 46 | 100% { 47 | transform: rotate(360deg); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-dom"; 3 | import Root from "./components/root"; 4 | import configureStore from "./store/store"; 5 | import jwt_decode from "jwt-decode"; 6 | import { setAuthToken } from "./util/session_api_util"; 7 | import { logout } from "./actions/session_actions"; 8 | import "../styles/index.scss"; 9 | 10 | document.addEventListener("DOMContentLoaded", () => { 11 | let store; 12 | 13 | if (localStorage.jwtToken) { 14 | setAuthToken(localStorage.jwtToken); 15 | const decodedUser = jwt_decode(localStorage.jwtToken); 16 | const preloadedState = { 17 | session: { isAuthenticated: true, user: decodedUser } 18 | }; 19 | 20 | store = configureStore(preloadedState); 21 | 22 | const currentTime = Date.now() / 1000; 23 | 24 | if (decodedUser.exp < currentTime) { 25 | store.dispatch(logout()); 26 | window.location.href = "/"; 27 | } 28 | } else { 29 | store = configureStore({}); 30 | } 31 | 32 | const root = document.getElementById("root"); 33 | render(, root); 34 | 35 | }); -------------------------------------------------------------------------------- /frontend/src/reducers/session_reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECEIVE_CURRENT_USER, 3 | RECEIVE_USER_LOGOUT, 4 | RECEIVE_USER_SIGN_IN 5 | } from "../actions/session_actions"; 6 | 7 | import { RECEIVE_TRADE } from "../actions/trade_actions"; 8 | 9 | const initialState = { 10 | isAuthenticated: false, 11 | user: {} 12 | }; 13 | 14 | export default function(state = initialState, action) { 15 | switch (action.type) { 16 | case RECEIVE_CURRENT_USER: 17 | return { 18 | ...state, 19 | isAuthenticated: !!action.currentUser, 20 | user: action.currentUser 21 | }; 22 | case RECEIVE_USER_LOGOUT: 23 | return { 24 | isAuthenticated: false, 25 | user: undefined 26 | }; 27 | case RECEIVE_TRADE: 28 | const newState = { ...state } 29 | newState.user.cash = action.user.cash; 30 | return newState; 31 | case RECEIVE_USER_SIGN_IN: 32 | return { 33 | ...state, 34 | isSignedIn: true 35 | }; 36 | default: 37 | return state; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/theme_switch/theme_switch.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 3 | import { faLightbulb } from "@fortawesome/free-regular-svg-icons"; 4 | 5 | class ThemeSwitch extends React.Component { 6 | 7 | componentDidMount() { 8 | if (!document.documentElement.getAttribute("theme-mode")) { 9 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) { 10 | document.documentElement.setAttribute("theme-mode", "dark"); 11 | } else { 12 | document.documentElement.setAttribute("theme-mode", "light"); 13 | } 14 | } 15 | } 16 | 17 | render() { 18 | return ( 19 | { 20 | if (document.documentElement.getAttribute("theme-mode") === "dark") { 21 | document.documentElement.setAttribute("theme-mode", "light"); 22 | } else { 23 | document.documentElement.setAttribute("theme-mode", "dark"); 24 | } 25 | } } /> 26 | ); 27 | } 28 | 29 | } 30 | 31 | export default ThemeSwitch; -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | InvestChest 19 | 20 | 21 | 22 | 23 |
    24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/components/modal/modal.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { closeModal } from "../../actions/modal_actions"; 3 | import { connect } from "react-redux"; 4 | import RegisterContainer from "../session/register_container"; 5 | import SignInContainer from "../session/sign_in_container"; 6 | 7 | function Modal({ modal, closeModal }) { 8 | if (!modal) { 9 | return null; 10 | } 11 | let component; 12 | switch (modal) { 13 | case "signIn": 14 | component = ; 15 | break; 16 | case "register": 17 | component = ; 18 | break; 19 | default: 20 | return null; 21 | } 22 | return ( 23 |
    24 |
    e.stopPropagation()}> 25 | {component} 26 |
    27 |
    28 | ); 29 | } 30 | 31 | const mapStateToProps = state => { 32 | return { 33 | modal: state.ui.modal 34 | }; 35 | }; 36 | 37 | const mapDispatchToProps = dispatch => { 38 | return { 39 | closeModal: () => dispatch(closeModal()) 40 | }; 41 | }; 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(Modal); -------------------------------------------------------------------------------- /frontend/src/components/app.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AuthRoute, ProtectedRoute } from "../util/route_util"; 3 | import { Switch } from "react-router-dom"; 4 | 5 | import Modal from "./modal/modal"; 6 | import HeaderContainer from "./header/header_container"; 7 | import Footer from "./footer/footer"; 8 | import SplashContainer from "./splash/splash_container"; 9 | import PortfolioContainer from "./portfolio/portfolio_container"; 10 | 11 | const App = () => ( 12 | <> 13 | 14 |
    15 | 16 |
    17 |
    18 | 19 | 20 | 25 | 30 | 31 |
    32 |