Ott 90 step register (#16)

keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 6 years ago committed by GitHub
parent b548d1e607
commit c205528de6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .eslintrc
  2. 2
      src/config/procedures.tsx
  3. 2
      src/config/routes.tsx
  4. 0
      src/features/Combobox/helpers/__tests__/index.tsx
  5. 0
      src/features/Combobox/helpers/index.tsx
  6. 81
      src/features/Combobox/hooks/index.tsx
  7. 28
      src/features/Combobox/hooks/useKeyboardScroll.tsx
  8. 72
      src/features/Combobox/index.tsx
  9. 86
      src/features/Combobox/styled.tsx
  10. 21
      src/features/Combobox/types.tsx
  11. 10
      src/features/Common/Input/index.tsx
  12. 88
      src/features/CountrySelector/hooks.tsx
  13. 63
      src/features/CountrySelector/index.tsx
  14. 27
      src/features/CountrySelector/styled.tsx
  15. 58
      src/features/Register/Steps/Registration/index.tsx
  16. 3
      src/features/Register/Steps/index.tsx
  17. 0
      src/features/Register/components/AdditionalSubscription/index.tsx
  18. 0
      src/features/Register/components/AdditionalSubscription/styled.tsx
  19. 10
      src/features/Register/components/CardStep/index.tsx
  20. 2
      src/features/Register/components/MainSubscription/index.tsx
  21. 0
      src/features/Register/components/Price/index.tsx
  22. 0
      src/features/Register/components/Price/styled.tsx
  23. 12
      src/features/Register/components/RegistrationStep/config.tsx
  24. 86
      src/features/Register/components/RegistrationStep/hooks/useCities.tsx
  25. 48
      src/features/Register/components/RegistrationStep/hooks/useCountries.tsx
  26. 101
      src/features/Register/components/RegistrationStep/hooks/useForm.tsx
  27. 121
      src/features/Register/components/RegistrationStep/index.tsx
  28. 5
      src/features/Register/components/SubscriptionsStep/index.tsx
  29. 0
      src/features/Register/components/SubscriptionsStep/styled.tsx
  30. 22
      src/features/Register/helpers/isValidEmail/__tests__/index.tsx
  31. 9
      src/features/Register/helpers/isValidEmail/index.tsx
  32. 15
      src/features/Register/helpers/isValidPassword/__tests__/index.tsx
  33. 14
      src/features/Register/helpers/isValidPassword/index.tsx
  34. 10
      src/features/Register/index.tsx
  35. 10
      src/features/Register/styled.tsx
  36. 1
      src/hooks/index.tsx
  37. 2
      src/hooks/useCurrentLang.tsx
  38. 28
      src/requests/getCountryCities.tsx
  39. 2
      src/requests/index.tsx
  40. 72
      src/requests/register.tsx

@ -82,6 +82,7 @@
"import/no-unresolved": "off",
"import/prefer-default-export": "off",
"indent": "off",
"no-underscore-dangle": "off",
"no-unused-vars": "off",
"react/jsx-one-expression-per-line": "off",
"react/jsx-fragments": "off",

@ -1,3 +1,5 @@
export const PROCEDURES = {
create_user: 'create_user',
get_cities: 'get_cities',
lst_c_country: 'lst_c_country',
}

@ -1,2 +1,2 @@
export const API_ROOT = 'http://85.10.224.24:8080'
export const API_ROOT = 'http://api-staging.instat.tv'
export const DATA_URL = `${API_ROOT}/data`

@ -0,0 +1,81 @@
import type { ChangeEvent, FocusEvent } from 'react'
import { useState, useCallback } from 'react'
import isUndefined from 'lodash/isUndefined'
import toLower from 'lodash/toLower'
import slice from 'lodash/slice'
import find from 'lodash/find'
import trim from 'lodash/trim'
import type { Props, Option } from '../types'
import { matchSort } from '../helpers'
import { useKeyboardScroll } from './useKeyboardScroll'
const isOptionClicked = (target: HTMLElement) => (
target?.getAttribute('role') === 'option'
)
const useQuery = <T extends Option>({ onChange, value }: Props<T>) => {
const [query, setQuery] = useState('')
const onQueryChange = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => {
setQuery(target.value)
}, [])
return {
onQueryChange: !isUndefined(onChange) ? onChange : onQueryChange,
query: !isUndefined(value) ? value : query,
setQuery,
}
}
export const useCombobox = <T extends Option>(props: Props<T>) => {
const { onSelect, options } = props
const {
onQueryChange,
query,
setQuery,
} = useQuery(props)
const results = matchSort(
options,
'name',
query,
)
const findOptionByName = (optionName: string) => (
find(
options,
({ name }) => toLower(name) === toLower(trim(optionName)),
) || null
)
const onOptionSelect = (option: string) => {
const selectedOption = findOptionByName(option)
setQuery(selectedOption?.name || '')
onSelect?.(selectedOption)
}
const onInputBlur = (event: FocusEvent<HTMLInputElement>) => {
const target = event.relatedTarget as HTMLElement | null
// клик по элементу списка тоже вызывает onBlur
// если кликали элемент списка то событие обрабатывает onOptionSelect
if (target && isOptionClicked(target)) return
onOptionSelect(query)
}
return {
...useKeyboardScroll(),
onInputBlur,
onOptionSelect,
onQueryChange,
options: slice(
results,
0,
20,
),
query,
}
}

@ -0,0 +1,28 @@
import type { KeyboardEvent } from 'react'
import { useRef } from 'react'
export const useKeyboardScroll = () => {
const popoverRef = useRef<HTMLUListElement>(null)
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
const container = popoverRef.current
if (event.isDefaultPrevented() || !container) return
window.requestAnimationFrame(() => {
const el = container.querySelector<HTMLElement>('[aria-selected=true]')
if (!el) return
const { clientHeight, scrollTop } = container
const top = el.offsetTop - scrollTop
const bottom = (scrollTop + clientHeight) - (el.offsetTop + el.clientHeight)
if (bottom < 0) {
container.scrollTop -= bottom
}
if (top < 0) {
container.scrollTop += top
}
})
}
return { onKeyDown, popoverRef }
}

@ -0,0 +1,72 @@
import React from 'react'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import '@reach/combobox/styles.css'
import { Label } from 'features/Common/Input/styled'
import { Props, Option } from './types'
import { useCombobox } from './hooks'
import {
ComboboxStyled,
ComboboxInputStyled,
ComboboxPopoverStyled,
ComboboxListStyled,
ComboboxOptionStyled,
Arrow,
} from './styled'
export const Combobox = <T extends Option>(props: Props<T>) => {
const {
disabled,
id,
label,
labelWidth,
pattern,
required,
} = props
const {
onInputBlur,
onKeyDown,
onOptionSelect,
onQueryChange,
options,
popoverRef,
query,
} = useCombobox(props)
return (
<ComboboxStyled onSelect={onOptionSelect}>
<Label
labelWidth={labelWidth}
htmlFor={id}
>
{label}
<Arrow />
</Label>
<ComboboxInputStyled
id={id}
required={required}
disabled={disabled}
value={query}
pattern={pattern}
onChange={onQueryChange}
onBlur={onInputBlur}
onKeyDown={onKeyDown}
/>
{!isEmpty(options) && (
<ComboboxPopoverStyled>
<ComboboxListStyled ref={popoverRef}>
{map(options, ({ id: optionId, name }) => (
<ComboboxOptionStyled
key={optionId}
value={name}
/>
))}
</ComboboxListStyled>
</ComboboxPopoverStyled>
)}
</ComboboxStyled>
)
}

@ -0,0 +1,86 @@
import styled from 'styled-components/macro'
import {
Combobox,
ComboboxInput,
ComboboxList,
ComboboxOption,
ComboboxPopover,
} from '@reach/combobox'
import { wrapperStyles, inputStyles } from 'features/Common/Input/styled'
export const ComboboxStyled = styled(Combobox)`
${wrapperStyles}
position: relative;
`
export const ComboboxInputStyled = styled(ComboboxInput)`
${inputStyles}
padding-right: 24px;
`
export const Arrow = styled.div`
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
background-image: url(/images/arrowDown.svg);
background-position: center;
background-repeat: no-repeat;
`
export const ComboboxPopoverStyled = styled(ComboboxPopover)`
border: none;
`
export const ComboboxListStyled = styled(ComboboxList)`
background: #666;
min-width: 544px;
max-height: 336px;
height: auto;
border-radius: 2px;
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
position: absolute;
top: 0;
left: 0;
transform: translate(-126px, 9px);
overflow: auto;
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #3F3F3F;
border-radius: 6px;
}
`
export const ComboboxOptionStyled = styled(ComboboxOption)`
width: 100%;
height: 48px;
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
padding-left: 24px;
color: #ccc;
background: transparent;
&[aria-selected="true"] {
background: #999;
color: #fff;
}
&:hover {
background: #999;
color: #fff;
}
`

@ -0,0 +1,21 @@
import type { InputHTMLAttributes, ChangeEvent } from 'react'
export type Option = {
id: number,
name: string,
}
export type Props<T> = Pick<InputHTMLAttributes<HTMLInputElement>, (
| 'id'
| 'onChange'
| 'required'
| 'disabled'
| 'pattern'
)> & {
label?: string,
labelWidth?: number,
onChange?: (event: ChangeEvent<HTMLInputElement>) => void,
onSelect?: (option: T | null) => void,
options: Array<T>,
value?: string,
}

@ -1,4 +1,4 @@
import React from 'react'
import React, { ChangeEvent } from 'react'
import {
TInputWrapper,
@ -14,8 +14,10 @@ type TInput = {
label: string,
labelWidth?: number,
maxLength?: number,
onChange?: () => void,
onChange?: (event: ChangeEvent<HTMLInputElement>) => void,
pattern?: string,
required?: boolean,
title?: string,
type?: string,
value?: string,
} & TInputWrapper
@ -29,7 +31,9 @@ export const Input = ({
maxLength,
onChange,
paddingX,
pattern,
required,
title,
type,
value,
wrapperWidth,
@ -53,6 +57,8 @@ export const Input = ({
onChange={onChange}
maxLength={maxLength}
inputWidth={inputWidth}
pattern={pattern}
title={title}
/>
</InputWrapper>
)

@ -1,88 +0,0 @@
import type { ChangeEvent, FocusEvent } from 'react'
import type { Countries } from 'requests'
import {
useEffect,
useState,
useCallback,
} from 'react'
import toLower from 'lodash/toLower'
import slice from 'lodash/slice'
import find from 'lodash/find'
import { getCountries } from 'requests'
import { matchSort } from './helpers'
// временно, будем считывать из стора(контекста) лексики
const useCurrentLang = () => 'eng'
const useCountriesList = () => {
const [countries, setCountries] = useState<Countries>([])
useEffect(() => {
getCountries().then(setCountries)
}, [])
return countries
}
const useQuery = () => {
const [query, setQuery] = useState('')
const onQueryChange = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => {
setQuery(target.value)
}, [])
return {
onQueryChange,
query,
setQuery,
}
}
const isOptionClicked = (target: HTMLElement) => target?.getAttribute('role') === 'option'
export const useCountrySelector = () => {
const lang = useCurrentLang()
const countries = useCountriesList()
const {
onQueryChange,
query,
setQuery,
} = useQuery()
const keyToSortBy = `name_${lang}` as 'name_eng'
const results = matchSort(
countries,
keyToSortBy,
query,
)
const onBlur = (event: FocusEvent<HTMLInputElement>) => {
const target = event.relatedTarget as HTMLElement | null
// клик по элементу списка тоже вызывает onBlur
// если кликали элемент списка то событие обрабатывается onCountrySelect
if (target && isOptionClicked(target)) return
const found = find(
countries,
(country) => toLower(country[keyToSortBy]) === toLower(query),
)
setQuery(found ? found[keyToSortBy] : '')
}
return {
countries: slice(
results,
0,
20,
),
onBlur,
onCountrySelect: setQuery,
onQueryChange,
query,
}
}

@ -1,63 +0,0 @@
import React from 'react'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import {
ComboboxPopover,
ComboboxList,
ComboboxOption,
} from '@reach/combobox'
import '@reach/combobox/styles.css'
import { Label } from 'features/Common/Input/styled'
import { useCountrySelector } from './hooks'
import {
ComboboxStyled,
ComboboxInputStyled,
Arrow,
} from './styled'
type Props = {
labelWidth?: number,
}
export const CountrySelector = ({ labelWidth }: Props) => {
const {
countries,
onBlur,
onCountrySelect,
onQueryChange,
query,
} = useCountrySelector()
return (
<ComboboxStyled onSelect={onCountrySelect}>
<Label
labelWidth={labelWidth}
htmlFor='country'
>
Страна
<Arrow />
</Label>
<ComboboxInputStyled
id='country'
value={query}
onChange={onQueryChange}
onBlur={onBlur}
/>
{!isEmpty(countries) && (
<ComboboxPopover>
<ComboboxList>
{map(countries, ({ id, name_eng }) => (
<ComboboxOption
key={id}
value={name_eng}
/>
))}
</ComboboxList>
</ComboboxPopover>
)}
</ComboboxStyled>
)
}

@ -1,27 +0,0 @@
import styled from 'styled-components/macro'
import { Combobox, ComboboxInput } from '@reach/combobox'
import { wrapperStyles, inputStyles } from 'features/Common/Input/styled'
export const ComboboxStyled = styled(Combobox)`
${wrapperStyles}
position: relative;
`
export const ComboboxInputStyled = styled(ComboboxInput)`
${inputStyles}
padding-right: 24px;
`
export const Arrow = styled.div`
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
background-image: url(/images/arrowDown.svg);
background-position: center;
background-repeat: no-repeat;
`

@ -1,58 +0,0 @@
import React from 'react'
import { PAGES } from 'config'
import { CountrySelector } from 'features/CountrySelector'
import { Input } from 'features/Common'
import {
BlockTitle,
ButtonsBlock,
Form,
} from 'features/Login/styled'
import { NextButton } from '../../styled'
const labelWidth = 78
export const Registration = () => (
<Form>
<BlockTitle>Регистрация</BlockTitle>
<Input
id='firstname'
label='Имя'
labelWidth={labelWidth}
/>
<Input
id='lastname'
label='Фамилия'
labelWidth={labelWidth}
/>
<Input
id='phone'
label='Телефон'
labelWidth={labelWidth}
/>
<Input
id='email'
type='email'
label='E-mail'
labelWidth={labelWidth}
/>
<CountrySelector labelWidth={78} />
<Input
id='address1'
label='Адрес 1'
labelWidth={labelWidth}
/>
<Input
id='address2'
label='Адрес 2'
labelWidth={labelWidth}
/>
<ButtonsBlock>
<NextButton to={`${PAGES.register}/card`}>Далее</NextButton>
</ButtonsBlock>
</Form>
)

@ -1,3 +0,0 @@
export * from './Registration'
export * from './Card'
export * from './Subscriptions'

@ -1,17 +1,13 @@
import React from 'react'
import { Input } from 'features/Common'
import { Input, ButtonSolid } from 'features/Common'
import {
BlockTitle,
ButtonsBlock,
Form,
} from 'features/Login/styled'
import {
NextButton,
Card,
Row,
} from '../../styled'
import { Card, Row } from '../../styled'
export const CardStep = () => (
<Form>
@ -42,7 +38,7 @@ export const CardStep = () => (
</Row>
</Card>
<ButtonsBlock>
<NextButton to='subscriptions'>Далее</NextButton>
<ButtonSolid>Далее</ButtonSolid>
</ButtonsBlock>
</Form>
)

@ -7,7 +7,7 @@ import {
SubscriptionWrapper,
SubscriptionTitle,
Row,
} from '../Steps/Subscriptions/styled'
} from '../SubscriptionsStep/styled'
type TMainSubscription = {
price: number,

@ -0,0 +1,12 @@
export const formIds = {
address1: 'address1',
address2: 'address2',
city: 'city',
country: 'country',
email: 'email',
firstname: 'firstname',
lastname: 'lastname',
password: 'password',
phone: 'phone',
postalCode: 'postalCode',
}

@ -0,0 +1,86 @@
import type { ChangeEvent } from 'react'
import {
useState,
useCallback,
useEffect,
} from 'react'
import debounce from 'lodash/debounce'
import trim from 'lodash/trim'
import type { Cities, City } from 'requests'
import { getCountryCities } from 'requests'
const useCitiesList = () => {
const [cities, setCities] = useState<Cities>([])
const getCities = (city: string, selectedCountryId: number) => {
getCountryCities(city, selectedCountryId).then(setCities)
}
const getCitiesDebounced = useCallback(
debounce(getCities, 300),
[],
)
const resetCities = useCallback(() => setCities([]), [])
return {
cities,
getCities: getCitiesDebounced,
resetCities,
}
}
export const useCities = (selectedCountryId?: number) => {
const [cityQuery, setCityQuery] = useState('')
const [selectedCity, setSelectedCity] = useState<City | null>(null)
const {
cities,
getCities,
resetCities,
} = useCitiesList()
const trimmedCity = trim(cityQuery)
useEffect(() => {
if (trimmedCity && selectedCountryId) {
getCities(trimmedCity, selectedCountryId)
}
}, [
trimmedCity,
selectedCountryId,
getCities,
])
const onCityQueryChange = ({ target }: ChangeEvent<HTMLInputElement>) => {
setCityQuery(target.value)
}
const onCitySelect = (newCity: City | null) => {
if (newCity) {
setCityQuery(newCity.name)
setSelectedCity(newCity)
} else {
setSelectedCity(null)
}
}
const resetSelectedCity = useCallback(
() => {
setCityQuery('')
setSelectedCity(null)
},
[setCityQuery, setSelectedCity],
)
return {
cities,
cityQuery,
onCityQueryChange,
onCitySelect,
resetCities,
resetSelectedCity,
selectedCity,
}
}

@ -0,0 +1,48 @@
import {
useEffect,
useState,
useMemo,
} from 'react'
import map from 'lodash/map'
import type { Countries } from 'requests'
import { getCountries } from 'requests'
import { useCurrentLang } from 'hooks'
type Country = {
id: number,
name: string,
}
const useCountriesList = () => {
const [countries, setCountries] = useState<Countries>([])
useEffect(() => {
getCountries().then(setCountries)
}, [])
return countries
}
export const useCountries = () => {
const countries = useCountriesList()
const lang = useCurrentLang()
const [selectedCountry, setSelectedCountry] = useState<Country | null>(null)
const nameField = `name_${lang}` as 'name_eng'
const transformedCountries = useMemo(
() => map(countries, (country) => ({
id: country.id,
name: country[nameField],
})),
[countries, nameField],
)
return {
countries: transformedCountries,
onCountrySelect: setSelectedCountry,
selectedCountry,
}
}

@ -0,0 +1,101 @@
import type { FormEvent } from 'react'
import { useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import trim from 'lodash/trim'
import { PAGES } from 'config'
import { register } from 'requests'
import { isValidEmail } from 'features/Register/helpers/isValidEmail'
import { formIds } from '../config'
import { useCountries } from './useCountries'
import { useCities } from './useCities'
const readFormValue = (event: FormEvent<HTMLFormElement>) => (
(fieldName: string) => trim(event.currentTarget[fieldName]?.value)
)
export const useForm = () => {
const history = useHistory()
const {
countries,
onCountrySelect,
selectedCountry,
} = useCountries()
const {
cities,
cityQuery,
onCityQueryChange,
onCitySelect,
resetCities,
resetSelectedCity,
selectedCity,
} = useCities(selectedCountry?.id)
useEffect(() => {
resetSelectedCity()
resetCities()
}, [
selectedCountry,
resetSelectedCity,
resetCities,
])
const goToLoginPage = () => {
history.replace(PAGES.login)
}
const showError = (message: string) => {
// eslint-disable-next-line no-alert
window.alert(message)
}
const getCityParams = () => {
if (selectedCity) return { cityId: selectedCity.id }
return { city: cityQuery }
}
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (!selectedCountry) return
const readFieldValue = readFormValue(event)
const firstname = readFieldValue(formIds.firstname)
const lastname = readFieldValue(formIds.lastname)
const phone = readFieldValue(formIds.phone)
const email = readFieldValue(formIds.email)
const password = readFieldValue(formIds.password)
const postalCode = Number(readFieldValue(formIds.postalCode))
const address1 = readFieldValue(formIds.address1)
const address2 = readFieldValue(formIds.address2)
if (isValidEmail(email)) {
register({
address1,
address2,
...getCityParams(),
countryId: selectedCountry.id,
email,
firstname,
lastname,
password,
phone,
postalCode,
}).then(goToLoginPage, showError)
}
}
return {
cities,
cityQuery,
countries,
handleSubmit,
onCityQueryChange,
onCitySelect,
onCountrySelect,
selectedCountry,
}
}

@ -0,0 +1,121 @@
import React from 'react'
import { Combobox } from 'features/Combobox'
import { Input, ButtonSolid } from 'features/Common'
import {
BlockTitle,
ButtonsBlock,
Form,
} from 'features/Login/styled'
import { formIds } from './config'
import { passwordRegex } from '../../helpers/isValidPassword'
import { emailRegex } from '../../helpers/isValidEmail'
import { useForm } from './hooks/useForm'
const commonFieldRegex = '^.{0,500}$'
const postalCodeRegex = '^\\d{5}(?:[-\\s]\\d{4})?$'
const labelWidth = 78
export const RegistrationStep = () => {
const {
cities,
cityQuery,
countries,
handleSubmit,
onCityQueryChange,
onCitySelect,
onCountrySelect,
selectedCountry,
} = useForm()
return (
<Form onSubmit={handleSubmit}>
<BlockTitle>Регистрация</BlockTitle>
<Input
required
id={formIds.firstname}
label='Имя'
labelWidth={labelWidth}
pattern={commonFieldRegex}
/>
<Input
required
id={formIds.lastname}
label='Фамилия'
labelWidth={labelWidth}
pattern={commonFieldRegex}
/>
<Input
required
id={formIds.phone}
type='tel'
label='Телефон'
labelWidth={labelWidth}
pattern='^[0-9]{8,100}$'
/>
<Input
required
id={formIds.email}
type='email'
label='E-mail'
title='example@mail.com'
labelWidth={labelWidth}
pattern={emailRegex}
/>
<Input
required
id={formIds.password}
type='password'
label='Пароль'
title='Inst@1TV'
labelWidth={labelWidth}
pattern={passwordRegex}
/>
<Combobox
required
id={formIds.country}
label='Страна'
labelWidth={labelWidth}
options={countries}
onSelect={onCountrySelect}
/>
<Combobox
required
id={formIds.city}
label='Город'
labelWidth={labelWidth}
disabled={!selectedCountry}
value={cityQuery}
onChange={onCityQueryChange}
options={cities}
onSelect={onCitySelect}
pattern={commonFieldRegex}
/>
<Input
required
id={formIds.postalCode}
label='Почтовый адрес'
pattern={postalCodeRegex}
/>
<Input
required
id={formIds.address1}
label='Адрес 1'
labelWidth={labelWidth}
pattern={commonFieldRegex}
/>
<Input
id={formIds.address2}
label='Адрес 2'
labelWidth={labelWidth}
pattern={commonFieldRegex}
/>
<ButtonsBlock>
<ButtonSolid type='submit'>Далее</ButtonSolid>
</ButtonsBlock>
</Form>
)
}

@ -3,9 +3,8 @@ import React, { Fragment } from 'react'
import { ButtonSolid } from 'features/Common/Button'
import { ArrowLeft, ArrowRight } from 'features/Common/Arrows'
import { ButtonsBlock } from 'features/Login/styled'
import { MainSubscription } from 'features/Register/MainSubscription'
import { AdditionalSubscription } from 'features/Register/AdditionalSubscription'
import { MainSubscription } from 'features/Register/components/MainSubscription'
import { AdditionalSubscription } from 'features/Register/components/AdditionalSubscription'
import {
SubscriptionsBlock,

@ -0,0 +1,22 @@
import { isValidEmail } from '..'
it('invalid emails', () => {
expect(isValidEmail('a')).toBeFalsy()
expect(isValidEmail('1')).toBeFalsy()
expect(isValidEmail('a@')).toBeFalsy()
expect(isValidEmail('a@m')).toBeFalsy()
expect(isValidEmail('a@m.')).toBeFalsy()
expect(isValidEmail('a@mail')).toBeFalsy()
expect(isValidEmail('a@mail.')).toBeFalsy()
expect(isValidEmail('abcd.mail.com')).toBeFalsy()
})
it('valid emails', () => {
expect(isValidEmail('a@mail.com')).toBeTruthy()
expect(isValidEmail('A@MAIL.COM')).toBeTruthy()
expect(isValidEmail('123@mail.com')).toBeTruthy()
expect(isValidEmail('A123@mail.com')).toBeTruthy()
expect(isValidEmail('a.b@mail.com')).toBeTruthy()
expect(isValidEmail('a-b-c@mail.com')).toBeTruthy()
expect(isValidEmail('a-b@a-b.com')).toBeTruthy()
})

@ -0,0 +1,9 @@
import size from 'lodash/size'
export const emailRegex = '[a-z0-9!#$%&/\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&/\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?'
const emailRegExp = new RegExp(emailRegex)
export const isValidEmail = (email: string) => (
emailRegExp.test(email.toLowerCase()) && size(email) <= 100
)

@ -0,0 +1,15 @@
import { isValidPassword } from '..'
it('invalid passwords', () => {
expect(isValidPassword('a')).toBeFalsy()
expect(isValidPassword('1')).toBeFalsy()
expect(isValidPassword('abcdef')).toBeFalsy()
expect(isValidPassword('abcdef@')).toBeFalsy()
expect(isValidPassword('abcdef@123')).toBeFalsy()
expect(isValidPassword('ABcd12$')).toBeFalsy()
})
it('valid passwords', () => {
expect(isValidPassword('aASbc!def123$')).toBeTruthy()
expect(isValidPassword('Abcdef@123$')).toBeTruthy()
})

@ -0,0 +1,14 @@
export const passwordRegex = '^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,500}$'
const passwordRegExp = new RegExp(passwordRegex)
/**
* At least one upper case English letter, (?=.*?[A-Z])
* At least one lower case English letter, (?=.*?[a-z])
* At least one digit, (?=.*?[0-9])
* At least one special character, (?=.*?[#?!@$%^&*-])
* Minimum eight in length .{8,} (with the anchors)
*/
export const isValidPassword = (password: string) => (
passwordRegExp.test(password)
)

@ -7,18 +7,16 @@ import { Background } from 'features/Background'
import { Logo } from 'features/Logo'
import { CenterBlock } from 'features/Login/styled'
import {
Registration,
CardStep,
SubscriptionStep,
} from './Steps'
import { RegistrationStep } from './components/RegistrationStep'
import { CardStep } from './components/CardStep'
import { SubscriptionStep } from './components/SubscriptionsStep'
export const Register = () => (
<Background>
<CenterBlock>
<Logo />
<Route exact path={`${PAGES.register}`}>
<Registration />
<RegistrationStep />
</Route>
<Route exact path={`${PAGES.register}/card`}>
<CardStep />

@ -1,15 +1,5 @@
import { Link } from 'react-router-dom'
import styled from 'styled-components/macro'
import { solidButtonStyles } from 'features/Common'
export const NextButton = styled(Link)`
${solidButtonStyles}
display: flex;
align-items: center;
justify-content: center;
`
export const Card = styled.div`
width: 546px;
height: 340px;

@ -1 +1,2 @@
export * from './usePageId'
export * from './useCurrentLang'

@ -0,0 +1,2 @@
// временно, будем считывать из стора(контекста) лексики
export const useCurrentLang = () => 'eng'

@ -0,0 +1,28 @@
import { DATA_URL, PROCEDURES } from 'config'
import { callApi, getResponseData } from 'helpers'
const proc = PROCEDURES.get_cities
export type City = {
id: number,
name: string,
}
export type Cities = Array<City>
export const getCountryCities = (query: string, countryId: number): Promise<Cities> => {
const config = {
body: {
params: {
_p_country_id: countryId,
_p_name: query,
},
proc,
},
}
return callApi({
config,
url: DATA_URL,
}).then(getResponseData(proc))
}

@ -1,2 +1,4 @@
export * from './register'
export * from './login'
export * from './getCountries'
export * from './getCountryCities'

@ -0,0 +1,72 @@
import { DATA_URL, PROCEDURES } from 'config'
import { callApi } from 'helpers'
const proc = PROCEDURES.create_user
const responseStatus = {
FAILURE: 2,
SUCCESS: 1,
}
type Response = {
_p_error: string | null,
_p_status: 1 | 2,
}
type Args = {
address1: string,
address2?: string,
city?: string,
cityId?: number,
countryId: number,
email: string,
firstname: string,
lastname: string,
password: string,
phone: string,
postalCode: number,
}
export const register = async ({
address1,
address2,
city,
cityId,
countryId,
email,
firstname,
lastname,
password,
phone,
postalCode,
}: Args) => {
const config = {
body: {
params: {
_p_address_line1: address1,
_p_address_line2: address2,
_p_city: city,
_p_city_id: cityId,
_p_country_id: countryId,
_p_email: email,
_p_firstname: firstname,
_p_lastname: lastname,
_p_password: password,
_p_phone: phone,
_p_postal_code: postalCode,
},
proc,
},
}
const response: Response = await callApi({
config,
url: DATA_URL,
})
if (response._p_status === responseStatus.SUCCESS) {
return Promise.resolve(response)
}
return Promise.reject(response._p_error)
}
Loading…
Cancel
Save