Gracefully process registration and profile edit errors

This commit is contained in:
IRBorisov 2024-05-28 15:09:43 +03:00
parent a9fbcab73b
commit 66918217b4
4 changed files with 65 additions and 12 deletions

View File

@ -13,6 +13,7 @@ interface IUserProfileContext {
loading: boolean; loading: boolean;
processing: boolean; processing: boolean;
error: ErrorData; error: ErrorData;
errorProcessing: ErrorData;
setError: (error: ErrorData) => void; setError: (error: ErrorData) => void;
updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void; updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void;
} }
@ -37,6 +38,7 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorData>(undefined); const [error, setError] = useState<ErrorData>(undefined);
const [errorProcessing, setErrorProcessing] = useState<ErrorData>(undefined);
const reload = useCallback(() => { const reload = useCallback(() => {
setError(undefined); setError(undefined);
@ -51,12 +53,12 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => {
const updateUser = useCallback( const updateUser = useCallback(
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => { (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
setError(undefined); setErrorProcessing(undefined);
patchProfile({ patchProfile({
data: data, data: data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: setError, onError: setErrorProcessing,
onSuccess: newData => { onSuccess: newData => {
setUser(newData); setUser(newData);
const libraryUser = users.find(item => item.id === user?.id); const libraryUser = users.find(item => item.id === user?.id);
@ -76,7 +78,7 @@ export const UserProfileState = ({ children }: UserProfileStateProps) => {
}, [reload]); }, [reload]);
return ( return (
<ProfileContext.Provider value={{ user, updateUser, error, loading, setError, processing }}> <ProfileContext.Provider value={{ user, updateUser, error, loading, setError, processing, errorProcessing }}>
{children} {children}
</ProfileContext.Provider> </ProfileContext.Provider>
); );

View File

@ -1,16 +1,18 @@
'use client'; 'use client';
import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { IconHelp } from '@/components/Icons'; import { IconHelp } from '@/components/Icons';
import InfoError from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Checkbox from '@/components/ui/Checkbox'; import Checkbox from '@/components/ui/Checkbox';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import PrettyJson from '@/components/ui/PrettyJSON';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
@ -22,6 +24,24 @@ import { useConceptNavigation } from '@/context/NavigationContext';
import { IUserSignupData } from '@/models/user'; import { IUserSignupData } from '@/models/user';
import { globals, patterns } from '@/utils/constants'; import { globals, patterns } from '@/utils/constants';
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
if ('email' in error.response.data) {
return (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
<div className='w-full text-sm text-center select-text clr-text-red'>{error.response.data.email}.</div>
);
} else {
return (
<div className='text-sm select-text clr-text-red'>
<PrettyJson data={error.response} />
</div>
);
}
}
return <InfoError error={error} />;
}
function RegisterPage() { function RegisterPage() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user, signup, loading, error, setError } = useAuth(); const { user, signup, loading, error, setError } = useAuth();
@ -35,6 +55,8 @@ function RegisterPage() {
const [acceptPrivacy, setAcceptPrivacy] = useState(false); const [acceptPrivacy, setAcceptPrivacy] = useState(false);
const isValid = useMemo(() => acceptPrivacy && !!email && !!username, [acceptPrivacy, email, username]);
useEffect(() => { useEffect(() => {
setError(undefined); setError(undefined);
}, [username, email, password, password2, setError]); }, [username, email, password, password2, setError]);
@ -83,8 +105,7 @@ function RegisterPage() {
<IconHelp size='1.25rem' className='icon-primary' /> <IconHelp size='1.25rem' className='icon-primary' />
</Overlay> </Overlay>
<Tooltip anchorSelect={`#${globals.password_tooltip}`} offset={6}> <Tooltip anchorSelect={`#${globals.password_tooltip}`} offset={6}>
<p>- используйте уникальный пароль</p> используйте уникальный пароль для каждого сайта
<p>- портал функционирует в тестовом режиме</p>
</Tooltip> </Tooltip>
</div> </div>
@ -122,6 +143,15 @@ function RegisterPage() {
</FlexColumn> </FlexColumn>
<FlexColumn className='w-[15rem]'> <FlexColumn className='w-[15rem]'>
<div className='absolute'>
<Overlay id={globals.email_tooltip} position='top-[0rem] right-[-15rem]'>
<IconHelp size='1.25rem' className='icon-primary' />
</Overlay>
<Tooltip anchorSelect={`#${globals.email_tooltip}`} offset={6}>
электронная почта используется для восстановления пароля
</Tooltip>
</div>
<TextInput <TextInput
id='email' id='email'
autoComplete='email' autoComplete='email'
@ -154,10 +184,10 @@ function RegisterPage() {
</div> </div>
<div className='flex justify-around my-3'> <div className='flex justify-around my-3'>
<SubmitButton text='Регистрировать' className='min-w-[10rem]' loading={loading} disabled={!acceptPrivacy} /> <SubmitButton text='Регистрировать' className='min-w-[10rem]' loading={loading} disabled={!isValid} />
<Button text='Назад' className='min-w-[10rem]' onClick={() => handleCancel()} /> <Button text='Назад' className='min-w-[10rem]' onClick={() => handleCancel()} />
</div> </div>
{error ? <InfoError error={error} /> : null} {error ? <ProcessError error={error} /> : null}
</form> </form>
</AnimateFade> </AnimateFade>
); );

View File

@ -1,17 +1,31 @@
'use client'; 'use client';
import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useLayoutEffect, useMemo, useState } from 'react'; import { useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useBlockNavigation } from '@/context/NavigationContext'; import { useBlockNavigation } from '@/context/NavigationContext';
import { useUserProfile } from '@/context/UserProfileContext'; import { useUserProfile } from '@/context/UserProfileContext';
import { IUserUpdateData } from '@/models/user'; import { IUserUpdateData } from '@/models/user';
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
if ('email' in error.response.data) {
return (
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
<div className='text-sm select-text clr-text-red'>{error.response.data.email}.</div>
);
}
}
return <InfoError error={error} />;
}
function EditorProfile() { function EditorProfile() {
const { updateUser, user, processing } = useUserProfile(); const { updateUser, user, errorProcessing, processing } = useUserProfile();
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
@ -47,7 +61,7 @@ function EditorProfile() {
} }
return ( return (
<form onSubmit={handleSubmit} className={clsx('cc-column', 'min-w-[18rem]', 'px-6 py-2')}> <form onSubmit={handleSubmit} className={clsx('cc-column', 'w-[18rem]', 'px-6 py-2')}>
<TextInput <TextInput
id='username' id='username'
autoComplete='username' autoComplete='username'
@ -80,7 +94,13 @@ function EditorProfile() {
value={email} value={email}
onChange={event => setEmail(event.target.value)} onChange={event => setEmail(event.target.value)}
/> />
<SubmitButton className='self-center mt-6' text='Сохранить данные' loading={processing} disabled={!isModified} /> {errorProcessing ? <ProcessError error={errorProcessing} /> : null}
<SubmitButton
className='self-center mt-6'
text='Сохранить данные'
loading={processing}
disabled={!isModified || email == ''}
/>
</form> </form>
); );
} }

View File

@ -113,6 +113,7 @@ export const storage = {
export const globals = { export const globals = {
tooltip: 'global_tooltip', tooltip: 'global_tooltip',
password_tooltip: 'password_tooltip', password_tooltip: 'password_tooltip',
email_tooltip: 'email_tooltip',
main_scroll: 'main_scroll', main_scroll: 'main_scroll',
library_item_editor: 'library_item_editor', library_item_editor: 'library_item_editor',
constituenta_editor: 'constituenta_editor' constituenta_editor: 'constituenta_editor'