mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: update user contacts
This commit is contained in:
parent
cd27d81ccd
commit
e402e6ac29
11
package-lock.json
generated
11
package-lock.json
generated
@ -3133,6 +3133,12 @@
|
||||
"source-map": "^0.6.1"
|
||||
}
|
||||
},
|
||||
"@types/validator": {
|
||||
"version": "13.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.6.3.tgz",
|
||||
"integrity": "sha512-fWG42pMJOL4jKsDDZZREnXLjc3UE0R8LOJfARWYg6U966rxDT7TYejYzLnUF5cvSObGg34nd0+H2wHHU5Omdfw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/warning": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
|
||||
@ -15258,6 +15264,11 @@
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"validator": {
|
||||
"version": "13.6.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.6.0.tgz",
|
||||
"integrity": "sha512-gVgKbdbHgtxpRyR8K0O6oFZPhhB5tT1jeEHZR0Znr9Svg03U0+r9DXWMrnRAB+HtCStDQKlaIZm42tVsVjqtjg=="
|
||||
},
|
||||
"value-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"typescript": "^3.9.5",
|
||||
"validator": "^13.6.0",
|
||||
"web-vitals": "^0.2.2",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
@ -58,6 +59,7 @@
|
||||
"@types/js-cookie": "^2.2.7",
|
||||
"@types/react": "^16.14.13",
|
||||
"@types/react-helmet": "^6.1.2",
|
||||
"@types/react-router-dom": "^5.1.8"
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/validator": "^13.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import useLoginQuery from 'hooks/useLoginQuery';
|
||||
import LoginValidatorSchema from 'validators/LoginValidatorSchema';
|
||||
import LoginSchema from 'validators/LoginSchema';
|
||||
|
||||
type AuthInputType = {
|
||||
login: string;
|
||||
@ -29,7 +29,7 @@ const AuthForm: React.FC = () => {
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
} = useForm<AuthInputType>({
|
||||
resolver: yupResolver(LoginValidatorSchema),
|
||||
resolver: yupResolver(LoginSchema),
|
||||
});
|
||||
const { login, loading } = useLoginQuery();
|
||||
|
||||
|
||||
@ -1,39 +1,107 @@
|
||||
import React from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { MdSave } from 'react-icons/md';
|
||||
import { IUserContacts } from 'interfaces';
|
||||
import { Box, Heading, Stack } from '@chakra-ui/layout';
|
||||
import { FormControl, FormHelperText } from '@chakra-ui/form-control';
|
||||
import { Input } from '@chakra-ui/input';
|
||||
import { Text } from '@chakra-ui/react';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
|
||||
import UpdateUserContactsSchema from 'validators/UpdateUserContactsSchema';
|
||||
import useUpdateUserContactsQuery from 'hooks/useUpdateUserContactsQuery';
|
||||
|
||||
type UpdateUserContactsInputType = {
|
||||
phone_number: string | null;
|
||||
twitter_id: string | null;
|
||||
telegram_id: string | null;
|
||||
vk_id: string | null;
|
||||
};
|
||||
|
||||
type UpdateUserContactsFormProps = {
|
||||
contacts: IUserContacts | null;
|
||||
};
|
||||
|
||||
const UpdateUserContactsForm: React.FC<UpdateUserContactsFormProps> = ({ contacts }) => {
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isDirty },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<UpdateUserContactsInputType>({
|
||||
resolver: yupResolver(UpdateUserContactsSchema),
|
||||
defaultValues: {
|
||||
phone_number: contacts && (contacts.phone_number ?? ''),
|
||||
twitter_id: contacts && (contacts.twitter_id?.replace(/^/, '@') ?? ''),
|
||||
telegram_id: contacts && (contacts.telegram_id?.replace(/^/, '@') ?? ''),
|
||||
vk_id: contacts && (contacts.vk_id ?? ''),
|
||||
},
|
||||
});
|
||||
const { updateContacts, loading } = useUpdateUserContactsQuery();
|
||||
|
||||
const onSubmit: SubmitHandler<UpdateUserContactsInputType> = data => {
|
||||
updateContacts(data).finally(() => reset(data));
|
||||
};
|
||||
|
||||
const UpdateUserContactsForm: React.FC = () => {
|
||||
return (
|
||||
<Box as='form'>
|
||||
<Box>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box borderRadius='md' borderWidth='1px' padding='20px'>
|
||||
<Heading as='h3' size='lg'>
|
||||
My Contacts
|
||||
</Heading>
|
||||
<Stack spacing='5px' margin='20px 0'>
|
||||
<FormControl id='tel'>
|
||||
<FormHelperText color='gray.500'>Telephone</FormHelperText>
|
||||
<Input type='number' size='md' variant='flushed' placeholder='123-456-7890' />
|
||||
<FormControl id='phone_number'>
|
||||
<FormHelperText color='gray.500'>Phone number with country code</FormHelperText>
|
||||
<Input
|
||||
type='text'
|
||||
size='md'
|
||||
variant='flushed'
|
||||
placeholder='+1 (234) 555-1234'
|
||||
{...register('phone_number')}
|
||||
/>
|
||||
<Text as='small' color='red.500'>
|
||||
{errors.phone_number?.message}
|
||||
</Text>
|
||||
</FormControl>
|
||||
<FormControl id='twitter'>
|
||||
<FormControl id='twitter_id'>
|
||||
<FormHelperText color='gray.500'>Twitter</FormHelperText>
|
||||
<Input type='text' size='md' variant='flushed' placeholder='https://twitter.com/id' />
|
||||
<Input type='text' size='md' variant='flushed' placeholder='@tw_username' {...register('twitter_id')} />
|
||||
<Text as='small' color='red.500'>
|
||||
{errors.twitter_id?.message}
|
||||
</Text>
|
||||
</FormControl>
|
||||
<FormControl id='telegram'>
|
||||
<FormHelperText color='gray.500'>Telegram</FormHelperText>
|
||||
<Input type='text' size='md' variant='flushed' placeholder='@tag' />
|
||||
<Input type='text' size='md' variant='flushed' placeholder='@tg_username' {...register('telegram_id')} />
|
||||
<Text as='small' color='red.500'>
|
||||
{errors.telegram_id?.message}
|
||||
</Text>
|
||||
</FormControl>
|
||||
<FormControl id='vk'>
|
||||
<FormHelperText color='gray.500'>VKontakte</FormHelperText>
|
||||
<Input type='text' size='md' variant='flushed' placeholder='https://vk.com/id' />
|
||||
<Input type='text' size='md' variant='flushed' placeholder='VKontakte id' {...register('vk_id')} />
|
||||
<Text as='small' color='red.500'>
|
||||
{errors.vk_id?.message}
|
||||
</Text>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Button colorScheme='blue' mt='4' type='submit' size='md' variant='outline' rightIcon={<MdSave />}>
|
||||
<Button
|
||||
colorScheme='blue'
|
||||
mt='4'
|
||||
type='submit'
|
||||
size='md'
|
||||
variant='outline'
|
||||
isLoading={loading}
|
||||
loadingText='Saving...'
|
||||
rightIcon={<MdSave />}
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@ -12,3 +12,13 @@ export function userHasRoles(userRoles: IUserRole[], roles: UserRoleEnum[]): boo
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean object values
|
||||
*
|
||||
* @param obj Current object that have null values
|
||||
* @returns Clean object that don't have null values
|
||||
*/
|
||||
export function removeEmptyValues(obj: object) {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null));
|
||||
}
|
||||
|
||||
65
src/hooks/useUpdateUserContactsQuery.ts
Normal file
65
src/hooks/useUpdateUserContactsQuery.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { IDataResult, IUserContacts } from 'interfaces';
|
||||
import { useState } from 'react';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import useIsMountedRef from './useIsMountedRef';
|
||||
import { useRootStore } from './useRootStore';
|
||||
|
||||
type UpdateContactsStateType = {
|
||||
result: IDataResult | null;
|
||||
error: any;
|
||||
loading: boolean;
|
||||
fetched: boolean;
|
||||
};
|
||||
|
||||
const useUpdateUserContactsQuery = () => {
|
||||
const isMountedRef = useIsMountedRef();
|
||||
const [state, setState] = useState<UpdateContactsStateType>({
|
||||
result: null,
|
||||
error: null,
|
||||
loading: false,
|
||||
fetched: false,
|
||||
});
|
||||
const { userStore } = useRootStore();
|
||||
const toast = useToast();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
const updateContacts = async (data: IUserContacts) => {
|
||||
try {
|
||||
setState(s => ({ ...s, loading: true }));
|
||||
const result = await userStore.updateCurrentUserContacts(data);
|
||||
toast({ title: `Contacts saved!`, isClosable: true, status: 'info' });
|
||||
|
||||
if (isMountedRef.current) {
|
||||
setState(s => ({ ...s, result }));
|
||||
}
|
||||
} catch (error) {
|
||||
if (isMountedRef.current) {
|
||||
setState(s => ({ ...s, error }));
|
||||
}
|
||||
if (error.response) {
|
||||
/**
|
||||
* Catch response error
|
||||
*/
|
||||
switch (error.response.status) {
|
||||
case 422:
|
||||
toast({ title: 'Validation error.', status: 'error', duration: 2000 });
|
||||
break;
|
||||
default:
|
||||
handleError(error);
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
handleError(error);
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setState(s => ({ ...s, loading: false, fetched: true }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { ...state, updateContacts };
|
||||
};
|
||||
|
||||
export default useUpdateUserContactsQuery;
|
||||
@ -24,6 +24,14 @@ export interface IUser {
|
||||
last_name: string;
|
||||
email: string;
|
||||
roles: IUserRole[];
|
||||
contacts: IUserContacts | null;
|
||||
}
|
||||
|
||||
export interface IUserContacts {
|
||||
phone_number: string | null;
|
||||
twitter_id: string | null;
|
||||
telegram_id: string | null;
|
||||
vk_id: string | null;
|
||||
}
|
||||
|
||||
export interface IUserRole {
|
||||
@ -46,14 +54,12 @@ export interface ILoginResult extends IDataResult {
|
||||
};
|
||||
}
|
||||
|
||||
export interface IUpdatedContactsResult extends IDataResult {
|
||||
data: IUserContacts;
|
||||
}
|
||||
|
||||
export interface IUserResult extends IDataResult {
|
||||
data: {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
roles: IUserRole[];
|
||||
};
|
||||
data: IUser;
|
||||
}
|
||||
|
||||
export interface IPageProps extends RouteComponentProps {
|
||||
|
||||
@ -108,7 +108,7 @@ const ProfilePage: React.FC<IPageProps> = ({ title }) => {
|
||||
<UserAccountInfo user={userStore.me} />
|
||||
</TabPanel>
|
||||
<TabPanel padding='10px 0' mt='20px'>
|
||||
<UpdateUserContactsForm />
|
||||
<UpdateUserContactsForm contacts={userStore.me.contacts} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { IUserResult } from 'interfaces';
|
||||
import { IUpdatedContactsResult, IUserContacts, IUserResult } from 'interfaces';
|
||||
import * as helpers from 'helpers';
|
||||
|
||||
export default class UserService {
|
||||
public api: AxiosInstance;
|
||||
@ -17,4 +18,15 @@ export default class UserService {
|
||||
const result = await this.api.get('v1/me');
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update authorized user contacts data
|
||||
*
|
||||
* @param data User contacts
|
||||
* @returns Updated user contacts data
|
||||
*/
|
||||
public async updateContacts(data: IUserContacts): Promise<IUpdatedContactsResult> {
|
||||
const result = await this.api.put('v1/me/contacts', helpers.removeEmptyValues(data));
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import { IUserResult, IUser } from 'interfaces';
|
||||
import { IUserResult, IUser, IUserContacts, IUpdatedContactsResult } from 'interfaces';
|
||||
|
||||
/**
|
||||
* Services
|
||||
@ -41,6 +41,22 @@ export default class UserStore {
|
||||
}
|
||||
}
|
||||
|
||||
public async updateCurrentUserContacts(data: IUserContacts): Promise<IUpdatedContactsResult> {
|
||||
try {
|
||||
const result = await this.userService.updateContacts(data);
|
||||
|
||||
runInAction(() => {
|
||||
if (this.me) {
|
||||
this.me.contacts = result.data;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
runInAction(() => {
|
||||
this.me = null;
|
||||
|
||||
10
src/types/yup/index.d.ts
vendored
Normal file
10
src/types/yup/index.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import validator from 'validator';
|
||||
|
||||
declare module 'yup' {
|
||||
interface StringSchema {
|
||||
isMobileValid(
|
||||
locales: validator.MobilePhoneLocale[],
|
||||
options?: validator.IsMobilePhoneOptions | undefined
|
||||
): StringSchema;
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import * as yup from 'yup';
|
||||
import yup from 'validators';
|
||||
|
||||
const LoginValidatorSchema = yup.object().shape({
|
||||
const LoginSchema = yup.object().shape({
|
||||
login: yup.string().required('Login is required'),
|
||||
password: yup.string().min(6, 'Password must be at least 6 characters').required('Password is required'),
|
||||
});
|
||||
|
||||
export default LoginValidatorSchema;
|
||||
export default LoginSchema;
|
||||
26
src/validators/UpdateUserContactsSchema.ts
Normal file
26
src/validators/UpdateUserContactsSchema.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import yup from 'validators';
|
||||
|
||||
const UpdateUserContactsSchema = yup.object().shape({
|
||||
phone_number: yup
|
||||
.string()
|
||||
.isMobileValid(['en-US', 'ru-RU'], { strictMode: true })
|
||||
.transform(val => (!val ? null : val))
|
||||
.nullable(true),
|
||||
twitter_id: yup
|
||||
.string()
|
||||
.matches(/(^|[^@\w])@(\w{1,15})\b/, 'Twitter username is not valid')
|
||||
.transform(val => (!val ? null : val))
|
||||
.nullable(true),
|
||||
telegram_id: yup
|
||||
.string()
|
||||
.matches(/(^|[^@\w])@(\w{1,64})\b/, 'Telegram username is not valid')
|
||||
.transform(val => (!val ? null : val))
|
||||
.nullable(true),
|
||||
vk_id: yup
|
||||
.string()
|
||||
.matches(/^([a-zA-Z0-9_]){1,64}$/, 'VKontakte username is not valid')
|
||||
.transform(val => (!val ? null : val))
|
||||
.nullable(true),
|
||||
});
|
||||
|
||||
export default UpdateUserContactsSchema;
|
||||
25
src/validators/index.ts
Normal file
25
src/validators/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import * as yup from 'yup';
|
||||
import validator from 'validator';
|
||||
import isMobilePhone from 'validator/lib/isMobilePhone';
|
||||
import { string, StringSchema } from 'yup';
|
||||
|
||||
/**
|
||||
* Delcare custom methods
|
||||
*/
|
||||
yup.addMethod<StringSchema>(
|
||||
string,
|
||||
'isMobileValid',
|
||||
function (
|
||||
this: StringSchema,
|
||||
locales: validator.MobilePhoneLocale[],
|
||||
options?: validator.IsMobilePhoneOptions | undefined
|
||||
) {
|
||||
return this.test({
|
||||
name: 'isMobileValid',
|
||||
message: 'Phone number is not valid',
|
||||
test: value => (!value ? true : isMobilePhone(value, locales, options)),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default yup;
|
||||
@ -15,7 +15,9 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"typeRoots": ["./types", "./node_modules/@types"]
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src"],
|
||||
"exclude": ["types"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user