Merge branch 'main' into NCFRONT-72
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import TanstakFilesTable from '@/app/components/tanstakTable';
|
||||
import TanstakFilesTable from '@/app/components/TanstakTable';
|
||||
|
||||
type FilesTable = {
|
||||
fileType: string
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function DashboardFilesInfo() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-files-info">
|
||||
@@ -24,20 +14,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('images')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.images_quantity ? filesInfo?.images_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.images.quantity ? filesInfo?.images.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-image')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images_check ? filesInfo?.images_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images_violations ? filesInfo?.images_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.violation ? filesInfo?.images.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,20 +36,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('videos')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.videos_quantity ? filesInfo?.videos_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.videos.quantity ? filesInfo?.videos.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-videos')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos_check ? filesInfo?.videos_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos_violations ? filesInfo?.videos_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.violation ? filesInfo?.videos.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,20 +58,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('audios')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.audios_quantity ? filesInfo?.audios_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.audios.quantity ? filesInfo?.audios.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-audios')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios_check ? filesInfo?.audios_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios_violations ? filesInfo?.audios_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.violation ? filesInfo?.audios.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,20 +80,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('documents')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.documents_quantity ? filesInfo?.documents_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.documents.quantity ? filesInfo?.documents.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-documents')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents_check ? convertBytes(filesInfo?.documents_check) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.check? convertBytes(filesInfo?.documents.check) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents_check ? filesInfo?.documents_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents_violations ? filesInfo?.documents_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.violation ? filesInfo?.documents.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function DashboardLastCheck() {
|
||||
return (
|
||||
<div className="dashboard-last-check">
|
||||
@@ -7,12 +9,18 @@ export default function DashboardLastCheck() {
|
||||
17.10.2025 в 10:23</div>
|
||||
</div>
|
||||
<div className="last-check-actions">
|
||||
<button className="btn-report">
|
||||
<Link
|
||||
className="btn-report"
|
||||
href={'reports'}
|
||||
>
|
||||
📄 Отчёт
|
||||
</button>
|
||||
<button className="btn-new-search">
|
||||
</Link>
|
||||
<Link
|
||||
className="btn-new-search"
|
||||
href={'violations'}
|
||||
>
|
||||
🔍 Мониторинг нарушений
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,48 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesData } from '@/app/actions/fileEntity';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import Link from 'next/link';
|
||||
|
||||
type FileItem = {
|
||||
id: string;
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
size?: number | undefined;
|
||||
uploadDate?: number;
|
||||
protectStatus: string;
|
||||
supportId: number;
|
||||
};
|
||||
|
||||
type ApiFile = {
|
||||
id: string;
|
||||
originalFileName: string;
|
||||
mimeType: string;
|
||||
fileSize: number;
|
||||
updatedAt: string;
|
||||
protectStatus: string;
|
||||
supportId: number;
|
||||
};
|
||||
|
||||
type ApiResponse = {
|
||||
files?: ApiFile[];
|
||||
};
|
||||
|
||||
export default function DashboardUserFiles() {
|
||||
const {
|
||||
data: tableData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({
|
||||
queryKey: ['userFilesData', 1, 5],
|
||||
queryFn: () => getUserFilesData(1, 5),
|
||||
|
||||
select: (data: ApiResponse): FileItem[] => {
|
||||
if (!data?.files) return [];
|
||||
|
||||
return data.files.map((item: ApiFile) => {
|
||||
const newDate = new Date(item.updatedAt).getTime();
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
fileName: item.originalFileName,
|
||||
fileType: item.mimeType.toLocaleLowerCase(),
|
||||
size: item.fileSize,
|
||||
uploadDate: newDate,
|
||||
protectStatus: item.protectStatus,
|
||||
supportId: item.supportId,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
const [openDropDownList, setOpenDropDownList] = useState(false);
|
||||
const dropDownList = useRef(null);
|
||||
useClickOutside(
|
||||
dropDownList,
|
||||
() => {
|
||||
setOpenDropDownList(false)
|
||||
},
|
||||
openDropDownList
|
||||
);
|
||||
|
||||
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}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">Ваши файлы</h3>
|
||||
<a href="pages/marking-photo.php" className="view-all-link">Загрузить файл →</a>
|
||||
<div
|
||||
className="section-add-file btn view-all-link"
|
||||
onClick={() => {
|
||||
setOpenDropDownList(!openDropDownList);
|
||||
}}
|
||||
ref={dropDownList}
|
||||
>
|
||||
{t('add')}
|
||||
<div
|
||||
className={`section-drop-down-list ${openDropDownList ? 'opened' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col text-center"
|
||||
>
|
||||
<Link href='/pages/marking-images'>
|
||||
{t('photo-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-video'>
|
||||
{t('video-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-audio'>
|
||||
{t('audio-marking')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-files-list">
|
||||
<div className="file-item">
|
||||
<div className="file-icon video">
|
||||
🎬</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title="Запись экрана 2026-01-09 в 15.45.28.mov">
|
||||
Запись экрана 2026-01-09 в 15.45.28.mov</div>
|
||||
<div className="file-meta">
|
||||
11.3 МБ •
|
||||
11.01.2026</div>
|
||||
|
||||
{tableData?.length ? (
|
||||
tableData?.map((file) => {
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className="file-item"
|
||||
>
|
||||
<div className="file-icon">
|
||||
<FileTypeIcon type={file.fileType} />
|
||||
</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title={file.fileName}>
|
||||
{file.fileName}
|
||||
</div>
|
||||
<div className="file-meta">
|
||||
{convertBytes(file.size ? file.size : 0)} • {formatDate(file.uploadDate ? file.uploadDate : 0)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-badge protected">
|
||||
{file.protectStatus ? t(file.protectStatus) : t('error')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
<div className="file-badge protected">✓ Защищено</div>
|
||||
</div>
|
||||
<div className="file-item">
|
||||
<div className="file-icon video">
|
||||
🎬</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title="SOUNE_миграция.mov">
|
||||
SOUNE_миграция.mov</div>
|
||||
<div className="file-meta">
|
||||
17.1 МБ •
|
||||
11.01.2026</div>
|
||||
</div>
|
||||
<div className="file-badge protected">✓ Защищено</div>
|
||||
</div>
|
||||
<div className="file-item">
|
||||
<div className="file-icon video">
|
||||
🎬</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title="SOUNE_миграция.mov">
|
||||
SOUNE_миграция.mov</div>
|
||||
<div className="file-meta">
|
||||
17.1 МБ •
|
||||
11.01.2026</div>
|
||||
</div>
|
||||
<div className="file-badge protected">✓ Защищено</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function DashboardUserStats() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-user-stats">
|
||||
|
||||
<div className="stat-card projects">
|
||||
<div className="stat-type">ЗАЩИЩЁННЫЕ ФАЙЛЫ</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-value">{filesInfo?.total.protected ? filesInfo?.total.protected : 0}</div>
|
||||
<div className="stat-label">Файлов под защитой</div>
|
||||
<div className="stat-icon">🛡️</div>
|
||||
</div>
|
||||
@@ -37,7 +27,7 @@ export default function DashboardUserStats() {
|
||||
|
||||
<div className="stat-card violations">
|
||||
<div className="stat-type">НАРУШЕНИЯ</div>
|
||||
<div className="stat-value">{filesInfo?.all_files_violations ? filesInfo?.all_files_violations : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div>
|
||||
<div className="stat-label">Требуют внимания</div>
|
||||
<div className="stat-icon">⚠️</div>
|
||||
</div>
|
||||
@@ -45,7 +35,7 @@ export default function DashboardUserStats() {
|
||||
<div className="stat-card storage">
|
||||
<div className="stat-type">ХРАНИЛИЩЕ</div>
|
||||
<div className="stat-value">
|
||||
{filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}
|
||||
{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0}
|
||||
</div>
|
||||
<div className="stat-label">Использовано места</div>
|
||||
<div className="stat-icon">💾</div>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { getProcents } from '@/app/lib/getProcents';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function DahboardLimitsSection() {
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-limits-section">
|
||||
<h3 className="section-title">💰 Использование лимитов</h3>
|
||||
@@ -9,13 +17,13 @@ export default function DahboardLimitsSection() {
|
||||
<span>🔍</span>
|
||||
<span>Проверки</span>
|
||||
</div>
|
||||
<div className="limit-value">520 / 10,000</div>
|
||||
<div className="limit-value">{filesInfo?.total.check ? filesInfo?.total.check : 0} / 10,000</div>
|
||||
</div>
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar low" style={{width: '44%'}}></div>
|
||||
<div className="progress-bar low" style={{ width: '0%' }}></div>
|
||||
</div>
|
||||
<div className="progress-bar-value">
|
||||
5%
|
||||
{getProcents(filesInfo?.total.check, 10000)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,13 +33,13 @@ export default function DahboardLimitsSection() {
|
||||
<span>💾</span>
|
||||
<span>Хранилище</span>
|
||||
</div>
|
||||
<div className="limit-value">790.2 / 1,024 МБ</div>
|
||||
<div className="limit-value">{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} / 1,024 МБ</div>
|
||||
</div>
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar medium" style={{width: '77%'}}></div>
|
||||
<div className="progress-bar medium" style={{ width: `${getProcents(filesInfo?.total.size, 1000000000)}%` }}></div>
|
||||
</div>
|
||||
<div className="progress-bar-value">
|
||||
77%
|
||||
{getProcents(filesInfo?.total.size, 1000000000)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="limit-item">
|
||||
@@ -40,13 +48,13 @@ export default function DahboardLimitsSection() {
|
||||
<span>📁</span>
|
||||
<span>Файлы</span>
|
||||
</div>
|
||||
<div className="limit-value">386 / 500</div>
|
||||
<div className="limit-value">{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0} / 500</div>
|
||||
</div>
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar medium" style={{width: '77%'}}></div>
|
||||
<div className="progress-bar medium" style={{ width: `${getProcents(filesInfo?.total.quantity, 500)}%` }}></div>
|
||||
</div>
|
||||
<div className="progress-bar-value">
|
||||
77%
|
||||
{getProcents(filesInfo?.total.quantity, 500)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,24 +7,33 @@ import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import testImage from '@/app/src/image-preview.png'
|
||||
import { useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ViolationsCarousel() {
|
||||
const paginationRef = useRef(null);
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">Нарушения</h3>
|
||||
<a href="pages/violations.php" className="view-all-link">Смотреть все →</a>
|
||||
<Link
|
||||
href="violations"
|
||||
className="view-all-link"
|
||||
>
|
||||
Смотреть все
|
||||
</Link>
|
||||
</div>
|
||||
<div className="violations-carousel">
|
||||
<div>
|
||||
{t('are-no-violations')}
|
||||
</div>
|
||||
{/* <div className="violations-carousel">
|
||||
<Swiper
|
||||
spaceBetween={50}
|
||||
slidesPerView={1}
|
||||
modules={[Pagination, Autoplay]}
|
||||
pagination={{
|
||||
/* dynamicBullets: true, */
|
||||
/* el: paginationRef.current, */
|
||||
clickable: true,
|
||||
el: '.custon-swiper-pagination'
|
||||
}}
|
||||
@@ -33,7 +42,6 @@ export default function ViolationsCarousel() {
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
/* width={300} */
|
||||
>
|
||||
<SwiperSlide>
|
||||
<div className="violation-slide">
|
||||
@@ -260,7 +268,7 @@ export default function ViolationsCarousel() {
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,23 +5,9 @@ import { PieChartComponent } from '@/app/components/PieChartComponent';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
|
||||
type FilesInfo = {
|
||||
totalSize: number;
|
||||
totalCount: number;
|
||||
imageQuantity: number;
|
||||
videoQuantity: number;
|
||||
audioQuantity: number;
|
||||
imageSize: number;
|
||||
videoSize: number;
|
||||
audioSize: number;
|
||||
documentSize: number;
|
||||
};
|
||||
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function ProtectionOverview() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -42,68 +28,23 @@ export default function ProtectionOverview() {
|
||||
openDropDownList
|
||||
);
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number,
|
||||
images_quantity: number,
|
||||
videos_quantity: number,
|
||||
audios_quantity: number,
|
||||
images_size: number,
|
||||
videos_size: number,
|
||||
audios_size: number,
|
||||
documents_size: number,
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0,
|
||||
imageQuantity: 0,
|
||||
videoQuantity: 0,
|
||||
audioQuantity: 0,
|
||||
imageSize: 0,
|
||||
videoSize: 0,
|
||||
audioSize: 0,
|
||||
documentSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize: data.all_files_size,
|
||||
totalCount: data.all_files_quantity,
|
||||
imageQuantity: data.images_quantity,
|
||||
videoQuantity: data.videos_quantity,
|
||||
audioQuantity: data.audios_quantity,
|
||||
imageSize: data.images_size,
|
||||
videoSize: data.videos_size,
|
||||
audioSize: data.audios_size,
|
||||
documentSize: data.documents_size,
|
||||
}
|
||||
},
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
useEffect(() => {
|
||||
setFilesCount([
|
||||
{
|
||||
name: 'images-few',
|
||||
value: filesInfo?.imageSize ? filesInfo?.imageSize : 0,
|
||||
value: filesInfo?.images.size ? filesInfo?.images.size : 0,
|
||||
color: '#f08c00'
|
||||
},
|
||||
{
|
||||
name: 'videos',
|
||||
value: filesInfo?.videoSize ? filesInfo?.videoSize : 0,
|
||||
value: filesInfo?.videos.size ? filesInfo?.videos.size : 0,
|
||||
color: '#2f9e44'
|
||||
},
|
||||
{
|
||||
name: 'audios',
|
||||
value: filesInfo?.audioSize ? filesInfo?.audioSize : 0,
|
||||
value: filesInfo?.audios.size ? filesInfo?.audios.size : 0,
|
||||
color: '#1971c2'
|
||||
},
|
||||
])
|
||||
@@ -134,14 +75,14 @@ export default function ProtectionOverview() {
|
||||
<div className={`protection-stats ${isOpen ? "opened" : ""}`}>
|
||||
|
||||
|
||||
{filesInfo?.totalCount ? (
|
||||
{filesInfo?.total.quantity ? (
|
||||
<>
|
||||
<div className="protection-stat total-files">
|
||||
<span className="protection-stat-value">
|
||||
{filesInfo?.totalCount ? filesInfo?.totalCount : 0}
|
||||
{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0}
|
||||
</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeFiles(filesInfo?.totalCount || 0)}
|
||||
{pluralizeFiles(filesInfo?.total.quantity || 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="protection-stat total-checks">
|
||||
@@ -180,7 +121,7 @@ export default function ProtectionOverview() {
|
||||
<div
|
||||
className="flex flex-col text-center"
|
||||
>
|
||||
<Link href='/pages/marking-photo'>
|
||||
<Link href='/pages/marking-images'>
|
||||
{t('photo-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-video'>
|
||||
@@ -196,18 +137,18 @@ export default function ProtectionOverview() {
|
||||
)}
|
||||
{isOpen ? (
|
||||
<div className="protection-stat total-usage">
|
||||
<PieChartComponent data={filesCount} show={filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} />
|
||||
<PieChartComponent data={filesCount} show={filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="protection-stat total-usage">
|
||||
<div className="protection-stat-value">{filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} / 0</div>
|
||||
<div className="protection-stat-value">{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} / 0</div>
|
||||
<div className="protection-stat-label">
|
||||
{t('disk-space-used')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!!filesInfo?.totalCount && (
|
||||
{!!filesInfo?.total.quantity && (
|
||||
<button
|
||||
className="protection-overview-switch cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
@@ -2,22 +2,12 @@
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function StatsGrid() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="block-wrapper stats-wrapper">
|
||||
@@ -46,10 +36,10 @@ export default function StatsGrid() {
|
||||
{t('images')}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
{filesInfo?.images_quantity ? filesInfo?.images_quantity : 0}
|
||||
{filesInfo?.images.quantity ? filesInfo?.images.quantity : 0}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}
|
||||
{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
@@ -63,10 +53,10 @@ export default function StatsGrid() {
|
||||
{t('videos')}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
{filesInfo?.videos_quantity ? filesInfo?.videos_quantity : 0}
|
||||
{filesInfo?.videos.quantity ? filesInfo?.videos.quantity : 0}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}
|
||||
{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
@@ -80,10 +70,10 @@ export default function StatsGrid() {
|
||||
{t('audios')}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
{filesInfo?.audios_quantity ? filesInfo?.audios_quantity : 0}
|
||||
{filesInfo?.audios.quantity ? filesInfo?.audios.quantity : 0}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}
|
||||
{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
|
||||
@@ -1,110 +1,5 @@
|
||||
/* deleted */
|
||||
|
||||
/* .violations-table {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f1f5f9;
|
||||
|
||||
.violation-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.violation-card {
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.violation-icon-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.photo {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.violation-content-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.violation-filename {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
font-size: 15px;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.violation-meta {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.violation-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.violation-status-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #fee2e2, #fecaca);
|
||||
color: #dc2626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.violation-sources-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
color: #d97706;
|
||||
white-space: nowrap;
|
||||
}
|
||||
} */
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
import { useActionState } from 'react';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { resetPassword } from '@/app/actions/auth';
|
||||
|
||||
export default function ForgotPasswordForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
() => {
|
||||
console.log("test");
|
||||
},
|
||||
resetPassword,
|
||||
undefined,
|
||||
);
|
||||
const t = useTranslations('Login-register-form');
|
||||
@@ -27,26 +25,14 @@ export default function ForgotPasswordForm() {
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('enter-email')}
|
||||
/>
|
||||
{/* {state?.error?.email && (
|
||||
{state?.error === 'error' && (
|
||||
<p className={`${styles['form-error']}`}>
|
||||
{t(state?.error?.email)}
|
||||
{t('register-error-email-invalid')}
|
||||
</p>
|
||||
)} */}
|
||||
</div>
|
||||
{/* {state?.error.server && (
|
||||
<p className={`${styles['form-error']}`}>
|
||||
{t(state?.error.server)}
|
||||
</p>
|
||||
)} */}
|
||||
<div className={`${styles['form-options']}`}>
|
||||
<div className={`${styles['form-checkbox']}`}>
|
||||
<small>
|
||||
функционал не реализован
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('sign-in')}
|
||||
{t('recover-password')}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -96,8 +96,18 @@ export default function LoginForm() {
|
||||
{t('forgot-password')}?
|
||||
</a>
|
||||
</div>
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('sign-in')}
|
||||
<button
|
||||
type="submit"
|
||||
className={`${styles['btn']}`}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
) : (
|
||||
t("sign-in")
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
import { ChangeEvent, useActionState, useEffect, useRef, useState, MouseEvent } from 'react';
|
||||
import { ChangeEvent, useActionState, useEffect, useRef, useState } from 'react';
|
||||
import { registration, FormState } 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';
|
||||
import { SignupFormSchema } from '@/app/actions/definitions';
|
||||
import { getSignupFormSchema } from '@/app/actions/definitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import * as z from 'zod'
|
||||
import ConfirmMailModalWindow from '@/app/ui/forms/confirm-mail-modal-window';
|
||||
import { fetchINN } from '@/app/actions/action';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
|
||||
export default function RegisterForm() {
|
||||
const searchParams = useSearchParams();
|
||||
const referralCode = searchParams.get('ref');
|
||||
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
registration,
|
||||
undefined,
|
||||
@@ -23,6 +32,7 @@ export default function RegisterForm() {
|
||||
const [formState, setFormState] = useState<FormState | undefined>(undefined);
|
||||
const [showMailConfirmWindow, setShowMailConfirmWindow] = useState(false);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const [accountType, setAccountType] = useState<'b2c' | 'b2b'>('b2c');
|
||||
|
||||
useEffect(() => {
|
||||
setFormState(state);
|
||||
@@ -30,7 +40,40 @@ export default function RegisterForm() {
|
||||
setShowMailConfirmWindow(true);
|
||||
}
|
||||
|
||||
}, [state])
|
||||
}, [state]);
|
||||
|
||||
const debouncedFetchINN = useDebouncedCallback(
|
||||
async (value) => {
|
||||
const response = await fetchINN(value);
|
||||
|
||||
if (response.companyName) {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
...formState?.previousState,
|
||||
companyName: response.companyName
|
||||
},
|
||||
error: {
|
||||
...formState?.error,
|
||||
companyName: ''
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
companyName: response.companyName
|
||||
},
|
||||
error: {
|
||||
...formState?.error,
|
||||
companyName: '',
|
||||
inn: 'inn-invalid'
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
500
|
||||
);
|
||||
|
||||
function showPassowrd(target: 'password' | 'confirm-password') {
|
||||
if (target === 'password') {
|
||||
@@ -40,13 +83,38 @@ export default function RegisterForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function validateField(
|
||||
type SignupFormSchema = Awaited<ReturnType<typeof getSignupFormSchema>>;
|
||||
const [signupFormSchema, setSignupFormSchema] = useState<SignupFormSchema | null>(null);;
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadSchema = async () => {
|
||||
const result = await getSignupFormSchema(accountType);
|
||||
if (isMounted) {
|
||||
setSignupFormSchema(result);
|
||||
}
|
||||
};
|
||||
|
||||
loadSchema();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [accountType]);
|
||||
|
||||
async function validateField(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
) {
|
||||
switch (fieldName) {
|
||||
case 'confirm_password':
|
||||
const baseSchema = SignupFormSchema.shape.confirm_password;
|
||||
const baseSchema = signupFormSchema?.shape.confirm_password;
|
||||
|
||||
if (!baseSchema) {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
|
||||
const baseResult = baseSchema.safeParse(value);
|
||||
|
||||
if (!baseResult.success) {
|
||||
@@ -72,13 +140,25 @@ export default function RegisterForm() {
|
||||
case 'email':
|
||||
case 'fullName':
|
||||
case 'phone':
|
||||
case 'referralCode':
|
||||
case 'password':
|
||||
case 'companyName':
|
||||
case 'agree':
|
||||
const schema = SignupFormSchema.shape[fieldName];
|
||||
case 'inn':
|
||||
const schema = signupFormSchema?.shape[fieldName];
|
||||
if (!schema) {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
|
||||
if (fieldName === 'phone') {
|
||||
return schema.safeParse(value.replace(/[-\(\)\s]/g, ''));
|
||||
}
|
||||
if (fieldName === 'companyName' && accountType === 'b2c') {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
if (fieldName === 'referralCode') {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
return schema.safeParse(value);
|
||||
|
||||
default:
|
||||
@@ -86,12 +166,45 @@ export default function RegisterForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeHandler(e: ChangeEvent<HTMLInputElement>): void {
|
||||
async function onChangeHandler(e: ChangeEvent<HTMLInputElement>): Promise<void> {
|
||||
const fieldName = e.target.name;
|
||||
if (fieldName !== 'phone') {
|
||||
e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, '');
|
||||
}
|
||||
const validatedField = validateField(fieldName, e.target.value);
|
||||
|
||||
if (fieldName === 'inn') {
|
||||
const currentValue = e.target.value;
|
||||
|
||||
const cleanedValue = currentValue.replace(/([^0-9])/g, '');
|
||||
|
||||
if (cleanedValue.length > 10) {
|
||||
e.target.value = cleanedValue.slice(0, 10);
|
||||
debouncedFetchINN(e.target.value);
|
||||
} else {
|
||||
e.target.value = cleanedValue;
|
||||
}
|
||||
|
||||
if (cleanedValue.length === 10) {
|
||||
debouncedFetchINN(cleanedValue);
|
||||
} else {
|
||||
if (formState) {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
...formState?.previousState,
|
||||
companyName: ''
|
||||
},
|
||||
error: {
|
||||
...formState?.error,
|
||||
companyName: '',
|
||||
inn: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validatedField = await validateField(fieldName, e.target.value);
|
||||
|
||||
if (validatedField.success) {
|
||||
setFormState(prevState => {
|
||||
@@ -109,6 +222,26 @@ export default function RegisterForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function switchRegistrationTypeHandler(e: React.MouseEvent<HTMLButtonElement>, type: 'b2c' | 'b2b') {
|
||||
e.preventDefault();
|
||||
setAccountType(type);
|
||||
if (formState?.previousState) {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
...formState.previousState,
|
||||
companyName: '',
|
||||
inn: ''
|
||||
},
|
||||
error: {
|
||||
...formState.error,
|
||||
companyName: '',
|
||||
inn: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -131,6 +264,32 @@ export default function RegisterForm() {
|
||||
<form action={(e) => {
|
||||
formAction(e);
|
||||
}}>
|
||||
<div className={`${styles['form-switcher']}`}>
|
||||
<button
|
||||
className={`${styles['form-switcher-button']} ${accountType === 'b2c' ? styles['active'] : ''}`}
|
||||
onClick={(e) => {
|
||||
switchRegistrationTypeHandler(e, 'b2c');
|
||||
}}
|
||||
>
|
||||
{t('personal')}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles['form-switcher-button']} ${accountType === 'b2b' ? styles['active'] : ''}`}
|
||||
onClick={(e) => {
|
||||
switchRegistrationTypeHandler(e, 'b2b');
|
||||
}}
|
||||
>
|
||||
{t('company')}
|
||||
</button>
|
||||
<input
|
||||
type="hidden"
|
||||
id="accountType"
|
||||
name="accountType"
|
||||
className={`${styles['form-input-hidden']}`}
|
||||
value={accountType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']} ${styles['required']}`}
|
||||
@@ -162,6 +321,107 @@ export default function RegisterForm() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{accountType === 'b2b' ? (
|
||||
<>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']} ${styles['required']}`}
|
||||
>
|
||||
{t('INN')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="inn"
|
||||
name="inn"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('inn-placeholder')}
|
||||
defaultValue={formState?.previousState?.inn ? formState?.previousState?.inn : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formState?.error?.inn && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.inn.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']}`}
|
||||
>
|
||||
{t('company')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('field-filled-automatically')}
|
||||
value={formState?.previousState?.companyName ?? ''}
|
||||
readOnly={true}
|
||||
/* defaultValue={formState?.previousState?.companyName ? formState?.previousState?.companyName : ''} */
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formState?.error?.companyName && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.companyName.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']}`}>
|
||||
{t('referal-code')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="referralCode"
|
||||
name="referralCode"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('referal-code')}
|
||||
defaultValue={referralCode ? referralCode : ''}
|
||||
onChange={(e) => {
|
||||
e.target.value = e.target.value.toLocaleLowerCase();
|
||||
onChangeHandler(e)
|
||||
}}
|
||||
/>
|
||||
{formState?.error?.referralCode && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.referralCode.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('email-adress')}
|
||||
@@ -193,6 +453,7 @@ export default function RegisterForm() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('phone')}
|
||||
@@ -214,37 +475,6 @@ export default function RegisterForm() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']}`}
|
||||
>
|
||||
{t('company')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('company-placeholder')}
|
||||
defaultValue={formState?.previousState?.companyName ? formState?.previousState?.companyName : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formState?.error?.companyName && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.companyName.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('password')}
|
||||
@@ -287,6 +517,7 @@ export default function RegisterForm() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('confirm-password')}
|
||||
@@ -350,8 +581,18 @@ export default function RegisterForm() {
|
||||
{t(formState?.error?.server)}
|
||||
</p>
|
||||
)}
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('create-an-account')}
|
||||
<button
|
||||
type="submit"
|
||||
className={`${styles['btn']}`}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
) : (
|
||||
t("create-an-account")
|
||||
)}
|
||||
</button>
|
||||
</form >
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
'use client'
|
||||
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
import { useActionState, useEffect, useRef, useState, ChangeEvent } from 'react';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { confirmPassword, FormStateConfirmPassword } from '@/app/actions/auth';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { IconEye } from '@/app/ui/icons/icons';
|
||||
import { SignupFormSchema } from '@/app/actions/definitions';
|
||||
import * as z from 'zod';
|
||||
|
||||
export default function ResetPasswordForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
confirmPassword,
|
||||
undefined,
|
||||
);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [formState, setFormState] = useState<FormStateConfirmPassword | undefined>(undefined);
|
||||
const t = useTranslations('Login-register-form');
|
||||
const searchParams = useSearchParams();
|
||||
const email = searchParams.get('email');
|
||||
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
setFormState(state);
|
||||
}
|
||||
}, [state])
|
||||
|
||||
function showPassowrd(target: 'password' | 'confirm-password') {
|
||||
if (target === 'password') {
|
||||
setShowPassword(!showPassword);
|
||||
} else {
|
||||
setShowConfirmPassword(!showConfirmPassword);
|
||||
}
|
||||
}
|
||||
|
||||
function validateField(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
) {
|
||||
switch (fieldName) {
|
||||
case 'confirm_password':
|
||||
const baseSchema = SignupFormSchema.shape.confirm_password;
|
||||
const baseResult = baseSchema.safeParse(value);
|
||||
|
||||
if (!baseResult.success) {
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
const currentPassword = passwordRef.current?.value || '';
|
||||
if (value !== currentPassword) {
|
||||
return {
|
||||
success: false,
|
||||
error: new z.ZodError([
|
||||
{
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'repeat-password',
|
||||
path: ['confirm_password']
|
||||
}
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: value };
|
||||
case 'password':
|
||||
const schema = SignupFormSchema.shape[fieldName];
|
||||
return schema.safeParse(value);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown field: ${fieldName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeHandler(e: ChangeEvent<HTMLInputElement>): void {
|
||||
const fieldName = e.target.name;
|
||||
if (fieldName !== 'phone') {
|
||||
e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, '');
|
||||
}
|
||||
const validatedField = validateField(fieldName, e.target.value);
|
||||
|
||||
if (validatedField.success) {
|
||||
setFormState(prevState => {
|
||||
if (!prevState?.error || !prevState?.error[fieldName]) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const { [fieldName]: _, ...newError } = prevState.error;
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
error: newError
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<input
|
||||
type="hidden"
|
||||
id="email"
|
||||
name="email"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('enter-email')}
|
||||
defaultValue={email ? email : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('recovery-code')}
|
||||
</label>
|
||||
<input
|
||||
type="readOnly"
|
||||
id="verify-token"
|
||||
name="verify-token"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('recovery-code')}
|
||||
defaultValue={formState?.previousState?.verifyToken ? formState?.previousState?.verifyToken : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('password')}
|
||||
</label>
|
||||
<div
|
||||
className={`${styles['password-wrapper']}`}
|
||||
>
|
||||
<input
|
||||
ref={passwordRef}
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
name="password"
|
||||
className={`${styles['form-input']} ${styles['password']}`}
|
||||
placeholder={t('password-placeholder')}
|
||||
defaultValue={formState?.previousState?.password ? formState?.previousState?.password : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
showPassowrd('password')
|
||||
}}
|
||||
type="button"
|
||||
className={`show-password-button ${showPassword ? 'show' : ''}`}
|
||||
>
|
||||
<IconEye />
|
||||
</button>
|
||||
</div>
|
||||
{formState?.error?.password && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.password.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('confirm-password')}
|
||||
</label>
|
||||
<div className={`${styles['password-wrapper']}`}>
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
className={`${styles['form-input']} ${styles['password']}`}
|
||||
placeholder={t('repeat-password')}
|
||||
defaultValue={formState?.previousState?.confirm_password ? formState?.previousState?.confirm_password : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
showPassowrd('confirm-password');
|
||||
}}
|
||||
type="button"
|
||||
className={`show-password-button ${showConfirmPassword ? 'show' : ''}`}
|
||||
>
|
||||
<IconEye />
|
||||
</button>
|
||||
</div>
|
||||
{formState?.error?.confirm_password && (
|
||||
<p className="text-sm text-red-500">
|
||||
{t(formState?.error?.confirm_password)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* {state?.error.server && (
|
||||
<p className={`${styles['form-error']}`}>
|
||||
{t(state?.error.server)}
|
||||
</p>
|
||||
)} */}
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('change-password')}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import NotificationsButton from './notificationsButton';
|
||||
import UserMenuButton from './userMenuButton';
|
||||
import Link from 'next/link';
|
||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import BurgerMenu from '@/app/ui/navigation/burger-menu';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function HeaderPanel() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
return (
|
||||
<header className="header">
|
||||
<BurgerMenu />
|
||||
<div className="header-action">
|
||||
<LanguageSwitcher />
|
||||
<Link
|
||||
href={'/pages/payment'}
|
||||
className="tokens-balance"
|
||||
>
|
||||
<div className="tokens-wrapper" title={t('current-balance-of-protection')}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: "16px", height: "16px" }}>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"></path>
|
||||
</svg>
|
||||
<span className="tokens-count">800</span>
|
||||
<span className="tokens-label">
|
||||
{t('tokens')}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
{/* <LanguageSwitcher /> */}
|
||||
{typeof userData?.tariffInfo?.tokens === 'number' && (
|
||||
<Link
|
||||
href={'/pages/payment'}
|
||||
className="tokens-balance"
|
||||
>
|
||||
<div className="tokens-wrapper" title={t('current-balance-of-protection')}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: "16px", height: "16px" }}>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"></path>
|
||||
</svg>
|
||||
<span className="tokens-count">{userData?.tariffInfo?.tokens}</span>
|
||||
<span className="tokens-label">
|
||||
{t('tokens')}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<NotificationsButton />
|
||||
<UserMenuButton />
|
||||
</div>
|
||||
|
||||
@@ -3,27 +3,15 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import Link from 'next/link';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { logout } from '@/app/actions/auth';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function UserMenuButton() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const t = useTranslations("Global");
|
||||
const {
|
||||
data: userData,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['userData'],
|
||||
queryFn: () => {
|
||||
return getUserData();
|
||||
}
|
||||
})
|
||||
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
useClickOutside(
|
||||
menuRef,
|
||||
@@ -50,7 +38,7 @@ export default function UserMenuButton() {
|
||||
<div
|
||||
className="user-avatar"
|
||||
id="user-menu-btn"
|
||||
title={userData?.fullName ? userData.fullName : ''}
|
||||
title={userData?.fullName ?? ''}
|
||||
>
|
||||
{userData?.fullName ? userData.fullName[0] : ''}
|
||||
</div>
|
||||
@@ -61,10 +49,10 @@ export default function UserMenuButton() {
|
||||
</div>
|
||||
<div className="user-details">
|
||||
<div className="user-name">
|
||||
{userData?.fullName ? userData.fullName : ''}
|
||||
{userData?.fullName ?? ''}
|
||||
</div>
|
||||
<div className="user-email">
|
||||
{userData?.email ? userData.email : ''}
|
||||
{userData?.email ?? ''}
|
||||
</div>
|
||||
<div className="user-subscription">
|
||||
<span className="text-[14px]">{t('rate')}: </span>
|
||||
|
||||
@@ -103,9 +103,24 @@ export function IconArrowDown() {
|
||||
|
||||
export function IconFilter() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
|
||||
<g transform="scale(0.6) translate(7, 7)">
|
||||
<path fill="currentColor" fillRule="evenodd" d="m4.5 1.529l2.838 2.603l-.676.737L5 3.344V14H4V3.344L2.338 4.869l-.676-.737zm6.454 10.745V1.91h1v10.363l1.662-1.525l.676.737l-2.838 2.603l-2.838-2.603l.676-.737z" clipRule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
/* <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
|
||||
<g transform="scale(0.6) translate(7, 7)">
|
||||
<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" />
|
||||
</g>
|
||||
</svg> */
|
||||
|
||||
{/* <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g transform="scale(0.6) translate(6, 6)">
|
||||
<path fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.758 19.414L9 21v-8.5L4.52 7.572A2 2 0 0 1 4 6.227V4h16v2.172a2 2 0 0 1-.586 1.414L15 12v1.5m7 8.5l-5-5m0 5l5-5" />
|
||||
</g>
|
||||
</svg> */}
|
||||
|
||||
export function IconNotification() {
|
||||
return (
|
||||
@@ -147,8 +162,23 @@ export function IconBurgerMenu() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function IconRefresh() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V4h2v7h-7V9h4.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.925 0 3.475-1.1T17.65 14h2.1q-.7 2.65-2.85 4.325T12 20" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconSearch() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5A6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5S14 7.01 14 9.5S11.99 14 9.5 14" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconDiscet() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
|
||||
<g fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 19V5a2 2 0 0 1 2-2h11.172a2 2 0 0 1 1.414.586l2.828 2.828A2 2 0 0 1 21 7.828V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" /><path d="M8.6 9h6.8a.6.6 0 0 0 .6-.6V3.6a.6.6 0 0 0-.6-.6H8.6a.6.6 0 0 0-.6.6v4.8a.6.6 0 0 0 .6.6ZM6 13.6V21h12v-7.4a.6.6 0 0 0-.6-.6H6.6a.6.6 0 0 0-.6.6Z" /></g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { useState, ChangeEvent } from 'react';
|
||||
import { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { InputMask } from '@react-input/mask';
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
|
||||
interface PhoneInputProps {
|
||||
phoneState: string | undefined;
|
||||
validateHandler: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
clearCondition?: string | undefined | null
|
||||
}
|
||||
|
||||
export default function PhoneInput({ phoneState, validateHandler }: PhoneInputProps) {
|
||||
const [phone, setPhone] = useState(phoneState ? phoneState : '');
|
||||
export default function PhoneInput({ phoneState, validateHandler, clearCondition }: PhoneInputProps) {
|
||||
const [phone, setPhone] = useState<string | undefined>(phoneState ? phoneState : '');
|
||||
|
||||
useEffect(() => {
|
||||
if (clearCondition) {
|
||||
setPhone('');
|
||||
}
|
||||
}, [clearCondition])
|
||||
|
||||
return (
|
||||
<InputMask
|
||||
|
||||
@@ -1,75 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
|
||||
type FilesInfo = {
|
||||
totalSize: number;
|
||||
totalCount: number;
|
||||
};
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function ProtectionStatistic({ fileType }: { fileType: string }) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const getFileTypeFields = (fileType: string): { totalSize: string, totalCount: string } => {
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
const getFileTypeData = (fileType: string) => {
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
return {
|
||||
totalSize: 'images_size',
|
||||
totalCount: 'images_quantity'
|
||||
};
|
||||
return filesInfo?.images;
|
||||
case 'video':
|
||||
return {
|
||||
totalSize: 'videos_size',
|
||||
totalCount: 'videos_quantity'
|
||||
};
|
||||
return filesInfo?.videos;
|
||||
case 'audio':
|
||||
return {
|
||||
totalSize: 'audios_size',
|
||||
totalCount: 'audios_quantity'
|
||||
};
|
||||
return filesInfo?.audios;
|
||||
case 'document':
|
||||
return filesInfo?.documents;
|
||||
default:
|
||||
return {
|
||||
totalSize: 'all_files_size',
|
||||
totalCount: 'all_files_quantity'
|
||||
};
|
||||
return filesInfo?.total;
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
const fields = getFileTypeFields(fileType);
|
||||
|
||||
return {
|
||||
totalSize: data[fields.totalSize as keyof typeof data],
|
||||
totalCount: data[fields.totalCount as keyof typeof data]
|
||||
}
|
||||
}
|
||||
});
|
||||
const currentData = getFileTypeData(fileType);
|
||||
|
||||
return (
|
||||
<div className="protection-statistic">
|
||||
<div className="protection-statistic-stat-card">
|
||||
<div className="protection-statistic-stat-number">0</div>
|
||||
<div className="protection-statistic-stat-number">
|
||||
{currentData?.protected ?? 0}
|
||||
</div>
|
||||
<div className="protection-statistic-stat-label">{t(`${fileType}-protected`)}</div>
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
@@ -78,7 +40,7 @@ export default function ProtectionStatistic({ fileType }: { fileType: string })
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
<div className="protection-statistic-stat-number">
|
||||
{filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0}
|
||||
{convertBytes(currentData?.size ?? 0)}
|
||||
</div>
|
||||
<div className="protection-statistic-stat-label">{t(`${fileType}-occupy`)}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import {useTranslations, useLocale} from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { useStoreWithDevtools } from '@/app/stores/useStoreWithDevtools';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
type FilesInfo = {
|
||||
totalSize: number;
|
||||
@@ -15,62 +13,25 @@ type FilesInfo = {
|
||||
|
||||
export default function ProtectionSummary({ fileType }: { fileType: string }) {
|
||||
const t = useTranslations('Global');
|
||||
const CHECKS = useStoreWithDevtools(s => s.value1);
|
||||
const VIOLATIONS = useStoreWithDevtools(s => s.value2);
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
const getFileTypeFields = (fileType: string): { totalSize: string, totalCount: string } => {
|
||||
const getFileTypeData = (fileType: string) => {
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
return {
|
||||
totalSize: 'images_size',
|
||||
totalCount: 'images_quantity'
|
||||
};
|
||||
return filesInfo?.images;
|
||||
case 'video':
|
||||
return {
|
||||
totalSize: 'videos_size',
|
||||
totalCount: 'videos_quantity'
|
||||
};
|
||||
return filesInfo?.videos;
|
||||
case 'audio':
|
||||
return {
|
||||
totalSize: 'audios_size',
|
||||
totalCount: 'audios_quantity'
|
||||
};
|
||||
return filesInfo?.audios;
|
||||
case 'document':
|
||||
return filesInfo?.documents;
|
||||
default:
|
||||
return {
|
||||
totalSize: 'all_files_size',
|
||||
totalCount: 'all_files_quantity'
|
||||
};
|
||||
return filesInfo?.total;
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
const fields = getFileTypeFields(fileType);
|
||||
|
||||
return {
|
||||
totalSize: data[fields.totalSize as keyof typeof data],
|
||||
totalCount: data[fields.totalCount as keyof typeof data]
|
||||
}
|
||||
}
|
||||
});
|
||||
const currentData = getFileTypeData(fileType);
|
||||
|
||||
const locale = useLocale();
|
||||
|
||||
@@ -95,27 +56,27 @@ export default function ProtectionSummary({ fileType }: { fileType: string }) {
|
||||
<p>{t('current-status-of')}</p>
|
||||
<div className="protection-stats">
|
||||
<div className="protection-stat total-files">
|
||||
<span className="protection-stat-value">{filesInfo?.totalCount ? filesInfo?.totalCount : 0}</span>
|
||||
<span className="protection-stat-value">{currentData?.quantity ?? 0}</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeFilesName(filesInfo?.totalCount || 0)}
|
||||
{pluralizeFilesName(currentData?.quantity ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="protection-stat total-checks">
|
||||
<div>
|
||||
<span className="protection-stat-value">{CHECKS}</span>
|
||||
<span className="protection-stat-value">{currentData?.check ?? 0}</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeCheks(CHECKS)}
|
||||
{pluralizeCheks(currentData?.check ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="protection-stat-value">{VIOLATIONS}</span>
|
||||
<span className="protection-stat-value">{currentData?.violation ?? 0}</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeViolations(VIOLATIONS)}
|
||||
{pluralizeViolations(currentData?.violation ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="protection-stat total-usage">
|
||||
<div className="protection-stat-value">{filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} / 0</div>
|
||||
<div className="protection-stat-value">{convertBytes(currentData?.size ?? 0)} / 0</div>
|
||||
<div className="protection-stat-label">
|
||||
{t('disk-space-used')}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
export function FileInfoModalWindow({ fileInfo, setWindowClose, setWindowChildren }: any) {
|
||||
|
||||
const exampleTest = {
|
||||
ownerName: 'string',
|
||||
ownerMail: 'string',
|
||||
ownerCompany: 'string',
|
||||
fileName: 'string',
|
||||
fileSize: 'number',
|
||||
fileUploadDate: 'number',
|
||||
fileFormat: 'string',
|
||||
protectStatus: 'string',
|
||||
checksCount: 'number'
|
||||
}
|
||||
|
||||
function closeHandler() {
|
||||
setWindowChildren(null);
|
||||
setWindowClose(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='file-info-modal-window'>
|
||||
|
||||
<div
|
||||
className="flex justify-between"
|
||||
>
|
||||
<h3 className="text-2xl mb-4">
|
||||
Результат проверки
|
||||
</h3>
|
||||
<button
|
||||
className="file-info-modal-window-close-button"
|
||||
onClick={closeHandler}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="file-info-modal-window-section"
|
||||
>
|
||||
|
||||
<h4
|
||||
className="mb-2"
|
||||
>
|
||||
Информация о владельце
|
||||
</h4>
|
||||
<div className="file-info-modal-window-grid" >
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Имя
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.ownerName ? fileInfo.ownerName : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Email
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.ownerEmail ? fileInfo.ownerEmail : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Компания
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.ownerCompany ? fileInfo.ownerCompany : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4
|
||||
className="mb-2"
|
||||
>
|
||||
Информация о файле
|
||||
</h4>
|
||||
<div className="file-info-modal-window-grid" >
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Имя файла
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileName ? fileInfo.fileName : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Размер
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileSize ? convertBytes(fileInfo.fileSize) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Загружен
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileUploadDate ? fileInfo.fileUploadDate : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Формат
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileExtension ? fileInfo.fileExtension : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4
|
||||
className="mb-2"
|
||||
>
|
||||
Детали защиты
|
||||
</h4>
|
||||
<div className="file-info-modal-window-grid" >
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Статус защиты
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.protectStatus ? fileInfo.protectStatus : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Количество проверок
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.checksCount ? fileInfo.checksCount : '0'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { IconDocument, IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {useTranslations, useLocale} from 'next-intl';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function MyContentInfoBlock() {
|
||||
const locale = useLocale();
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
const fileTypeSizePercents = () => {
|
||||
if (!filesInfo?.all_files_size) {
|
||||
if (!filesInfo?.total.size) {
|
||||
return {
|
||||
images: 0,
|
||||
videos: 0,
|
||||
@@ -32,13 +23,13 @@ export default function MyContentInfoBlock() {
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
images: filesInfo?.images_size || 0,
|
||||
videos: filesInfo?.videos_size || 0,
|
||||
audios: filesInfo?.audios_size || 0,
|
||||
documents: filesInfo?.documents_size || 0,
|
||||
images: filesInfo?.images.size || 0,
|
||||
videos: filesInfo?.videos.size || 0,
|
||||
audios: filesInfo?.audios.size || 0,
|
||||
documents: filesInfo?.documents.size || 0,
|
||||
};
|
||||
|
||||
const total = filesInfo?.all_files_size;
|
||||
const total = filesInfo?.total.size;
|
||||
|
||||
const rawPercents = {
|
||||
images: (sizes.images / total) * 100,
|
||||
@@ -90,10 +81,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.images_quantity || 0} {pluralizeFiles(filesInfo?.images_quantity || 0)}
|
||||
{filesInfo?.images.quantity || 0} {pluralizeFiles(filesInfo?.images.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}
|
||||
{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,10 +101,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.videos_quantity || 0} {pluralizeFiles(filesInfo?.videos_quantity || 0)}
|
||||
{filesInfo?.videos.quantity || 0} {pluralizeFiles(filesInfo?.videos.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}
|
||||
{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,10 +121,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.audios_quantity || 0} {pluralizeFiles(filesInfo?.audios_quantity || 0)}
|
||||
{filesInfo?.audios.quantity || 0} {pluralizeFiles(filesInfo?.audios.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}
|
||||
{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,10 +141,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.documents_quantity || 0} {pluralizeFiles(filesInfo?.documents_quantity || 0)}
|
||||
{filesInfo?.documents.quantity || 0} {pluralizeFiles(filesInfo?.documents.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.documents_size ? convertBytes(filesInfo?.documents_size) : 0}
|
||||
{filesInfo?.documents.size ? convertBytes(filesInfo?.documents.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { PieChartComponent } from '@/app/components/PieChartComponent';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
|
||||
type FilesInfo = {
|
||||
@@ -28,76 +27,27 @@ export default function MyContentPieChart() {
|
||||
{ name: 'documents', value: 0, color: '#a561e6' },
|
||||
]);
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number,
|
||||
images_quantity: number,
|
||||
videos_quantity: number,
|
||||
audios_quantity: number,
|
||||
documents_quantity: number,
|
||||
images_size: number,
|
||||
videos_size: number,
|
||||
audios_size: number,
|
||||
documents_size: number,
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0,
|
||||
imageQuantity: 0,
|
||||
videoQuantity: 0,
|
||||
audioQuantity: 0,
|
||||
documentQuantity: 0,
|
||||
imageSize: 0,
|
||||
videoSize: 0,
|
||||
audioSize: 0,
|
||||
documentSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize: data.all_files_size,
|
||||
totalCount: data.all_files_quantity,
|
||||
imageQuantity: data.images_quantity,
|
||||
videoQuantity: data.videos_quantity,
|
||||
audioQuantity: data.audios_quantity,
|
||||
documentQuantity: data.documents_quantity,
|
||||
imageSize: data.images_size,
|
||||
videoSize: data.videos_size,
|
||||
audioSize: data.audios_size,
|
||||
documentSize: data.documents_size,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
useEffect(() => {
|
||||
setFilesCount([
|
||||
{
|
||||
name: 'images-few',
|
||||
value: filesInfo?.imageSize ? filesInfo?.imageSize : 0,
|
||||
value: filesInfo?.images.size ? filesInfo?.images.size : 0,
|
||||
color: '#f08c00'
|
||||
},
|
||||
{
|
||||
name: 'videos',
|
||||
value: filesInfo?.videoSize ? filesInfo?.videoSize : 0,
|
||||
value: filesInfo?.videos.size ? filesInfo?.videos.size : 0,
|
||||
color: '#2f9e44'
|
||||
},
|
||||
{
|
||||
name: 'audios',
|
||||
value: filesInfo?.audioSize ? filesInfo?.audioSize : 0,
|
||||
value: filesInfo?.audios.size ? filesInfo?.audios.size : 0,
|
||||
color: '#1971c2'
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
value: filesInfo?.documentSize ? filesInfo?.documentSize : 0,
|
||||
value: filesInfo?.documents.size ? filesInfo?.documents.size : 0,
|
||||
color: '#a561e6'
|
||||
},
|
||||
])
|
||||
@@ -105,7 +55,7 @@ export default function MyContentPieChart() {
|
||||
|
||||
return (
|
||||
<div className="protection-overview">
|
||||
<PieChartComponent data={filesCount} show={filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} />
|
||||
<PieChartComponent data={filesCount} show={convertBytes(filesInfo?.total.size ?? 0)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,14 @@
|
||||
//removed
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function MyContentPageTitleColorFrame({ title, description }: { title: string, description: string }) {
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="page-title-color-frame">
|
||||
@@ -30,7 +23,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
|
||||
</div>
|
||||
<div className="header-stats">
|
||||
<div className="header-stat-item">
|
||||
<div className="header-stat-value">{filesInfo?.all_files_quantity}</div>
|
||||
<div className="header-stat-value">{filesInfo?.total.quantity}</div>
|
||||
<div className="header-stat-label">Файлов</div>
|
||||
</div>
|
||||
<div className="header-stat-divider"></div>
|
||||
@@ -40,7 +33,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
|
||||
</div>
|
||||
<div className="header-stat-divider"></div>
|
||||
<div className="header-stat-item">
|
||||
<div className="header-stat-value">{filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}</div>
|
||||
<div className="header-stat-value">{convertBytes(filesInfo?.total.size ?? 0)}</div>
|
||||
<div className="header-stat-label">Хранилище</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { IconDocument, IconShield, IconSearch, IconDiscet } from '@/app/ui/icons/icons';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function MyContentStatsOverview() {
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="my-content-stats-overview">
|
||||
<div className="stat-card files" data-icon="📄">
|
||||
<div className="stat-card files" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">📄</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconDocument />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.all_files_quantity}</div>
|
||||
<div className="stat-value">{filesInfo?.total.quantity}</div>
|
||||
<div className="stat-label">Всего файлов</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card protected" data-icon="🛡">
|
||||
<div className="stat-card protected" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">🛡</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconShield />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-value">{filesInfo?.total.protected}</div>
|
||||
<div className="stat-label">Защищено файлов</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card checks" data-icon="🔍">
|
||||
<div className="stat-card checks" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">🔍</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconSearch />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-label">Проверок защиты</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card storage" data-icon="💾">
|
||||
<div className="stat-card storage" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">💾</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconDiscet />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}</div>
|
||||
<div className="stat-value">{convertBytes(filesInfo?.total.size ?? 0)}</div>
|
||||
<div className="stat-label">Хранилище</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { usePathname } from 'next/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSideMenuStore } from '@/app/stores/sideMenuStore';
|
||||
|
||||
export default function DropDownList() {
|
||||
export default function DropDownList(
|
||||
{ referralLink }: { referralLink: string | null }
|
||||
) {
|
||||
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
||||
const [dropDownExpanded, setDropDownExpanded] = useState(false);
|
||||
const t = useTranslations('Global');
|
||||
@@ -17,16 +19,20 @@ export default function DropDownList() {
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: t('photo-marking'),
|
||||
href: '/pages/marking-photo'
|
||||
name: 'photo-marking',
|
||||
href: '/pages/marking-images'
|
||||
},
|
||||
{
|
||||
name: t('video-marking'),
|
||||
name: 'video-marking',
|
||||
href: '/pages/marking-video'
|
||||
},
|
||||
{
|
||||
name: t('audio-marking'),
|
||||
name: 'audio-marking',
|
||||
href: '/pages/marking-audio'
|
||||
},
|
||||
{
|
||||
name: 'document-marking',
|
||||
href: '/pages/marking-document'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -50,6 +56,9 @@ export default function DropDownList() {
|
||||
</div>
|
||||
<ul className={`sub-menu ${dropDownExpanded ? 'expanded' : ''}`}>
|
||||
{links.map((link) => {
|
||||
if (referralLink && link.name === 'document-marking') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={link.name}
|
||||
@@ -64,7 +73,7 @@ export default function DropDownList() {
|
||||
setMenuState(false)
|
||||
}}
|
||||
>
|
||||
<p className="">{link.name}</p>
|
||||
<p className="">{t(link.name)}</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,8 +11,70 @@ import navigation from './navigation.json';
|
||||
import { useSideMenuStore } from '@/app/stores/sideMenuStore';
|
||||
import { useRef } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { env } from 'process';
|
||||
import { getBuildData } from '@/app/actions/action';
|
||||
|
||||
export default function NavLinks() {
|
||||
const {
|
||||
data: referralLink,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => {
|
||||
return fetchReferralUserStats();
|
||||
},
|
||||
select: (data) => {
|
||||
if (data?.referralLink) {
|
||||
return data?.referralLink;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: frontendBuildDate,
|
||||
} = useQuery({
|
||||
queryKey: ['frontendBuildDate'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch('/build-info.json');
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
select: (data) => {
|
||||
if (data?.buildTime) {
|
||||
return data.buildTime;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
|
||||
const {
|
||||
data: backendBuildDate,
|
||||
} = useQuery({
|
||||
queryKey: ['backendBuildDate'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await getBuildData();
|
||||
return response.buildTimeBack ?? null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
|
||||
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
||||
const t = useTranslations('Global');
|
||||
|
||||
@@ -66,9 +128,13 @@ export default function NavLinks() {
|
||||
<p className="">{t('home')}</p>
|
||||
</Link>
|
||||
|
||||
<DropDownList />
|
||||
<DropDownList referralLink={referralLink} />
|
||||
|
||||
{links.map((link) => {
|
||||
if (!referralLink && link.name === 'referral-program') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.name}
|
||||
@@ -108,6 +174,19 @@ export default function NavLinks() {
|
||||
<p className="">{t('exit')}</p>
|
||||
</Link>
|
||||
</ul>
|
||||
|
||||
<div className="build-versions">
|
||||
{frontendBuildDate && (
|
||||
<div className="">
|
||||
front: {frontendBuildDate}
|
||||
</div>
|
||||
)}
|
||||
{backendBuildDate && (
|
||||
<div className="">
|
||||
back: {backendBuildDate}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
"img": "M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.5 1.5 0 0 0 18.54 8H16c-.8 0-1.54.37-2 1l-3 4v2h2l2.54-3.82L16.5 18H20z"
|
||||
},
|
||||
{
|
||||
"name": "violations",
|
||||
"name": "violations-few",
|
||||
"href": "/pages/violations",
|
||||
"img": "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"href": "/pages/emptypage",
|
||||
"href": "/pages/search",
|
||||
"img": "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,122 +1,184 @@
|
||||
'use client'
|
||||
|
||||
import { fetchTariffs } from '@/app/actions/action';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { yooKasaCreatePayment } from '@/app/actions/yooKassa';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import ModalWindow from '@/app/components/ModalWindow';
|
||||
|
||||
export default function PaymentTabTariffs() {
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="billing-toggle">
|
||||
<div>
|
||||
<button className="billing-btn active">💳 Помесячно</button>
|
||||
<button className="billing-btn">
|
||||
🔥 Годовая
|
||||
<span>ВЫГОДНО</span>
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: userData,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['tariffData'],
|
||||
queryFn: () => {
|
||||
return fetchTariffs();
|
||||
}
|
||||
});
|
||||
|
||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||
const [selectedTariff, setSelectedTariff] = useState<{
|
||||
value: number;
|
||||
tariff: string;
|
||||
tokens: number;
|
||||
} | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
async function createPayment(value: number, tariff: string) {
|
||||
if (isProcessing) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
const response = await yooKasaCreatePayment(value, tariff);
|
||||
console.log(response);
|
||||
if (response) {
|
||||
window.open(response, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openPaymentWindow(value: number, tariff: string, tokens: number) {
|
||||
setSelectedTariff({ value, tariff, tokens });
|
||||
setIsProcessing(false);
|
||||
setOpenWindow(true);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
setOpenWindow(false);
|
||||
setSelectedTariff(null);
|
||||
setIsProcessing(false);
|
||||
}
|
||||
|
||||
function PaymentConfirmationModal() {
|
||||
if (!selectedTariff) return null;
|
||||
|
||||
const handleConfirm = () => {
|
||||
createPayment(selectedTariff.value, selectedTariff.tariff);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-window-confirm-payment">
|
||||
<h3 className="modal-window-confirm-payment-title">
|
||||
Подтверждение оплаты
|
||||
</h3>
|
||||
<h4 className="modal-window-confirm-payment-tariff">
|
||||
Подписка: {selectedTariff.tariff}
|
||||
</h4>
|
||||
<div className="modal-window-confirm-payment-description">
|
||||
Месячная подписка будет автоматически продлеваться
|
||||
<br />
|
||||
<strong>Включено {selectedTariff.tokens} токенов!</strong>
|
||||
</div>
|
||||
|
||||
<div className="modal-window-confirm-payment-price">
|
||||
<div>{selectedTariff.value}₽</div>
|
||||
<div>в месяц</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
className="btn-primary btn-cancel"
|
||||
onClick={handleCancel}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{t('no')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn-primary btn-confirm ${isProcessing ? 'btn-loading' : ''}`}
|
||||
onClick={handleConfirm}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Обработка...' : t('yes')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="subscription-grid">
|
||||
<div className="plan-card animate-card">
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">СТАРТ</h3>
|
||||
<div className="plan-price">900₽</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">100 токенов включено</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalWindow state={openWindow} callBack={setOpenWindow}>
|
||||
<PaymentConfirmationModal />
|
||||
</ModalWindow>
|
||||
|
||||
<div className="tab-content">
|
||||
<div className="billing-toggle" style={{ display: 'none' }}>
|
||||
<div>
|
||||
<button className="billing-btn active">💳 Помесячно</button>
|
||||
<button className="billing-btn">
|
||||
🔥 Годовая
|
||||
<span>ВЫГОДНО</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
100 файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
1 ГБ хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
До 10 МБ на файл
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="plan-card animate-card">
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">ПРЕМИУМ</h3>
|
||||
<div className="plan-price">3500₽</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">100 токенов включено</div>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
100 файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
1 ГБ хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
До 10 МБ на файл
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="plan-card animate-card">
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">ПРОФИ</h3>
|
||||
<div className="plan-price">9500₽</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">100 токенов включено</div>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
100 файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
1 ГБ хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
До 10 МБ на файл
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
<div className="subscription-grid">
|
||||
{userData?.map((item: {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
tokens: number;
|
||||
maxFilesCount: number;
|
||||
diskSize: number;
|
||||
}) => (
|
||||
<div className="plan-card animate-card" key={item.id}>
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">
|
||||
{t.has(item.name) ? t(item.name) : item.name}
|
||||
</h3>
|
||||
<div className="plan-price">{item.price}</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">{item.tokens} токенов включено</div>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
{item.maxFilesCount} файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
{item.diskSize ? convertBytes(item.diskSize) : 0} хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => openPaymentWindow(item.price, item.name, item.tokens)}
|
||||
>
|
||||
Выбрать план
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import { referralRefill } from '@/app/actions/referralsActions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function SecurePayments() {
|
||||
|
||||
return (
|
||||
<div className="secure-payments-wrapper">
|
||||
<h3>
|
||||
🔒 Безопасные платежи
|
||||
Безопасные платежи
|
||||
</h3>
|
||||
<div
|
||||
className='flex gap-4'
|
||||
>
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
onClick={async () => {
|
||||
const response = await referralRefill();
|
||||
console.log(response);
|
||||
if (response?.account === 'refill') {
|
||||
toast.success('refill');
|
||||
} else {
|
||||
toast.error(`error refill: ${response?.error?.message_desc}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
referralRefill
|
||||
</button>
|
||||
</div>
|
||||
<div className="secure-payments-grid">
|
||||
<div className="secure-payments-card">
|
||||
<div className="secure-payments-card-header">
|
||||
|
||||
@@ -1,38 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { getProcents } from '@/app/lib/getProcents';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function UserUsage() {
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
return (
|
||||
<div className="usage-stats">
|
||||
<h3>
|
||||
📊 Ваше использование
|
||||
Ваше использование
|
||||
</h3>
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">0 / 2,000</div>
|
||||
<div className="stat-value">
|
||||
{userData?.tariffInfo?.currentFileCounts} / {userData?.tariffInfo?.maxFileCounts}
|
||||
</div>
|
||||
<div className="stat-label">Файлов в месяц</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: '50%' }}
|
||||
style={{ width: `${getProcents(userData?.tariffInfo?.currentFileCounts, userData?.tariffInfo?.maxFileCounts)}%` }}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">0 / 20 ГБ</div>
|
||||
<div className="stat-value">
|
||||
{convertBytes(userData?.tariffInfo?.currentFileOnDisk ?? 0)} / {convertBytes(userData?.tariffInfo?.maxFileOnDisk ?? 0)}
|
||||
</div>
|
||||
<div className="stat-label">Объём хранилища</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: '50%' }}
|
||||
style={
|
||||
{ width: `${getProcents(userData?.tariffInfo?.currentFileOnDisk, userData?.tariffInfo?.maxFileOnDisk ?? 0)}%` }
|
||||
}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-value">
|
||||
{userData?.tariffInfo?.tokens ?? 0}
|
||||
</div>
|
||||
<div className="stat-label">Токенов на балансе</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">ПРОФИ</div>
|
||||
<div className="stat-value">
|
||||
{userData?.tariffInfo?.tariffName ? userData?.tariffInfo?.tariffName : 'ERROR'}
|
||||
</div>
|
||||
<div className="stat-label">Текущий план</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="earning-card success">
|
||||
<h4>💰 Доступно к выводу</h4>
|
||||
<p>
|
||||
135.00 Б
|
||||
0
|
||||
</p>
|
||||
<small>Минимальная сумма вывода: 100 Б</small>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="earning-card">
|
||||
<h4>📈 Всего заработано</h4>
|
||||
<p>
|
||||
135.00 Б
|
||||
0
|
||||
</p>
|
||||
<small>За все время участия в программе</small>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="earning-card warning">
|
||||
<h4>✅ Выплачено</h4>
|
||||
<p>
|
||||
0.00 Б
|
||||
0
|
||||
</p>
|
||||
<small>Успешно выплачено на ваши счета</small>
|
||||
</div>
|
||||
@@ -70,8 +70,8 @@ export default function IncomeAndPayments() {
|
||||
Обработано
|
||||
</div>
|
||||
|
||||
<div className="stats-item first-column second-row">
|
||||
100.00
|
||||
<div className="stats-item first-column left-bottom-corner">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
@@ -82,39 +82,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row last-column">
|
||||
0
|
||||
</div>
|
||||
|
||||
<div className="stats-item first-column">
|
||||
100.00
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-column">
|
||||
0
|
||||
</div>
|
||||
|
||||
<div className="stats-item last-row left-bottom-corner first-column">
|
||||
100.00
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row last-column right-bottom-corner">
|
||||
<div className="stats-item last-column right-bottom-corner">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchReferralsLevels, fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
|
||||
interface ReferralLevel {
|
||||
name: string;
|
||||
id: string;
|
||||
next: string | null;
|
||||
share: number;
|
||||
maxInvitee: number;
|
||||
minInvitee: number;
|
||||
}
|
||||
|
||||
interface ReferralsData {
|
||||
referrals: ReferralLevel[];
|
||||
}
|
||||
|
||||
export default function CommissionLevels() {
|
||||
|
||||
const {
|
||||
data: referralsLevels,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery<ReferralsData, Error, ReferralLevel[]>({
|
||||
queryKey: ['referralsLevels'],
|
||||
queryFn: () => fetchReferralsLevels(),
|
||||
select: (data) => {
|
||||
if (data?.referrals) {
|
||||
const itemsMap = new Map<string, ReferralLevel>();
|
||||
data.referrals.forEach(item => {
|
||||
itemsMap.set(item.id, item);
|
||||
});
|
||||
|
||||
const referencedIds = new Set(
|
||||
data.referrals
|
||||
.map(item => item.next)
|
||||
.filter((next): next is string => next !== null)
|
||||
);
|
||||
|
||||
const firstItem = data.referrals.find(item => !referencedIds.has(item.id));
|
||||
|
||||
if (!firstItem) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sortedReferrals: ReferralLevel[] = [];
|
||||
let currentItem = firstItem;
|
||||
|
||||
while (currentItem) {
|
||||
sortedReferrals.push(currentItem);
|
||||
|
||||
if (currentItem.next === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentItem = itemsMap.get(currentItem.next)!;
|
||||
|
||||
if (sortedReferrals.includes(currentItem)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sortedReferrals;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: activeInvites,
|
||||
error: activeInvitesError
|
||||
} = useQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => {
|
||||
return fetchReferralUserStats();
|
||||
},
|
||||
select: (data) => {
|
||||
if (data?.activeInvitee) {
|
||||
return data.activeInvitee
|
||||
} else {
|
||||
return 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (!referralsLevels?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getReferralIcon(referral: string) {
|
||||
switch (referral) {
|
||||
case 'bronze':
|
||||
return '🥉'
|
||||
case 'silver':
|
||||
return '🥈'
|
||||
case 'gold':
|
||||
return '🥇'
|
||||
case 'platinum':
|
||||
return '💎'
|
||||
default:
|
||||
return '🥉'
|
||||
}
|
||||
}
|
||||
|
||||
function isActiveReferralLevel(activeInvites: number, maxInvites: number, minInvites: number) {
|
||||
if (activeInvites <= maxInvites && activeInvites >= minInvites) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<h3 className="section-title">
|
||||
@@ -6,32 +120,28 @@ export default function CommissionLevels() {
|
||||
Уровни комиссии
|
||||
</h3>
|
||||
<div className="commission-levels">
|
||||
<div className="level-card active">
|
||||
<div className="level-number">🥉</div>
|
||||
<div className="level-commission">15%</div>
|
||||
<div className="level-subscribe">BRONZE</div>
|
||||
<div className="level-requirement">0-5 рефералов</div>
|
||||
</div>
|
||||
<div className="level-card ">
|
||||
<div className="level-number">🥈</div>
|
||||
<div className="level-commission">18%</div>
|
||||
<div className="level-subscribe">SILVER</div>
|
||||
<div className="level-requirement">6-15 рефералов</div>
|
||||
</div>
|
||||
|
||||
<div className="level-card ">
|
||||
<div className="level-number">🥇</div>
|
||||
<div className="level-commission">22%</div>
|
||||
<div className="level-subscribe">GOLD</div>
|
||||
<div className="level-requirement">16-50 рефералов</div>
|
||||
</div>
|
||||
|
||||
<div className="level-card ">
|
||||
<div className="level-number">💎</div>
|
||||
<div className="level-commission">25%</div>
|
||||
<div className="level-subscribe">PLATINUM</div>
|
||||
<div className="level-requirement">50+ рефералов</div>
|
||||
</div>
|
||||
{
|
||||
referralsLevels.map((item: ReferralLevel) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`level-card ${isActiveReferralLevel(activeInvites, item.maxInvitee, item.minInvitee) ? 'active' : ''}`
|
||||
}
|
||||
key={item.id}
|
||||
>
|
||||
<div className="level-number">
|
||||
{getReferralIcon(item.id)}
|
||||
</div>
|
||||
<div className="level-commission">{item.share}%</div>
|
||||
<div className="level-subscribe">{item.name}</div>
|
||||
<div className="level-requirement">{
|
||||
item.maxInvitee < 100 ? `${item.minInvitee}-${item.maxInvitee}` : `${item.minInvitee}+`
|
||||
} рефералов
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,77 +1,484 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getPaginationRowModel,
|
||||
getFilteredRowModel,
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
} from '@tanstack/react-table';
|
||||
import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } from '@/app/ui/icons/icons';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import DropDownList from '@/app/components/DropDownList';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { fetchReferralInvitees } from '@/app/actions/referralsActions';
|
||||
|
||||
type InviteesType = {
|
||||
status?: string;
|
||||
email?: string;
|
||||
registrationData: string;
|
||||
};
|
||||
|
||||
type InviteType = {
|
||||
active?: string;
|
||||
email?: string;
|
||||
regDate: string;
|
||||
};
|
||||
|
||||
export default function InvitationsTable() {
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: referralInvitees,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<InviteType[], Error, InviteesType[], ['referralInvitees']>({
|
||||
queryKey: ['referralInvitees'],
|
||||
queryFn: () => {
|
||||
return fetchReferralInvitees();
|
||||
},
|
||||
|
||||
select: (data: InviteType[]): InviteesType[] => {
|
||||
if (!data) return [
|
||||
];
|
||||
|
||||
return data.map((item: InviteType) => {
|
||||
console.log(item);
|
||||
return {
|
||||
status: item?.active ? 'active' : 'not-active',
|
||||
email: item?.email,
|
||||
registrationData: item?.regDate
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Состояния
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [dateFilter, setDateFilter] = useState<string>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const t = useTranslations("Global");
|
||||
const locale = useLocale();
|
||||
|
||||
// Определение колонок
|
||||
const columns = useMemo<ColumnDef<InviteesType>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('email')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center table-item">
|
||||
{/* {row.original.size !== undefined ? convertBytes(row.original.size) : '-'} */}
|
||||
{row.original.email}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('status')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center font-semibold table-item">
|
||||
<span className="table-item-status">
|
||||
{row.original.status}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'registrationData',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('registration-date')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center table-item">
|
||||
{row.original.registrationData?.split('T')[0]}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableColumnFilter: false,
|
||||
}
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Фильтрация по типу файла и дате
|
||||
const filteredData = useMemo(() => {
|
||||
let result = referralInvitees;
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Фильтр по типу файла
|
||||
if (statusFilter !== 'all') {
|
||||
result = result.filter(item => item.status === statusFilter);
|
||||
}
|
||||
|
||||
// Фильтр по дате
|
||||
/* 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 => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData >= todayStart
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'week':
|
||||
const weekAgo = now - sevenDays;
|
||||
result = result.filter(item => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData >= weekAgo;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'month':
|
||||
const monthAgo = now - thirtyDays;
|
||||
result = result.filter(item => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData >= monthAgo;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'older':
|
||||
const monthAgo2 = now - thirtyDays;
|
||||
result = result.filter(item => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData < monthAgo2;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
} */
|
||||
|
||||
return result;
|
||||
}, [referralInvitees, statusFilter, dateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPageRows = table.getRowModel().rows;
|
||||
const pageCount = table.getPageCount();
|
||||
|
||||
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
|
||||
table.setPageIndex(pagination.pageIndex - 1);
|
||||
}
|
||||
}, [filteredData, pagination.pageIndex]);
|
||||
|
||||
// Создание таблицы
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
pagination
|
||||
},
|
||||
autoResetPageIndex: false,
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pluralizeFiles = (number: number) => {
|
||||
const translate = [t('file'), t('files-few'), t('files')];
|
||||
return pluralize(number, translate[0], translate[1], translate[2], locale);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<h2 className="section-title">
|
||||
История приглашений
|
||||
</h2>
|
||||
<div
|
||||
className="block-wrapper"
|
||||
>
|
||||
<div className="tanstak-table-wrapper">
|
||||
<h3
|
||||
className="tanstak-table-title"
|
||||
>
|
||||
История приглашений
|
||||
</h3>
|
||||
{/* Фильтры */}
|
||||
<div className="tanstak-table-filtres" style={{ display: "none" }}>
|
||||
<div className="table-filtres-wrapper">
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{t('date-filter')}:</div>
|
||||
<DropDownList
|
||||
value={dateFilter}
|
||||
translatedValue={(() => {
|
||||
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>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stats-item left-top-corner stats-header">
|
||||
Email
|
||||
</div>
|
||||
<div className="stats-item stats-header">
|
||||
Статус
|
||||
</div>
|
||||
<div className="stats-item stats-header">
|
||||
Заработано
|
||||
</div>
|
||||
<div className="stats-item stats-header">
|
||||
Дата приглашения
|
||||
</div>
|
||||
<div className="stats-item last-column right-top-corner stats-header">
|
||||
Регистрация
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{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="stats-item first-column second-row">
|
||||
email@mail.com
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
{t('status')}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row last-column">
|
||||
0
|
||||
{/* Таблица */}
|
||||
<div className="tanstak-table-block">
|
||||
<table className="tanstak-table">
|
||||
<thead className="tanstak-table-head">
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: typeof header.column.columnDef.header === 'function'
|
||||
? header.column.columnDef.header(header.getContext())
|
||||
: header.column.columnDef.header as string}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="tanstak-table-body">
|
||||
{table.getRowModel().rows.length > 0 ? (
|
||||
table.getRowModel().rows.map(row => (
|
||||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>
|
||||
{typeof cell.column.columnDef.cell === 'function'
|
||||
? cell.column.columnDef.cell(cell.getContext())
|
||||
: cell.getValue() as string}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center">
|
||||
{t('no-data-for-selected-filters')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="stats-item first-column">
|
||||
email@mail.com
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
{t('status')}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-column">
|
||||
0
|
||||
</div>
|
||||
{/* Пагинация */}
|
||||
<div className="tanstak-table-pagination">
|
||||
<div className="pagination-info">
|
||||
{/* <span className="pagination-info-pages">
|
||||
{t('page')}{' '}
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="pagination-info-files">
|
||||
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
|
||||
</span> */}
|
||||
</div>
|
||||
|
||||
<div className="stats-item last-row left-bottom-corner first-column">
|
||||
email@mail.com
|
||||
<div className="pagination-controls">
|
||||
{table.getCanPreviousPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.firstPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconDoubleArrowLeft />
|
||||
</button>
|
||||
)}
|
||||
{table.getCanPreviousPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="pagination-controls-pages">
|
||||
{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={`${table.getState().pagination.pageIndex === pageIndex
|
||||
? 'current'
|
||||
: 'other'
|
||||
}`}
|
||||
onClick={() => table.setPageIndex(pageIndex)}
|
||||
>
|
||||
{pageIndex + 1}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{table.getCanNextPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconArrowRight />
|
||||
</button>
|
||||
)}
|
||||
{table.getCanNextPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.lastPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconDoubleArrowRight />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
{t('status')}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row last-column right-bottom-corner">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function ReferralLinkSection() {
|
||||
const {
|
||||
data: referralUserStats,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => {
|
||||
return fetchReferralUserStats();
|
||||
},
|
||||
select: (data) => {
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations('Global')
|
||||
const [referralUrl, setReferralUrl] = useState<string>('');
|
||||
const refLink = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setReferralUrl(`${window.location.origin}/register?ref=${referralUserStats?.referralLink}`);
|
||||
}
|
||||
}, [referralUserStats]);
|
||||
|
||||
const copyHandler = async () => {
|
||||
if (!refLink.current?.textContent) return;
|
||||
|
||||
const textToCopy = refLink.current.textContent;
|
||||
|
||||
if (typeof window === 'undefined' || !navigator.clipboard) {
|
||||
console.warn('Clipboard API не доступен');
|
||||
|
||||
const success = fallbackCopy(textToCopy);
|
||||
if (success) {
|
||||
toast.success(`${t('copied')}: ${textToCopy}`);
|
||||
} else {
|
||||
toast.error('Не удалось скопировать');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
toast.success(`${t('copied')}: ${textToCopy}`);
|
||||
} catch (err) {
|
||||
console.error('Ошибка копирования:', err);
|
||||
|
||||
const success = fallbackCopy(textToCopy);
|
||||
if (success) {
|
||||
toast.success(`${t('copied')}: ${textToCopy}`);
|
||||
} else {
|
||||
toast.error('Не удалось скопировать');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackCopy = (text: string) => {
|
||||
try {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
|
||||
textArea.select();
|
||||
textArea.setSelectionRange(0, 99999);
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
return successful;
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (!referralUserStats?.referralLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="referral-link-section">
|
||||
<h3>🔗 Ваша реферальная ссылка</h3>
|
||||
<h3>Ваша реферальная ссылка</h3>
|
||||
<p>Поделитесь этой ссылкой с друзьями и получайте комиссию с каждой их покупки</p>
|
||||
<div className="referral-link-input">
|
||||
<span id="referral-link" >
|
||||
https://dashboard.no-copy.ru/register.php?ref=REFA0406AC8
|
||||
<span
|
||||
id="referral-link"
|
||||
ref={refLink}
|
||||
>
|
||||
{referralUrl}
|
||||
</span >
|
||||
<button className="copy-btn">📋 Копировать</button>
|
||||
<button
|
||||
className="copy-btn"
|
||||
onClick={copyHandler}
|
||||
>
|
||||
Копировать
|
||||
</button>
|
||||
</div>
|
||||
<p className="referral-link-footer">
|
||||
Код: <strong>REFA0406AC8</strong> •
|
||||
Кликов: <strong>21</strong> •
|
||||
Комиссия: <strong>15%</strong>
|
||||
Код: <strong>{referralUserStats?.referralLink}</strong> •
|
||||
Приглашений: <strong>
|
||||
{referralUserStats?.totalInvitee}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function SettingReferralProgram() {
|
||||
</div>
|
||||
|
||||
<form className="settings-form">
|
||||
<div>
|
||||
<div className="settings-form-wrapper">
|
||||
<h4>💳 Настройки выплат</h4>
|
||||
|
||||
<div className="form-group">
|
||||
@@ -32,7 +32,7 @@ export default function SettingReferralProgram() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="settings-form-wrapper">
|
||||
<h4>🔔 Уведомления</h4>
|
||||
|
||||
<div className="checkbox-group">
|
||||
@@ -40,11 +40,6 @@ export default function SettingReferralProgram() {
|
||||
<label>Email уведомления о новых рефералах</label>
|
||||
</div>
|
||||
|
||||
<div className="checkbox-group">
|
||||
<input type="checkbox" name="sms_notifications" defaultValue="1" />
|
||||
<label>SMS уведомления о выплатах</label>
|
||||
</div>
|
||||
|
||||
<div className="checkbox-group">
|
||||
<input type="checkbox" name="marketing_emails" defaultValue="1" />
|
||||
<label>Маркетинговые рассылки о программе</label>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function ReportsInfo() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-files-info">
|
||||
<div className="type-card images">
|
||||
<div className="stat-type">
|
||||
{t('all-content')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0}</div>
|
||||
</div>
|
||||
|
||||
<div className="type-card videos">
|
||||
<div className="stat-type">
|
||||
{t('violations-few')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div>
|
||||
</div>
|
||||
|
||||
<div className="type-card audio">
|
||||
<div className="stat-type">
|
||||
{t('monitoring')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.check ? filesInfo?.total.check : 0}</div>
|
||||
</div>
|
||||
|
||||
<div className="type-card scripts">
|
||||
<div className="stat-type">
|
||||
{t('efficiency')}
|
||||
</div>
|
||||
<div className="stat-value">100%</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { SearchedUserFilesList } from '@/app/ui/search/searched-user-files-list';
|
||||
import { SearchedGlobalFilesList } from '@/app/ui/search/searched-global-files-list';
|
||||
import { useCallback, useState, useImperativeHandle } from 'react';
|
||||
import { searchUserFiles, searchGlobalFiles } from '@/app/actions/searchActions';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Pagination } from '@/app/components/Pagination';
|
||||
|
||||
export interface SearchItem {
|
||||
url: string,
|
||||
pageTitle: string,
|
||||
height: number,
|
||||
width: number,
|
||||
host: string,
|
||||
pageUrl: string
|
||||
}
|
||||
|
||||
export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: any }) {
|
||||
const [searchedUserFiles, setSearchedUserFiles] = useState<string[]>([]);
|
||||
const [searchedUserFilesShowNull, setSearchedUserFilesShowNull] = useState<boolean>(false);
|
||||
const [searchedGlobalFiles, setSearchedGlobalFiles] = useState<SearchItem[]>([]);
|
||||
const [searchedGlobalFilesShowNull, setSearchedGlobalFilesShowNull] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [paginationLoading, setPaginationLoading] = useState<boolean>(false);
|
||||
const [showGlobalSearch, setShowGlobalSearch] = useState<boolean>(false);
|
||||
const queryClient = useQueryClient();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [searchTotalPages, setSearchTotalPages] = useState<number>(0);
|
||||
const [searchCurrentPages, setSearchCurrentPages] = useState<number>(1);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
clearList: () => {
|
||||
setSearchedUserFiles([]);
|
||||
setSearchedGlobalFiles([]);
|
||||
setSearchedUserFilesShowNull(false);
|
||||
setSearchedGlobalFilesShowNull(false);
|
||||
setShowGlobalSearch(false);
|
||||
setSearchCurrentPages(1);
|
||||
setSearchTotalPages(0);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
const handlerSearchUserFile = useCallback(async (fileId: string, page: number): Promise<void> => {
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
let result = await searchUserFiles(fileId);
|
||||
|
||||
if (result.content.length) {
|
||||
setSearchedUserFiles(result.content);
|
||||
setShowGlobalSearch(true);
|
||||
handlerSearchGlobalFile(fileId, page)
|
||||
} else {
|
||||
setSearchedUserFilesShowNull(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage('error');
|
||||
}
|
||||
|
||||
}, [fileId])
|
||||
|
||||
const handlerSearchGlobalFile = useCallback(async (fileId: string, page: number, actionFromPagination?: boolean): Promise<void> => {
|
||||
if (!actionFromPagination) {
|
||||
setLoading(true);
|
||||
setSearchedGlobalFilesShowNull(false);
|
||||
setSearchedGlobalFiles([]);
|
||||
} else {
|
||||
setPaginationLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
let result = await searchGlobalFiles(fileId, page);
|
||||
console.log('searchGlobalFiles');
|
||||
console.log(result);
|
||||
if (result.images.length) {
|
||||
setSearchedGlobalFiles(result.images);
|
||||
setSearchTotalPages(result.totalPages);
|
||||
setSearchCurrentPages(page);
|
||||
|
||||
} else {
|
||||
setSearchedGlobalFilesShowNull(true);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
await queryClient.invalidateQueries({ queryKey: ['userData'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userSearchData'] });
|
||||
setLoading(false);
|
||||
setPaginationLoading(false);
|
||||
}
|
||||
|
||||
}, [fileId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="mb-4"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary btn-search"
|
||||
disabled={fileId ? false : true}
|
||||
onClick={() => {
|
||||
if (fileId) {
|
||||
handlerSearchUserFile(fileId, 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Начать поиск
|
||||
</button>
|
||||
|
||||
{errorMessage && (
|
||||
<div className='mt-2 text-red-600 text-sm'>
|
||||
Ошибка поиска
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(searchedUserFiles.length !== 0) && (
|
||||
<SearchedUserFilesList list={searchedUserFiles} />
|
||||
)}
|
||||
|
||||
{searchedUserFilesShowNull && (
|
||||
<div
|
||||
className="user-file-search-results"
|
||||
>
|
||||
<div className="results-header">
|
||||
<div className="results-title">Результаты поиска</div>
|
||||
<div className="results-count">
|
||||
Найдено файлов: {searchedUserFiles.length}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="empty-results"
|
||||
>
|
||||
<h3>Похожих файлов не найдено</h3>
|
||||
<p>В вашей библиотеке нет файлов, похожих на загруженный</p>
|
||||
<div className="empty-results-hint">
|
||||
<strong>Совет:</strong> Убедитесь, что вы загружали этот файл ранее через страницу "Маркировка"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showGlobalSearch && (
|
||||
<div
|
||||
className="global-search-section"
|
||||
>
|
||||
<div className="global-search-header">
|
||||
<div className="global-search-title">
|
||||
Глобальный поиск изображений
|
||||
</div>
|
||||
<div className="">
|
||||
Найти где ещё используется ваше изображение в интернете
|
||||
</div>
|
||||
<div className="global-search-badge">
|
||||
Интернет поиск
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="global-search-content">
|
||||
<div className="global-search-info">
|
||||
<div className="global-search-info-title">
|
||||
Поиск по всему интернету
|
||||
</div>
|
||||
<div className="global-search-info-text">
|
||||
Используем продвинутые технологии обратного поиска по изображению для поиска копий вашего контента
|
||||
на сайтах, в социальных сетях и других источниках. Поможет обнаружить несанкционированное использование.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="search-counter" id="search-counter">
|
||||
<div className="counter-info">
|
||||
<div className="counter-icon" id="counter-icon"></div>
|
||||
<div className="counter-text">
|
||||
<div className="counter-label">Глобальных поисков сегодня</div>
|
||||
<div className="counter-value" id="counter-value">0 из 10</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
id="progress-fill"
|
||||
style={{ width: '0%' }}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`global-loading ${loading ? 'show' : ''}`} id="global-loading">
|
||||
<div className="global-spinner"></div>
|
||||
<div className="global-loading-text" id="global-loading-text">
|
||||
Поиск изображений в интернете...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`global-result ${searchedGlobalFiles.length ? 'show' : ''}`} id="global-result">
|
||||
<div className="global-result-card">
|
||||
<div className="global-result-header">
|
||||
<span>Найдено в интернете</span>
|
||||
{paginationLoading && (
|
||||
<div className="loading-animation">
|
||||
<span className="global-spinner"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="global-result-content">
|
||||
<SearchedGlobalFilesList list={searchedGlobalFiles} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{searchedGlobalFilesShowNull && (
|
||||
<div className="global-no-result" id="global-no-result">
|
||||
<div className="global-no-result-icon">‍</div>
|
||||
<h4>Похожие изображения в интернете не найдены</h4>
|
||||
<p>Это может означать, что ваше изображение уникально и не используется на других сайтах.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="global-error" id="global-error">
|
||||
<strong>Ошибка глобального поиска:</strong>
|
||||
<span id="global-error-text"></span>
|
||||
</div>
|
||||
|
||||
<Pagination totalPages={searchTotalPages} currentPage={searchCurrentPages} fileId={fileId} callBack={handlerSearchGlobalFile} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import { IconImageFile, IconDocument, IconAudioFile, IconVideoFile } from '@/app/ui/icons/icons';
|
||||
import { useUserSearchData } from '@/app/hooks/react-query/useUserSearchData';
|
||||
|
||||
export function RecentSearches() {
|
||||
const { data: userSearchData, isLoading, isError, error, } = useUserSearchData()
|
||||
|
||||
return (
|
||||
<div className="recent-searches">
|
||||
<div className="recent-title">
|
||||
Недавние поиски
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type document">
|
||||
<IconDocument /> document</span>
|
||||
<div>
|
||||
<div>{userSearchData?.document?.count ? userSearchData?.document?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type image">
|
||||
<IconImageFile /> image</span>
|
||||
<div>
|
||||
<div>{userSearchData?.image?.count ? userSearchData?.image?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type audio">
|
||||
<IconAudioFile /> audio</span>
|
||||
<div>
|
||||
<div>{userSearchData?.audio?.count ? userSearchData?.audio?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type video">
|
||||
<IconVideoFile /> video</span>
|
||||
<div>
|
||||
<div>{userSearchData?.video?.count ? userSearchData?.video?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { useUserSearchData } from '@/app/hooks/react-query/useUserSearchData';
|
||||
|
||||
export function SearchStats() {
|
||||
const { data: userSearchData, isLoading, isError, error, } = useUserSearchData()
|
||||
|
||||
return (
|
||||
<div className="stats-search">
|
||||
<div className="stats-title">
|
||||
Статистика поиска
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Изображения</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.image?.count ? userSearchData?.image?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Видео</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.video?.count ? userSearchData?.video?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Аудио</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.audio?.count ? userSearchData?.audio?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Документы</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.document?.count ? userSearchData?.document?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Всего поисков</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.allCountForPeriod ? userSearchData?.allCountForPeriod : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { SearchItem } from './file-search-panel'
|
||||
|
||||
export function SearchedGlobalFilesList({ list }: { list: SearchItem[] }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.map(
|
||||
(
|
||||
item: SearchItem, index: number
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className="global-result-item"
|
||||
key={index}
|
||||
>
|
||||
<div className="global-result-image">
|
||||
<img src={item?.url} alt={item?.pageTitle} />
|
||||
</div>
|
||||
<div className="global-result-info">
|
||||
<div className="global-result-title">
|
||||
{item?.pageTitle}
|
||||
</div>
|
||||
<div className="global-result-meta">
|
||||
{item?.height & item?.width && (
|
||||
<div className="global-result-size">
|
||||
{item?.height}x{item?.width}
|
||||
</div>
|
||||
)}
|
||||
<div className="global-result-host">
|
||||
{item?.host}
|
||||
</div>
|
||||
</div>
|
||||
<div className="global-result-passage">
|
||||
{item?.pageTitle}
|
||||
</div>
|
||||
<a href={item?.pageUrl} target="_blank" className="global-result-url" title={item?.pageUrl}>
|
||||
{item?.pageUrl}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { toast } from 'sonner';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import ModalWindow from '@/app/components/ModalWindow';
|
||||
import { IconEye, IconDownload } from '@/app/ui/icons/icons';
|
||||
|
||||
export function SearchedUserFilesList({ list }: { list: any }) {
|
||||
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const handlerDownload = async (fileId: string, fileName: string) => {
|
||||
setIsFileLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/download/${fileId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`error: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName || `file-${fileId}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success(`${t('file-is-downloading')} - ${fileName}`);
|
||||
} catch (error) {
|
||||
toast.error(t('failed-to-download-file'))
|
||||
} finally {
|
||||
setIsFileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerViewfile = async (e: any) => {
|
||||
setOpenWindowChildren(() => {
|
||||
return (
|
||||
<div
|
||||
className="modal-window-view-file"
|
||||
>
|
||||
<div className="modal-window-view-file-header">
|
||||
<div
|
||||
className="modal-window-view-file-title"
|
||||
>
|
||||
{e.originalFileName}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-window-view-file-content"
|
||||
>
|
||||
<div className="modal-window-view-file-content-wrapper">
|
||||
<div className="image-section">
|
||||
<div className="image-container">
|
||||
<img
|
||||
src={e.url ? e.url : '#'}
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="file-info-card"
|
||||
>
|
||||
<div className="info-header">
|
||||
<h4>Информация о файле</h4>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">ID в системе:</span>
|
||||
<span className="info-value">
|
||||
{e.supportId ? e.supportId : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Дата загрузки:</span>
|
||||
<span className="info-value">
|
||||
{e.uploadDate ? e.uploadDate : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Размер файла:</span>
|
||||
<span className="info-value">
|
||||
{convertBytes(e.fileSize)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Статус защиты:</span>
|
||||
<span className="info-value">
|
||||
{e.status ? e.status : '#'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="user-file-search-results"
|
||||
>
|
||||
<div className="results-header">
|
||||
<div className="results-title">Результаты поиска</div>
|
||||
<div className="results-count">
|
||||
Найдено файлов: {list.length}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="results-list"
|
||||
>
|
||||
{list.map((e: any, index: number) => {
|
||||
if (e.similarityLevel !== 'DIFFERENT') {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
|
||||
>
|
||||
<div className="result-item">
|
||||
<div className="result-header">
|
||||
<div className="result-preview">
|
||||
{e.url ? (
|
||||
<img src={e.url} alt="Preview" />
|
||||
) : (
|
||||
<div className="preview-fallback">
|
||||
<span>📄</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="result-info">
|
||||
<div className="result-filename" title={e.originalFileName}>
|
||||
{e.originalFileName}
|
||||
</div>
|
||||
<div className="result-details">
|
||||
Загружен: {e.uploadDate ? e.uploadDate : '#'} •
|
||||
Размер: {convertBytes(e.fileSize)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="similarity-score">
|
||||
<div className="similarity-percentage">
|
||||
{e.similarityLevel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="result-badges">
|
||||
<span className={`badge ${e.status === 'PROTECTED' ? 'badge-protected' : 'badge-not-protected'}`}>
|
||||
{e.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="result-actions">
|
||||
<button
|
||||
className="btn btn-secondary gap-1"
|
||||
onClick={() => {
|
||||
handlerViewfile(e);
|
||||
}}
|
||||
>
|
||||
<IconEye />
|
||||
Просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success gap-1"
|
||||
disabled={isFileLoading}
|
||||
onClick={() => handlerDownload(e.fileId, e.originalFileName)}
|
||||
>
|
||||
<IconDownload />
|
||||
Скачать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
|
||||
import { IconSearch } from '@/app/ui/icons/icons';
|
||||
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||
import { FileSearchPanel } from '@/app/ui/search/file-search-panel';
|
||||
interface SelectedFile {
|
||||
file: File;
|
||||
preview: string | undefined;
|
||||
name: string;
|
||||
size: string;
|
||||
}
|
||||
|
||||
interface FileUploadInitResponse {
|
||||
upload_id: string,
|
||||
file_name: string,
|
||||
total_chunks: number,
|
||||
chunk_size: number,
|
||||
status: string
|
||||
}
|
||||
|
||||
interface SectionSearchFile {
|
||||
allowedExtensions: {
|
||||
images: string[],
|
||||
videos: string[],
|
||||
audios: string[],
|
||||
documents: string[]
|
||||
}
|
||||
maxFileSize: number
|
||||
}
|
||||
|
||||
export default function SectionSearchFile({ allowedExtensions, maxFileSize }: SectionSearchFile) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [uploadId, setUploadId] = useState<string | null>(null);
|
||||
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||
const [fileId, setFileId] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
const isCancelledRef = useRef(false);
|
||||
|
||||
const childRef = useRef(null);
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const allExtensions = useMemo(() => { return [...allowedExtensions.images, ...allowedExtensions.videos, ...allowedExtensions.audios, ...allowedExtensions.documents] }, []);
|
||||
|
||||
const acceptString = useMemo(() => {
|
||||
if (!allExtensions || !Array.isArray(allExtensions)) {
|
||||
return '';
|
||||
}
|
||||
return allExtensions.map(e => `.${e}`).join(', ');
|
||||
}, [allExtensions]);
|
||||
|
||||
|
||||
const getFileType = (fileName: string, allowedExtensions: {
|
||||
images: string[],
|
||||
videos: string[],
|
||||
audios: string[],
|
||||
documents: string[]
|
||||
}): 'image' | 'video' | 'audio' | 'document' | 'unknown' => {
|
||||
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
const extension = fileName.substring(lastDotIndex + 1).toLowerCase();
|
||||
|
||||
if (allowedExtensions.images.includes(extension)) {
|
||||
return 'image';
|
||||
}
|
||||
|
||||
if (allowedExtensions.videos.includes(extension)) {
|
||||
return 'video';
|
||||
}
|
||||
|
||||
if (allowedExtensions.audios.includes(extension)) {
|
||||
return 'audio';
|
||||
}
|
||||
|
||||
if (allowedExtensions.documents.includes(extension)) {
|
||||
return 'document';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
|
||||
if (!allExtensions.includes(file.type as string)) {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!extension || !allExtensions.includes(extension as string)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('unsupported-file-format')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_SIZE = maxFileSize;
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('the-file-is-too-large')
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
|
||||
if (!file) {
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileId) {
|
||||
await removeUserFile(fileId, 1);
|
||||
setFileId(null);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsFileUploaded(false);
|
||||
|
||||
if (childRef.current) {
|
||||
//@ts-ignore
|
||||
childRef.current.clearList();
|
||||
}
|
||||
|
||||
setUploadProgress(0);
|
||||
|
||||
const validation = validateFile(file);
|
||||
|
||||
if (!validation.isValid) {
|
||||
setError(validation.errorMessage || t('unknown-validation-error'));
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedFile({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
||||
});
|
||||
|
||||
handlerFileUpload({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
||||
});
|
||||
|
||||
}, [t]);
|
||||
|
||||
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = event.target.files?.[0] || null;
|
||||
handleFileSelect(file);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = (): void => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (): void => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
handleFileSelect(file);
|
||||
};
|
||||
|
||||
const handlerFileUpload = useCallback(async (fileInfo: SelectedFile): Promise<void> => {
|
||||
if (uploadId) return;
|
||||
|
||||
setError(null);
|
||||
isCancelledRef.current = false;
|
||||
const file = fileInfo.file;
|
||||
const fileType = getFileType(fileInfo.file.name, allowedExtensions);
|
||||
|
||||
try {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
const initMessageBody = {
|
||||
file_name: file.name,
|
||||
file_type: fileType,
|
||||
extension: extension,
|
||||
file_size: file.size
|
||||
};
|
||||
|
||||
const response = await fileUpload(initMessageBody) as FileUploadInitResponse;
|
||||
|
||||
if (!response?.upload_id) {
|
||||
throw new Error('Failed to get upload_id');
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = response.chunk_size;
|
||||
const totalChunks = response.total_chunks;
|
||||
|
||||
setUploadId(response.upload_id);
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
if (isCancelledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isLastChunk = chunkIndex === totalChunks - 1;
|
||||
const start = chunkIndex * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('upload_id', response.upload_id);
|
||||
formData.append('chunk_number', chunkIndex.toString());
|
||||
formData.append('chunk', chunk);
|
||||
formData.append('findSimilar', '1');
|
||||
|
||||
const chunkResponse = await chunkUpload(formData);
|
||||
|
||||
if (chunkResponse.errorMesage !== null) {
|
||||
throw chunkResponse.errorMesage;
|
||||
}
|
||||
|
||||
setUploadProgress(Math.floor((chunkIndex + 1) / totalChunks * 100));
|
||||
|
||||
if (isLastChunk && chunkResponse.fileId) {
|
||||
setFileId(chunkResponse.fileId);
|
||||
document.cookie = `searchedFileId=${chunkResponse.fileId}`
|
||||
}
|
||||
}
|
||||
|
||||
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||
setIsFileUploaded(true);
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
|
||||
} else {
|
||||
throw new Error('Not all chunks were uploaded');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
setError(t('error-uploading-file'));
|
||||
console.error('Upload error:', error);
|
||||
}
|
||||
} finally {
|
||||
setUploadId(null);
|
||||
}
|
||||
}, [uploadId, allowedExtensions]);
|
||||
|
||||
useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
||||
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
||||
// но у него есть свои особенности и стандартные запросы не работают.
|
||||
const handleUnload = () => {
|
||||
if (uploadId) {
|
||||
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
|
||||
//const blob = new Blob([data], { type: 'application/json' });
|
||||
//navigator.sendBeacon('/api/v1/data/cancel', blob);
|
||||
console.log('sendBeacon');
|
||||
}
|
||||
};
|
||||
if (uploadId) {
|
||||
window.addEventListener('unload', handleUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unload', handleUnload);
|
||||
};
|
||||
}, [uploadId]);
|
||||
|
||||
useNavigationBlocker({
|
||||
shouldBlock: !!uploadId,
|
||||
message: t('have-unsaved-changes'),
|
||||
onConfirm: async () => {
|
||||
console.log('User confirmed navigation');
|
||||
if (uploadId) {
|
||||
await cancelUpload(uploadId);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('User cancelled navigation');
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="upload-section">
|
||||
<div className="search-info">
|
||||
<div className="search-info-title">Как работает поиск?</div>
|
||||
<div className="search-info-text">
|
||||
Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами,
|
||||
используя алгоритмы компьютерного зрения и цифровых отпечатков.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<IconSearch />
|
||||
<h4>
|
||||
Выберите файл для поиска
|
||||
</h4>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={acceptString}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
aria-label="Выбор файла для защиты"
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleButtonClick}
|
||||
type="button"
|
||||
disabled={uploadProgress && uploadProgress !== 100 ? true : false}
|
||||
>
|
||||
Выбрать файл
|
||||
</button>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedFile && (
|
||||
<div
|
||||
className={`selected-file`}
|
||||
>
|
||||
<div className="selected-file-file-info relative">
|
||||
<div>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">
|
||||
{t('file')}:
|
||||
</span> {selectedFile.name}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">
|
||||
{t('size')}:
|
||||
</span> {selectedFile.size}
|
||||
</p>
|
||||
</div>
|
||||
{uploadProgress !== 0 && (
|
||||
<div
|
||||
className={`absolute top-0 right-0 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
|
||||
>
|
||||
{uploadProgress}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFileUploaded && (
|
||||
<FileSearchPanel fileId={fileId} ref={childRef} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
|
||||
|
||||
interface supportedFormats {
|
||||
extensionVideo: string[]
|
||||
extensionAudio: string[]
|
||||
extensionImage: string[]
|
||||
extensionDocument: string[]
|
||||
}
|
||||
|
||||
export function SupportedFormats({ extensionVideo, extensionAudio, extensionImage, extensionDocument }: supportedFormats) {
|
||||
return (
|
||||
<div className="supported-formats">
|
||||
<div className="formats-title">
|
||||
Поддерживаемые форматы
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type image">
|
||||
<IconImageFile /> Изображения
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionImage.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type video">
|
||||
<IconVideoFile /> Видео
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionVideo.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type audio">
|
||||
<IconAudioFile /> Аудио
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionAudio.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type document">
|
||||
<IconDocument /> Документы
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionDocument.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
'use client'
|
||||
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useActionState, useEffect, useRef, useState, ChangeEvent } from 'react';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { companyUserRegistration } from '@/app/actions/companyActions';
|
||||
import { CompanyUserRegistrationFormState } from '@/app/actions/companyActions';
|
||||
import { SignupFormSchema } from '@/app/actions/definitions';
|
||||
import * as z from 'zod';
|
||||
import PhoneInput from '@/app/ui/inputs/phone-input';
|
||||
import { IconEye } from '@/app/ui/icons/icons';
|
||||
import { toast } from 'sonner';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export function CompanyUsersSettingPanel() {
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
if (userData?.permission !== 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
companyUserRegistration,
|
||||
undefined
|
||||
);
|
||||
|
||||
const t = useTranslations('Login-register-form');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [formState, setFormState] = useState<CompanyUserRegistrationFormState | undefined>(undefined);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
setFormState(state);
|
||||
if (state.createdUser) {
|
||||
toast.success(`Создан пользователь - ${state.createdUser}`);
|
||||
}
|
||||
}
|
||||
|
||||
}, [state])
|
||||
|
||||
function validateField(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
) {
|
||||
switch (fieldName) {
|
||||
case 'confirm_password':
|
||||
const baseSchema = SignupFormSchema.shape.confirm_password;
|
||||
const baseResult = baseSchema.safeParse(value);
|
||||
|
||||
if (!baseResult.success) {
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
const currentPassword = passwordRef.current?.value || '';
|
||||
if (value !== currentPassword) {
|
||||
return {
|
||||
success: false,
|
||||
error: new z.ZodError([
|
||||
{
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'repeat-password',
|
||||
path: ['confirm_password']
|
||||
}
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: value };
|
||||
|
||||
case 'email':
|
||||
case 'fullName':
|
||||
case 'phone':
|
||||
case 'password':
|
||||
case 'companyName':
|
||||
case 'agree':
|
||||
const schema = SignupFormSchema.shape[fieldName];
|
||||
if (fieldName === 'phone') {
|
||||
return schema.safeParse(value.replace(/[-\(\)\s]/g, ''));
|
||||
}
|
||||
if (fieldName === 'companyName') {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
return schema.safeParse(value);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown field: ${fieldName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeHandler(e: ChangeEvent<HTMLInputElement>): void {
|
||||
const fieldName = e.target.name;
|
||||
if (fieldName !== 'phone') {
|
||||
e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, '');
|
||||
}
|
||||
const validatedField = validateField(fieldName, e.target.value);
|
||||
|
||||
if (validatedField.success) {
|
||||
setFormState(prevState => {
|
||||
if (!prevState?.error || !prevState?.error[fieldName]) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const { [fieldName]: _, ...newError } = prevState.error;
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
error: newError
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showPassowrd(target: 'password' | 'confirm-password') {
|
||||
if (target === 'password') {
|
||||
setShowPassword(!showPassword);
|
||||
} else {
|
||||
setShowConfirmPassword(!showConfirmPassword);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-form">
|
||||
<div className="form-section">
|
||||
<h3 className="form-section-title">
|
||||
Добавление пользователей
|
||||
</h3>
|
||||
<form action={(e) => {
|
||||
formAction(e);
|
||||
}}>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('full-name')}:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="fullName"
|
||||
name="fullName"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('name-placeholder')}
|
||||
defaultValue={formState?.previousState?.fullName ? formState?.previousState?.fullName : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formState?.error?.fullName && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.fullName.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('email-adress')}:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder="ivan@example.com"
|
||||
defaultValue={formState?.previousState?.email ? formState?.previousState?.email : ''}
|
||||
onChange={(e) => {
|
||||
e.target.value = e.target.value.toLocaleLowerCase();
|
||||
onChangeHandler(e)
|
||||
}}
|
||||
/>
|
||||
{formState?.error?.email && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.email.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('phone')}:
|
||||
</label>
|
||||
<PhoneInput
|
||||
phoneState={formState?.previousState?.phone}
|
||||
validateHandler={onChangeHandler}
|
||||
clearCondition={state?.createdUser}
|
||||
/>
|
||||
{formState?.error?.phone && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.phone.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('password')}:
|
||||
</label>
|
||||
<div
|
||||
className={`${styles['password-wrapper']}`}
|
||||
>
|
||||
<input
|
||||
ref={passwordRef}
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
name="password"
|
||||
className={`${styles['form-input']} ${styles['password']}`}
|
||||
placeholder={t('password-placeholder')}
|
||||
defaultValue={formState?.previousState?.password ? formState?.previousState?.password : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
showPassowrd('password')
|
||||
}}
|
||||
type="button"
|
||||
className={`show-password-button ${showPassword ? 'show' : ''}`}
|
||||
>
|
||||
<IconEye />
|
||||
</button>
|
||||
</div>
|
||||
{formState?.error?.password && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.password.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('confirm-password')}:
|
||||
</label>
|
||||
<div className={`${styles['password-wrapper']}`}>
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
className={`${styles['form-input']} ${styles['password']}`}
|
||||
placeholder={t('repeat-password')}
|
||||
defaultValue={formState?.previousState?.confirm_password ? formState?.previousState?.confirm_password : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
showPassowrd('confirm-password');
|
||||
}}
|
||||
type="button"
|
||||
className={`show-password-button ${showConfirmPassword ? 'show' : ''}`}
|
||||
>
|
||||
<IconEye />
|
||||
</button>
|
||||
</div>
|
||||
{formState?.error?.confirm_password && (
|
||||
<p className="text-sm text-red-500">
|
||||
{t(formState?.error?.confirm_password)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formState?.error?.server && (
|
||||
<p className="text-sm text-red-500 mt-4">
|
||||
{t(formState?.error?.server)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className={`${styles['btn']}`}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
) : (
|
||||
"Добавить пользователя"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useActionState, useState } from 'react'
|
||||
import { useActionState, useState, useEffect } from 'react';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function PersonalDataSettings() {
|
||||
const [errorMessage, formAction, isPending] = useActionState(
|
||||
@@ -11,7 +14,11 @@ export default function PersonalDataSettings() {
|
||||
undefined
|
||||
);
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState('male');
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
useEffect(() => {
|
||||
console.log(userData)
|
||||
}, [userData]);
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
@@ -26,19 +33,37 @@ export default function PersonalDataSettings() {
|
||||
<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">
|
||||
{t('company')}:
|
||||
</label>
|
||||
<input type="text" name="company" className="form-input" />
|
||||
<input
|
||||
type="text"
|
||||
name="full_name"
|
||||
className="form-input"
|
||||
defaultValue={userData?.fullName}
|
||||
/>
|
||||
</div>
|
||||
{userData?.company && (
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
{t('company')}:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="company"
|
||||
className="form-input"
|
||||
defaultValue={userData?.company}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
{t('email')}:
|
||||
</label>
|
||||
<input type="text" name="email" className="form-input" />
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
className="form-input"
|
||||
defaultValue={userData?.email}
|
||||
disabled={true}
|
||||
/>
|
||||
<small>
|
||||
{t('email-cant-change')}
|
||||
</small>
|
||||
@@ -47,45 +72,13 @@ export default function PersonalDataSettings() {
|
||||
<label className="form-label">
|
||||
{t('phone')}:
|
||||
</label>
|
||||
<input type="phone" name="phone" className="form-input" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
{t('gender')}:
|
||||
</label>
|
||||
<select
|
||||
name="gender"
|
||||
<input
|
||||
type="phone"
|
||||
name="phone"
|
||||
className="form-input"
|
||||
value={selectedValue}
|
||||
onChange={(e) => setSelectedValue(e.target.value)}
|
||||
>
|
||||
<option value="male">
|
||||
{t('male')}
|
||||
</option>
|
||||
<option value="female">
|
||||
{t('female')}
|
||||
</option>
|
||||
</select>
|
||||
defaultValue={userData?.phone}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<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">
|
||||
{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">
|
||||
{t('save-changes')}
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function SubscriptionSettings() {
|
||||
const t = useTranslations('Global');
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
function dateHandler(date: string | undefined) {
|
||||
if (!date) {
|
||||
return 'error'
|
||||
}
|
||||
|
||||
const newDate = new Date(date);
|
||||
const day = newDate.getDate().toString().padStart(2, '0');
|
||||
const month = (newDate.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = newDate.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-form">
|
||||
@@ -12,18 +32,18 @@ export default function SubscriptionSettings() {
|
||||
<div className="subscription-info">
|
||||
<div className="subscription-info-header">
|
||||
<div>
|
||||
<h4 className="subscription-info-title">🎯 Подписка "ПРОФИ"</h4>
|
||||
<p>📅 Активна до 10.06.2028</p>
|
||||
<h4 className="subscription-info-title">Подписка {userData?.tariffInfo?.tariffName}</h4>
|
||||
<p>📅 Активна до {dateHandler(userData?.tariffInfo?.endTariff)}</p>
|
||||
</div>
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<div className="subscription-info-title">2,000</div>
|
||||
<div className="subscription-info-title">{userData?.tariffInfo?.maxFileCounts}</div>
|
||||
<p>файлов в месяц</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="subscription-info-main">
|
||||
<div className="subscription-info-item">
|
||||
<div>💾 Хранилище</div>
|
||||
<span>20 ГБ</span>
|
||||
<span>{userData?.tariffInfo?.maxFileOnDisk ? convertBytes(userData?.tariffInfo?.maxFileOnDisk) : 0}</span>
|
||||
</div>
|
||||
<div className="subscription-info-item">
|
||||
<div>🔌 API доступ</div>
|
||||
@@ -36,7 +56,7 @@ export default function SubscriptionSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="subscription-info-footer">
|
||||
{/* <div className="subscription-info-footer">
|
||||
<button className="btn btn-outline">
|
||||
🚀 СТАРТ
|
||||
</button>
|
||||
@@ -46,7 +66,7 @@ export default function SubscriptionSettings() {
|
||||
<button className="btn btn-outline">
|
||||
💼 ПРОФИ
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+3
-14
@@ -1,20 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function HomePage() {
|
||||
const {
|
||||
data: userData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userData'],
|
||||
queryFn: () => {
|
||||
return getUserData();
|
||||
},
|
||||
});
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (isError) return <div>Error: {error.message}</div>;
|
||||
@@ -23,7 +12,7 @@ export default function HomePage() {
|
||||
<div>
|
||||
<h1>user email</h1>
|
||||
<span>
|
||||
{userData.email}
|
||||
{userData?.email}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,51 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
|
||||
type FilesInfo = {
|
||||
totalSize: number;
|
||||
totalCount: number;
|
||||
};
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function ViolationsStatistic() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize: data.all_files_size,
|
||||
totalCount: data.all_files_quantity
|
||||
}
|
||||
}
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="protection-statistic">
|
||||
<div className="protection-statistic-stat-card">
|
||||
<div className="protection-statistic-stat-number">0</div>
|
||||
<div className="protection-statistic-stat-number">
|
||||
{filesInfo?.total.violation}
|
||||
</div>
|
||||
<div className="protection-statistic-stat-label">{t(`total-violations`)}</div>
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
<div className="protection-statistic-stat-number">0</div>
|
||||
<div className="protection-statistic-stat-number">
|
||||
{filesInfo?.total.violation}
|
||||
</div>
|
||||
<div className="protection-statistic-stat-label">{t(`new`)}</div>
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
|
||||
Reference in New Issue
Block a user