Ott 1633 add address (#541)

* feat: 🎸 ott-1633-add-address

address added, fix

* feat: 🎸 ott-1633-add-address

fields removed

* feat: 🎸 ott-1633-add-address

home removed

* feat: 🎸 ott-1633-add-address

fix

* feat: 🎸 ott-1633-address

fix pr

* feat: 🎸 ott-1633-add-address

fix pr
keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Zoia 4 years ago committed by Mirlan
parent c42818cc63
commit 90fc409557
  1. BIN
      public/images/home.png
  2. 9
      src/config/lexics/payment.tsx
  3. 1
      src/config/lexics/userAccount.tsx
  4. 5
      src/features/AddCardForm/components/ElementContainer/index.tsx
  5. 94
      src/features/AddCardForm/components/Form/hooks/index.tsx
  6. 46
      src/features/AddCardForm/components/Form/hooks/useCountries.tsx
  7. 20
      src/features/AddCardForm/components/Form/hooks/validateFields.tsx
  8. 60
      src/features/AddCardForm/components/Form/index.tsx
  9. 41
      src/features/AddCardForm/styled.tsx
  10. 12
      src/features/Combobox/index.tsx
  11. 1
      src/features/Combobox/styled.tsx
  12. 1
      src/features/Combobox/types.tsx
  13. 21
      src/features/UserAccount/components/Header/index.tsx
  14. 70
      src/features/UserAccount/components/PersonalInfoForm/hooks/useCountries.tsx
  15. 12
      src/features/UserAccount/components/PersonalInfoForm/hooks/useUserInfo.tsx
  16. 43
      src/features/UserAccount/components/PersonalInfoForm/hooks/useUserInfoForm.tsx
  17. 16
      src/features/UserAccount/components/PersonalInfoForm/index.tsx

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

@ -1,7 +1,16 @@
export const paymentLexics = {
add_card: 8313,
address: 15203,
billing_address: 15489,
card_holder_name: 2021,
city: 15206,
country: 835,
error_address_latin_letters: 15758,
error_can_not_add_card: 14447,
error_city_latin_letters: 15759,
error_empty_address: 15755,
error_empty_city: 15754,
error_empty_country: 15753,
error_empty_name: 15290,
error_payment_unsuccessful: 14446,
}

@ -13,7 +13,6 @@ const navigations = {
export const userAccountLexics = {
change: 12614,
change_password: 15054,
country: 835,
delete: 848,
delete_card: 8692,
language: 15053,

@ -28,10 +28,7 @@ const Label = styled.label<LabelProps>`
line-height: 48px;
letter-spacing: -0.01em;
color: rgba(255, 255, 255, 0.5);
:not(:first-child) {
margin-top: 10px;
}
margin-bottom: 10px;
${isMobileDevice
? css`

@ -23,7 +23,17 @@ import { useObjectState } from 'hooks'
import { useCardsStore } from 'features/CardsStore'
import { useLexicsStore } from 'features/LexicsStore'
import { SelectedCountry, useCountries } from './useCountries'
import {
isValidAddress,
isValidName,
validateFields,
} from './validateFields'
export enum ElementTypes {
CardAddress = 'cardAddress',
CardCity = 'cardCity',
CardCountry = 'cardCountry',
CardCvc = 'cardCvc',
CardExpiry = 'cardExpiry',
CardHolder = 'cardHolder',
@ -44,6 +54,9 @@ const inputState = {
}
const initialState = {
cardAddress: inputState,
cardCity: inputState,
cardCountry: inputState,
cardCvc: inputState,
cardExpiry: inputState,
cardHolder: inputState,
@ -56,29 +69,69 @@ export const useFormSubmit = ({ onAddSuccess }: Props) => {
const { translate } = useLexicsStore()
const { onAddCard, setError: setCardError } = useCardsStore()
const [name, setName] = useState('')
const [city, setCity] = useState('')
const [address, setAddress] = useState('')
const [inputStates, setInputStates] = useObjectState(initialState)
const [errorMessage, setErrorMessage] = useState('')
const [loader, setLoader] = useState(false)
const {
countries,
selectedCountry,
setSelectedCountry,
} = useCountries()
const resetErrors = useCallback(() => {
setErrorMessage('')
setCardError('')
}, [setErrorMessage, setCardError])
const setElementTypeState = useCallback((elementType: ElementTypes, value: string) => {
const elementState = inputStates[elementType]
setInputStates({
[elementType]: {
...elementState,
empty: !value,
},
})
}, [inputStates, setInputStates])
const onNameChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target
if (/^[A-Za-z .,'-]{0,500}$/.test(value)) {
if (isValidName(value)) {
setName(toUpper(value))
resetErrors()
setElementTypeState(ElementTypes.CardHolder, value)
}
}
const onCountryChange = useCallback((country: SelectedCountry) => {
resetErrors()
if (country?.id === selectedCountry?.id) return
setSelectedCountry(country)
setElementTypeState(ElementTypes.CardCountry, country?.name_eng || '')
}, [resetErrors, selectedCountry?.id, setElementTypeState, setSelectedCountry])
const cardHolderState = inputStates[ElementTypes.CardHolder]
setInputStates({
[ElementTypes.CardHolder]: {
...cardHolderState,
empty: !value,
},
})
const onCityChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target
if (!isValidAddress(value)) {
setErrorMessage(translate('error_city_latin_letters'))
return
}
setCity(value)
resetErrors()
setElementTypeState(ElementTypes.CardCity, value)
}
const onAddressChange = (e: ChangeEvent<HTMLInputElement>) => {
const { value } = e.target
if (!isValidAddress(value)) {
setErrorMessage(translate('error_address_latin_letters'))
return
}
setAddress(value)
resetErrors()
setElementTypeState(ElementTypes.CardAddress, value)
}
const onInputsChange = (e: StripeElementChangeEvent) => {
@ -110,8 +163,15 @@ export const useFormSubmit = ({ onAddSuccess }: Props) => {
return
}
if (!name) {
setErrorMessage(translate('error_empty_name'))
const fieldError = validateFields({
address,
city,
country: selectedCountry?.name || '',
name,
})
if (fieldError) {
setErrorMessage(translate(fieldError))
return
}
@ -120,7 +180,12 @@ export const useFormSubmit = ({ onAddSuccess }: Props) => {
setLoader(true)
const { error: tokenError, token } = await stripe.createToken(
cardNumberElement,
{ name },
{
address_city: city,
address_country: selectedCountry?.name || '',
address_line1: address,
name,
},
)
if (tokenError) {
@ -139,14 +204,21 @@ export const useFormSubmit = ({ onAddSuccess }: Props) => {
}, [setCardError])
return {
address,
city,
countries,
errorMessage,
handleSubmit,
isLabelVisible,
loader,
name,
onAddressChange,
onCityChange,
onCountryChange,
onInputsBlur,
onInputsChange,
onInputsFocus,
onNameChange,
selectedCountry,
}
}

@ -0,0 +1,46 @@
import {
useEffect,
useState,
useMemo,
} from 'react'
import orderBy from 'lodash/orderBy'
import map from 'lodash/map'
import type { Countries, Country } from 'requests'
import { getCountries } from 'requests'
export type SelectedCountry = (Country & {
name: string,
}) | null
const useCountriesList = () => {
const [countries, setCountries] = useState<Countries>([])
useEffect(() => {
getCountries().then(setCountries)
}, [])
return countries
}
export const useCountries = () => {
const countries = useCountriesList()
const [selectedCountry, setSelectedCountry] = useState<SelectedCountry>(null)
const transformedCountries = useMemo(
() => orderBy(
map(countries, (country) => ({
...country,
name: country.name_eng,
})),
({ name }) => name,
),
[countries],
)
return {
countries: transformedCountries,
selectedCountry,
setSelectedCountry,
}
}

@ -0,0 +1,20 @@
import size from 'lodash/size'
export const isValidName = (value: string) => (/^[A-Za-z .,'-]{0,500}$/.test(value))
export const isValidAddress = (value: string) => (/^[A-Za-z0-9 #!.,'-_]{0,500}$/.test(value))
type fieldsType = {
address: string,
city: string,
country: string,
name: string,
}
export const validateFields = (fields: fieldsType) => {
if (!fields.name) return 'error_empty_name'
if (!fields.country) return 'error_empty_country'
if (size(fields.city) < 3) return 'error_empty_city'
if (size(fields.address) < 10) return 'error_empty_address'
return false
}

@ -1,3 +1,4 @@
import { useRouteMatch } from 'react-router-dom'
import {
CardNumberElement,
CardExpiryElement,
@ -5,6 +6,7 @@ import {
} from '@stripe/react-stripe-js'
import { isMobileDevice } from 'config/userAgent'
import { PAGES } from 'config/pages'
import { T9n } from 'features/T9n'
import { useCardsStore } from 'features/CardsStore'
@ -18,6 +20,8 @@ import { useFormSubmit, ElementTypes } from './hooks'
import {
Form,
Column,
CountryWrapper,
CustomCombobox,
ButtonsBlock,
Input,
Errors,
@ -55,16 +59,26 @@ export const AddCardFormInner = (props: Props) => {
inputsBackground,
} = props
const { error: cardError } = useCardsStore()
const isUserAccountPage = useRouteMatch(PAGES.useraccount)?.path === '/useraccount'
const {
address,
city,
countries,
errorMessage,
handleSubmit,
isLabelVisible,
loader,
name,
onAddressChange,
onCityChange,
onCountryChange,
onInputsBlur,
onInputsChange,
onInputsFocus,
onNameChange,
selectedCountry,
} = useFormSubmit(props)
return (
@ -129,7 +143,51 @@ export const AddCardFormInner = (props: Props) => {
/>
</ElementContainer>
</Column>
<ButtonsBlock>
<SectionTitle>
<T9n t='billing_address' />
</SectionTitle>
<Column>
<CountryWrapper>
<CustomCombobox
value={selectedCountry?.name || ''}
labelLexic={selectedCountry?.name ? '' : 'country'}
onSelect={onCountryChange}
onBlur={onInputsBlur(ElementTypes.CardCountry)}
options={countries}
withError={false}
selected={Boolean(selectedCountry?.name)}
noSearch
/>
</CountryWrapper>
<ElementContainer
label={isLabelVisible(ElementTypes.CardCity) ? 'city' : ''}
width='275px'
backgroundColor={inputsBackground}
>
<Input
type='text'
autoComplete='address-level2'
value={city}
onChange={onCityChange}
onFocus={onInputsFocus(ElementTypes.CardCity)}
onBlur={onInputsBlur(ElementTypes.CardCity)}
/>
</ElementContainer>
<ElementContainer
label={isLabelVisible(ElementTypes.CardAddress) ? 'address' : ''}
backgroundColor={inputsBackground}
>
<Input
type='text'
autoComplete='street-address'
value={address}
onChange={onAddressChange}
onFocus={onInputsFocus(ElementTypes.CardAddress)}
onBlur={onInputsBlur(ElementTypes.CardAddress)}
/>
</ElementContainer>
</Column>
<ButtonsBlock isUserAccountPage={isUserAccountPage}>
{(errorMessage || cardError) && (
<Errors>
{errorMessage}

@ -1,5 +1,16 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { Combobox } from 'features/Combobox'
import { PopOver } from 'features/Combobox/styled'
import {
InputStyled,
InputWrapper,
LabelTitle,
} from 'features/Common/Input/styled'
type ButtonsBlockTypes = {
isUserAccountPage?: boolean,
}
export const Form = styled.form``
@ -8,13 +19,14 @@ export const Column = styled.div`
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 8px;
`
export const ButtonsBlock = styled.div`
export const ButtonsBlock = styled.div<ButtonsBlockTypes>`
display: flex;
flex-direction: column;
align-items: start;
margin-top: 25px;
align-items: ${({ isUserAccountPage }) => (isUserAccountPage ? 'start' : 'center')};
margin-top: 15px;
`
export const Input = styled.input`
@ -80,3 +92,26 @@ export const SectionTitle = styled.span`
: ''};
`
export const CountryWrapper = styled.div`
width: 275px;
`
export const CustomCombobox = styled(Combobox)`
${InputWrapper}{
height: 50px;
margin-top: 0;
}
${LabelTitle}{
line-height:45px;
font-size: 16px;
}
${InputStyled}{
height: 50px;
margin-left: 0;
}
${PopOver}{
top: 55px;
max-height: 300px;
}
` as typeof Combobox

@ -28,6 +28,7 @@ import { Arrow } from './components/Arrow'
export const Combobox = <T extends Option>(props: Props<T>) => {
const {
className,
disabled,
error,
label,
@ -58,12 +59,15 @@ export const Combobox = <T extends Option>(props: Props<T>) => {
const isUserAccountPage = useRouteMatch(PAGES.useraccount)?.isExact || false
return (
<Column isUserAccountPage={isUserAccountPage}>
<Column isUserAccountPage={isUserAccountPage} className={className}>
<InputWrapper
error={error}
>
<Label>
<LabelTitle labelWidth={labelWidth} isUserAccountPage={isUserAccountPage}>
<LabelTitle
labelWidth={labelWidth}
isUserAccountPage={isUserAccountPage}
>
{labelLexic ? <T9n t={labelLexic} /> : label}
</LabelTitle>
<InputStyled
@ -88,9 +92,7 @@ export const Combobox = <T extends Option>(props: Props<T>) => {
/>
{isOpen && !isEmpty(options) && (
<OutsideClick onClick={onOutsideClick}>
<PopOver
ref={popoverRef}
>
<PopOver ref={popoverRef}>
{map(options, (option, i) => (
<ListOption
onClick={(e) => onOptionSelect(option.name, e)}

@ -10,7 +10,6 @@ export const Label = styled.label`
export const PopOver = styled.ul`
position: absolute;
max-height: 400px;
width: 100%;
top: 55px;
left: -1px;

@ -16,6 +16,7 @@ export type Props<T> = Pick<InputHTMLAttributes<HTMLInputElement>, (
| 'placeholder'
| 'onBlur'
)> & {
className?: string,
customListStyles?: CustomStyles,
error?: string | null,
label?: string,

@ -11,7 +11,6 @@ import { Logo } from 'features/Logo'
const HeaderStyled = styled.header`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 65px;
@ -31,13 +30,29 @@ const HeaderStyled = styled.header`
`
: ''};
`
const CustomHeaderGroup = styled(HeaderGroup)`
width: 100%;
justify-content: space-between;
`
const HomeIcon = styled.div`
display: block;
width: 30px;
height:30px;
background-size: contain;
background-repeat: no-repeat;
background-image: url(/images/home.png);
`
export const Header = () => (
<HeaderStyled>
<HeaderGroup>
<CustomHeaderGroup>
<Link to={PAGES.home}>
<Logo />
</Link>
</HeaderGroup>
<Link to={PAGES.home}>
<HomeIcon />
</Link>
</CustomHeaderGroup>
</HeaderStyled>
)

@ -1,70 +0,0 @@
import {
useEffect,
useState,
useMemo,
} from 'react'
import orderBy from 'lodash/orderBy'
import find from 'lodash/find'
import map from 'lodash/map'
import type { Countries, Country } from 'requests'
import { getCountries } from 'requests'
import { formIds } from 'config/form'
import { useLexicsStore } from 'features/LexicsStore'
import { useForm } from 'features/FormStore'
export type SelectedCountry = (Country & {
name: string,
}) | null
type Names = 'name_eng' | 'name_rus'
const useCountriesList = () => {
const [countries, setCountries] = useState<Countries>([])
useEffect(() => {
getCountries().then(setCountries)
}, [])
return countries
}
export const useCountries = () => {
const { readFormValue, updateFormValue } = useForm()
const countries = useCountriesList()
const { suffix } = useLexicsStore()
const [selectedCountry, setSelectedCountry] = useState<SelectedCountry>(null)
const transformedCountries = useMemo(
() => orderBy(
map(countries, (country) => {
const nameField = `name_${suffix}` as Names
return {
...country,
name: country[nameField],
}
}),
({ name }) => name,
),
[countries, suffix],
)
const onCountryBlur = () => {
updateFormValue(formIds.country)('')
}
const countryId = readFormValue(formIds.initialCountryId)
useEffect(() => {
const initialCountry = find(transformedCountries, { id: Number(countryId) })
setSelectedCountry(initialCountry || null)
}, [transformedCountries, countryId])
return {
countries: transformedCountries,
onCountryBlur,
selectedCountry,
setSelectedCountry,
}
}

@ -26,13 +26,7 @@ export const useUserInfo = ({ loader, onSubmit }: Props) => {
updateFormValue,
} = useForm()
const validateForm = useValidateForm()
const {
countries,
onCountryBlur,
onCountrySelect,
onPhoneBlur,
selectedCountry,
} = useUserInfoForm()
const { onPhoneBlur } = useUserInfoForm()
const readTrimmedValue = useCallback(
(fieldName: string) => trim(readFormValue(fieldName)) || null,
[readFormValue],
@ -92,18 +86,14 @@ export const useUserInfo = ({ loader, onSubmit }: Props) => {
}
return {
countries,
handleSubmit,
hasChanges,
lang: selectedlangOption,
loader,
onCountryBlur,
onCountrySelect,
onLangSelect,
onPhoneBlur,
readFormError,
readFormValue,
selectedCountry,
updateFormValue,
}
}

@ -1,18 +1,12 @@
import type { ChangeEvent } from 'react'
import { useCallback } from 'react'
import trim from 'lodash/trim'
import { isValidPhone } from 'helpers/isValidPhone'
import { formatPhoneCode } from 'helpers/formatPhoneCode'
import { formIds } from 'config/form'
import { useForm } from 'features/FormStore'
import type { SelectedCountry } from './useCountries'
import { useCountries } from './useCountries'
export const useUserInfoForm = () => {
const {
readFormValue,
@ -20,13 +14,6 @@ export const useUserInfoForm = () => {
updateFormValue,
} = useForm()
const {
countries,
onCountryBlur,
selectedCountry,
setSelectedCountry,
} = useCountries()
const onPhoneBlur = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => {
const phone = target.value
if (phone && !isValidPhone(phone)) {
@ -34,39 +21,9 @@ export const useUserInfoForm = () => {
}
}, [updateFormError])
const phone = trim(readFormValue(formIds.phone))
const onCountrySelect = useCallback((country: SelectedCountry) => {
if (country?.id === selectedCountry?.id) return
setSelectedCountry(country)
updateFormValue(formIds.countryId)(country?.id ? String(country?.id) : '')
updateFormValue(formIds.region)('')
const selectedCountryCode = formatPhoneCode(selectedCountry?.phone_code || '')
const hasPhoneNumber = (
phone
&& selectedCountryCode
&& phone !== selectedCountryCode
)
if (!hasPhoneNumber) {
const code = formatPhoneCode(country?.phone_code || '')
updateFormValue(formIds.phone)(code)
}
}, [
phone,
selectedCountry,
setSelectedCountry,
updateFormValue,
])
return {
countries,
onCountryBlur,
onCountrySelect,
onPhoneBlur,
readFormValue,
selectedCountry,
setSelectedCountry,
updateFormValue,
}
}

@ -21,7 +21,6 @@ import {
const labelWidth = 76
const {
country,
email,
firstname,
formError,
@ -31,18 +30,14 @@ const {
export const PersonalInfoForm = (props: Props) => {
const {
countries,
handleSubmit,
hasChanges,
lang,
loader,
onCountryBlur,
onCountrySelect,
onLangSelect,
onPhoneBlur,
readFormError,
readFormValue,
selectedCountry,
updateFormValue,
} = useUserInfo(props)
@ -91,17 +86,6 @@ export const PersonalInfoForm = (props: Props) => {
withError={false}
disabled
/>
<Combobox
value={selectedCountry?.name ?? readFormValue(country)}
labelLexic='form_country'
labelWidth={labelWidth}
onChange={updateFormValue(country)}
onSelect={onCountrySelect}
onBlur={onCountryBlur}
options={countries}
withError={false}
selected={Boolean(selectedCountry)}
/>
<Combobox
noSearch
selected

Loading…
Cancel
Save