apps/web: refactor Form component

This commit is contained in:
vchikalkin 2024-05-28 11:39:46 +03:00
parent e4357070af
commit 001c83e902
6 changed files with 142 additions and 90 deletions

View File

@ -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"
/>
Войти как &nbsp; <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"
/>
Войти как &nbsp; <b>{user?.displayName}</b>
</button>
</BaseForm>
);
}
return (
<BaseForm onSubmit={() => handleTelegramLogin()}>
<button className={styles['button-submit']} type="submit">
Войти
</button>
</BaseForm>
);
},
};

View File

@ -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>
);

View File

@ -1,7 +0,0 @@
import { createContext } from 'react';
type AuthMode = {
tfa: boolean;
};
export const AuthModeContext = createContext<AuthMode>({ tfa: false });

View File

@ -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,
});

View File

@ -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>
);
}

View File

@ -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>
);
}