Merge branch 'main' into NCFRONT-17

This commit is contained in:
smanylov
2025-12-17 13:03:30 +07:00
35 changed files with 1669 additions and 600 deletions
+18 -2
View File
@@ -1,17 +1,18 @@
{
"name": "no-copy-frontend",
"version": "0.1.0",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "no-copy-frontend",
"version": "0.1.0",
"version": "0.2.0",
"dependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2",
"clsx": "^2.1.1",
"jose": "^6.1.2",
"next": "^16.0.7",
@@ -2953,6 +2954,15 @@
"win32"
]
},
"node_modules/@vkid/sdk": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@vkid/sdk/-/sdk-2.6.2.tgz",
"integrity": "sha512-TCaGh6BvQNY8Atru0KJXdGuwm70y9WS2xBOLNripHjYMEcft0BMIiLKUvJ7pAeJOtk/15MvPaUIEmBS2Vt0NBg==",
"license": "MIT",
"dependencies": {
"crypto-js": "^4.1.1"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -3491,6 +3501,12 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "no-copy-frontend",
"version": "0.1.1",
"version": "0.4.0",
"private": true,
"scripts": {
"dev": "next dev -p 2999",
@@ -13,6 +13,7 @@
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2",
"clsx": "^2.1.1",
"jose": "^6.1.2",
"next": "^16.0.7",
+5 -1
View File
@@ -2,9 +2,10 @@ import { Metadata } from 'next';
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 LoginForm from '@/app/ui/forms/login-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import { useTranslations } from 'next-intl';
import VKLogin from '@/app/components/VKLogin';
export const metadata: Metadata = {
title: 'Login',
@@ -32,6 +33,9 @@ export default function Page() {
{t('or-log-in-via')}
</span>
</div>
<div>
<VKLogin />
</div>
<div className={`${styles['register-link']}`}>
<p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
@@ -1,7 +1,6 @@
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import TestSection from '@/app/ui/marking-page/test-section';
import { IconAudioFile } from '@/app/ui/icons/icons';
import FilesTable from '@/app/ui/dashboard/files-table';
export default function Page() {
return (
@@ -10,8 +9,8 @@ export default function Page() {
<h1 >Защита аудио</h1>
</div>
<ProtectionSummary />
<TestSection />
<ProtectedFilesTable />
{/* <TestSection /> */}
<FilesTable />
</div>
)
}
@@ -2,18 +2,23 @@ import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import TestSection from '@/app/ui/marking-page/test-section';
import UploadSectionImage from '@/app/ui/marking-page/upload-section-image';
import { IconImageFile } from '@/app/ui/icons/icons';
import { useTranslations } from 'next-intl';
import FilesTable from '@/app/ui/dashboard/files-table';
export default function Page() {
const t = useTranslations('Global');
return (
<div>
<div className="page-title">
<h1 >Защита изображений</h1>
<h1>
{t('image-protection')}
</h1>
</div>
<ProtectionSummary />
<UploadSectionImage />
<TestSection />
<ProtectedFilesTable />
{/* <TestSection /> */}
<FilesTable />
{/* <ProtectedFilesTable /> */}
</div>
)
}
@@ -1,7 +1,7 @@
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import TestSection from '@/app/ui/marking-page/test-section';
import UploadSectionVideo from '@/app/ui/marking-page/upload-section-video';
import FilesTable from '@/app/ui/dashboard/files-table';
export default function Page() {
@@ -12,8 +12,8 @@ export default function Page() {
</div>
<ProtectionSummary />
<UploadSectionVideo />
<TestSection />
<ProtectedFilesTable />
{/* <TestSection /> */}
<FilesTable />
</div>
)
}
+12 -7
View File
@@ -5,20 +5,25 @@ import SubscriptionSettings from '@/app/ui/settings/subscription-settings';
import APISettings from '@/app/ui/settings/api-settings';
import LastActivitySettings from '@/app/ui/settings/last-activity-settings';
import DangerZone from '@/app/ui/settings/danger-zone';
import { useTranslations } from 'next-intl';
export default function Page() {
const t = useTranslations('Global');
return (
<>
<h1 className="page-title"> Настройки аккаунта</h1>
<h1 className="page-title">
{t('account-settings')}
</h1>
<div className="settings-grid">
<PersonalDataSettings />
<NotificationsMonitoringSettings/>
<SafetySetting/>
<SubscriptionSettings/>
<APISettings/>
<LastActivitySettings/>
<NotificationsMonitoringSettings />
<SafetySetting />
<SubscriptionSettings />
<APISettings />
<LastActivitySettings />
</div>
<DangerZone/>
<DangerZone />
</>
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
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 RegisterForm from '@/app/ui/forms/register-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import {useTranslations} from 'next-intl';
+10 -2
View File
@@ -16,20 +16,28 @@ export type FormAction = (
formData: FormData
) => Promise<FormState>
const emailRegex = /^([a-zA-Z0-9.\-_]+|[а-яА-ЯёЁ0-9.\-_]+)@(([a-zA-Z0-9.\-]+)\.([a-zA-Z]{2,})|([a-zA-Z0-9.\-]+)\.([а-яА-ЯёЁ]{2,})|([а-яА-ЯёЁ0-9.\-]+)\.([a-zA-Z]{2,})|([а-яА-ЯёЁ0-9.\-]+)\.([а-яА-ЯёЁ]{2,}))$/i;
export const SignupFormSchema = z
.object({
full_name: z
.string()
.min(3, { error: 'register-error-name' })
.trim(),
email: z.email({ error: 'register-error-valid-email' }).trim(),
email: z
.string()
.trim()
.refine(
(value) => emailRegex.test(value),
{ message: 'register-error-valid-email' }
),
phone: z
.string()
.min(12, { error: 'register-error-phone' }),
password: z
.string()
.min(8, { error: 'register-error-password-symbols' })
.regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter'})
.regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter' })
.regex(/[0-9]/, { error: 'register-error-password-one-digit' })
.trim(),
confirm_password: z
+2 -2
View File
@@ -16,13 +16,13 @@ export const PieChartComponent = ({ data }: {
>
<ResponsiveContainer width={200} height={200}>
<PieChart>
<text x={100} y={80} className="pie-char-text">
<text x={100} y={75} className="pie-char-text">
0
</text>
<text x={100} y={100} className="pie-char-text">
{t('out-of')}
</text>
<text x={100} y={120} className="pie-char-text">
<text x={100} y={125} className="pie-char-text">
0
</text>
<Pie
+185
View File
@@ -0,0 +1,185 @@
import { useTranslations } from 'next-intl';
import { BarChart, Bar, Cell, XAxis, Tooltip } from 'recharts';
type Files = {
data: {
name: string,
checks: number,
violations: number
}[]
}
interface BarShapeProps {
x: number;
y: number;
width: number;
height: number;
fill: string;
radius?: number[];
}
export const StackedBarChart = ({ data }: Files) => {
const t = useTranslations('Global');
const strokeColor = "#dfdede24";
const strokeWidth = 1;
const renderBarWithPartialBorder = (props: BarShapeProps) => {
const { x, y, width, height, fill } = props;
const borderRadius = 10;
return (
<>
{/* Основной прямоугольник с закругленными углами */}
<rect
x={x}
y={y}
width={width}
height={height}
fill={fill}
rx={borderRadius}
ry={borderRadius}
/>
{/* Левая граница (центральная часть, без закруглений) */}
<line
x1={x}
y1={y + borderRadius}
x2={x}
y2={y + height - borderRadius}
/>
{/* Правая граница (центральная часть, без закруглений) */}
<line
x1={x + width}
y1={y + borderRadius}
x2={x + width}
y2={y + height - borderRadius}
/>
{/* Нижняя граница (прямая часть между закруглениями) */}
<line
x1={x + borderRadius}
y1={y + height}
x2={x + width - borderRadius}
y2={y + height}
/>
{/* Верхняя граница (прямая часть между закруглениями) */}
<line
x1={x + borderRadius}
y1={y}
x2={x + width - borderRadius}
y2={y}
/>
</>
);
};
const renderBarWithTopBorder = (props: BarShapeProps) => {
const { x, y, width, height, fill } = props;
const borderRadius = 10;
if (height <= 0 || width <= 0) return null;
return (
<>
{/* Основной прямоугольник с закругленными углами */}
<rect
x={x}
y={y}
width={width}
height={height}
fill={fill}
rx={borderRadius}
ry={borderRadius}
/>
{/* Левая граница (центральная часть, без закруглений) */}
<line
x1={x}
y1={y + borderRadius}
x2={x}
y2={y + height - borderRadius}
stroke={strokeColor}
strokeWidth={strokeWidth}
/>
{/* Правая граница (центральная часть, без закруглений) */}
<line
x1={x + width}
y1={y + borderRadius}
x2={x + width}
y2={y + height - borderRadius}
stroke={strokeColor}
strokeWidth={strokeWidth}
/>
{/* Нижняя граница (прямая часть между закруглениями) */}
<line
x1={x + borderRadius}
y1={y + height}
x2={x + width - borderRadius}
y2={y + height}
stroke={strokeColor}
strokeWidth={strokeWidth}
/>
{/* Верхняя граница (прямая часть между закруглениями) */}
<line
x1={x + borderRadius}
y1={y}
x2={x + width - borderRadius}
y2={y}
stroke={strokeColor}
strokeWidth={strokeWidth}
/>
</>
);
};
return (
<BarChart
style={{ width: '100%', maxWidth: '600px', maxHeight: '210px', aspectRatio: 1.618 }}
responsive
data={data}
barCategoryGap={15}
margin={{
top: 20,
right: 0,
left: 0,
bottom: 5,
}}
>
{/* <Tooltip /> */}
<XAxis
dataKey="name"
stroke="#fff"
fontSize={12}
axisLine={false}
/>
<Bar
dataKey="checks"
stackId="a"
label={{ position: 'top', fill: '#fff', fontSize: 12 }}
radius={5}
shape={renderBarWithPartialBorder as any}
>
{data.map((_entry, index) => (
<Cell key={`cell-${index}`} fill={'#fff'} />
))}
</Bar>
<Bar
dataKey="violations"
stackId="b"
label={{ position: 'top', fill: '#fff', fontSize: 12 }}
radius={5}
shape={renderBarWithTopBorder as any}
>
{data.map((_entry, index) => (
<Cell key={`cell-${index}`} fill={'#f08c00'} />
))}
</Bar>
</BarChart>
);
};
+48
View File
@@ -0,0 +1,48 @@
'use client'
import { useEffect, useRef } from 'react';
import * as VKID from '@vkid/sdk';
export default function VKLogin() {
const vkButton = useRef<HTMLDivElement>(null)
function clickHandler() {
alert('Кнопка пока не подключена к бизнес аккаунту!');
/* VKID.Auth.login()
.then(vkidOnSuccess)
.catch(console.error); */
}
/* function vkidOnSuccess(data: any) {
console.log('vkidOnSuccess');
console.log(data);
} */
/* useEffect(() => {
VKID.Config.init({
app: 54389559,
redirectUrl: 'http://localhost',
responseMode: VKID.ConfigResponseMode.Callback,
source: VKID.ConfigSource.LOWCODE,
scope: 'email'
});
}, []); */
return (
<button id="VKIDSDKAuthButton" className="VkIdWebSdk__button VkIdWebSdk__button_reset">
<div className="VkIdWebSdk__button_container">
<div className="VkIdWebSdk__button_icon">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M4.54648 4.54648C3 6.09295 3 8.58197 3 13.56V14.44C3 19.418 3 21.907 4.54648 23.4535C6.09295 25 8.58197 25 13.56 25H14.44C19.418 25 21.907 25 23.4535 23.4535C25 21.907 25
19.418 25 14.44V13.56C25 8.58197 25 6.09295 23.4535 4.54648C21.907 3 19.418 3 14.44 3H13.56C8.58197 3 6.09295 3 4.54648 4.54648ZM6.79999 10.15C6.91798 15.8728 9.92951 19.31 14.8932 19.31H15.1812V16.05C16.989 16.2332 18.3371
17.5682 18.8875 19.31H21.4939C20.7869 16.7044 18.9535 15.2604 17.8141 14.71C18.9526 14.0293 20.5641 12.3893 20.9436 10.15H18.5722C18.0747 11.971 16.5945 13.6233 15.1803 13.78V10.15H12.7711V16.5C11.305 16.1337 9.39237 14.3538 9.314 10.15H6.79999Z" fill="white" />
</svg>
</div>
<div className="VkIdWebSdk__button_text" onClick={clickHandler}>
Войти с VK ID
</div>
</div>
</button>
)
}
+79
View File
@@ -0,0 +1,79 @@
import React, { ReactNode, useState, useRef } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside';
interface DropDownListProps {
children: ReactNode;
value: string | number;
callBack: (value: string) => void;
}
interface ChildProps {
value?: string;
onClick?: () => void;
className?: string;
children?: React.ReactNode;
}
export default function DropDownList({ children, value, callBack }: DropDownListProps) {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
useClickOutside(dropdownRef, () => {
setIsOpen(false);
});
const enhancedChildren = React.Children.map(children, (child, index) => {
if (React.isValidElement<ChildProps>(child)) {
const childValue = child.props.value || undefined;
return React.cloneElement<ChildProps>(child, {
onClick: () => {
callBack(childValue || "");
setIsOpen(false);
},
className: `flex items-center w-full px-4 py-2 text-sm text-left hover:bg-blue-100 cursor-pointer select-none`,
key: index
});
}
return child;
});
return (
<div className="relative w-full" ref={dropdownRef}>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
className="flex items-center justify-between w-full px-4 py-2 text-sm font-medium bg-white border-2 border-[#d1cece] rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#6365f186]"
>
<span className="flex items-center">
<span className="mr-2">
{value}
</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-[#d1cece] focus:outline-none">
<ul
className="py-1"
role="listbox"
aria-labelledby="language-selector"
>
{enhancedChildren}
</ul>
</div>
)}
</div>
)
}
+455 -205
View File
@@ -10,292 +10,542 @@ import {
ColumnDef,
SortingState,
ColumnFiltersState,
FilterFn,
} from '@tanstack/react-table';
import { rankItem } from '@tanstack/match-sorter-utils';
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDownload, IconShield, DoubleArrowRight, ArrowRight, DoubleArrowLeft, ArrowLeft, ArrowUp, ArrowDown, Filter } from '@/app/ui/icons/icons';
import { useTranslations } from 'next-intl';
import DropDownList from '@/app/components/dropDownList';
// Типы данных
type Person = {
type FileType = 'image' | 'video' | 'audio';
type FileItem = {
id: number;
name: string;
age: number;
email: string;
department: string;
salary: number;
fileName: string;
fileType: FileType;
violations: number;
checks: number;
lastCheck: number; // timestamp в миллисекундах
};
// Кастомная функция фильтрации
const fuzzyFilter: FilterFn<Person> = (row, columnId, value, addMeta) => {
const itemRank = rankItem(row.getValue(columnId), value);
addMeta({ itemRank });
return itemRank.passed;
};
// Определяем тип для фильтров
declare module '@tanstack/react-table' {
interface FilterFns {
fuzzy: FilterFn<unknown>;
// Иконки для типов файлов
const FileTypeIcon = ({ type }: { type: FileType }) => {
switch (type) {
case 'image':
return <span className="text-[#f08c00]">
<IconImageFile />
</span>;
case 'video':
return <span className="text-[#2f9e44]">
<IconVideoFile />
</span>;
case 'audio':
return <span className="text-[#1971c2]">
<IconAudioFile />
</span>;
default:
return <span>📄</span>;
}
}
};
export default function DataTable() {
// Данные
const [data] = useState<Person[]>([
{ id: 1, name: 'Иван Петров', age: 28, email: 'ivan@example.com', department: 'IT', salary: 80000 },
{ id: 2, name: 'Анна Сидорова', age: 32, email: 'anna@example.com', department: 'HR', salary: 65000 },
{ id: 3, name: 'Сергей Иванов', age: 45, email: 'sergey@example.com', department: 'Финансы', salary: 95000 },
{ id: 4, name: 'Мария Кузнецова', age: 26, email: 'maria@example.com', department: 'Маркетинг', salary: 60000 },
{ id: 5, name: 'Алексей Смирнов', age: 38, email: 'alex@example.com', department: 'IT', salary: 85000 },
{ id: 6, name: 'Ольга Васнецова', age: 29, email: 'olga@example.com', department: 'HR', salary: 62000 },
{ id: 7, name: 'Дмитрий Орлов', age: 41, email: 'dmitry@example.com', department: 'Финансы', salary: 92000 },
{ id: 8, name: 'Екатерина Новикова', age: 35, email: 'ekaterina@example.com', department: 'Маркетинг', salary: 70000 },
// Форматирование даты из timestamp
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
// Компонент таблицы
export default function TanstakFilesTable() {
// Данные с timestamp вместо Date объектов
const [data] = useState<FileItem[]>([
{ id: 1, fileName: 'image1.jpg', fileType: 'image', violations: 5, checks: 12, lastCheck: 1705267200000 }, // 15.01.2024
{ id: 2, fileName: 'presentation.mp4', fileType: 'video', violations: 2, checks: 8, lastCheck: 1705180800000 }, // 14.01.2024
{ id: 3, fileName: 'song.mp3', fileType: 'audio', violations: 0, checks: 5, lastCheck: 1705094400000 }, // 13.01.2024
{ id: 4, fileName: 'photo1.jpg', fileType: 'image', violations: 3, checks: 10, lastCheck: 1705008000000 }, // 12.01.2024
{ id: 5, fileName: 'movie.mp4', fileType: 'video', violations: 1, checks: 7, lastCheck: 1704921600000 }, // 11.01.2024
{ id: 6, fileName: 'podcast.mp3', fileType: 'audio', violations: 4, checks: 9, lastCheck: 1704835200000 }, // 10.01.2024
{ id: 7, fileName: 'screenshot.png', fileType: 'image', violations: 0, checks: 3, lastCheck: 1704748800000 }, // 09.01.2024
{ id: 8, fileName: 'tutorial.mp4', fileType: 'video', violations: 2, checks: 6, lastCheck: 1704662400000 }, // 08.01.2024
{ id: 9, fileName: 'music.mp3', fileType: 'audio', violations: 1, checks: 4, lastCheck: 1704576000000 }, // 07.01.2024
{ id: 10, fileName: 'image2.jpg', fileType: 'image', violations: 5, checks: 11, lastCheck: 1704489600000 }, // 06.01.2024
{ id: 11, fileName: 'film.mov', fileType: 'video', violations: 0, checks: 2, lastCheck: 1704403200000 }, // 05.01.2024
{ id: 12, fileName: 'sound.wav', fileType: 'audio', violations: 3, checks: 8, lastCheck: 1704316800000 }, // 04.01.2024
{ id: 13, fileName: 'picture.png', fileType: 'image', violations: 2, checks: 7, lastCheck: 1704230400000 }, // 03.01.2024
{ id: 14, fileName: 'video2.mp4', fileType: 'video', violations: 1, checks: 5, lastCheck: 1704144000000 }, // 02.01.2024
{ id: 15, fileName: 'audio2.mp3', fileType: 'audio', violations: 0, checks: 3, lastCheck: 1704057600000 }, // 01.01.2024
]);
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const [rowSelection, setRowSelection] = useState({});
const [dateFilter, setDateFilter] = useState<string>('all');
const [typeFilter, setTypeFilter] = useState<string>('all');
const t = useTranslations("Global");
// Определение колонок
const columns = useMemo<ColumnDef<Person>[]>(
const columns = useMemo<ColumnDef<FileItem>[]>(
() => [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllPageRowsSelected()}
onChange={table.getToggleAllPageRowsSelectedHandler()}
/>
accessorKey: 'fileName',
header: ({ column }) => (
<div className="flex items-center justify-start">
<span>
{t('file')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="ml-2 p-1 hover:bg-gray-200 rounded"
>
{
column.getIsSorted() === 'asc' ?
<span>
<ArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<ArrowDown />
</span>
: <span>
<Filter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
<div className="flex items-center space-x-2">
<FileTypeIcon type={row.original.fileType} />
<div>
<div className="font-medium">{row.original.fileName}</div>
</div>
</div>
),
enableSorting: false,
enableColumnFilter: false,
},
{
accessorKey: 'id',
header: 'ID',
cell: (info) => info.getValue(),
accessorKey: 'violations',
header: ({ column }) => (
<div className="flex items-center justify-center">
<span>
{t('violations')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="ml-2 p-1 hover:bg-gray-200 rounded"
>
{
column.getIsSorted() === 'asc' ?
<span>
<ArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<ArrowDown />
</span>
: <span>
<Filter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className={`text-center font-semibold ${row.original.violations > 0 ? 'text-red-600' : 'text-green-600'}`}>
{row.original.violations}
</div>
),
},
{
accessorKey: 'name',
header: 'Имя',
cell: (info) => info.getValue(),
filterFn: fuzzyFilter,
accessorKey: 'checks',
header: ({ column }) => (
<div className="flex items-center justify-center">
<span>
{t('checks')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="ml-2 p-1 hover:bg-gray-200 rounded"
>
{
column.getIsSorted() === 'asc' ?
<span>
<ArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<ArrowDown />
</span>
: <span>
<Filter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center">
{row.original.checks}
</div>
),
},
{
accessorKey: 'age',
header: 'Возраст',
cell: (info) => info.getValue(),
accessorKey: 'lastCheck',
header: ({ column }) => (
<div className="flex items-center justify-center">
<span>
{t('last-check')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="ml-2 p-1 hover:bg-gray-200 rounded"
>
{
column.getIsSorted() === 'asc' ?
<span>
<ArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<ArrowDown />
</span>
: <span>
<Filter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center">
{formatDate(row.original.lastCheck)}
</div>
),
enableColumnFilter: false,
},
{
accessorKey: 'email',
header: 'Email',
cell: (info) => info.getValue(),
},
{
accessorKey: 'department',
header: 'Отдел',
cell: (info) => info.getValue(),
},
{
accessorKey: 'salary',
header: 'Зарплата',
cell: (info) => `$${Number(info.getValue()).toLocaleString()}`,
id: 'actions',
header: () => {
return (
<div className="flex items-center justify-center">
{t('actions')}
</div>
)
},
cell: ({ row }) => (
<div className="flex space-x-2 justify-center">
<button
onClick={() => handleView(row.original)}
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 text-sm"
>
<IconEye />
</button>
<button
onClick={() => handleDownload(row.original)}
className="px-3 py-1 bg-green-500 text-white rounded hover:bg-green-600 text-sm"
>
<IconDownload />
</button>
<button
onClick={() => handleProtect(row.original)}
className="px-3 py-1 bg-violet-500 text-white rounded hover:bg-violet-600 text-sm"
>
<IconShield />
</button>
</div>
),
enableSorting: false,
enableColumnFilter: false,
},
],
[]
);
// Обработчики действий
const handleView = (file: FileItem) => {
console.log(`Просмотр файла: ${file.fileName}`);
};
const handleDownload = (file: FileItem) => {
console.log(`Скачать: ${file.fileName}`);
};
const handleProtect = (file: FileItem) => {
console.log(`Щиток: ${file.fileName}`);
};
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
let result = data;
// Фильтр по типу файла
if (typeFilter !== 'all') {
result = result.filter(item => item.fileType === typeFilter);
}
// Фильтр по дате
if (dateFilter !== 'all') {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
switch (dateFilter) {
case 'today':
const todayStart = new Date().setHours(0, 0, 0, 0);
result = result.filter(item => item.lastCheck >= todayStart);
break;
case 'week':
const weekAgo = now - sevenDays;
result = result.filter(item => item.lastCheck >= weekAgo);
break;
case 'month':
const monthAgo = now - thirtyDays;
result = result.filter(item => item.lastCheck >= monthAgo);
break;
case 'older':
const monthAgo2 = now - thirtyDays;
result = result.filter(item => item.lastCheck < monthAgo2);
break;
}
}
return result;
}, [data, typeFilter, dateFilter]);
// Создание таблицы
const table = useReactTable({
data,
data: filteredData,
columns,
filterFns: {
fuzzy: fuzzyFilter,
},
state: {
sorting,
columnFilters,
globalFilter,
rowSelection,
},
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
globalFilterFn: fuzzyFilter, // Используем саму функцию, а не строку
initialState: {
pagination: {
pageSize: 10,
},
},
});
return (
<div className="p-6 max-w-6xl mx-auto">
<h1 className="text-2xl font-bold mb-6">Таблица сотрудников</h1>
<div className="p-6 mx-auto">
{/* Фильтры */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 p-4">
<div className="flex flex-col md:flex-row gap-4 w-full md:w-auto">
<div className="flex flex-col w-40">
<div className="text-sm font-medium mb-1">{t('date-filter')}:</div>
<DropDownList
value={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates')
case 'week':
return t('for-a-week')
case 'month':
return t('for-a-month')
case 'older':
return t('older-than-a-month')
default:
return t('today')
}
})()}
callBack={setDateFilter}
>
<li value="all">
{t('all-dates')}
</li>
<li value="today">
{t('today')}
</li>
<li value="week">
{t('for-a-week')}
</li>
<li value="month">
{t('for-a-month')}
</li>
<li value="older">
{t('older-than-a-month')}
</li>
</DropDownList>
</div>
{/* Глобальный поиск */}
<div className="mb-4">
<input
type="text"
placeholder="Поиск по всем полям..."
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
className="px-4 py-2 border rounded-md w-full max-w-md"
/>
</div>
<div className="flex flex-col w-40">
<div className="text-sm font-medium mb-1">{t('type-filter')}:</div>
<DropDownList
value={(() => {
switch (typeFilter) {
case 'image':
return t('images')
case 'video':
return t('videos')
case 'audio':
return t('audios')
default:
return t('all-types')
}
})()}
callBack={setTypeFilter}
>
<li value="all">
{t('all-types')}
</li>
<li value="image">
{t('images')}
</li>
<li value="video">
{t('videos')}
</li>
<li value="audio">
{t('audios')}
</li>
</DropDownList>
</div>
</div>
{/* Фильтры по колонкам */}
<div className="flex gap-4 mb-4 flex-wrap">
{table.getAllColumns()
.filter(column => column.getCanFilter() && column.id !== 'select')
.map(column => (
<div key={column.id} className="flex flex-col">
<label className="text-sm font-medium mb-1">
{typeof column.columnDef.header === 'string'
? column.columnDef.header
: column.id}
</label>
{column.id === 'department' ? (
<select
value={(column.getFilterValue() as string) || ''}
onChange={e => column.setFilterValue(e.target.value)}
className="px-3 py-1 border rounded"
>
<option value="">Все отделы</option>
<option value="IT">IT</option>
<option value="HR">HR</option>
<option value="Финансы">Финансы</option>
<option value="Маркетинг">Маркетинг</option>
</select>
) : column.id === 'age' ? (
<input
type="number"
placeholder="Возраст от..."
value={(column.getFilterValue() as number) || ''}
onChange={e => column.setFilterValue(e.target.value ? parseInt(e.target.value) : undefined)}
className="px-3 py-1 border rounded w-32"
/>
) : (
<input
type="text"
placeholder={`Фильтр ${column.id}`}
value={(column.getFilterValue() as string) || ''}
onChange={e => column.setFilterValue(e.target.value)}
className="px-3 py-1 border rounded"
/>
)}
</div>
))}
<div className="flex flex-col w-full md:w-auto">
<div className="text-sm font-medium mb-1">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
callBack={table.setPageSize}
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
{t('show')} {pageSize}
</li>
))}
</DropDownList>
</div>
</div>
{/* Таблица */}
<div className="overflow-x-auto border rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center">
<div className='w-full tanstak-file-table'>
<div className="overflow-x-auto rounded-lg shadow">
<table className=" divide-y divide-gray-200 table-fixed md:table-auto w-full">
<thead className="bg-gray-50">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-700 uppercase tracking-wider whitespace-nowrap"
>
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'string'
? header.column.columnDef.header
: header.column.id}
{{
asc: ' 🔼',
desc: ' 🔽',
}[header.column.getIsSorted() as string] ?? null}
</div>
</th>
))}
</tr>
))}
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{table.getRowModel().rows.map(row => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap">
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header as string}
</th>
))}
</tr>
))}
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id} className="hover:bg-gray-50 transition-colors">
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap">
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="px-6 py-8 text-center text-gray-500">
{t('no-data-for-selected-filters')}
</td>
))}
</tr>
))}
</tbody>
</table>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Пагинация */}
<div className="flex items-center justify-between mt-4">
<div className="flex flex-col md:flex-row items-center justify-between mt-6 gap-4">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-700">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount()}
</strong>
</span>
<span className="text-sm text-gray-500">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {t('files')}
</span>
</div>
<div className="flex items-center gap-2">
<button
className="px-3 py-1 border rounded disabled:opacity-50"
onClick={() => table.setPageIndex(0)}
className="px-2 py-1 border rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<DoubleArrowLeft />
</button>
<button
className="px-3 py-1 border rounded disabled:opacity-50"
className="px-2 py-1 border rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ArrowLeft />
</button>
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
const pageIndex = Math.max(
0,
Math.min(
table.getPageCount() - 5,
table.getState().pagination.pageIndex - 2
)
) + i;
if (pageIndex < table.getPageCount()) {
return (
<button
key={pageIndex}
className={`border border-black px-3 py-1 rounded-md ${table.getState().pagination.pageIndex === pageIndex
? 'bg-blue-500 text-white'
: 'hover:bg-gray-100'
}`}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
return null;
})}
</div>
<button
className="px-3 py-1 border rounded disabled:opacity-50"
className="px-2 py-1 border rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<ArrowRight />
</button>
<button
className="px-3 py-1 border rounded disabled:opacity-50"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
className="px-2 py-1 border rounded-md hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<DoubleArrowRight />
</button>
</div>
<div className="flex items-center gap-4">
<span>
Страница{' '}
<strong>
{table.getState().pagination.pageIndex + 1} из {table.getPageCount()}
</strong>
</span>
<select
value={table.getState().pagination.pageSize}
onChange={e => table.setPageSize(Number(e.target.value))}
className="px-2 py-1 border rounded"
>
{[5, 10, 20, 30, 40, 50].map(pageSize => (
<option key={pageSize} value={pageSize}>
Показать {pageSize}
</option>
))}
</select>
<span>
Всего строк: {data.length} | Отфильтровано: {table.getFilteredRowModel().rows.length}
</span>
</div>
</div>
</div>
);
+52
View File
@@ -0,0 +1,52 @@
.VkIdWebSdk__button_reset {
border: none;
margin: 0;
padding: 0;
width: auto;
overflow: visible;
background: transparent;
color: inherit;
font: inherit;
line-height: normal;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
}
.VkIdWebSdk__button {
background: #0077ff;
cursor: pointer;
transition: all .1s ease-out;
}
.VkIdWebSdk__button:hover {
opacity: 0.8;
}
.VkIdWebSdk__button:active {
opacity: .7;
transform: scale(.97);
}
.VkIdWebSdk__button {
border-radius: 8px;
width: 100%;
min-height: 44px;
}
.VkIdWebSdk__button_container {
display: flex;
align-items: center;
padding: 12px 10px;
}
.VkIdWebSdk__button_icon+.VkIdWebSdk__button_text {
margin-left: -28px;
}
.VkIdWebSdk__button_text {
display: flex;
font-family: -apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
flex: 1;
justify-content: center;
color: #ffffff;
}
+35 -2
View File
@@ -1,10 +1,10 @@
@use './header.scss';
@use './dashboard.scss';
@use './pages-styles.scss';
@use './settings.scss';
@use './payment-container.scss';
@use './reports.scss';
@use './marking-pages.scss';
@use './privacy-and-terms-pages.scss';
@use './VKLogin.scss';
.btn {
padding: 12px 24px;
@@ -93,3 +93,36 @@
padding: 0 24px;
}
}
.show-password-button {
cursor: pointer;
position: absolute;
top: 0px;
right: 6px;
svg {
width: 16px;
color: #686060;
transition: color 0.2s ease;
}
&.show {
svg {
color: black;
}
}
}
@media (max-width: 1300px) {
.tanstak-file-table {
max-width: 800px;
margin: 0 auto;
}
}
@media (max-width: 1200px) {
.tanstak-file-table {
max-width: 500px;
margin: 0 auto;
}
}
+4
View File
@@ -189,3 +189,7 @@
line-height: 1.4;
}
}
.password-wrapper {
position: relative;
}
-284
View File
@@ -1,284 +0,0 @@
.protection-summary {
background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
border: 2px solid #4caf50;
border-radius: 15px;
padding: 20px;
margin-bottom: 25px;
text-align: center;
h3 {
color: #2e7d32;
margin-bottom: 15px;
font-size: 18px;
}
.protection-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-card {
background: rgba(255, 255, 255, 0.8);
padding: 15px;
border-radius: 10px;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #2e7d32;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
}
.upload-section {
background: white;
border-radius: 20px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
h3 {
margin-bottom: 20px;
color: #111827;
font-size: 20px;
}
.drag-drop-zone {
border: 3px dashed #6366f1;
border-radius: 15px;
padding: 40px 20px;
text-align: center;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
margin-bottom: 20px;
transition: all 0.3s ease;
h4 {
color: #6366f1;
margin-bottom: 10px;
}
.description {
color: #6b7280;
margin-bottom: 20px;
}
.btn-primary {
background: linear-gradient(45deg, #6566f1, #01579b);
margin-bottom: 20px;
}
}
.selected-file {
border-radius: 15px;
background: #fff;
padding: 10px;
&-file-info {
text-align: left;
}
.uploaded-image {
border-radius: 0.25rem;
max-height: calc(4px * 40);
margin-inline: auto;
margin-bottom: 20px;
border: 1px solid #d1d5db;
}
.btn-confirm {
background: linear-gradient(45deg, #6566f1, #01579b);
color: white;
border: none;
padding: 5px 15px;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
transition: 0.3s;
min-width: 160px;
transform: translateY(0px);
box-shadow: none;
}
.btn-cancel {
background: #fff;
color: rgb(99, 102, 241);
border: 2px solid rgb(99, 102, 241);
padding: 5px 15px;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
transition: 0.3s;
min-width: 120px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px #6366f14d;
}
}
}
.test-section {
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
border-radius: 20px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
h3 {
margin-bottom: 20px;
color: #e65100;
font-size: 20px;
}
.test-section-content {
border: 2px dashed #ff9800;
border-radius: 10px;
padding: 20px;
text-align: center;
background: #fff8e1;
h4 {
font-size: 32px;
margin-bottom: 10px;
}
.btn {
background: linear-gradient(45deg, #ff9800, #f57c00);
}
p {
margin-top: 15px;
font-size: 12px;
color: #e65100;
}
}
}
.protected-files-table {
background: white;
border-radius: 15px;
overflow: hidden;
margin-top: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
h3 {
padding: 20px 20px 0;
color: #111827;
font-size: 18px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 15px;
text-align: left;
font-weight: 600;
font-size: 14px;
}
td {
padding: 15px;
border-bottom: 1px solid #f3f4f6;
vertical-align: middle;
}
.files-table {
&-file {
display: flex;
align-items: center;
}
&-icon {
width: 40px;
height: 40px;
background: #f0f9ff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
border: 2px solid #6366f1;
color: #6366f1;
}
&-title {
font-weight: 500;
color: #111827;
font-size: 14px;
}
&-protect {
font-size: 11px;
color: #6366f1;
font-weight: 600;
}
&-size {
color: #6b7280;
font-size: 14px;
}
&-protect {
display: flex;
align-items: center;
}
&-bar {
width: 30px;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
margin-right: 8px;
}
&-fill {
width: 100%;
height: 100%;
background: #6366f1;
border-radius: 3px;
}
&-date {
color: #6b7280;
font-size: 13px;
}
}
.btn-secondary {
background: #6366f1;
color: white;
padding: 5px 16px 5px 10px;
}
.icon {
height: 18px;
}
.real-protection-badge {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: bold;
margin-left: 5px;
}
}
@@ -118,6 +118,24 @@
font-size: 14px;
opacity: 0.9;
}
&-total-info {
position: absolute;
top: 10;
left: 10;
background-color: #1f293718;
padding: 10px;
border-radius: 10;
font-size: 15px;
}
.stacked-bar-chart {
border-radius: 10;
width: 100%;
padding: 0 20px;
display: flex;
justify-content: center;
}
}
&-switch {
@@ -128,13 +146,6 @@
}
.stats-wrapper {
border-radius: 20px;
overflow: hidden;
background: white;
box-shadow: 0 4px 20px #00000014;
margin-bottom: 30px;
padding: 20px;
h3 {
font-size: 20px;
margin-bottom: 10px;
@@ -172,13 +183,15 @@
&.stats-header {
background: #f9fafb;
border-left: none;
font-size: 18px;
font-size: 16px;
font-weight: 500;
}
&.first-column {
font-size: 18px;
font-size: 16px;
justify-content: start;
padding-left: 20px;
font-weight: 500;
.icon {
color: var(--card-color-1);
@@ -186,9 +199,6 @@
}
}
&.last-column {
}
&.last-row {
border-bottom: 1px solid var(--boder-collor);
}
@@ -348,3 +358,297 @@
}
}
}
.protection-summary {
background: linear-gradient(135deg, #e8f5e8, #c8e6c9);
border: 2px solid #4caf50;
border-radius: 15px;
padding: 20px;
margin-bottom: 25px;
text-align: center;
h3 {
color: #2e7d32;
margin-bottom: 15px;
font-size: 18px;
}
.protection-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-card {
background: rgba(255, 255, 255, 0.8);
padding: 15px;
border-radius: 10px;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #2e7d32;
}
.stat-label {
font-size: 12px;
color: #666;
margin-top: 5px;
}
}
.upload-section {
background: white;
border-radius: 20px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
h3 {
margin-bottom: 20px;
color: #111827;
font-size: 20px;
}
.drag-drop-zone {
border: 3px dashed #6366f1;
border-radius: 15px;
padding: 40px 20px;
text-align: center;
background: linear-gradient(135deg, #f0f9ff, #e0f2fe);
margin-bottom: 20px;
transition: all 0.3s ease;
.icon {
width: 70px;
height: 70px;
margin: 0 auto;
color: #6366f1;
}
h4 {
color: #6366f1;
margin-bottom: 5px;
font-size: 22px;
}
.description {
color: #6b7280;
margin-bottom: 5px;
text-align: start;
}
.btn-primary {
background: linear-gradient(45deg, #6566f1, #5a5ce9);
margin-bottom: 40px;
}
}
.selected-file {
border-radius: 15px;
background: #fff;
padding: 10px;
&-file-info {
text-align: left;
}
.uploaded-image {
border-radius: 0.25rem;
max-height: calc(4px * 40);
margin-inline: auto;
margin-bottom: 20px;
border: 1px solid #d1d5db;
}
.btn-confirm {
background: linear-gradient(45deg, #6566f1, #01579b);
color: white;
border: none;
padding: 5px 15px;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
transition: 0.3s;
min-width: 160px;
transform: translateY(0px);
box-shadow: none;
}
.btn-cancel {
background: #fff;
color: rgb(99, 102, 241);
border: 2px solid rgb(99, 102, 241);
padding: 5px 15px;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 16px;
transition: 0.3s;
min-width: 120px;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px #6366f14d;
}
}
}
.test-section {
background: linear-gradient(135deg, #fff3e0, #ffe0b2);
border-radius: 20px;
padding: 30px;
margin-bottom: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
h3 {
margin-bottom: 20px;
color: #e65100;
font-size: 20px;
}
.test-section-content {
border: 2px dashed #ff9800;
border-radius: 10px;
padding: 20px;
text-align: center;
background: #fff8e1;
h4 {
font-size: 32px;
margin-bottom: 10px;
}
.btn {
background: linear-gradient(45deg, #ff9800, #f57c00);
}
p {
margin-top: 15px;
font-size: 12px;
color: #e65100;
}
}
}
.protected-files-table {
background: white;
border-radius: 15px;
overflow: hidden;
margin-top: 30px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
h3 {
padding: 20px 20px 0;
color: #111827;
font-size: 18px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background: linear-gradient(180deg, #6366f1, #8b5cf6);
color: white;
padding: 15px;
text-align: left;
font-weight: 600;
font-size: 14px;
}
td {
padding: 15px;
border-bottom: 1px solid #f3f4f6;
vertical-align: middle;
}
.files-table {
&-file {
display: flex;
align-items: center;
}
&-icon {
width: 40px;
height: 40px;
background: #f0f9ff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
border: 2px solid #6366f1;
color: #6366f1;
}
&-title {
font-weight: 500;
color: #111827;
font-size: 14px;
}
&-protect {
font-size: 11px;
color: #6366f1;
font-weight: 600;
}
&-size {
color: #6b7280;
font-size: 14px;
}
&-protect {
display: flex;
align-items: center;
}
&-bar {
width: 30px;
height: 6px;
background: #e5e7eb;
border-radius: 3px;
margin-right: 8px;
}
&-fill {
width: 100%;
height: 100%;
background: #6366f1;
border-radius: 3px;
}
&-date {
color: #6b7280;
font-size: 13px;
}
}
.btn-secondary {
background: #6366f1;
color: white;
padding: 5px 16px 5px 10px;
}
.icon {
height: 18px;
}
.real-protection-badge {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: bold;
margin-left: 5px;
}
}
-1
View File
@@ -2,7 +2,6 @@
width: 280px;
background: linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%);
padding: 30px 0;
border-radius: 0 30px 30px 0;
position: fixed;
height: 100vh;
overflow-y: auto;
+3 -3
View File
@@ -1,8 +1,8 @@
import DataTable from '@/app/components/tanstakTable';
import TanstakFilesTable from '@/app/components/tanstakTable';
export default function FilesTable() {
return (
<div>
<DataTable/>
<div className="block-wrapper">
<TanstakFilesTable/>
</div>
)
}
+1 -1
View File
@@ -4,7 +4,7 @@ export default function StatsGrid() {
const t = useTranslations("Global");
return (
<div className="stats-wrapper">
<div className="block-wrapper stats-wrapper">
<h3>
{t('your-content')}
</h3>
@@ -1,9 +1,10 @@
'use client'
import styles from '@/app/styles/login.module.scss'
import { useActionState } from 'react';
import { useActionState, useState } from 'react';
import { authorization } from '@/app/actions/auth';
import { useTranslations } from 'next-intl';
import { IconEye } from '@/app/ui/icons/icons';
export default function LoginForm() {
const [state, formAction, isPending] = useActionState(
@@ -12,6 +13,20 @@ export default function LoginForm() {
);
const t = useTranslations('Login-register-form');
const [showPassword, setShowPassword] = useState(false);
function handleMouseDown() {
setShowPassword(true);
}
function handleMouseUp() {
setShowPassword(false);
}
function handleMouseOut() {
setShowPassword(false);
}
return (
<form action={formAction}>
<div className={`${styles['form-group']}`}>
@@ -36,13 +51,24 @@ export default function LoginForm() {
<label className={`${styles['form-label']}`}>
{t('password')}
</label>
<input
type="password"
id="password"
name="password"
className={`${styles['form-input']}`}
placeholder={t('enter-password')}
/>
<div className={`${styles['password-wrapper']}`}>
<input
type={showPassword ? "text" : "password"}
id="password"
name="password"
className={`${styles['form-input']}`}
placeholder={t('enter-password')}
/>
<button
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseOut={handleMouseOut}
type="button"
className={`show-password-button ${showPassword ? 'show' : ''}`}
>
<IconEye />
</button>
</div>
{state?.error?.password && (
<p className={`${styles['form-error']}`}>
{t(state?.error?.password)}
@@ -1,11 +1,12 @@
'use client'
import styles from '@/app/styles/login.module.scss';
import { useActionState } from 'react';
import { useActionState, useState } 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';
import { IconEye } from '@/app/ui/icons/icons';
export default function RegisterForm() {
const [state, formAction, isPending] = useActionState(
@@ -14,6 +15,34 @@ export default function RegisterForm() {
);
const t = useTranslations('Login-register-form');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
function handleMouseDown(target: 'password' | 'confirm-password') {
if (target === 'password') {
setShowPassword(true);
} else {
setShowConfirmPassword(true);
}
}
function handleMouseUp(target: 'password' | 'confirm-password') {
if (target === 'password') {
setShowPassword(false);
} else {
setShowConfirmPassword(false);
}
}
function handleMouseOut(target: 'password' | 'confirm-password') {
if (target === 'password') {
setShowPassword(false);
} else {
setShowConfirmPassword(false);
}
}
return (
<form action={(e) => {
formAction(e);
@@ -89,13 +118,32 @@ export default function RegisterForm() {
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('password')}
</label>
<input
type="password"
id="password" name="password"
className={`${styles['form-input']}`}
placeholder={t('password-placeholder')}
defaultValue={state?.previousState?.password ? state?.previousState?.password : ''}
/>
<div
className={`${styles['password-wrapper']}`}
>
<input
type={showPassword ? "text" : "password"}
id="password" name="password"
className={`${styles['form-input']}`}
placeholder={t('password-placeholder')}
defaultValue={state?.previousState?.password ? state?.previousState?.password : ''}
/>
<button
onMouseDown={() => {
handleMouseDown('password');
}}
onMouseUp={() => {
handleMouseUp('password');
}}
onMouseOut={() => {
handleMouseOut('password')
}}
type="button"
className={`show-password-button ${showPassword ? 'show' : ''}`}
>
<IconEye />
</button>
</div>
{state?.error?.password && (
<p className="text-sm text-red-500">
{
@@ -110,14 +158,31 @@ export default function RegisterForm() {
<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={t('repeat-password')}
defaultValue={state?.previousState?.confirm_password ? state?.previousState?.confirm_password : ''}
/>
<div className={`${styles['password-wrapper']}`}>
<input
type={showConfirmPassword ? "text" : "password"}
id="confirm_password"
name="confirm_password"
className={`${styles['form-input']}`}
placeholder={t('repeat-password')}
defaultValue={state?.previousState?.confirm_password ? state?.previousState?.confirm_password : ''}
/>
<button
onMouseDown={() => {
handleMouseDown('confirm-password');
}}
onMouseUp={() => {
handleMouseUp('confirm-password');
}}
onMouseOut={() => {
handleMouseOut('confirm-password')
}}
type="button"
className={`show-password-button ${showPassword ? 'show' : ''}`}
>
<IconEye />
</button>
</div>
{state?.error?.confirm_password && (
<p className="text-sm text-red-500">
{t(state?.error?.confirm_password)}
+1 -1
View File
@@ -65,7 +65,7 @@ export default function NotificationsButton() {
<div className="notification-item">
<div className="notification-icon"></div>
<div className="notification-content">
<div className="notification-title">🪙 Токены добавлены</div>
<div className="notification-title">Токены добавлены</div>
<div className="notification-text">На ваш баланс добавлено 100 токенов. Теперь вы можете защитить ещё больше файлов!</div>
<div className="notification-time">4 дн назад</div>
</div>
+62 -1
View File
@@ -26,7 +26,17 @@ export function IconAudioFile() {
export function IconShield() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22q-3.475-.875-5.738-3.988T4 11.1V5l8-3l8 3v6.1q0 3.8-2.262 6.913T12 22m0-2.1q2.6-.825 4.3-3.3t1.7-5.5V6.375l-6-2.25l-6 2.25V11.1q0 3.025 1.7 5.5t4.3 3.3M10 16h4q.425 0 .713-.288T15 15v-3q0-.425-.288-.712T14 11v-1q0-.825-.587-1.412T12 8t-1.412.588T10 10v1q-.425 0-.712.288T9 12v3q0 .425.288.713T10 16m1-5v-1q0-.425.288-.712T12 9t.713.288T13 10v1z" /></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
<path fill="currentColor" d="M12 22q-3.475-.875-5.738-3.988T4 11.1V5l8-3l8 3v6.1q0 3.8-2.262 6.913T12 22m0-2.1q2.6-.825 4.3-3.3t1.7-5.5V6.375l-6-2.25l-6 2.25V11.1q0 3.025 1.7 5.5t4.3 3.3M10 16h4q.425 0 .713-.288T15 15v-3q0-.425-.288-.712T14 11v-1q0-.825-.587-1.412T12 8t-1.412.588T10 10v1q-.425 0-.712.288T9 12v3q0 .425.288.713T10 16m1-5v-1q0-.425.288-.712T12 9t.713.288T13 10v1z" />
</svg>
)
}
export function IconShieldAdd() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16" className="icon">
<path fill="currentColor" d="M7.647 2.146a.5.5 0 0 1 .708 0C9.595 3.39 10.969 4 12.5 4a.5.5 0 0 1 .5.5v1.1a5.5 5.5 0 0 0-1-.393v-.226c-1.48-.112-2.815-.726-4-1.792c-1.186 1.066-2.52 1.68-4 1.792v2.52c0 1.431.361 2.56 1.017 3.44c.053.66.222 1.288.487 1.862C3.844 11.59 3 9.81 3 7.502V4.5a.5.5 0 0 1 .5-.5c1.53 0 2.904-.611 4.147-1.854M15 10.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4-2a.5.5 0 0 0-1 0V10H8.5a.5.5 0 0 0 0 1H10v1.5a.5.5 0 0 0 1 0V11h1.5a.5.5 0 0 0 0-1H11z" />
</svg>
)
}
@@ -37,3 +47,54 @@ export function IconDownload() {
</svg>
)
}
export function IconEye() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
<path fill="currentColor" d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5s5 2.24 5 5s-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3s3-1.34 3-3s-1.34-3-3-3" />
</svg>
)
}
export function DoubleArrowRight() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M6.41 6L5 7.41L9.58 12L5 16.59L6.41 18l6-6z" /><path fill="currentColor" d="m13 6l-1.41 1.41L16.17 12l-4.58 4.59L13 18l6-6z" /></svg>
)
}
export function DoubleArrowLeft() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M17.59 18L19 16.59L14.42 12L19 7.41L17.59 6l-6 6z" /><path fill="currentColor" d="m11 18l1.41-1.41L7.83 12l4.58-4.59L11 6l-6 6z" /></svg>
)
}
export function ArrowRight() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M8.59 16.59L13.17 12L8.59 7.41L10 6l6 6l-6 6z" /></svg>
)
}
export function ArrowLeft() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6l6 6z" /></svg>
)
}
export function ArrowUp() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m7 14l5-5l5 5z" /></svg>
)
}
export function ArrowDown() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="m7 10l5 5l5-5z" /></svg>
)
}
export function Filter() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M11 18h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1m4 6h10c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1" /></svg>
)
}
+2 -2
View File
@@ -22,8 +22,8 @@ export default function PhoneInput({ phoneState }: PhoneInputProps) {
}
const limitedDigits = digitsOnly.slice(0, 11);
const countryCode = limitedDigits.startsWith('8') ? '8' : '7';
const phoneNumber = limitedDigits.startsWith('8') || limitedDigits.startsWith('7')
const countryCode = 7;
const phoneNumber = limitedDigits.startsWith('7')
? limitedDigits.slice(1)
: limitedDigits;
@@ -1,3 +1,5 @@
/* removed */
import { IconShield, IconDownload } from '@/app/ui/icons/icons';
export default function ProtectedFilesTable() {
return (
+120 -13
View File
@@ -1,21 +1,128 @@
'use client'
import { useState } from 'react';
import { StackedBarChart } from '@/app/components/StackedBarChart';
import { useTranslations } from 'next-intl';
const data = [
{
name: 'File A',
checks: 4000,
violations: 2000
},
{
name: 'File B',
checks: 3000,
violations: 1400
},
{
name: 'File C',
checks: 2000,
violations: 1000
},
{
name: 'File D',
checks: 2780,
violations: 1000
},
{
name: 'File E',
checks: 5090,
violations: 4800
},
{
name: 'File F',
checks: 5390,
violations: 3800
},
{
name: 'File H',
checks: 4000,
violations: 1500
}
];
export default function ProtectionSummary() {
const [isOpen, setIsOpen] = useState(false);
const t = useTranslations('Global');
return (
<div className="protection-summary">
<h3>Статистика защиты</h3>
<div className="protection-stats">
<div className="stat-card">
<div className="stat-number">0</div>
<div className="stat-label">Всего файлов</div>
<div className="protection-overview">
<h3>{t('protecting-your-content')}</h3>
<p>{t('current-status-of')}</p>
<div className={`protection-stats ${isOpen ? "opened" : ""}`}>
<div className="protection-stat total-files">
<div className="protection-stat-value">0</div>
<div className="protection-stat-label">{t('total-files')}</div>
</div>
<div className="stat-card">
<div className="stat-number">0</div>
<div className="stat-label">Общий размер</div>
</div>
<div className="stat-card">
<div className="stat-number">0</div>
<div className="stat-label">Проверки/нарушения</div>
<div className="protection-stat total-checks">
{isOpen ? (
<>
<div className="protection-stat-value">0</div>
<div className="protection-stat-label">
{t('size')}
</div>
</>
) :
<>
<div className="protection-stat-row">
<div className="protection-stat-value">0</div>
<div className="protection-stat-label">{t('check')}</div>
</div>
<div className="protection-stat-row">
<div className="protection-stat-value">0</div>
<div className="protection-stat-label">{t('violations')}</div>
</div>
</>
}
</div>
{isOpen ? (
<div className="protection-stat total-usage relative">
<div className="stacked-bar-chart">
<StackedBarChart data={data} />
</div>
<div className="protection-stat-total-info">
<div
className="font-bold"
>
{t('check')}: 777
</div>
<div
className="font-bold text-[#f08c00]"
>
{t('violations')}: 777
</div>
</div>
</div>
) : (
<div className="protection-stat total-usage">
<div className="protection-stat-value">0</div>
<div className="protection-stat-label">
{t('size')}
</div>
</div>
)}
</div>
<button
className="protection-overview-switch cursor-pointer"
onClick={() => {
setIsOpen(!isOpen);
}}
>
<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>
</div>
)
}
@@ -1,6 +1,7 @@
'use client'
import React, { useRef, useState, useCallback, ChangeEvent, DragEvent } from 'react';
import {IconShieldAdd} from '@/app/ui/icons/icons';
interface SelectedFile {
file: File;
@@ -188,11 +189,9 @@ export default function UploadSectionImage() {
transition: 'all .2s ease-in-out'
}}
>
<IconShieldAdd />
<h4>Перетащите изображения сюда</h4>
<p className="description">
Размер: 150x150 - 4000x4000 пикселей, форматы: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается)
</p>
<p className="mb-2.5 text-[#6b7280]">или</p>
<input
ref={fileInputRef}
type="file"
@@ -210,6 +209,16 @@ export default function UploadSectionImage() {
Выбрать файлы для защиты
</button>
<p className="description">
Разрешение изображение: 100x100 - 10000x10000 пикселей
</p>
<p className="description">
Размер файла: до 20 МБ
</p>
<p className="description">
Формат файла: JPG, PNG, GIF, BMP (WEBP НЕ поддерживается)
</p>
{error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-600 font-medium"> {error}</p>
+3 -1
View File
@@ -6,7 +6,9 @@ import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
export default function DropDownList() {
const pathname = usePathname();
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
console.log('pathname');
console.log(pathname);
const [dropDownExpanded, setDropDownExpanded] = useState(false);
const t = useTranslations('Global');
+1 -1
View File
@@ -10,7 +10,7 @@ import { logout } from '@/app/actions/auth';
import { useTranslations } from 'next-intl';
export default function NavLinks() {
const pathname = usePathname();
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
const t = useTranslations('Global');
const links = [
+39 -14
View File
@@ -1,5 +1,6 @@
'use client'
import { useTranslations } from 'next-intl';
import { useActionState, useState } from 'react'
export default function PersonalDataSettings() {
@@ -12,58 +13,82 @@ export default function PersonalDataSettings() {
const [selectedValue, setSelectedValue] = useState('male');
const t = useTranslations('Global');
return (
<div className="settings-form">
<div className="form-section">
<h3 className="form-section-title">👤 Персональные данные</h3>
<h3 className="form-section-title">
{t('personal-data')}
</h3>
<form action={formAction}>
<div className="form-group">
<label className="form-label">Полное имя:</label>
<label className="form-label">
{t('full-name')}:
</label>
<input type="text" name="full_name" className="form-input" />
</div>
<div className="form-group">
<label className="form-label">Компания:</label>
<label className="form-label">
{t('company')}:
</label>
<input type="text" name="company" className="form-input" />
</div>
<div className="form-group">
<label className="form-label">Email:</label>
<label className="form-label">
{t('email')}:
</label>
<input type="text" name="email" className="form-input" />
<small>
Email нельзя изменить. Обратитесь в поддержку.
{t('email-cant-change')}
</small>
</div>
<div className="form-group">
<label className="form-label">Телефон:</label>
<label className="form-label">
{t('phone')}:
</label>
<input type="phone" name="phone" className="form-input" />
</div>
<div className="form-group">
<label className="form-label">Пол:</label>
<label className="form-label">
{t('gender')}:
</label>
<select
name="gender"
className="form-input"
value={selectedValue}
onChange={(e) => setSelectedValue(e.target.value)}
>
<option value="male">Мужской</option>
<option value="female">Женский</option>
<option value="male">
{t('male')}
</option>
<option value="female">
{t('female')}
</option>
</select>
</div>
<div className="form-group">
<label className="form-label">🎂 День рождения:</label>
<label className="form-label">
{t('birthday')}:
</label>
<input type="date" name="birthday" className="form-input" />
<small>
Используется для отправки поздравления
{t('used-to-send-congratulations')}
</small>
</div>
<div className="form-group">
<label className="form-label">Возраст:</label>
<label className="form-label">
{t('age')}:
</label>
<input type="text" name="age" className="form-input" />
<small>
Заполняется автоматически исходя из даты рождения
{t('automatically-filled-in-based-on-date-of-birth')}
</small>
</div>
<button type="submit" className="btn btn-primary">💾 Сохранить изменения</button>
<button type="submit" className="btn btn-primary">
{t('save-changes')}
</button>
</form>
</div >
</div >
+34 -2
View File
@@ -4,9 +4,12 @@
"description": "This is a demo application with i18n support"
},
"Global": {
"actions": "Actions",
"all-types": "All types",
"statistics": "Statistics",
"your-content": "Your content",
"fail": "Fail",
"file": "File",
"files": "Files",
"content-type": "Content type",
"quantity": "Quantity",
"size": "Size",
@@ -37,7 +40,36 @@
"current-status-of": "Current status of security and activity of the monitoring system",
"total-files": "Total files",
"disk-space-used": "Disk space used",
"out-of": "out of"
"out-of": "out of",
"last-check": "Last check",
"all-dates": "All dates",
"today": "Today",
"for-a-week": "For a week",
"for-a-month": "For a month",
"older-than-a-month": "Older than a month",
"show": "Show",
"no-data-for-selected-filters": "No data for selected filters",
"page": "Page",
"shown": "Shown",
"date-filter": "Date filter",
"type-filter": "Type filter",
"items-per-page": "Items per page",
"email": "Email",
"image-protection": "Защита изображений",
"account-settings": "Account settings",
"personal-data": "Personal data",
"full-name": "Full name",
"company": "Company",
"email-cant-change": "Email address cannot be changed. Please contact support.",
"phone": "Phone",
"gender": "Gender",
"birthday": "Birthday",
"age": "Age",
"male": "Male",
"female": "Female",
"used-to-send-congratulations": "Used to send congratulations",
"automatically-filled-in-based-on-date-of-birth": "Automatically filled in based on date of birth",
"save-changes": "Save changes"
},
"Login-register-form": {
"and": "and",
+34 -2
View File
@@ -4,9 +4,12 @@
"description": "Это демонстрационное приложение с поддержкой i18n"
},
"Global": {
"actions": "Действия",
"all-types": "Все типы",
"statistics": "Статистика",
"your-content": "Ваш контент",
"fail": "Фаил",
"file": "Фаил",
"files": "Фаил",
"content-type": "Тип контента",
"quantity": "Количество",
"size": "Размер",
@@ -37,7 +40,36 @@
"current-status-of": "Текущий статус защищенности и активности системы мониторинга",
"total-files": "Всего файлов",
"disk-space-used": "Использовано места на диске",
"out-of": "из"
"out-of": "из",
"last-check": "Последняя проверка",
"all-dates": "Все даты",
"today": "Сегодня",
"for-a-week": "За неделю",
"for-a-month": "За месяц",
"older-than-a-month": "Старше месяца",
"show": "Показать",
"no-data-for-selected-filters": "Нет данных по выбранным фильтрам",
"page": "Страница",
"shown": "Показано",
"date-filter": "Фильтр по дате",
"type-filter": "Фильтр по типу",
"items-per-page": "Записей на странице",
"email": "Почта",
"image-protection": "Защита изображений",
"account-settings": "Настройки аккаунта",
"personal-data": "Персональные данные",
"full-name": "Полное имя",
"company": "Компания",
"email-cant-change": "Email нельзя изменить. Обратитесь в поддержку.",
"phone": "Телефон",
"gender": "Пол",
"birthday": "День рождения",
"age": "Возраст",
"male": "Мужской",
"female": "Женский",
"used-to-send-congratulations": "Используется для отправки поздравления",
"automatically-filled-in-based-on-date-of-birth": "Заполняется автоматически исходя из даты рождения",
"save-changes": "Сохранить изменения"
},
"Login-register-form": {
"and": "и",