Skip to content

Commit 3e2ff18

Browse files
authored
Setup foundation of centralizing user state (#142)
Setup foundation of centralizing user state
2 parents 1d3c82b + a17d3ee commit 3e2ff18

8 files changed

+184
-40
lines changed

.eslintrc.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports = {
2+
parser: '@typescript-eslint/parser',
3+
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
4+
settings: {
5+
react: {
6+
version: 'detect',
7+
},
8+
},
9+
env: {
10+
browser: true,
11+
es2017: true,
12+
node: true,
13+
},
14+
parserOptions: {
15+
ecmaFeatures: {
16+
jsx: true,
17+
},
18+
ecmaVersion: 2017,
19+
sourceType: 'module',
20+
},
21+
rules: {
22+
'react-hooks/rules-of-hooks': 'error',
23+
'react-hooks/exhaustive-deps': 'error',
24+
},
25+
}

globals.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare namespace NodeJS {
2+
// By default, Typescript does not have sessionStorage on the Global type. We add it here.
3+
interface Global {
4+
sessionStorage: {
5+
getItem: (key: string) => string
6+
setItem: (key: string, value: string) => void
7+
}
8+
}
9+
}

package.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
"@observablehq/runtime": "4.2.1",
1111
"@reach/router": "^1.2.1",
1212
"@rebass/grid": "^6.0.0",
13+
"@types/react-datepicker": "^2.10.0",
1314
"@types/react-helmet": "^5.0.8",
15+
"@types/use-persisted-state": "^0.3.0",
1416
"babel-preset-gatsby": "^0.1.11",
1517
"d3": "^5.11.0",
1618
"dayjs": "^1.8.15",
@@ -36,14 +38,20 @@
3638
"react-helmet": "^5.2.0",
3739
"tesseract.js": "^2.0.0-alpha.11",
3840
"typescript": "^3.3.4000",
41+
"use-persisted-state": "^0.3.0",
3942
"windfall-awareness-notebook-prototype": "https://api.observablehq.com/@thadk/windfall-awareness-notebook-prototype.tgz?v=1"
4043
},
4144
"devDependencies": {
4245
"@types/rc-slider": "^8.6.5",
4346
"@types/react": "^16.8.8",
4447
"@types/react-dom": "^16.8.3",
48+
"@typescript-eslint/eslint-plugin": "^2.15.0",
49+
"@typescript-eslint/parser": "^2.15.0",
4550
"babel-jest": "^24.9.0",
4651
"babel-plugin-emotion": "^10.0.9",
52+
"eslint": "^6.8.0",
53+
"eslint-plugin-react": "^7.17.0",
54+
"eslint-plugin-react-hooks": "^2.3.0",
4755
"identity-obj-proxy": "^3.0.0",
4856
"jest": "^24.9.0",
4957
"prettier": "^1.16.4",
@@ -59,7 +67,7 @@
5967
"format": "prettier --write src/**/*.{js,jsx,tsx}",
6068
"start": "npm run develop",
6169
"serve": "gatsby serve",
62-
"test": "jest"
70+
"test": "eslint . --ext=ts,tsx && jest"
6371
},
6472
"repository": {
6573
"type": "git",

src/layouts/index.tsx

+18-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Location } from "@reach/router";
55
import { Header, QuestionProvider, Footer, ButtonLink, ButtonLinkGreen } from "../components";
66
import "./layout.css";
77
import { ProgressTracker } from "../components/progress-tracker";
8+
import UserStateManager from "../library/user-state-manager"
89

910
const Wrapper = styled("div")`
1011
display: block;
@@ -89,20 +90,24 @@ const Layout = ({ children }) => (
8990
<Header />
9091
<link href="https://fonts.googleapis.com/css?family=Merriweather|Montserrat&display=swap" rel="stylesheet"/>
9192
<ContentContainer>
92-
<Location>
93-
{({ location }) => (
94-
<ProgressTracker
95-
linkProps={LINKSPATH}
96-
activePath={location.pathname}
97-
/>
98-
)}
99-
</Location>
93+
<Location>
94+
{({ location }) => (
95+
<ProgressTracker
96+
linkProps={LINKSPATH}
97+
activePath={location.pathname}
98+
/>
99+
)}
100+
</Location>
100101
<Main id='child-wrapper'>
101-
{/* TODO test out this provider */}
102-
<QuestionProvider>{children}</QuestionProvider>
102+
<UserStateManager>
103+
{/* TODO test out this provider */}
104+
<QuestionProvider>
105+
{children}
106+
</QuestionProvider>
107+
</UserStateManager>
103108
</Main>
104-
</ContentContainer>
105-
<Footer>
109+
</ContentContainer>
110+
<Footer>
106111
<Location>
107112
{({ location }) => {
108113
const index = LINKSPATH.findIndex(path => path.path === location.pathname)
@@ -149,4 +154,4 @@ const Layout = ({ children }) => (
149154
/>
150155
);
151156

152-
export default Layout;
157+
export default Layout;
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as React from 'react'
2+
3+
export interface UserStateActions {
4+
setBirthDate: (date: Date) => void
5+
setRetireDate: (date: Date) => void
6+
}
7+
8+
const UserStateActionsContext = React.createContext<UserStateActions | null>(null)
9+
10+
export const UserStateActionsContextProvider = UserStateActionsContext.Provider
11+
12+
export function useUserStateActions(): UserStateActions {
13+
const actions = React.useContext(UserStateActionsContext)
14+
if (!actions) {
15+
throw new Error('Cannot access user state actions outside of a `<UserStateManager>`.')
16+
}
17+
return actions
18+
}

src/library/user-state-context.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as React from 'react'
2+
3+
export interface UserState {
4+
birthDate: Date | null
5+
retireDate: Date | null
6+
}
7+
8+
const UserStateContext = React.createContext<UserState | null>(null)
9+
10+
export const UserStateContextProvider = UserStateContext.Provider
11+
12+
export function useUserState(): UserState {
13+
const userState = React.useContext(UserStateContext)
14+
if (!userState) {
15+
throw new Error('Cannot access user state outside of a `<UserStateManager>`.')
16+
}
17+
return userState
18+
}

src/library/user-state-manager.tsx

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, {useMemo} from 'react'
2+
import createPersistedState from 'use-persisted-state';
3+
import dayjs from 'dayjs'
4+
5+
import {UserStateContextProvider, UserState} from './user-state-context'
6+
import {UserStateActions, UserStateActionsContextProvider} from './user-state-actions-context'
7+
8+
// Must use sessionStorage (not localStorage) or else it conflicts with other uses of sessionStorage within app
9+
const useBirthDateState = createPersistedState('BirthDate', global.sessionStorage);
10+
const useRetireDateState = createPersistedState('RetireDate', global.sessionStorage);
11+
12+
/**
13+
* Helper function to get a Date object equivalent to the start of the date given
14+
*/
15+
function startOfDay(date: Date): Date {
16+
return dayjs(date).startOf('day').toDate()
17+
}
18+
19+
interface UserStateManagerProps {
20+
children: React.ReactNode
21+
}
22+
23+
/**
24+
* Serve as a multi-context provider for descendent components, providing them with access to
25+
* the user state and a set of actions to mutate that state, while persisting it to session storage.
26+
*/
27+
export default function UserStateManager(props: UserStateManagerProps): JSX.Element {
28+
const {children} = props
29+
const [birthDate, setBirthDate] = useBirthDateState<Date | null>(null)
30+
const [retireDate, setRetireDate] = useRetireDateState<Date | null>(null)
31+
32+
const actions: UserStateActions = useMemo(() => ({
33+
setBirthDate: date => setBirthDate(startOfDay(date)),
34+
setRetireDate: date => setRetireDate(startOfDay(date)),
35+
}), [setBirthDate, setRetireDate])
36+
37+
const userState: UserState = useMemo(() => ({
38+
birthDate: birthDate ? new Date(birthDate) : null,
39+
retireDate: retireDate ? new Date(retireDate) : null,
40+
}), [birthDate, retireDate])
41+
42+
return (
43+
<UserStateContextProvider value={userState}>
44+
<UserStateActionsContextProvider value={actions}>
45+
{children}
46+
</UserStateActionsContextProvider>
47+
</UserStateContextProvider>
48+
)
49+
}

src/pages/prescreen-1a.tsx

+38-26
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { colors } from "../constants";
77
import { SessionStore } from "../library/session-store";
88
import dayjs from "dayjs";
99

10+
import {UserState, useUserState} from '../library/user-state-context'
11+
import {UserStateActions, useUserStateActions} from '../library/user-state-actions-context'
12+
1013
import {
1114
TextBlock,
1215
SEO,
@@ -31,26 +34,25 @@ const H4 = styled.h4`
3134
margin: 5px 0;
3235
`
3336

34-
export default class Prescreen1c extends React.Component {
37+
interface Prescreen1aProps {
38+
userState: UserState
39+
userStateActions: UserStateActions
40+
}
41+
42+
interface Prescreen1aState {
43+
retireAge: number | null
44+
}
45+
46+
class Prescreen1a extends React.Component<Prescreen1aProps, Prescreen1aState> {
47+
public state: Prescreen1aState = {
48+
retireAge: null,
49+
}
3550
constructor(props, context){
3651
super(props, context)
3752
this.handleDateChange = this.handleDateChange.bind(this);
38-
this.state = {
39-
birthDate: null,
40-
retireAge: null,
41-
retireDate: null
42-
};
4353
}
4454

4555
componentDidMount() {
46-
if (SessionStore.get("BirthDate") && (this.state.birthDate === null)){
47-
var birthdate = new Date(JSON.parse(SessionStore.get("BirthDate")))
48-
this.setState({
49-
birthDate: birthdate
50-
})
51-
52-
}
53-
5456
if (this.state.retireAge === null) {
5557
if (SessionStore.get("RetireAge") !== null) {
5658
var retireAge = JSON.parse(SessionStore.get("RetireAge"))
@@ -63,25 +65,24 @@ export default class Prescreen1c extends React.Component {
6365
}
6466
}
6567

66-
async handleDateChange(name, value){
68+
async handleDateChange(name, value) {
69+
const {userStateActions, userState} = this.props
70+
const {birthDate} = userState
6771
if (name === "birthDatePicked") {
68-
SessionStore.push("BirthDate", JSON.stringify(value))
72+
userStateActions.setBirthDate(value)
6973
var year62 = new Date(value).getFullYear() + 62;
7074
SessionStore.push("Year62", year62)
71-
var fullRetirementAge = await ObsFuncs.getFullRetirementDateSimple(this.state.birthDate)
75+
var fullRetirementAge = await ObsFuncs.getFullRetirementDateSimple(birthDate)
7276
this.setRetireDate(value, fullRetirementAge)
73-
var state = {birthDate: value}
74-
this.setState(state)
7577
}
7678
}
7779

7880
async setRetireDate(dateOfBirth, retireAge) {
81+
const {userStateActions} = this.props
7982
var retireDate = dayjs(dateOfBirth).add(await retireAge, 'years').toDate()
80-
SessionStore.push("RetireDate", JSON.stringify(retireDate))
83+
userStateActions.setRetireDate(retireDate)
8184
this.setState({
8285
retireAge: JSON.stringify(retireAge),
83-
retireDate: JSON.stringify(retireDate),
84-
retireDateYear: JSON.stringify(retireDate.getFullYear()),
8586
})
8687
}
8788

@@ -96,6 +97,9 @@ export default class Prescreen1c extends React.Component {
9697
// }
9798

9899
render() {
100+
const {userState} = this.props
101+
const {birthDate, retireDate} = userState
102+
const retireDateYear = retireDate ? retireDate.getFullYear() : null
99103
return (
100104
<div>
101105
<SEO title="Pre-Screen 1a" keywords={[`gatsby`, `application`, `react`]} />
@@ -108,21 +112,29 @@ export default class Prescreen1c extends React.Component {
108112
<StyledDatePicker
109113
id="birthDatePicked"
110114
placeholderText="Click to select a date"
111-
selected={this.state.birthDate}
115+
selected={birthDate}
112116
showYearDropdown
113-
openToDate={this.state.birthDate || dayjs().subtract(64, 'years').toDate()}
117+
openToDate={birthDate || dayjs().subtract(64, 'years').toDate()}
114118
onChange={(value) => this.handleDateChange("birthDatePicked", value)}
115119
/>
116120
</Card>
117-
{ this.state.retireDateYear &&
121+
{ retireDateYear &&
118122
<Card>
119123
<H4>Retirement Age</H4>
120124
<p>Your Full Retirement Age (FRA) to collect Social Security
121125
Benefits is {this.state.retireAge} years old, which is in
122-
year {this.state.retireDateYear}.</p>
126+
year {retireDateYear}.</p>
123127
</Card>
124128
}
125129
</div>
126130
)
127131
}
128132
}
133+
134+
export default function Prescreen1aWrapper() {
135+
const userStateActions = useUserStateActions()
136+
const userState = useUserState()
137+
return (
138+
<Prescreen1a userState={userState} userStateActions={userStateActions} />
139+
)
140+
}

0 commit comments

Comments
 (0)