Merge branch 'main' into NCFRONT-72

This commit is contained in:
smanylov
2026-02-17 18:20:50 +07:00
107 changed files with 7351 additions and 1431 deletions
+1 -1
View File
@@ -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>
)
+138 -36
View File
@@ -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>
)
}
+12 -71
View File
@@ -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={() => {
+9 -19
View File
@@ -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
-105
View File
@@ -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';
+6 -20
View File
@@ -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>
)
+12 -2
View File
@@ -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>
)
+282 -41
View File
@@ -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 >
</>
+207
View File
@@ -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>
)
}
+21 -16
View File
@@ -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>
+5 -17
View File
@@ -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>
+31 -1
View File
@@ -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>
)
}
+10 -3
View File
@@ -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>
+18 -57
View File
@@ -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>
)
}
+17 -26
View File
@@ -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>
+7 -57
View File
@@ -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>
+15 -6
View File
@@ -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>
);
})}
+80 -1
View File
@@ -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>
);
}
+2 -2
View File
@@ -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"
},
{
+175 -113
View File
@@ -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>
)
</>
);
}
+25 -1
View File
@@ -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">
+25 -7
View File
@@ -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>
+136 -26
View File
@@ -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>
)
+473 -66
View File
@@ -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>
+43
View File
@@ -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>
)
}
+234
View File
@@ -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">&zwj;</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>
)}
</>
)
}
+56
View File
@@ -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>
)
}
+50
View File
@@ -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} />
</>
)
}
+382
View File
@@ -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>
);
}
+54
View File
@@ -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>
)
}
+40 -47
View File
@@ -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>
+26 -6
View File
@@ -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
View File
@@ -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>
);
+8 -34
View File
@@ -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">