├── __mocks__ ├── fileMock.js ├── lottie-web.js └── gatsby.js ├── config ├── loaderShim.js ├── jest-preprocess.js ├── jest.setup.js └── helpers.js ├── src ├── images │ ├── favicon.png │ ├── socialBanner1200x628.png │ ├── star_solid.svg │ ├── plus.svg │ ├── arrow.svg │ ├── caret-left.svg │ └── star.svg ├── pages │ ├── 404.js │ ├── setup.js │ ├── index.js │ ├── success.js │ └── about.js ├── components │ ├── shared │ │ ├── hooks │ │ │ ├── useBackgroundColorUpdater.js │ │ │ ├── useQuestionnaire.js │ │ │ ├── usePopup.js │ │ │ ├── useOuterClick.js │ │ │ ├── useForm.js │ │ │ └── useCheckbox.js │ │ ├── styles.js │ │ ├── providers │ │ │ └── questionnaire │ │ │ │ ├── index.js │ │ │ │ └── reducers.js │ │ ├── transition.js │ │ ├── animations │ │ │ └── rocket-link.js │ │ ├── form-components │ │ │ ├── checkbox.js │ │ │ └── index.js │ │ ├── layout.js │ │ ├── modal.js │ │ ├── layout.css │ │ ├── header.js │ │ └── shared-styles.js │ ├── questionnaire │ │ ├── __tests__ │ │ │ ├── index.test.js │ │ │ └── steps │ │ │ │ ├── health_plan.test.js │ │ │ │ ├── bonus_moonshot.test.js │ │ │ │ ├── bonus_community.test.js │ │ │ │ ├── occupation_plan.test.js │ │ │ │ ├── interests_plan.test.js │ │ │ │ ├── relationships_plan.test.js │ │ │ │ ├── about.test.js │ │ │ │ ├── health.test.js │ │ │ │ ├── occupation.test.js │ │ │ │ ├── relationships.test.js │ │ │ │ ├── interests.tests.js │ │ │ │ └── final.test.js │ │ ├── steps │ │ │ ├── health │ │ │ │ ├── index.js │ │ │ │ └── health-plan.js │ │ │ ├── relationships │ │ │ │ ├── index.js │ │ │ │ └── relationships-plan.js │ │ │ ├── hobbies │ │ │ │ ├── personal-interests-plan.js │ │ │ │ └── index.js │ │ │ ├── shared │ │ │ │ ├── intro.js │ │ │ │ ├── index.js │ │ │ │ └── add-new-checkbox-item.js │ │ │ ├── moonshot │ │ │ │ └── index.js │ │ │ ├── community │ │ │ │ └── index.js │ │ │ ├── occupation │ │ │ │ ├── occupation-plan.js │ │ │ │ ├── reducers.js │ │ │ │ └── index.js │ │ │ ├── about │ │ │ │ └── index.js │ │ │ └── final │ │ │ │ └── index.js │ │ ├── questionnaire-steps.js │ │ └── index.js │ └── image.js ├── models │ └── questionnaire.js └── utils.js ├── .prettierrc ├── .babelrc ├── gatsby-node.js ├── README.md ├── jest.config.js ├── LICENSE ├── .gitignore ├── gatsby-config.js └── package.json /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub' 2 | -------------------------------------------------------------------------------- /config/loaderShim.js: -------------------------------------------------------------------------------- 1 | global.___loader = { 2 | enqueue: jest.fn(), 3 | } 4 | -------------------------------------------------------------------------------- /src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timc1/time-capsule/HEAD/src/images/favicon.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /src/images/socialBanner1200x628.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timc1/time-capsule/HEAD/src/images/socialBanner1200x628.png -------------------------------------------------------------------------------- /__mocks__/lottie-web.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | 3 | module.exports = { 4 | loadAnimation: jest.fn(), 5 | destroy: jest.fn(), 6 | } 7 | -------------------------------------------------------------------------------- /config/jest-preprocess.js: -------------------------------------------------------------------------------- 1 | const babelOptions = { 2 | presets: ['babel-preset-gatsby'], 3 | } 4 | 5 | module.exports = require('babel-jest').createTransformer(babelOptions) 6 | -------------------------------------------------------------------------------- /config/jest.setup.js: -------------------------------------------------------------------------------- 1 | // add some helpful assertions 2 | import 'jest-dom/extend-expect' 3 | 4 | // this is basically: afterEach(cleanup) 5 | import 'react-testing-library/cleanup-after-each' 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [ 3 | [ 4 | "babel-preset-gatsby", 5 | { 6 | targets: { 7 | browsers: [">0.25%", "not dead"], 8 | }, 9 | }, 10 | ], 11 | ], 12 | plugins: [ 13 | '@babel/plugin-proposal-optional-chaining', 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Layout from '../components/shared/layout' 3 | 4 | const NotFoundPage = () => ( 5 | 6 |

NOT FOUND

7 |

You just hit a route that doesn't exist... the sadness.

8 |
9 | ) 10 | 11 | export default NotFoundPage 12 | -------------------------------------------------------------------------------- /src/components/shared/hooks/useBackgroundColorUpdater.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | export default (location = {}) => { 4 | useEffect( 5 | () => { 6 | if (location.pathname) 7 | document.body.setAttribute('data-url', location.pathname) 8 | }, 9 | [location.pathname] 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => { 2 | if (stage === 'build-html') { 3 | actions.setWebpackConfig({ 4 | module: { 5 | rules: [ 6 | { 7 | test: /animateplus/, 8 | use: loaders.null(), 9 | }, 10 | ], 11 | }, 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__mocks__/gatsby.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const gatsby = jest.requireActual('gatsby') 3 | 4 | module.exports = { 5 | ...gatsby, 6 | graphql: jest.fn(), 7 | Link: jest.fn().mockImplementation(({ to, ...rest }) => 8 | React.createElement('a', { 9 | ...rest, 10 | href: to, 11 | }) 12 | ), 13 | navigate: jest.fn(), 14 | StaticQuery: jest.fn(), 15 | } 16 | -------------------------------------------------------------------------------- /src/components/shared/styles.js: -------------------------------------------------------------------------------- 1 | export { 2 | screensm, 3 | screenmd, 4 | screenlg, 5 | slideInFromLeft, 6 | fadeIn, 7 | fadeInUp, 8 | zoomIn, 9 | scroll, 10 | verticalScroll, 11 | scaleIn, 12 | UnstyledLink, 13 | UnstyledButton, 14 | AnimatedButton, 15 | ExitButton, 16 | ExitIcon, 17 | Loader, 18 | } from './shared-styles' 19 | 20 | export { RocketLink } from './animations/rocket-link' 21 | -------------------------------------------------------------------------------- /src/components/shared/hooks/useQuestionnaire.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { QuestionnaireContext } from '../providers/questionnaire/index' 3 | 4 | export default () => { 5 | const { questionnaireState, questionnaireDispatch } = useContext( 6 | QuestionnaireContext 7 | ) 8 | 9 | return { 10 | context: { 11 | questionnaireState, 12 | questionnaireDispatch, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/images/star_solid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | This Next Year 🥳 3 |
4 | (www.thisnextyear.com) 5 |
6 |

7 |

An animated, fun, and accessible web app to send your future (365 days) self a letter.

8 | 9 | ## About 10 | 11 | With the New Year coming up, write your future self a letter on your goals, interests, and how you plan to accomplish things you'd like to get done. 12 | 13 | 365 days from today, you'll receive your own message in your inbox! 14 | 15 | Hope you accomplish your goals! 16 | 17 | ## 🚀 Set up 18 | 19 | ``` 20 | npm install 21 | ``` 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ``` 28 | npm run test 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.jsx?$': '/config/jest-preprocess.js', 4 | }, 5 | moduleNameMapper: { 6 | '.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy', 7 | '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 8 | '/__mocks__/fileMock.js', 9 | }, 10 | testPathIgnorePatterns: ['node_modules', '.cache'], 11 | transformIgnorePatterns: ['node_modules/(?!(gatsby)/)'], 12 | globals: { 13 | __PATH_PREFIX__: '', 14 | }, 15 | testURL: 'http://localhost', 16 | setupFiles: ['/config/loadershim.js', '/config/helpers.js'], 17 | setupTestFrameworkScriptFile: require.resolve('./config/jest.setup.js'), 18 | } 19 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-testing-library' 3 | 4 | describe(`Questionnaire Wizard Index`, () => { 5 | test(`Should render without errors.`, () => { 6 | jest.doMock(`../../shared/hooks/useQuestionnaire`, () => { 7 | return () => window.getQuestionnaireContext('OCCUPATION_INFO') 8 | }) 9 | 10 | // Component 11 | const Index = require(`../index`).default 12 | 13 | const { getByTestId } = render() 14 | // Initial Render - Base components should all be present. 15 | expect(getByTestId(`section-title`)).toBeInTheDocument() 16 | expect(getByTestId(`section-question`)).toBeInTheDocument() 17 | expect(getByTestId(`next-button`)).toHaveAttribute(`disabled`) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/images/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/shared/providers/questionnaire/index.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react' 2 | import { getQuestionnaire } from '../../../../models/questionnaire' 3 | import { questionnaireReducer } from './reducers' 4 | 5 | const QuestionnaireContext = React.createContext() 6 | 7 | const QuestionnaireProvider = React.memo(({ children }) => { 8 | const [questionnaireState, questionnaireDispatch] = useReducer( 9 | questionnaireReducer, 10 | initialQuestionnaireState 11 | ) 12 | 13 | return ( 14 | 20 | {children} 21 | 22 | ) 23 | }) 24 | 25 | const initialQuestionnaireState = getQuestionnaire() 26 | 27 | export { QuestionnaireContext, QuestionnaireProvider } 28 | -------------------------------------------------------------------------------- /src/images/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /config/helpers.js: -------------------------------------------------------------------------------- 1 | // Setup a root
2 | ;(function setupRootDiv() { 3 | // Our modal blurs the root app div, so we need to mock it so jest 4 | // doesn't break. 5 | const appRoot = global.document.createElement('div') 6 | 7 | appRoot.setAttribute('id', '___gatsby') 8 | const body = global.document.querySelector('body') 9 | body.appendChild(appRoot) 10 | 11 | // Mock scrollTo. 12 | window.scrollTo = jest.fn() 13 | })() 14 | 15 | // TODO: Test context.questionnaireDispatch so it's not just an empty jest mock function. 😳 16 | const getQuestionnaire = require('../src/models/questionnaire').getQuestionnaire 17 | window.getQuestionnaireContext = currentStepId => ({ 18 | context: { 19 | questionnaireState: { 20 | ...getQuestionnaire(), 21 | meta: { 22 | currentStepId, 23 | }, 24 | }, 25 | questionnaireDispatch: jest.fn(), 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/health/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 3 | import { Section, Checkboxes } from '../shared/index' 4 | 5 | export default React.memo(({ canContinue, setContinue, dispatchModal }) => { 6 | const { context } = useQuestionnaire() 7 | 8 | return ( 9 |
10 | { 13 | if (!canContinue) setContinue(true) 14 | context.questionnaireDispatch({ 15 | type: 'UPDATE_ANSWERS', 16 | payload: { 17 | id: 'personalHealth', 18 | value, 19 | }, 20 | }) 21 | }} 22 | onError={error => { 23 | if (canContinue) setContinue(false) 24 | }} 25 | limit={1} 26 | /> 27 |
28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/relationships/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Section, Checkboxes } from '../shared/index' 3 | 4 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 5 | 6 | export default React.memo(({ canContinue, setContinue }) => { 7 | const { context } = useQuestionnaire() 8 | return ( 9 |
10 | { 13 | if (!canContinue) setContinue(true) 14 | 15 | context.questionnaireDispatch({ 16 | type: 'UPDATE_ANSWERS', 17 | payload: { 18 | id: 'currentRelationships', 19 | value, 20 | }, 21 | }) 22 | }} 23 | onError={error => { 24 | if (canContinue) setContinue(false) 25 | }} 26 | limit={1} 27 | /> 28 |
29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/health/health-plan.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DebouncedInput } from '../shared/index' 3 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 4 | 5 | export default React.memo(({ canContinue, setContinue }) => { 6 | const { context } = useQuestionnaire() 7 | return ( 8 | { 13 | if (!canContinue) setContinue(true) 14 | context.questionnaireDispatch({ 15 | type: 'UPDATE_ANSWERS', 16 | payload: { 17 | id: 'personalHealthPlan', 18 | value: values, 19 | }, 20 | }) 21 | }} 22 | onError={error => { 23 | if (canContinue) setContinue(false) 24 | }} 25 | placeholder="I will run 365 miles this upcoming year." 26 | maxLength="500" 27 | /> 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/hobbies/personal-interests-plan.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DebouncedInput } from '../shared/index' 3 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 4 | 5 | export default React.memo(({ canContinue, setContinue }) => { 6 | const { context } = useQuestionnaire() 7 | return ( 8 | { 13 | if (!canContinue) setContinue(true) 14 | context.questionnaireDispatch({ 15 | type: 'UPDATE_ANSWERS', 16 | payload: { 17 | id: 'hobbiesPlan', 18 | value: values, 19 | }, 20 | }) 21 | }} 22 | onError={error => { 23 | if (canContinue) setContinue(false) 24 | }} 25 | placeholder="I plan on attending a kickboxing class twice a week." 26 | maxLength="500" 27 | /> 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/shared/intro.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | import styled from '@emotion/styled' 3 | import lottie from 'lottie-web' 4 | 5 | const Intro = React.memo( 6 | ({ canContinue, setContinue, title, illustration }) => { 7 | const illustrationRef = useRef() 8 | const animationRef = useRef() 9 | 10 | useEffect(() => { 11 | if (!canContinue) setContinue(true) 12 | 13 | if (illustration) { 14 | animationRef.current = lottie.loadAnimation({ 15 | container: illustrationRef.current, 16 | renderer: 'svg', 17 | loop: true, 18 | autoplay: true, 19 | animationData: illustration, 20 | }) 21 | } 22 | return () => { 23 | if (animationRef.current) { 24 | animationRef.current.destroy() 25 | } 26 | } 27 | }, []) 28 | 29 | return 30 | } 31 | ) 32 | 33 | export default Intro 34 | 35 | const Container = styled.div` 36 | margin-top: -2rem; 37 | ` 38 | -------------------------------------------------------------------------------- /src/images/caret-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | caret-symbol (1) 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/relationships/relationships-plan.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DebouncedInput } from '../shared/index' 3 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 4 | 5 | export default React.memo(({ canContinue, setContinue }) => { 6 | const { context } = useQuestionnaire() 7 | return ( 8 | { 13 | if (!canContinue) setContinue(true) 14 | context.questionnaireDispatch({ 15 | type: 'UPDATE_ANSWERS', 16 | payload: { 17 | id: 'relationshipsPlan', 18 | value: values, 19 | }, 20 | }) 21 | }} 22 | onError={error => { 23 | if (canContinue) setContinue(false) 24 | }} 25 | placeholder="I am going to reach out and try to meet up with someone I look up to once a month." 26 | maxLength="500" 27 | /> 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/pages/setup.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SetupIndex from '../components/questionnaire/index' 3 | import { QuestionnaireProvider } from '../components/shared/providers/questionnaire/index' 4 | import Helmet from 'react-helmet' 5 | 6 | export default () => { 7 | return ( 8 | <> 9 | 23 |

Setup - This Next Year

24 |

25 | Get Started - write a letter to your future self, take action, receive 26 | it 365 days from today. 27 |

28 | 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/health_plan.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Health Plan', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('HEALTH_PLAN') 7 | }) 8 | 9 | const HealthPlan = require('../../index').default 10 | 11 | test('Should render a single textarea and a disabled next button.', async () => { 12 | const { getByTestId } = render() 13 | 14 | await wait(async () => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const textarea = getByTestId('textarea') 18 | expect(textarea).toBeInTheDocument() 19 | expect(nextButton).toHaveAttribute('disabled') 20 | 21 | fireEvent.change(textarea, { 22 | target: { 23 | value: 'This is a test! 🤓', 24 | }, 25 | }) 26 | await wait(() => { 27 | expect(nextButton).not.toHaveAttribute('disabled') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/bonus_moonshot.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Bonus Moonshot', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('BONUS_MOONSHOT') 7 | }) 8 | 9 | const Moonshot = require('../../index').default 10 | 11 | test('Should render a single textarea and a disabled next button.', async () => { 12 | const { getByTestId } = render() 13 | 14 | await wait(async () => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const textarea = getByTestId('textarea') 18 | expect(textarea).toBeInTheDocument() 19 | expect(nextButton).toHaveAttribute('disabled') 20 | 21 | fireEvent.change(textarea, { 22 | target: { 23 | value: 'This is a test! 🤓', 24 | }, 25 | }) 26 | await wait(() => { 27 | expect(nextButton).not.toHaveAttribute('disabled') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/bonus_community.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Bonus Community', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('BONUS_COMMUNITY') 7 | }) 8 | 9 | const Community = require('../../index').default 10 | 11 | test('Should render a single textarea and a disabled next button.', async () => { 12 | const { getByTestId } = render() 13 | 14 | await wait(async () => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const textarea = getByTestId('textarea') 18 | expect(textarea).toBeInTheDocument() 19 | expect(nextButton).toHaveAttribute('disabled') 20 | 21 | fireEvent.change(textarea, { 22 | target: { 23 | value: 'This is a test! 🤓', 24 | }, 25 | }) 26 | await wait(() => { 27 | expect(nextButton).not.toHaveAttribute('disabled') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/occupation_plan.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Occupation Plan', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('OCCUPATION_PLAN') 7 | }) 8 | 9 | const OccupationPlan = require('../../index').default 10 | 11 | test('Should render a single textarea and a disabled next button.', async () => { 12 | const { getByTestId } = render() 13 | 14 | await wait(async () => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const textarea = getByTestId('textarea') 18 | expect(textarea).toBeInTheDocument() 19 | expect(nextButton).toHaveAttribute('disabled') 20 | 21 | fireEvent.change(textarea, { 22 | target: { 23 | value: 'This is a test! 🤓', 24 | }, 25 | }) 26 | await wait(() => { 27 | expect(nextButton).not.toHaveAttribute('disabled') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/moonshot/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DebouncedInput } from '../shared/index' 3 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 4 | 5 | export default React.memo(({ canContinue, setContinue }) => { 6 | const { context } = useQuestionnaire() 7 | return ( 8 | { 13 | if (!canContinue) setContinue(true) 14 | context.questionnaireDispatch({ 15 | type: 'UPDATE_ANSWERS', 16 | payload: { 17 | id: 'moonshot', 18 | value: values, 19 | }, 20 | }) 21 | }} 22 | onError={error => { 23 | if (canContinue) setContinue(false) 24 | }} 25 | placeholder={`One of the biggest things I've been looking forward to is traveling solo across Asia. My plan is to go in the Fall of ${new Date().getFullYear() + 26 | 1}.`} 27 | maxLength={`500`} 28 | /> 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/interests_plan.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Interests Plan', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('PERSONAL_INTERESTS_PLAN') 7 | }) 8 | 9 | const InterestsPlan = require('../../index').default 10 | 11 | test('Should render a single textarea and a disabled next button.', async () => { 12 | const { getByTestId } = render() 13 | 14 | await wait(async () => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const textarea = getByTestId('textarea') 18 | expect(textarea).toBeInTheDocument() 19 | expect(nextButton).toHaveAttribute('disabled') 20 | 21 | fireEvent.change(textarea, { 22 | target: { 23 | value: 'This is a test! 🤓', 24 | }, 25 | }) 26 | await wait(() => { 27 | expect(nextButton).not.toHaveAttribute('disabled') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/relationships_plan.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Relationships Plan', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('RELATIONSHIPS_PLAN') 7 | }) 8 | 9 | const RelationshipsPlan = require('../../index').default 10 | 11 | test('Should render a single textarea and a disabled next button.', async () => { 12 | const { getByTestId } = render() 13 | 14 | await wait(async () => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const textarea = getByTestId('textarea') 18 | expect(textarea).toBeInTheDocument() 19 | expect(nextButton).toHaveAttribute('disabled') 20 | 21 | fireEvent.change(textarea, { 22 | target: { 23 | value: 'This is a test! 🤓', 24 | }, 25 | }) 26 | await wait(() => { 27 | expect(nextButton).not.toHaveAttribute('disabled') 28 | }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/community/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DebouncedInput } from '../shared/index' 3 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 4 | 5 | export default React.memo(({ canContinue, setContinue }) => { 6 | const { context } = useQuestionnaire() 7 | return ( 8 | { 13 | if (!canContinue) setContinue(true) 14 | context.questionnaireDispatch({ 15 | type: 'UPDATE_ANSWERS', 16 | payload: { 17 | id: 'community', 18 | value: values, 19 | }, 20 | }) 21 | }} 22 | onError={error => { 23 | if (canContinue) setContinue(false) 24 | }} 25 | placeholder={`I have been creating graphic design tutorials and posting them on YouTube this past year. I've gotten great feedback and plan on being more consistent with uploading - once a week.`} 26 | maxLength="500" 27 | /> 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/image.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { StaticQuery, graphql } from 'gatsby' 3 | import Img from 'gatsby-image' 4 | 5 | /* 6 | * This component is built using `gatsby-image` to automatically serve optimized 7 | * images with lazy loading and reduced file sizes. The image is loaded using a 8 | * `StaticQuery`, which allows us to load the image from directly within this 9 | * component, rather than having to pass the image data down from pages. 10 | * 11 | * For more information, see the docs: 12 | * - `gatsby-image`: https://gatsby.app/gatsby-image 13 | * - `StaticQuery`: https://gatsby.app/staticquery 14 | */ 15 | 16 | const Image = () => ( 17 | } 30 | /> 31 | ) 32 | export default Image 33 | -------------------------------------------------------------------------------- /src/components/shared/providers/questionnaire/reducers.js: -------------------------------------------------------------------------------- 1 | import { deepClone } from '../../../../utils' 2 | 3 | const questionnaireReducer = (state, { type, payload }) => { 4 | console.log(state) 5 | switch (type) { 6 | case 'NEXT': 7 | return { 8 | ...state, 9 | meta: { 10 | currentStepId: payload.value, 11 | }, 12 | } 13 | case 'UPDATE_USER': 14 | const user = Object.assign({}, state.user, payload.user) 15 | return { 16 | ...state, 17 | user, 18 | } 19 | case 'UPDATE_ANSWERS': 20 | return { 21 | ...state, 22 | answers: { 23 | ...state.answers, 24 | [payload.id]: payload.value, 25 | }, 26 | } 27 | case 'ADD_UNIQUE_CHECKBOX_ITEM': 28 | const copy = deepClone(state.answers[payload.id]) 29 | copy.push(payload.value) 30 | 31 | return { 32 | ...state, 33 | answers: { 34 | ...state.answers, 35 | [payload.id]: copy, 36 | }, 37 | } 38 | default: 39 | return state 40 | } 41 | } 42 | 43 | export { questionnaireReducer } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 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 | 23 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/occupation/occupation-plan.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { DebouncedInput } from '../shared/index' 3 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 4 | 5 | export default React.memo(({ canContinue, setContinue }) => { 6 | const { context } = useQuestionnaire() 7 | return ( 8 | { 13 | if (!canContinue) setContinue(true) 14 | context.questionnaireDispatch({ 15 | type: 'UPDATE_ANSWERS', 16 | payload: { 17 | id: 'occupationPlan', 18 | value: values, 19 | }, 20 | }) 21 | }} 22 | onError={error => { 23 | if (canContinue) setContinue(false) 24 | }} 25 | placeholder="I've really enjoyed working with my creative director, and am interested in learning more about her field of work. So, I will help her with her work more this upcoming year and learn alongside her." 26 | maxLength="500" 27 | /> 28 | ) 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/shared/hooks/usePopup.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react' 2 | import useOuterClick from './useOuterClick' 3 | 4 | export default () => { 5 | const [isOpen, setOpen] = useState(false) 6 | const containerRef = useRef() 7 | const togglerRef = useRef() 8 | 9 | useOuterClick({ 10 | toggle: setOpen, 11 | isOpen, 12 | ref: containerRef, 13 | togglerRef, 14 | }) 15 | 16 | const getContainerProps = ({ ...props }) => ({ 17 | position: 'relative', 18 | 'aria-expanded': isOpen ? 'true' : 'false', 19 | 'aria-haspopup': 'listbox', 20 | ...props, 21 | }) 22 | 23 | const getTogglerProps = ({ ...props }) => ({ 24 | onClick: e => setOpen(!isOpen), 25 | 'aria-label': isOpen ? 'close popup' : 'open popup', 26 | ref: togglerRef, 27 | ...props, 28 | }) 29 | 30 | const getMenuProps = ({ ...props }) => ({ 31 | role: 'listbox', 32 | ref: containerRef, 33 | isOpen, 34 | ...props, 35 | }) 36 | 37 | const getItemProps = ({ ...props }) => ({ 38 | tabIndex: isOpen ? 0 : -1, 39 | role: 'option', 40 | ...props, 41 | }) 42 | 43 | return { 44 | isOpen, 45 | setOpen, 46 | getContainerProps, 47 | getTogglerProps, 48 | getMenuProps, 49 | getItemProps, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | *.swo 72 | *.swp 73 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/about.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe(`Questionnaire -- About`, () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('ABOUT') 7 | }) 8 | 9 | const About = require(`../../index`).default 10 | 11 | test(`Should render an input field for user's first name.`, async () => { 12 | const { getByTestId, getByPlaceholderText } = render() 13 | await wait(() => { 14 | expect(getByPlaceholderText(`My name`)).toBeInTheDocument() 15 | }) 16 | }) 17 | 18 | test(`Next Button should NOT be disabled after user types their name.`, async () => { 19 | const { getByTestId, getByPlaceholderText } = render() 20 | 21 | await wait(async () => { 22 | const input = getByPlaceholderText(`My name`) 23 | const nextButton = getByTestId(`next-button`) 24 | 25 | expect(nextButton).toBeInTheDocument() 26 | expect(nextButton).toHaveAttribute('disabled') 27 | 28 | fireEvent.change(input, { 29 | target: { value: `Tim` }, 30 | }) 31 | 32 | await wait(() => { 33 | expect(nextButton).not.toHaveAttribute('disabled') 34 | }) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/health.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Health', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('HEALTH') 7 | }) 8 | 9 | const Health = require('../../index').default 10 | 11 | test('Should render at first showing initial health checkboxes and a disabled next button.', async () => { 12 | const { getByTestId, getAllByTestId } = render() 13 | 14 | await wait(() => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const roleCheckboxes = getAllByTestId('checkbox-button') 18 | expect(roleCheckboxes).toHaveLength(5) 19 | expect(nextButton).toHaveAttribute('disabled') 20 | }) 21 | }) 22 | 23 | test('Should allow user to continue once an option is clicked.', async () => { 24 | const { getByTestId, getAllByTestId } = render() 25 | 26 | await wait(async () => { 27 | const nextButton = getByTestId('next-button') 28 | 29 | const roleCheckboxes = getAllByTestId('checkbox-button') 30 | 31 | fireEvent.click(roleCheckboxes[0]) 32 | 33 | await wait(() => expect(nextButton).not.toHaveAttribute('disabled')) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/occupation/reducers.js: -------------------------------------------------------------------------------- 1 | const getInitialUIState = (context = {}) => { 2 | const hasOccupation = 3 | context.questionnaireState.answers.occupationRole.filter(i => i.isChecked) 4 | .length > 0 5 | return { 6 | isCompanyTypeShowing: hasOccupation ? true : false, 7 | isHappinessShowing: hasOccupation ? true : false, 8 | } 9 | } 10 | 11 | const occupationsUIReducer = (state, { type, payload }) => { 12 | switch (type) { 13 | case 'SHOW_COMPANY_TYPE': 14 | return { 15 | ...state, 16 | isCompanyTypeShowing: true, 17 | } 18 | case 'HIDE_COMPANY_TYPE': 19 | return { 20 | ...state, 21 | isCompanyTypeShowing: false, 22 | } 23 | case 'RESET': 24 | return getInitialUIState() 25 | default: 26 | return state 27 | } 28 | } 29 | 30 | const formatMessage = ({ roles }) => { 31 | const selected = roles.filter(i => i.isChecked) 32 | let formatted = '' 33 | 34 | selected.forEach((item, index) => { 35 | if (index === selected.length - 1 && index !== 0) { 36 | formatted += ' and ' 37 | } 38 | formatted += `${item.name}` 39 | if (index !== selected.length - 1 && index !== selected.length - 2) { 40 | formatted += ', ' 41 | } 42 | }) 43 | 44 | return formatted.toLowerCase() 45 | } 46 | 47 | export { getInitialUIState, occupationsUIReducer, formatMessage } 48 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/occupation.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait, waitForElement } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Occupation', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('OCCUPATION_INFO') 7 | }) 8 | 9 | const Occupation = require('../../index').default 10 | 11 | test('Should render at first only showing roles checkboxes and a disabled next button.', async () => { 12 | const { getByTestId, getAllByTestId } = render() 13 | 14 | await wait(() => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const roleCheckboxes = getAllByTestId('checkbox-button') 18 | expect(roleCheckboxes).toHaveLength(6) 19 | expect(nextButton).toHaveAttribute('disabled') 20 | }) 21 | }) 22 | 23 | test('Should display the next row of options after user selects a job role.', async () => { 24 | const { getByTestId, getAllByTestId } = render() 25 | 26 | await wait(async () => { 27 | const nextButton = getByTestId('next-button') 28 | const roleCheckboxes = getAllByTestId('checkbox-button') 29 | 30 | fireEvent.click(roleCheckboxes[0]) 31 | 32 | const nextRowCheckboxes = await waitForElement(() => 33 | getByTestId('company-selectors') 34 | ) 35 | 36 | // Assert 37 | expect(nextRowCheckboxes).toBeInTheDocument() 38 | expect(nextButton).not.toHaveAttribute('disabled') 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/images/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 14 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/questionnaire/steps/hobbies/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useQuestionnaire from '../../../shared/hooks/useQuestionnaire' 3 | import { ClickForMoreButton, Section, Checkboxes } from '../shared/index' 4 | 5 | import AddNewCheckboxItem from '../shared/add-new-checkbox-item' 6 | 7 | export default React.memo(({ canContinue, setContinue, dispatchModal }) => { 8 | const { context } = useQuestionnaire() 9 | 10 | return ( 11 |
12 | { 15 | if (!canContinue) setContinue(true) 16 | context.questionnaireDispatch({ 17 | type: 'UPDATE_ANSWERS', 18 | payload: { 19 | id: 'hobbies', 20 | value, 21 | }, 22 | }) 23 | }} 24 | onError={error => { 25 | if (canContinue) setContinue(false) 26 | }} 27 | /> 28 | 30 | dispatchModal({ 31 | type: 'TOGGLE_MODAL_ON', 32 | payload: { 33 | modal: ( 34 | { 36 | dispatchModal({ type: 'TOGGLE_MODAL_OFF' }) 37 | }} 38 | sectionToUpdate="hobbies" 39 | title="What are some hobbies and activities that interest you?" 40 | placeholder="Marathon-ing" 41 | /> 42 | ), 43 | }, 44 | }) 45 | } 46 | > 47 | More 48 | 49 |
50 | ) 51 | }) 52 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteMetadata: { 3 | title: 'This Next Year — Send Your Future Self A Letter', 4 | description: `Write your future self a letter talking about your goals and how you plan on accomplishing them in the upcoming year. 365 days later you'll receive your letter in your email. Reflect, and hopefully you'll be a few steps closer to where you want to be!`, 5 | url: `https://thisnextyear.com`, 6 | image: `https://thisnextyear.com/images/socialBanner1200x628.png`, 7 | themeColor: `#0b1cfd`, 8 | }, 9 | plugins: [ 10 | 'gatsby-plugin-react-helmet', 11 | { 12 | resolve: `gatsby-source-filesystem`, 13 | options: { 14 | name: `images`, 15 | path: `${__dirname}/src/images`, 16 | }, 17 | }, 18 | 'gatsby-transformer-sharp', 19 | 'gatsby-plugin-sharp', 20 | { 21 | resolve: `gatsby-plugin-manifest`, 22 | options: { 23 | name: 'This Next Year', 24 | short_name: 'ThisNextYear', 25 | start_url: '/', 26 | background_color: '#ffffff', 27 | theme_color: '#0b1cfd', 28 | display: 'minimal-ui', 29 | icon: 'src/images/favicon.png', // This path is relative to the root of the site. 30 | }, 31 | }, 32 | { 33 | resolve: `gatsby-plugin-layout`, 34 | options: { 35 | component: require.resolve(`${__dirname}/src/components/shared/layout`), 36 | }, 37 | }, 38 | { 39 | resolve: `gatsby-plugin-google-analytics`, 40 | options: { 41 | trackingId: 'UA-128015210-4', 42 | head: true, 43 | }, 44 | }, 45 | // this (optional) plugin enables Progressive Web App + Offline functionality 46 | // To learn more, visit: https://gatsby.app/offline 47 | // 'gatsby-plugin-offline', 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /src/components/shared/hooks/useOuterClick.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | const isMobile = () => { 4 | if (typeof document !== `undefined`) { 5 | return 'ontouchstart' in document.documentElement === true 6 | } 7 | return false 8 | } 9 | // For touch devices, we don't want to listen to click events but rather touchstart. 10 | const clickEvent = isMobile() ? 'touchstart' : 'click' 11 | 12 | export default ({ toggle, isOpen, ref, togglerRef }) => { 13 | const clickListener = useRef() 14 | const keydownListener = useRef() 15 | 16 | // Setup event listeners, adding the container element's ref as a closure. 17 | useEffect(() => { 18 | clickListener.current = e => handleOuterClick(e, ref, toggle, togglerRef) 19 | keydownListener.current = e => handleKeydown(e, toggle) 20 | }, []) 21 | 22 | useEffect( 23 | () => { 24 | if (ref.current) { 25 | if (isOpen) { 26 | document.addEventListener(clickEvent, clickListener.current) 27 | if (!isMobile()) { 28 | document.addEventListener('keydown', keydownListener.current) 29 | } 30 | } else { 31 | document.removeEventListener(clickEvent, clickListener.current) 32 | if (!isMobile()) { 33 | document.removeEventListener('keydown', keydownListener.current) 34 | } 35 | } 36 | } 37 | return () => { 38 | document.removeEventListener(clickEvent, clickListener.current) 39 | document.removeEventListener('keydown', keydownListener.current) 40 | } 41 | }, 42 | [isOpen] 43 | ) 44 | } 45 | 46 | const handleOuterClick = (event, ref, toggle, togglerRef) => { 47 | if ( 48 | !ref.current.contains(event.target) && 49 | !togglerRef.current.contains(event.target) 50 | ) { 51 | toggle(prev => !prev) 52 | } 53 | } 54 | 55 | const handleKeydown = (event, toggle) => { 56 | if (event.key.toUpperCase() === 'ESCAPE') { 57 | toggle(prev => !prev) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "time-capsule", 3 | "description": "Write your future self a letter", 4 | "version": "1.0.0", 5 | "author": "Tim Chang ", 6 | "dependencies": { 7 | "@emotion/core": "^10.0.2", 8 | "@emotion/styled": "^10.0.2", 9 | "@reach/router": "^1.2.1", 10 | "animateplus": "^2.1.1", 11 | "gatsby": "^2.0.53", 12 | "gatsby-image": "^2.0.20", 13 | "gatsby-plugin-google-analytics": "^2.0.8", 14 | "gatsby-plugin-layout": "^1.0.10", 15 | "gatsby-plugin-manifest": "^2.0.9", 16 | "gatsby-plugin-offline": "^2.0.16", 17 | "gatsby-plugin-react-helmet": "^3.0.2", 18 | "gatsby-plugin-sharp": "^2.0.17", 19 | "gatsby-source-filesystem": "^2.0.8", 20 | "gatsby-transformer-sharp": "^2.1.8", 21 | "lottie-web": "^5.4.2", 22 | "react": "^16.7.0-alpha.2", 23 | "react-dom": "^16.7.0-alpha.2", 24 | "react-helmet": "^5.2.0", 25 | "react-testing-library": "^5.4.0", 26 | "react-transition-group": "^2.5.0" 27 | }, 28 | "keywords": [ 29 | "gatsby" 30 | ], 31 | "license": "MIT", 32 | "scripts": { 33 | "build": "gatsby build", 34 | "develop": "gatsby develop", 35 | "start": "npm run develop", 36 | "deploy": "gatsby build --prefix-paths && s3-deploy './public/**' --cwd './public/' --bucket thisnextyear.com --deleteRemoved --gzip && npm run clear", 37 | "clear": "aws-cloudfront-invalidate E1637DSVT3IWD2", 38 | "format": "prettier --write \"src/**/*.js\"", 39 | "test": "jest", 40 | "test-watch": "jest --watch" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.2.2", 44 | "@babel/plugin-proposal-optional-chaining": "^7.2.0", 45 | "babel-core": "^7.0.0-bridge.0", 46 | "babel-jest": "^23.6.0", 47 | "babel-preset-gatsby": "^0.1.6", 48 | "identity-obj-proxy": "^3.0.0", 49 | "jest": "^23.6.0", 50 | "jest-dom": "^3.0.0", 51 | "prettier": "^1.15.2", 52 | "react-test-renderer": "^16.6.3" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "https://github.com/timc1/time-capsule" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/questionnaire/__tests__/steps/relationships.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, fireEvent, wait } from 'react-testing-library' 3 | 4 | describe('Questionnaire -- Relationships', () => { 5 | jest.doMock(`../../../shared/hooks/useQuestionnaire`, () => { 6 | return () => window.getQuestionnaireContext('RELATIONSHIPS') 7 | }) 8 | 9 | const Relationships = require('../../index').default 10 | 11 | test('Should render at first showing initial relationships checkboxes and a disabled next button.', async () => { 12 | const { getByTestId, getAllByTestId } = render() 13 | 14 | await wait(() => { 15 | const nextButton = getByTestId('next-button') 16 | 17 | const roleCheckboxes = getAllByTestId('checkbox-button') 18 | expect(roleCheckboxes).toHaveLength(5) 19 | expect(nextButton).toHaveAttribute('disabled') 20 | }) 21 | }) 22 | 23 | test('Should allow users to click next when they have selected one option.', async () => { 24 | const { getByTestId, getAllByTestId } = render() 25 | 26 | await wait(async () => { 27 | const nextButton = getByTestId('next-button') 28 | 29 | const roleCheckboxes = getAllByTestId('checkbox-button') 30 | 31 | fireEvent.click(roleCheckboxes[0]) 32 | 33 | await wait(() => { 34 | expect(nextButton).not.toHaveAttribute('disabled') 35 | }) 36 | }) 37 | }) 38 | 39 | test('Should only allow users to click one checkbox at a time.', async () => { 40 | const { getByTestId, getAllByTestId } = render() 41 | 42 | await wait(() => { 43 | const roleCheckboxes = getAllByTestId('checkbox-button') 44 | 45 | // Click multiple checkboxes. 46 | fireEvent.click(roleCheckboxes[0]) 47 | fireEvent.click(roleCheckboxes[1]) 48 | fireEvent.click(roleCheckboxes[2]) 49 | 50 | // Filter out checked elements. 51 | const checked = roleCheckboxes.filter( 52 | el => el.getAttribute('data-ischecked') === 'true' 53 | ) 54 | 55 | // Assert. 56 | expect(checked).toHaveLength(1) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/components/shared/transition.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | TransitionGroup, 4 | Transition as ReactTransition, 5 | CSSTransition, 6 | } from 'react-transition-group' 7 | 8 | import useBackgroundColorUpdater from './hooks/useBackgroundColorUpdater' 9 | 10 | const getBaseStyles = ({ delay }) => ({ 11 | entering: { 12 | position: 'absolute', 13 | opacity: 0, 14 | }, 15 | entered: { 16 | transition: `${delay}ms ease-in`, 17 | opacity: 1, 18 | }, 19 | exiting: { 20 | transition: `${delay}ms ease-in`, 21 | opacity: 0, 22 | }, 23 | }) 24 | 25 | const Transition = React.memo( 26 | ({ children, location, transitionKey, type = 'default', delay = 250 }) => { 27 | useBackgroundColorUpdater(location) 28 | 29 | return ( 30 | 31 | 39 | {status => ( 40 |
45 | {children} 46 |
47 | )} 48 |
49 |
50 | ) 51 | } 52 | ) 53 | 54 | const WizardTransition = React.memo( 55 | ({ 56 | children, 57 | location, 58 | transitionKey, 59 | type = 'horizontal-left', 60 | delay = 1000, 61 | }) => { 62 | //useBackgroundColorUpdater(location) 63 | 64 | const classNames = `wizard wizard-${type}` 65 | return ( 66 |
67 | 70 | React.cloneElement(child, { 71 | classNames, 72 | timeout: { 73 | enter: delay, 74 | exit: delay, 75 | }, 76 | }) 77 | } 78 | > 79 | 85 |
{children}
86 |
87 |
88 |
89 | ) 90 | } 91 | ) 92 | 93 | export default Transition 94 | export { WizardTransition } 95 | -------------------------------------------------------------------------------- /src/components/shared/animations/rocket-link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import arrow from '../../../images/arrow.svg' 4 | 5 | import { UnstyledLink, screenmd, scroll } from '../styles' 6 | 7 | export function RocketLink({ to, text, ...props }) { 8 | return ( 9 | 10 |

{text}

11 |