web: split Form into files

This commit is contained in:
vchikalkin 2024-07-13 19:34:46 +03:00
parent 2719f4bb2d
commit 58fbfad17a
10 changed files with 283 additions and 270 deletions

View File

@ -1,268 +0,0 @@
/* eslint-disable react/jsx-curly-newline */
/* eslint-disable sonarjs/no-small-switch */
import TelegramIcon from '../public/assets/images/telegram.svg';
import styles from './Form.module.scss';
import { publicRuntimeConfig } from '@/config/runtime';
import { FormStateContext } from '@/context/form-state';
import { useSocket } from '@/hooks/socket';
import type { TelegramUrlResponse } from '@/types/error';
import type { LdapUser } from '@/types/user';
import axios, { isAxiosError } from 'axios';
import Image from 'next/image';
import type { PropsWithChildren } from 'react';
import { useContext, useEffect } from 'react';
import { useForm } from 'react-hook-form';
const ERROR_INVALID_CREDENTIALS = 'Неверный логин или пароль';
const ERROR_SERVER = 'Не удалось войти. Повторите попытку позже';
const { APP_BASE_PATH, TELEGRAM_BOT_URL } = publicRuntimeConfig;
type FormData = {
readonly login: string;
readonly password: string;
};
type FormProps = {
readonly onSubmit: (data: FormData) => void;
};
function redirect() {
const redirectUrl =
(window.location.pathname.replace(APP_BASE_PATH, '') || '/') + (window.location.search || '');
window.location.replace(redirectUrl);
}
function BaseForm({ children, onSubmit }: FormProps & PropsWithChildren) {
const { handleSubmit, register } = useForm<FormData>();
const {
state: { error, step },
} = useContext(FormStateContext);
return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<input
disabled={step !== 'login'}
type="text"
placeholder="Логин"
required
autoComplete="on"
{...register('login', { required: true })}
/>
<input
disabled={step !== 'login'}
type="password"
placeholder="Пароль"
required
autoComplete="on"
{...register('password', { required: true })}
/>
{step === 'telegram-login' ? (
<a target="_blank" className="info" href={TELEGRAM_BOT_URL} rel="noreferrer">
Открыть чат с ботом
</a>
) : null}
{error ? <span className="error">{error}</span> : null}
{children}
</form>
);
}
export const Form = {
Default() {
const {
dispatch,
state: { step, user },
} = useContext(FormStateContext);
function handleRefreshToken() {
axios
.get('/refresh-token')
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_SERVER },
type: 'set-error',
})
);
}
if (step === 'login' && user) {
return (
<button
className={styles['button-submit']}
type="submit"
onClick={() => handleRefreshToken()}
>
Продолжить как <b>{user?.displayName || user.username}</b>
</button>
);
}
function handleLogin(data: FormData) {
return axios
.post('/login', data)
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_INVALID_CREDENTIALS },
type: 'set-error',
})
);
}
return (
<BaseForm onSubmit={(data) => handleLogin(data)}>
<button className={styles['button-submit']} type="submit">
Войти
</button>
</BaseForm>
);
},
Telegram() {
const {
dispatch,
state: { step, user },
} = useContext(FormStateContext);
const { socket } = useSocket();
useEffect(() => {
if (step === 'telegram-login') {
socket.open();
socket.on('connect', () => {});
socket.on('auth-allow', () => {
socket.off('connect');
axios
.get('/login-confirm')
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_SERVER },
type: 'set-error',
})
);
});
}
return () => {
socket.off('connect');
};
}, [dispatch, socket, step]);
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(() => {
dispatch({
payload: {
step: 'telegram-login',
},
type: 'set-step',
});
})
.catch((error_) => {
let error = ERROR_SERVER;
if (isAxiosError<TelegramUrlResponse>(error_) && error_.response?.data?.message) {
error = error_.response?.data?.message;
}
return dispatch({
payload: { error },
type: 'set-error',
});
});
}
// eslint-disable-next-line sonarjs/no-identical-functions
function handleRefreshToken() {
axios
.get('/refresh-token')
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_SERVER },
type: 'set-error',
})
);
}
if (step === 'login' && user) {
return (
<button
className={styles['button-submit']}
type="submit"
onClick={() => handleRefreshToken()}
>
Продолжить как <b>{user?.displayName || user.username}</b>
</button>
);
}
if (step === 'telegram') {
return (
<BaseForm onSubmit={() => handleTelegramLogin()}>
<button type="submit" 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>
);
}
if (step === 'telegram-login') {
return (
<BaseForm onSubmit={() => {}}>
<button disabled type="submit" className={styles['button-telegram']}>
<Image
className={styles['button-telegram-icon']}
src={TelegramIcon}
width={24}
height={22}
alt="Telegram icon"
/>
Ожидаем подтверждения...
</button>
</BaseForm>
);
}
return (
<BaseForm onSubmit={(data) => handleLogin(data)}>
<button className={styles['button-submit']} type="submit">
Войти
</button>
</BaseForm>
);
},
};

View File

@ -0,0 +1,44 @@
import styles from './Form.module.scss';
import type { FormData, FormProps } from './types';
import { publicRuntimeConfig } from '@/config/runtime';
import { FormStateContext } from '@/context/form-state';
import type { PropsWithChildren } from 'react';
import { useContext } from 'react';
import { useForm } from 'react-hook-form';
const { TELEGRAM_BOT_URL } = publicRuntimeConfig;
export function BaseForm({ children, onSubmit }: FormProps & PropsWithChildren) {
const { handleSubmit, register } = useForm<FormData>();
const {
state: { error, step },
} = useContext(FormStateContext);
return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
<input
disabled={step !== 'login'}
type="text"
placeholder="Логин"
required
autoComplete="on"
{...register('login', { required: true })}
/>
<input
disabled={step !== 'login'}
type="password"
placeholder="Пароль"
required
autoComplete="on"
{...register('password', { required: true })}
/>
{step === 'telegram-login' ? (
<a target="_blank" className="info" href={TELEGRAM_BOT_URL} rel="noreferrer">
Открыть чат с ботом
</a>
) : null}
{error ? <span className="error">{error}</span> : null}
{children}
</form>
);
}

View File

@ -0,0 +1,59 @@
import { BaseForm } from './base-form';
import { ERROR_INVALID_CREDENTIALS, ERROR_SERVER } from './errors';
import styles from './Form.module.scss';
import type { FormData } from './types';
import { FormStateContext } from '@/context/form-state';
import { redirect } from '@/utils/router';
import axios from 'axios';
import { useContext } from 'react';
export function DefaultForm() {
const {
dispatch,
state: { step, user },
} = useContext(FormStateContext);
function handleRefreshToken() {
axios
.get('/refresh-token')
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_SERVER },
type: 'set-error',
})
);
}
if (step === 'login' && user) {
return (
<button
className={styles['button-submit']}
type="submit"
onClick={() => handleRefreshToken()}
>
Продолжить как <b>{user?.displayName || user.username}</b>
</button>
);
}
function handleLogin(data: FormData) {
return axios
.post('/login', data)
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_INVALID_CREDENTIALS },
type: 'set-error',
})
);
}
return (
<BaseForm onSubmit={(data) => handleLogin(data)}>
<button className={styles['button-submit']} type="submit">
Войти
</button>
</BaseForm>
);
}

View File

@ -0,0 +1,2 @@
export const ERROR_INVALID_CREDENTIALS = 'Неверный логин или пароль';
export const ERROR_SERVER = 'Не удалось войти. Повторите попытку позже';

View File

@ -0,0 +1,2 @@
export * from './default-form';
export * from './telegram-form';

View File

@ -0,0 +1,157 @@
import TelegramIcon from '../../public/assets/images/telegram.svg';
import { BaseForm } from './base-form';
import { ERROR_INVALID_CREDENTIALS, ERROR_SERVER } from './errors';
import styles from './Form.module.scss';
import type { FormData } from './types';
import { FormStateContext } from '@/context/form-state';
import { useSocket } from '@/hooks/socket';
import type { TelegramUrlResponse } from '@/types/error';
import type { LdapUser } from '@/types/user';
import { redirect } from '@/utils/router';
import axios, { isAxiosError } from 'axios';
import Image from 'next/image';
import { useContext, useEffect } from 'react';
export function TelegramForm() {
const {
dispatch,
state: { step, user },
} = useContext(FormStateContext);
const { socket } = useSocket();
useEffect(() => {
if (step === 'telegram-login') {
socket.open();
socket.on('connect', () => {});
socket.on('auth-allow', () => {
socket.off('connect');
axios
.get('/login-confirm')
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_SERVER },
type: 'set-error',
})
);
});
}
return () => {
socket.off('connect');
};
}, [dispatch, socket, step]);
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(() => {
dispatch({
payload: {
step: 'telegram-login',
},
type: 'set-step',
});
})
.catch((error_) => {
let error = ERROR_SERVER;
if (isAxiosError<TelegramUrlResponse>(error_) && error_.response?.data?.message) {
error = error_.response?.data?.message;
}
return dispatch({
payload: { error },
type: 'set-error',
});
});
}
function handleRefreshToken() {
axios
.get('/refresh-token')
.then(() => redirect())
.catch(() =>
dispatch({
payload: { error: ERROR_SERVER },
type: 'set-error',
})
);
}
if (step === 'login' && user) {
return (
<button
className={styles['button-submit']}
type="submit"
onClick={() => handleRefreshToken()}
>
Продолжить как <b>{user?.displayName || user.username}</b>
</button>
);
}
if (step === 'telegram') {
return (
<BaseForm onSubmit={() => handleTelegramLogin()}>
<button type="submit" 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>
);
}
if (step === 'telegram-login') {
return (
<BaseForm onSubmit={() => {}}>
<button disabled type="submit" className={styles['button-telegram']}>
<Image
className={styles['button-telegram-icon']}
src={TelegramIcon}
width={24}
height={22}
alt="Telegram icon"
/>
Ожидаем подтверждения...
</button>
</BaseForm>
);
}
return (
<BaseForm onSubmit={(data) => handleLogin(data)}>
<button className={styles['button-submit']} type="submit">
Войти
</button>
</BaseForm>
);
}

View File

@ -0,0 +1,7 @@
export type FormData = {
readonly login: string;
readonly password: string;
};
export type FormProps = {
readonly onSubmit: (data: FormData) => void;
};

View File

@ -1,4 +1,4 @@
import { Form } from './Form';
import * as Form from './Form';
import styles from './Login.module.scss';
import { Logo } from '@/elements';
@ -7,7 +7,7 @@ export function Login({ tfa }) {
<div className={styles.wrapper}>
<div className={styles.login}>
<Logo />
{tfa ? <Form.Telegram /> : <Form.Default />}
{tfa ? <Form.TelegramForm /> : <Form.DefaultForm />}
</div>
</div>
);

10
apps/web/utils/router.ts Normal file
View File

@ -0,0 +1,10 @@
import { publicRuntimeConfig } from '@/config/runtime';
const { APP_BASE_PATH } = publicRuntimeConfig;
export function redirect() {
const redirectUrl =
(window.location.pathname.replace(APP_BASE_PATH, '') || '/') + (window.location.search || '');
window.location.replace(redirectUrl);
}