diff --git a/package.json b/package.json index 81592df3d..1a42a9111 100644 --- a/package.json +++ b/package.json @@ -192,9 +192,9 @@ "dev": "node scripts/start.js", "dev:server": "nodemon", "build": "node scripts/build.js", - "test": "cross-env FAST_REFRESH=false BROWSER=none REACT_APP_API_URL=http://127.0.0.1:9001 HTTPS=0 PORT=9001 WDS_SOCKET_PORT=9001 concurrently --kill-others --success first \"npm run dev\" \"wait-on http://127.0.0.1:9001 && cypress run\"", + "test": "cross-env NODE_ENV=development FAST_REFRESH=false BROWSER=none REACT_APP_API_URL=http://127.0.0.1:9001 HTTPS=0 PORT=9001 WDS_SOCKET_PORT=9001 concurrently --kill-others --success first \"npm run dev\" \"wait-on http://127.0.0.1:9001 && cypress run\"", "test:components": "NODE_ENV=development cypress run --component --headed", - "test:build": "cross-env FAST_REFRESH=false BROWSER=none PORT=9001 CYPRESS_WS_URL=http://127.0.0.1:9002 REACT_APP_API_URL=http://127.0.0.1:9001 concurrently --kill-others --success first \"npm run start\" \"wait-on http://127.0.0.1:9001 && cypress run\"", + "test:build": "cross-env NODE_ENV=production FAST_REFRESH=false BROWSER=none PORT=9001 CYPRESS_WS_URL=http://127.0.0.1:9002 REACT_APP_API_URL=http://127.0.0.1:9001 concurrently --kill-others --success first \"npm run start\" \"wait-on http://127.0.0.1:9001 && cypress run\"", "release": "standard-version", "lint": "npm run lint:scripts && npm run lint:style", "lint:scripts": "eslint src && tsc", diff --git a/src/api/apiAgencySelection.ts b/src/api/apiAgencySelection.ts index 8f7acc972..9c89ad7b6 100644 --- a/src/api/apiAgencySelection.ts +++ b/src/api/apiAgencySelection.ts @@ -9,7 +9,7 @@ export const apiAgencySelection = async ( ...params }: { postcode?: string; - consultingType?: number | undefined; + consultingType?: number; topicId?: number; age?: number; gender?: string; diff --git a/src/api/apiGetTopicGroups.ts b/src/api/apiGetTopicGroups.ts index 334c17c9c..87fe0bb33 100644 --- a/src/api/apiGetTopicGroups.ts +++ b/src/api/apiGetTopicGroups.ts @@ -1,4 +1,4 @@ -import { TopicGroup } from '../globalState/interfaces/TopicGroups'; +import { TopicGroup } from '../globalState/interfaces'; import { endpoints } from '../resources/scripts/endpoints'; import { fetchData, FETCH_ERRORS, FETCH_METHODS } from './fetchData'; @@ -7,7 +7,7 @@ export const apiGetTopicGroups = async (): Promise<{ }> => { return fetchData({ url: endpoints.topicGroups, - responseHandling: [FETCH_ERRORS.EMPTY], + responseHandling: [FETCH_ERRORS.EMPTY, FETCH_ERRORS.NO_MATCH], method: FETCH_METHODS.GET }); }; diff --git a/src/components/input/input.tsx b/src/components/input/input.tsx index 55a1e349d..2b97290df 100644 --- a/src/components/input/input.tsx +++ b/src/components/input/input.tsx @@ -147,15 +147,15 @@ export const Input = ({ }} sx={{ '&[type=number]': { - '-moz-appearance': 'textfield' + MozAppearance: 'textfield' }, '&::-webkit-outer-spin-button': { - '-webkit-appearance': 'none', - 'margin': 0 + WebkitAppearance: 'none', + margin: 0 }, '&::-webkit-inner-spin-button': { - '-webkit-appearance': 'none', - 'margin': 0 + WebkitAppearance: 'none', + margin: 0 }, 'mt': '24px', '& legend': { @@ -249,6 +249,7 @@ export const Input = ({ multipleCriteria.map((criteria) => { return ( { // As long as no consulting type or topic is selected we can't show any agencies - if (!consultingType || topicsEnabledAndUnSelected) { + if (!consultingType && topicsEnabledAndUnSelected) { return []; } @@ -131,8 +131,9 @@ export const useAgenciesForRegistration = ({ apiAgencySelection( { ...(autoSelectPostcode ? {} : { postcode }), - consultingType: consultingType?.id, topicId: topic?.id, + // ToDo: API will ignore consultingType if topicId isset but its required to be send + consultingType: topic?.id || consultingType?.id, fetchConsultingTypeDetails: true }, abortController.signal diff --git a/src/extensions/components/app/app.tsx b/src/extensions/components/app/app.tsx deleted file mode 100644 index 658d2cbd1..000000000 --- a/src/extensions/components/app/app.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import * as React from 'react'; -import { ThemeProvider } from '@mui/material'; -import { UrlParamsProvider } from '../../../globalState/provider/UrlParamsProvider'; -import { RegistrationProvider } from '../../../globalState'; -import { App as CoreApp, AppProps } from '../../../components/app/app'; -import { lazy } from 'react'; -import '../../../resources/styles/mui-variables-mapping'; -import theme from '../../theme'; - -const Registration = lazy(() => - import('../registration/Registration').then((m) => ({ - default: m.Registration - })) -); - -const NewRegistration = () => ( - - - - - -); - -export const App = (props: AppProps) => { - // The login is possible both at the root URL as well as with an - // optional resort name. Since resort names are dynamic, we have - // to find out if the provided path is a resort name. If not, we - // use the authenticated app as a catch-all fallback. - - const extraRoutes = [ - ...props.extraRoutes, - { - route: { - path: [ - '/registration', - '/registration/topic-selection', - '/registration/zipcode', - '/registration/account-data', - '/registration/agency-selection' - ] - }, - component: NewRegistration - } - ]; - - return ( - - - - ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypes.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypes.styles.scss deleted file mode 100644 index c87469db5..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypes.styles.scss +++ /dev/null @@ -1,63 +0,0 @@ -.consultingTypes { - padding: 0; - - @include breakpoint($fromMedium) { - width: 490px; - } - - &__intro { - font-size: $font-size-h3; - line-height: 32px; - } - - &__groupSelect { - max-width: 100%; - margin: $grid-base-three 0; - padding: 0; - } - - &__children { - margin: $grid-base-six -#{$grid-base-three} 0; - } - - &__learnMore { - display: flex; - align-items: flex-start; - margin-top: $grid-base-eight; - appearance: none; - border: none; - background: transparent; - cursor: pointer; - text-decoration: underline; - padding-left: 0; - - &:hover { - color: var(--skin-color-primary-hover, $hover-primary); - } - } - - &__learnMoreIcon { - margin-right: $grid-base; - position: relative; - color: $primary; - } - - &__learnMoreText { - color: $primary; - &:hover { - color: var(--skin-color-primary-hover, $hover-primary); - } - } - - &__explanationOverlay .overlay__content { - text-align: left; - } - - .button-as-link { - &:focus-visible { - outline: $focus-outline; - outline-offset: 4px; - border-radius: $login-button-focus-border-radius; - } - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypes.tsx b/src/extensions/components/consultingTypes/ConsultingTypes.tsx deleted file mode 100644 index 2ec79a776..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypes.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import * as React from 'react'; -import { useEffect, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { apiGetConsultingTypeGroups } from '../../api/apiGetConsultingTypeGroups'; -import { ConsultingTypeGroupInterface } from '../../globalState/interfaces/ConsultingTypeGroupInterface'; -import { Stage } from '../stage/stage'; -import { ConsultingTypesGroupChild } from './ConsultingTypesGroupChild'; -import { ConsultingTypesOverlay } from './ConsultingTypesOverlay'; -import './ConsultingTypes.styles.scss'; -import useIsFirstVisit from '../../../utils/useIsFirstVisit'; -import { Headline } from '../../../components/headline/Headline'; -import { InfoIcon } from '../../../resources/img/icons'; -import { LoadingIndicator } from '../../../components/loadingIndicator/LoadingIndicator'; -import { SelectDropdown } from '../../../components/select/SelectDropdown'; -import { StageLayout } from '../../../components/stageLayout/StageLayout'; -import { Text } from '../../../components/text/Text'; - -export const ConsultingTypes = () => { - const { t: translate } = useTranslation(['common', 'consultingTypes']); - - const [loadedConsultingTypeGroups, setLoadedConsultingTypeGroups] = - useState([]); - - const [consultingTypeGroups, setConsultingTypeGroups] = useState< - (ConsultingTypeGroupInterface & { - id: string; - })[] - >([]); - - const [loading, setLoading] = useState(true); - const [selectedGroupId, setSelectedGroupId] = useState(null); - const [expandedConsultingTypeId, setExpandedConsultingTypeId] = - useState(null); - const [isExplanationOverlayOpen, setIsExplanationOverlayOpen] = - useState(false); - const [selectedGroup, setSelectedGroup] = useState(null); - - useEffect(() => { - if (selectedGroupId === null) { - return; - } - setSelectedGroup( - consultingTypeGroups.find((group) => group.id === selectedGroupId) - ); - }, [selectedGroupId, consultingTypeGroups]); - - const handleGroupSelect = useCallback((item) => { - setSelectedGroupId(item.value); - setExpandedConsultingTypeId(undefined); - }, []); - - const handleConsultingTypeToggle = useCallback( - (consultingTypeId) => { - setExpandedConsultingTypeId( - consultingTypeId === expandedConsultingTypeId - ? undefined - : consultingTypeId - ); - }, - [expandedConsultingTypeId] - ); - - const handleExplanationOverlayToggle = () => { - setIsExplanationOverlayOpen(!isExplanationOverlayOpen); - }; - - const sortConsultingTypeGroups = useCallback( - (groups: ConsultingTypeGroupInterface[]) => - groups - // Translate titles before sorting - .map( - ( - group - ): ConsultingTypeGroupInterface & { - id: string; - } => ({ - ...group, - id: group.consultingTypes - .map((consultingType) => consultingType.id) - .sort() - .join('-'), - title: translate( - [ - `consultingTypeGroup.${group.title}.title`, - group.title - ], - { ns: 'consultingTypes' } - ) - }) - ) - .sort((a, b) => (a.title < b.title ? -1 : 1)) - .map((group) => ({ - ...group, - consultingTypes: group.consultingTypes - .slice() - // Translate titles before sorting - .map((consultingType) => ({ - ...consultingType, - titles: { - ...consultingType.titles, - long: translate( - [ - `consultingType.${consultingType.id}.titles.long`, - consultingType.titles.long - ], - { ns: 'consultingTypes' } - ), - default: translate( - [ - `consultingType.${consultingType.id}.titles.default`, - consultingType.titles.default - ], - { ns: 'consultingTypes' } - ) - } - })) - .sort((a, b) => - a.titles.long < b.titles.long ? -1 : 1 - ) - })), - [translate] - ); - - useEffect(() => { - apiGetConsultingTypeGroups() - .then(setLoadedConsultingTypeGroups) - .catch((error) => { - console.log(error); - }); - }, []); - - useEffect(() => { - if (!loadedConsultingTypeGroups) { - return; - } - - setConsultingTypeGroups( - sortConsultingTypeGroups(loadedConsultingTypeGroups) - ); - setLoading(false); - - return () => { - setLoading(true); - }; - }, [loadedConsultingTypeGroups, sortConsultingTypeGroups]); - - const isFirstVisit = useIsFirstVisit(); - - if (loading) { - return null; - } - - return ( - - } - > - {consultingTypeGroups == null ? ( - - ) : ( -
- - - ({ - value: group.id, - label: group.title - }))} - selectInputLabel={translate( - 'consultingTypes.selectGroup' - )} - handleDropdownSelect={handleGroupSelect} - useIconOption={false} - isSearchable={false} - menuPlacement="bottom" - /> -
- {selectedGroup !== null && - selectedGroup.consultingTypes.map( - (consultingType) => ( - - handleConsultingTypeToggle( - consultingType.id - ) - } - groupChild={consultingType} - /> - ) - )} -
- -
- )} - - {isExplanationOverlayOpen && ( - - )} -
- ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.styles.scss deleted file mode 100644 index c3d88ca86..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.styles.scss +++ /dev/null @@ -1,41 +0,0 @@ -.consultingTypesAgencySelection { - margin-top: $grid-base-three; - - &__innerWrapper { - @include breakpoint($xlarge) { - display: flex; - } - } - - &__postcode { - @include breakpoint($xlarge) { - max-width: 200px; - } - } - - &__register { - margin-top: $grid-base-three; - - button { - width: 100%; - } - - @include breakpoint($xlarge) { - margin-top: 0; - margin-left: $grid-base-two; - } - } - - &__postcodeFallback { - margin-top: $grid-base-three; - - p { - margin-bottom: 20px; - } - - a { - display: block; - text-decoration: underline; - } - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.tsx b/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.tsx deleted file mode 100644 index 104fb1adb..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesAgencySelection.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import './ConsultingTypesAgencySelection.styles.scss'; -import { useAppConfig } from '../../../hooks/useAppConfig'; -import { VALID_POSTCODE_LENGTH } from '../../../components/agencySelection/agencySelectionHelpers'; -import { apiAgencySelection, FETCH_ERRORS } from '../../../api'; -import { parsePlaceholderString } from '../../../utils/parsePlaceholderString'; -import { InputField } from '../../../components/inputField/InputField'; -import { PinIcon } from '../../../resources/img/icons'; -import { Button, BUTTON_TYPES } from '../../../components/button/Button'; -import { Notice } from '../../../components/notice/Notice'; -import { - AgencyDataInterface, - ConsultingTypeInterface -} from '../../../globalState/interfaces'; -import { AskerRegistrationExternalAgencyOverlay } from '../../../components/profile/AskerRegistrationExternalAgencyOverlay'; -import { Text } from '../../../components/text/Text'; - -interface ConsultingTypesAgencySelectionProps { - consultingType: ConsultingTypeInterface; -} - -const ConsultingTypesAgencySelection = ({ - consultingType -}: ConsultingTypesAgencySelectionProps) => { - const settings = useAppConfig(); - const [postcode, setPostcode] = useState(''); - const [agency, setAgency] = useState(); - const [postcodeFallbackLink, setPostcodeFallbackLink] = useState(); - const [externalAgencyOverlayActive, setExternalAgencyOverlayActive] = - useState(null); - const { t: translate } = useTranslation(); - - const handlePostcodeInput = (e) => { - setPostcode(e.target.value); - setPostcodeFallbackLink(null); - }; - - const handleButton = () => { - if (agency.external) { - if (!agency.url) { - console.error( - "Agency is external but didn't provide a `url`. Failed to redirect." - ); - return; - } - setExternalAgencyOverlayActive(true); - } else { - window.location.href = `/${consultingType.slug}/registration?postcode=${postcode}`; - } - }; - - useEffect(() => { - setAgency(undefined); - - if (postcode.length !== VALID_POSTCODE_LENGTH) { - return; - } - - let isCanceled = false; - apiAgencySelection({ - postcode, - consultingType: consultingType.id - }) - .then((result) => { - if (isCanceled) return; - - if (!result || result.length === 0) { - console.error( - 'Agency selection returned an empty result. This should never happen.' - ); - return; - } - setAgency(result[0]); - }) - .catch((error) => { - if (error.message === FETCH_ERRORS.EMPTY) { - setPostcodeFallbackLink( - parsePlaceholderString(settings.postcodeFallbackUrl, { - url: consultingType.urls - .registrationPostcodeFallbackUrl, - postcode: postcode - }) - ); - } - - return null; - }); - - return () => { - isCanceled = true; - }; - }, [ - consultingType.id, - consultingType.urls.registrationPostcodeFallbackUrl, - postcode, - settings.postcodeFallbackUrl - ]); - - const handleExternalAgencyOverlayAction = () => { - setExternalAgencyOverlayActive(false); - }; - - return ( -
-
- - }} - inputHandle={handlePostcodeInput} - /> -
- {postcodeFallbackLink != null && ( - - - - {translate( - 'registration.agencySelection.postcode.search' - )} - - - )} - {externalAgencyOverlayActive && ( - - )} -
- ); -}; - -export default ConsultingTypesAgencySelection; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.styles.scss deleted file mode 100644 index fa4ae97c6..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.styles.scss +++ /dev/null @@ -1,55 +0,0 @@ -.consultingTypesGroupChild { - &:last-of-type:not(.consultingTypesGroupChild--expanded) { - border-bottom: 1px solid $grey-light; - } - - &:not(.consultingTypesGroupChild--expanded) { - border-top: 1px solid $grey-light; - } - - &:hover { - background-color: $background-lighter; - } - - &--expanded { - border: none; - background-color: $background-lighter; - - & + .consultingTypesGroupChild { - border-top: none; - } - - .consultingTypesGroupChild__titleLabel { - font-weight: $font-weight-bold; - color: $secondary; - } - } - - &__title { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - padding: $grid-base-three; - background: transparent; - appearance: none; - border: none; - cursor: pointer; - } - - &__titleLabel { - color: $primary; - } - - &__icon { - margin-left: $grid-base-three; - - path { - fill: $primary-4; - } - } - - &__details { - padding: 0 $grid-base-three $grid-base-three; - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.tsx b/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.tsx deleted file mode 100644 index 69fd2a2c4..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesGroupChild.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import clsx from 'clsx'; -import * as React from 'react'; -import { useTranslation } from 'react-i18next'; -import { ConsultingTypesGroupChildDetails } from './ConsultingTypesGroupChildDetails'; -import { ConsultingTypeGroupChildInterface } from '../../globalState/interfaces/ConsultingTypeGroupInterface'; -import { ArrowDownIcon, ArrowUpIcon } from '../../../resources/img/icons'; -import { Text } from '../../../components/text/Text'; -import './ConsultingTypesGroupChild.styles.scss'; - -interface ConsultingTypesGroupChildProps { - groupChild: ConsultingTypeGroupChildInterface; - handleToggleExpanded(): void; - isExpanded: boolean; -} - -export const ConsultingTypesGroupChild = ({ - groupChild, - handleToggleExpanded, - isExpanded -}: ConsultingTypesGroupChildProps) => { - const { t: translate } = useTranslation(['consultingTypes']); - const iconProps = { - width: 14, - className: 'consultingTypesGroupChild__icon' - }; - - return ( -
- - {isExpanded && ( - - )} -
- ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesGroupChildDetails.tsx b/src/extensions/components/consultingTypes/ConsultingTypesGroupChildDetails.tsx deleted file mode 100644 index 960036f8b..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesGroupChildDetails.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import ConsultingTypesAgencySelection from './ConsultingTypesAgencySelection'; -import './consultingTypesGroupChildDetails.styles.scss'; -import { useTranslation } from 'react-i18next'; -import { apiGetConsultingType } from '../../../api'; -import { LoadingIndicator } from '../../../components/loadingIndicator/LoadingIndicator'; -import { ConsultingTypeInterface } from '../../../globalState/interfaces'; -import { Text } from '../../../components/text/Text'; -import { ArrowRightIcon } from '../../../resources/img/icons'; -interface ConsultingTypesGroupChildDetailsProps { - className?: string; - consultingTypeId: number; -} - -export const ConsultingTypesGroupChildDetails = ({ - className, - consultingTypeId -}: ConsultingTypesGroupChildDetailsProps) => { - const [consultingType, setConsultingType] = - useState(); - const { t: translate } = useTranslation(['common', 'consultingTypes']); - - useEffect(() => { - let isCanceled; - - apiGetConsultingType({ consultingTypeId }).then((result) => { - if (isCanceled) return; - setConsultingType(result); - }); - return () => { - isCanceled = true; - }; - }, [consultingTypeId]); - - if (consultingType == null) { - return ( -
- -
- ); - } - - return ( -
- - {consultingType.furtherInformation.url && ( - - - - - )} - -
- {translate('consultingTypes.details.explanation.description') - .split('\n') - .map((part, index) => ( - - ))} -
- -
- ); -}; diff --git a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.styles.scss b/src/extensions/components/consultingTypes/ConsultingTypesOverlay.styles.scss deleted file mode 100644 index 9bf15f8e2..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.styles.scss +++ /dev/null @@ -1,12 +0,0 @@ -.consultingTypesOverlay { - &__serviceExplanation { - display: grid; - grid-template-columns: auto; - padding: $grid-base-four 0 $grid-base; - row-gap: $grid-base-three; - - .text { - text-align: left; - } - } -} diff --git a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.tsx b/src/extensions/components/consultingTypes/ConsultingTypesOverlay.tsx deleted file mode 100644 index 05448d865..000000000 --- a/src/extensions/components/consultingTypes/ConsultingTypesOverlay.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import './ConsultingTypesOverlay.styles.scss'; -import { - Overlay, - OVERLAY_FUNCTIONS -} from '../../../components/overlay/Overlay'; -import { ServiceExplanation } from '../../../components/serviceExplanation/ServiceExplanation'; -import { BUTTON_TYPES } from '../../../components/button/Button'; - -interface ConsultingTypesOverlayProps { - handleOverlay?(): void; -} - -export const ConsultingTypesOverlay = ({ - handleOverlay -}: ConsultingTypesOverlayProps) => { - const { t: translate } = useTranslation(); - return ( - - ), - buttonSet: [ - { - label: translate('consultingTypes.overlay.close'), - function: OVERLAY_FUNCTIONS.CLOSE, - type: BUTTON_TYPES.PRIMARY - } - ] - }} - handleOverlay={handleOverlay} - /> - ); -}; diff --git a/src/extensions/components/consultingTypes/consultingTypesGroupChildDetails.styles.scss b/src/extensions/components/consultingTypes/consultingTypesGroupChildDetails.styles.scss deleted file mode 100644 index d7abf504a..000000000 --- a/src/extensions/components/consultingTypes/consultingTypesGroupChildDetails.styles.scss +++ /dev/null @@ -1,40 +0,0 @@ -.consultingTypesGroupChildDetails { - &__loadingIndicator { - display: flex; - justify-content: center; - padding: $grid-base-three; - - // Reserve some extra space to avoid the layout shifting too much - min-height: 280px; - } - - &__details { - display: inline-flex; - align-items: flex-start; - margin-top: $grid-base-two; - } - - &__detailsIcon { - position: relative; - margin-right: $grid-base * 0.5; - top: -3px; // Align to baseline - } - - &__detailsLabel { - color: $primary; - } - - &__explanationTitle { - margin-top: $grid-base-three; - font-weight: $font-weight-bold; - } - - &__explanationDescription { - margin-top: $grid-base * 1.5; - white-space: pre-wrap; - } - - &__explanationDescriptionPart { - margin-top: $grid-base * 0.5; - } -} diff --git a/src/extensions/components/registration/Registration.tsx b/src/extensions/components/registration/Registration.tsx index e75ee0d13..b28bc4819 100644 --- a/src/extensions/components/registration/Registration.tsx +++ b/src/extensions/components/registration/Registration.tsx @@ -1,24 +1,30 @@ import { Typography, Link, Button, Box } from '@mui/material'; import * as React from 'react'; -import { useState, useEffect, useContext, useCallback } from 'react'; +import { useState, useEffect, useContext, useCallback, useMemo } from 'react'; +import { + Route, + Switch, + useHistory, + useLocation, + useParams, + useRouteMatch, + generatePath, + Link as RouterLink +} from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Helmet } from 'react-helmet'; + import { StageLayout } from '../../../components/stageLayout/StageLayout'; import useIsFirstVisit from '../../../utils/useIsFirstVisit'; import { ReactComponent as HelloBannerIcon } from '../../../resources/img/illustrations/hello-banner.svg'; import { StepBar } from './stepBar/StepBar'; -import { AccountData } from './accountData/AccountData'; -import { ZipcodeInput } from './zipcodeInput/ZipcodeInput'; -import { AgencySelection } from './agencySelection/AgencySelection'; -import { TopicSelection } from './topicSelection/TopicSelection'; -import { useHistory, useLocation } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { Link as RouterLink } from 'react-router-dom'; import { WelcomeScreen } from './welcomeScreen/WelcomeScreen'; import { RegistrationContext, TenantContext, - registrationSessionStorageKey + registrationSessionStorageKey, + RegistrationData } from '../../../globalState'; -import { Helmet } from 'react-helmet'; import { GlobalComponentContext } from '../../../globalState/provider/GlobalComponentContext'; import { OVERLAY_FUNCTIONS, @@ -32,29 +38,34 @@ import { endpoints } from '../../../resources/scripts/endpoints'; import { apiPostRegistration } from '../../../api'; import { useAppConfig } from '../../../hooks/useAppConfig'; import { REGISTRATION_DATA_VALIDATION } from './registrationDataValidation'; +import { UrlParamsContext } from '../../../globalState/provider/UrlParamsProvider'; export const Registration = () => { const { t } = useTranslation(['common', 'consultingTypes', 'agencies']); const settings = useAppConfig(); const isFirstVisit = useIsFirstVisit(); + const location = useLocation(); + const history = useHistory(); + const { path } = useRouteMatch(); + const { step, topicSlug } = useParams<{ + step: string; + topicSlug: string; + }>(); + const { Stage } = useContext(GlobalComponentContext); const { disabledNextButton, - setDisabledNextButton, - updateSessionStorageWithPreppedData, - refreshSessionStorageRegistrationData, - sessionStorageRegistrationData, - availableSteps, - dataPrepForSessionStorage, - consultant, - isConsultantLink + updateRegistrationData, + registrationData, + availableSteps } = useContext(RegistrationContext); + const { consultant: preselectedConsultant } = useContext(UrlParamsContext); const { tenant } = useContext(TenantContext); - const [currentStep, setCurrentStep] = useState(1); + + const [stepData, setStepData] = useState>({}); const [redirectOverlayActive, setRedirectOverlayActive] = useState(false); - const location = useLocation(); - const history = useHistory(); + const handleOverlayAction = (buttonFunction: string) => { if (buttonFunction === OVERLAY_FUNCTIONS.REDIRECT_WITH_BLUR) { redirectToApp(); @@ -74,27 +85,14 @@ export const Registration = () => { ] }; - const updateCurrentStep = () => { - const currentLocation = location?.pathname?.replace( - '/registration', - '' - ); - const step = availableSteps.findIndex( - (step) => step?.urlSuffix === currentLocation - ); - setCurrentStep(step === -1 ? 0 : step); - }; - - const checkForStepsWithMissingMandatoryFields = (): number[] => { - if (currentStep > 0) { + const checkForStepsWithMissingMandatoryFields = + useCallback((): number[] => { return availableSteps.reduce( (missingSteps, step, currentIndex) => { if ( step?.mandatoryFields?.some( (mandatoryField) => - sessionStorageRegistrationData?.[ - mandatoryField - ] === undefined + registrationData?.[mandatoryField] === undefined ) ) { return [...missingSteps, currentIndex]; @@ -103,62 +101,61 @@ export const Registration = () => { }, [] ); - } - return []; - }; + }, [availableSteps, registrationData]); - const onNextClick = () => { - updateSessionStorageWithPreppedData(); - }; + const onNextClick = useCallback(() => { + updateRegistrationData(stepData); + setStepData({}); + }, [updateRegistrationData, stepData]); - useEffect(() => { - setDisabledNextButton(true); - updateCurrentStep(); - refreshSessionStorageRegistrationData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location]); + const currStepIndex = useMemo( + () => availableSteps.findIndex(({ name }) => name === step), + [availableSteps, step] + ); useEffect(() => { // Check if mandatory fields from previous steps are missing const missingPreviousSteps = checkForStepsWithMissingMandatoryFields() .sort() - .filter((missingStep) => missingStep < currentStep); + .filter((missingStep) => missingStep < currStepIndex); if (missingPreviousSteps.length > 0) { history.push( - `/registration${ - availableSteps[missingPreviousSteps[0]]?.urlSuffix - }${location.search}` + `${generatePath(path, { topicSlug, step: availableSteps[missingPreviousSteps[0]]?.name })}${location.search}` ); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentStep, availableSteps, sessionStorageRegistrationData]); + }, [ + availableSteps, + checkForStepsWithMissingMandatoryFields, + history, + location.search, + currStepIndex, + path, + topicSlug + ]); const onRegisterClick = useCallback(() => { - const registrationData = { - ...sessionStorageRegistrationData, - ...dataPrepForSessionStorage, - agencyId: sessionStorageRegistrationData.agencyId.toString(), - postcode: sessionStorageRegistrationData.zipcode, + const data = { + ...registrationData, + ...stepData, + agencyId: registrationData.agency.id.toString(), + postcode: registrationData.zipcode, termsAccepted: 'true', preferredLanguage: 'de', - // ConsultingType and mainTopicId are identical for the MVP - consultingType: sessionStorageRegistrationData.mainTopicId, - ...(isConsultantLink - ? { consultantId: consultant?.consultantId } + consultingType: registrationData.agency.consultingType, + ...(preselectedConsultant + ? { consultantId: preselectedConsultant?.consultantId } : {}) }; if ( Object.keys(REGISTRATION_DATA_VALIDATION).every((item) => - REGISTRATION_DATA_VALIDATION[item].validation( - registrationData[item] - ) + REGISTRATION_DATA_VALIDATION[item].validation(data[item]) ) ) { apiPostRegistration( endpoints.registerAsker, - registrationData, + data, settings.multitenancyWithSingleDomainEnabled, tenant ).then(() => { @@ -167,14 +164,35 @@ export const Registration = () => { }); } }, [ - consultant?.consultantId, - dataPrepForSessionStorage, - isConsultantLink, - sessionStorageRegistrationData, + registrationData, + stepData, + preselectedConsultant, settings.multitenancyWithSingleDomainEnabled, tenant ]); + const [prevStepUrl, nextStepUrl] = useMemo( + () => [ + availableSteps[currStepIndex - 1] + ? `${generatePath(path, { topicSlug, step: availableSteps[currStepIndex - 1]?.name || 'welcome' })}${location.search}` + : null, + availableSteps[currStepIndex + 1] + ? `${generatePath(path, { topicSlug, step: availableSteps[currStepIndex + 1]?.name || 'welcome' })}${location.search}` + : null + ], + [availableSteps, currStepIndex, path, topicSlug, location.search] + ); + + const stepPaths = useMemo( + () => + availableSteps.reduce( + (acc, { name }) => + acc.concat(generatePath(path, { topicSlug, step: name })), + [] + ), + [availableSteps, path, topicSlug] + ); + return ( <> { width: '100%' }} > - {availableSteps[currentStep]?.component === 'welcome' ? ( - - ) : ( - <> + + @@ -210,125 +222,105 @@ export const Registration = () => { {t('registration.headline')}
- {} + - {availableSteps[currentStep]?.component === - 'topicSelection' && ( - - )} - {availableSteps[currentStep]?.component === - 'zipcode' && } - {availableSteps[currentStep]?.component === - 'agencySelection' && ( - - )} - {availableSteps[currentStep]?.component === - 'accountData' && } - - {availableSteps[currentStep]?.component !== - 'welcome' && ( + + {availableSteps.map( + ({ name, component: Component }) => ( + + + + ) + )} + + - - {currentStep > 0 && ( - - {t('registration.back')} - - )} - {currentStep === - availableSteps.length - 1 ? ( - - ) : ( - - )} - + {t('registration.back')} + + + {!nextStepUrl ? ( + + ) : ( + + )} - )} + - - )} + + + + + {redirectOverlayActive && ( diff --git a/src/extensions/components/registration/accountData/AccountData.tsx b/src/extensions/components/registration/accountData/AccountData.tsx index cf76ecaef..6544aa7cc 100644 --- a/src/extensions/components/registration/accountData/AccountData.tsx +++ b/src/extensions/components/registration/accountData/AccountData.tsx @@ -7,7 +7,14 @@ import { FormControlLabel } from '@mui/material'; import * as React from 'react'; -import { useState, useContext, useEffect } from 'react'; +import { + useState, + useContext, + useEffect, + VFC, + Dispatch, + SetStateAction +} from 'react'; import { useTranslation } from 'react-i18next'; import PersonIcon from '@mui/icons-material/Person'; import LockIcon from '@mui/icons-material/Lock'; @@ -19,7 +26,7 @@ import { hasSpecialChar } from '../../../../utils/validateInputValue'; import { LegalLinksContext } from '../../../../globalState/provider/LegalLinksProvider'; -import { RegistrationContext } from '../../../../globalState'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { apiGetIsUsernameAvailable } from '../../../../api/apiGetIsUsernameAvailable'; import { REGISTRATION_DATA_VALIDATION } from '../registrationDataValidation'; import LegalLinks from '../../../../components/legalLinks/LegalLinks'; @@ -43,7 +50,9 @@ export const passwordCriteria = [ } ]; -export const AccountData = () => { +export const AccountData: VFC<{ + onChange: Dispatch>>; +}> = ({ onChange }) => { const legalLinks = useContext(LegalLinksContext); const { t } = useTranslation(); const [password, setPassword] = useState(''); @@ -56,8 +65,7 @@ export const AccountData = () => { const [username, setUsername] = useState(''); const [isUsernameAvailable, setIsUsernameAvailable] = useState(true); - const { setDisabledNextButton, setDataForSessionStorage } = - useContext(RegistrationContext); + const { setDisabledNextButton } = useContext(RegistrationContext); useEffect(() => { if ( @@ -68,7 +76,7 @@ export const AccountData = () => { dataProtectionChecked ) { setDisabledNextButton(false); - setDataForSessionStorage({ username, password }); + onChange({ username, password }); } else { setDisabledNextButton(true); } diff --git a/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx b/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx index 32d6d9f15..b226aae42 100644 --- a/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx +++ b/src/extensions/components/registration/agencySelection/AgencyLanguages.tsx @@ -20,9 +20,10 @@ export const AgencyLanguages = ({ agencyId }: AgencyLanguagesProps) => { const allLanguages = [...fixedLanguages, ...res.languages]; setLanguages( allLanguages - .filter((element, index) => { - return allLanguages.indexOf(element) === index; - }) + .filter( + (element, index) => + allLanguages.indexOf(element) === index + ) .sort() ); }); diff --git a/src/extensions/components/registration/agencySelection/AgencySelection.tsx b/src/extensions/components/registration/agencySelection/AgencySelection.tsx index ca9e6b66f..48a4954c4 100644 --- a/src/extensions/components/registration/agencySelection/AgencySelection.tsx +++ b/src/extensions/components/registration/agencySelection/AgencySelection.tsx @@ -1,88 +1,56 @@ import * as React from 'react'; -import { useState, useEffect, VFC, useContext } from 'react'; +import { + useState, + useEffect, + VFC, + useContext, + Dispatch, + SetStateAction +} from 'react'; import { AgencySelectionResults } from './AgencySelectionResults'; -import { apiAgencySelection } from '../../../../api'; -import { RegistrationContext } from '../../../../globalState'; -import { AgencyDataInterface } from '../../../../globalState/interfaces'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; +import { useAgenciesForRegistration } from '../../../../containers/registration/hooks/useAgenciesForRegistration'; export const AgencySelection: VFC<{ + onChange: Dispatch>>; nextStepUrl: string; onNextClick(): void; -}> = ({ nextStepUrl, onNextClick }) => { - const { - sessionStorageRegistrationData, - isConsultantLink, - consultant, - setDataForSessionStorage - } = useContext(RegistrationContext); - +}> = ({ nextStepUrl, onNextClick, onChange }) => { const { t } = useTranslation(); + const { registrationData } = useContext(RegistrationContext); + const { consultant: preselectedConsultant } = useContext(UrlParamsContext); + + const { isLoading, agencies } = useAgenciesForRegistration({ + topic: registrationData.mainTopic, + postcode: registrationData?.zipcode + }); + const [headlineZipcode, setHeadlineZipcode] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [results, setResults] = useState( - undefined - ); - useEffect(() => { - if (isConsultantLink) { - setResults( - consultant?.agencies?.filter((agency) => - agency?.topicIds?.includes( - sessionStorageRegistrationData?.mainTopicId - ) - ) - ); - } else if (sessionStorageRegistrationData?.zipcode?.length === 5) { - setHeadlineZipcode(sessionStorageRegistrationData.zipcode); - setResults(undefined); - (async () => { - setIsLoading(true); - try { - const agencyResponse = await apiAgencySelection({ - postcode: sessionStorageRegistrationData.zipcode, - // We will keep consultingTypeId identical to mainTopicId - consultingType: - sessionStorageRegistrationData.mainTopicId || - undefined, - topicId: - sessionStorageRegistrationData.mainTopicId || - undefined - }); - setResults(agencyResponse); - if ( - agencyResponse.every( - (agency) => - agency.id !== - sessionStorageRegistrationData.agencyId - ) - ) { - setDataForSessionStorage({ agencyId: undefined }); - } - } catch { - setResults([]); - } - setIsLoading(false); - })(); + useEffect(() => { + if (!preselectedConsultant && registrationData?.zipcode?.length === 5) { + setHeadlineZipcode(registrationData.zipcode); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessionStorageRegistrationData, consultant]); + }, [registrationData, preselectedConsultant]); return ( <> - {(isConsultantLink && consultant?.agencies?.length === 1) || - results?.length === 1 + {preselectedConsultant?.agencies?.length === 1 || + agencies?.length === 1 ? t('registration.agency.consultantheadline') : t('registration.agency.headline')} ); diff --git a/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx b/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx index 2ae005206..ff975953d 100644 --- a/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx +++ b/src/extensions/components/registration/agencySelection/AgencySelectionResults.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { Typography, FormControlLabel, @@ -8,15 +9,20 @@ import { Button, Link } from '@mui/material'; -import * as React from 'react'; -import { useContext, useEffect, useState } from 'react'; +import { + Dispatch, + SetStateAction, + useContext, + useEffect, + useState +} from 'react'; import TaskAltIcon from '@mui/icons-material/TaskAlt'; import NoResultsIllustration from '../../../../resources/img/illustrations/no-results.svg'; import ConsultantIllustration from '../../../../resources/img/illustrations/consultant-found.svg'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import { Loading } from '../../../../components/app/Loading'; import { useTranslation } from 'react-i18next'; -import { RegistrationContext } from '../../../../globalState'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { AgencyDataInterface } from '../../../../globalState/interfaces'; import { AgencyLanguages } from './AgencyLanguages'; import { parsePlaceholderString } from '../../../../utils/parsePlaceholderString'; @@ -24,8 +30,10 @@ import { useAppConfig } from '../../../../hooks/useAppConfig'; import { VFC } from 'react'; import { MetaInfo } from '../metaInfo/MetaInfo'; import { REGISTRATION_DATA_VALIDATION } from '../registrationDataValidation'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; interface AgencySelectionResultsProps { + onChange: Dispatch>>; isLoading?: boolean; zipcode?: string; results?: AgencyDataInterface[]; @@ -34,6 +42,7 @@ interface AgencySelectionResultsProps { } export const AgencySelectionResults: VFC = ({ + onChange, isLoading, zipcode, results, @@ -42,15 +51,12 @@ export const AgencySelectionResults: VFC = ({ }) => { const { t } = useTranslation(); const settings = useAppConfig(); - const { - setDisabledNextButton, - setDataForSessionStorage, - sessionStorageRegistrationData, - isConsultantLink - } = useContext(RegistrationContext); + const { setDisabledNextButton, registrationData } = + useContext(RegistrationContext); + const { consultant: preselectedConsultant } = useContext(UrlParamsContext); - const [agencyId, setAgencyId] = useState( - sessionStorageRegistrationData?.agencyId + const [selectedAgency, setSelectedAgency] = useState( + registrationData?.agency ); useEffect(() => { @@ -59,10 +65,10 @@ export const AgencySelectionResults: VFC = ({ results?.length > 0 && results?.every((agency) => agency.external) ) { - setAgencyId(undefined); + setSelectedAgency(undefined); setDisabledNextButton(true); - setDataForSessionStorage({ - agencyId: undefined + onChange({ + agency: undefined }); return; } @@ -71,43 +77,36 @@ export const AgencySelectionResults: VFC = ({ results?.length === 1 && results?.every((agency) => !agency.external) ) { - setAgencyId(results[0].id); + setSelectedAgency(results[0]); setDisabledNextButton(false); - setDataForSessionStorage({ - agencyId: results[0].id + onChange({ + agency: results[0] }); return; } + if ( // invalid agencyId, needs to be removed - agencyId && + selectedAgency && results?.length === 0 ) { setDisabledNextButton(true); - setDataForSessionStorage({ - agencyId: undefined + onChange({ + agency: undefined }); return; } + if ( // valid agencyId REGISTRATION_DATA_VALIDATION.agencyId.validation( - agencyId?.toString() + selectedAgency?.id?.toString() ) ) { setDisabledNextButton(false); - setDataForSessionStorage({ - agencyId: agencyId - }); - return; + onChange({ agency: selectedAgency }); } - }, [ - agencyId, - results, - setDataForSessionStorage, - setDisabledNextButton, - zipcode - ]); + }, [selectedAgency, results, onChange, setDisabledNextButton, zipcode]); return ( <> @@ -123,7 +122,7 @@ export const AgencySelectionResults: VFC = ({ )} - {!!results && !isLoading && !isConsultantLink && ( + {!!results && !isLoading && !preselectedConsultant && ( {t('registration.agency.result.headline') + ' ' + zipcode}: @@ -244,9 +243,7 @@ export const AgencySelectionResults: VFC = ({ )} {/* one Result */} {results?.length === 1 && - !results?.every((agency) => { - return agency.external; - }) && ( + !results?.every((agency) => agency.external) && ( = ({ headline={results?.[0].name} description={results?.[0].description} onOverlayClose={() => - setAgencyId(undefined) + setSelectedAgency(undefined) } backButtonLabel={t( 'registration.agency.infoOverlay.backButtonLabel' @@ -312,10 +309,10 @@ export const AgencySelectionResults: VFC = ({ nextStepUrl={nextStepUrl} onNextClick={onNextClick} onOverlayOpen={() => { - setDataForSessionStorage({ - agencyId: results?.[0].id + onChange({ + agency: results?.[0] }); - setAgencyId(results?.[0].id); + setSelectedAgency(results?.[0]); }} /> )} @@ -333,6 +330,7 @@ export const AgencySelectionResults: VFC = ({ > {results?.map((agency, index) => ( = ({ { setDisabledNextButton(false); - setAgencyId(agency.id); - setDataForSessionStorage({ - agencyId: agency.id - }); + setSelectedAgency(agency); + onChange({ agency }); }} sx={{ alignItems: 'flex-start' @@ -354,7 +350,10 @@ export const AgencySelectionResults: VFC = ({ value={agency.id} control={ } label={ @@ -390,7 +389,7 @@ export const AgencySelectionResults: VFC = ({ headline={agency.name} description={agency.description} onOverlayClose={() => - setAgencyId(undefined) + setSelectedAgency(undefined) } backButtonLabel={t( 'registration.agency.infoOverlay.backButtonLabel' @@ -401,10 +400,8 @@ export const AgencySelectionResults: VFC = ({ nextStepUrl={nextStepUrl} onNextClick={onNextClick} onOverlayOpen={() => { - setDataForSessionStorage({ - agencyId: agency.id - }); - setAgencyId(agency.id); + onChange({ agency }); + setSelectedAgency(agency); }} /> )} diff --git a/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx b/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx index d1c65e786..986a2863a 100644 --- a/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx +++ b/src/extensions/components/registration/agencySelection/agencySelection.cy.tsx @@ -15,10 +15,10 @@ it('Get results for zipcode', () => { {}, - setDataForSessionStorage: () => {}, - isConsultantLink: false, - consultant: null, - sessionStorageRegistrationData: { + updateRegistrationData: () => {}, + registrationData: { + agency: null, + mainTopic: null, zipcode: '12345', username: null, agencyId: null, @@ -27,7 +27,11 @@ it('Get results for zipcode', () => { } }} > - {}} /> + {}} + onChange={() => {}} + /> ); diff --git a/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx b/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx index c10a35e00..dc16ee96c 100644 --- a/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx +++ b/src/extensions/components/registration/infoDrawer/InfoDrawer.tsx @@ -1,31 +1,28 @@ import * as React from 'react'; -import { useState, useContext, useEffect } from 'react'; +import { useState, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import { Box, SwipeableDrawer, Typography } from '@mui/material'; import { RegistrationContext } from '../../../../globalState'; import { PreselectionError } from '../preselectionError/PreselectionError'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; interface InfoDrawerProps { trigger?: boolean; } export const InfoDrawer = ({ trigger }: InfoDrawerProps) => { + const { hasAgencyError, hasConsultantError, hasTopicError } = + useContext(RegistrationContext); const { - preselectedAgency, - preselectedTopicName, - hasAgencyError, - hasConsultantError, - hasTopicError, - isConsultantLink, - preselectedData - } = useContext(RegistrationContext); + topic: preselectedTopic, + agency: preselectedAgency, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); const { t } = useTranslation(); const drawerBleeding = 92; const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const [topicName, setTopicName] = useState('-'); - const [agencyName, setAgencyName] = useState('-'); const toggleDrawer = (event: React.KeyboardEvent | React.MouseEvent) => { if ( @@ -39,157 +36,142 @@ export const InfoDrawer = ({ trigger }: InfoDrawerProps) => { setIsDrawerOpen(!isDrawerOpen); }; - useEffect(() => { - if (preselectedTopicName) { - setTopicName(preselectedTopicName); - } - if (preselectedAgency) { - setAgencyName(preselectedAgency.name); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [preselectedAgency, preselectedTopicName]); - - if ( - !(preselectedData.includes('tid') || preselectedData.includes('aid')) && - !isConsultantLink - ) { + if (!preselectedTopic && !preselectedAgency && !preselectedConsultant) { return null; } return ( - <> - .MuiPaper-root': { - top: -drawerBleeding, - overflow: 'visible' - }, - 'top': 0 + .MuiPaper-root': { + top: -drawerBleeding, + overflow: 'visible' + }, + 'top': 0 + }} + anchor="top" + onClose={() => setIsDrawerOpen(false)} + onOpen={() => setIsDrawerOpen(true)} + open={isDrawerOpen} + ModalProps={{ + keepMounted: true + }} + > + { + if (e.key === 'Enter') { + toggleDrawer(e); + } }} - anchor="top" - onClose={() => setIsDrawerOpen(false)} - onOpen={() => setIsDrawerOpen(true)} - open={isDrawerOpen} - ModalProps={{ - keepMounted: true + tabIndex={0} + sx={{ + 'px': '16px', + 'pt': '16px', + 'mt': trigger ? 0 : '48px', + 'position': 'relative', + 'borderBottomLeftRadius': 8, + 'borderBottomRightRadius': 8, + 'visibility': 'visible', + 'right': 0, + 'left': 0, + 'width': '100vw', + 'backgroundColor': 'primary.main', + 'color': 'white', + 'animationName': 'slideIn', + 'animationDuration': '0.8s', + 'animationDelay': '0.3s', + 'animationFillMode': 'forwards', + '@keyframes slideIn': { + '0%': { + top: 0 + }, + '100%': { + top: drawerBleeding + } + } }} > { - if (e.key === 'Enter') { - toggleDrawer(e); - } - }} - tabIndex={0} sx={{ - 'px': '16px', - 'pt': '16px', - 'mt': trigger ? 0 : '48px', - 'position': 'relative', - 'borderBottomLeftRadius': 8, - 'borderBottomRightRadius': 8, - 'visibility': 'visible', - 'right': 0, - 'left': 0, - 'width': '100vw', - 'backgroundColor': 'primary.main', - 'color': 'white', - 'animationName': 'slideIn', - 'animationDuration': '0.8s', - 'animationDelay': '0.3s', - 'animationFillMode': 'forwards', - '@keyframes slideIn': { - '0%': { - top: 0 - }, - '100%': { - top: drawerBleeding - } - } + opacity: isDrawerOpen ? 1 : 0, + overflow: 'scroll', + maxHeight: isDrawerOpen ? '75vh' : 0 }} > - - {hasConsultantError ? ( - - ) : ( - <> - - {t('registration.topic.summary')} - - {hasTopicError && topicName === '-' ? ( - - ) : ( + {hasConsultantError ? ( + + ) : ( + <> + + {t('registration.topic.summary')} + + {hasTopicError && !preselectedTopic ? ( + + ) : ( + preselectedTopic && ( - {topicName} + {preselectedTopic.name} - )} - - {t('registration.agency.summary')} - - {hasAgencyError && agencyName === '-' ? ( - - ) : ( + ) + )} + + {t('registration.agency.summary')} + + {hasAgencyError && !preselectedAgency ? ( + + ) : ( + preselectedAgency && ( - {agencyName} + {preselectedAgency.name} - )} - - )} - - - {isDrawerOpen ? ( - - ) : ( - - )} - + ) + )} + + )} + + + {isDrawerOpen ? ( + + ) : ( + + )} - - + + ); }; diff --git a/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx b/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx index c74a7b7fd..5fd8f208c 100644 --- a/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx +++ b/src/extensions/components/registration/preselectionBox/PreselectionBox.tsx @@ -1,47 +1,27 @@ import * as React from 'react'; -import { useContext, useState, useEffect, VFC } from 'react'; +import { useContext, VFC } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, Typography } from '@mui/material'; import { RegistrationContext } from '../../../../globalState'; import { PreselectionDrawer } from '../preselectionDrawer/preselectionDrawer'; import ReportProblemIcon from '@mui/icons-material/ReportProblem'; import { useResponsive } from '../../../../hooks/useResponsive'; +import { UrlParamsContext } from '../../../../globalState/provider/UrlParamsProvider'; export const PreselectionBox: VFC<{ hasDrawer?: boolean; }> = ({ hasDrawer = false }) => { + const { hasConsultantError, hasTopicError, hasAgencyError } = + useContext(RegistrationContext); const { - preselectedAgency, - preselectedTopicName, - isConsultantLink, - hasConsultantError, - hasTopicError, - hasAgencyError, - preselectedData - } = useContext(RegistrationContext); + agency: preselectedAgency, + topic: preselectedTopic, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); const { t } = useTranslation(); - const [topicName, setTopicName] = useState('-'); - const [agencyName, setAgencyName] = useState('-'); const { fromM } = useResponsive(); - useEffect(() => { - if (preselectedTopicName) { - setTopicName(preselectedTopicName); - } else { - setTopicName('-'); - } - if (preselectedAgency) { - setAgencyName(preselectedAgency?.name); - } else { - setAgencyName('-'); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [preselectedAgency, preselectedTopicName]); - - if ( - !(preselectedData.includes('tid') || preselectedData.includes('aid')) && - (!isConsultantLink || !hasDrawer) - ) { + if (!preselectedTopic && !preselectedAgency && !preselectedConsultant) { return null; } @@ -59,7 +39,7 @@ export const PreselectionBox: VFC<{ border: '1px solid #c6c5c4' }} > - {isConsultantLink && + {preselectedConsultant && (hasConsultantError ? ( ))} - {(preselectedTopicName || hasTopicError) && - !isConsultantLink && ( + {(preselectedTopic || hasTopicError) && + !preselectedConsultant && ( <> {t('registration.topic.summary')} - {hasTopicError && !preselectedTopicName ? ( + {hasTopicError && !preselectedTopic ? ( @@ -113,47 +93,50 @@ export const PreselectionBox: VFC<{ : '0px' }} > - {preselectedTopicName} + {preselectedTopic.name} )} )} - {(preselectedAgency || hasAgencyError) && !isConsultantLink && ( - <> - - {t('registration.agency.summary')} - - {hasAgencyError && !preselectedAgency ? ( - - <> - - {!loading && ( + {!isloading && ( - {isConsultantLink ? ( + {preselectedConsultant ? ( hasConsultantError ? ( >>; nextStepUrl: string; onNextClick(): void; -}> = ({ nextStepUrl, onNextClick }) => { +}> = ({ onChange, nextStepUrl, onNextClick }) => { + const { setDisabledNextButton, registrationData } = + useContext(RegistrationContext); const { - setDisabledNextButton, - setDataForSessionStorage, - sessionStorageRegistrationData, - preselectedData, - preselectedAgency, - isConsultantLink, - consultant, - hasAgencyError - } = useContext(RegistrationContext); + topic: preselectedTopic, + agency: preselectedAgency, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); const { t } = useTranslation(); const [value, setValue] = useState( - sessionStorageRegistrationData.mainTopicId || undefined + registrationData.mainTopicId || undefined ); - const [topicGroups, setTopicGroups] = useState([]); - const [topics, setTopics] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [topicGroups, setTopicGroups] = useState(); + const [topics, setTopics] = useState(); const [listView, setListView] = useState(false); const [topicGroupId, setTopicGroupId] = useState( - sessionStorageRegistrationData.topicGroupId || undefined + registrationData.topicGroupId || undefined ); - const getTopic = (mainTopicId: number) => { - return topics?.find((topic) => topic?.id === mainTopicId); - }; + const getTopic = useCallback( + (mainTopicId: number) => + topics?.find((topic) => topic?.id === mainTopicId), + [topics] + ); useEffect(() => { if ( REGISTRATION_DATA_VALIDATION.mainTopicId.validation( value?.toString() ) && - (topicGroups.some((topicGroup) => + (topicGroups?.some((topicGroup) => topicGroup.topicIds.includes(value) ) || - (listView && topics.some((topic) => topic.id === value))) + (listView && topics?.some((topic) => topic.id === value))) ) { setDisabledNextButton(false); } }, [setDisabledNextButton, value, topicGroups, listView, topics]); useEffect(() => { - if ( - (preselectedData.includes('aid') && !hasAgencyError) || - (isConsultantLink && consultant) - ) { - setListView(true); - } else { - setListView(false); - } - }, [consultant, hasAgencyError, isConsultantLink, preselectedData]); + setListView(!!(preselectedAgency || preselectedConsultant)); + }, [preselectedConsultant, preselectedAgency]); useEffect(() => { - if (topics.length === 1) { + if (topics?.length === 1) { setValue(topics[0].id); - setDataForSessionStorage({ - mainTopicId: topics[0].id + onChange({ + mainTopic: topics[0] }); } - }, [setDataForSessionStorage, topics]); + }, [topics, onChange]); useEffect(() => { - const getFilteredTopics = (topics: TopicsDataInterface[]) => { - if (preselectedData.includes('aid') && !hasAgencyError) { - const topicIds = preselectedAgency?.topicIds; - return topics?.filter((topic) => topicIds.includes(topic.id)); - } - if (isConsultantLink && consultant) { - const topicIds = consultant?.agencies - .map((agency) => agency.topicIds) - .flat(); - return topics?.filter((topic) => topicIds.includes(topic.id)); - } - return topics; - }; + const getFilteredTopics = (topics: TopicsDataInterface[]) => + topics + // Filter topic by preselected topic + .filter( + (t) => !preselectedTopic || t.id === preselectedTopic?.id + ) + // Filter topics by consultant topics + .filter( + (t) => + !preselectedConsultant || + preselectedConsultant.agencies.some((a) => + a.topicIds?.includes(t.id) + ) + ) + // Filter topics by preselected agency + .filter( + (t) => + !preselectedAgency || + preselectedAgency.topicIds?.includes(t.id) + ); (async () => { + setTopics(undefined); + setTopicGroups(undefined); + + const topicsResponse = await apiGetTopicsData(); + const topics = getFilteredTopics(topicsResponse); try { - setIsLoading(true); + const topicIds = topics.map((t) => t.id); const topicGroupsResponse = await apiGetTopicGroups(); - const topicsResponse = await apiGetTopicsData(); - setTopics(getFilteredTopics(topicsResponse)); setTopicGroups( topicGroupsResponse.data.items .filter((topicGroup) => topicGroup.topicIds.length > 0) + .filter((topicGroup) => + topicGroup.topicIds.some((tid) => + topicIds.includes(tid) + ) + ) .sort((a, b) => { if (a.name === b.name) return 0; return a.name < b.name ? -1 : 1; }) ); - setIsLoading(false); } catch { - setTopics([]); setTopicGroups([]); + setListView(true); } + + setTopics(topics); })(); - }, [ - consultant, - hasAgencyError, - isConsultantLink, - preselectedAgency, - preselectedData - ]); + }, [preselectedConsultant, preselectedAgency, preselectedTopic]); return ( <> - {topics.length === 1 ? ( + {topics?.length === 1 ? ( {t('registration.topic.oneResult')} @@ -148,7 +159,7 @@ export const TopicSelection: VFC<{ )} - {isLoading ? ( + {topics === undefined || topicGroups === undefined ? ( - {topicGroups && topics && listView - ? topics + {listView + ? (topics || []) .sort((a, b) => { if (a.name === b.name) return 0; return a.name < b.name ? -1 : 1; }) .map((topic, index) => ( - + setValue(undefined) + } + onOverlayOpen={() => + setValue(topic.id) + } + onChange={() => { + setValue(topic.id); + onChange({ + mainTopic: topic + }); }} - > - { - setValue(topic.id); - setDataForSessionStorage( - { - mainTopicId: - topic?.id - } - ); - }} - checked={ - value === topic?.id - } - checkedIcon={ - topics.length === - 1 ? ( - - ) : undefined - } - icon={ - topics.length === - 1 ? ( - - ) : undefined - } - /> - } - label={ - - - {topic?.name} - - - } - /> - {topic?.description && ( - - setValue(undefined) - } - backButtonLabel={t( - 'registration.topic.infoOverlay.backButtonLabel' - )} - nextButtonLabel={t( - 'registration.topic.infoOverlay.nextButtonLabel' - )} - nextStepUrl={nextStepUrl} - onNextClick={onNextClick} - onOverlayOpen={() => { - setDataForSessionStorage( - { - mainTopicId: - topic.id - } - ); - setValue(topic.id); - }} - /> - )} - + /> )) - : topicGroups.map((topicGroup) => ( + : (topicGroups || []).map((topicGroup) => ( {topicGroup.topicIds .map((t) => getTopic(t)) + .filter(Boolean) .sort((a, b) => { if (a.name === b.name) return 0; @@ -324,109 +274,48 @@ export const TopicSelection: VFC<{ : 1; }) .map((topic, index) => ( - { + setValue(undefined); + setTopicGroupId( + undefined + ); + }} + onOverlayOpen={() => { + setValue(topic.id); + setTopicGroupId( + topicGroup.id + ); }} - > - { - setValue( - topic.id - ); - setTopicGroupId( - topicGroup.id - ); - setDataForSessionStorage( - { - mainTopicId: - topic?.id, - topicGroupId: - topicGroup?.id - } - ); - }} - checked={ - value === - topic?.id && - topicGroup.id === - topicGroupId - } - /> - } - label={ - - - { - topic?.name - } - - - } - /> - {topic.description && ( - - setValue( - undefined - ) - } - backButtonLabel={t( - 'registration.topic.infoOverlay.backButtonLabel' - )} - nextButtonLabel={t( - 'registration.topic.infoOverlay.nextButtonLabel' - )} - nextStepUrl={ - nextStepUrl - } - onNextClick={ - onNextClick - } - onOverlayOpen={() => { - setDataForSessionStorage( - { - mainTopicId: - topic.id, - topicGroupId: - topicGroup.id - } - ); - setValue( - topic.id - ); - setTopicGroupId( - topicGroup.id - ); - }} - /> - )} - + checked={ + value === + topic.id && + topicGroup.id === + topicGroupId + } + onChange={() => { + setValue(topic.id); + setTopicGroupId( + topicGroup.id + ); + onChange({ + mainTopic: + topic, + topicGroupId: + topicGroup?.id + }); + }} + /> ))} @@ -437,3 +326,79 @@ export const TopicSelection: VFC<{ ); }; + +const TopicSelect = ({ + topics, + topic, + index, + nextStepUrl, + onNextClick, + onChange, + onOverlayClose, + onOverlayOpen, + checked +}) => { + const { t } = useTranslation(); + + return ( + + + ) : undefined + } + icon={ + topics.length === 1 ? ( + + ) : undefined + } + /> + } + label={ + + {topic?.name} + + } + /> + {topic?.description && ( + onOverlayClose(topic)} + onOverlayOpen={() => onOverlayOpen(topic)} + /> + )} + + ); +}; diff --git a/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx b/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx index eb6eaded4..87de83801 100644 --- a/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx +++ b/src/extensions/components/registration/topicSelection/topicSelection.cy.tsx @@ -16,7 +16,11 @@ it('Get accordion content', () => { cy.mount( - {}} /> + {}} + onChange={() => {}} + /> ); diff --git a/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx b/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx index 146382005..61ce5fa81 100644 --- a/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx +++ b/src/extensions/components/registration/welcomeScreen/WelcomeScreen.tsx @@ -5,17 +5,16 @@ import CreateIcon from '@mui/icons-material/Create'; import ChatIcon from '@mui/icons-material/Chat'; import MailIcon from '@mui/icons-material/Mail'; import LockIcon from '@mui/icons-material/Lock'; -import { Link as RouterLink, useLocation } from 'react-router-dom'; -import { useMemo, VFC } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { useMemo } from 'react'; import { PreselectionBox } from '../preselectionBox/PreselectionBox'; interface WelcomeScreenProps { nextStepUrl: string; } -export const WelcomeScreen: VFC = ({ nextStepUrl }) => { +export const WelcomeScreen = ({ nextStepUrl }: WelcomeScreenProps) => { const { t } = useTranslation(); - const { search } = useLocation(); const infoDefinitions = useMemo( () => [ @@ -73,7 +72,10 @@ export const WelcomeScreen: VFC = ({ nextStepUrl }) => { {t('registration.welcomeScreen.subline')} {infoDefinitions.map((info) => ( - + {info.icon} {info.headline} @@ -112,7 +114,7 @@ export const WelcomeScreen: VFC = ({ nextStepUrl }) => { sx={{ mt: { xs: '8px', md: '16px' } }} variant="contained" component={RouterLink} - to={`${nextStepUrl}${search}`} + to={nextStepUrl} > {t('registration.welcomeScreen.register.buttonLabel')} diff --git a/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx b/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx index 5d547e10d..95e2cea7e 100644 --- a/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx +++ b/src/extensions/components/registration/zipcodeInput/ZipcodeInput.tsx @@ -1,33 +1,35 @@ import { InputAdornment, Typography } from '@mui/material'; import * as React from 'react'; import FmdGoodIcon from '@mui/icons-material/FmdGood'; -import { useState, VFC, useContext, useEffect } from 'react'; +import { + useState, + VFC, + useContext, + useEffect, + Dispatch, + SetStateAction +} from 'react'; import { useTranslation } from 'react-i18next'; import { Input } from '../../../../components/input/input'; -import { RegistrationContext } from '../../../../globalState'; +import { RegistrationContext, RegistrationData } from '../../../../globalState'; import { REGISTRATION_DATA_VALIDATION } from '../registrationDataValidation'; -export const ZipcodeInput: VFC = () => { +export const ZipcodeInput: VFC<{ + onChange: Dispatch>>; +}> = ({ onChange }) => { const { t } = useTranslation(); - const { - setDisabledNextButton, - setDataForSessionStorage, - sessionStorageRegistrationData - } = useContext(RegistrationContext); - const [value, setValue] = useState( - sessionStorageRegistrationData.zipcode || '' - ); + const { setDisabledNextButton, registrationData } = + useContext(RegistrationContext); + const [value, setValue] = useState(registrationData.zipcode || ''); useEffect(() => { if (REGISTRATION_DATA_VALIDATION.zipcode.validation(value)) { setDisabledNextButton(false); - setDataForSessionStorage({ zipcode: value }); + onChange({ zipcode: value }); } else { setDisabledNextButton(true); } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value]); + }, [setDisabledNextButton, onChange, value]); return ( <> diff --git a/src/extensions/components/registration/zipcodeInput/zipCodeInput.cy.tsx b/src/extensions/components/registration/zipcodeInput/zipCodeInput.cy.tsx index f201f8692..4cf85d8a4 100644 --- a/src/extensions/components/registration/zipcodeInput/zipCodeInput.cy.tsx +++ b/src/extensions/components/registration/zipcodeInput/zipCodeInput.cy.tsx @@ -7,7 +7,7 @@ it('shows correct label', () => { cy.mount( - + {}} /> ); @@ -18,7 +18,7 @@ it('shows correct value', () => { cy.mount( - + {}} /> ); @@ -30,7 +30,7 @@ it('show invalid input onBlur', () => { cy.mount( - + {}} /> ); @@ -43,7 +43,7 @@ it('does not show wrong characters', () => { cy.mount( - + {}} /> ); diff --git a/src/extensions/initApp.tsx b/src/extensions/initApp.tsx index 9a56184f1..54d3e427a 100644 --- a/src/extensions/initApp.tsx +++ b/src/extensions/initApp.tsx @@ -1,36 +1,72 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { ThemeProvider } from '@mui/material'; import { App } from '../components/app/app'; import { Stage } from './components/stage/stage'; -import { ConsultingTypes } from './components/consultingTypes/ConsultingTypes'; import { Imprint } from './components/legalInformationLinks/Imprint'; import { DataProtection } from './components/legalInformationLinks/DataProtection'; import { config, legalLinks } from './resources/scripts/config'; +import { UrlParamsProvider } from '../globalState/provider/UrlParamsProvider'; +import { RegistrationProvider } from '../globalState'; +import { lazy } from 'react'; +import '../resources/styles/mui-variables-mapping.scss'; +import theme from './theme'; +import { Redirect } from 'react-router-dom'; + +const Registration = lazy(() => + import('./components/registration/Registration').then((m) => ({ + default: m.Registration + })) +); + +const NewRegistration = () => ( + + + + + +); ReactDOM.render( - + ( + + ) }, - component: Imprint - }, - { - route: { - path: legalLinks.privacy + { + route: { + path: legalLinks.imprint + }, + component: Imprint }, - component: DataProtection - } - ]} - stageComponent={Stage} - />, + { + route: { + path: legalLinks.privacy + }, + component: DataProtection + } + ]} + stageComponent={Stage} + /> + , document.getElementById('appRoot') ); diff --git a/src/globalState/interfaces/TopicsDataInterface.ts b/src/globalState/interfaces/TopicsDataInterface.ts index 87ee3e0dd..280ef9b76 100644 --- a/src/globalState/interfaces/TopicsDataInterface.ts +++ b/src/globalState/interfaces/TopicsDataInterface.ts @@ -1,6 +1,7 @@ export interface TopicsDataInterface { id: number; name: string; + slug: string; description: string; internalIdentifier: string; status: string; diff --git a/src/globalState/provider/RegistrationProvider.tsx b/src/globalState/provider/RegistrationProvider.tsx index 22fc9fc42..e714e6a7f 100644 --- a/src/globalState/provider/RegistrationProvider.tsx +++ b/src/globalState/provider/RegistrationProvider.tsx @@ -1,53 +1,31 @@ import { createContext, Dispatch, + PropsWithChildren, SetStateAction, + useCallback, + useContext, useEffect, useMemo, useState } from 'react'; import * as React from 'react'; -import { useLocation } from 'react-router-dom'; +import { AgencyDataInterface, TopicsDataInterface } from '../interfaces'; +import { UrlParamsContext } from './UrlParamsProvider'; +import { getUrlParameter } from '../../utils/getUrlParameter'; +import { apiGetTopicById } from '../../api/apiGetTopicId'; import { apiGetAgencyById } from '../../api'; -import { apiGetTopicsData } from '../../api/apiGetTopicsData'; -import { apiGetConsultant } from '../../api/apiGetConsultant'; -import { - AgencyDataInterface, - ConsultantDataInterface, - TopicsDataInterface -} from '../interfaces'; - -interface RegistrationContextInterface { - disabledNextButton?: boolean; - setDisabledNextButton?: Dispatch>; - setDataForSessionStorage?: Dispatch< - SetStateAction> - >; - sessionStorageRegistrationData?: RegistrationSessionStorageData; - dataPrepForSessionStorage?: Partial; - updateSessionStorageWithPreppedData?: () => void; - refreshSessionStorageRegistrationData?: () => void; - availableSteps?: { - component: string; - urlSuffix?: string; - mandatoryFields?: string[]; - urlParams?: string[]; - }[]; - preselectedData?: Array<'tid' | 'zipcode' | 'aid'>; - preselectedAgency?: AgencyDataInterface; - preselectedTopicName?: string; - isConsultantLink?: boolean; - consultant?: ConsultantDataInterface; - hasConsultantError?: boolean; - hasAgencyError?: boolean; - hasTopicError?: boolean; -} +import { TopicSelection } from '../../extensions/components/registration/topicSelection/TopicSelection'; +import { ZipcodeInput } from '../../extensions/components/registration/zipcodeInput/ZipcodeInput'; +import { AgencySelection } from '../../extensions/components/registration/agencySelection/AgencySelection'; +import { AccountData } from '../../extensions/components/registration/accountData/AccountData'; +import { RouteProps, useRouteMatch } from 'react-router-dom'; export const RegistrationContext = createContext( {} ); -interface RegistrationSessionStorageData { +interface SessionStorageData { username: string; password: string; agencyId: number; @@ -57,296 +35,226 @@ interface RegistrationSessionStorageData { zipcode: string; } +export interface RegistrationData extends SessionStorageData { + agency: AgencyDataInterface; + mainTopic: TopicsDataInterface; + topic?: TopicsDataInterface; +} + +interface RegistrationContextInterface { + disabledNextButton?: boolean; + setDisabledNextButton?: Dispatch>; + registrationData?: RegistrationData; + updateRegistrationData?: (data: Partial) => void; + availableSteps?: { + component: any; + route: Omit & { path: string }; + name: string; + //urlSuffix?: string; + mandatoryFields?: string[]; + urlParams?: string[]; + }[]; + hasConsultantError?: boolean; + hasAgencyError?: boolean; + hasTopicError?: boolean; +} + export const registrationSessionStorageKey = 'registrationData'; -export function RegistrationProvider(props) { - const getSessionStorageData = () => +export function RegistrationProvider({ children }: PropsWithChildren<{}>) { + const getSessionStorageData = (): SessionStorageData => JSON.parse( sessionStorage.getItem(registrationSessionStorageKey) || '{}' ); + const setSessionStorageData = (data: Partial) => + sessionStorage.setItem( + registrationSessionStorageKey, + JSON.stringify(data) + ); + + const { url } = useRouteMatch(); + const [disabledNextButton, setDisabledNextButton] = useState(true); const [hasTopicError, setHasTopicError] = useState(false); const [hasAgencyError, setHasAgencyError] = useState(false); const [hasConsultantError, setHasConsultantError] = useState(false); - const [isConsultantLink, setIsConsultantLink] = useState(false); - const [consultant, setConsultant] = useState(); - const [preselectedData, setPreselectedData] = useState< - Array<'tid' | 'zipcode' | 'aid'> - >([]); - const [preselectedAgency, setPreselectedAgency] = - useState(); - const [preselectedTopic, setPreselectedTopic] = - useState(); - const [preselectedTopicName, setPreselectedTopicName] = useState(); - const [dataPrepForSessionStorage, setDataPrepForSessionStorage] = useState< - Partial - >({}); - const [sessionStorageRegistrationData, setSessionStorageRegistrationData] = - useState(getSessionStorageData()); - const useQuery = () => { - const { search } = useLocation(); - return useMemo(() => new URLSearchParams(search), [search]); - }; - const urlQuery: URLSearchParams = useQuery(); - const defaultSteps = [ - { component: 'welcome', urlSuffix: '' }, - { - component: 'topicSelection', - urlSuffix: '/topic-selection', - mandatoryFields: ['mainTopicId'], - urlParams: ['tid'] - }, - { - component: 'zipcode', - urlSuffix: '/zipcode', - mandatoryFields: ['zipcode'], - // old links used postcode as parameter - urlParams: ['postcode'] - }, - { - component: 'agencySelection', - urlSuffix: '/agency-selection', - mandatoryFields: ['agencyId'], - urlParams: ['aid'] - }, - { - component: 'accountData', - urlSuffix: '/account-data', - mandatoryFields: ['username', 'password'] - } - ]; - const [availableSteps, setAvailableSteps] = useState(defaultSteps); + const [registrationData, setRegistrationData] = + useState(); - const updateSessionStorage = ( - dataToAdd?: Partial - ) => { - const updatedData = { - ...sessionStorageRegistrationData, - ...dataPrepForSessionStorage, - ...dataToAdd - }; + const preselectedTopicId = getUrlParameter('tid'); + const preselectedAgencyId = getUrlParameter('aid'); + const preselectedConsultantId = getUrlParameter('cid'); + const { + loaded, + agency: preselectedAgency, + topic: preselectedTopic, + zipcode: preselectedZipcode, + consultant: preselectedConsultant + } = useContext(UrlParamsContext); - sessionStorage.setItem( - registrationSessionStorageKey, - JSON.stringify(updatedData) - ); - setSessionStorageRegistrationData(updatedData); - }; - - const refreshSessionStorage = () => { - setSessionStorageRegistrationData(getSessionStorageData()); - }; + const defaultSteps = useMemo( + () => [ + { + component: TopicSelection, + name: 'topic-selection', + route: { + path: `${url}/topic-selection`, + exact: true + }, + mandatoryFields: ['mainTopic'], + condition: ({ topic }) => !!topic + }, + { + component: ZipcodeInput, + name: 'zipcode', + route: { + path: `${url}/zipcode`, + exact: true + }, + mandatoryFields: ['zipcode'], + condition: ({ zipcode }) => !!zipcode + }, + { + component: AgencySelection, + name: 'agency-selection', + route: { + path: `${url}/agency-selection`, + exact: true + }, + mandatoryFields: ['agency'], + condition: ({ agency }) => !!agency + }, + { + component: AccountData, + name: 'account-data', + route: { + path: `/account-data`, + exact: true + }, + mandatoryFields: ['username', 'password'] + } + ], + [url] + ); + const [availableSteps, setAvailableSteps] = useState(defaultSteps); + // Init already stored data from session storage useEffect(() => { - if ( - urlQuery.get('postcode') || - urlQuery.get('aid') || - urlQuery.get('tid') || - urlQuery.get('cid') - ) { - const zipcodeRegex = new RegExp( - /^([0]{1}[1-9]{1}|[1-9]{1}[0-9]{1})[0-9]{3}$/ - ); - const isZipcodeValid = zipcodeRegex.test(urlQuery.get('postcode')); - setPreselectedData( - [ - urlQuery.get('postcode') && isZipcodeValid ? 'zipcode' : '', - urlQuery.get('aid') ? 'aid' : '', - urlQuery.get('tid') ? 'tid' : '' - ].filter((preselection) => preselection !== '') as ( - | 'tid' - | 'aid' - | 'zipcode' - )[] - ); - updateSessionStorage({ - zipcode: isZipcodeValid - ? urlQuery.get('postcode') - : sessionStorageRegistrationData.zipcode, - agencyId: urlQuery.get('aid') - ? parseInt(urlQuery.get('aid')) - : sessionStorageRegistrationData.agencyId, - mainTopicId: urlQuery.get('tid') - ? parseInt(urlQuery.get('tid')) - : sessionStorageRegistrationData.mainTopicId - }); + (async () => { + const registrationData = + getSessionStorageData() as RegistrationData; + if (registrationData.mainTopicId) { + registrationData.mainTopic = await apiGetTopicById( + registrationData.mainTopicId + ); + } + if (registrationData.agencyId) { + // Load agency + registrationData.agency = await apiGetAgencyById( + registrationData.agencyId + ); + } + if (registrationData.topicId) { + registrationData.topic = await apiGetTopicById( + registrationData.topicId + ); + } + setRegistrationData(registrationData); + })(); + }, []); - setAvailableSteps( - availableSteps.filter( - (step) => - !step.urlParams?.every( - (param) => - urlQuery.get(param) && - (param !== 'postcode' || isZipcodeValid) - ) - ) - ); - } - if (urlQuery.get('cid')) { - setIsConsultantLink(true); - (async () => { - try { - const consultantResponse = await apiGetConsultant( - urlQuery.get('cid'), - true, - true - ); - setConsultant(consultantResponse); - } catch { - setHasConsultantError(true); - } - })(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [urlQuery]); + const updateRegistrationData = useCallback( + (data?: Partial) => { + setRegistrationData((registrationData) => ({ + ...registrationData, + ...data + })); + }, + [] + ); useEffect(() => { - if (sessionStorageRegistrationData.mainTopicId) { - (async () => { - try { - const topicsResponse = await apiGetTopicsData(); - setPreselectedTopic( - topicsResponse.filter( - (topic) => - topic.id === - sessionStorageRegistrationData.mainTopicId - )[0] || undefined - ); - setPreselectedTopicName( - topicsResponse.filter( - (topic) => - topic.id === - sessionStorageRegistrationData.mainTopicId - )[0]?.name || undefined - ); - if ( - urlQuery.get('tid') && - topicsResponse.filter( - (topic) => - topic.id === - sessionStorageRegistrationData.mainTopicId - )[0]?.name === undefined - ) { - updateSessionStorage({ - mainTopicId: undefined - }); - setHasTopicError(true); - } - } catch { - setPreselectedTopicName(undefined); - if (urlQuery.get('tid')) { - updateSessionStorage({ - mainTopicId: undefined - }); - setHasTopicError(true); - } - } - })(); - } else { - setPreselectedTopic(undefined); - setPreselectedTopicName(undefined); - } - if (sessionStorageRegistrationData.agencyId) { - (async () => { - try { - const agencyResponse = await apiGetAgencyById( - sessionStorageRegistrationData.agencyId - ); - setPreselectedAgency(agencyResponse || undefined); - } catch { - setPreselectedAgency(undefined); - if (urlQuery.get('aid')) { - updateSessionStorage({ - agencyId: undefined - }); - setHasAgencyError(true); - } - } - })(); - } else { - setPreselectedAgency(undefined); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sessionStorageRegistrationData]); + const { topic, mainTopic, agency, ...sessionStorageData } = + registrationData || {}; - useEffect(() => { - // readd steps for agency and topic if error in preselection - if (hasAgencyError || hasTopicError) { - setAvailableSteps( - defaultSteps.filter( - (step) => - !step.urlParams?.every( - (param) => - urlQuery.get(param) && - (param !== 'aid' || !hasAgencyError) && - (param !== 'tid' || !hasTopicError) - ) - ) - ); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hasAgencyError, hasTopicError]); + setSessionStorageData({ + ...sessionStorageData, + topicId: topic?.id, + mainTopicId: mainTopic?.id, + agencyId: agency?.id + }); + }, [registrationData]); useEffect(() => { // Check if agency matches preselected topic - if ( - urlQuery.get('tid') && - preselectedTopic && - preselectedAgency && - !preselectedAgency?.topicIds?.includes(preselectedTopic.id) - ) { - setPreselectedAgency(undefined); - updateSessionStorage({ - agencyId: undefined - }); - setHasAgencyError(true); - } - // Check if saved topic matches preselected agency - if ( - !urlQuery.get('tid') && - urlQuery.get('aid') && - sessionStorageRegistrationData.mainTopicId && - preselectedAgency && - !preselectedAgency?.topicIds?.includes( - sessionStorageRegistrationData.mainTopicId + const hasTopicError = preselectedTopicId && !preselectedTopic; + + // Check if agency matches preselected topic + const hasAgencyError = + preselectedAgencyId && preselectedTopic && !preselectedAgency; + + setHasConsultantError( + preselectedConsultantId && !preselectedConsultant + ); + setHasTopicError(hasTopicError); + setHasAgencyError(hasAgencyError); + + updateRegistrationData({ + ...(preselectedZipcode ? { zipcode: preselectedZipcode } : {}), + ...(preselectedAgency ? { agency: preselectedAgency } : {}), + ...(preselectedTopic ? { mainTopic: preselectedTopic } : {}) + }); + + setAvailableSteps( + defaultSteps.filter( + (step) => + !step.condition?.({ + agency: preselectedAgency, + topic: preselectedTopic, + zipcode: preselectedZipcode + }) ) - ) { - setPreselectedTopic(undefined); - setPreselectedTopicName(undefined); - updateSessionStorage({ mainTopicId: undefined }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps + ); }, [ - preselectedAgency, + updateRegistrationData, + preselectedAgencyId, + preselectedConsultantId, + preselectedTopicId, + defaultSteps, preselectedTopic, - urlQuery, - sessionStorageRegistrationData.mainTopicId, - preselectedTopicName + preselectedAgency, + preselectedZipcode, + preselectedConsultant ]); + const context = useMemo( + () => ({ + disabledNextButton, + setDisabledNextButton, + registrationData, + updateRegistrationData: updateRegistrationData, + availableSteps, + hasConsultantError, + hasAgencyError, + hasTopicError + }), + [ + availableSteps, + disabledNextButton, + hasAgencyError, + hasConsultantError, + hasTopicError, + registrationData, + updateRegistrationData + ] + ); + + if (!loaded) return null; + return ( - - {props.children} + + {children} ); } diff --git a/src/globalState/provider/UrlParamsProvider.tsx b/src/globalState/provider/UrlParamsProvider.tsx index 3fd5f664e..236ece2c2 100644 --- a/src/globalState/provider/UrlParamsProvider.tsx +++ b/src/globalState/provider/UrlParamsProvider.tsx @@ -20,13 +20,15 @@ export const UrlParamsContext = createContext<{ topic: TopicsDataInterface | null; loaded: boolean; slugFallback: string; + zipcode: string; }>({ agency: null, consultingType: null, consultant: null, topic: null, loaded: false, - slugFallback: undefined + slugFallback: undefined, + zipcode: undefined }); export const UrlParamsProvider = ({ children }: PropsWithChildren<{}>) => { @@ -35,8 +37,15 @@ export const UrlParamsProvider = ({ children }: PropsWithChildren<{}>) => { () => (document.location.href = settings.urls.toRegistration), [settings.urls.toRegistration] ); - const { agency, consultingType, consultant, topic, loaded, slugFallback } = - useUrlParamsLoader(handleBadRequest); + const { + agency, + consultingType, + consultant, + topic, + loaded, + slugFallback, + zipcode + } = useUrlParamsLoader(handleBadRequest); const context = useMemo( () => ({ @@ -45,9 +54,18 @@ export const UrlParamsProvider = ({ children }: PropsWithChildren<{}>) => { consultant, topic, loaded, - slugFallback + slugFallback, + zipcode }), - [agency, consultingType, consultant, topic, loaded, slugFallback] + [ + agency, + consultingType, + consultant, + topic, + loaded, + slugFallback, + zipcode + ] ); return ( diff --git a/src/resources/scripts/config.ts b/src/resources/scripts/config.ts index ba5196fea..d4541d89c 100644 --- a/src/resources/scripts/config.ts +++ b/src/resources/scripts/config.ts @@ -18,7 +18,7 @@ export const config: AppConfigInterface = { enableWalkthrough: false, // Feature flag to enable walkthrough (false by default here & true in the theme repo) disableVideoAppointments: false, // Feature flag to enable Video-Termine page multitenancyWithSingleDomainEnabled: false, // Feature flag to enable the multi tenancy with a single domain ex: lands - useTenantService: false, + useTenantService: true, useApiClusterSettings: true, // Feature flag to enable the cluster use the cluster settings instead of the config file mainTenantSubdomainForSingleDomainMultitenancy: 'app', attachmentEncryption: true, // Feature flag for attachment end to end encryption - e2e must also be enabled in rocket.chat diff --git a/src/utils/useUrlParamsLoader.tsx b/src/utils/useUrlParamsLoader.tsx index 6005dddb2..99f052278 100644 --- a/src/utils/useUrlParamsLoader.tsx +++ b/src/utils/useUrlParamsLoader.tsx @@ -18,14 +18,17 @@ import { apiGetTopicsData } from '../api/apiGetTopicsData'; export default function useUrlParamsLoader(handleBadRequest?: () => void) { const { setLocale } = useContext(LocaleContext); - const { consultingTypeSlug } = useParams<{ + const { consultingTypeSlug, topicSlug } = useParams<{ consultingTypeSlug: string; + topicSlug: string; }>(); const settings = useAppConfig(); const agencyId = getUrlParameter('aid'); const consultantId = getUrlParameter('cid'); const topicIdOrName = getUrlParameter('tid'); const language = getUrlParameter('lang'); + const zipcodeParam = + getUrlParameter('zipcode') || getUrlParameter('postcode'); const [consultingType, setConsultingType] = useState(null); @@ -35,22 +38,29 @@ export default function useUrlParamsLoader(handleBadRequest?: () => void) { const [loaded, setLoaded] = useState(false); const [topic, setTopic] = useState(null); const [slugFallback, setSlugFallback] = useState(); + const [zipcode, setZipcode] = useState(); const loadTopic = useCallback( async (agency) => { let topic = null; if (isNumber(topicIdOrName)) { topic = await apiGetTopicById(topicIdOrName).catch(() => null); - } else if (isString(topicIdOrName)) { + } else if (isString(topicIdOrName) || isString(topicSlug)) { topic = await apiGetTopicsData() .then( (allTopics) => allTopics.find( (topic) => - topic.name?.toLowerCase() === - decodeURIComponent( - topicIdOrName.toLowerCase() - ) + (topicIdOrName && + topic.name?.toLowerCase() === + decodeURIComponent( + topicIdOrName.toLowerCase() + )) || + (topicSlug && + topic.slug?.toLowerCase() === + decodeURIComponent( + topicSlug.toLowerCase() + )) ) || null ) .catch(() => null); @@ -67,7 +77,7 @@ export default function useUrlParamsLoader(handleBadRequest?: () => void) { return [agency, topic]; }, - [topicIdOrName] + [topicIdOrName, topicSlug] ); const handleConsultant = useCallback( @@ -123,7 +133,7 @@ export default function useUrlParamsLoader(handleBadRequest?: () => void) { } else if ( // If the consultingType does not match the consultant's consultingTypes, set the consultingType to null !consultant.agencies.some( - (a) => + (a: AgencyDataInterface) => !consultingType || a.consultingType === consultingType?.id ) @@ -172,7 +182,7 @@ export default function useUrlParamsLoader(handleBadRequest?: () => void) { } } - if (topicIdOrName !== null) { + if (topicIdOrName !== null || topicSlug) { [agency, topic] = await loadTopic(agency); } @@ -197,6 +207,11 @@ export default function useUrlParamsLoader(handleBadRequest?: () => void) { return; } + const zipcodeRegex = new RegExp(/^(0[1-9]|[1-9]\d)\d{3}$/); + if (zipcodeParam && zipcodeRegex.test(zipcodeParam)) { + setZipcode(zipcodeParam); + } + setTopic(topic); setConsultant(consultant); setConsultingType(consultingType); @@ -211,6 +226,8 @@ export default function useUrlParamsLoader(handleBadRequest?: () => void) { agencyId, consultantId, topicIdOrName, + topicSlug, + zipcodeParam, settings.multitenancyWithSingleDomainEnabled, settings.urls.toRegistration, settings?.registration?.useConsultingTypeSlug, @@ -225,5 +242,13 @@ export default function useUrlParamsLoader(handleBadRequest?: () => void) { } }, [language, setLocale]); - return { agency, consultant, consultingType, loaded, topic, slugFallback }; + return { + agency, + consultant, + consultingType, + loaded, + topic, + slugFallback, + zipcode + }; }