diff --git a/public/images/home.png b/public/images/home.png new file mode 100644 index 00000000..47113841 Binary files /dev/null and b/public/images/home.png differ diff --git a/src/config/lexics/payment.tsx b/src/config/lexics/payment.tsx index eecfb17d..d298de17 100644 --- a/src/config/lexics/payment.tsx +++ b/src/config/lexics/payment.tsx @@ -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, } diff --git a/src/config/lexics/userAccount.tsx b/src/config/lexics/userAccount.tsx index 7b055729..37df1a87 100644 --- a/src/config/lexics/userAccount.tsx +++ b/src/config/lexics/userAccount.tsx @@ -13,7 +13,6 @@ const navigations = { export const userAccountLexics = { change: 12614, change_password: 15054, - country: 835, delete: 848, delete_card: 8692, language: 15053, diff --git a/src/features/AddCardForm/components/ElementContainer/index.tsx b/src/features/AddCardForm/components/ElementContainer/index.tsx index 26164392..ed093cd9 100644 --- a/src/features/AddCardForm/components/ElementContainer/index.tsx +++ b/src/features/AddCardForm/components/ElementContainer/index.tsx @@ -28,10 +28,7 @@ const Label = styled.label` 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` diff --git a/src/features/AddCardForm/components/Form/hooks/index.tsx b/src/features/AddCardForm/components/Form/hooks/index.tsx index 938edb60..fec42dde 100644 --- a/src/features/AddCardForm/components/Form/hooks/index.tsx +++ b/src/features/AddCardForm/components/Form/hooks/index.tsx @@ -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) => { 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) => { + 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) => { + 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, } } diff --git a/src/features/AddCardForm/components/Form/hooks/useCountries.tsx b/src/features/AddCardForm/components/Form/hooks/useCountries.tsx new file mode 100644 index 00000000..5924f048 --- /dev/null +++ b/src/features/AddCardForm/components/Form/hooks/useCountries.tsx @@ -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([]) + + useEffect(() => { + getCountries().then(setCountries) + }, []) + + return countries +} + +export const useCountries = () => { + const countries = useCountriesList() + const [selectedCountry, setSelectedCountry] = useState(null) + const transformedCountries = useMemo( + () => orderBy( + map(countries, (country) => ({ + ...country, + name: country.name_eng, + })), + ({ name }) => name, + ), + [countries], + ) + + return { + countries: transformedCountries, + selectedCountry, + setSelectedCountry, + } +} diff --git a/src/features/AddCardForm/components/Form/hooks/validateFields.tsx b/src/features/AddCardForm/components/Form/hooks/validateFields.tsx new file mode 100644 index 00000000..0b0527e4 --- /dev/null +++ b/src/features/AddCardForm/components/Form/hooks/validateFields.tsx @@ -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 +} diff --git a/src/features/AddCardForm/components/Form/index.tsx b/src/features/AddCardForm/components/Form/index.tsx index b6dbc23c..82fdac83 100644 --- a/src/features/AddCardForm/components/Form/index.tsx +++ b/src/features/AddCardForm/components/Form/index.tsx @@ -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) => { /> - + + + + + + + + + + + + + + + {(errorMessage || cardError) && ( {errorMessage} diff --git a/src/features/AddCardForm/styled.tsx b/src/features/AddCardForm/styled.tsx index 34e2ccc4..e4e541e8 100644 --- a/src/features/AddCardForm/styled.tsx +++ b/src/features/AddCardForm/styled.tsx @@ -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` 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 diff --git a/src/features/Combobox/index.tsx b/src/features/Combobox/index.tsx index 8534854c..4f8b57a4 100644 --- a/src/features/Combobox/index.tsx +++ b/src/features/Combobox/index.tsx @@ -28,6 +28,7 @@ import { Arrow } from './components/Arrow' export const Combobox = (props: Props) => { const { + className, disabled, error, label, @@ -58,12 +59,15 @@ export const Combobox = (props: Props) => { const isUserAccountPage = useRouteMatch(PAGES.useraccount)?.isExact || false return ( - +