fix(auth): i18n и react-hook-form на странице логина
Добавлены i18next, схема zod и react-hook-form для формы входа. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Generated
+2696
-31
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import ruRU from 'antd/locale/ru_RU';
|
||||
import enUS from 'antd/locale/en_US';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/ru';
|
||||
import 'dayjs/locale/en';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
const antdLocales = { ru: ruRU, en: enUS } as const;
|
||||
|
||||
const LocaleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { i18n } = useTranslation();
|
||||
const language = useAuthStore((s) => s.user?.language);
|
||||
const locale = language === 'en' ? 'en' : 'ru';
|
||||
|
||||
useEffect(() => {
|
||||
void i18n.changeLanguage(locale);
|
||||
dayjs.locale(locale);
|
||||
}, [i18n, locale]);
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={antdLocales[locale]}>
|
||||
{children}
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocaleProvider;
|
||||
@@ -0,0 +1,16 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import ru from '../locales/ru.json';
|
||||
import en from '../locales/en.json';
|
||||
|
||||
void i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
ru: { translation: ru },
|
||||
en: { translation: en },
|
||||
},
|
||||
lng: 'ru',
|
||||
fallbackLng: 'ru',
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Dashboard",
|
||||
"users": "Users",
|
||||
"calendars": "Calendars",
|
||||
"events": "Events",
|
||||
"reports": "Reports",
|
||||
"reviews": "Reviews",
|
||||
"bannedWords": "Banned words",
|
||||
"tickets": "Tickets",
|
||||
"subscriptions": "Subscriptions",
|
||||
"admins": "Admins",
|
||||
"audit": "Audit",
|
||||
"monitoring": "Monitoring"
|
||||
},
|
||||
"profile": {
|
||||
"title": "My profile",
|
||||
"edit": "Edit",
|
||||
"saved": "Profile updated"
|
||||
},
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"invalid_credentials": "Invalid email or password",
|
||||
"insufficient_permissions": "Insufficient permissions",
|
||||
"error": "Sign-in error"
|
||||
},
|
||||
"ws": {
|
||||
"newReport": "New report",
|
||||
"newTicket": "New ticket"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"menu": {
|
||||
"dashboard": "Дашборд",
|
||||
"users": "Пользователи",
|
||||
"calendars": "Календари",
|
||||
"events": "События",
|
||||
"reports": "Жалобы",
|
||||
"reviews": "Отзывы",
|
||||
"bannedWords": "Бан-слова",
|
||||
"tickets": "Тикеты",
|
||||
"subscriptions": "Подписки",
|
||||
"admins": "Администраторы",
|
||||
"audit": "Аудит",
|
||||
"monitoring": "Мониторинг"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Мой профиль",
|
||||
"edit": "Редактировать",
|
||||
"saved": "Профиль обновлён"
|
||||
},
|
||||
"login": {
|
||||
"title": "Вход",
|
||||
"email": "Email",
|
||||
"password": "Пароль",
|
||||
"submit": "Войти",
|
||||
"invalid_credentials": "Неверный email или пароль",
|
||||
"insufficient_permissions": "Недостаточно прав для входа",
|
||||
"error": "Ошибка входа"
|
||||
},
|
||||
"ws": {
|
||||
"newReport": "Новая жалоба",
|
||||
"newTicket": "Новый тикет"
|
||||
}
|
||||
}
|
||||
+5
-9
@@ -1,17 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/ru';
|
||||
import locale from 'antd/locale/ru_RU';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import App from './App';
|
||||
|
||||
dayjs.locale('ru');
|
||||
import './i18n';
|
||||
import LocaleProvider from './components/LocaleProvider';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ConfigProvider locale={locale}>
|
||||
<LocaleProvider>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
</LocaleProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
);
|
||||
|
||||
@@ -1,51 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Form, Input, Button, Card, message } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
const LOGIN_ERROR_MESSAGES: Record<string, string> = {
|
||||
invalid_credentials: 'Неверный email или пароль',
|
||||
insufficient_permissions: 'Недостаточно прав для входа',
|
||||
};
|
||||
|
||||
const getLoginErrorMessage = (error: unknown): string => {
|
||||
if (isAxiosError(error)) {
|
||||
const apiError = error.response?.data?.error;
|
||||
if (typeof apiError === 'string') {
|
||||
return LOGIN_ERROR_MESSAGES[apiError] ?? apiError;
|
||||
}
|
||||
}
|
||||
return 'Ошибка входа';
|
||||
};
|
||||
import { loginSchema, LoginFormValues } from '../../schemas/loginSchema';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const login = useAuthStore((s) => s.login);
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const { control, handleSubmit, formState: { errors, isSubmitting } } = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: { email: '', password: '' },
|
||||
});
|
||||
|
||||
const onFinish = async (values: { email: string; password: string }) => {
|
||||
const onFinish = async (values: LoginFormValues) => {
|
||||
try {
|
||||
await login(values.email, values.password);
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
message.error(getLoginErrorMessage(error));
|
||||
if (isAxiosError(error)) {
|
||||
const apiError = error.response?.data?.error;
|
||||
if (typeof apiError === 'string') {
|
||||
message.error(t(`login.${apiError}`, { defaultValue: apiError }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
message.error(t('login.error'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<Card title="Вход" style={{ width: 400 }}>
|
||||
<Form form={form} onFinish={onFinish} layout="vertical">
|
||||
<Form.Item name="email" label="Email" rules={[{ required: true }]}>
|
||||
<Input autoComplete="username" />
|
||||
<Card title={t('login.title')} style={{ width: 400 }}>
|
||||
<Form layout="vertical" onFinish={handleSubmit(onFinish)}>
|
||||
<Form.Item
|
||||
label={t('login.email')}
|
||||
validateStatus={errors.email ? 'error' : ''}
|
||||
help={errors.email?.message}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={control}
|
||||
render={({ field }) => <Input {...field} autoComplete="username" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="password" label="Пароль" rules={[{ required: true }]}>
|
||||
<Input.Password autoComplete="current-password" />
|
||||
<Form.Item
|
||||
label={t('login.password')}
|
||||
validateStatus={errors.password ? 'error' : ''}
|
||||
help={errors.password?.message}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={control}
|
||||
render={({ field }) => <Input.Password {...field} autoComplete="current-password" />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
Войти
|
||||
<Button type="primary" htmlType="submit" block loading={isSubmitting}>
|
||||
{t('login.submit')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().min(1, 'Email обязателен').email('Некорректный email'),
|
||||
password: z.string().min(1, 'Пароль обязателен'),
|
||||
});
|
||||
|
||||
export type LoginFormValues = z.infer<typeof loginSchema>;
|
||||
Reference in New Issue
Block a user