mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
using formik on profile settings
This commit is contained in:
parent
3c0571816c
commit
e8f0ec1f36
5 changed files with 106 additions and 75 deletions
|
@ -1,4 +1,4 @@
|
||||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
import React, { FC } from 'react';
|
||||||
import styles from './styles.module.scss';
|
import styles from './styles.module.scss';
|
||||||
import { Textarea } from '~/components/input/Textarea';
|
import { Textarea } from '~/components/input/Textarea';
|
||||||
import { Button } from '~/components/input/Button';
|
import { Button } from '~/components/input/Button';
|
||||||
|
@ -10,62 +10,22 @@ import { ProfileAccounts } from '~/components/profile/ProfileAccounts';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useUser } from '~/hooks/user/userUser';
|
import { useUser } from '~/hooks/user/userUser';
|
||||||
import { useProfileContext } from '~/utils/providers/ProfileProvider';
|
import { useProfileContext } from '~/utils/providers/ProfileProvider';
|
||||||
import { showErrorToast } from '~/utils/errors/showToast';
|
import { useProfileForm } from '~/hooks/profile/useProfileForm';
|
||||||
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
|
import { has } from 'ramda';
|
||||||
import { showToastSuccess } from '~/utils/toast';
|
|
||||||
import { getRandomPhrase } from '~/constants/phrases';
|
const getError = (error?: string) => (error && has(error, ERROR_LITERAL) ? error : undefined);
|
||||||
|
|
||||||
const ProfileSettings: FC = () => {
|
const ProfileSettings: FC = () => {
|
||||||
const [errors, setErrors] = useState<Record<string, any>>({});
|
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
const { updateProfile } = useProfileContext();
|
const { updateProfile } = useProfileContext();
|
||||||
|
|
||||||
const [password, setPassword] = useState('');
|
const { handleSubmit, values, errors, handleChange } = useProfileForm(
|
||||||
const [new_password, setNewPassword] = useState('');
|
{ ...user, password: '', newPassword: '' },
|
||||||
|
updateProfile
|
||||||
const [data, setData] = useState(user);
|
|
||||||
|
|
||||||
const setDescription = useCallback(description => setData({ ...data, description }), [
|
|
||||||
data,
|
|
||||||
setData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const setEmail = useCallback(email => setData({ ...data, email }), [data, setData]);
|
|
||||||
const setUsername = useCallback(username => setData({ ...data, username }), [data, setData]);
|
|
||||||
const setFullname = useCallback(fullname => setData({ ...data, fullname }), [data, setData]);
|
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
|
||||||
async event => {
|
|
||||||
try {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const fields = {
|
|
||||||
...data,
|
|
||||||
password: password.length > 0 && password ? password : undefined,
|
|
||||||
new_password: new_password.length > 0 && new_password ? new_password : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateProfile(fields);
|
|
||||||
|
|
||||||
showToastSuccess(getRandomPhrase('SUCCESS'));
|
|
||||||
} catch (error) {
|
|
||||||
showErrorToast(error);
|
|
||||||
|
|
||||||
const validationErrors = getValidationErrors(error);
|
|
||||||
if (validationErrors) {
|
|
||||||
setErrors(validationErrors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[data, password, new_password, updateProfile]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setErrors({});
|
|
||||||
}, [password, new_password, data]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className={styles.wrap} onSubmit={onSubmit}>
|
<form className={styles.wrap} onSubmit={handleSubmit}>
|
||||||
<Group>
|
<Group>
|
||||||
<Group className={styles.pad}>
|
<Group className={styles.pad}>
|
||||||
<div className={styles.pad__title}>
|
<div className={styles.pad__title}>
|
||||||
|
@ -73,13 +33,17 @@ const ProfileSettings: FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={data.fullname}
|
value={values.fullname}
|
||||||
handler={setFullname}
|
handler={handleChange('fullname')}
|
||||||
title="Полное имя"
|
title="Полное имя"
|
||||||
error={errors.fullname && ERROR_LITERAL[errors.fullname]}
|
error={getError(errors.fullname)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea value={data.description} handler={setDescription} title="Описание" />
|
<Textarea
|
||||||
|
value={values.description}
|
||||||
|
handler={handleChange('description')}
|
||||||
|
title="Описание"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.small}>
|
<div className={styles.small}>
|
||||||
Описание будет видно на странице профиля. Здесь работают те же правила оформления, что и
|
Описание будет видно на странице профиля. Здесь работают те же правила оформления, что и
|
||||||
|
@ -95,33 +59,33 @@ const ProfileSettings: FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={data.username}
|
value={values.username}
|
||||||
handler={setUsername}
|
handler={handleChange('username')}
|
||||||
title="Логин"
|
title="Логин"
|
||||||
error={errors.username && ERROR_LITERAL[errors.username]}
|
error={getError(errors.username)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={data.email}
|
value={values.email}
|
||||||
handler={setEmail}
|
handler={handleChange('email')}
|
||||||
title="E-mail"
|
title="E-mail"
|
||||||
error={errors.email && ERROR_LITERAL[errors.email]}
|
error={getError(errors.email)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={new_password}
|
value={values.newPassword}
|
||||||
handler={setNewPassword}
|
handler={handleChange('newPassword')}
|
||||||
title="Новый пароль"
|
title="Новый пароль"
|
||||||
type="password"
|
type="password"
|
||||||
error={errors.new_password && ERROR_LITERAL[errors.new_password]}
|
error={getError(errors.newPassword)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputText
|
<InputText
|
||||||
value={password}
|
value={values.password}
|
||||||
handler={setPassword}
|
handler={handleChange('password')}
|
||||||
title="Старый пароль"
|
title="Старый пароль"
|
||||||
type="password"
|
type="password"
|
||||||
error={errors.password && ERROR_LITERAL[errors.password]}
|
error={getError(errors.password)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.small}>
|
<div className={styles.small}>
|
||||||
|
|
|
@ -2,17 +2,20 @@ import { useUploader } from '~/hooks/data/useUploader';
|
||||||
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { showErrorToast } from '~/utils/errors/showToast';
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
import { IUser } from '~/redux/auth/types';
|
import { ApiUpdateUserRequest, IUser } from '~/redux/auth/types';
|
||||||
import { apiUpdateUser } from '~/redux/auth/api';
|
import { apiUpdateUser } from '~/redux/auth/api';
|
||||||
|
|
||||||
export const usePatchProfile = (updateUserData: (user: Partial<IUser>) => void) => {
|
export const usePatchProfile = (updateUserData: (user: Partial<IUser>) => void) => {
|
||||||
const { uploadFile } = useUploader(UploadSubject.Avatar, UploadTarget.Profiles);
|
const { uploadFile } = useUploader(UploadSubject.Avatar, UploadTarget.Profiles);
|
||||||
|
|
||||||
const updateProfile = useCallback(async (user: Partial<IUser>) => {
|
const updateProfile = useCallback(
|
||||||
|
async (user: Partial<ApiUpdateUserRequest['user']>) => {
|
||||||
const result = await apiUpdateUser({ user });
|
const result = await apiUpdateUser({ user });
|
||||||
await updateUserData(result.user);
|
await updateUserData(result.user);
|
||||||
return result.user;
|
return result.user;
|
||||||
}, []);
|
},
|
||||||
|
[updateUserData]
|
||||||
|
);
|
||||||
|
|
||||||
const updatePhoto = useCallback(
|
const updatePhoto = useCallback(
|
||||||
async (file: File) => {
|
async (file: File) => {
|
||||||
|
@ -23,7 +26,7 @@ export const usePatchProfile = (updateUserData: (user: Partial<IUser>) => void)
|
||||||
showErrorToast(error);
|
showErrorToast(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateUserData, uploadFile, updateProfile]
|
[uploadFile, updateProfile]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { updatePhoto, updateProfile };
|
return { updatePhoto, updateProfile };
|
||||||
|
|
62
src/hooks/profile/useProfileForm.ts
Normal file
62
src/hooks/profile/useProfileForm.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { IUser } from '~/redux/auth/types';
|
||||||
|
import { Asserts, object, string } from 'yup';
|
||||||
|
import { ERRORS } from '~/constants/errors';
|
||||||
|
import { FormikConfig, useFormik } from 'formik';
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
import { showToastSuccess } from '~/utils/toast';
|
||||||
|
import { getRandomPhrase } from '~/constants/phrases';
|
||||||
|
import { showErrorToast } from '~/utils/errors/showToast';
|
||||||
|
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
|
||||||
|
|
||||||
|
const validationSchema = object({
|
||||||
|
username: string()
|
||||||
|
.default('')
|
||||||
|
.required(ERRORS.REQUIRED),
|
||||||
|
fullname: string().default(''),
|
||||||
|
newPassword: string().optional(),
|
||||||
|
description: string().default(''),
|
||||||
|
email: string()
|
||||||
|
.default('')
|
||||||
|
.email(ERRORS.NOT_AN_EMAIL),
|
||||||
|
password: string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProfileFormData = Asserts<typeof validationSchema>;
|
||||||
|
|
||||||
|
export const useProfileForm = (
|
||||||
|
values: ProfileFormData,
|
||||||
|
submitter: (data: ProfileFormData) => Promise<IUser>
|
||||||
|
) => {
|
||||||
|
const initialValues = useRef(values).current;
|
||||||
|
|
||||||
|
const onSubmit = useCallback<FormikConfig<ProfileFormData>['onSubmit']>(
|
||||||
|
async (values, { setErrors, setValues }) => {
|
||||||
|
try {
|
||||||
|
const fields = {
|
||||||
|
...values,
|
||||||
|
password: values.password?.length ? values.password : undefined,
|
||||||
|
new_password: values.newPassword?.length ? values.newPassword : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await submitter(fields);
|
||||||
|
|
||||||
|
setValues({ ...result, password: '', newPassword: '' });
|
||||||
|
showToastSuccess(getRandomPhrase('SUCCESS'));
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(error);
|
||||||
|
|
||||||
|
const validationErrors = getValidationErrors(error);
|
||||||
|
if (validationErrors) {
|
||||||
|
setErrors(validationErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[submitter]
|
||||||
|
);
|
||||||
|
|
||||||
|
return useFormik({
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
validationSchema,
|
||||||
|
});
|
||||||
|
};
|
|
@ -91,7 +91,9 @@ export type ApiUserLoginResult = { token: string; user: IUser };
|
||||||
export type ApiAuthGetUserRequest = {};
|
export type ApiAuthGetUserRequest = {};
|
||||||
export type ApiAuthGetUserResult = { user: IUser };
|
export type ApiAuthGetUserResult = { user: IUser };
|
||||||
|
|
||||||
export type ApiUpdateUserRequest = { user: Partial<IUser> };
|
export type ApiUpdateUserRequest = {
|
||||||
|
user: Partial<IUser & { password: string; newPassword: string }>;
|
||||||
|
};
|
||||||
export type ApiUpdateUserResult = { user: IUser; errors: Record<Partial<keyof IUser>, string> };
|
export type ApiUpdateUserResult = { user: IUser; errors: Record<Partial<keyof IUser>, string> };
|
||||||
|
|
||||||
export type ApiAuthGetUserProfileRequest = { username: string };
|
export type ApiAuthGetUserProfileRequest = { username: string };
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createContext, FC, useCallback, useContext } from "react";
|
import { createContext, FC, useCallback, useContext } from "react";
|
||||||
import { IUser } from "~/redux/auth/types";
|
import { ApiUpdateUserRequest, IUser } from "~/redux/auth/types";
|
||||||
import { useGetProfile } from "~/hooks/profile/useGetProfile";
|
import { useGetProfile } from "~/hooks/profile/useGetProfile";
|
||||||
import { EMPTY_USER } from "~/redux/auth/constants";
|
import { EMPTY_USER } from "~/redux/auth/constants";
|
||||||
import { usePatchProfile } from "~/hooks/profile/usePatchProfile";
|
import { usePatchProfile } from "~/hooks/profile/usePatchProfile";
|
||||||
|
@ -15,7 +15,7 @@ interface ProfileContextValue {
|
||||||
profile: IUser;
|
profile: IUser;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
updatePhoto: (file: File) => Promise<unknown>;
|
updatePhoto: (file: File) => Promise<unknown>;
|
||||||
updateProfile: (user: Partial<IUser>) => Promise<IUser>;
|
updateProfile: (user: Partial<ApiUpdateUserRequest['user']>) => Promise<IUser>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProfileContext = createContext<ProfileContextValue>({
|
const ProfileContext = createContext<ProfileContextValue>({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue