├── .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 | [](https://github.com/gabrielwr/React-Retirement-Calculator/issues)
2 | [](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 |
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 |
27 | You need to enable JavaScript to run this app.
28 |
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 |
20 |
21 | );
22 | };
23 |
24 | export default App;
25 |
--------------------------------------------------------------------------------
/src/elements/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | const NotFound = (props) => {
5 | const { pathname } = props.location || { pathname: "<< no path >>" };
6 | console.error("NotFound: %s not found (%o)", pathname, props);
7 | return (
8 |
9 |
10 | Sorry, I couldn't find {pathname}
11 |
12 |
The router gave me these props:
13 |
{JSON.stringify(props, null, 2)}
14 |
15 | Lost? Here's a way home.
16 |
17 |
~ xoxo.
18 |
19 | );
20 | };
21 |
22 | export default NotFound;
23 |
--------------------------------------------------------------------------------
/src/elements/footer/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Foot = (props) => {
4 | return (
5 |
10 |
More from me!
11 |
12 |
16 |
17 |
18 |
22 |
23 |
24 |
28 |
29 |
30 | }>
31 |
Hey!
32 |
Thanks so much for coming to check this out.
33 |
34 | It's a fun little side project that is still very much a work in
35 | progress!
36 |
37 |
38 | );
39 | };
40 |
41 | export default Foot;
42 |
--------------------------------------------------------------------------------
/src/elements/footer/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Footer = styled.div`
4 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);
5 | `;
6 |
--------------------------------------------------------------------------------
/src/elements/line-chart/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import XAxisTick from "./x-axis-tick";
3 |
4 | import {
5 | ResponsiveContainer,
6 | AreaChart,
7 | Area,
8 | XAxis,
9 | YAxis,
10 | Tooltip
11 | } from "recharts";
12 |
13 | import { GRAPH_AREA_PURPLE, LINE_DEEP_PURPLE } from "../styled";
14 |
15 | import { formatMoney } from "../../utils/formatMoney";
16 |
17 | class LineChart extends Component {
18 | render() {
19 | const { graphData } = this.props;
20 | return (
21 |
22 |
25 | }
30 | dataKey="age"
31 | />
32 | "$" + formatMoney(+money, 0)}
34 | dataKey="savings"
35 | />
36 |
42 | `Age: ${age}`}
45 | formatter={(money) => `$${formatMoney(+money, 0)}`}
46 | />
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default LineChart;
54 |
--------------------------------------------------------------------------------
/src/elements/line-chart/x-axis-tick.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const CustomizedXAxisTick = ({ x, y, payload, startingAge, retireAge }) => {
4 | let xAxisMarker = null;
5 |
6 | if (payload.value === startingAge) {
7 | xAxisMarker = `Today`;
8 | } else if (payload.value === retireAge) {
9 | xAxisMarker = `Retirement`;
10 | }
11 |
12 | if (!xAxisMarker) return null;
13 |
14 | return (
15 |
16 |
17 | {xAxisMarker}
18 |
19 |
20 | );
21 | };
22 |
23 | export default CustomizedXAxisTick;
24 |
--------------------------------------------------------------------------------
/src/elements/navbar/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Navbar, NavbarList, NavbarLink, StyledLink } from "./styled";
4 |
5 | const Nav = () => (
6 |
7 |
8 |
9 | Retirement Calculator
10 |
11 |
12 |
13 | );
14 |
15 | export default Nav;
16 |
--------------------------------------------------------------------------------
/src/elements/navbar/styled.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | import { Link } from "react-router-dom";
4 |
5 | export const Navbar = styled.nav`
6 | background-image: linear-gradient(
7 | 90deg,
8 | #3023ae 0%,
9 | #53a0fd 77%,
10 | #59a5f2 88%,
11 | #82c5ab 95%
12 | );
13 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);
14 | height: 10%;
15 | `;
16 |
17 | export const NavbarList = styled.ul`
18 | height: 100%;
19 | width: 20%;
20 | margin: 0;
21 | display: flex;
22 | align-items: center;
23 | `;
24 |
25 | export const NavbarLink = styled.li`
26 | list-style-type: none;
27 | display: block;
28 | `;
29 |
30 | export const StyledLink = styled(Link)`
31 | color: white;
32 | font-size: 24px;
33 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
34 | text-decoration: none;
35 | `;
36 |
--------------------------------------------------------------------------------
/src/elements/styled/index.js:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | /* Colors */
4 | export const BUTTON_BORDER_BLUE = "#4A90E2";
5 | export const GRAPH_AREA_PURPLE = "rgb(219, 218, 240)";
6 | export const LINE_DEEP_PURPLE = "rgb(70, 65, 180)";
7 |
8 | /* Font Family */
9 | export const FONT_FAMILY_LIGHT =
10 | "HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif";
11 | export const FONT_FAMILY_MEDIUM =
12 | "HelveticaNeue-Medium, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif";
13 | export const FONT_FAMILY =
14 | "Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif";
15 |
16 | /* Font Sizing */
17 | export const FONT_SIZE_BUTTON = "16px";
18 | export const FONT_SIZE_SMALL = "18px";
19 | export const FONT_SIZE_MEDIUM = "20px";
20 | export const FONT_SIZE_LARGE = "22px";
21 | export const FONT_SIZE_X_LARGE = "24px";
22 |
23 | export const IconButton = styled.div`
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | width: 55px;
28 | height: 55px;
29 | border-radius: 50%;
30 | background-color: white;
31 | border: 1px solid ${BUTTON_BORDER_BLUE};
32 | margin-left: ${(props) => props.marginLeft && props.marginLeft};
33 | transition: transform 50ms;
34 | &:hover {
35 | transform: scale(1.15);
36 | }
37 | `;
38 |
39 | export const IconImage = styled.img`
40 | height: 25px;
41 | width: 25px;
42 | `;
43 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: Roboto;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter, Switch, Route } from "react-router-dom";
3 | import { render } from "react-dom";
4 | import { Provider } from "react-redux";
5 |
6 | import store from "./store/";
7 |
8 | import App from "./elements/App";
9 | import CalculatorContainer from "./containers/calculator-container";
10 | import NotFound from "./elements/NotFound";
11 |
12 | import "./index.css";
13 |
14 | render(
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ,
23 | document.getElementById("root")
24 | );
25 |
--------------------------------------------------------------------------------
/src/reducers/calculation-data.js:
--------------------------------------------------------------------------------
1 | import { assoc } from "ramda";
2 |
3 | /*
4 | # (reducer) calculation-data
5 |
6 | The `calculation-data` reducer describes user-specified data needed for graph calculation
7 |
8 | */
9 | export const actionTypes = {
10 | SET_FINAL_SAVINGS: "CALCULATION_DATA_SET_FINAL_SAVINGS",
11 | SET_GRAPH_DATA: "CALCULATION_DATA_SET_GRAPH_DATA",
12 | SET_INVESTMENT_RETURN_RATE: "CALCULATION_DATA_SET_INVESTMENT_RETURN_RATE",
13 | SET_LIFE_EXPECTANCY: "CALCULATION_DATA_SET_LIFE_EXPECTANCY",
14 | SET_RETIRE_AGE: "CALCULATION_DATA_SET_RETIRE_AGE",
15 | SET_RETIRE_AMT: "CALCULATION_DATA_SET_RETIRE_AMT",
16 | SET_RETIRE_SPENDING: "CALCULATION_DATA_SET_RETIRE_SPENDING",
17 | SET_SALARY: "CALCULATION_DATA_SET_SALARY",
18 | SET_SALARY_INCREASE: "CALCULATION_DATA_SET_SALARY_INCREASE",
19 | SET_SAVINGS_RATE: "CALCULATION_DATA_SET_SAVINGS_RATE",
20 | SET_STARTING_AGE: "CALCULATION_DATA_SET_STARTING_AGE",
21 | SET_STARTING_SAVINGS: "CALCULATION_DATA_SET_STARTING_SAVINGS"
22 | };
23 |
24 | export const actions = {
25 | setFinalSavings(savings) {
26 | return { type: actionTypes.SET_FINAL_SAVINGS, savings };
27 | },
28 | setGraphData(graphData) {
29 | return { type: actionTypes.SET_GRAPH_DATA, graphData };
30 | },
31 | setInvestmentReturnRate(returnRate) {
32 | return { type: actionTypes.SET_INVESTMENT_RETURN_RATE, returnRate };
33 | },
34 | setLifeExpectancy(lifeExpectancy) {
35 | return { type: actionTypes.SET_LIFE_EXPECTANCY, lifeExpectancy };
36 | },
37 | setRetireAge(retireAge) {
38 | return { type: actionTypes.SET_RETIRE_AGE, retireAge };
39 | },
40 | setRetireAmt(retireAmt) {
41 | return { type: actionTypes.SET_RETIRE_AMT, retireAmt };
42 | },
43 | setRetireSpending(retireSpending) {
44 | return { type: actionTypes.SET_RETIRE_SPENDING, retireSpending };
45 | },
46 | setSalary(salary) {
47 | return { type: actionTypes.SET_SALARY, salary };
48 | },
49 | setSalaryIncrease(salaryIncrease) {
50 | return { type: actionTypes.SET_SALARY, salaryIncrease };
51 | },
52 | setSavingsRate(savingsRate) {
53 | return { type: actionTypes.SET_SAVINGS_RATE, savingsRate };
54 | },
55 | setStartingAge(age) {
56 | return { type: actionTypes.SET_STARTING_AGE, age };
57 | },
58 | setStartingSavings(savings) {
59 | return { type: actionTypes.SET_STARTING_SAVINGS, savings };
60 | }
61 | };
62 |
63 | export const INITIAL_STATE = {
64 | finalSavings: 0,
65 | graphData: [],
66 | investmentReturnRate: 4,
67 | lifeExpectancy: 90,
68 | retireAge: 65,
69 | retireAmt: 0,
70 | retireSpending: 40000,
71 | salary: 50000,
72 | salaryIncrease: 3,
73 | savingsRate: 10,
74 | startingAge: 26,
75 | startingSavings: 0
76 | };
77 |
78 | export default (_state = INITIAL_STATE, action) => {
79 | let state = Object.assign({}, _state);
80 |
81 | switch (action.type) {
82 | case actionTypes.SET_FINAL_SAVINGS:
83 | state = assoc("finalSavings", action.savings)(state);
84 | break;
85 |
86 | case actionTypes.SET_GRAPH_DATA:
87 | state = assoc("graphData", action.graphData)(state);
88 | break;
89 |
90 | case actionTypes.SET_INVESTMENT_RETURN_RATE:
91 | state = assoc("investmentReturnRate", action.returnRate)(state);
92 | break;
93 |
94 | case actionTypes.SET_LIFE_EXPECTANCY:
95 | state = assoc("lifeExpectancy", action.lifeExpectancy)(state);
96 | break;
97 |
98 | case actionTypes.SET_RETIRE_AGE:
99 | state = assoc("retireAge", action.retireAge)(state);
100 | break;
101 |
102 | case actionTypes.SET_RETIRE_AMT:
103 | state = assoc("retireAmt", action.retireAmt)(state);
104 | break;
105 |
106 | case actionTypes.SET_RETIRE_SPENDING:
107 | state = assoc("retireSpending", action.retireSpending)(state);
108 | break;
109 |
110 | case actionTypes.SET_SALARY:
111 | state = assoc("salary", action.salary)(state);
112 | break;
113 |
114 | case actionTypes.SET_SALARY_INCREASE:
115 | state = assoc("salaryIncrease", action.salaryIncrease)(state);
116 | break;
117 |
118 | case actionTypes.SET_SAVINGS_RATE:
119 | state = assoc("savingsRate", action.savingsRate)(state);
120 | break;
121 |
122 | case actionTypes.SET_STARTING_AGE:
123 | state = assoc("startingAge", action.age)(state);
124 | break;
125 |
126 | case actionTypes.SET_STARTING_SAVINGS:
127 | state = assoc("startingSavings", action.savings)(state);
128 | break;
129 |
130 | default:
131 | return state;
132 | }
133 |
134 | return state;
135 | };
136 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 |
3 | import calculationData from "./calculation-data";
4 |
5 | const rootReducer = combineReducers({
6 | calculationData
7 | });
8 |
9 | export default rootReducer;
10 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === "localhost" ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === "[::1]" ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener("load", () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | "This web app is being served cache-first by a service " +
44 | "worker. To learn more, visit https://goo.gl/SC7cgQ"
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then((registration) => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === "installed") {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log("New content is available; please refresh.");
69 | } else {
70 | // At this point, everything has been precached.
71 | // It's the perfect time to display a
72 | // "Content is cached for offline use." message.
73 | console.log("Content is cached for offline use.");
74 | }
75 | }
76 | };
77 | };
78 | })
79 | .catch((error) => {
80 | console.error("Error during service worker registration:", error);
81 | });
82 | }
83 |
84 | function checkValidServiceWorker(swUrl) {
85 | // Check if the service worker can be found. If it can't reload the page.
86 | fetch(swUrl)
87 | .then((response) => {
88 | // Ensure service worker exists, and that we really are getting a JS file.
89 | if (
90 | response.status === 404 ||
91 | response.headers.get("content-type").indexOf("javascript") === -1
92 | ) {
93 | // No service worker found. Probably a different app. Reload the page.
94 | navigator.serviceWorker.ready.then((registration) => {
95 | registration.unregister().then(() => {
96 | window.location.reload();
97 | });
98 | });
99 | } else {
100 | // Service worker found. Proceed as normal.
101 | registerValidSW(swUrl);
102 | }
103 | })
104 | .catch(() => {
105 | console.log(
106 | "No internet connection found. App is running in offline mode."
107 | );
108 | });
109 | }
110 |
111 | export function unregister() {
112 | if ("serviceWorker" in navigator) {
113 | navigator.serviceWorker.ready.then((registration) => {
114 | registration.unregister();
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from "redux";
2 | import { createLogger } from "redux-logger";
3 |
4 | import rootReducer from "../reducers/";
5 |
6 | const store = createStore(rootReducer, applyMiddleware(createLogger()));
7 |
8 | export default store;
9 |
--------------------------------------------------------------------------------
/src/utils/formatMoney.js:
--------------------------------------------------------------------------------
1 | export const formatMoney = (n, c = ".", d = ",", t) => {
2 | c = isNaN((c = Math.abs(c))) ? 2 : c;
3 | d = d === undefined ? "." : d;
4 | t = t === undefined ? "," : t;
5 | let s = n < 0 ? "-" : "";
6 | let i = String(parseInt((n = Math.abs(Number(n) || 0).toFixed(c)), 10));
7 | let j;
8 | j = (j = i.length) > 3 ? j % 3 : 0;
9 |
10 | return (
11 | s +
12 | (j ? i.substr(0, j) + t : "") +
13 | i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) +
14 | (c
15 | ? d +
16 | Math.abs(n - i)
17 | .toFixed(c)
18 | .slice(2)
19 | : "")
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
--------------------------------------------------------------------------------