From 2994726834c00d219d278180899612be991007aa Mon Sep 17 00:00:00 2001 From: Armen <35077035+Armen9393@users.noreply.github.com> Date: Wed, 30 Sep 2020 13:34:59 +0300 Subject: [PATCH] Ott 415 change user account data (#159) * fix(ott-394): deleted search box target blank * fix(ott-394): minor change * fix(ott-415): not finished yet * fix(ott-415): not finished yet * fix(ott-415): not finished yet * fix(ott-415): not finished yet * fix(ott-415): not finished yet * fix(ott-415): added configs * fix(ott-415): partly fixed validation * fix(ott-415): almost finished * fix(ott-415): almost finished * fix(ott-415): 90% finished * fix(ott-415): deleted offset from infinite scroll * fix(ott-415): minor bug * fix(ott-415): deleted console.logs * fix(ott-415): deleted delete test component * fix(ott-415): deleted mutation * fix(ott-415): merged dropdown with combobox * fix(ott-415): deleted dropdown * fix(ott-415): changed any to string * fix(ott-415): minor bug fix * fix(ott-415): fixed some bugs * fix(ott-415): added types * fix(ott-415): deleted filter options * fix(ott-415): user account consistency * fix(ott-415): added onClickOutside wrapper * fix(ott-415): finish --- src/config/lexics/userAccount.tsx | 3 + src/config/procedures.tsx | 2 + src/features/App/AuthenticatedApp.tsx | 10 +- src/features/App/UnauthenticatedApp.tsx | 3 +- .../Combobox/components/Arrow/index.tsx | 17 +-- src/features/Combobox/hooks/index.tsx | 114 +++++++++++++---- src/features/Combobox/index.tsx | 110 ++++++++-------- src/features/Combobox/styled.tsx | 117 ++++-------------- src/features/Combobox/types.tsx | 7 -- src/features/Common/Checkbox/index.tsx | 1 - src/features/Common/Input/index.tsx | 48 ++++--- src/features/Common/Input/stories.tsx | 4 - src/features/Common/Input/styled.tsx | 15 ++- src/features/Common/Radio/index.tsx | 1 - src/features/FormStore/hooks/useFormState.tsx | 5 +- .../components/TournamentFilter/hooks.tsx | 2 - src/features/Login/index.tsx | 2 - src/features/Modal/index.tsx | 17 +-- src/features/OutsideClick/hooks/index.tsx | 10 +- src/features/OutsideClick/index.tsx | 11 +- .../Register/components/CardStep/index.tsx | 3 - .../RegistrationStep/hooks/useForm.tsx | 6 +- .../hooks/useSubmitHandler.tsx | 2 +- .../components/RegistrationStep/index.tsx | 13 -- src/features/Search/index.tsx | 1 - src/features/UserAccount/config.tsx | 28 +++++ src/features/UserAccount/hooks.tsx | 117 ++++++++++++++++++ src/features/UserAccount/index.tsx | 97 +++++++++------ src/features/UserAccount/styled.tsx | 7 ++ src/features/UserAccount/useValidateForm.tsx | 57 +++++++++ .../hooks/useCountries.tsx | 0 src/requests/getUserInfo.tsx | 19 +++ src/requests/index.tsx | 2 + src/requests/saveUserInfo.tsx | 74 +++++++++++ 34 files changed, 613 insertions(+), 312 deletions(-) create mode 100644 src/features/UserAccount/config.tsx create mode 100644 src/features/UserAccount/hooks.tsx create mode 100644 src/features/UserAccount/useValidateForm.tsx rename src/{features/Register/components/RegistrationStep => }/hooks/useCountries.tsx (100%) create mode 100644 src/requests/getUserInfo.tsx create mode 100644 src/requests/saveUserInfo.tsx diff --git a/src/config/lexics/userAccount.tsx b/src/config/lexics/userAccount.tsx index 16e9cbb3..3108a9a1 100644 --- a/src/config/lexics/userAccount.tsx +++ b/src/config/lexics/userAccount.tsx @@ -1,3 +1,5 @@ +import { publicLexics } from 'config/lexics/public' + export const userAccountLexics = { add_card: 8313, change: 12614, @@ -15,4 +17,5 @@ export const userAccountLexics = { select_subscription: 12583, subscriptions: 13016, user_account: 12928, + ...publicLexics, } diff --git a/src/config/procedures.tsx b/src/config/procedures.tsx index ab2522f1..3e9d1dd3 100644 --- a/src/config/procedures.tsx +++ b/src/config/procedures.tsx @@ -11,9 +11,11 @@ export const PROCEDURES = { get_tournament_info: 'get_tournament_info', get_tournament_list: 'get_tournament_list', get_user_favorites: 'get_user_favorites', + get_user_info: 'get_user_info', logout_user: 'logout_user', lst_c_country: 'lst_c_country', param_lexical: 'param_lexical', save_user_favorite: 'save_user_favorite', + save_user_info: 'save_user_info', save_user_match_second: 'save_user_match_second', } diff --git a/src/features/App/AuthenticatedApp.tsx b/src/features/App/AuthenticatedApp.tsx index 9f7370e5..a581b910 100644 --- a/src/features/App/AuthenticatedApp.tsx +++ b/src/features/App/AuthenticatedApp.tsx @@ -30,6 +30,7 @@ import { UserFavorites } from 'features/UserFavorites' import { UserFavoritesStore } from 'features/UserFavorites/store' import { useMediaQuery } from 'features/MediaQuery' import { HeaderMobile } from 'features/HeaderMobile' +import { FormStore } from 'features/FormStore' export const AuthenticatedApp = () => { useLexicsConfig(indexLexics) @@ -49,9 +50,8 @@ export const AuthenticatedApp = () => { } - + - @@ -100,3 +100,9 @@ export const AuthenticatedApp = () => { ) } + +export const UserAccountForm = () => ( + + + +) diff --git a/src/features/App/UnauthenticatedApp.tsx b/src/features/App/UnauthenticatedApp.tsx index 90b464bb..d982fe5e 100644 --- a/src/features/App/UnauthenticatedApp.tsx +++ b/src/features/App/UnauthenticatedApp.tsx @@ -8,13 +8,14 @@ import { import { PAGES } from 'config' import { publicLexics } from 'config/lexics/public' +import { useLexicsConfig } from 'features/LexicsStore' import { Login } from 'features/Login' import { Register } from 'features/Register' import { LanguageSelect } from 'features/LanguageSelect' -import { useLexicsConfig } from 'features/LexicsStore' export const UnauthenticatedApp = () => { useLexicsConfig(publicLexics) + return ( diff --git a/src/features/Combobox/components/Arrow/index.tsx b/src/features/Combobox/components/Arrow/index.tsx index 523981a3..ef407a9f 100644 --- a/src/features/Combobox/components/Arrow/index.tsx +++ b/src/features/Combobox/components/Arrow/index.tsx @@ -2,7 +2,10 @@ import React from 'react' import styled from 'styled-components/macro' -import { useComboboxContext } from '@reach/combobox' +type Props = { + isExpanded: boolean, + toggle: () => void, +} const Wrapper = styled.div<{ isExpanded: boolean }>` position: absolute; @@ -19,14 +22,14 @@ const Wrapper = styled.div<{ isExpanded: boolean }>` )}); background-position: center; background-repeat: no-repeat; - :hover { cursor: pointer; } ` -export const Arrow = () => { - const { isExpanded } = useComboboxContext() - - return -} +export const Arrow = ({ isExpanded, toggle }: Props) => ( + +) diff --git a/src/features/Combobox/hooks/index.tsx b/src/features/Combobox/hooks/index.tsx index 84b5cfd8..95e8280c 100644 --- a/src/features/Combobox/hooks/index.tsx +++ b/src/features/Combobox/hooks/index.tsx @@ -1,19 +1,22 @@ -import type { ChangeEvent, FocusEvent } from 'react' -import { useState, useCallback } from 'react' +import type { ChangeEvent } from 'react' +import { + useState, + useCallback, + useRef, + useEffect, +} from 'react' -import isUndefined from 'lodash/isUndefined' import toLower from 'lodash/toLower' import find from 'lodash/find' import trim from 'lodash/trim' +import size from 'lodash/size' + +import { useToggle, useEventListener } from 'hooks' import type { Props, Option } from '../types' import { matchSort } from '../helpers' import { useKeyboardScroll } from './useKeyboardScroll' -const isOptionClicked = (target: HTMLElement) => ( - target?.getAttribute('role') === 'option' -) - const useQuery = ({ onChange, value }: Props) => { const [query, setQuery] = useState('') @@ -22,59 +25,124 @@ const useQuery = ({ onChange, value }: Props) => { }, []) return { - onQueryChange: !isUndefined(onChange) ? onChange : onQueryChange, - query: !isUndefined(value) ? value : query, + onQueryChange: onChange ?? onQueryChange, + query: value ?? query, setQuery, } } export const useCombobox = (props: Props) => { const { - filterOptions = true, onSelect, options, } = props + const inputFieldRef = useRef(null) + const [index, setIndex] = useState(0) + + const { onKeyDown, popoverRef } = useKeyboardScroll() + const { onQueryChange, query, setQuery, } = useQuery(props) + const { + close, + isOpen, + open, + toggle, + } = useToggle() + const results = matchSort( options, 'name', query, ) - const findOptionByName = (optionName: string) => ( + const findOptionByName = useCallback((optionName: string) => ( find( options, ({ name }) => toLower(name) === toLower(trim(optionName)), ) || null - ) + ), [options]) - const onOptionSelect = (option: string) => { + const onOptionSelect = useCallback((option: string) => { const selectedOption = findOptionByName(option) setQuery(selectedOption?.name || '') onSelect?.(selectedOption) + close() + }, [ + close, + findOptionByName, + onSelect, + setQuery, + ]) + + const onOutsideClick = (event: MouseEvent) => { + if (event.target !== inputFieldRef.current as HTMLInputElement) { + close() + } } - const onInputBlur = (event: FocusEvent) => { - const target = event.relatedTarget as HTMLElement | null - // клик по элементу списка тоже вызывает onBlur - // если кликали элемент списка то событие обрабатывает onOptionSelect - if (target && isOptionClicked(target)) return + const optionSelectListener = useCallback((event: KeyboardEvent) => { + if (event.key === 'ArrowUp') { + setIndex(index - 1) + } else if (event.key === 'ArrowDown') { + setIndex(index + 1) + } else if (event.key === 'Enter') { + onSelect?.(results[index]) + close() + inputFieldRef.current?.blur() + } + }, [ + close, + index, + results, + onSelect, + ]) - onOptionSelect(query) - } + useEffect(() => { + const lastElementIndex = size(results) - 1 + if (index < 0) setIndex(0) + if (index >= lastElementIndex) setIndex(lastElementIndex) + }, [ + index, + options, + results, + ]) + + useEventListener({ + callback: optionSelectListener, + event: 'keydown', + }) + + const escPressListener = useCallback((event: KeyboardEvent) => { + if (event.key === 'Escape') { + close() + inputFieldRef.current?.blur() + } + }, [close]) + + useEventListener({ + callback: escPressListener, + event: 'keydown', + }) return { - ...useKeyboardScroll(), - onInputBlur, + close, + index, + inputFieldRef, + isOpen, + onKeyDown, onOptionSelect, + onOutsideClick, onQueryChange, - options: filterOptions ? results : options, + open, + options: results, + popoverRef, query, + toggle, } } diff --git a/src/features/Combobox/index.tsx b/src/features/Combobox/index.tsx index 1e08bdc0..78f4d214 100644 --- a/src/features/Combobox/index.tsx +++ b/src/features/Combobox/index.tsx @@ -2,104 +2,98 @@ import React from 'react' import isEmpty from 'lodash/isEmpty' import map from 'lodash/map' -import '@reach/combobox/styles.css' import { T9n } from 'features/T9n' -import { useLexicsStore } from 'features/LexicsStore' +import { OutsideClick } from 'features/OutsideClick' import { - Label, Column, Error, + InputWrapper, + InputStyled, + Label, + LabelTitle, } from 'features/Common/Input/styled' import { Props, Option } from './types' import { useCombobox } from './hooks' import { - ComboboxStyled, - ComboboxInputStyled, - ComboboxPopoverStyled, - ComboboxListStyled, - ComboboxOptionStyled, + PopOver, + ListOption, } from './styled' + import { Arrow } from './components/Arrow' export const Combobox = (props: Props) => { const { - customListStyles, disabled, error, - id, label, labelLexic, labelWidth, - openOnFocus, - pattern, - readOnly, - required, title, withArrow, } = props + const { - onInputBlur, + index, + inputFieldRef, + isOpen, onKeyDown, onOptionSelect, + onOutsideClick, onQueryChange, + open, options, popoverRef, query, + toggle, } = useCombobox(props) - const { translate } = useLexicsStore() return ( - - + ) diff --git a/src/features/Combobox/styled.tsx b/src/features/Combobox/styled.tsx index d773d775..c478e71b 100644 --- a/src/features/Combobox/styled.tsx +++ b/src/features/Combobox/styled.tsx @@ -1,100 +1,27 @@ import styled from 'styled-components/macro' -import { devices } from 'config/devices' - -import { - Combobox, - ComboboxInput, - ComboboxList, - ComboboxOption, - ComboboxPopover, -} from '@reach/combobox' - -import { - wrapperStyles, - inputStyles, - Label, -} from 'features/Common/Input/styled' import { customScrollbar, customStylesMixin } from 'features/Common' -export const ComboboxStyled = styled(Combobox)` - ${wrapperStyles} - position: relative; - - @media ${devices.laptop} { - margin-top: 12px; - - ${Label} { - display: block; - - span { - display: none; - } - } - } - - @media ${devices.tablet} { - margin-top: -8px; - } -` - -export const ComboboxInputStyled = styled(ComboboxInput)` - ${inputStyles} - padding-right: 24px; - - ::placeholder { - opacity: 0; - } - - @media ${devices.laptop} { - - margin-left: -105px; - padding-right: 4px; - ::placeholder { - opacity: 1; - width: 90%; - font-style: normal; - font-weight: normal; - white-space: nowrap; - font-size: 16px; - line-height: 24px; - letter-spacing: -0.01em; - text-align: center; - } - } - - @media ${devices.mobile} { - text-align: center; - font-size: 14px; - } -` - -export const ComboboxPopoverStyled = styled(ComboboxPopover)` - border: none; +export const Label = styled.label` + letter-spacing: -0.01em; + color: #999999; + font-size: 16px; ` -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); +export const PopOver = styled.ul` position: absolute; - top: 9px; - left: -164px; + min-height: 70px; + max-height: 400px; + width: 100%; + top: 55px; + left: 0px; overflow: auto; - + z-index: 2; ${customScrollbar} ${customStylesMixin} - - @media ${devices.laptop} { - left: -33px; - min-width: calc(100% + 58px); - } ` -export const ComboboxOptionStyled = styled(ComboboxOption)` +export const ListOption = styled.li<{isHighlighted?: boolean}>` width: 100%; height: 48px; font-size: 16px; @@ -102,16 +29,20 @@ export const ComboboxOptionStyled = styled(ComboboxOption)` display: flex; align-items: center; padding-left: 24px; - color: #ccc; - background: transparent; - - &[aria-selected="true"] { - background: #999; - color: #fff; - } + color: ${({ isHighlighted }) => ( + isHighlighted + ? '#fff' + : '#ccc' + )}; + background-color: ${({ isHighlighted }) => ( + isHighlighted + ? '#999' + : '#3F3F3F' + )}; + cursor: pointer; &:hover { - background: #999; + background-color: #999; color: #fff; } ` diff --git a/src/features/Combobox/types.tsx b/src/features/Combobox/types.tsx index d980a63a..08cb1e01 100644 --- a/src/features/Combobox/types.tsx +++ b/src/features/Combobox/types.tsx @@ -13,24 +13,17 @@ export type Option = { } export type Props = Pick, ( - | 'id' - | 'onChange' - | 'required' | 'disabled' - | 'pattern' | 'title' | 'placeholder' - | 'readOnly' )> & { customListStyles?: CustomStyles, error?: string | null, - filterOptions?: boolean, label?: string, labelLexic?: string, labelWidth?: number, onChange?: (event: ChangeEvent) => void, onSelect?: (option: T | null) => void, - openOnFocus?: boolean, options: Array, value?: string, withArrow?: boolean, diff --git a/src/features/Common/Checkbox/index.tsx b/src/features/Common/Checkbox/index.tsx index a2938754..69561388 100644 --- a/src/features/Common/Checkbox/index.tsx +++ b/src/features/Common/Checkbox/index.tsx @@ -26,7 +26,6 @@ export const Checkbox = ({ }: Props) => ( { const { translate } = useLexicsStore() + return ( - diff --git a/src/features/Common/Input/stories.tsx b/src/features/Common/Input/stories.tsx index 75144628..efe1fc6d 100644 --- a/src/features/Common/Input/stories.tsx +++ b/src/features/Common/Input/stories.tsx @@ -9,7 +9,6 @@ export default { export const Empty = () => ( @@ -18,14 +17,12 @@ export const Empty = () => ( export const EmailPassword = () => ( ( export const WithError = () => ( ` border-width: 1px; ` -type LabelProps = { +type TitleProps = { labelWidth?: number, } -export const Label = styled.label` +export const Label = styled.label` + width: 100%; + display: flex; +` +export const LabelTitle = styled.p` font-style: normal; font-weight: normal; white-space: nowrap; @@ -42,8 +46,8 @@ export const Label = styled.label` padding-top: 2px; color: ${({ theme: { colors } }) => colors.secondary}; width: ${({ labelWidth }) => (labelWidth ? `${labelWidth}px` : '')}; - - @media ${devices.laptop} { + + @media ${devices.laptop} { display: none; } ` @@ -117,8 +121,8 @@ export const InputStyled = styled.input` } } ` - export const InputWrapper = styled.div` + position: relative; ${wrapperStyles} @@ -150,6 +154,7 @@ type ErrorProps = { } export const Error = styled(T9n)` + display: block; min-height: 16px; margin-top: 5px; font-style: normal; diff --git a/src/features/Common/Radio/index.tsx b/src/features/Common/Radio/index.tsx index 4a8116ec..587b36f5 100644 --- a/src/features/Common/Radio/index.tsx +++ b/src/features/Common/Radio/index.tsx @@ -26,7 +26,6 @@ export const Radio = ({ }: Props) => ( { const updateFormValue = useCallback( (fieldName: string) => ( (event: ChangeEvent | string) => { - const value = isString(event) ? event : event.target.value + const value = isObject(event) ? event.target.value : event setFormState(((state) => { const newState = { ...state, @@ -55,6 +55,7 @@ export const useFormState = () => { }, [], ) + return { readFormError, readFormValue, diff --git a/src/features/HeaderFilters/components/TournamentFilter/hooks.tsx b/src/features/HeaderFilters/components/TournamentFilter/hooks.tsx index 54f73e35..3063835f 100644 --- a/src/features/HeaderFilters/components/TournamentFilter/hooks.tsx +++ b/src/features/HeaderFilters/components/TournamentFilter/hooks.tsx @@ -32,7 +32,6 @@ export const useTournamentFilter = () => { getSportTournaments(selectedSportTypeId).then(setList) }, [selectedSportTypeId]) - const [offset, setOffset] = useState(0) const [requestSent, setRequestSent] = useState(false) const handleScroll = (e: BaseSyntheticEvent) => { @@ -41,7 +40,6 @@ export const useTournamentFilter = () => { if (scrollPositionBottom && !requestSent) { setRequestSent(true) - setOffset(offset + 1) getSportTournaments(selectedSportTypeId).then((res) => { setList([...list, ...res]) setRequestSent(false) diff --git a/src/features/Login/index.tsx b/src/features/Login/index.tsx index 9a49d87e..c3fe0bc9 100644 --- a/src/features/Login/index.tsx +++ b/src/features/Login/index.tsx @@ -40,7 +40,6 @@ const LoginForm = () => { { labelWidth={labelWidth} /> - - - {children} - + + + + {children} + + , modalRoot.current as Element, ) diff --git a/src/features/OutsideClick/hooks/index.tsx b/src/features/OutsideClick/hooks/index.tsx index eea659ac..b8c7c8aa 100644 --- a/src/features/OutsideClick/hooks/index.tsx +++ b/src/features/OutsideClick/hooks/index.tsx @@ -3,21 +3,21 @@ import { useRef } from 'react' import { useEventListener } from 'hooks' type Args = { - onClick: Function, + onClick: (event: MouseEvent) => void, } export const useOutsideClickEffect = ({ onClick, }: Args) => { const wrapperRef = useRef(null) - const handleOutsideClick = ({ target }: MouseEvent) => { - const targetNode = target instanceof Node - ? target + const handleOutsideClick = (e: MouseEvent) => { + const targetNode = e.target instanceof Node + ? e.target : null /** деструктуризация wrapperRef не сработает, ссылка на реф теряется */ if (!wrapperRef.current?.contains(targetNode)) { - onClick() + onClick(e) } } diff --git a/src/features/OutsideClick/index.tsx b/src/features/OutsideClick/index.tsx index 822cb023..0dec7657 100644 --- a/src/features/OutsideClick/index.tsx +++ b/src/features/OutsideClick/index.tsx @@ -8,20 +8,23 @@ import { useOutsideClickEffect } from './hooks' type Props = { /** элемент, которому необходим функционал `OutsideClick` */ children: ReactNode, + fullWidth?: boolean, /** функция-коллбек, отрабатывающая по клику вне области элемента */ - onClick: () => void, + onClick: (event: MouseEvent) => void, } -const OutsideClickWrapper = styled.div`` +const OutsideClickWrapper = styled.div<{fullWidth: boolean}>` + width: ${({ fullWidth }) => (fullWidth ? '100%' : '')}; +` export const OutsideClick = ({ children, + fullWidth = false, onClick, }: Props) => { const wrapperRef = useOutsideClickEffect({ onClick }) - return ( - + {children} ) diff --git a/src/features/Register/components/CardStep/index.tsx b/src/features/Register/components/CardStep/index.tsx index 16acfd2c..068ec120 100644 --- a/src/features/Register/components/CardStep/index.tsx +++ b/src/features/Register/components/CardStep/index.tsx @@ -22,7 +22,6 @@ export const CardStep = () => { { /> { updateFormError, updateFormValue, } = useForm() + const { countries, selectedCountry, setSelectedCountry, } = useCountries() + const { cities, getCities, diff --git a/src/features/Register/components/RegistrationStep/hooks/useSubmitHandler.tsx b/src/features/Register/components/RegistrationStep/hooks/useSubmitHandler.tsx index b37e1727..e0fa53ac 100644 --- a/src/features/Register/components/RegistrationStep/hooks/useSubmitHandler.tsx +++ b/src/features/Register/components/RegistrationStep/hooks/useSubmitHandler.tsx @@ -7,8 +7,8 @@ import type { City } from 'requests' import { useAuthStore } from 'features/AuthStore' import { useForm } from 'features/FormStore' +import type { SelectedCountry } from 'hooks/useCountries' import { formIds } from '../config' -import type { SelectedCountry } from './useCountries' import { useValidateForm } from './useValidateForm' type Args = { diff --git a/src/features/Register/components/RegistrationStep/index.tsx b/src/features/Register/components/RegistrationStep/index.tsx index 8f1ea91c..813daa1b 100644 --- a/src/features/Register/components/RegistrationStep/index.tsx +++ b/src/features/Register/components/RegistrationStep/index.tsx @@ -36,9 +36,7 @@ const Registration = () => { - { onChange={updateFormValue(formIds.firstname)} /> { onChange={updateFormValue(formIds.lastname)} /> { onBlur={onEmailBlur} /> { onChange={updateFormValue(formIds.password)} /> { onSelect={onCountrySelect} /> { onChange={onRegionOrCityChange(formIds.region)} /> { onSelect={onCitySelect} /> { onChange={updateFormValue(formIds.postalCode)} /> { onChange={updateFormValue(formIds.address1)} /> { onChange={updateFormValue(formIds.address2)} /> {
{ + const { + readFormError, + readFormValue, + updateFormError, + updateFormValue, + } = useForm() + + const { suffix } = useLexicsStore() + const validateForm = useValidateForm() + const saveButton = useRef(null) + + const { + countries, + setSelectedCountry, + } = useCountries() + + const readTrimmedValue = useCallback( + (fieldName: string) => trim(readFormValue(fieldName)), + [readFormValue], + ) + + const handleSubmit = useCallback(() => { + if (validateForm()) { + const firstname = readTrimmedValue(formIds.firstname) + const lastname = readTrimmedValue(formIds.lastname) + const phone = readTrimmedValue(formIds.phone) + const password = readTrimmedValue(formIds.password) + const postalCode = Number(readTrimmedValue(formIds.postalCode)) + const region = readTrimmedValue(formIds.region) + const address_line1 = readTrimmedValue(formIds.address_line1) + const address_line2 = readTrimmedValue(formIds.address_line2) + const city = readTrimmedValue(formIds.city) + const city_id = Number(readTrimmedValue(formIds.city_id)) + const countryId = Number(readTrimmedValue(formIds.countryId)) + + saveUserInfo({ + address_line1, + address_line2, + city, + city_id, + countryId, + firstname, + lastname, + password, + phone, + postalCode, + region, + }).then(() => saveButton?.current?.blur()) + } + }, [ + readTrimmedValue, + validateForm, + ]) + + const onCountrySelect = useCallback((country: SelectedCountry) => { + setSelectedCountry(country) + const countryName = country ? country[`name_${suffix}` as Name] : '' + updateFormValue(formIds.country)(countryName) + // TO DO пока не решили сбрасываем ли город и адрес + // updateFormValue(formIds.region)('') + updateFormValue(formIds.countryId)(`${country?.id}`) + }, [ + setSelectedCountry, + updateFormValue, + suffix, + ]) + + useEffect(() => { + getUserInfo().then((res: UserInfo) => { + forEach(res, (value: string | number | SelectedCountry, key: string) => { + if (value && typeof value === 'object') { + onCountrySelect(value) + } else if (isString(value)) { + updateFormValue(key)(value) + } + }) + }) + }, [updateFormValue, onCountrySelect]) + + return { + countries, + handleSubmit, + onCountrySelect, + readFormError, + readFormValue, + saveButton, + updateFormError, + updateFormValue, + } +} diff --git a/src/features/UserAccount/index.tsx b/src/features/UserAccount/index.tsx index 190dbe0f..1451f120 100644 --- a/src/features/UserAccount/index.tsx +++ b/src/features/UserAccount/index.tsx @@ -1,19 +1,21 @@ import React from 'react' +import { userAccountLexics } from 'config/lexics/userAccount' + import { Background } from 'features/Background' import { Combobox } from 'features/Combobox' import { Input } from 'features/Common' import { Form } from 'features/Login/styled' import { T9n } from 'features/T9n' +import { Error } from 'features/Common/Input/styled' import { useLexicsStore, useLexicsConfig } from 'features/LexicsStore' - -import { userAccountLexics } from 'config/lexics/userAccount' - import { CardNumber } from './CardNumber' import { UserAccountButton } from './UserAccountButton' import { PageTitle } from './PageTitle' import { UserAccountSubscription } from './UserAccountSubscription' +import { formIds } from './config' import { TextNoBorder } from './TextNoBorder' +import { useUserInfo } from './hooks' import { FormWrapper, @@ -22,14 +24,26 @@ import { UserAccountFormWrapper, UserAccountBlockTitle, UserAccountComponentWrapper, + ButtonWrapper, } from './styled' const labelWidth = 78 export const UserAccount = () => { useLexicsConfig(userAccountLexics) + const { translate } = useLexicsStore() + const { + countries, + handleSubmit, + onCountrySelect, + readFormError, + readFormValue, + saveButton, + updateFormValue, + } = useUserInfo() + return ( @@ -42,39 +56,46 @@ export const UserAccount = () => { - - - - + + + + + + @@ -138,35 +159,31 @@ export const UserAccount = () => { diff --git a/src/features/UserAccount/styled.tsx b/src/features/UserAccount/styled.tsx index 0ec1427a..1a9942fe 100644 --- a/src/features/UserAccount/styled.tsx +++ b/src/features/UserAccount/styled.tsx @@ -22,6 +22,13 @@ export const UserAccountFormWrapper = styled.div` flex-wrap: wrap; ` +export const ButtonWrapper = styled.div` + display: flex; + align-self: flex-start; + flex-direction: column; + align-items: center; +` + export const UserAccountWrapper = styled.div` width: 1776px; margin-top: 140px; diff --git a/src/features/UserAccount/useValidateForm.tsx b/src/features/UserAccount/useValidateForm.tsx new file mode 100644 index 00000000..300d6a88 --- /dev/null +++ b/src/features/UserAccount/useValidateForm.tsx @@ -0,0 +1,57 @@ +import trim from 'lodash/trim' +import reduce from 'lodash/reduce' + +import { useForm } from 'features/FormStore' +import { isValidPhone } from 'features/Register/helpers/isValidPhone' + +import { + formIds, + requiredFields, + simpleValidationFields, +} from './config' + +export const useValidateForm = () => { + const { + allFieldsEmpty, + isFieldEmpty, + readFormValue, + updateFormError, + } = useForm() + + const readTrimmedValue = (fieldName: string) => trim(readFormValue(fieldName)) + + const setErrorOnEmptyFields = (fieldNames: Array, message: string) => ( + reduce( + fieldNames, + (acc, fieldName) => { + if (isFieldEmpty(fieldName)) { + updateFormError(fieldName, message) + return true + } + return acc + }, + false, + ) + ) + + const validateForm = () => { + let hasError = false + const phone = readTrimmedValue(formIds.phone) + + if (allFieldsEmpty(requiredFields)) { + updateFormError(formIds.formError, 'error_fill_out_required_fields') + setErrorOnEmptyFields(requiredFields, '') + return false + } + + hasError = setErrorOnEmptyFields(simpleValidationFields, 'error_fill_out_this_field') + + if (!isValidPhone(phone)) { + updateFormError(formIds.phone, 'error_invalid_phone_format') + hasError = true + } + return !hasError + } + + return validateForm +} diff --git a/src/features/Register/components/RegistrationStep/hooks/useCountries.tsx b/src/hooks/useCountries.tsx similarity index 100% rename from src/features/Register/components/RegistrationStep/hooks/useCountries.tsx rename to src/hooks/useCountries.tsx diff --git a/src/requests/getUserInfo.tsx b/src/requests/getUserInfo.tsx new file mode 100644 index 00000000..44a67c8b --- /dev/null +++ b/src/requests/getUserInfo.tsx @@ -0,0 +1,19 @@ +import { + DATA_URL, + PROCEDURES, +} from 'config' +import { callApi, getResponseData } from 'helpers' +import type { UserInfo } from 'requests/saveUserInfo' + +const proc = PROCEDURES.get_user_info + +export const getUserInfo = (): Promise => { + const config = { + body: { params: {}, proc }, + } + + return callApi({ + config, + url: DATA_URL, + }).then(getResponseData(proc)) +} diff --git a/src/requests/index.tsx b/src/requests/index.tsx index 49989e00..6713131b 100644 --- a/src/requests/index.tsx +++ b/src/requests/index.tsx @@ -11,6 +11,8 @@ export * from './modifyUserSportFavs' export * from './getSportTournaments' export * from './getTournamentInfo' export * from './getTeamInfo' +export * from './getUserInfo' export * from './getMatchInfo' export * from './reportPlayerProgress' export * from './getVideos' +export * from './saveUserInfo' diff --git a/src/requests/saveUserInfo.tsx b/src/requests/saveUserInfo.tsx new file mode 100644 index 00000000..4153d9a9 --- /dev/null +++ b/src/requests/saveUserInfo.tsx @@ -0,0 +1,74 @@ +import { + DATA_URL, + PROCEDURES, +} from 'config' +import { callApi } from 'helpers' + +const proc = PROCEDURES.save_user_info + +export type UserInfo = { + address_line1: string, + address_line2: string, + city: string, + city_id: number, + countryId: number, + firstname: string, + lastname: string, + password: string, + phone: string, + postalCode: number, + region: string, +} + +type Response = { + _p_error: string | null, + _p_status: 1 | 2, +} + +const responseStatus = { + FAILURE: 2, + SUCCESS: 1, +} + +export const saveUserInfo = async ({ + address_line1, + address_line2, + city, + city_id, + countryId, + firstname, + lastname, + password, + phone, + postalCode, + region, +}: UserInfo) => { + const config = { + body: { + params: { + _p_address_line1: address_line1, + _p_address_line2: address_line2, + _p_city: city, + _p_city_id: city_id, + _p_country_id: countryId, + _p_firstname: firstname, + _p_lastname: lastname, + _p_password: password, + _p_phone: phone, + _p_postal_code: postalCode, + _p_region: region, + }, + 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) +}