apps/web: refactor Form component
This commit is contained in:
parent
e4357070af
commit
001c83e902
@ -3,14 +3,17 @@
|
||||
import TelegramIcon from '../public/assets/images/telegram.svg';
|
||||
import styles from './Form.module.scss';
|
||||
import { publicRuntimeConfig } from '@/config/runtime';
|
||||
import { AuthModeContext } from '@/context/auth-mode';
|
||||
import { FormStateContext } from '@/context/form-state';
|
||||
import type { LdapUser } from '@/types/user';
|
||||
import axios from 'axios';
|
||||
import Image from 'next/image';
|
||||
import { useContext, useState } from 'react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
const ERROR_INVALID_CREDENTIALS = 'Неверный логин или пароль';
|
||||
const ERROR_SERVER = 'Не удалось войти. Повторите попытку позже';
|
||||
|
||||
const { APP_BASE_PATH } = publicRuntimeConfig;
|
||||
|
||||
type FormData = {
|
||||
@ -18,45 +21,18 @@ type FormData = {
|
||||
readonly password: string;
|
||||
};
|
||||
|
||||
function handleDefaultLogin(data: FormData) {
|
||||
const redirectUrl =
|
||||
(window.location.pathname.replace(APP_BASE_PATH, '') || '/') + (window.location.search || '');
|
||||
type FormProps = {
|
||||
readonly onSubmit: (data: FormData) => void;
|
||||
};
|
||||
|
||||
return axios.post('/login', data).then(() => {
|
||||
window.location.replace(redirectUrl);
|
||||
});
|
||||
}
|
||||
|
||||
function handleTelegramLogin(data: FormData) {
|
||||
return axios.post<LdapUser>('/login', data);
|
||||
}
|
||||
|
||||
export function Form() {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
function BaseForm({ children, onSubmit }: FormProps & PropsWithChildren) {
|
||||
const { handleSubmit, register } = useForm<FormData>();
|
||||
const { tfa } = useContext(AuthModeContext);
|
||||
const {
|
||||
dispatch,
|
||||
state: { step, user },
|
||||
state: { error, step },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
return (
|
||||
<form
|
||||
className={styles.form}
|
||||
onSubmit={handleSubmit((data) => {
|
||||
if (!tfa) return handleDefaultLogin(data).catch(() => setHasError(true));
|
||||
|
||||
return handleTelegramLogin(data).then((res) =>
|
||||
dispatch({
|
||||
payload: {
|
||||
step: 'telegram',
|
||||
user: res.data,
|
||||
},
|
||||
type: 'set-step',
|
||||
})
|
||||
);
|
||||
})}
|
||||
>
|
||||
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<input
|
||||
disabled={step === 'telegram'}
|
||||
type="text"
|
||||
@ -73,37 +49,105 @@ export function Form() {
|
||||
autoComplete="on"
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
{hasError ? <span className="error">Неверный логин или пароль</span> : null}
|
||||
<ButtonSubmit tfa={tfa} step={step} user={user} />
|
||||
{error ? <span className="error">{error}</span> : null}
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
type ButtonSumitProps = {
|
||||
readonly step: string;
|
||||
readonly tfa: boolean;
|
||||
readonly user: LdapUser | undefined;
|
||||
};
|
||||
export const Form = {
|
||||
Default() {
|
||||
const { dispatch } = useContext(FormStateContext);
|
||||
|
||||
function handleLogin(data: FormData) {
|
||||
const redirectUrl =
|
||||
(window.location.pathname.replace(APP_BASE_PATH, '') || '/') +
|
||||
(window.location.search || '');
|
||||
|
||||
return axios
|
||||
.post('/login', data)
|
||||
.then(() => window.location.replace(redirectUrl))
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_INVALID_CREDENTIALS },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonSubmit({ step, tfa, user }: ButtonSumitProps) {
|
||||
if (!tfa || step === 'login') {
|
||||
return (
|
||||
<button className={styles['button-submit']} type="submit">
|
||||
Войти
|
||||
</button>
|
||||
<BaseForm onSubmit={(data) => handleLogin(data)}>
|
||||
<button className={styles['button-submit']} type="submit">
|
||||
Войти
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
return (
|
||||
<button type="button" className={styles['button-telegram']}>
|
||||
<Image
|
||||
className={styles['button-telegram-icon']}
|
||||
src={TelegramIcon}
|
||||
width={24}
|
||||
height={22}
|
||||
alt="Telegram icon"
|
||||
/>
|
||||
Войти как <b>{user?.displayName}</b>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Telegram() {
|
||||
const {
|
||||
dispatch,
|
||||
state: { step, user },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
function handleLogin(data: FormData) {
|
||||
axios
|
||||
.post<LdapUser>('/login', data)
|
||||
.then((res) => {
|
||||
dispatch({
|
||||
payload: {
|
||||
step: 'telegram',
|
||||
user: res.data,
|
||||
},
|
||||
type: 'set-step',
|
||||
});
|
||||
})
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_INVALID_CREDENTIALS },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function handleTelegramLogin() {
|
||||
axios
|
||||
.post<LdapUser>('/login-telegram')
|
||||
.then((res) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🚀 ~ .then ~ res:', res);
|
||||
})
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'telegram') {
|
||||
return (
|
||||
<BaseForm onSubmit={(data) => handleLogin(data)}>
|
||||
<button type="button" className={styles['button-telegram']}>
|
||||
<Image
|
||||
className={styles['button-telegram-icon']}
|
||||
src={TelegramIcon}
|
||||
width={24}
|
||||
height={22}
|
||||
alt="Telegram icon"
|
||||
/>
|
||||
Войти как <b>{user?.displayName}</b>
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseForm onSubmit={() => handleTelegramLogin()}>
|
||||
<button className={styles['button-submit']} type="submit">
|
||||
Войти
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,12 +2,12 @@ import { Form } from './Form';
|
||||
import styles from './Login.module.scss';
|
||||
import { Logo } from '@/elements';
|
||||
|
||||
export function Login() {
|
||||
export function Login({ tfa }) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.login}>
|
||||
<Logo />
|
||||
<Form />
|
||||
{tfa ? <Form.Telegram /> : <Form.Default />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type AuthMode = {
|
||||
tfa: boolean;
|
||||
};
|
||||
|
||||
export const AuthModeContext = createContext<AuthMode>({ tfa: false });
|
||||
@ -4,22 +4,45 @@ import type { PropsWithChildren } from 'react';
|
||||
import { createContext, useMemo, useReducer } from 'react';
|
||||
|
||||
type State = {
|
||||
error: string | undefined;
|
||||
step: 'login' | 'telegram';
|
||||
user: LdapUser | undefined;
|
||||
};
|
||||
|
||||
type Action = {
|
||||
payload: State;
|
||||
type: 'set-step';
|
||||
payload: Partial<State>;
|
||||
type: 'set-step' | 'set-error' | 'reset-error';
|
||||
};
|
||||
|
||||
const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'set-step':
|
||||
case 'set-step': {
|
||||
if (action.payload.step)
|
||||
return {
|
||||
...state,
|
||||
step: action.payload.step,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'set-error': {
|
||||
if (action.payload.error) {
|
||||
return {
|
||||
...state,
|
||||
error: action.payload.error,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
case 'reset-error': {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
error: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
@ -35,6 +58,7 @@ export const FormStateContext = createContext<Context>({} as Context);
|
||||
|
||||
export function FormStateProvider({ children }: PropsWithChildren) {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
error: undefined,
|
||||
step: 'login',
|
||||
user: undefined,
|
||||
});
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { Login } from '@/components';
|
||||
import { publicRuntimeConfig } from '@/config/runtime';
|
||||
import { AuthModeContext } from '@/context/auth-mode';
|
||||
import { FormStateProvider } from '@/context/form-state';
|
||||
import Head from 'next/head';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const { APP_DESCRIPTION } = publicRuntimeConfig;
|
||||
|
||||
@ -16,12 +15,10 @@ function PageHead() {
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const value = useMemo(() => ({ tfa: false }), []);
|
||||
|
||||
return (
|
||||
<AuthModeContext.Provider value={value}>
|
||||
<FormStateProvider>
|
||||
<PageHead />
|
||||
<Login />
|
||||
</AuthModeContext.Provider>
|
||||
</FormStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { Login } from '@/components';
|
||||
import { publicRuntimeConfig } from '@/config/runtime';
|
||||
import { AuthModeContext } from '@/context/auth-mode';
|
||||
import { FormStateProvider } from '@/context/form-state';
|
||||
import Head from 'next/head';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const { APP_DESCRIPTION } = publicRuntimeConfig;
|
||||
|
||||
@ -17,14 +15,10 @@ function PageHead() {
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const value = useMemo(() => ({ tfa: true }), []);
|
||||
|
||||
return (
|
||||
<AuthModeContext.Provider value={value}>
|
||||
<FormStateProvider>
|
||||
<PageHead />
|
||||
<Login />
|
||||
</FormStateProvider>
|
||||
</AuthModeContext.Provider>
|
||||
<FormStateProvider>
|
||||
<PageHead />
|
||||
<Login tfa />
|
||||
</FormStateProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user