├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── Procfile ├── README.md ├── demo1.gif ├── package-lock.json ├── package.json ├── public ├── img │ └── linkedin.png ├── index.html └── manifest.json ├── server └── index.js ├── src ├── assets │ ├── continue.svg │ └── reset.svg ├── components │ ├── age-form │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ └── index.js.snap │ │ │ └── index.js │ │ ├── index.js │ │ └── styled.js │ ├── calculator-form │ │ ├── index.js │ │ └── styled.js │ └── chart │ │ ├── index.js │ │ └── styled.js ├── containers │ └── calculator-container │ │ ├── index.js │ │ └── styled.js ├── elements │ ├── App.js │ ├── NotFound.js │ ├── footer │ │ ├── index.js │ │ └── styled.js │ ├── line-chart │ │ ├── index.js │ │ └── x-axis-tick.js │ ├── navbar │ │ ├── index.js │ │ └── styled.js │ └── styled │ │ └── index.js ├── index.css ├── index.js ├── reducers │ ├── calculation-data.js │ └── index.js ├── registerServiceWorker.js ├── store │ └── index.js └── utils │ └── formatMoney.js └── travis.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # testing 5 | /coverage 6 | 7 | # production 8 | /build 9 | 10 | # misc 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | package-lock.json 3 | node_modules/ 4 | src/assets/ 5 | public/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "none", 8 | "jsxBracketSameLine": true 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gabriel Rowe 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node ./server/index.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub issues](https://img.shields.io/github/issues/gabrielwr/React-Retirement-Calculator.svg)](https://github.com/gabrielwr/React-Retirement-Calculator/issues) 2 | [![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/gabrielwr/React-Retirement-Calculator.svg)](https://github.com/gabrielwr/React-Retirement-Calculator/pulls?q=is%3Apr+is%3Aclosed) 3 | 4 | # React Retirement Calculator 5 | 6 | Deployed link here: https://dynamic-retirement.herokuapp.com/ (give Heroku a bit to wake up!) 7 | 8 | 9 | React Retirement Calculator is a front end application using recharts.js to provide a dynamically updating graph and user experience. 10 | 11 | ### Demo 12 | Video Walkthrough 13 | 14 | ## Table of Contents 15 | 16 | * [Background](#background) 17 | * [Install](#install) 18 | * [License](#license) 19 | 20 | 21 | ## Background 22 | 23 | React Retirement Calculator is a fun side project that I worked on while working at Fullstack Academy as a teaching fellow and software engineer. 24 | 25 | I originally conceived of the idea for this calculator after a chapter of learning about personal finance. During that time, I had a favorite retirement calculator that just disappeared from the internet one day. So I figured I would try to recreate it and even improve upon it (since it was probably made in the early 2000's...) 26 | 27 | This project is an updated version of a calculator I originally made in vanilla JS, HTML, and CSS. 28 | 29 | The basic premise of this calculator is to show the power of compounding interest over time. This calculator has been a great asset for me when I need a little help with remembering to save money ;). 30 | 31 | ## Install 32 | 33 | To install and locally run RRC clone or download the project to your machine and run the following commands: 34 | 35 | Install all project dependencies: 36 | ```bash 37 | $ npm install 38 | ``` 39 | To start the server and app: 40 | ```bash 41 | $ npm start 42 | ``` 43 | ## License 44 | MIT (c) Gabriel Rowe 45 | 46 | -------------------------------------------------------------------------------- /demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielwr/React-Retirement-Calculator/089f16efbdf684df81f6e4ee813cc504acdc6613/demo1.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "investment-calc", 3 | "version": "2.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "body-parser": "^1.19.0", 7 | "express": "^4.17.1", 8 | "express-session": "^1.17.1", 9 | "ramda": "^0.27.1", 10 | "react": "^16.14.0", 11 | "react-dom": "^16.14.0", 12 | "react-redux": "^5.1.2", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "^4.0.0", 15 | "recharts": "^1.8.5", 16 | "redux": "^3.7.2", 17 | "styled-components": "^5.2.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test --env=jsdom", 23 | "eject": "react-scripts eject" 24 | }, 25 | "devDependencies": { 26 | "react-test-renderer": "^16.14.0", 27 | "redux-logger": "^3.0.6", 28 | "volleyball": "^1.5.1" 29 | }, 30 | "browserslist": [ 31 | ">0.2%", 32 | "not dead", 33 | "not ie <= 11", 34 | "not op_mini all" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /public/img/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabrielwr/React-Retirement-Calculator/089f16efbdf684df81f6e4ee813cc504acdc6613/public/img/linkedin.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | 23 | Retirement Calculator 24 | 25 | 26 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const express = require("express"); 3 | const volleyball = require("volleyball"); 4 | const bodyParser = require("body-parser"); 5 | const session = require("express-session"); 6 | 7 | const app = express(); 8 | const PORT = process.env.PORT || 3000; 9 | 10 | app.use(express.static(path.join(__dirname, "../public"))); 11 | 12 | app.use(bodyParser.json()); 13 | app.use(bodyParser.urlencoded({ extended: true })); 14 | 15 | app.use( 16 | session({ 17 | secret: "a wildly insecure secret", 18 | resave: false, 19 | saveUninitialized: false 20 | }) 21 | ); 22 | 23 | app.use(volleyball); 24 | 25 | app.listen(PORT, () => { 26 | console.log("Server listening on port: ", PORT); 27 | }); 28 | 29 | app.use((err, req, res, next) => { 30 | console.error(err); 31 | console.error(err.stack); 32 | res.status(err.status || 500).send(err.message || "Internal Server Error"); 33 | }); 34 | 35 | //Sends index.html file to client on all get requests 36 | app.get("*", (req, res, next) => { 37 | res.sendFile(path.join(__dirname, "../public/index.html")); 38 | }); 39 | -------------------------------------------------------------------------------- /src/assets/continue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/age-form/__tests__/__snapshots__/index.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 |
7 | 8 | I am 9 | 10 | 15 | 16 | years old with a life expectancy of 17 | 18 | 23 |
24 | `; 25 | -------------------------------------------------------------------------------- /src/components/age-form/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AgeForm } from "../"; 3 | import renderer from "react-test-renderer"; 4 | 5 | it("renders correctly", () => { 6 | const tree = renderer 7 | .create() 8 | .toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/age-form/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Proptypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import * as R from "ramda"; 5 | 6 | import { AgeFormWrapper, AgeFormInput } from "./styled"; 7 | 8 | class AgeForm extends Component { 9 | handleLifespanAge = (_evt, lifeExpectancy) => { 10 | const { startingAge, addCurrentAge, setLifeExpectancy } = this.props; 11 | 12 | if (lifeExpectancy <= +startingAge) { 13 | addCurrentAge(lifeExpectancy - 1); 14 | } 15 | setLifeExpectancy(lifeExpectancy); 16 | this.computeData(); 17 | }; 18 | 19 | render() { 20 | const { startingAge, lifeExpectancy } = this.props; 21 | return ( 22 | 23 | I am 24 | 25 | years old with a life expectancy of 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | const mapStateToProps = (state) => { 33 | const lifeExpectancy = R.path(["calculationData", "lifeExpectancy"])(state); 34 | const startingAge = R.path(["calculationData", "startingAge"])(state); 35 | 36 | return { 37 | lifeExpectancy, 38 | startingAge 39 | }; 40 | }; 41 | 42 | AgeForm.proptypes = { 43 | lifeExpectancy: Proptypes.number, 44 | startingAge: Proptypes.number 45 | }; 46 | 47 | export { AgeForm }; 48 | export default connect(mapStateToProps)(AgeForm); 49 | -------------------------------------------------------------------------------- /src/components/age-form/styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const AgeFormWrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | height: 7%; 8 | `; 9 | 10 | export const AgeFormInput = styled.input` 11 | border: 0; 12 | outline: 0; 13 | background: transparent; 14 | border-bottom: 1px dashed black; 15 | text-align: center; 16 | margin: 0 10px 0 10px; 17 | `; 18 | -------------------------------------------------------------------------------- /src/components/calculator-form/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import { formatMoney } from "../../utils/formatMoney"; 4 | 5 | import { FormWrapper, FormColumn, Input, Row } from "./styled"; 6 | 7 | class CalculatorForm extends Component { 8 | render() { 9 | const { 10 | handleCurrentAge, 11 | handleRetirementAge, 12 | handleLifespanAge, 13 | startingSavings, 14 | investmentReturnRate, 15 | retireAge, 16 | retireSpending, 17 | salary, 18 | salaryIncrease, 19 | savingsRate 20 | } = this.props; 21 | 22 | return ( 23 | 24 | 25 | 26 | Net Income 27 | 28 | $/yr 29 | 30 | 31 | Current Savings 32 | 33 | $ 34 | 35 | 36 | Investment Return 37 | 38 | % 39 | 40 | 41 | 42 | 43 | Savings Rate 44 | 45 | % 46 | 47 | 48 | Retirement Age 49 | 50 | yrs 51 | 52 | 53 | Withdrawal Rate 54 | 55 | % 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default CalculatorForm; 64 | -------------------------------------------------------------------------------- /src/components/calculator-form/styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const FormWrapper = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | flex-direction: row; 7 | align-items: center; 8 | justify-content: space-evenly; 9 | box-shadow: 0 4px 6px 0 #e5e5e5; 10 | width: 60%; 11 | height: 25%; 12 | `; 13 | 14 | export const FormColumn = styled.div` 15 | display: flex; 16 | flex-direction: column; 17 | height: 100%; 18 | width: 40%; 19 | `; 20 | 21 | export const Row = styled.div` 22 | display: flex; 23 | flex-direction: row; 24 | justify-content: space-between; 25 | align-items: center; 26 | height: ${(props) => (props.advanced ? 25 : 100 / 3)}%; 27 | `; 28 | 29 | export const Input = styled.input` 30 | text-align: center; 31 | `; 32 | -------------------------------------------------------------------------------- /src/components/chart/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import * as R from "ramda"; 4 | 5 | import LineChart from "../../elements/line-chart/index"; 6 | 7 | import { ChartWrapper } from "./styled"; 8 | 9 | class Chart extends Component { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | 19 | const mapState = (state) => { 20 | const graphData = R.path(["calculationData", "graphData"])(state); 21 | const retireAge = R.path(["calculationData", "retireAge"])(state); 22 | const startingAge = R.path(["calculationData", "startingAge"])(state); 23 | 24 | return { 25 | graphData, 26 | retireAge, 27 | startingAge 28 | }; 29 | }; 30 | 31 | export default connect(mapState, null)(Chart); 32 | -------------------------------------------------------------------------------- /src/components/chart/styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ChartWrapper = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | height: 40vh; 8 | width: 100%; 9 | `; 10 | -------------------------------------------------------------------------------- /src/containers/calculator-container/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import * as R from "ramda"; 4 | 5 | import { actions as calculationDataActions } from "../../reducers/calculation-data"; 6 | 7 | import CalculatorForm from "../../components/calculator-form"; 8 | import Chart from "../../components/chart"; 9 | import AgeForm from "../../components/age-form"; 10 | 11 | import { CalculatorContainerWrapper, ContentWrapper } from "./styled"; 12 | 13 | import { IconButton, IconImage } from "../../elements/styled"; 14 | 15 | import ContinueIcon from "../../assets/continue.svg"; 16 | 17 | const { 18 | setFinalSavings, 19 | setGraphData, 20 | setInvestmentReturnRate, 21 | setLifeExpectancy, 22 | setRetireAge, 23 | setRetireAmt, 24 | setRetireSpending, 25 | setSalary, 26 | setSalaryIncrease, 27 | setSavingsRate, 28 | setStartingAge, 29 | setStartingSavings 30 | } = calculationDataActions; 31 | 32 | class CalculatorContainer extends Component { 33 | componentWillMount() { 34 | this.computeData(); 35 | } 36 | 37 | computeData() { 38 | const { 39 | startingAge, 40 | salary, 41 | salaryIncrease, 42 | retireAge, 43 | savingsRate, 44 | lifeExpectancy, 45 | startingSavings, 46 | investmentReturnRate, 47 | retireSpending 48 | } = this.props; 49 | 50 | const graphData = []; 51 | const yearsToRetirement = retireAge - startingAge; 52 | const totalYears = lifeExpectancy - startingAge; 53 | let salarySaved = Math.floor((salary / 100) * savingsRate); 54 | let currentAge = startingAge; 55 | let accumulatedSavings = startingSavings; 56 | let isRetired = yearsToRetirement > 0 ? false : true; 57 | 58 | for (let currentYear = 0; currentYear <= totalYears; currentYear++) { 59 | graphData.push({ 60 | savings: accumulatedSavings, 61 | age: currentAge 62 | }); 63 | 64 | accumulatedSavings += Math.floor( 65 | (accumulatedSavings / 100) * investmentReturnRate 66 | ); 67 | if (currentYear === yearsToRetirement) { 68 | this.props.setRetireAmt(accumulatedSavings); 69 | } 70 | if (isRetired) { 71 | accumulatedSavings -= retireSpending; 72 | } else { 73 | salarySaved += Math.floor((salarySaved / 100) * salaryIncrease); 74 | accumulatedSavings += salarySaved; 75 | } 76 | 77 | currentAge++; 78 | } 79 | 80 | this.props.setFinalSavings(accumulatedSavings); 81 | this.props.setGraphData(graphData); 82 | } 83 | 84 | handleStartingAge = (_evt, age) => { 85 | const { 86 | retireAge, 87 | setLifeExpectancy, 88 | lifeExpectancy, 89 | setStartingAge, 90 | setRetireAge 91 | } = this.props; 92 | 93 | if (age >= retireAge) { 94 | setRetireAge(age + 1); 95 | } 96 | if (age >= lifeExpectancy) { 97 | setLifeExpectancy(age + 1); 98 | } 99 | setStartingAge(age); 100 | this.computeData(); 101 | }; 102 | 103 | handleRetirementAge = (_evt, retireAge) => { 104 | const { 105 | setStartingAge, 106 | setRetireAge, 107 | startingAge, 108 | lifeExpectancy 109 | } = this.props; 110 | if (retireAge <= startingAge) { 111 | setStartingAge(retireAge - 1); 112 | } else if (retireAge >= lifeExpectancy) { 113 | setRetireAge(lifeExpectancy - 1); 114 | } 115 | setRetireAge(retireAge); 116 | this.computeData(); 117 | }; 118 | 119 | changeHandler = (keyName) => { 120 | return (_evt, updatedValue) => { 121 | this.props[`set${keyName}`](`${updatedValue}`); 122 | this.computeData(); 123 | }; 124 | }; 125 | 126 | render() { 127 | const { 128 | finalSavings, 129 | investmentReturnRate, 130 | retireAge, 131 | retireAmt, 132 | retireSpending, 133 | salary, 134 | salaryIncrease, 135 | savingsRate, 136 | startingSavings, 137 | startingAge 138 | } = this.props; 139 | 140 | const formProps = { 141 | finalSavings, 142 | investmentReturnRate, 143 | retireAge, 144 | retireAmt, 145 | retireSpending, 146 | salary, 147 | salaryIncrease, 148 | savingsRate, 149 | startingSavings, 150 | startingAge, 151 | handleStartingAge: this.handleStartingAge, 152 | handleRetirementAge: this.handleRetirementAge 153 | }; 154 | 155 | return ( 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | ); 164 | } 165 | } 166 | 167 | const mapState = (state) => { 168 | const finalSavings = R.path(["calculationData", "finalSavings"])(state); 169 | const investmentReturnRate = R.path([ 170 | "calculationData", 171 | "investmentReturnRate" 172 | ])(state); 173 | const retireAge = R.path(["calculationData", "retireAge"])(state); 174 | const retireAmt = R.path(["calculationData", "retireAmt"])(state); 175 | const retireSpending = R.path(["calculationData", "retireSpending"])(state); 176 | const salary = R.path(["calculationData", "salary"])(state); 177 | const salaryIncrease = R.path(["calculationData", "retireAmt"])(state); 178 | const savingsRate = R.path(["calculationData", "savingsRate"])(state); 179 | const startingSavings = R.path(["calculationData", "startingSavings"])(state); 180 | const startingAge = R.path(["calculationData", "startingAge"])(state); 181 | 182 | const lifeExpectancy = R.path(["calculationData", "lifeExpectancy"])(state); 183 | 184 | return { 185 | finalSavings, 186 | investmentReturnRate, 187 | retireAge, 188 | retireAmt, 189 | retireSpending, 190 | salary, 191 | salaryIncrease, 192 | savingsRate, 193 | startingSavings, 194 | startingAge, 195 | lifeExpectancy 196 | }; 197 | }; 198 | 199 | const mapDispatch = { 200 | setFinalSavings, 201 | setGraphData, 202 | setInvestmentReturnRate, 203 | setLifeExpectancy, 204 | setRetireAge, 205 | setRetireAmt, 206 | setRetireSpending, 207 | setSalary, 208 | setSalaryIncrease, 209 | setSavingsRate, 210 | setStartingAge, 211 | setStartingSavings 212 | }; 213 | 214 | export default connect(mapState, mapDispatch)(CalculatorContainer); 215 | -------------------------------------------------------------------------------- /src/containers/calculator-container/styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const CalculatorContainerWrapper = styled.div` 4 | height: 100%; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-around; 9 | `; 10 | 11 | export const ContentWrapper = styled.div` 12 | height: 90%; 13 | `; 14 | -------------------------------------------------------------------------------- /src/elements/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import { Route } from "react-router-dom"; 4 | 5 | import CalculatorContainer from "../containers/calculator-container"; 6 | import Navbar from "./navbar"; 7 | import Footer from "./footer"; 8 | 9 | const AppWrapper = styled.div` 10 | height: 100vh; 11 | width: 100vw; 12 | `; 13 | 14 | const App = () => { 15 | return ( 16 | 17 | 18 | 19 |