refactor(869): user account form refactor and bug fixes (#322)
parent
07ba41b100
commit
b717e2a1e6
@ -0,0 +1,22 @@ |
||||
import includes from 'lodash/includes' |
||||
import every from 'lodash/every' |
||||
|
||||
import { formIds } from 'config/form' |
||||
|
||||
import type { FormState } from './hooks/useFormState' |
||||
|
||||
const ignoredFields = [formIds.initialCountryId, formIds.country] |
||||
|
||||
export const isFormStateEqual = (state1: FormState, state2: FormState) => ( |
||||
every(state1, (_, key) => { |
||||
if (includes(ignoredFields, key)) return true |
||||
|
||||
const cityId1 = state1[formIds.cityId]?.value |
||||
const cityId2 = state2[formIds.cityId]?.value |
||||
if (key === formIds.city && cityId1 && cityId2) { |
||||
return cityId1 === cityId2 |
||||
} |
||||
|
||||
return state1[key]?.value === state2[key]?.value |
||||
}) |
||||
) |
||||
@ -0,0 +1,74 @@ |
||||
import { |
||||
useState, |
||||
useEffect, |
||||
useCallback, |
||||
} from 'react' |
||||
|
||||
import find from 'lodash/find' |
||||
import reduce from 'lodash/reduce' |
||||
import isNull from 'lodash/isNull' |
||||
import isNumber from 'lodash/isNumber' |
||||
import isString from 'lodash/isString' |
||||
import isObject from 'lodash/isObject' |
||||
|
||||
import { formIds } from 'config/form' |
||||
|
||||
import type { UserInfo, SaveUserInfo } from 'requests' |
||||
import { |
||||
getUserInfo, |
||||
getCountryCities, |
||||
saveUserInfo, |
||||
} from 'requests' |
||||
|
||||
import type { FormState } from 'features/FormStore/hooks/useFormState' |
||||
|
||||
const transformToFormState = async (userInfo: UserInfo) => { |
||||
const cities = userInfo.country?.id |
||||
? await getCountryCities('', userInfo.country.id) |
||||
: [] |
||||
return reduce( |
||||
formIds, |
||||
( |
||||
acc: FormState, |
||||
field, |
||||
) => { |
||||
const key = field as keyof UserInfo |
||||
const value = userInfo[key] |
||||
if (key === formIds.cityId && value) { |
||||
const city = find(cities, { id: Number(value) }) || { id: '', name: acc[formIds.city]?.value } |
||||
acc[formIds.cityId] = { value: String(city.id) } |
||||
acc[formIds.city] = { value: city.name } |
||||
} else if (key === formIds.country && isObject(value) && value.id) { |
||||
const formValue = { value: String(value.id) } |
||||
acc[formIds.initialCountryId] = formValue |
||||
acc[formIds.countryId] = formValue |
||||
} else if (isNumber(value) || isString(value)) { |
||||
acc[key] = { value: String(value) } |
||||
} else if (isNull(value)) { |
||||
acc[key] = { value: '' } |
||||
} |
||||
return acc |
||||
}, |
||||
{}, |
||||
) |
||||
} |
||||
|
||||
export const useUserInfo = () => { |
||||
const [userInfo, setUserInfo] = useState<FormState>() |
||||
|
||||
const fetchUserInfo = useCallback(() => { |
||||
getUserInfo() |
||||
.then((data) => transformToFormState(data)) |
||||
.then(setUserInfo) |
||||
}, []) |
||||
|
||||
const onSubmit = useCallback((data: SaveUserInfo) => { |
||||
saveUserInfo(data).then(fetchUserInfo) |
||||
}, [fetchUserInfo]) |
||||
|
||||
useEffect(() => { |
||||
fetchUserInfo() |
||||
}, [fetchUserInfo]) |
||||
|
||||
return { onSubmit, userInfo } |
||||
} |
||||
@ -1,137 +0,0 @@ |
||||
import { |
||||
useEffect, |
||||
useCallback, |
||||
useRef, |
||||
} from 'react' |
||||
|
||||
import forEach from 'lodash/forEach' |
||||
import trim from 'lodash/trim' |
||||
|
||||
import { formIds } from 'config/form' |
||||
|
||||
import { getUserInfo } from 'requests/getUserInfo' |
||||
import { saveUserInfo } from 'requests/saveUserInfo' |
||||
import { City, getCountryCities } from 'requests/getCountryCities' |
||||
|
||||
import { useLexicsStore } from 'features/LexicsStore' |
||||
|
||||
import type { SelectedCountry } from './useCountries' |
||||
import type { Name } from './useUserInfoForm' |
||||
import { useUserInfoForm } from './useUserInfoForm' |
||||
import { useValidateForm } from './useValidateForm' |
||||
|
||||
export const useUserInfo = () => { |
||||
const validateForm = useValidateForm() |
||||
const saveButton = useRef<HTMLButtonElement>(null) |
||||
const { suffix } = useLexicsStore() |
||||
|
||||
const { |
||||
cities, |
||||
countries, |
||||
onCitySelect, |
||||
onCountryBlur, |
||||
onCountrySelect, |
||||
onPhoneBlur, |
||||
onRegionOrCityChange, |
||||
readFormError, |
||||
readFormValue, |
||||
selectedCity, |
||||
selectedCountry, |
||||
setSelectedCountry, |
||||
updateFormValue, |
||||
} = useUserInfoForm() |
||||
const readTrimmedValue = useCallback( |
||||
(fieldName: string) => trim(readFormValue(fieldName)) || null, |
||||
[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.address1) |
||||
const address_line2 = readTrimmedValue(formIds.address2) |
||||
const city = readTrimmedValue(formIds.city) |
||||
const countryId = Number(readTrimmedValue(formIds.countryId)) |
||||
const newUserInfo = { |
||||
address_line1, |
||||
address_line2, |
||||
city, |
||||
cityId: selectedCity?.id || null, |
||||
countryId, |
||||
firstname, |
||||
lastname, |
||||
password, |
||||
phone, |
||||
postalCode, |
||||
region, |
||||
} |
||||
const newUserInfoForStorage = { |
||||
...newUserInfo, |
||||
country: JSON.parse(localStorage.getItem('initCountry') as string), |
||||
} |
||||
localStorage.setItem('init', JSON.stringify(newUserInfoForStorage)) |
||||
localStorage.setItem('disabled', 'true') |
||||
saveUserInfo(newUserInfo).then(() => saveButton?.current?.blur()) |
||||
} |
||||
}, [ |
||||
readTrimmedValue, |
||||
validateForm, |
||||
selectedCity, |
||||
]) |
||||
|
||||
useEffect(() => { |
||||
getUserInfo().then((res: any) => { |
||||
const response = res |
||||
const convertResponse = () => { |
||||
forEach(response, (value: string | number | SelectedCountry, key: string) => { |
||||
if (value && typeof value === 'object') { |
||||
setSelectedCountry({ |
||||
...value, |
||||
name: value[`name_${suffix}` as Name], |
||||
}) |
||||
updateFormValue(formIds.countryId)(String(value.id)) |
||||
} else if (value) { |
||||
updateFormValue(key)(String(value)) |
||||
} |
||||
}) |
||||
} |
||||
if (response.city === null && response.city_id !== null) { |
||||
getCountryCities('', response.country.id).then((resp) => { |
||||
const initCity = resp.find((i:City) => i.id.toString() === response.city_id.toString()) |
||||
if (initCity !== undefined) { |
||||
response.city = initCity.name |
||||
localStorage.setItem('init', JSON.stringify(res)) |
||||
convertResponse() |
||||
} |
||||
}) |
||||
return |
||||
} |
||||
localStorage.setItem('init', JSON.stringify(response)) |
||||
convertResponse() |
||||
}) |
||||
}, [ |
||||
suffix, |
||||
updateFormValue, |
||||
setSelectedCountry, |
||||
]) |
||||
|
||||
return { |
||||
cities, |
||||
countries, |
||||
handleSubmit, |
||||
onCitySelect, |
||||
onCountryBlur, |
||||
onCountrySelect, |
||||
onPhoneBlur, |
||||
onRegionOrCityChange, |
||||
readFormError, |
||||
readFormValue, |
||||
saveButton, |
||||
selectedCountry, |
||||
updateFormValue, |
||||
} |
||||
} |
||||
@ -1,158 +1,14 @@ |
||||
import { formIds } from 'config/form' |
||||
import { FormStore } from 'features/FormStore' |
||||
|
||||
import { Combobox } from 'features/Combobox' |
||||
import { Input } from 'features/Common' |
||||
import { T9n } from 'features/T9n' |
||||
import { Error } from 'features/Common/Input/styled' |
||||
|
||||
import { useState } from 'react' |
||||
import { useUserInfo } from './hooks/useUserInfo' |
||||
|
||||
import { SolidButton } from '../../styled' |
||||
import { Form, ButtonWrapper } from './styled' |
||||
|
||||
const labelWidth = 104 |
||||
|
||||
const { |
||||
address1, |
||||
address2, |
||||
city, |
||||
country, |
||||
firstname, |
||||
formError, |
||||
lastname, |
||||
password, |
||||
phone, |
||||
region, |
||||
} = formIds |
||||
import { useUserInfo } from './hooks' |
||||
import { PersonalInfoForm } from '../PersonalInfoForm' |
||||
|
||||
export const PagePersonalInfo = () => { |
||||
const { |
||||
cities, |
||||
countries, |
||||
handleSubmit, |
||||
onCitySelect, |
||||
onCountryBlur, |
||||
onCountrySelect, |
||||
onPhoneBlur, |
||||
onRegionOrCityChange, |
||||
readFormError, |
||||
readFormValue, |
||||
saveButton, |
||||
selectedCountry, |
||||
updateFormValue, |
||||
} = useUserInfo() |
||||
|
||||
const [state, setState] = useState(true) |
||||
const isDisabled = JSON.parse(localStorage.getItem('disabled') as string) |
||||
|
||||
const { onSubmit, userInfo } = useUserInfo() |
||||
if (!userInfo) return null |
||||
return ( |
||||
<Form> |
||||
<Input |
||||
value={readFormValue(firstname)} |
||||
labelLexic='name' |
||||
autoComplete='given-name' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(firstname)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(lastname)} |
||||
labelLexic='lastname' |
||||
autoComplete='family-name' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(lastname)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(password)} |
||||
error={readFormError(password)} |
||||
type='password' |
||||
autoComplete='new-password' |
||||
labelLexic='form_password' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(password)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
onBlur={onPhoneBlur} |
||||
value={readFormValue(phone)} |
||||
labelLexic='phone' |
||||
autoComplete='tel' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(phone)} |
||||
error={readFormError(phone)} |
||||
editIcon |
||||
maxLength={100} |
||||
withError={false} |
||||
/> |
||||
<Combobox |
||||
value={selectedCountry?.name ?? readFormValue(country)} |
||||
labelLexic='form_country' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(country)} |
||||
onBlur={onCountryBlur} |
||||
options={countries} |
||||
onSelect={onCountrySelect} |
||||
withError={false} |
||||
selected={Boolean(selectedCountry)} |
||||
/> |
||||
<Input |
||||
value={readFormValue(region)} |
||||
labelLexic='form_region' |
||||
labelWidth={labelWidth} |
||||
onChange={onRegionOrCityChange(region)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Combobox |
||||
value={readFormValue(city)} |
||||
labelLexic='form_city' |
||||
labelWidth={labelWidth} |
||||
onChange={onRegionOrCityChange(city)} |
||||
options={cities} |
||||
onSelect={onCitySelect} |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(address1)} |
||||
labelLexic='form_address1' |
||||
labelWidth={labelWidth} |
||||
autoComplete='address-line1' |
||||
onChange={updateFormValue(address1)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(address2)} |
||||
labelLexic='form_address2' |
||||
labelWidth={labelWidth} |
||||
autoComplete='address-line2' |
||||
onChange={updateFormValue(address2)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<ButtonWrapper> |
||||
<SolidButton |
||||
disabled={isDisabled} |
||||
type='button' |
||||
onClick={() => { handleSubmit(); setState((!state)) }} |
||||
ref={saveButton} |
||||
> |
||||
<T9n t='save_changes' /> |
||||
</SolidButton> |
||||
<Error t={readFormError(formError) || ''} /> |
||||
</ButtonWrapper> |
||||
</Form> |
||||
<FormStore initialState={userInfo}> |
||||
<PersonalInfoForm onSubmit={onSubmit} /> |
||||
</FormStore> |
||||
) |
||||
} |
||||
|
||||
@ -0,0 +1,98 @@ |
||||
import { useCallback } from 'react' |
||||
|
||||
import trim from 'lodash/trim' |
||||
|
||||
import { formIds } from 'config/form' |
||||
|
||||
import type { SaveUserInfo } from 'requests/saveUserInfo' |
||||
|
||||
import { useForm } from 'features/FormStore' |
||||
|
||||
import { useUserInfoForm } from './useUserInfoForm' |
||||
import { useValidateForm } from './useValidateForm' |
||||
|
||||
export type Props = { |
||||
onSubmit: (data: SaveUserInfo) => void, |
||||
} |
||||
|
||||
export const useUserInfo = ({ onSubmit }: Props) => { |
||||
const { |
||||
hasChanges, |
||||
readFormError, |
||||
readFormValue, |
||||
updateFormValue, |
||||
} = useForm() |
||||
const validateForm = useValidateForm() |
||||
const { |
||||
cities, |
||||
countries, |
||||
onCityBlur, |
||||
onCitySelect, |
||||
onCountryBlur, |
||||
onCountrySelect, |
||||
onPhoneBlur, |
||||
onRegionOrCityChange, |
||||
selectedCountry, |
||||
} = useUserInfoForm() |
||||
const readTrimmedValue = useCallback( |
||||
(fieldName: string) => trim(readFormValue(fieldName)) || null, |
||||
[readFormValue], |
||||
) |
||||
const readNumberValue = useCallback( |
||||
(fieldName: string) => { |
||||
const value = readTrimmedValue(fieldName) |
||||
return value ? Number(value) : null |
||||
}, |
||||
[readTrimmedValue], |
||||
) |
||||
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 = readNumberValue(formIds.postalCode) |
||||
const region = readTrimmedValue(formIds.region) |
||||
const address_line1 = readTrimmedValue(formIds.address1) |
||||
const address_line2 = readTrimmedValue(formIds.address2) |
||||
const city = readTrimmedValue(formIds.city) |
||||
const cityId = readNumberValue(formIds.cityId) |
||||
const countryId = readNumberValue(formIds.countryId) |
||||
onSubmit({ |
||||
address_line1, |
||||
address_line2, |
||||
city: cityId ? null : city, |
||||
cityId, |
||||
countryId, |
||||
firstname, |
||||
lastname, |
||||
password, |
||||
phone, |
||||
postalCode, |
||||
region, |
||||
}) |
||||
} |
||||
}, [ |
||||
onSubmit, |
||||
readTrimmedValue, |
||||
readNumberValue, |
||||
validateForm, |
||||
]) |
||||
|
||||
return { |
||||
cities, |
||||
countries, |
||||
handleSubmit, |
||||
hasChanges, |
||||
onCityBlur, |
||||
onCitySelect, |
||||
onCountryBlur, |
||||
onCountrySelect, |
||||
onPhoneBlur, |
||||
onRegionOrCityChange, |
||||
readFormError, |
||||
readFormValue, |
||||
selectedCountry, |
||||
updateFormValue, |
||||
} |
||||
} |
||||
@ -0,0 +1,158 @@ |
||||
import { formIds } from 'config/form' |
||||
|
||||
import { Combobox } from 'features/Combobox' |
||||
import { Input } from 'features/Common' |
||||
import { T9n } from 'features/T9n' |
||||
import { Error } from 'features/Common/Input/styled' |
||||
|
||||
import type { Props } from './hooks/useUserInfo' |
||||
import { useUserInfo } from './hooks/useUserInfo' |
||||
|
||||
import { SolidButton } from '../../styled' |
||||
import { Form, ButtonWrapper } from './styled' |
||||
|
||||
const labelWidth = 104 |
||||
|
||||
const { |
||||
address1, |
||||
address2, |
||||
city, |
||||
cityId, |
||||
country, |
||||
firstname, |
||||
formError, |
||||
lastname, |
||||
password, |
||||
phone, |
||||
region, |
||||
} = formIds |
||||
|
||||
export const PersonalInfoForm = (props: Props) => { |
||||
const { |
||||
cities, |
||||
countries, |
||||
handleSubmit, |
||||
hasChanges, |
||||
onCityBlur, |
||||
onCitySelect, |
||||
onCountryBlur, |
||||
onCountrySelect, |
||||
onPhoneBlur, |
||||
onRegionOrCityChange, |
||||
readFormError, |
||||
readFormValue, |
||||
selectedCountry, |
||||
updateFormValue, |
||||
} = useUserInfo(props) |
||||
|
||||
return ( |
||||
<Form> |
||||
<Input |
||||
value={readFormValue(firstname)} |
||||
labelLexic='name' |
||||
autoComplete='given-name' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(firstname)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(lastname)} |
||||
labelLexic='lastname' |
||||
autoComplete='family-name' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(lastname)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(password)} |
||||
error={readFormError(password)} |
||||
type='password' |
||||
autoComplete='new-password' |
||||
labelLexic='form_password' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(password)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
onBlur={onPhoneBlur} |
||||
value={readFormValue(phone)} |
||||
labelLexic='phone' |
||||
autoComplete='tel' |
||||
labelWidth={labelWidth} |
||||
onChange={updateFormValue(phone)} |
||||
error={readFormError(phone)} |
||||
editIcon |
||||
maxLength={100} |
||||
withError={false} |
||||
/> |
||||
<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)} |
||||
/> |
||||
<Input |
||||
value={readFormValue(region)} |
||||
labelLexic='form_region' |
||||
labelWidth={labelWidth} |
||||
onChange={onRegionOrCityChange(region)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Combobox |
||||
value={readFormValue(city)} |
||||
labelLexic='form_city' |
||||
labelWidth={labelWidth} |
||||
onChange={onRegionOrCityChange(city)} |
||||
onSelect={onCitySelect} |
||||
onBlur={onCityBlur} |
||||
options={cities} |
||||
selected={Boolean(readFormValue(cityId))} |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(address1)} |
||||
labelLexic='form_address1' |
||||
labelWidth={labelWidth} |
||||
autoComplete='address-line1' |
||||
onChange={updateFormValue(address1)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<Input |
||||
value={readFormValue(address2)} |
||||
labelLexic='form_address2' |
||||
labelWidth={labelWidth} |
||||
autoComplete='address-line2' |
||||
onChange={updateFormValue(address2)} |
||||
editIcon |
||||
maxLength={500} |
||||
withError={false} |
||||
/> |
||||
<ButtonWrapper> |
||||
<SolidButton |
||||
disabled={!hasChanges()} |
||||
type='button' |
||||
onClick={handleSubmit} |
||||
> |
||||
<T9n t='save_changes' /> |
||||
</SolidButton> |
||||
<Error t={readFormError(formError) || ''} /> |
||||
</ButtonWrapper> |
||||
</Form> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue