mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
F: Implement login form using react-hook-form
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
This commit is contained in:
parent
c97b8997ce
commit
27234c488d
|
@ -39,6 +39,7 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- react-error-boundary
|
- react-error-boundary
|
||||||
- react-tooltip
|
- react-tooltip
|
||||||
- react-zoom-pan-pinch
|
- react-zoom-pan-pinch
|
||||||
|
- react-hook-form
|
||||||
- reactflow
|
- reactflow
|
||||||
- js-file-download
|
- js-file-download
|
||||||
- use-debounce
|
- use-debounce
|
||||||
|
@ -46,6 +47,7 @@ This readme file is used mostly to document project dependencies and conventions
|
||||||
- html-to-image
|
- html-to-image
|
||||||
- zustand
|
- zustand
|
||||||
- zod
|
- zod
|
||||||
|
- @hookform/resolvers
|
||||||
- @tanstack/react-table
|
- @tanstack/react-table
|
||||||
- @tanstack/react-query
|
- @tanstack/react-query
|
||||||
- @tanstack/react-query-devtools
|
- @tanstack/react-query-devtools
|
||||||
|
|
27
rsconcept/frontend/package-lock.json
generated
27
rsconcept/frontend/package-lock.json
generated
|
@ -9,6 +9,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-query": "^5.64.2",
|
"@tanstack/react-query": "^5.64.2",
|
||||||
"@tanstack/react-query-devtools": "^5.64.2",
|
"@tanstack/react-query-devtools": "^5.64.2",
|
||||||
|
@ -23,6 +24,7 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-error-boundary": "^5.0.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-intl": "^7.1.5",
|
"react-intl": "^7.1.5",
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
|
@ -1699,6 +1701,15 @@
|
||||||
"tslib": "2"
|
"tslib": "2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hookform/resolvers": {
|
||||||
|
"version": "3.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
|
||||||
|
"integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-hook-form": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
@ -9102,6 +9113,22 @@
|
||||||
"react": ">=16.13.1"
|
"react": ">=16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hook-form": {
|
||||||
|
"version": "7.54.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz",
|
||||||
|
"integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-hook-form"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-icons": {
|
"node_modules/react-icons": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@lezer/lr": "^1.4.2",
|
"@lezer/lr": "^1.4.2",
|
||||||
"@tanstack/react-query": "^5.64.2",
|
"@tanstack/react-query": "^5.64.2",
|
||||||
"@tanstack/react-query-devtools": "^5.64.2",
|
"@tanstack/react-query-devtools": "^5.64.2",
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-error-boundary": "^5.0.0",
|
"react-error-boundary": "^5.0.0",
|
||||||
|
"react-hook-form": "^7.54.2",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-intl": "^7.1.5",
|
"react-intl": "^7.1.5",
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
|
|
|
@ -11,6 +11,7 @@ interface UserButtonProps {
|
||||||
|
|
||||||
function UserButton({ onLogin, onClickUser }: UserButtonProps) {
|
function UserButton({ onLogin, onClickUser }: UserButtonProps) {
|
||||||
const { user, isAnonymous } = useAuthSuspense();
|
const { user, isAnonymous } = useAuthSuspense();
|
||||||
|
console.log(user);
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
if (isAnonymous) {
|
if (isAnonymous) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { information } from '@/utils/labels';
|
||||||
* Represents login data, used to authenticate users.
|
* Represents login data, used to authenticate users.
|
||||||
*/
|
*/
|
||||||
export const UserLoginSchema = z.object({
|
export const UserLoginSchema = z.object({
|
||||||
username: z.string(),
|
username: z.string().nonempty('Поле логина обязательно для заполнения'),
|
||||||
password: z.string()
|
password: z.string().nonempty('Поле пароля обязательно для заполнения')
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { libraryApi } from '@/backend/library/api';
|
|
||||||
|
|
||||||
import { authApi, IUserLoginDTO } from './api';
|
import { authApi, IUserLoginDTO } from './api';
|
||||||
|
|
||||||
|
@ -10,10 +9,11 @@ export const useLogin = () => {
|
||||||
mutationKey: ['login'],
|
mutationKey: ['login'],
|
||||||
mutationFn: authApi.login,
|
mutationFn: authApi.login,
|
||||||
onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }),
|
onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }),
|
||||||
onSuccess: () => client.removeQueries({ queryKey: [libraryApi.baseKey] })
|
onSuccess: () => client.resetQueries()
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
login: (data: IUserLoginDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }),
|
login: (data: IUserLoginDTO, onSuccess?: () => void, onError?: (error: AxiosError) => void) =>
|
||||||
|
mutation.mutate(data, { onSuccess, onError }),
|
||||||
isPending: mutation.isPending,
|
isPending: mutation.isPending,
|
||||||
error: mutation.error,
|
error: mutation.error,
|
||||||
reset: mutation.reset
|
reset: mutation.reset
|
||||||
|
|
|
@ -7,8 +7,7 @@ export const useLogout = () => {
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationKey: ['logout'],
|
mutationKey: ['logout'],
|
||||||
mutationFn: authApi.logout,
|
mutationFn: authApi.logout,
|
||||||
onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] }),
|
onSuccess: () => client.resetQueries()
|
||||||
onSuccess: () => client.removeQueries()
|
|
||||||
});
|
});
|
||||||
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
|
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,9 +2,17 @@ import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
import { axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
|
||||||
import { DELAYS } from '@/backend/configuration';
|
import { DELAYS } from '@/backend/configuration';
|
||||||
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
|
import { IUser, IUserInfo, IUserProfile } from '@/models/user';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents signup data, used to create new users.
|
||||||
|
*/
|
||||||
|
export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
|
||||||
|
password: string;
|
||||||
|
password2: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents user data, intended to update user profile in persistent storage.
|
* Represents user data, intended to update user profile in persistent storage.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
import { usersApi } from '@/backend/users/api';
|
import { IUserSignupData, usersApi } from '@/backend/users/api';
|
||||||
import { IUserProfile, IUserSignupData } from '@/models/user';
|
import { IUserProfile } from '@/models/user';
|
||||||
|
|
||||||
export const useSignup = () => {
|
export const useSignup = () => {
|
||||||
const client = useQueryClient();
|
const client = useQueryClient();
|
||||||
|
|
8
rsconcept/frontend/src/components/props.d.ts
vendored
8
rsconcept/frontend/src/components/props.d.ts
vendored
|
@ -1,5 +1,6 @@
|
||||||
// =========== Module contains interfaces for common UI elements. ==========
|
// =========== Module contains interfaces for common UI elements. ==========
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { FieldError } from 'react-hook-form';
|
||||||
|
|
||||||
export namespace CProps {
|
export namespace CProps {
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +36,13 @@ export namespace CProps {
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an object that can have an error message.
|
||||||
|
*/
|
||||||
|
export interface ErrorProcessing {
|
||||||
|
error?: FieldError;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents `control` component with optional title and configuration options.
|
* Represents `control` component with optional title and configuration options.
|
||||||
*
|
*
|
||||||
|
|
17
rsconcept/frontend/src/components/ui/ErrorField.tsx
Normal file
17
rsconcept/frontend/src/components/ui/ErrorField.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { FieldError, GlobalError } from 'react-hook-form';
|
||||||
|
|
||||||
|
interface ErrorFieldProps {
|
||||||
|
error?: FieldError | GlobalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays an error message for input field.
|
||||||
|
*/
|
||||||
|
function ErrorField({ error }: ErrorFieldProps) {
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <div className='text-sm text-warn-600'>{error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorField;
|
|
@ -2,9 +2,10 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
|
|
||||||
|
import ErrorField from './ErrorField';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
|
|
||||||
interface TextInputProps extends CProps.Editor, CProps.Colors, CProps.Input {
|
interface TextInputProps extends CProps.Editor, CProps.ErrorProcessing, CProps.Colors, CProps.Input {
|
||||||
/** Indicates that padding should be minimal. */
|
/** Indicates that padding should be minimal. */
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ function TextInput({
|
||||||
className,
|
className,
|
||||||
colors = 'clr-input',
|
colors = 'clr-input',
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
error,
|
||||||
...restProps
|
...restProps
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -63,6 +65,7 @@ function TextInput({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
<ErrorField error={error} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ function DlgChangeLocation() {
|
||||||
<SelectLocationHead
|
<SelectLocationHead
|
||||||
value={head} // prettier: split-lines
|
value={head} // prettier: split-lines
|
||||||
onChange={setHead}
|
onChange={setHead}
|
||||||
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
excluded={!user.is_staff ? [LocationHead.LIBRARY] : []}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectLocationContext value={location} onChange={handleSelectLocation} className='max-h-[9.2rem]' />
|
<SelectLocationContext value={location} onChange={handleSelectLocation} className='max-h-[9.2rem]' />
|
||||||
|
|
|
@ -119,11 +119,7 @@ function DlgCloneLibraryItem() {
|
||||||
<div className='flex justify-between gap-3'>
|
<div className='flex justify-between gap-3'>
|
||||||
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 w-[7rem] h-min'>
|
||||||
<Label text='Корень' />
|
<Label text='Корень' />
|
||||||
<SelectLocationHead
|
<SelectLocationHead value={head} onChange={setHead} excluded={!user.is_staff ? [LocationHead.LIBRARY] : []} />
|
||||||
value={head}
|
|
||||||
onChange={setHead}
|
|
||||||
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<SelectLocationContext value={location} onChange={handleSelectLocation} />
|
<SelectLocationContext value={location} onChange={handleSelectLocation} />
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|
|
@ -32,14 +32,6 @@ export interface ICurrentUser {
|
||||||
editor: LibraryItemID[];
|
editor: LibraryItemID[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents signup data, used to create new users.
|
|
||||||
*/
|
|
||||||
export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
|
|
||||||
password: string;
|
|
||||||
password2: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents user profile for viewing and editing {@link IUser}.
|
* Represents user profile for viewing and editing {@link IUser}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -196,11 +196,7 @@ function FormCreateItem() {
|
||||||
<div className='flex justify-between gap-3 flex-grow'>
|
<div className='flex justify-between gap-3 flex-grow'>
|
||||||
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
<div className='flex flex-col gap-2 min-w-[7rem] h-min'>
|
||||||
<Label text='Корень' />
|
<Label text='Корень' />
|
||||||
<SelectLocationHead
|
<SelectLocationHead value={head} onChange={setHead} excluded={!user.is_staff ? [LocationHead.LIBRARY] : []} />
|
||||||
value={head}
|
|
||||||
onChange={setHead}
|
|
||||||
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<SelectLocationContext value={location} onChange={handleSelectLocation} />
|
<SelectLocationContext value={location} onChange={handleSelectLocation} />
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|
|
@ -34,7 +34,7 @@ function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocationProps
|
||||||
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
|
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
|
||||||
|
|
||||||
const canRename = (() => {
|
const canRename = (() => {
|
||||||
if (location.length <= 3 || isAnonymous || !user) {
|
if (location.length <= 3 || isAnonymous) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (user.is_staff) {
|
if (user.is_staff) {
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { UserLoginSchema } from '@/backend/auth/api';
|
import { IUserLoginDTO, UserLoginSchema } from '@/backend/auth/api';
|
||||||
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
import { useAuthSuspense } from '@/backend/auth/useAuth';
|
||||||
import { useLogin } from '@/backend/auth/useLogin';
|
import { useLogin } from '@/backend/auth/useLogin';
|
||||||
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
|
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
import { ErrorData } from '@/components/info/InfoError';
|
||||||
|
import ErrorField from '@/components/ui/ErrorField';
|
||||||
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,65 +24,74 @@ function LoginPage() {
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const initialName = query.get('username') ?? '';
|
const initialName = query.get('username') ?? '';
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
clearErrors,
|
||||||
|
setError,
|
||||||
|
resetField,
|
||||||
|
formState: { errors }
|
||||||
|
} = useForm({
|
||||||
|
resolver: zodResolver(UserLoginSchema),
|
||||||
|
defaultValues: { username: initialName, password: '' }
|
||||||
|
});
|
||||||
|
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
const { login, isPending, error: loginError, reset } = useLogin();
|
const { login, isPending, error: loginError, reset } = useLogin();
|
||||||
const [validationError, setValidationError] = useState<ErrorData | undefined>(undefined);
|
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function onSubmit(data: IUserLoginDTO) {
|
||||||
event.preventDefault();
|
login(
|
||||||
if (!isPending) {
|
data,
|
||||||
const formData = new FormData(event.currentTarget);
|
() => {
|
||||||
const result = UserLoginSchema.safeParse({
|
resetField('password');
|
||||||
username: formData.get('username'),
|
if (router.canBack()) {
|
||||||
password: formData.get('password')
|
router.back();
|
||||||
});
|
} else {
|
||||||
|
router.push(urls.library);
|
||||||
if (!result.success) {
|
}
|
||||||
setValidationError(result.error);
|
},
|
||||||
} else {
|
error => {
|
||||||
login(result.data, () => {
|
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
||||||
if (router.canBack()) {
|
setError('root', { message: 'На Портале отсутствует такое сочетание имени пользователя и пароля' });
|
||||||
router.back();
|
}
|
||||||
} else {
|
|
||||||
router.push(urls.library);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetErrors() {
|
function resetErrors() {
|
||||||
reset();
|
reset();
|
||||||
setValidationError(undefined);
|
clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAnonymous) {
|
if (!isAnonymous) {
|
||||||
return <ExpectedAnonymous />;
|
return <ExpectedAnonymous />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<form className={clsx('cc-column cc-fade-in', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')} onSubmit={handleSubmit}>
|
<form
|
||||||
|
className={clsx('cc-column cc-fade-in', 'w-[24rem] mx-auto', 'pt-12 pb-6 px-6')}
|
||||||
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
|
onChange={resetErrors}
|
||||||
|
>
|
||||||
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
|
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
|
||||||
<TextInput
|
<TextInput
|
||||||
id='username'
|
id='username'
|
||||||
name='username'
|
|
||||||
autoComplete='username'
|
autoComplete='username'
|
||||||
label='Логин или email'
|
label='Логин или email'
|
||||||
|
{...register('username', { required: true })}
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
|
||||||
allowEnter
|
allowEnter
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
defaultValue={initialName}
|
defaultValue={initialName}
|
||||||
onChange={resetErrors}
|
error={errors.username}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='password'
|
id='password'
|
||||||
name='password'
|
{...register('password', { required: true })}
|
||||||
type='password'
|
type='password'
|
||||||
autoComplete='current-password'
|
autoComplete='current-password'
|
||||||
label='Пароль'
|
label='Пароль'
|
||||||
required
|
|
||||||
allowEnter
|
allowEnter
|
||||||
onChange={resetErrors}
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SubmitButton text='Войти' className='self-center w-[12rem] mt-3' loading={isPending} />
|
<SubmitButton text='Войти' className='self-center w-[12rem] mt-3' loading={isPending} />
|
||||||
|
@ -88,7 +99,8 @@ function LoginPage() {
|
||||||
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
<TextURL text='Восстановить пароль...' href='/restore-password' />
|
||||||
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
<TextURL text='Нет аккаунта? Зарегистрируйтесь...' href='/signup' />
|
||||||
</div>
|
</div>
|
||||||
{!!loginError || !!validationError ? <ProcessError error={loginError ?? validationError} /> : null}
|
<ErrorField error={errors.root} />
|
||||||
|
<EscalateError error={loginError} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -96,13 +108,13 @@ function LoginPage() {
|
||||||
export default LoginPage;
|
export default LoginPage;
|
||||||
|
|
||||||
// ====== Internals =========
|
// ====== Internals =========
|
||||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
function EscalateError({ error }: { error: ErrorData }): React.ReactElement | null {
|
||||||
|
// TODO: rework error escalation mechanism. Probably make it global.
|
||||||
|
if (!error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
||||||
return (
|
return null;
|
||||||
<div className='text-sm select-text text-warn-600'>
|
|
||||||
На Портале отсутствует такое сочетание имени пользователя и пароля
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw error as Error;
|
throw error as Error;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ function FormSignup() {
|
||||||
const [acceptPrivacy, setAcceptPrivacy] = useState(false);
|
const [acceptPrivacy, setAcceptPrivacy] = useState(false);
|
||||||
const [acceptRules, setAcceptRules] = useState(false);
|
const [acceptRules, setAcceptRules] = useState(false);
|
||||||
|
|
||||||
const isValid = acceptPrivacy && acceptRules && !!email && !!username;
|
// const isValid = acceptPrivacy && acceptRules && !!email && !!username;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
reset();
|
||||||
|
@ -87,6 +87,7 @@ function FormSignup() {
|
||||||
<FlexColumn>
|
<FlexColumn>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='username'
|
id='username'
|
||||||
|
name='username'
|
||||||
autoComplete='username'
|
autoComplete='username'
|
||||||
required
|
required
|
||||||
label='Имя пользователя (логин)'
|
label='Имя пользователя (логин)'
|
||||||
|
@ -100,6 +101,7 @@ function FormSignup() {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='password'
|
id='password'
|
||||||
type='password'
|
type='password'
|
||||||
|
name='password'
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
required
|
required
|
||||||
label='Пароль'
|
label='Пароль'
|
||||||
|
@ -110,6 +112,7 @@ function FormSignup() {
|
||||||
<TextInput
|
<TextInput
|
||||||
id='password2'
|
id='password2'
|
||||||
type='password'
|
type='password'
|
||||||
|
name='password2'
|
||||||
label='Повторите пароль'
|
label='Повторите пароль'
|
||||||
autoComplete='new-password'
|
autoComplete='new-password'
|
||||||
required
|
required
|
||||||
|
@ -122,6 +125,7 @@ function FormSignup() {
|
||||||
<FlexColumn className='w-[15rem]'>
|
<FlexColumn className='w-[15rem]'>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='email'
|
id='email'
|
||||||
|
name='email'
|
||||||
autoComplete='email'
|
autoComplete='email'
|
||||||
required
|
required
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
|
@ -132,6 +136,7 @@ function FormSignup() {
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='first_name'
|
id='first_name'
|
||||||
|
name='first_name'
|
||||||
label='Отображаемое имя'
|
label='Отображаемое имя'
|
||||||
autoComplete='given-name'
|
autoComplete='given-name'
|
||||||
value={firstName}
|
value={firstName}
|
||||||
|
@ -139,6 +144,7 @@ function FormSignup() {
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='last_name'
|
id='last_name'
|
||||||
|
name='last_name'
|
||||||
label='Отображаемая фамилия'
|
label='Отображаемая фамилия'
|
||||||
autoComplete='family-name'
|
autoComplete='family-name'
|
||||||
value={lastName}
|
value={lastName}
|
||||||
|
@ -157,7 +163,7 @@ function FormSignup() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-around my-3'>
|
<div className='flex justify-around my-3'>
|
||||||
<SubmitButton text='Регистрировать' className='min-w-[10rem]' loading={isPending} disabled={!isValid} />
|
<SubmitButton text='Регистрировать' className='min-w-[10rem]' loading={isPending} />
|
||||||
<Button text='Назад' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
<Button text='Назад' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
||||||
</div>
|
</div>
|
||||||
{error ? <ProcessError error={error} /> : null}
|
{error ? <ProcessError error={error} /> : null}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user