Ott 90 step register (#16)
parent
b548d1e607
commit
c205528de6
@ -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,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' |
||||
@ -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> |
||||
) |
||||
} |
||||
@ -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) |
||||
) |
||||
@ -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…
Reference in new issue