diff --git a/package.json b/package.json index eaf88a3..0dbfbc2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reality-check", - "version": "1.0.1", + "version": "1.1.0", "private": true, "dependencies": { "@craco/craco": "^6.1.2", diff --git a/src/App.tsx b/src/App.tsx index dbcfe46..efdd148 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,15 +26,19 @@ function App(): JSX.Element {
{!submitted && ( <> -
-

- Sometimes you need a reality check when it comes to your - earnings. -

-

Spoiler alert: You'll be fine

-
- +

+ All data sourced{' '} + + here + +

)} {submitted && jobInfo && } diff --git a/src/chevron-down-solid.svg b/src/chevron-down-solid.svg new file mode 100644 index 0000000..026b7c0 --- /dev/null +++ b/src/chevron-down-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index c3c0332..62c3649 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -4,7 +4,6 @@ export default function Header(): JSX.Element { return (

Reality Check

-

Chances are you're doing fine

); } diff --git a/src/components/salary check form/SalaryCheckForm.test.tsx b/src/components/salary check form/SalaryCheckForm.test.tsx index e23ab5c..f247f3e 100644 --- a/src/components/salary check form/SalaryCheckForm.test.tsx +++ b/src/components/salary check form/SalaryCheckForm.test.tsx @@ -1,5 +1,10 @@ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { + render, + screen, + fireEvent, + getAllByRole, +} from '@testing-library/react'; import SalaryCheckForm from './SalaryCheckForm'; const mockSubmit = jest.fn(({ data: JobInformation }) => @@ -12,6 +17,15 @@ describe('SalaryCheckForm', () => { }); it('should display required error when no salary entered', async () => { + fireEvent.change( + await screen.getByRole('combobox', { name: 'Select an age range' }), + { + target: { + value: 'all', + }, + } + ); + fireEvent.submit(screen.getByRole('button')); expect(await screen.findAllByRole('alert')).toHaveLength(1); @@ -21,15 +35,42 @@ describe('SalaryCheckForm', () => { expect(mockSubmit).not.toBeCalled(); }); + it('should display required error when no age range entered', async () => { + fireEvent.input( + await screen.getByRole('textbox', { name: 'Enter Salary (Gross)' }), + { + target: { + value: '1000', + }, + } + ); + + fireEvent.submit(screen.getByRole('button')); + expect(await screen.findAllByRole('alert')).toHaveLength(1); + expect(await screen.findByRole('alert')).toHaveTextContent( + 'Must select an age range' + ); + expect(mockSubmit).not.toBeCalled(); + }); + it('should display positive error when a negative salary is entered', async () => { fireEvent.input( - await screen.getByRole('textbox', { name: 'Enter Salary' }), + await screen.getByRole('textbox', { name: 'Enter Salary (Gross)' }), { target: { value: '-1000', }, } ); + fireEvent.change( + await screen.getByRole('combobox', { name: 'Select an age range' }), + { + target: { + value: 'all', + }, + } + ); + fireEvent.submit(screen.getByRole('button')); expect(await screen.findAllByRole('alert')).toHaveLength(1); expect(await screen.findByRole('alert')).toHaveTextContent( diff --git a/src/components/salary check form/SalaryCheckForm.tsx b/src/components/salary check form/SalaryCheckForm.tsx index f6e24dd..52c02b3 100644 --- a/src/components/salary check form/SalaryCheckForm.tsx +++ b/src/components/salary check form/SalaryCheckForm.tsx @@ -1,12 +1,13 @@ /* eslint-disable react/jsx-props-no-spreading */ import React from 'react'; -import { useForm } from 'react-hook-form'; +import { useForm, FormProvider } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; import ValidationMessage from './validation message/ValidationMessage'; import JobInformation from '../../types/JobInformation'; import { ReactComponent as PoundSign } from '../../solid_pound_sign.svg'; import SubmitButton from '../shared/SubmitButton'; +import Select from '../shared/Select'; type FormProps = { onSubmit: (data: JobInformation) => void; @@ -18,79 +19,99 @@ const schema = yup.object().shape({ .transform((x) => (Number.isNaN(x) ? undefined : x)) .positive('Must provide a positive salary') .required('Must provide a salary'), + ageRange: yup.string().required('Must select an age range'), }); export default function SalaryCheckForm({ onSubmit }: FormProps): JSX.Element { + const formMethods = useForm({ + resolver: yupResolver(schema), + }); const { register, handleSubmit, - getValues, formState: { errors }, - } = useForm({ resolver: yupResolver(schema) }); + } = formMethods; const submitHandler = handleSubmit((data) => onSubmit(data)); return ( -
-
- - {errors.salary && ( - {errors.salary.message} - )} -
-
-
- -
- - + + + ); } diff --git a/src/components/salary results/SalaryResults.tsx b/src/components/salary results/SalaryResults.tsx index cd216c6..fc893cf 100644 --- a/src/components/salary results/SalaryResults.tsx +++ b/src/components/salary results/SalaryResults.tsx @@ -10,8 +10,19 @@ type ResultProps = { }; export default function SalaryResults({ jobInfo }: ResultProps): JSX.Element { - const salaryData = - jobInfo.type === 'FullTime' ? ukJobData.fullTime[0] : ukJobData.partTime[0]; + const ageRanges = + jobInfo.type === 'FullTime' + ? ukJobData.fullTime[0].ageRanges + : ukJobData.partTime[0].ageRanges; + + const salaryData = ageRanges.find( + (x) => x.range === jobInfo.ageRange + )?.salaryData; + + if (salaryData === undefined) { + return <>; + } + return (
diff --git a/src/components/salary results/median text/MedianText.tsx b/src/components/salary results/median text/MedianText.tsx index 5c2e4b8..1fa8f63 100644 --- a/src/components/salary results/median text/MedianText.tsx +++ b/src/components/salary results/median text/MedianText.tsx @@ -31,7 +31,7 @@ export default function MedianText({ return (

- Your salary is {resultText} the national median of  + Your salary is {resultText} the median of  {Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', diff --git a/src/components/shared/Select.tsx b/src/components/shared/Select.tsx new file mode 100644 index 0000000..2ff6733 --- /dev/null +++ b/src/components/shared/Select.tsx @@ -0,0 +1,34 @@ +/* eslint-disable react/jsx-props-no-spreading */ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { ReactComponent as Chevron } from '../../chevron-down-solid.svg'; + +type SelectProps = { + id: string; + name: string; + children: React.ReactNode; +}; + +export default function Select({ + id, + name, + children, +}: SelectProps): JSX.Element { + const { register } = useFormContext(); + const formattedName = name.replace(/([A-Z])/g, ' $1').toLowerCase(); + return ( +

+ + + +
+ ); +} diff --git a/src/data/UK salaries.json b/src/data/UK salaries.json index d69e5cc..a72a285 100644 --- a/src/data/UK salaries.json +++ b/src/data/UK salaries.json @@ -2,36 +2,266 @@ "fullTime": [ { "location": "United Kingdom", - "medianSalary": 31461, - "percentiles": [ - { "percentile": "10th", "value": 18001 }, - { "percentile": "20th", "value": 21155 }, - { "percentile": "25th", "value": 22745 }, - { "percentile": "30th", "value": 24344 }, - { "percentile": "40th", "value": 27717 }, - { "percentile": "60th", "value": 35740 }, - { "percentile": "70th", "value": 40922 }, - { "percentile": "75th", "value": 44119 }, - { "percentile": "80th", "value": 47928 }, - { "percentile": "90th", "value": 62589 } + "ageRanges": [ + { + "range": "all", + "salaryData": { + "medianSalary": 31461, + "percentiles": [ + { "percentile": "10th", "value": 18001 }, + { "percentile": "20th", "value": 21155 }, + { "percentile": "25th", "value": 22745 }, + { "percentile": "30th", "value": 24344 }, + { "percentile": "40th", "value": 27717 }, + { "percentile": "60th", "value": 35740 }, + { "percentile": "70th", "value": 40922 }, + { "percentile": "75th", "value": 44119 }, + { "percentile": "80th", "value": 47928 }, + { "percentile": "90th", "value": 62589 } + ] + } + }, + { + "range": "18-21", + "salaryData": { + "medianSalary": 18087, + "percentiles": [ + { "percentile": "10th", "value": 10906 }, + { "percentile": "20th", "value": 13345 }, + { "percentile": "25th", "value": 14403 }, + { "percentile": "30th", "value": 15150 }, + { "percentile": "40th", "value": 16815 }, + { "percentile": "60th", "value": 19394 }, + { "percentile": "70th", "value": 20426 }, + { "percentile": "75th", "value": 21426 }, + { "percentile": "80th", "value": 22456 }, + { "percentile": "90th", "value": 26208 } + ] + } + }, + { + "range": "22-29", + "salaryData": { + "medianSalary": 26096, + "percentiles": [ + { "percentile": "10th", "value": 17071 }, + { "percentile": "20th", "value": 19348 }, + { "percentile": "25th", "value": 20471 }, + { "percentile": "30th", "value": 21503 }, + { "percentile": "40th", "value": 23774 }, + { "percentile": "60th", "value": 28624 }, + { "percentile": "70th", "value": 31536 }, + { "percentile": "75th", "value": 33185 }, + { "percentile": "80th", "value": 35481 }, + { "percentile": "90th", "value": 43094 } + ] + } + }, + { + "range": "30-39", + "salaryData": { + "medianSalary": 32965, + "percentiles": [ + { "percentile": "10th", "value": 18981 }, + { "percentile": "20th", "value": 22563 }, + { "percentile": "25th", "value": 24274 }, + { "percentile": "30th", "value": 25957 }, + { "percentile": "40th", "value": 29363 }, + { "percentile": "60th", "value": 36832 }, + { "percentile": "70th", "value": 41404 }, + { "percentile": "75th", "value": 44398 }, + { "percentile": "80th", "value": 47662 }, + { "percentile": "90th", "value": 61058 } + ] + } + }, + { + "range": "40-49", + "salaryData": { + "medianSalary": 35904, + "percentiles": [ + { "percentile": "10th", "value": 19249 }, + { "percentile": "20th", "value": 23149 }, + { "percentile": "25th", "value": 25026 }, + { "percentile": "30th", "value": 27123 }, + { "percentile": "40th", "value": 31445 }, + { "percentile": "60th", "value": 40649 }, + { "percentile": "70th", "value": 46042 }, + { "percentile": "75th", "value": 49991 }, + { "percentile": "80th", "value": 54544 }, + { "percentile": "90th", "value": 73236 } + ] + } + }, + { + "range": "50-59", + "salaryData": { + "medianSalary": 33231, + "percentiles": [ + { "percentile": "10th", "value": 18520 }, + { "percentile": "20th", "value": 21848 }, + { "percentile": "25th", "value": 23646 }, + { "percentile": "30th", "value": 25402 }, + { "percentile": "40th", "value": 29120 }, + { "percentile": "60th", "value": 38334 }, + { "percentile": "70th", "value": 43989 }, + { "percentile": "75th", "value": 47327 }, + { "percentile": "80th", "value": 51710 }, + { "percentile": "90th", "value": 68901 } + ] + } + }, + { + "range": "60+", + "salaryData": { + "medianSalary": 28854, + "percentiles": [ + { "percentile": "10th", "value": 17505 }, + { "percentile": "20th", "value": 20048 }, + { "percentile": "25th", "value": 21334 }, + { "percentile": "30th", "value": 22745 }, + { "percentile": "40th", "value": 25464 }, + { "percentile": "60th", "value": 32885 }, + { "percentile": "70th", "value": 37704 }, + { "percentile": "75th", "value": 40622 }, + { "percentile": "80th", "value": 44583 }, + { "percentile": "90th", "value": 59691 } + ] + } + } ] } ], "partTime": [ { "location": "United Kingdom", - "medianSalary": 11234, - "percentiles": [ - { "percentile": "10th", "value": 3764 }, - { "percentile": "20th", "value": 6389 }, - { "percentile": "25th", "value": 7255 }, - { "percentile": "30th", "value": 8161 }, - { "percentile": "40th", "value": 9600 }, - { "percentile": "60th", "value": 12885 }, - { "percentile": "70th", "value": 15230 }, - { "percentile": "75th", "value": 16770 }, - { "percentile": "80th", "value": 18750 }, - { "percentile": "90th", "value": 25910 } + "ageRanges": [ + { + "range": "all", + "salaryData": { + "medianSalary": 11234, + "percentiles": [ + { "percentile": "10th", "value": 3764 }, + { "percentile": "20th", "value": 6389 }, + { "percentile": "25th", "value": 7255 }, + { "percentile": "30th", "value": 8161 }, + { "percentile": "40th", "value": 9600 }, + { "percentile": "60th", "value": 12885 }, + { "percentile": "70th", "value": 15230 }, + { "percentile": "75th", "value": 16770 }, + { "percentile": "80th", "value": 18750 }, + { "percentile": "90th", "value": 25910 } + ] + } + }, + { + "range": "18-21", + "salaryData": { + "medianSalary": 6514, + "percentiles": [ + { "percentile": "10th", "value": 1982 }, + { "percentile": "20th", "value": 3201 }, + { "percentile": "25th", "value": 3713 }, + { "percentile": "30th", "value": 4287 }, + { "percentile": "40th", "value": 5425 }, + { "percentile": "60th", "value": 7544 }, + { "percentile": "70th", "value": 8675 }, + { "percentile": "75th", "value": 9569 }, + { "percentile": "80th", "value": 10567 }, + { "percentile": "90th", "value": 13229 } + ] + } + }, + { + "range": "22-29", + "salaryData": { + "medianSalary": 10507, + "percentiles": [ + { "percentile": "10th", "value": 3365 }, + { "percentile": "20th", "value": 5930 }, + { "percentile": "25th", "value": 6831 }, + { "percentile": "30th", "value": 7491 }, + { "percentile": "40th", "value": 8905 }, + { "percentile": "60th", "value": 12021 }, + { "percentile": "70th", "value": 13869 }, + { "percentile": "75th", "value": 14825 }, + { "percentile": "80th", "value": 16118 }, + { "percentile": "90th", "value": 20549 } + ] + } + }, + { + "range": "30-39", + "salaryData": { + "medianSalary": 12167, + "percentiles": [ + { "percentile": "10th", "value": 5127 }, + { "percentile": "20th", "value": 7396 }, + { "percentile": "25th", "value": 8354 }, + { "percentile": "30th", "value": 8949 }, + { "percentile": "40th", "value": 10615 }, + { "percentile": "60th", "value": 14182 }, + { "percentile": "70th", "value": 16898 }, + { "percentile": "75th", "value": 18720 }, + { "percentile": "80th", "value": 20945 }, + { "percentile": "90th", "value": 28210 } + ] + } + }, + { + "range": "40-49", + "salaryData": { + "medianSalary": 12284, + "percentiles": [ + { "percentile": "10th", "value": 4536 }, + { "percentile": "20th", "value": 7208 }, + { "percentile": "25th", "value": 8320 }, + { "percentile": "30th", "value": 8837 }, + { "percentile": "40th", "value": 10589 }, + { "percentile": "60th", "value": 14082 }, + { "percentile": "70th", "value": 16819 }, + { "percentile": "75th", "value": 18890 }, + { "percentile": "80th", "value": 21815 }, + { "percentile": "90th", "value": 29110 } + ] + } + }, + { + "range": "50-59", + "salaryData": { + "medianSalary": 11914, + "percentiles": [ + { "percentile": "10th", "value": 4417 }, + { "percentile": "20th", "value": 7129 }, + { "percentile": "25th", "value": 8109 }, + { "percentile": "30th", "value": 8640 }, + { "percentile": "40th", "value": 10262 }, + { "percentile": "60th", "value": 13499 }, + { "percentile": "70th", "value": 15783 }, + { "percentile": "75th", "value": 17352 }, + { "percentile": "80th", "value": 19229 }, + { "percentile": "90th", "value": 26292 } + ] + } + }, + { + "range": "60+", + "salaryData": { + "medianSalary": 10505, + "percentiles": [ + { "percentile": "10th", "value": 3569 }, + { "percentile": "20th", "value": 5929 }, + { "percentile": "25th", "value": 6807 }, + { "percentile": "30th", "value": 7615 }, + { "percentile": "40th", "value": 9000 }, + { "percentile": "60th", "value": 12250 }, + { "percentile": "70th", "value": 14379 }, + { "percentile": "75th", "value": 15885 }, + { "percentile": "80th", "value": 17633 }, + { "percentile": "90th", "value": 24104 } + ] + } + } ] } ] diff --git a/src/types/JobInformation.ts b/src/types/JobInformation.ts index 6ac24d9..3f8216d 100644 --- a/src/types/JobInformation.ts +++ b/src/types/JobInformation.ts @@ -1,4 +1,5 @@ export default interface JobInformation { salary: number; type: 'FullTime' | 'PartTime'; + ageRange: 'all' | '18-21' | '22-30' | '31-39' | '40-49' | '50-59' | '60+'; } diff --git a/tailwind.config.js b/tailwind.config.js index d68e6b3..3f8393f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -16,7 +16,7 @@ module.exports = { '1/6': '16.66%', }, gridTemplateRows: { - header: '12% auto 50px', + header: '10% auto 50px', }, fontFamily: { sans: ['Raleway', ...defaultTheme.fontFamily.sans],