make translate for login, register pages

This commit is contained in:
smanylov
2025-12-10 16:39:13 +07:00
parent d9c26b079d
commit f281c13f87
31 changed files with 830 additions and 103 deletions
+41
View File
@@ -0,0 +1,41 @@
import Providers from '@/app/providers/getQueryServer'
import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import "../styles/globals.css";
import "../styles/global-styles.scss";
export function generateStaticParams() {
return routing.locales.map((locale: any) => ({ locale }));
}
export const metadata: Metadata = {
title: "No copy",
description: "No copy",
};
export default async function RootLayout({
children,
params
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return (
<html lang={locale}>
<body>
<NextIntlClientProvider>
<Providers>
{children}
</Providers>
</NextIntlClientProvider>
</body>
</html>
);
}
@@ -3,45 +3,58 @@ import styles from '@/app/styles/login.module.scss'
import LogoIcon from '@/app/ui/logo-icon';
import Link from 'next/link';
import LoginForm from '@/app/ui/login-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import { useTranslations } from 'next-intl';
export const metadata: Metadata = {
title: 'Login',
};
export default function Page() {
const t = useTranslations('Login-register-form');
return (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
<h1>NO COPY</h1>
<p>Защита авторских прав</p>
<p>
{t('copyright-protection')}
</p>
</div>
<LoginForm />
<div className="divider">
<span>или войти через</span>
<span>
{t('or-log-in-via')}
</span>
</div>
<div className={`${styles['register-link']}`}>
<p>Нет аккаунта? <Link href="register">Зарегистрироваться</Link></p>
<p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
</p>
</div>
<div className={`${styles['features']}`}>
<div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Защита контента скрытыми водяными знаками
{t('protecting-watermark')}
</div>
<div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
Мониторинг нарушений в реальном времени
{t('monitoring-violations')}
</div>
<div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path>
</svg>
Надежное хранение и безопасность данных
{t('safe-storage')}
</div>
</div>
</div>
@@ -1,17 +1,21 @@
import styles from "@/app/styles/page.module.scss";
import { Metadata } from 'next';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
export const metadata: Metadata = {
title: 'Home page',
};
export default function Home() {
const t = useTranslations('HomePage');
return (
<div className={styles.page}>
<main className={styles.main}>
<div className={styles.intro}>
Home page
{t('title')}
</div>
<Link key={'dashboard'}
href={'/pages/dashboard'}
@@ -1,25 +1,26 @@
import { Metadata } from 'next';
import { Suspense } from 'react';
import ButtonTest from '@/app/ui/button-test'
import ProtectionOverview from '@/app/ui/dashboard/protection-overview';
import StatsGrid from '@/app/ui/dashboard/stats-grid';
import StorageBreakdown from '@/app/ui/dashboard/storage-breakdown';
import RecentFilesList from '@/app/ui/dashboard/recent-files-list';
import ChartContainer from '@/app/ui/dashboard/chart-container';
import UserInfo from '@/app/ui/dashboard/user-info';
import ViolationsTable from '@/app/ui/dashboard/violations-table';
import ViolationsTypeTable from '@/app/ui/dashboard/violations-type-table';
import ActivityChart from '@/app/ui/dashboard/activity-chart';
import { useTranslations } from 'next-intl';
export const metadata: Metadata = {
title: 'Dashboard',
};
export default function Page() {
const t = useTranslations('Global');
return (
<div>
<Suspense fallback={<>...</>}>
<h1 className="page-title">Статистика</h1>
<h1 className="page-title">
{t("statistics")}
</h1>
<ProtectionOverview />
<StatsGrid />
<StorageBreakdown />
@@ -1,6 +1,6 @@
import NavLinks from '@/app/ui/nav-links'
import styles from '@/app/styles/page.module.scss'
import HeaderPanel from '../ui/header/headerPanel';
import HeaderPanel from '@/app/ui/header/headerPanel';
import { getQueryClient } from '@/app/providers/getQueryClient';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getUserData } from '@/app/actions/action';
@@ -2,26 +2,39 @@ import LogoIcon from '@/app/ui/logo-icon';
import styles from '@/app/styles/login.module.scss'
import Link from 'next/link';
import RegisterForm from '@/app/ui/register-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import {useTranslations} from 'next-intl';
export default function Page() {
const t = useTranslations('Login-register-form');
return (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
<h1>NO COPY</h1>
<p>Создать аккаунт</p>
<p>
{t('create-an-account')}
</p>
</div>
<div className={`${styles['subscription-info']}`}>
<h4>🎉 Бесплатный старт</h4>
<p>При регистрации вы получаете подписку "ДЕМО" с возможностью маркировки до 3 файлов в месяц и 10 МБ хранилища.</p>
<h4>🎉 {t('free-start')}</h4>
<p>
{t('free-start-description')}
</p>
</div>
<RegisterForm />
<div className="divider">
<span>или зарегистрироваться через</span>
<span>
{t('or-register-via')}
</span>
</div>
<div className={`${styles['register-link']}`}>
<p>Уже есть аккаунт? <Link href="login">Войти</Link></p>
<p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
</div>
</div>
</div>
+5 -5
View File
@@ -83,7 +83,7 @@ export async function authorization(
let parsed = await response.json();
await createSession(parsed.token, parsed.email);
} else {
throw (`Запрос завершился с ошибкой: ${response.status}`);
throw ('request-ended-with-an-error');
}
} catch (error) {
@@ -146,7 +146,7 @@ export async function registration(
path: string
}) => {
if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
} else {
errors[obj.path[0]] = obj.message;
}
@@ -168,9 +168,9 @@ export async function registration(
try {
const { full_name, email, password, phone } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/v1/api/auth/register`, {
const response = await fetch(`${API_BASE_URL}/v1/api/auth/register1`, {
method: 'POST',
body: JSON.stringify({ fullName: full_name, email, password, companyName: company, phone: phone}),
body: JSON.stringify({ fullName: full_name, email, password, companyName: company, phone: phone }),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
@@ -186,7 +186,7 @@ export async function registration(
} catch (error) {
if (error) {
const errors: Record<string, string> = {};
errors['server'] = `Запрос завершился с ошибкой: ${error}`
errors['server'] = 'request-ended-with-an-error'
return {
previousState: {
+10 -10
View File
@@ -20,26 +20,26 @@ export const SignupFormSchema = z
.object({
full_name: z
.string()
.min(3, { error: 'Имя должно содержать не менее 3 символов.' })
.min(3, { error: 'register-error-name' })
.trim(),
email: z.email({ error: 'Пожалуйста, введите правильный адрес электронной почты.' }).trim(),
email: z.email({ error: 'register-error-valid-email' }).trim(),
phone: z
.string()
.min(12, { error: 'Пожалуйста, введите телефонный номер.' }),
.min(12, { error: 'register-error-phone' }),
password: z
.string()
.min(8, { error: 'Длина пароля должна быть не менее 8 символов.' })
.regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'Пароль должен содержать как минимум 1 букву.' })
.regex(/[0-9]/, { error: 'Пароль должен содержать как минимум 1 цифру.' })
.min(8, { error: 'register-error-password-symbols' })
.regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter'})
.regex(/[0-9]/, { error: 'register-error-password-one-digit' })
.trim(),
confirm_password: z
.string(),
agree: z
.string()
.min(2, { error: 'Пожалуйста, ознакомьтесь с условиями использования и политикой конфиденциальности.' })
.min(2, { error: 'register-error-agree' })
})
.refine((data) => data.password === data.confirm_password, {
message: 'Повторите пароль',
message: 'repeat-password',
path: ['confirm_password'],
});
@@ -47,11 +47,11 @@ export const loginFormSchema = z
.object({
email: z
.string()
.min(1, { error: 'Адрес электронной почты не должен быть пустым.' })
.min(1, { error: 'login-error-email' })
.trim(),
password: z
.string()
.min(1, { error: 'Пароль не должен быть пустым.' })
.min(1, { error: 'login-error-password' })
.trim(),
})
+85
View File
@@ -0,0 +1,85 @@
'use client'
import { useLocale } from 'next-intl'
import { usePathname, useRouter } from '@/i18n/navigation'
import { routing } from '@/i18n/routing'
import { useState, useRef, useEffect } from 'react'
import { useClickOutside } from '@/app/hooks/useClickOutside';
export default function LanguageSwitcher() {
const locale = useLocale()
const router = useRouter()
const pathname = usePathname()
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const switchLanguage = (newLocale: string) => {
router.replace(pathname, { locale: newLocale })
setIsOpen(false)
}
useClickOutside(dropdownRef, () => {
setIsOpen(false);
});
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') setIsOpen(false)
}
document.addEventListener('keydown', handleEscape)
return () => document.removeEventListener('keydown', handleEscape)
}, []);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between w-full px-4 py-2 text-sm font-medium text-[#6366f1] bg-white border-2 border-[#6366f1] rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
aria-haspopup="listbox"
aria-expanded={isOpen}
>
<span className="flex items-center">
<span className="mr-2">{locale === 'ru' ? 'РУ' : locale.toUpperCase()}</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && (
<div className="absolute right-0 z-10 w-full mt-2 origin-top-right bg-white rounded-md shadow-lg border-2 border-[#6366f1] focus:outline-none">
<ul
className="py-1"
role="listbox"
aria-labelledby="language-selector"
>
{routing.locales.map((lang) => (
<li key={lang} role="option">
<button
onClick={() => switchLanguage(lang)}
className={`flex items-center w-full px-4 py-2 text-sm text-left hover:bg-blue-100 ${locale === lang ? 'bg-blue-50 text-blue-600 font-medium' : 'text-[#6366f1]'
}`}
role="menuitem"
>
<span className="mr-2 font-medium">
{lang === 'ru' ? 'РУ' : lang.toUpperCase()}
</span>
</button>
</li>
))}
</ul>
</div>
)}
</div>
)
}
-25
View File
@@ -1,25 +0,0 @@
import Providers from './providers/getQueryServer'
import type { Metadata } from "next";
import "./styles/globals.css";
import "./styles/global-styles.scss"
export const metadata: Metadata = {
title: "No copy",
description: "No copy",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ru">
<body className={``}>
<Providers>
{children}
</Providers>
</body>
</html>
);
}
+6
View File
@@ -15,6 +15,12 @@
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 500px;
&-language {
position: fixed;
right: 20px;
top: 20px;
}
}
.logo {
+2
View File
@@ -1,11 +1,13 @@
import NotificationsButton from './notificationsButton';
import UserMenuButton from './userMenuButton';
import Link from 'next/link';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
export default function HeaderPanel() {
return (
<header className="header">
<div className="header-action">
<LanguageSwitcher/>
<Link
href={'/pages/payment'}
className="tokens-balance"
+28 -10
View File
@@ -3,53 +3,71 @@
import styles from '@/app/styles/login.module.scss'
import { useActionState } from 'react';
import { authorization } from '@/app/actions/auth';
import { useTranslations } from 'next-intl';
export default function LoginForm() {
const [state, formAction, isPending] = useActionState(
authorization,
undefined,
);
const t = useTranslations('Login-register-form');
return (
<form action={formAction}>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']}`}>Email адрес</label>
<label className={`${styles['form-label']}`}>
{t('email-adress')}
</label>
<input
type="readOnly"
id="email"
name="email"
className={`${styles['form-input']}`}
placeholder="Введите ваш email"
placeholder={t('enter-email')}
defaultValue={state?.previousState?.email ? state?.previousState?.email : ''}
/>
{state?.error?.email && (
<p className={`${styles['form-error']}`}>{state?.error?.email}</p>
<p className={`${styles['form-error']}`}>
{t(state?.error?.email)}
</p>
)}
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']}`}>Пароль</label>
<label className={`${styles['form-label']}`}>
{t('password')}
</label>
<input
type="password"
id="password"
name="password"
className={`${styles['form-input']}`}
placeholder="Введите пароль"
placeholder={t('enter-password')}
/>
{state?.error?.password && (
<p className={`${styles['form-error']}`}>{state?.error?.password}</p>
<p className={`${styles['form-error']}`}>
{t(state?.error?.password)}
</p>
)}
</div>
{state?.error.server && (
<p className={`${styles['form-error']}`}>{state?.error.server}</p>
<p className={`${styles['form-error']}`}>
{t(state?.error.server)}
</p>
)}
<div className={`${styles['form-options']}`}>
<div className={`${styles['form-checkbox']}`}>
<input type="checkbox" id="remember" name="remember" />
<label>Запомнить меня</label>
<label>
{t('remember-me')}
</label>
</div>
<a href="forgot-password" className={`${styles['forgot-password']}`} >Забыли пароль?</a>
<a href="forgot-password" className={`${styles['forgot-password']}`} >
{t('forgot-password')}?
</a>
</div>
<button type="submit" className={`${styles['btn']}`}>Войти в систему</button>
<button type="submit" className={`${styles['btn']}`}>
{t('sign-in')}
</button>
</form>
)
}
+52 -22
View File
@@ -5,12 +5,14 @@ import { useActionState } from 'react';
import { registration } from '@/app/actions/auth';
import PhoneInput from '@/app/ui/inputs/phone-input';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
export default function RegisterForm() {
const [state, formAction, isPending] = useActionState(
registration,
undefined,
);
const t = useTranslations('Login-register-form');
return (
<form action={(e) => {
@@ -20,23 +22,27 @@ export default function RegisterForm() {
<label
className={`${styles['form-label']} ${styles['required']}`}
>
Полное имя
{t('full-name')}
</label>
<input
type="text"
id="full_name"
name="full_name"
className={`${styles['form-input']}`}
placeholder="Иванов Иван Иванович"
placeholder={t('name-placeholder')}
defaultValue={state?.previousState?.full_name ? state?.previousState?.full_name : ''}
/>
{state?.error?.full_name && (
<p className="text-sm text-red-500">{state?.error?.full_name}</p>
<p className="text-sm text-red-500">
{t(state?.error?.full_name)}
</p>
)}
</div>
<div className={`${styles['form-row']}`}>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Email адрес</label>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('email-adress')}
</label>
<input
type="text"
id="email"
@@ -46,14 +52,20 @@ export default function RegisterForm() {
defaultValue={state?.previousState?.email ? state?.previousState?.email : ''}
/>
{state?.error?.email && (
<p className="text-sm text-red-500">{state?.error?.email}</p>
<p className="text-sm text-red-500">
{t(state?.error?.email)}
</p>
)}
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Телефон</label>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('phone')}
</label>
<PhoneInput phoneState={state?.previousState?.phone} />
{state?.error?.phone && (
<p className="text-sm text-red-500">{state?.error?.phone}</p>
<p className="text-sm text-red-500">
{t(state?.error?.phone)}
</p>
)}
</div>
</div>
@@ -61,43 +73,55 @@ export default function RegisterForm() {
<label
className={`${styles['form-label']}`}
>
Компания
{t('company')}
</label>
<input
type="text"
id="company"
name="company"
className={`${styles['form-input']}`}
placeholder="ООО «Ваша компания»"
placeholder={t('company-placeholder')}
defaultValue={state?.previousState?.company ? state?.previousState?.company : ''}
/>
</div>
<div className={`${styles['form-row']}`}>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Пароль</label>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('password')}
</label>
<input
type="password"
id="password" name="password"
className={`${styles['form-input']}`}
placeholder="Минимум 8 символов"
placeholder={t('password-placeholder')}
defaultValue={state?.previousState?.password ? state?.previousState?.password : ''}
/>
{state?.error?.password && (
<p className="text-sm text-red-500">{state?.error?.password}</p>
<p className="text-sm text-red-500">
{
state?.error?.password.split('&').map((e) => {
return t(e)
})
}
</p>
)}
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>Подтвердите пароль</label>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('confirm-password')}
</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
className={`${styles['form-input']}`}
placeholder="Повторите пароль"
placeholder={t('repeat-password')}
defaultValue={state?.previousState?.confirm_password ? state?.previousState?.confirm_password : ''}
/>
{state?.error?.confirm_password && (
<p className="text-sm text-red-500">{state?.error?.confirm_password}</p>
<p className="text-sm text-red-500">
{t(state?.error?.confirm_password)}
</p>
)}
</div>
</div>
@@ -108,22 +132,28 @@ export default function RegisterForm() {
name="agree"
defaultChecked={state?.previousState?.agree ? true : false}
/>
<label htmlFor="agree">Я соглашаюсь с
<Link
<label htmlFor="agree">
{t('i-agree-to')} <Link
href="/terms-and-privacy-policy"
target="_blank"
>
условиями использования и политикой конфиденциальности
>
{t('terms-of-use')}
</Link>
</label>
</div>
{state?.error?.agree && (
<p className="text-sm text-red-500">{state?.error?.agree}</p>
<p className="text-sm text-red-500">
{t(state?.error?.agree)}
</p>
)}
{state?.error?.server && (
<p className="text-sm text-red-500">{state?.error?.server}</p>
<p className="text-sm text-red-500">
{t(state?.error?.server)}
</p>
)}
<button type="submit" className={`${styles['btn']}`}>Создать аккаунт</button>
<button type="submit" className={`${styles['btn']}`}>
{t('create-an-account')}
</button>
</form >
)
}