From 0effaf858ff2d126ca4685ab8d739090ee167a0d Mon Sep 17 00:00:00 2001 From: mushtaque12 <48409277+mushtaque12@users.noreply.github.com> Date: Wed, 13 Jul 2022 13:41:39 +0100 Subject: [PATCH] Adding quiet hours and days in disruptions signup page (#281) * Adding quiet hours and days in disruptions signup page * Fixing label for checkbox * Updating state * Updating state * Updating aria labels * Adding functionality to strip overlapping times * Adding functionality to strip overlapping times Co-authored-by: Mushtaque Ahmed --- .env | 4 +- .env.development | 2 +- netlify.toml | 4 +- public/index.html | 20 +-- public/manifest.json | 6 +- src/assets/styles/_vars.scss | 3 +- src/components/Breadcrumb.js | 2 +- src/components/Form/Form.js | 29 ++-- .../Form/Step10SubmitConfirmation/Success.js | 8 +- .../Form/Step2SmsAlert/Step2SmsAlert.js | 22 +-- .../Form/Step6EmailAlert/Step6EmailAlert.js | 14 +- .../Form/Step9Confirm/Step9SummarySection.js | 48 +++++++ .../StepDisruptionAlert.js | 71 ++++++++++ .../Form/StepQuietHours/AddQuietDays.js | 110 +++++++++++++++ .../Form/StepQuietHours/AddQuietHours.js | 111 +++++++++++++++ .../StepQuietHours/QuietHoursComponent.js | 126 ++++++++++++++++++ .../QuietHoursComponent.module.scss | 12 ++ .../Form/StepQuietHours/StepQuietHours.js | 58 ++++++++ src/components/Form/useFormData.js | 6 + src/components/Form/useStepLogic.js | 16 ++- src/components/Form/useSubmitForm.js | 51 +++++++ src/components/HeaderAndBreadCrumb.js | 4 +- .../Checkboxes/Checkbox/Checkbox.js | 46 +++++++ .../Checkboxes/Checkbox/Checkbox.module.scss | 23 ++++ .../FormElements/Checkboxes/Checkboxes.js | 82 ++++++++++++ .../shared/FormElements/Dropdown/Dropdown.js | 83 ++++++++++++ .../Dropdown/Dropdown.module.scss | 7 + .../shared/FormElements/Radios/Radio/Radio.js | 5 +- .../shared/FormElements/Radios/Radios.js | 5 +- .../shared/HoursMinutes/HoursMinutes.js | 23 ++++ src/globalState/FormDataContext.js | 13 +- 31 files changed, 940 insertions(+), 74 deletions(-) create mode 100644 src/components/Form/StepDisruptionAlert/StepDisruptionAlert.js create mode 100644 src/components/Form/StepQuietHours/AddQuietDays.js create mode 100644 src/components/Form/StepQuietHours/AddQuietHours.js create mode 100644 src/components/Form/StepQuietHours/QuietHoursComponent.js create mode 100644 src/components/Form/StepQuietHours/QuietHoursComponent.module.scss create mode 100644 src/components/Form/StepQuietHours/StepQuietHours.js create mode 100644 src/components/shared/FormElements/Checkboxes/Checkbox/Checkbox.js create mode 100644 src/components/shared/FormElements/Checkboxes/Checkbox/Checkbox.module.scss create mode 100644 src/components/shared/FormElements/Checkboxes/Checkboxes.js create mode 100644 src/components/shared/FormElements/Dropdown/Dropdown.js create mode 100644 src/components/shared/FormElements/Dropdown/Dropdown.module.scss create mode 100644 src/components/shared/HoursMinutes/HoursMinutes.js diff --git a/.env b/.env index 8c0f329a..0f7ac73b 100644 --- a/.env +++ b/.env @@ -5,5 +5,5 @@ REACT_APP_WMNDS_VERSION = 1.7.3 # Variables that control the site title and meta descriptions -REACT_APP_TITLE = "Sign up to email alerts about disruption" -REACT_APP_DESCRIPTION= "Sign up to email alerts about disruption - West Midlands Network" \ No newline at end of file +REACT_APP_TITLE = "Sign up to alerts about disruptions" +REACT_APP_DESCRIPTION= "Sign up to alerts about disruption - Transport for West Midlands" \ No newline at end of file diff --git a/.env.development b/.env.development index e7ae49e5..0c951510 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,5 @@ # developemnt API and keys -REACT_APP_API_HOST='https://rtccdisruptions6zqwajo6s.azurewebsites.net' +REACT_APP_API_HOST='https://rtccdisruptionsbfasldoiz.azurewebsites.net' # Autocomplete API REACT_APP_AUTOCOMPLETE_API='https://wmca-api-portal-staging.azure-api.net' REACT_APP_AUTOCOMPLETE_API_KEY='0d4cca4a2c5d40c3bfbbfe45d1bbf294' diff --git a/netlify.toml b/netlify.toml index 62a52584..64a779ff 100644 --- a/netlify.toml +++ b/netlify.toml @@ -14,13 +14,13 @@ package = "@sentry/netlify-build-plugin" # Use staging for branch deploys for next-release # So that dummy data from testing does not enter live DB [context."next-release".environment] - REACT_APP_API_HOST="https://rtccdisruptions6zqwajo6s.azurewebsites.net" + REACT_APP_API_HOST="https://rtccdisruptionsbfasldoiz.azurewebsites.net" REACT_APP_AUTOCOMPLETE_API="https://wmca-api-portal-staging.azure-api.net" REACT_APP_AUTOCOMPLETE_API_KEY="0d4cca4a2c5d40c3bfbbfe45d1bbf294" REACT_APP_ROADS_AUTOCOMPLETE_KEY="e0c1216f818a41be8d528ac1d4f7ebfd" [context.deploy-preview.environment] - REACT_APP_API_HOST="https://rtccdisruptions6zqwajo6s.azurewebsites.net" + REACT_APP_API_HOST="https://rtccdisruptionsbfasldoiz.azurewebsites.net" REACT_APP_AUTOCOMPLETE_API="https://wmca-api-portal-staging.azure-api.net" REACT_APP_AUTOCOMPLETE_API_KEY="0d4cca4a2c5d40c3bfbbfe45d1bbf294" REACT_APP_ROADS_AUTOCOMPLETE_KEY="e0c1216f818a41be8d528ac1d4f7ebfd" \ No newline at end of file diff --git a/public/index.html b/public/index.html index 9b561f7c..84041a60 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,7 @@ - %REACT_APP_TITLE% - West Midlands Network + %REACT_APP_TITLE% - Transport West Midlands @@ -102,6 +102,7 @@ padding: 0; background-color: #f3f2f1; } + .wmnds-header--mega-menu {display: none} @@ -130,21 +131,8 @@
- + + diff --git a/public/manifest.json b/public/manifest.json index 96956941..82f09954 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { - "short_name": "WMN - Sign up to email alerts about disruption", - "name": "Sign up to email alerts about disruption - West Midlands Network ", - "description": "Sign up to email alerts about disruption section of the West Midlands Network website", + "short_name": "WMN - Sign up to alerts about disruption", + "name": "Sign up to alerts about disruption - West Midlands Network ", + "description": "Sign up to alerts about disruption section of the West Midlands Network website", "icons": [ { "src": "favicon.ico", diff --git a/src/assets/styles/_vars.scss b/src/assets/styles/_vars.scss index 1dc53b86..b0ecce07 100644 --- a/src/assets/styles/_vars.scss +++ b/src/assets/styles/_vars.scss @@ -18,7 +18,8 @@ $palettes: ( information: #84329b, disable: #676869, plannedDisruption: #ffdd00, - background: #f3f2f1 + background: #f3f2f1, + hover: #e2cee7 ); $white: #ffffff; diff --git a/src/components/Breadcrumb.js b/src/components/Breadcrumb.js index 2466d4b6..a9eef4fa 100644 --- a/src/components/Breadcrumb.js +++ b/src/components/Breadcrumb.js @@ -26,7 +26,7 @@ function Breadcrumb() { className="wmnds-breadcrumb__link wmnds-breadcrumb__link--current" aria-current="page" > - Sign up to email alerts about disruption + Sign up to alerts about disruption diff --git a/src/components/Form/Form.js b/src/components/Form/Form.js index e1f4c580..adda9751 100644 --- a/src/components/Form/Form.js +++ b/src/components/Form/Form.js @@ -16,6 +16,8 @@ import Step6EmailAlert from './Step6EmailAlert/Step6EmailAlert'; import Step7AddService from './Step7AddService/Step7AddService'; import Step8SearchForService from './Step8SearchForService/Step8SearchForService'; import Step9Confirm from './Step9Confirm/Step9Confirm'; +import StepDisruptionAlert from './StepDisruptionAlert/StepDisruptionAlert'; +import StepQuietHours from './StepQuietHours/StepQuietHours'; import SubmitSuccess from './Step10SubmitConfirmation/Success'; import SubmitError from './Step10SubmitConfirmation/Error'; // Custom Hooks @@ -49,15 +51,15 @@ const Form = ({ useTrackFormAbandonment(currentStep, formSubmitStatus); // Show debug options for below (this should be deleted on release) - const debugStepOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + const debugStepOptions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; let stepToGoTo; if (!ExistingUser) { - // NEW USERS: Show back button if the step is between 1 or 9 + // NEW USERS: Show back button if the step is between 1 or 11 if ( currentStep > 1 && - currentStep < 9 && + currentStep < 11 && !(currentStep === 5 && SMSAlert === 'no') && !(currentStep === 7 && SMSAlert === 'no') ) { @@ -67,13 +69,18 @@ const Form = ({ if (currentStep === 5 && SMSAlert === 'no') { stepToGoTo = 2; } - + if (currentStep === 4) { + stepToGoTo = 2; + } + if (currentStep === 2 && SMSAlert === 'yes') { + stepToGoTo = 4; + } if (currentStep === 7 && SMSAlert === 'no') { stepToGoTo = 5; } } else { /* EXISTING USERS: Show back button if the step is 4 or 6. Step 3 has no back button */ - if (currentStep > 3 && currentStep < 9 && currentStep !== 6) { + if (currentStep > 3 && currentStep < 11 && currentStep !== 6) { stepToGoTo = currentStep - 1; } @@ -100,7 +107,7 @@ const Form = ({ }, []); useEffect(() => { - if (currentStep === 9) scrollToTopOfSummary(); + if (currentStep === 11) scrollToTopOfSummary(); }, [currentStep, scrollToTopOfSummary]); // Run! Like go get some data from an API. @@ -118,7 +125,7 @@ const Form = ({ onClick={() => formDataDispatch({ type: 'UPDATE_STEP', - payload: hasReachedConfirmation ? 9 : stepToGoTo, + payload: hasReachedConfirmation ? 11 : stepToGoTo, }) } > @@ -152,10 +159,12 @@ const Form = ({ {currentStep === 6 && } {currentStep === 7 && } {currentStep === 8 && } - {currentStep === 9 && } + {currentStep === 9 && } + {currentStep === 10 && } + {currentStep === 11 && } {/* for testing only */} - {currentStep === 10 && } - {currentStep === 11 && } + {currentStep === 12 && } + {currentStep === 13 && } {/* If in development based on envs then show form debugging */} diff --git a/src/components/Form/Step10SubmitConfirmation/Success.js b/src/components/Form/Step10SubmitConfirmation/Success.js index 636abc7d..fb99a0b1 100644 --- a/src/components/Form/Step10SubmitConfirmation/Success.js +++ b/src/components/Form/Step10SubmitConfirmation/Success.js @@ -7,7 +7,7 @@ function Success() { // eslint-disable-next-line no-unused-vars const [formDataState, formDataDispatch] = useContext(FormDataContext); const { isRequestingRecovery } = formDataState; - const { Phone, SMSAlert, EmailAlert, SMSTerms } = formDataState.formData; + const { Phone, SMSAlert, EmailAlert } = formDataState.formData; const alignCenter = { textAlign: 'center', @@ -25,7 +25,7 @@ function Success() { 'Visit the link in the email to manage your disruption alerts.', 'You can now manage your services and communication preferences. You can access the page at any time by visiting the link in your email.', ]; - } else if (Phone && (SMSAlert === 'yes' || SMSTerms) && EmailAlert === 'yes') { + } else if (Phone && SMSAlert === 'yes' && EmailAlert === 'yes') { /* Text messages AND Email */ message = 'You have successfully signed up to text message and email alerts'; steps = [ @@ -34,7 +34,7 @@ function Success() { 'Visit the link in the confirmation email to access your disruption alert dashboard. Enter the PIN code sent to you via text message.', 'Once you have confirmed your mobile phone number, you’ll receive disruption alerts to your mobile phone.', ]; - } else if (Phone && (SMSAlert === 'yes' || SMSTerms)) { + } else if (Phone && SMSAlert === 'yes') { /* Text messages */ message = 'You have successfully signed up to text message alerts'; steps = [ @@ -81,7 +81,7 @@ function Success() {

{ { text: 'No', value: 'no' }, ]; - const selectedOption = document.querySelector( - 'input.wmnds-fe-radios__input[name="SMSAlert"]:checked' - ); - let extraInfo; - if (selectedOption && selectedOption.value === 'no') { - extraInfo = ( - - ); - } - return (

{/* Subsection */} @@ -39,20 +26,23 @@ const Step2SmsAlert = () => {

- Would you like to sign up to a trial to receive text message alerts for disruptions? + Do you want to recieve text message alerts for disruptions?

We’ll automatically send text message alerts straight to your mobile phone.

+
- {extraInfo}
{/* Continue button */} diff --git a/src/components/Form/Step6EmailAlert/Step6EmailAlert.js b/src/components/Form/Step6EmailAlert/Step6EmailAlert.js index ecba81f8..327d3cf3 100644 --- a/src/components/Form/Step6EmailAlert/Step6EmailAlert.js +++ b/src/components/Form/Step6EmailAlert/Step6EmailAlert.js @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; // Import custom hooks import useStepLogic from 'components/Form/useStepLogic'; // Import components @@ -10,18 +10,18 @@ import useFormData from '../useFormData'; const Step6EmailAlert = () => { const formRef = useRef(); // Used so we can keep track of the form DOM element const { register, handleSubmit, showGenericError, continueButton } = useStepLogic(formRef); // Custom hook for handling continue button (validation, errors etc) + const [radioValue, setRadioValue] = useState(); const radioButtons = [ { text: 'Yes', value: 'yes' }, { text: 'No', value: 'no' }, ]; - + const setCurrentValue = (e) => { + setRadioValue(e.toLowerCase()); + }; // Add InsetText with extra info when selected option is "no" let extraInfo; - const selectedOption = document.querySelector( - 'input.wmnds-fe-radios__input[name="EmailAlert"]:checked' - ); - if (selectedOption && selectedOption.value === 'no') { + if (radioValue && radioValue === 'no') { extraInfo = ( { setCurrentValue(e.target.value)} fieldValidation={register({ required: `Please select one option to proceed`, })} diff --git a/src/components/Form/Step9Confirm/Step9SummarySection.js b/src/components/Form/Step9Confirm/Step9SummarySection.js index 22d51f68..a56863e0 100644 --- a/src/components/Form/Step9Confirm/Step9SummarySection.js +++ b/src/components/Form/Step9Confirm/Step9SummarySection.js @@ -6,6 +6,7 @@ import { FormDataContext } from 'globalState/FormDataContext'; // Components import RemoveService from 'components/shared/RemoveService/RemoveService'; import Table from 'components/shared/Table/Table'; +import HoursMinutes from '../../shared/HoursMinutes/HoursMinutes'; function Step9SummarySection() { const [formDataState, formDataDispatch] = useContext(FormDataContext); @@ -20,6 +21,8 @@ function Step9SummarySection() { RoadAreas, LineId, ExistingUser, + QuietHours, + QuietDays, } = formDataState.formData; const { filterTramLineInfo } = useSelectableTramLines(); @@ -210,6 +213,51 @@ function Step9SummarySection() { )} + {!ExistingUser && QuietHours.length > 0 && ( + <> +
+

Daily quiet hours

+ +
+ You will not receive alerts between + . + + )} + {!ExistingUser && QuietDays.length > 0 && ( + <> +
+

Quiet days

+ +
+

+ You will not receive alerts on + {QuietDays.length > 1 ? ( + + {QuietDays.slice(0, -1).join(', ')} and{' '} + + ) : ( + ` ` + )} + {QuietDays[QuietDays.length - 1]}. +

+ + )} {ExistingUser && (

Your services

diff --git a/src/components/Form/StepDisruptionAlert/StepDisruptionAlert.js b/src/components/Form/StepDisruptionAlert/StepDisruptionAlert.js new file mode 100644 index 00000000..a8037e6c --- /dev/null +++ b/src/components/Form/StepDisruptionAlert/StepDisruptionAlert.js @@ -0,0 +1,71 @@ +import React, { useRef, useState } from 'react'; +// Import custom hooks +import useStepLogic from 'components/Form/useStepLogic'; +// Import components +import Radios from 'components/shared/FormElements/Radios/Radios'; +import SectionStepInfo from 'components/shared/SectionStepInfo/SectionStepInfo'; +import InsetText from 'components/shared/InsetText/InsetText'; +import useFormData from '../useFormData'; + +const StepDisruptionAlert = () => { + const formRef = useRef(); // Used so we can keep track of the form DOM element + const { register, handleSubmit, showGenericError, continueButton } = useStepLogic(formRef); // Custom hook for handling continue button (validation, errors etc) + const [radioValue, setRadioValue] = useState(); + const radioButtons = [ + { text: 'Yes', value: 'yes' }, + { text: 'No', value: 'no' }, + ]; + const setCurrentValue = (e) => { + setRadioValue(e.toLowerCase()); + }; + + // Add InsetText with extra info when selected option is "no" + let extraInfo; + + if (radioValue && radioValue === 'no') { + extraInfo = ( + + ); + } + + // Check if it is an existing user already + const { ExistingUser } = useFormData(); + const title = 'Do you want to choose when to receive disruption alerts?'; + const text = + 'You can set quiet hours and days so you only receive alerts when it’s best for you.'; + + return ( + + {/* Subsection */} + {!ExistingUser && } + + {/* Show generic error message */} + {showGenericError} + +
+ +

{title}

+

{text}

+
+ setCurrentValue(e.target.value)} + fieldValidation={register({ + required: `Please select one option to proceed`, + })} + /> + {extraInfo} +
+ + {/* Continue button */} + {continueButton()} + + ); +}; + +export default StepDisruptionAlert; diff --git a/src/components/Form/StepQuietHours/AddQuietDays.js b/src/components/Form/StepQuietHours/AddQuietDays.js new file mode 100644 index 00000000..f37ca0e3 --- /dev/null +++ b/src/components/Form/StepQuietHours/AddQuietDays.js @@ -0,0 +1,110 @@ +import React, { useState, useCallback, useRef } from 'react'; +// Context +// Components +import Button from 'components/shared/Button/Button'; +import Checkboxes from 'components/shared/FormElements/Checkboxes/Checkboxes'; +import useStepLogic from 'components/Form/useStepLogic'; + +const AddQuietDays = () => { + const formRef = useRef(); // Used so we can keep track of the form DOM element + const { formDataState, formDataDispatch } = useStepLogic(formRef); + const [showDays, setShowDays] = useState(false); + const [confirmDays, setConfirmDays] = useState(false); + const { QuietDays } = formDataState.formData; + const checkBoxes = [ + { text: 'Mon', value: 'Monday' }, + { text: 'Tue', value: 'Tuesday' }, + { text: 'Wed', value: 'Wednesday' }, + { text: 'Thu', value: 'Thursday' }, + { text: 'Fri', value: 'Friday' }, + { text: 'Sat', value: 'Saturday' }, + { text: 'Sun', value: 'Sunday' }, + ]; + const [days, setDays] = useState(0); + + const callback = useCallback((day) => { + setDays(day); + }, []); + + const handleCancelDays = () => { + setConfirmDays(false); + setShowDays(false); + }; + + const handleAddDays = () => { + if (days) { + formDataDispatch({ + type: 'UPDATE_FORM_DATA', + payload: { + QuietDays: [...days], + }, + }); + } + setConfirmDays(false); + setShowDays(false); + }; + const handleShowDays = () => { + setShowDays(true); + setConfirmDays(true); + }; + + return ( + <> +

Quiet days

+ {QuietDays && QuietDays.length < 1 ? ( +

You will not receive alerts for 24 hours on selected days.

+ ) : ( +

+ You will not receive alerts on + {QuietDays.length > 1 ? ( + + {QuietDays.slice(0, -1).join(', ')} and{' '} + + ) : ( + ` ` + )} + {QuietDays[QuietDays.length - 1]}. +

+ )} + {/* Add quiet days button */} + {!showDays && !confirmDays ? ( +
+
+ ) : ( +
+ +
+
+
+ )} +
+ + ); +}; + +export default AddQuietDays; diff --git a/src/components/Form/StepQuietHours/AddQuietHours.js b/src/components/Form/StepQuietHours/AddQuietHours.js new file mode 100644 index 00000000..fdd4ae92 --- /dev/null +++ b/src/components/Form/StepQuietHours/AddQuietHours.js @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +// Context +// Components +import Button from 'components/shared/Button/Button'; +import useStepLogic from 'components/Form/useStepLogic'; +import QuietHoursComponent from './QuietHoursComponent'; +import HoursMinutes from '../../shared/HoursMinutes/HoursMinutes'; + +const AddQuietHours = () => { + const { formDataState, formDataDispatch } = useStepLogic(); + const [showHours, setShowHours] = useState(false); + const { QuietHours } = formDataState.formData; + const [times, setTimes] = useState({ + id: `${Math.random()}`, + startHour: '00', + startMinute: '00', + endHour: '00', + endMinute: '00', + }); + + const handleConfirmHours = () => { + setShowHours(false); + }; + + const handleAddHours = () => { + setTimes({ + id: `${Math.random()}`, + startHour: '00', + startMinute: '00', + endHour: '00', + endMinute: '00', + }); + formDataDispatch({ + type: 'UPDATE_FORM_DATA', + payload: { + QuietHours: [...QuietHours, times], + }, + }); + }; + const handleShowHours = () => { + setShowHours(true); + if (QuietHours.length < 1) { + handleAddHours(); + } + }; + + return ( + <> +
+ {/* Subsection */} +

Daily quiet hours

+ {QuietHours && QuietHours.length < 1 ? ( +

You will not receive alerts at the selected times.

+ ) : ( +

+ You will not receive alerts between + . +

+ )} + {/* Add quiet hours button */} + {!showHours || (QuietHours && QuietHours.length < 1) ? ( +
+
+ ) : ( +
+ {/* Show the quiet hours the user has added */} + {QuietHours && QuietHours.length > 0 && ( + <> + {QuietHours.map((quietHours) => { + return ( + + ); + })} + + )} +
+ {QuietHours && QuietHours.length < 10 && ( +
+
+ )} +
+
+ + ); +}; + +export default AddQuietHours; diff --git a/src/components/Form/StepQuietHours/QuietHoursComponent.js b/src/components/Form/StepQuietHours/QuietHoursComponent.js new file mode 100644 index 00000000..ce1125c2 --- /dev/null +++ b/src/components/Form/StepQuietHours/QuietHoursComponent.js @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; + +// Components +import useStepLogic from 'components/Form/useStepLogic'; +import Button from 'components/shared/Button/Button'; +import Dropdown from 'components/shared/FormElements/Dropdown/Dropdown'; +// Import styling +import s from './QuietHoursComponent.module.scss'; + +const QuietHoursComponent = ({ name, quietHours }) => { + const { formDataState, formDataDispatch } = useStepLogic(); + const { QuietHours } = formDataState.formData; + const [hoursArray] = useState({ + id: name, + startHour: quietHours.startHour, + startMinute: quietHours.startMinute, + endHour: quietHours.endHour, + endMinute: quietHours.endMinute, + }); + + const currentHours = QuietHours.filter((item) => item.id === hoursArray.id); + + const onSelectedChange = (e) => { + const v = e.target.value; + const t = e.target.name; + Object.keys(hoursArray).map((item) => { + if (item === t) { + hoursArray[item] = v; + } + return item; + }); + const updatedArray = QuietHours.map((a) => { + if (a.id === hoursArray.id) { + // eslint-disable-next-line no-param-reassign + a = hoursArray; + } + return a; + }); + formDataDispatch({ + type: 'UPDATE_FORM_DATA', + payload: { + QuietHours: [...updatedArray], + }, + }); + }; + + const handleRemoveHours = (id) => { + formDataDispatch({ type: 'REMOVE_QUIET_HOURS', payload: id }); + }; + const times = () => { + const hour = []; + for (let i = 0; i < 24; i += 1) { + hour.push(`${`0${i}`.slice(-2)}`); + } + return hour; + }; + const minute = () => { + const j = []; + for (let i = 0; i < 60; i += 5) { + j.push(`${`0${i}`.slice(-2)}`); + } + return j; + }; + const hours = times().map((i) => ({ ...i, value: i, text: i })); + const minutes = minute().map((i) => ({ ...i, value: i, text: i })); + return ( + <> +
+
+
+
Start
+ + +
+
+
+
+
End
+ + +
+
+
+ + ); +}; +QuietHoursComponent.propTypes = { + name: PropTypes.string, + quietHours: PropTypes.oneOfType([PropTypes.shape, PropTypes.object]).isRequired, +}; + +QuietHoursComponent.defaultProps = { + name: '', +}; + +export default QuietHoursComponent; diff --git a/src/components/Form/StepQuietHours/QuietHoursComponent.module.scss b/src/components/Form/StepQuietHours/QuietHoursComponent.module.scss new file mode 100644 index 00000000..2373138d --- /dev/null +++ b/src/components/Form/StepQuietHours/QuietHoursComponent.module.scss @@ -0,0 +1,12 @@ +@import '~assets/styles/vars'; + +.button { + top: 5px; + padding: 5px; +} + +.button svg { + width: 40px; + height: 40px; + margin-left: 0; +} diff --git a/src/components/Form/StepQuietHours/StepQuietHours.js b/src/components/Form/StepQuietHours/StepQuietHours.js new file mode 100644 index 00000000..a899c725 --- /dev/null +++ b/src/components/Form/StepQuietHours/StepQuietHours.js @@ -0,0 +1,58 @@ +import React, { useRef } from 'react'; +// Import custom hooks +import useStepLogic from 'components/Form/useStepLogic'; +// Import components +import SectionStepInfo from 'components/shared/SectionStepInfo/SectionStepInfo'; +import InsetText from 'components/shared/InsetText/InsetText'; +import useFormData from '../useFormData'; +import AddQuietHours from './AddQuietHours'; +import AddQuietDays from './AddQuietDays'; + +const StepQuietHours = () => { + const formRef = useRef(); // Used so we can keep track of the form DOM element + const { handleSubmit, showGenericError, continueButton } = useStepLogic(formRef); // Custom hook for handling continue button (validation, errors etc) + // Add InsetText with extra info when selected option is "no" + let extraInfo; + const selectedOption = document.querySelector( + 'input.wmnds-fe-radios__input[name="EmailAlert"]:checked' + ); + if (selectedOption && selectedOption.value === 'no') { + extraInfo = ( + + ); + } + + // Check if it is an existing user already + const { ExistingUser } = useFormData(); + const title = 'Choose when you recieve disruption alerts'; + const text = + 'You can set quiet hours and days so you only receive alerts when it’s best for you.'; + + return ( +
+ {/* Subsection */} + {!ExistingUser && } + + {/* Show generic error message */} + {showGenericError} + +
+ +

{title}

+

{text}

+
+ + + {extraInfo} +
+ + {/* Continue button */} + {continueButton()} + + ); +}; + +export default StepQuietHours; diff --git a/src/components/Form/useFormData.js b/src/components/Form/useFormData.js index 08f341e0..74b78a13 100644 --- a/src/components/Form/useFormData.js +++ b/src/components/Form/useFormData.js @@ -18,9 +18,12 @@ const useFormData = () => { Phone, BusServices, TramServices, + QuietHours, + QuietDays, ExistingUser, SMSAlert, EmailAlert, + DisruptionAlert, SMSTerms, } = formDataState.formData; return { @@ -30,6 +33,8 @@ const useFormData = () => { Phone, BusServices, TramServices, + QuietHours, + QuietDays, ExistingUser, formDataState, formDataDispatch, @@ -37,6 +42,7 @@ const useFormData = () => { setMode, SMSAlert, EmailAlert, + DisruptionAlert, SMSTerms, }; }; diff --git a/src/components/Form/useStepLogic.js b/src/components/Form/useStepLogic.js index b2cdd055..7b717c9f 100644 --- a/src/components/Form/useStepLogic.js +++ b/src/components/Form/useStepLogic.js @@ -30,7 +30,7 @@ const useStepLogic = (formRef) => { if (formDataState.currentStep === 2) { // Step2: SMS alert? Yes -> Step 3 No -> Step 5 - setStep(getValues().SMSAlert === 'no' ? 5 : formDataState.currentStep + 1); + setStep(getValues().SMSAlert === 'no' ? 5 : formDataState.currentStep + 2); } else if (formDataState.currentStep === 5 && formDataState.formData.SMSAlert === 'no') { // If user opt out of SMS Alert (on step 2), ask for email (step 5) and automatically choose the email alerts (Step 5 -> 7) formDataDispatch({ @@ -38,20 +38,22 @@ const useStepLogic = (formRef) => { payload: { EmailAlert: 'yes' }, }); - setStep(formDataState.hasReachedConfirmation ? 9 : formDataState.currentStep + 2); + setStep(formDataState.hasReachedConfirmation ? 11 : formDataState.currentStep + 2); + } else if (formDataState.currentStep === 9 && getValues().DisruptionAlert === 'no') { + setStep(11); } else { - setStep(formDataState.hasReachedConfirmation ? 9 : formDataState.currentStep + 1); + setStep(formDataState.hasReachedConfirmation ? 11 : formDataState.currentStep + 1); } // EXISTING USERS: exceptions to the usual workflow if (formDataState.formData.ExistingUser) { if (formDataState.currentStep === 4) { // Step4: Phone -> Step 6: Keep receiving email alerts? - // Step4: Phone -> Step 9: summary (if using is editing the phone number) - setStep(formDataState.hasReachedConfirmation ? 9 : 6); + // Step4: Phone -> Step 11: summary (if using is editing the phone number) + setStep(formDataState.hasReachedConfirmation ? 11 : 6); } else if (formDataState.currentStep === 6) { - // Step6: Keep receiving email alerts? -> Step 9: Summary - setStep(9); + // Step6: Keep receiving email alerts? -> Step 11: Summary + setStep(11); } } } diff --git a/src/components/Form/useSubmitForm.js b/src/components/Form/useSubmitForm.js index 1949c607..5ac3bb18 100644 --- a/src/components/Form/useSubmitForm.js +++ b/src/components/Form/useSubmitForm.js @@ -1,3 +1,4 @@ +/* eslint-disable no-plusplus */ import { useState, useContext } from 'react'; import axios from 'axios'; import { useFormContext } from 'react-hook-form'; @@ -23,6 +24,8 @@ const useSubmitForm = (setFormSubmitStatus) => { Phone, ExistingUser, UserId, + QuietHours, + QuietDays, } = formDataState.formData; // Check if mobile phone has +44, if not, remove the 0 and add +44 @@ -39,6 +42,52 @@ const useSubmitForm = (setFormSubmitStatus) => { distance: area.radius * 1609.34, })); + const convertH2M = (timeInHour) => { + const timeParts = timeInHour.split(':'); + return Number(timeParts[0]) * 60 + Number(timeParts[1]); + }; + // Convert date to correct shape for the api + const QuietTimes = QuietHours.map((time) => ({ + StartTime: convertH2M(`${time.startHour}:${time.startMinute}`), + EndTime: convertH2M(`${time.endHour}:${time.endMinute}`), + })); + + const format = (n) => { + // eslint-disable-next-line no-bitwise + return `${`0${(n / 60) ^ 0}`.slice(-2)}:${`0${n % 60}`.slice(-2)}`; + }; + + const merge = (arr) => { + const arrFiltered = (r) => { + return r.EndTime > r.StartTime; + }; + const arrSorted = arr.filter(arrFiltered); + const result = arrSorted.sort((a, b) => { + return a.StartTime - b.StartTime; + }); + let i = 0; + + while (i < result.length - 1) { + const current = result[i]; + const next = result[i + 1]; + + // check if there is an overlapping + if (current.EndTime >= next.StartTime) { + current.EndTime = Math.max(current.EndTime, next.EndTime); + // remove next + result.splice(i + 1, 1); + } else { + // move to next + i++; + } + } + return result; + }; + const results = merge(QuietTimes).map((i) => [format(i.StartTime), format(i.EndTime)]); + const QuietTimesFiltered = results.map((time) => ({ + StartTime: time[0], + EndTime: time[1], + })); // Map all destructured vals above to an object we will send to API const dataToSend = { Name: `${Firstname} ${LastName}`, @@ -50,6 +99,8 @@ const useSubmitForm = (setFormSubmitStatus) => { EmailDisabled: EmailAlert !== 'yes', MobileNumber: englishNumber || '', siteCode: ExistingUser ? UserId : '', + QuietDays: QuietDays.map((v) => ({ day: v })), + QuietTimePeriods: QuietTimesFiltered, }; const handleSubmit = async (event) => { diff --git a/src/components/HeaderAndBreadCrumb.js b/src/components/HeaderAndBreadCrumb.js index 852396b0..9f34d3ad 100644 --- a/src/components/HeaderAndBreadCrumb.js +++ b/src/components/HeaderAndBreadCrumb.js @@ -11,7 +11,7 @@ const HeaderAndBreadcrumb = ({ isFormStarted, formSubmitStatus }) => { {/* */}
{

This is a new service - your{' '} { + return ( + <> +

+ + ); +}; + +// PropTypes +Checkbox.propTypes = { + name: PropTypes.string.isRequired, + fieldValidation: PropTypes.func, + onChange: PropTypes.func.isRequired, + text: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + checked: PropTypes.bool.isRequired, +}; + +Checkbox.defaultProps = { + fieldValidation: null, +}; + +export default Checkbox; diff --git a/src/components/shared/FormElements/Checkboxes/Checkbox/Checkbox.module.scss b/src/components/shared/FormElements/Checkboxes/Checkbox/Checkbox.module.scss new file mode 100644 index 00000000..ea0be83b --- /dev/null +++ b/src/components/shared/FormElements/Checkboxes/Checkbox/Checkbox.module.scss @@ -0,0 +1,23 @@ +@import '~assets/styles/vars'; +@import '~assets/styles/mixins'; + +.checkbox { + position: absolute; + opacity: 0; +} + +.checkbox:checked + .button { + background-color: get-color(hover); +} + +.checkbox:focus + .button { + box-shadow: 0 0 0 2px $white, 0 0 0 4px get-color(secondary); +} + +.button { + display: inline; + width: 4rem; + margin-right: 0.5rem; + margin-bottom: 2rem; + float: left; +} diff --git a/src/components/shared/FormElements/Checkboxes/Checkboxes.js b/src/components/shared/FormElements/Checkboxes/Checkboxes.js new file mode 100644 index 00000000..579afa56 --- /dev/null +++ b/src/components/shared/FormElements/Checkboxes/Checkboxes.js @@ -0,0 +1,82 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import dompurify from 'dompurify'; +// Import contexts +import { useFormContext } from 'react-hook-form'; +// Import components +import useStepLogic from 'components/Form/useStepLogic'; +import Checkbox from './Checkbox/Checkbox'; + +const { sanitize } = dompurify; + +const Checkboxes = ({ name, label, checkboxes, fieldValidation, parentCallback }) => { + const { errors } = useFormContext(); + const { formDataState } = useStepLogic(); + const { QuietDays } = formDataState.formData; + const selectedDays = checkboxes.map((a) => QuietDays.includes(a.value)); + const [checkedState, setCheckedState] = useState(selectedDays); + + const handleOnChange = (position) => { + const updatedCheckedState = checkedState.map((item, index) => + index === position ? !item : item + ); + setCheckedState(updatedCheckedState); + const daysSelected = updatedCheckedState.map((currentState, index) => { + if (currentState === true) { + return checkboxes[index].value; + } + return null; + }); + const filtered = daysSelected.filter((a) => a); + parentCallback(filtered); + }; + return ( +
+
+ + {label &&

{label}

} + {/* If there is an error, show here */} + {errors[name] && ( + + )} +
+
+ {/* Loop through checkboxes and display each checkbox */} + {checkboxes.map(({ text, value }, index) => ( + handleOnChange(index)} + aria-label={name} + /> + ))} +
+
+
+ ); +}; + +// PropTypes +Checkboxes.propTypes = { + fieldValidation: PropTypes.func, + parentCallback: PropTypes.func.isRequired, + name: PropTypes.string.isRequired, + label: PropTypes.string, + checkboxes: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string, PropTypes.string)).isRequired, +}; + +Checkboxes.defaultProps = { + fieldValidation: null, + label: null, +}; + +export default Checkboxes; diff --git a/src/components/shared/FormElements/Dropdown/Dropdown.js b/src/components/shared/FormElements/Dropdown/Dropdown.js new file mode 100644 index 00000000..da8926d0 --- /dev/null +++ b/src/components/shared/FormElements/Dropdown/Dropdown.js @@ -0,0 +1,83 @@ +import React from 'react'; +import dompurify from 'dompurify'; +import PropTypes from 'prop-types'; +import useStepLogic from 'components/Form/useStepLogic'; + +// Import styling +import s from './Dropdown.module.scss'; + +const { sanitize } = dompurify; + +const Dropdown = ({ + name, + hint, + parent, + label, + error, + options, + defaultValue, + onChange, + onBlur, +}) => { + const { formDataState } = useStepLogic(); + const defaultSelectValue = defaultValue || formDataState.formData.QuietHours[parent]; // cast to acceptable types for a select element + + return ( +
+
+
+ {/* If there is an error, show here */} + {error && ( + + )} + + +
+
+
+ ); +}; + +// Set props +Dropdown.propTypes = { + name: PropTypes.string, + parent: PropTypes.string, + hint: PropTypes.string, + error: PropTypes.string, + label: PropTypes.string, + defaultValue: PropTypes.string, + options: PropTypes.PropTypes.oneOfType([PropTypes.shape, PropTypes.array]).isRequired, + onChange: PropTypes.func.isRequired, + onBlur: PropTypes.func, +}; + +Dropdown.defaultProps = { + name: '', + parent: '', + hint: '', + error: '', + label: null, + defaultValue: '', + onBlur: () => {}, +}; +export default Dropdown; diff --git a/src/components/shared/FormElements/Dropdown/Dropdown.module.scss b/src/components/shared/FormElements/Dropdown/Dropdown.module.scss new file mode 100644 index 00000000..c6d7ec6f --- /dev/null +++ b/src/components/shared/FormElements/Dropdown/Dropdown.module.scss @@ -0,0 +1,7 @@ +@import '~assets/styles/vars'; + +.select { + display: inline-block; + width: 100px; + margin-right: 0.5rem; +} diff --git a/src/components/shared/FormElements/Radios/Radio/Radio.js b/src/components/shared/FormElements/Radios/Radio/Radio.js index 9dce8f7c..3939adbb 100644 --- a/src/components/shared/FormElements/Radios/Radio/Radio.js +++ b/src/components/shared/FormElements/Radios/Radio/Radio.js @@ -7,7 +7,7 @@ import { FormDataContext } from 'globalState/FormDataContext'; const { sanitize } = dompurify; -const Radio = ({ name, fieldValidation, text, value }) => { +const Radio = ({ name, fieldValidation, text, value, onChange }) => { const [formDataState] = useContext(FormDataContext); // Get the state of form data from FormDataContext return ( @@ -25,6 +25,7 @@ const Radio = ({ name, fieldValidation, text, value }) => { ref={fieldValidation} value={value} defaultChecked={formDataState.formData[name] === value} + onChange={onChange} /> @@ -36,12 +37,14 @@ const Radio = ({ name, fieldValidation, text, value }) => { Radio.propTypes = { name: PropTypes.string.isRequired, fieldValidation: PropTypes.func, + onChange: PropTypes.func, text: PropTypes.string.isRequired, value: PropTypes.string.isRequired, }; Radio.defaultProps = { fieldValidation: null, + onChange: null, }; export default Radio; diff --git a/src/components/shared/FormElements/Radios/Radios.js b/src/components/shared/FormElements/Radios/Radios.js index 05f34ad0..4f6b9031 100644 --- a/src/components/shared/FormElements/Radios/Radios.js +++ b/src/components/shared/FormElements/Radios/Radios.js @@ -8,7 +8,7 @@ import Radio from './Radio/Radio'; const { sanitize } = dompurify; -const Radios = ({ name, classes, label, radios, fieldValidation }) => { +const Radios = ({ name, classes, label, radios, fieldValidation, onChange }) => { const { errors } = useFormContext(); return ( @@ -35,6 +35,7 @@ const Radios = ({ name, classes, label, radios, fieldValidation }) => { text={radio.text} value={radio.value} fieldValidation={fieldValidation} + onChange={onChange} /> ))}
@@ -46,6 +47,7 @@ const Radios = ({ name, classes, label, radios, fieldValidation }) => { // PropTypes Radios.propTypes = { fieldValidation: PropTypes.func, + onChange: PropTypes.func, classes: PropTypes.string, name: PropTypes.string.isRequired, label: PropTypes.string, @@ -54,6 +56,7 @@ Radios.propTypes = { Radios.defaultProps = { fieldValidation: null, + onChange: null, classes: null, label: null, }; diff --git a/src/components/shared/HoursMinutes/HoursMinutes.js b/src/components/shared/HoursMinutes/HoursMinutes.js new file mode 100644 index 00000000..b6c5ede3 --- /dev/null +++ b/src/components/shared/HoursMinutes/HoursMinutes.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const HoursMinutes = ({ times }) => { + return times.map((time, i) => { + let last = false; + if (times.length - 1 === i) { + last = true; + } + return ( + + {time.startHour}:{time.startMinute} + and + {time.endHour}:{time.endMinute} + {!last && ,} + + ); + }); +}; +HoursMinutes.propTypes = { + times: PropTypes.oneOfType([PropTypes.shape, PropTypes.array]).isRequired, +}; +export default HoursMinutes; diff --git a/src/globalState/FormDataContext.js b/src/globalState/FormDataContext.js index 46443601..d4f28f0d 100644 --- a/src/globalState/FormDataContext.js +++ b/src/globalState/FormDataContext.js @@ -28,6 +28,8 @@ export const FormDataProvider = (props) => { TramLines: [], Trains: [], RoadAreas: [], + QuietHours: [], + QuietDays: [], }, formRef: '', hasReachedConfirmation: false, @@ -110,7 +112,16 @@ export const FormDataProvider = (props) => { }, }; } - + // Remove the quite hours from form data + case 'REMOVE_QUIET_HOURS': { + return { + ...state, + formData: { + ...state.formData, + QuietHours: state.formData.QuietHours.filter((hours) => action.payload !== hours.id), + }, + }; + } // Update service mode case 'UPDATE_MODE': { return {