Merge branch 'main' into NCFRONT-72
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ WORKDIR /no-copy-frontend
|
||||
|
||||
COPY . .
|
||||
COPY --from=dependencies /no-copy-frontend/node_modules ./node_modules
|
||||
RUN npm run build
|
||||
RUN rm -rf .next && npm run build --force
|
||||
|
||||
FROM node:22.20.0 AS runner
|
||||
WORKDIR /no-copy-frontend
|
||||
|
||||
Generated
+78
-2
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.17.0",
|
||||
"version": "0.40.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.17.0",
|
||||
"version": "0.40.0",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@react-input/mask": "^2.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
@@ -26,6 +27,7 @@
|
||||
"sonner": "^2.0.7",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"use-debounce": "^10.1.0",
|
||||
"validator": "^13.15.26",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
@@ -40,6 +42,20 @@
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
"node_modules/@a2seven/yoo-checkout": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@a2seven/yoo-checkout/-/yoo-checkout-1.1.4.tgz",
|
||||
"integrity": "sha512-Xn8E6fJKVUpSqTTEgigoi0Woyvmc1/UuHukwpQUTEtxD2l4skTEeqPlhd0J4xum6lQ/0eGzeJP+/MgpkE9Ak9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"axios": "^0.21.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||
@@ -2313,6 +2329,16 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/axios": {
|
||||
"version": "0.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz",
|
||||
"integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==",
|
||||
"deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
@@ -3288,6 +3314,15 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axobject-query": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
@@ -4653,6 +4688,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||
@@ -7822,6 +7877,18 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-debounce": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.0.tgz",
|
||||
"integrity": "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/use-intl": {
|
||||
"version": "4.5.8",
|
||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz",
|
||||
@@ -7845,6 +7912,15 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.26",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
||||
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.23.0",
|
||||
"version": "0.42.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@react-input/mask": "^2.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
@@ -27,6 +28,7 @@
|
||||
"sonner": "^2.0.7",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"use-debounce": "^10.1.0",
|
||||
"validator": "^13.15.26",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"buildTime": null
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { useTranslations } from 'next-intl'
|
||||
import LogoIcon from '@/app/ui/logo-icon';
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
import Link from 'next/link';
|
||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import ForgotPasswordForm from '@/app/ui/forms/forgot-password-form';
|
||||
|
||||
export default function Page() {
|
||||
@@ -10,9 +9,6 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container-language']}`}>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
|
||||
@@ -6,7 +6,6 @@ import { routing } from '@/i18n/routing';
|
||||
import { ToastProvider } from '@/app/providers/ToastProvider';
|
||||
import "../styles/globals.css";
|
||||
import "../styles/global-styles.scss";
|
||||
import ActivityTracker from '../components/ActivityTracker';
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale: any) => ({ locale }));
|
||||
@@ -35,7 +34,6 @@ export default async function RootLayout({
|
||||
<NextIntlClientProvider>
|
||||
<Providers>
|
||||
{children}
|
||||
<ActivityTracker/>
|
||||
<ToastProvider />
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
|
||||
@@ -17,9 +17,9 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container-language']}`}>
|
||||
{/* <div className={`${styles['login-container-language']}`}>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div> */}
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function Page() {
|
||||
<div className="dashboard-main-grid">
|
||||
<div className="left-column">
|
||||
<DashboardUserFiles />
|
||||
<DashboardPlatformsList />
|
||||
{/* <DashboardPlatformsList /> */}
|
||||
</div>
|
||||
<div className="right-column">
|
||||
<ViolationsCarousel />
|
||||
@@ -34,8 +34,13 @@ export default function Page() {
|
||||
</div>
|
||||
<DashboardFilesInfo />
|
||||
<div className="dashboard-main-grid">
|
||||
|
||||
{/* <DahboardGeographySection /> */}
|
||||
</div>
|
||||
<div
|
||||
className="mb-4"
|
||||
>
|
||||
<DahboardLimitsSection />
|
||||
<DahboardGeographySection />
|
||||
</div>
|
||||
<DashboardLastCheck />
|
||||
</Suspense>
|
||||
|
||||
@@ -3,6 +3,7 @@ import HeaderPanel from '@/app/ui/header/headerPanel';
|
||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
||||
import ActivityTracker from '@/app/components/ActivityTracker';
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = getQueryClient();
|
||||
@@ -20,6 +21,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
||||
</main>
|
||||
</div>
|
||||
</HydrationBoundary>
|
||||
<ActivityTracker/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import UploadSectionFile from '@/app/components/upload-section-file';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "document";
|
||||
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
|
||||
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
try {
|
||||
const data = await queryClient.fetchQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: fetchReferralUserStats,
|
||||
});
|
||||
if (data?.referralLink) {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitleColorFrame
|
||||
title="document-protection"
|
||||
description="multi-layered-protection-for-your-document"
|
||||
/>
|
||||
<ProtectionStatistic fileType={FILE_TYPE} />
|
||||
<UploadSectionFile
|
||||
fileType={FILE_TYPE}
|
||||
allowedExtensions={file_extension}
|
||||
maxFileSize={max_file_size}
|
||||
/>
|
||||
<FilesTable fileType={FILE_TYPE} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import UploadSectionFile from '@/app/components/upload-section-file';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||
@@ -1,7 +1,7 @@
|
||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import UploadSectionFile from '@/app/components/upload-section-file';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import MyContentInfoBlock from '@/app/ui/my-content/my-content-info-block';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import MyContentPieChart from '@/app/ui/my-content/my-content-pie-chart';
|
||||
import MyContentPageTitleColorFrame from '@/app/ui/my-content/new/my-content-page-title';
|
||||
import MyContentStatsOverview from '@/app/ui/my-content/new/my-content-stats-overview';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<MyContentPageTitleColorFrame title="my-content" description="my-content-description" />
|
||||
<PageTitleColorFrame title="my-content" description="my-content-description"/>
|
||||
<MyContentStatsOverview />
|
||||
{/* <div className="split-blocks">
|
||||
<MyContentInfoBlock />
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function Page() {
|
||||
<Suspense fallback={<>...</>}>
|
||||
<PageTitleColorFrame title="refferal-program" description="referral-program-description" />
|
||||
<CommissionLevels />
|
||||
<ReferralStats />
|
||||
{/* <ReferralStats /> */}
|
||||
<ReferralLinkSection />
|
||||
<InvitationsTable />
|
||||
<IncomeAndPayments />
|
||||
|
||||
@@ -5,12 +5,14 @@ import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month';
|
||||
import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography';
|
||||
import ActivityByContentType from '@/app/ui/reports/activity-by-content-type';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import ReportsInfo from '@/app/ui/reports/reports-info';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="reports-and-analytics" />
|
||||
<StatsGrid />
|
||||
{/* <StatsGrid /> */}
|
||||
<ReportsInfo />
|
||||
<ActivityByContentType />
|
||||
<div className="analytics-grid">
|
||||
<AnalyticsCardMonth />
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import SectionSearchFile from '@/app/ui/search/section-search-file';
|
||||
import { SupportedFormats } from '@/app/ui/search/supported-formats';
|
||||
import { SearchStats } from '@/app/ui/search/search-stats';
|
||||
import { RecentSearches } from '@/app/ui/search/recent-searches';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
|
||||
export default async function Page() {
|
||||
const { file_extension: extensionVideo, max_file_size } = await getAllowedFilesExtensions('video');
|
||||
const { file_extension: extensionAudio } = await getAllowedFilesExtensions('audio');
|
||||
const { file_extension: extensionImage } = await getAllowedFilesExtensions('image');
|
||||
const { file_extension: extensionDocument } = await getAllowedFilesExtensions('document');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="block-wrapper">
|
||||
<h1 className="page-title">
|
||||
Поиск защищенного контента
|
||||
</h1>
|
||||
<p className="page-description">
|
||||
Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете
|
||||
</p>
|
||||
</div>
|
||||
<div className="search-grid">
|
||||
<SectionSearchFile
|
||||
maxFileSize={max_file_size}
|
||||
/* allowedExtensions={allExtensions} */
|
||||
allowedExtensions={{
|
||||
images: extensionImage,
|
||||
videos: extensionVideo,
|
||||
audios: extensionAudio,
|
||||
documents: extensionDocument
|
||||
}}
|
||||
/>
|
||||
<div className="search-sidebar">
|
||||
<SupportedFormats
|
||||
extensionVideo={extensionVideo}
|
||||
extensionAudio={extensionAudio}
|
||||
extensionImage={extensionImage}
|
||||
extensionDocument={extensionDocument}
|
||||
/>
|
||||
<SearchStats />
|
||||
<RecentSearches />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import LastActivitySettings from '@/app/ui/settings/last-activity-settings';
|
||||
import DangerZone from '@/app/ui/settings/danger-zone';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Global');
|
||||
@@ -16,11 +17,12 @@ export default function Page() {
|
||||
<PageTitle title="account-settings" />
|
||||
<div className="settings-grid">
|
||||
<PersonalDataSettings />
|
||||
<NotificationsMonitoringSettings />
|
||||
{/* <NotificationsMonitoringSettings /> */}
|
||||
<SafetySetting />
|
||||
<SubscriptionSettings />
|
||||
<APISettings />
|
||||
{/* <APISettings /> */}
|
||||
<LastActivitySettings />
|
||||
<CompanyUsersSettingPanel />
|
||||
</div>
|
||||
<DangerZone />
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,6 @@ import LogoIcon from '@/app/ui/logo-icon';
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
import Link from 'next/link';
|
||||
import RegisterForm from '@/app/ui/forms/register-form';
|
||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
export default function Page() {
|
||||
@@ -10,9 +9,6 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container-language']}`}>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslations } from 'next-intl'
|
||||
import LogoIcon from '@/app/ui/logo-icon';
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
import Link from 'next/link';
|
||||
import ResetPasswordForm from '@/app/ui/forms/reset-password-form';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Login-register-form');
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
<h1>NO COPY</h1>
|
||||
<p>
|
||||
{t('recover-password')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ResetPasswordForm />
|
||||
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
<Link href="register"> {t('register')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+104
-11
@@ -1,15 +1,14 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData, deleteSession } from '@/app/actions/session';
|
||||
import { testUserData, API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { tokenLifeExtension } from '@/app/actions/auth';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export async function getUserData() {
|
||||
const userEmail = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
|
||||
/* для теста */
|
||||
if (userEmail === "test" && token === "1111") {
|
||||
if (userEmail === 'test' && token === '1111') {
|
||||
return testUserData;
|
||||
}
|
||||
/* для теста */
|
||||
@@ -18,17 +17,19 @@ export async function getUserData() {
|
||||
const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error');
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +43,13 @@ export async function getUserFilesInfo() {
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: "user_files_info",
|
||||
action: 'user_files_info',
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,8 +62,100 @@ export async function getUserFilesInfo() {
|
||||
} else {
|
||||
throw parsed.message_code;
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`error: ${error}`);
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTariffs() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30001,
|
||||
message_body: {
|
||||
action: 'get',
|
||||
user_token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json()
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed.message_body?.tariffs;
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBuildData() {
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check/api/build`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchINN(inn: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30004,
|
||||
message_body: {
|
||||
inn: inn,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json()
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed.message_body
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+132
-59
@@ -3,6 +3,7 @@
|
||||
import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
|
||||
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { fetchINN } from '@/app/actions/action';
|
||||
|
||||
export async function logout() {
|
||||
const token = await getSessionData('token');
|
||||
@@ -11,9 +12,9 @@ export async function logout() {
|
||||
await fetch(`${API_BASE_URL}/v1/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -36,8 +37,8 @@ export async function authorization(
|
||||
const password = formData.get('password');
|
||||
|
||||
/* для теста */
|
||||
if (email === "test" && password === "test") {
|
||||
await createSession("1111", "test");
|
||||
if (email === 'test' && password === 'test') {
|
||||
await createSession('1111', 'test');
|
||||
redirect('/pages/dashboard');
|
||||
}
|
||||
/* для теста */
|
||||
@@ -81,13 +82,13 @@ export async function authorization(
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.message_desc === 'Operation successful') {
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.token, email);
|
||||
} else {
|
||||
throw (`${parsed.message_code}`);
|
||||
@@ -130,14 +131,16 @@ export async function authorization(
|
||||
|
||||
export type FormState = {
|
||||
previousState: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
phone: string;
|
||||
companyName: string;
|
||||
agree: string;
|
||||
},
|
||||
fullName?: string | undefined;
|
||||
email?: string | undefined;
|
||||
password?: string | undefined;
|
||||
confirm_password?: string | undefined;
|
||||
phone?: string | undefined;
|
||||
companyName?: string | undefined;
|
||||
agree?: string | undefined;
|
||||
referralCode?: string | undefined;
|
||||
inn?: string | undefined;
|
||||
} | undefined,
|
||||
mailConfirm?: boolean,
|
||||
userId?: number,
|
||||
error: Record<string, string> | null
|
||||
@@ -147,6 +150,8 @@ export async function registration(
|
||||
state: FormState | undefined,
|
||||
formData: FormData,
|
||||
): Promise<FormState> {
|
||||
const accountType = formData.get('accountType') as 'b2b' | 'b2c';
|
||||
|
||||
const fullName = formData.get('fullName') as string || '';
|
||||
const email = formData.get('email') as string || '';
|
||||
const password = formData.get('password') as string || '';
|
||||
@@ -154,8 +159,10 @@ export async function registration(
|
||||
const phone = formData.get('phone') as string || '';
|
||||
const companyName = formData.get('companyName') as string || '';
|
||||
const agree = formData.get('agree') as string || '';
|
||||
const referralCode = formData.get('referralCode');
|
||||
const inn = formData.get('inn') as string || '';
|
||||
|
||||
const SignupFormSchema = await getSignupFormSchema();
|
||||
const SignupFormSchema = await getSignupFormSchema(accountType);
|
||||
|
||||
const dataToValidate = {
|
||||
fullName,
|
||||
@@ -164,23 +171,41 @@ export async function registration(
|
||||
password,
|
||||
confirm_password,
|
||||
agree,
|
||||
companyName
|
||||
...(accountType === 'b2b' && { companyName, inn })
|
||||
};
|
||||
|
||||
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
|
||||
const validatedFields = await SignupFormSchema.safeParse(dataToValidate);
|
||||
|
||||
if (!validatedFields.success) {
|
||||
console.log(`try to register ${accountType}`);
|
||||
|
||||
let confirmedCompanyName: boolean = true;
|
||||
if (accountType === 'b2b') {
|
||||
try {
|
||||
const response = await fetchINN(inn);
|
||||
confirmedCompanyName = response?.companyName ? true : false;
|
||||
} catch (error) {
|
||||
confirmedCompanyName = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validatedFields.success || !confirmedCompanyName) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
if (validatedFields?.error?.message) {
|
||||
JSON?.parse(validatedFields?.error?.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!confirmedCompanyName) {
|
||||
errors.inn = 'inn-invalid'
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
@@ -189,8 +214,8 @@ export async function registration(
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree
|
||||
agree,
|
||||
...(accountType === 'b2b' && { companyName, inn })
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
@@ -208,25 +233,47 @@ export async function registration(
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
companyName: companyName,
|
||||
accountType,
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||
...(referralCode !== '' && { referralLink: referralCode })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||
...(referralCode !== '' && { referralLink: referralCode })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json() as {
|
||||
message_desc: string,
|
||||
message_body: { token: string, userId: number },
|
||||
message_code: string
|
||||
message_code: number
|
||||
};
|
||||
console.log(parsed);
|
||||
if (parsed.message_desc === 'Operation successful') {
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
console.log(`registration - ${accountType}`);
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
...(referralCode !== '' && { referralLink: referralCode }),
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
|
||||
});
|
||||
/* await createSession(parsed.message_body.token, email as string); */
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
@@ -234,14 +281,17 @@ export async function registration(
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree
|
||||
agree,
|
||||
...(referralCode !== '' && { referralLink: referralCode }),
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
|
||||
},
|
||||
mailConfirm: true,
|
||||
userId: parsed.message_body.userId,
|
||||
error: null
|
||||
};
|
||||
|
||||
} else {
|
||||
console.log(parsed);
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
@@ -249,26 +299,35 @@ export async function registration(
|
||||
}
|
||||
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
switch (typedError.message_code) {
|
||||
case 1:
|
||||
errors['server'] = 'email-or-already-registered'
|
||||
break;
|
||||
|
||||
if (typedError.message_desc === 'Refferal link is not exist') {
|
||||
errors['server'] = 'referral-link-is-not-exist'
|
||||
break;
|
||||
} else {
|
||||
errors['server'] = 'email-or-already-registered'
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
typedError.message_body.fieldErrors.forEach((obj: {
|
||||
code: string,
|
||||
field: string
|
||||
}) => {
|
||||
if (errors[obj.field]) {
|
||||
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
|
||||
} else {
|
||||
errors[obj.field] = obj.code.replaceAll('.', '-');
|
||||
}
|
||||
});
|
||||
if (typedError.message_body?.fieldErrors) {
|
||||
typedError.message_body?.fieldErrors?.forEach((obj: {
|
||||
code: string,
|
||||
field: string
|
||||
}) => {
|
||||
if (errors[obj.field]) {
|
||||
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
|
||||
} else {
|
||||
errors[obj.field] = obj.code.replaceAll('.', '-');
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
errors['server'] = 'unknown-error'
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -284,14 +343,27 @@ export async function registration(
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree
|
||||
agree,
|
||||
inn
|
||||
},
|
||||
mailConfirm: false,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree,
|
||||
inn
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
@@ -306,8 +378,8 @@ export async function tokenLifeExtension() {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -331,11 +403,12 @@ export async function tokenLifeExtension() {
|
||||
}
|
||||
|
||||
export async function confirmEmail(
|
||||
state: {error: string} | undefined,
|
||||
state: { error: string } | undefined,
|
||||
formData: FormData
|
||||
) {
|
||||
console.log('confirmEmail');
|
||||
const emailConfirm = formData.get('emailConfirm') as string || '';
|
||||
console.log(emailConfirm);
|
||||
const userId = formData.get('userId') as string || '';
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
'use server'
|
||||
|
||||
import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
|
||||
export type CompanyUserRegistrationFormState = {
|
||||
previousState: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
phone: string;
|
||||
},
|
||||
createdUser: string | null
|
||||
error: Record<string, string> | null
|
||||
} | undefined;
|
||||
|
||||
export async function companyUserRegistration(
|
||||
state: CompanyUserRegistrationFormState | undefined,
|
||||
formData: FormData,
|
||||
): Promise<CompanyUserRegistrationFormState> {
|
||||
const token = await getSessionData('token');
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullName = formData.get('fullName') as string || '';
|
||||
const email = formData.get('email') as string || '';
|
||||
const password = formData.get('password') as string || '';
|
||||
const confirm_password = formData.get('confirm_password') as string || '';
|
||||
const phone = formData.get('phone') as string || '';
|
||||
|
||||
const SignupFormSchema = await getSignupFormSchema('b2c');
|
||||
|
||||
const dataToValidate = {
|
||||
fullName,
|
||||
email,
|
||||
phone: phone.replace(/[-\(\)\s]/g, ''),
|
||||
password,
|
||||
confirm_password,
|
||||
agree: 'on'
|
||||
};
|
||||
|
||||
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
|
||||
|
||||
console.log(`try to register company users ${email}`);
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('registration error');
|
||||
console.log(errors);
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone
|
||||
},
|
||||
createdUser: null,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { fullName, email, password, phone } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20002,
|
||||
message_body: {
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType: 'b2b',
|
||||
authToken: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json() as {
|
||||
message_desc: string,
|
||||
message_body: { token: string },
|
||||
message_code: number
|
||||
};
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType: 'b2b',
|
||||
authToken: token
|
||||
});
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
console.log(`registration - ${email}`);
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password
|
||||
});
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
phone: ''
|
||||
},
|
||||
createdUser: fullName,
|
||||
error: null
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
|
||||
switch (typedError.message_code) {
|
||||
case 1:
|
||||
errors['server'] = 'email-or-already-registered'
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (typedError.message_body?.fieldErrors) {
|
||||
typedError.message_body?.fieldErrors?.forEach((obj: {
|
||||
code: string,
|
||||
field: string
|
||||
}) => {
|
||||
if (errors[obj.field]) {
|
||||
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
|
||||
} else {
|
||||
errors[obj.field] = obj.code.replaceAll('.', '-');
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
errors['server'] = 'unknown-error'
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errors['server'] = 'request-ended-with-an-error'
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone
|
||||
},
|
||||
createdUser: null,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone
|
||||
},
|
||||
createdUser: null,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from 'zod'
|
||||
import { createValidationSchema } from "@/app/lib/validation-config-parser";
|
||||
import {createValidationSchema, createValidationSchemaForPassword} from '@/app/lib/validation-config-parser';
|
||||
import validationConfig from '@/app/lib/validation-config.json';
|
||||
|
||||
export type FormState =
|
||||
@@ -35,30 +35,31 @@ interface ValidationConfig {
|
||||
|
||||
const defaultConfig = validationConfig as ValidationConfig;
|
||||
|
||||
let cachedSignupSchema: any = null;
|
||||
let schemaPromise: Promise<any> | null = null;
|
||||
|
||||
async function loadConfigFromBackend(): Promise<any> {
|
||||
async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('/api/validation-config');
|
||||
if (!response.ok) throw new Error('Failed to load config');
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Using default validation config:', error);
|
||||
//сейчас выполняем это пока бека нету
|
||||
/* console.error('Using default validation config:', error); */
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSignupFormSchema() {
|
||||
console.log('get scheme');
|
||||
if (cachedSignupSchema) {
|
||||
return cachedSignupSchema;
|
||||
}
|
||||
export async function getSignupFormSchema(schemaType?: 'b2b' | 'b2c' | 'resetPassword') {
|
||||
let schemaPromise: Promise<any> | null = null;
|
||||
let cachedSignupSchema: any = null;
|
||||
|
||||
if (!schemaPromise) {
|
||||
schemaPromise = (async () => {
|
||||
const config = await loadConfigFromBackend();
|
||||
cachedSignupSchema = createValidationSchema(config);
|
||||
if (schemaType === 'resetPassword') {
|
||||
cachedSignupSchema = createValidationSchemaForPassword(config);
|
||||
} else {
|
||||
cachedSignupSchema = createValidationSchema(config, schemaType);
|
||||
}
|
||||
|
||||
return cachedSignupSchema;
|
||||
})();
|
||||
}
|
||||
@@ -66,6 +67,7 @@ export async function getSignupFormSchema() {
|
||||
return await schemaPromise;
|
||||
}
|
||||
|
||||
// для валидации в реально времени на клиентской странице
|
||||
export const SignupFormSchema = createValidationSchema(defaultConfig);
|
||||
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ export async function getUserFilesData(page: number, pageSize: number) {
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function getUserFilesData(page: number, pageSize: number) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeUserFile(fileId: string) {
|
||||
export async function removeUserFile(fileId: string, fullDelete: number) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
@@ -54,12 +54,13 @@ export async function removeUserFile(fileId: string) {
|
||||
message_body: {
|
||||
action: 'delete_file',
|
||||
file_id: fileId,
|
||||
full_delete: fullDelete,
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,6 +76,44 @@ export async function removeUserFile(fileId: string) {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function viewFileInfo(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'file_info',
|
||||
file_id: fileId,
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { FormState } from '@/app/actions/auth';
|
||||
import { unknown } from 'zod';
|
||||
|
||||
interface initMessageBody {
|
||||
file_name: string,
|
||||
@@ -12,15 +14,18 @@ interface initMessageBody {
|
||||
token?: string
|
||||
}
|
||||
|
||||
export async function fileUpload(messageBody: initMessageBody) {
|
||||
export async function fileUpload(messageBody: initMessageBody, convertTo: string | null | undefined) {
|
||||
const token = await getSessionData('token');
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const message = messageBody;
|
||||
message.action = "init";
|
||||
message.action = 'init';
|
||||
message.token = token;
|
||||
|
||||
// нужно будет добавить свойство convertTo в тело запроса он может прийти со значением reject тогда конвертировать не нужно.
|
||||
// console.log(convertTo);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -30,8 +35,8 @@ export async function fileUpload(messageBody: initMessageBody) {
|
||||
message_body: message
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,13 +73,13 @@ export async function cancelUpload(udloadId: string) {
|
||||
version: 1,
|
||||
msg_id: 20004,
|
||||
message_body: {
|
||||
action: "cancel",
|
||||
action: 'cancel',
|
||||
upload_id: udloadId,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -101,7 +106,12 @@ export async function cancelUpload(udloadId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function chunkUpload(formData: FormData) {
|
||||
export type ChunkResponse = {
|
||||
errorMesage: string | null
|
||||
fileId?: string
|
||||
};
|
||||
|
||||
export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, {
|
||||
method: 'POST',
|
||||
@@ -111,17 +121,34 @@ export async function chunkUpload(formData: FormData) {
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_desc === 'Upload cancelled successfully') {
|
||||
return parsed.message_body;
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
errorMesage: null,
|
||||
fileId: parsed.message_body.file_id
|
||||
}
|
||||
} else if (parsed.message_desc === 'Duplicate file upload') {
|
||||
throw 'duplicate-file'
|
||||
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: User not have tokens for protect') {
|
||||
throw 'user-not-have-tokens-for-protect'
|
||||
} else {
|
||||
throw parsed;
|
||||
throw 'error'
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
throw 'error'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
|
||||
return {
|
||||
errorMesage: typedError
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
errorMesage: 'error'
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +165,8 @@ export async function checkChunkStatus(upload_id: string) {
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -173,8 +200,8 @@ export async function getAllowedFilesExtensions(type: string) {
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function fetchReferralsLevels() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'levels',
|
||||
token: token,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
referrals: parsed.message_body,
|
||||
error: null
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
referrals: [],
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReferralUserStats() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'userStats',
|
||||
token: token,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReferralInvitees() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'invitees',
|
||||
token: token,
|
||||
pageSize: 100,
|
||||
pageNumber: 0
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log({
|
||||
action: 'invitees',
|
||||
token: token,
|
||||
pageSize: 10,
|
||||
pageNumber: 0
|
||||
})
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function referralRefill() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'refill',
|
||||
token: token,
|
||||
amount: 1,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('referralRefill');
|
||||
console.log({
|
||||
action: 'refill',
|
||||
token: token,
|
||||
amount: 1,
|
||||
})
|
||||
let parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function searchUserFiles(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY&auth_token=${token}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchGlobalFiles(fileId: string, currentPage: number) {
|
||||
const token = await getSessionData('token');
|
||||
console.log('searchGlobalFiles');
|
||||
console.log(currentPage);
|
||||
|
||||
//удалить когда поиск будет нормально работать
|
||||
/* return {
|
||||
images: [
|
||||
{
|
||||
url: 'string1',
|
||||
pageTitle: `string1 from page ${currentPage}`,
|
||||
height: 100,
|
||||
width: 100,
|
||||
host: 'string1',
|
||||
pageUrl: 'string1'
|
||||
},
|
||||
{
|
||||
url: 'string1',
|
||||
pageTitle: `string2 from page ${currentPage}`,
|
||||
height: 100,
|
||||
width: 100,
|
||||
host: 'string1',
|
||||
pageUrl: 'string1'
|
||||
},
|
||||
{
|
||||
url: 'string1',
|
||||
pageTitle: `string3 from page ${currentPage}`,
|
||||
height: 100,
|
||||
width: 100,
|
||||
host: 'string1',
|
||||
pageUrl: 'string1'
|
||||
}
|
||||
],
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
totalPages: 11,
|
||||
totalResults: 100,
|
||||
} */
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20007,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
page: currentPage,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSearchStats() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/check/file_stats`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
return parsed;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,12 @@ export async function getSessionData(type: 'token' | 'email'): Promise<string |
|
||||
|
||||
export async function deleteSession() {
|
||||
console.log('delete session')
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session');
|
||||
redirect('/login');
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session');
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
redirect('/login');
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ export async function vkAuthorization(data : any, codeVerifier: string) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/vk/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
|
||||
@@ -15,8 +15,8 @@ export async function YandexAuthorization(data: any, codeVerifier: string) {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/yandex/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'use server'
|
||||
|
||||
import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
|
||||
const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4' });
|
||||
|
||||
const idempotenceKey = '02347fc4-a1f0-49db-807e-f0d67c2ed5a5';
|
||||
|
||||
export async function yooKasaCreatePayment(amount: number, tariff: string) {
|
||||
console.log(amount);
|
||||
console.log(tariff);
|
||||
/* return true; */
|
||||
|
||||
const userEmail = await getSessionData('email');
|
||||
|
||||
const createPayload: ICreatePayment = {
|
||||
amount: {
|
||||
value: amount.toLocaleString(),
|
||||
currency: 'RUB'
|
||||
},
|
||||
payment_method_data: {
|
||||
type: 'bank_card'
|
||||
},
|
||||
confirmation: {
|
||||
type: 'redirect',
|
||||
return_url: 'test'
|
||||
},
|
||||
metadata: {
|
||||
userId: userEmail,
|
||||
tariff: tariff
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const payment = await checkout.createPayment(createPayload, Date.now().toLocaleString());
|
||||
console.log(payment);
|
||||
return payment?.confirmation.confirmation_url;
|
||||
/* return payment; */
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,36 @@
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { tokenLifeExtension } from '@/app/actions/auth';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||
|
||||
export default function ActivityTracker() {
|
||||
const lastRefreshTime = useRef(Date.now());
|
||||
const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
|
||||
const isRefreshing = useRef(false);
|
||||
|
||||
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const selectedId = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('searchedFileId='))
|
||||
?.split('=')[1];
|
||||
|
||||
const handleCookie = async () => {
|
||||
if (selectedId) {
|
||||
await removeUserFile(selectedId, 1);
|
||||
document.cookie = 'searchedFileId=; path=/; max-age=0';
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedId) {
|
||||
handleCookie();
|
||||
}
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
// Debounce функция
|
||||
const debounce = useCallback((func: () => void, delay: number) => {
|
||||
if (activityTimeoutRef.current) {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { ReactNode, useState, useRef } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { SelectedFile } from './UploadSectionFile';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface DropDownItem {
|
||||
file: SelectedFile;
|
||||
convertTo: string;
|
||||
}
|
||||
|
||||
interface DropDownListProps {
|
||||
children: ReactNode;
|
||||
value: string | null | undefined;
|
||||
callBack: (value: DropDownItem) => void;
|
||||
translatedValue?: string | null | undefined;
|
||||
selectedFiles: SelectedFile[];
|
||||
}
|
||||
|
||||
interface LiElementProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
||||
'data-file'?: string;
|
||||
'data-convert-to'?: string;
|
||||
}
|
||||
|
||||
export default function DropDownListForImageConvert({
|
||||
children,
|
||||
value,
|
||||
callBack,
|
||||
translatedValue,
|
||||
selectedFiles
|
||||
}: DropDownListProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const t = useTranslations('Global')
|
||||
|
||||
useClickOutside(dropdownRef,
|
||||
() => {
|
||||
setIsOpen(false);
|
||||
},
|
||||
isOpen
|
||||
);
|
||||
|
||||
const getDisplayValue = (): string => {
|
||||
if (translatedValue) return translatedValue;
|
||||
return value ? value : t('do-not-convert');
|
||||
};
|
||||
|
||||
const findFileByName = (fileName: string): SelectedFile | undefined => {
|
||||
return selectedFiles.find(file => file.name === fileName);
|
||||
};
|
||||
|
||||
const enhancedChildren = React.Children.map(children, (child, index) => {
|
||||
if (React.isValidElement<LiElementProps>(child)) {
|
||||
const fileName = child.props['data-file'];
|
||||
const convertTo = child.props['data-convert-to'];
|
||||
|
||||
const isCurrent = value === convertTo;
|
||||
|
||||
return React.cloneElement(child, {
|
||||
onClick: () => {
|
||||
if (fileName && convertTo) {
|
||||
const file = findFileByName(fileName);
|
||||
if (file) {
|
||||
callBack({ file, convertTo });
|
||||
} else {
|
||||
console.error(`Файл с именем ${fileName} не найден`);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
className: `dropdown-item ${isCurrent ? "current" : ""}`,
|
||||
key: index,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dropdown-wrapper" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="dropdown-button"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">
|
||||
{getDisplayValue()}
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="dropdown-list">
|
||||
<ul
|
||||
className=""
|
||||
role="listbox"
|
||||
>
|
||||
{enhancedChildren}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
|
||||
|
||||
export const FileTypeIcon = ({ type }: { type: string }) => {
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return <span className="color-image">
|
||||
<IconImageFile />
|
||||
</span>;
|
||||
case 'video':
|
||||
return <span className="color-video">
|
||||
<IconVideoFile />
|
||||
</span>;
|
||||
case 'audio':
|
||||
return <span className="color-audio">
|
||||
<IconAudioFile />
|
||||
</span>;
|
||||
default:
|
||||
return <span className="color-document">
|
||||
<IconDocument />
|
||||
</span>;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
export function Pagination({ totalPages, currentPage, fileId, callBack }: any) {
|
||||
|
||||
if (totalPages === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getVisiblePages = () => {
|
||||
const pages = [];
|
||||
const maxVisible = 7;
|
||||
let start = Math.max(1, currentPage - 3);
|
||||
let end = Math.min(totalPages, currentPage + 3);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
if (start === 1) {
|
||||
end = Math.min(totalPages, start + maxVisible - 1);
|
||||
} else if (end === totalPages) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const visiblePages = getVisiblePages();
|
||||
|
||||
return (
|
||||
<div className="pagination-wrapper">
|
||||
{visiblePages[0] > 1 && (
|
||||
<>
|
||||
<button
|
||||
className={`pagination-button`}
|
||||
onClick={() => callBack(fileId, 1, true)}
|
||||
>
|
||||
1
|
||||
</button>
|
||||
{visiblePages[0] > 2 && <span>...</span>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{visiblePages.map(page => (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => callBack(fileId, page, true)}
|
||||
className={`pagination-button ${page === currentPage ? 'current-page' : ''}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{visiblePages[visiblePages.length - 1] < totalPages && (
|
||||
<>
|
||||
{visiblePages[visiblePages.length - 1] < totalPages - 1 && <span>...</span>}
|
||||
<button
|
||||
className={`pagination-button`}
|
||||
onClick={() => callBack(fileId, totalPages, true)}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,15 +11,17 @@ import {
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
} from '@tanstack/react-table';
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons';
|
||||
import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation } from '@/app/ui/icons/icons';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import DropDownList from '@/app/components/dropDownList';
|
||||
import DropDownList from '@/app/components/DropDownList';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { getUserFilesData, removeUserFile } from '@/app/actions/fileEntity';
|
||||
import ModalWindow from '@/app/components/modalWindow';
|
||||
import { getUserFilesData, removeUserFile, viewFileInfo } from '@/app/actions/fileEntity';
|
||||
import ModalWindow from '@/app/components/ModalWindow';
|
||||
import { toast } from 'sonner';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { FileInfoModalWindow } from '@/app/ui/modal-windows/file-info-modal-window';
|
||||
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||
|
||||
type FileType = 'image' | 'video' | 'audio';
|
||||
|
||||
@@ -31,7 +33,9 @@ type FileItem = {
|
||||
size?: number | undefined;
|
||||
uploadDate?: number;
|
||||
status?: string;
|
||||
protectStatus: string;
|
||||
_original?: ApiFile;
|
||||
supportId: number;
|
||||
};
|
||||
|
||||
type ApiFile = {
|
||||
@@ -42,32 +46,15 @@ type ApiFile = {
|
||||
fileSize: number;
|
||||
updatedAt: string;
|
||||
status: string;
|
||||
protectStatus: string;
|
||||
supportId: number;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
type ApiResponse = {
|
||||
files?: ApiFile[];
|
||||
};
|
||||
|
||||
// Иконки для типов файлов
|
||||
const FileTypeIcon = ({ type }: { type: string }) => {
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return <span className="color-image">
|
||||
<IconImageFile />
|
||||
</span>;
|
||||
case 'video':
|
||||
return <span className="color-video">
|
||||
<IconVideoFile />
|
||||
</span>;
|
||||
case 'audio':
|
||||
return <span className="color-audio">
|
||||
<IconAudioFile />
|
||||
</span>;
|
||||
default:
|
||||
return <span>📄</span>;
|
||||
}
|
||||
};
|
||||
|
||||
// Форматирование даты из timestamp
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
@@ -130,10 +117,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
if (!data?.files) return [];
|
||||
|
||||
return data.files.map((item: ApiFile) => {
|
||||
const [datePart, timePart] = item.updatedAt.split(' ');
|
||||
const [day, month, year] = datePart.split('-').map(Number);
|
||||
const [hours, minutes, seconds] = timePart.split(':').map(Number);
|
||||
const newDate = new Date(year, month - 1, day, hours, minutes, seconds).getTime();
|
||||
const newDate = new Date(item.updatedAt).getTime();
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
@@ -143,6 +127,8 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
size: item.fileSize,
|
||||
uploadDate: newDate,
|
||||
status: item.status,
|
||||
protectStatus: item.protectStatus,
|
||||
supportId: item.supportId,
|
||||
_original: item
|
||||
};
|
||||
});
|
||||
@@ -171,6 +157,41 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
// Определение колонок
|
||||
const columns = useMemo<ColumnDef<FileItem>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
ID
|
||||
</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 table-item-id">
|
||||
{row.original.supportId ? row.original.supportId : '-'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'fileName',
|
||||
header: ({ column }) => (
|
||||
@@ -207,7 +228,9 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
{cutFileName(row.original.fileName)}
|
||||
</span>
|
||||
<br />
|
||||
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <span className="table-item-protected">{row.original?.status}</span>
|
||||
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <span className="table-item-protected">
|
||||
{row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,7 +387,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
return (
|
||||
<div className="text-center font-semibold table-item">
|
||||
<span className="table-item-status">
|
||||
{t('status')}
|
||||
{row.original.status}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@@ -382,31 +405,13 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
cell: ({ row }) => (
|
||||
<div className="actions">
|
||||
<div className="actions-group" style={{ display: "none" }}>
|
||||
<button
|
||||
onClick={() => handleView(row.original)}
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
>
|
||||
<IconEye />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleProtect(row.original)}
|
||||
className="bg-violet-500 hover:bg-violet-600"
|
||||
>
|
||||
<IconShieldExclamation />
|
||||
</button>
|
||||
</div>
|
||||
<div className="actions-group">
|
||||
<button
|
||||
onClick={() => handleDownload(row.original)}
|
||||
disabled={isFileLoading}
|
||||
className="table-action-download"
|
||||
>
|
||||
<IconFileDownload />
|
||||
<span>
|
||||
{t('download')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
{/* <button
|
||||
onClick={() => handleOpenWindowForRemove(row.original)}
|
||||
className="table-action-delete"
|
||||
>
|
||||
@@ -414,6 +419,31 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
<span>
|
||||
{t('delete')}
|
||||
</span>
|
||||
</button> */}
|
||||
</div>
|
||||
<div className="actions-group">
|
||||
{row.original.protectStatus === 'PROTECTED' && (
|
||||
<button
|
||||
onClick={() => handleDownload(row.original)}
|
||||
disabled={isFileLoading}
|
||||
className="table-action-download"
|
||||
>
|
||||
<IconFileDownload />
|
||||
<span>
|
||||
{t('download')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
handleView(row.original)
|
||||
}}
|
||||
className="table-action-view"
|
||||
>
|
||||
<IconEye />
|
||||
<span>
|
||||
{t('make-check')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -426,9 +456,19 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
[]
|
||||
);
|
||||
|
||||
// Обработчики действий
|
||||
const handleView = (file: FileItem) => {
|
||||
const handleView = async (file: FileItem) => {
|
||||
console.log(`Просмотр файла: ${file.fileName}`);
|
||||
console.log(file);
|
||||
|
||||
const fileInfo = await viewFileInfo(file.id);
|
||||
console.log(fileInfo);
|
||||
|
||||
setOpenWindowChildren(() => {
|
||||
return (
|
||||
<FileInfoModalWindow fileInfo={fileInfo} setWindowClose={setOpenWindow} setWindowChildren={setOpenWindowChildren} />
|
||||
)
|
||||
})
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
const handleDownload = async (file: FileItem) => {
|
||||
@@ -445,7 +485,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = file.fileName || `file-${file.id}`;
|
||||
a.download = file._original?.fileName || file.fileName || `file-${file.id}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
@@ -474,7 +514,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
<div className="flex justify-center gap-4">
|
||||
<button className="btn-primary btn-modal"
|
||||
onClick={() => {
|
||||
deleteMutation.mutate(file.id)
|
||||
deleteMutation.mutate({
|
||||
fileId: file.id,
|
||||
removeParam: 1,
|
||||
})
|
||||
}}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
@@ -496,9 +539,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
};
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: removeUserFile,
|
||||
mutationFn: ({ fileId, removeParam }: { fileId: string; removeParam: number }) =>
|
||||
removeUserFile(fileId, removeParam),
|
||||
|
||||
onMutate: async (fileId: string) => {
|
||||
onMutate: async ({ fileId, removeParam }: { fileId: string; removeParam: number }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
|
||||
|
||||
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
|
||||
@@ -515,7 +559,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
toast.error(t('error'));
|
||||
},
|
||||
|
||||
onSuccess: (response, fileId) => {
|
||||
onSuccess: (response, { fileId }) => {
|
||||
if (response === fileId) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['userFilesData'],
|
||||
@@ -651,7 +695,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
</h3>
|
||||
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
|
||||
{/* Фильтры */}
|
||||
<div className="tanstak-table-filtres" style={{ display: "none" }}>
|
||||
<div className="tanstak-table-filtres">
|
||||
<div className="table-filtres-wrapper">
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{t('date-filter')}:</div>
|
||||
@@ -0,0 +1,637 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
|
||||
import { IconShieldAdd } 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 DropDownListForImageConvert from '@/app/components/DropDownListForImageConvert';
|
||||
|
||||
export interface SelectedFile {
|
||||
file: File;
|
||||
preview: string | undefined;
|
||||
name: string;
|
||||
size: string;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
uploadId?: string;
|
||||
progress: number;
|
||||
status: 'pending' | 'uploading' | 'completed' | 'error' | 'cancelled';
|
||||
error?: string;
|
||||
convertTo?: string | null;
|
||||
}
|
||||
|
||||
interface FileUploadInitResponse {
|
||||
upload_id: string;
|
||||
file_name: string;
|
||||
total_chunks: number;
|
||||
chunk_size: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface UploadSectionFile {
|
||||
fileType: string;
|
||||
allowedExtensions: string[];
|
||||
maxFileSize: number;
|
||||
maxParallelUploads?: number;
|
||||
}
|
||||
|
||||
export default function UploadSectionFile({
|
||||
fileType,
|
||||
allowedExtensions,
|
||||
maxFileSize,
|
||||
maxParallelUploads = 3
|
||||
}: UploadSectionFile) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeUploads, setActiveUploads] = useState<Set<string>>(new Set()); // ID активных загрузок
|
||||
const queryClient = useQueryClient();
|
||||
const isCancelledRef = useRef<Map<string, boolean>>(new Map()); // Для отслеживания отмен по файлам
|
||||
const selectedFilesRef = useRef<SelectedFile[]>(selectedFiles);
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const acceptString = useMemo(() => {
|
||||
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
|
||||
return '';
|
||||
}
|
||||
return allowedExtensions.map(e => `.${e}`).join(', ');
|
||||
}, [allowedExtensions]);
|
||||
|
||||
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
|
||||
if (!allowedExtensions.includes(file.type as string)) {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!extension || !allowedExtensions.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 getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
img.onload = () => {
|
||||
const dimensions = {
|
||||
width: img.width,
|
||||
height: img.height
|
||||
};
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
resolve(dimensions);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
reject(new Error(t('error-uploading-file')));
|
||||
};
|
||||
|
||||
img.src = objectUrl;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFilesSelect = useCallback(async (files: File[]): Promise<void> => {
|
||||
setError(null);
|
||||
|
||||
const newFiles: SelectedFile[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const validation = validateFile(file);
|
||||
|
||||
if (!validation.isValid) {
|
||||
setError(prev => prev ? `${prev}, ${validation.errorMessage}` : validation.errorMessage || t('unknown-validation-error'));
|
||||
continue;
|
||||
}
|
||||
|
||||
let dimensions: {
|
||||
width: number,
|
||||
height: number,
|
||||
} | undefined = undefined;
|
||||
if (fileType === "image") {
|
||||
try {
|
||||
dimensions = await getImageDimensions(file) as { width: number, height: number };
|
||||
if (dimensions?.width < 100 || dimensions?.height < 100 ||
|
||||
dimensions?.width > 10000 || dimensions?.height > 10000) {
|
||||
setError(prev => prev ? `${prev}, ${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.` :
|
||||
`${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(prev => prev ? `${prev}, ${t('error-reading-image')}` : t('error-reading-image'));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
newFiles.push({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
dimensions,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined,
|
||||
progress: 0,
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||
}, [fileType, t]);
|
||||
|
||||
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
handleFilesSelect(files);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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 files = Array.from(event.dataTransfer.files);
|
||||
handleFilesSelect(files);
|
||||
};
|
||||
|
||||
const handleClearFile = async (fileId?: string): Promise<void> => {
|
||||
if (fileId) {
|
||||
const fileToClear = selectedFiles.find(f => f.uploadId === fileId || f.name === fileId);
|
||||
if (fileToClear?.preview) {
|
||||
URL.revokeObjectURL(fileToClear.preview);
|
||||
}
|
||||
|
||||
if (fileToClear?.uploadId) {
|
||||
await cancelUpload(fileToClear.uploadId);
|
||||
isCancelledRef.current.set(fileToClear.uploadId, true);
|
||||
setActiveUploads(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(fileToClear.uploadId!);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => prev.filter(f =>
|
||||
f.uploadId !== fileId && f.name !== fileId
|
||||
));
|
||||
} else {
|
||||
selectedFiles.forEach(file => {
|
||||
if (file.preview) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
});
|
||||
|
||||
const cancelPromises = Array.from(activeUploads).map(uploadId =>
|
||||
cancelUpload(uploadId)
|
||||
);
|
||||
await Promise.allSettled(cancelPromises);
|
||||
|
||||
isCancelledRef.current.clear();
|
||||
setActiveUploads(new Set());
|
||||
setSelectedFiles([]);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const uploadSingleFile = async (fileInfo: SelectedFile): Promise<void> => {
|
||||
const fileId = `${fileInfo.name}-${Date.now()}`;
|
||||
isCancelledRef.current.set(fileId, false);
|
||||
|
||||
try {
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'uploading' } : f
|
||||
));
|
||||
|
||||
const extension = fileInfo.file.name.split('.').pop() || '';
|
||||
const initMessageBody = {
|
||||
file_name: fileInfo.file.name,
|
||||
file_type: fileType,
|
||||
extension: extension,
|
||||
file_size: fileInfo.file.size
|
||||
};
|
||||
|
||||
const response = await fileUpload(initMessageBody, fileInfo.convertTo) as FileUploadInitResponse;
|
||||
|
||||
if (!response?.upload_id) {
|
||||
throw new Error('Failed to get upload_id');
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, uploadId: response.upload_id } : f
|
||||
));
|
||||
|
||||
setActiveUploads(prev => new Set(prev).add(response.upload_id));
|
||||
|
||||
const CHUNK_SIZE = response.chunk_size;
|
||||
const totalChunks = response.total_chunks;
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
if (isCancelledRef.current.get(fileId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = chunkIndex * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, fileInfo.file.size);
|
||||
const chunk = fileInfo.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);
|
||||
|
||||
const chunkResponse = await chunkUpload(formData);
|
||||
|
||||
if (chunkResponse.errorMesage !== null) {
|
||||
throw chunkResponse.errorMesage;
|
||||
}
|
||||
|
||||
const progress = Math.floor((chunkIndex + 1) / totalChunks * 100);
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, progress } : f
|
||||
));
|
||||
}
|
||||
|
||||
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'completed', progress: 100 } : f
|
||||
));
|
||||
|
||||
setActiveUploads(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(response.upload_id);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userData'] });
|
||||
} else {
|
||||
throw new Error('Not all chunks were uploaded');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current.get(fileId)) {
|
||||
const errorMessage = (() => {
|
||||
switch (error) {
|
||||
case 'duplicate-file':
|
||||
return t('error-duplicate-file');
|
||||
case 'user-not-have-tokens-for-protect':
|
||||
return t('error-user-not-have-tokens-for-protect');
|
||||
default:
|
||||
return t('error-uploading-file');
|
||||
}
|
||||
})();
|
||||
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'error', error: errorMessage } : f
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
selectedFilesRef.current = selectedFiles;
|
||||
}, [selectedFiles]);
|
||||
|
||||
const handleStartAllUploads = useCallback(async (): Promise<void> => {
|
||||
const uploadNextBatch = async () => {
|
||||
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
|
||||
const batch = currentPending.slice(0, maxParallelUploads);
|
||||
|
||||
if (batch.length === 0) return;
|
||||
|
||||
await Promise.all(batch.map(file => uploadSingleFile(file)));
|
||||
|
||||
if (selectedFilesRef.current.some(f => f.status === 'pending')) {
|
||||
await uploadNextBatch();
|
||||
}
|
||||
};
|
||||
|
||||
await uploadNextBatch();
|
||||
}, [maxParallelUploads]);
|
||||
|
||||
const getTotalProgress = useMemo(() => {
|
||||
if (selectedFiles.length === 0) return 0;
|
||||
const total = selectedFiles.reduce((sum, file) => sum + file.progress, 0);
|
||||
return Math.floor(total / selectedFiles.length);
|
||||
}, [selectedFiles]);
|
||||
|
||||
const completedCount = useMemo(() =>
|
||||
selectedFiles.filter(f => f.status === 'completed').length,
|
||||
[selectedFiles]
|
||||
);
|
||||
|
||||
const uploadingCount = useMemo(() =>
|
||||
selectedFiles.filter(f => f.status === 'uploading').length,
|
||||
[selectedFiles]
|
||||
);
|
||||
|
||||
|
||||
/* useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
||||
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
||||
// но у него есть свои особенности и стандартные запросы не работают.
|
||||
const handleUnload = () => {
|
||||
if (activeUploads.size) {
|
||||
//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 (activeUploads.size) {
|
||||
window.addEventListener('unload', handleUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unload', handleUnload);
|
||||
};
|
||||
}, [activeUploads.size]); */
|
||||
|
||||
useNavigationBlocker({
|
||||
shouldBlock: !!activeUploads.size,
|
||||
message: t('have-unsaved-changes'),
|
||||
onConfirm: async () => {
|
||||
console.log('User confirmed navigation');
|
||||
if (activeUploads.size) {
|
||||
await handleClearFile();
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('User cancelled navigation');
|
||||
}
|
||||
});
|
||||
|
||||
function converButtonHandler(value: {
|
||||
convertTo: string;
|
||||
file: SelectedFile
|
||||
}): void {
|
||||
setSelectedFiles(selectedFiles.map(selectedFile => {
|
||||
if (selectedFile.name === value.file.name) {
|
||||
return {
|
||||
...selectedFile,
|
||||
convertTo: value.convertTo
|
||||
};
|
||||
}
|
||||
return selectedFile;
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="upload-section" >
|
||||
<h3>
|
||||
{t('upload-for-protection', {
|
||||
fileType: t(fileType.toLocaleLowerCase())
|
||||
})}
|
||||
</h3>
|
||||
|
||||
< div
|
||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<IconShieldAdd />
|
||||
<h4>
|
||||
{t('drag-the-file-here')}
|
||||
</h4>
|
||||
|
||||
{
|
||||
fileType === "image" && (
|
||||
<p className="description" >
|
||||
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<p className="description" >
|
||||
{t('file-format')}: {acceptString}
|
||||
</p>
|
||||
< p className="description" >
|
||||
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
|
||||
</p>
|
||||
|
||||
< input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={acceptString}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
aria-label="Выбор файла для защиты"
|
||||
multiple // Добавляем multiple для выбора нескольких файлов
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
type="button"
|
||||
disabled={uploadingCount > maxParallelUploads}
|
||||
>
|
||||
{t('select-files-to-protect')}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
selectedFiles.length > 0 && (
|
||||
<div className="mt-6" >
|
||||
<div className="selected-file-action-panel" >
|
||||
<div>
|
||||
<p className="selected-file-action-info" >
|
||||
<span>{t('total-files')}: {selectedFiles.length}</span>
|
||||
<span>{t('uploading')}: {uploadingCount}</span>
|
||||
<span>{t('completed')}: {completedCount}</span>
|
||||
</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${getTotalProgress}%` }
|
||||
}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`btn btn-primary`}
|
||||
onClick={handleStartAllUploads}
|
||||
type="button"
|
||||
disabled={uploadingCount > 0 || selectedFiles.every(f => f.status !== 'pending')}
|
||||
>
|
||||
{t('upload-all')}
|
||||
</button>
|
||||
< button
|
||||
className="btn btn-primary"
|
||||
onClick={() => handleClearFile()}
|
||||
type="button"
|
||||
>
|
||||
{t('clear-all')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="selected-file-wrapper">
|
||||
{
|
||||
selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={`${file.name}-${index}`}
|
||||
className={`selected-file ${file.status === 'completed' ? 'done' : ''} ${file.status === 'error' ? 'error' : ''}`}
|
||||
>
|
||||
<div className="selected-file-file-info">
|
||||
<div>
|
||||
<p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('file')}: </span> {file.name}
|
||||
</p>
|
||||
< p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('size')}: </span> {file.size}
|
||||
</p>
|
||||
< p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('status')}: </span>
|
||||
<span className={`ml-2 ${file.status === 'completed' ? 'text-green-600' :
|
||||
file.status === 'uploading' ? 'text-blue-600' :
|
||||
file.status === 'error' ? 'text-red-600' :
|
||||
'text-gray-600'
|
||||
}`}>
|
||||
{
|
||||
file.status === 'completed' ? t('completed') :
|
||||
file.status === 'uploading' ? t('uploading') :
|
||||
file.status === 'error' ? t('error') :
|
||||
t('pending')
|
||||
}
|
||||
</span>
|
||||
</p>
|
||||
{
|
||||
file.error && (
|
||||
<p className="text-red-600 text-sm mt-1" > {file.error} </p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="selected-file-right-block">
|
||||
{
|
||||
(fileType === 'image' && file.status === 'pending') && (
|
||||
<>
|
||||
<DropDownListForImageConvert
|
||||
value={file.convertTo}
|
||||
translatedValue={file.convertTo === 'reject' ? t('do-not-convert') : file.convertTo}
|
||||
callBack={converButtonHandler}
|
||||
selectedFiles={selectedFiles}
|
||||
>
|
||||
<li data-file={file.name} data-convert-to="reject">
|
||||
{t('do-not-convert')}
|
||||
</li>
|
||||
{
|
||||
allowedExtensions && allowedExtensions.map(extension => {
|
||||
if (extension === 'gif') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={extension} data-file={file.name} data-convert-to={extension}>
|
||||
{t('convert-to')} {extension}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</DropDownListForImageConvert>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<span className={
|
||||
`upload-progress ${file.status === 'completed' ? 'text-green-600' :
|
||||
file.status === 'uploading' ? 'text-blue-600' :
|
||||
'text-gray-600'
|
||||
}`
|
||||
}>
|
||||
{file.progress} %
|
||||
</span>
|
||||
< button
|
||||
className="btn btn-cancel"
|
||||
onClick={() => handleClearFile(file.uploadId || file.name)}
|
||||
type="button"
|
||||
>
|
||||
{t('remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
(fileType === 'image' && file.preview) && (
|
||||
<div
|
||||
className="image-preview-wrapper"
|
||||
>
|
||||
<img
|
||||
src={file.preview}
|
||||
alt="Предпросмотр загруженного изображения"
|
||||
className="uploaded-image"
|
||||
onError={() => setError('Не удалось загрузить превью изображения')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
file.status === 'pending' && (
|
||||
<div className="mt-2 text-center" >
|
||||
<button
|
||||
className="btn btn-confirm"
|
||||
onClick={() => uploadSingleFile(file)
|
||||
}
|
||||
type="button"
|
||||
disabled={uploadingCount >= maxParallelUploads}
|
||||
>
|
||||
{t('start-upload')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
|
||||
export interface Tariff {
|
||||
id: number;
|
||||
type: string;
|
||||
name: string;
|
||||
price: number;
|
||||
tokens: number;
|
||||
maxFilesCount: number;
|
||||
diskSize: number;
|
||||
maxUsers?: number;
|
||||
}
|
||||
|
||||
export interface TariffInfo {
|
||||
id: string;
|
||||
status: string;
|
||||
startTariff: string;
|
||||
endTariff: string;
|
||||
tariffId: number | null;
|
||||
tariffName: string | null;
|
||||
tokens: number | null;
|
||||
maxFileOnDisk: number | null;
|
||||
currentFileOnDisk: number;
|
||||
currentFileCounts: number;
|
||||
maxFileCounts: number;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
fullName: string;
|
||||
company: string | null;
|
||||
email: string;
|
||||
active: boolean;
|
||||
phone: string;
|
||||
genderType: string | null;
|
||||
birthday: string | null;
|
||||
createdAt: string;
|
||||
subscriptionType: string;
|
||||
tariffs: Tariff[];
|
||||
tariffInfo: TariffInfo;
|
||||
permission: number;
|
||||
}
|
||||
|
||||
export const useUserProfile = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userData'],
|
||||
queryFn: getUserData,
|
||||
select: (data): UserData => {
|
||||
if (!data) {
|
||||
return {
|
||||
fullName: '',
|
||||
company: null,
|
||||
email: '',
|
||||
active: false,
|
||||
phone: '',
|
||||
genderType: '',
|
||||
birthday: null,
|
||||
createdAt: '',
|
||||
subscriptionType: '',
|
||||
tariffs: [],
|
||||
tariffInfo: {
|
||||
id: '',
|
||||
status: '',
|
||||
startTariff: '',
|
||||
endTariff: '',
|
||||
tariffId: 0,
|
||||
tariffName: null,
|
||||
tokens: 0,
|
||||
maxFileOnDisk: null,
|
||||
currentFileOnDisk: 0,
|
||||
currentFileCounts: 0,
|
||||
maxFileCounts: 0,
|
||||
},
|
||||
permission: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
|
||||
export interface FilesInfo {
|
||||
total: {
|
||||
size: number;
|
||||
quantity: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
images: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
videos: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
audios: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
documents: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const useUserFilesInfo = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo | null => {
|
||||
if (!data) {
|
||||
return {
|
||||
total: { size: 0, quantity: 0, check: 0, violation: 0, protected: 0 },
|
||||
images: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
videos: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
audios: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
documents: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
total: {
|
||||
size: data.all_files_size,
|
||||
quantity: data.all_files_quantity,
|
||||
check: data.all_files_check,
|
||||
violation: data.all_files_violation,
|
||||
protected: data.protected_files_count,
|
||||
},
|
||||
images: {
|
||||
quantity: data.images_quantity,
|
||||
size: data.images_size,
|
||||
check: data.images_check,
|
||||
violation: data.images_violations,
|
||||
protected: data.protected_image_files_count,
|
||||
},
|
||||
videos: {
|
||||
quantity: data.videos_quantity,
|
||||
size: data.videos_size,
|
||||
check: data.videos_check,
|
||||
violation: data.videos_violations,
|
||||
protected: data.protected_video_files_count,
|
||||
},
|
||||
audios: {
|
||||
quantity: data.audios_quantity,
|
||||
size: data.audios_size,
|
||||
check: data.audios_check,
|
||||
violation: data.audios_violations,
|
||||
protected: data.protected_audio_files_count,
|
||||
},
|
||||
documents: {
|
||||
quantity: data.document_quantity,
|
||||
size: data.document_size,
|
||||
check: data.document_check,
|
||||
violation: data.document_violations,
|
||||
protected: data.protected_document_files_count,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getSearchStats } from '@/app/actions/searchActions';
|
||||
|
||||
interface SearchType {
|
||||
count: number,
|
||||
fileType: string,
|
||||
}
|
||||
|
||||
export interface UserSearchData {
|
||||
allCountForPeriod: number;
|
||||
image: SearchType;
|
||||
video: SearchType;
|
||||
audio: SearchType;
|
||||
document: SearchType;
|
||||
}
|
||||
|
||||
export const useUserSearchData = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userSearchData'],
|
||||
queryFn: getSearchStats,
|
||||
select: (data): UserSearchData | null => {
|
||||
if (!data) {
|
||||
return {
|
||||
allCountForPeriod: 0,
|
||||
image: {
|
||||
count: 0,
|
||||
fileType: 'image'
|
||||
},
|
||||
video: {
|
||||
count: 0,
|
||||
fileType: 'video'
|
||||
},
|
||||
audio: {
|
||||
count: 0,
|
||||
fileType: 'audio'
|
||||
},
|
||||
document: {
|
||||
count: 0,
|
||||
fileType: 'document'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,8 @@
|
||||
export function convertBytes(bytes: number) {
|
||||
if (!bytes) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export function getProcents(
|
||||
currentNumber: number | undefined,
|
||||
totalNumber: number | undefined,
|
||||
decimals: number = 0
|
||||
): number {
|
||||
if (!currentNumber && !totalNumber) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (totalNumber === 0 || typeof currentNumber === 'undefined' || typeof totalNumber === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const percentage = (currentNumber / totalNumber) * 100;
|
||||
const factor = Math.pow(10, decimals);
|
||||
|
||||
return Math.round(percentage * factor) / factor;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { getUserData, getUserFilesInfo } from '@/app/actions/action';
|
||||
import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action';
|
||||
import { getUserFilesData } from '@/app/actions/fileEntity';
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
|
||||
export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||
await Promise.all([
|
||||
@@ -19,5 +20,20 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: () => getUserFilesInfo()
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => fetchReferralUserStats()
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['backendBuildDate'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await getBuildData();
|
||||
return response.buildTimeBack ?? null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -19,7 +19,7 @@ interface ValidationConfig {
|
||||
|
||||
/* const EMAIL_REGEX = /^([a-z0-9.\-_]+|[а-яё0-9.\-_]+)@(([a-z0-9.\-]+)\.([a-z]{2,})|([a-z0-9.\-]+)\.([а-яё]{2,})|([а-яё0-9.\-]+)\.([a-z]{2,})|([а-яё0-9.\-]+)\.([а-яё]{2,}))$/i; */
|
||||
|
||||
export function createValidationSchema(config: ValidationConfig) {
|
||||
export function createValidationSchema(config: ValidationConfig, accountType?: 'b2b' | 'b2c') {
|
||||
const schema: Record<string, any> = {};
|
||||
|
||||
// fullName
|
||||
@@ -38,6 +38,22 @@ export function createValidationSchema(config: ValidationConfig) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
// fullName
|
||||
if (config.companyName && accountType !== 'b2c') {
|
||||
schema.companyName = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(config.companyName.minSize?.value || 1, {
|
||||
error: config.companyName.minSize?.message || 'register-error-company-name-min'
|
||||
})
|
||||
.max(config.companyName.maxSize?.value || 100, {
|
||||
error: config.companyName.maxSize?.message || 'register-error-company-name-max'
|
||||
})
|
||||
.regex(new RegExp(config.companyName.allowedSymbols?.value || '.*'), {
|
||||
message: config.companyName.allowedSymbols?.message || 'register-error-company-name-not-allowed-symbols'
|
||||
});
|
||||
}
|
||||
|
||||
// email
|
||||
if (config.email) {
|
||||
let emailSchema = z
|
||||
@@ -147,79 +163,92 @@ export function createValidationSchema(config: ValidationConfig) {
|
||||
schema.password = passwordSchema;
|
||||
}
|
||||
|
||||
// companyName (опциональное поле)
|
||||
if (config.companyName) {
|
||||
let companySchema = z.string();
|
||||
// inn
|
||||
if (config.inn && accountType !== 'b2c') {
|
||||
let innSchema = z.string();
|
||||
|
||||
// Если поле опциональное, добавляем .optional().nullable()
|
||||
if (config.companyName.optional?.value) {
|
||||
companySchema = companySchema
|
||||
.optional()
|
||||
.nullable()
|
||||
.transform((val) => (val === null || val === undefined ? '' : val))
|
||||
.refine(
|
||||
(value) => {
|
||||
// Для пустого значения пропускаем проверки
|
||||
if (!value || value.trim().length === 0) return true;
|
||||
|
||||
// Проверка минимальной длины
|
||||
const minLength = config.companyName?.minSize?.value || 2;
|
||||
return value.trim().length >= minLength;
|
||||
},
|
||||
{
|
||||
message: config.companyName.minSize?.message || 'register-error-company-name-min'
|
||||
}
|
||||
) as unknown as z.ZodString;
|
||||
} else {
|
||||
// Если поле обязательное
|
||||
if (config.companyName.minSize) {
|
||||
companySchema = companySchema.min(config.companyName.minSize.value, {
|
||||
error: config.companyName.minSize.message
|
||||
});
|
||||
}
|
||||
// Минимальная длина
|
||||
if (config.inn.minSize) {
|
||||
innSchema = innSchema.min(config.inn.minSize.value, {
|
||||
error: config.inn.minSize.message
|
||||
});
|
||||
}
|
||||
|
||||
// Максимальная длина (применяется всегда)
|
||||
if (config.companyName.maxSize) {
|
||||
companySchema = companySchema.refine(
|
||||
(value) => {
|
||||
if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) {
|
||||
return true;
|
||||
}
|
||||
const maxLength = config.companyName?.maxSize?.value || 200;
|
||||
return (value || '').trim().length <= maxLength;
|
||||
},
|
||||
{
|
||||
message: config.companyName.maxSize.message
|
||||
}
|
||||
);
|
||||
// Максимальная длина
|
||||
if (config.inn.maxSize) {
|
||||
innSchema = innSchema.max(config.inn.maxSize.value, {
|
||||
error: config.inn.maxSize.message
|
||||
});
|
||||
}
|
||||
|
||||
// Допустимые символы
|
||||
if (config.companyName.allowedSymbols?.value) {
|
||||
companySchema = companySchema.refine(
|
||||
(value) => {
|
||||
if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) {
|
||||
return true;
|
||||
}
|
||||
const regex = new RegExp(config.companyName.allowedSymbols!.value);
|
||||
return regex.test(value || '');
|
||||
},
|
||||
{
|
||||
message: config.companyName.allowedSymbols.message
|
||||
}
|
||||
if (config.inn.allowedSymbols) {
|
||||
innSchema = innSchema.regex(
|
||||
new RegExp(config.inn.allowedSymbols.value),
|
||||
{ message: config.inn.allowedSymbols.message }
|
||||
);
|
||||
}
|
||||
|
||||
companySchema = companySchema.transform((val) => val?.trim() || '') as unknown as z.ZodString;
|
||||
|
||||
schema.companyName = companySchema;
|
||||
schema.inn = innSchema;
|
||||
}
|
||||
|
||||
// Дополнительные поля для формы регистрации
|
||||
schema.confirm_password = z.string();
|
||||
schema.agree = z.string().min(2, { error: 'register-error-agree' });
|
||||
|
||||
return z
|
||||
.object(schema)
|
||||
.refine((data) => data.password === data.confirm_password, {
|
||||
message: 'repeat-password',
|
||||
path: ['confirm_password'],
|
||||
});
|
||||
}
|
||||
|
||||
export function createValidationSchemaForPassword(config: ValidationConfig) {
|
||||
const schema: Record<string, any> = {};
|
||||
|
||||
// password
|
||||
if (config.password) {
|
||||
let passwordSchema = z
|
||||
.string()
|
||||
.min(config.password.minSize?.value || 8, {
|
||||
error: config.password.minSize?.message || 'register-error-password-symbols'
|
||||
})
|
||||
.max(config.password.maxSize?.value || 124, {
|
||||
error: config.password.maxSize?.message || 'register-error-max-password'
|
||||
})
|
||||
.trim();
|
||||
|
||||
// Проверка допустимых символов
|
||||
if (config.password.allowedSymbols?.value) {
|
||||
passwordSchema = passwordSchema.regex(
|
||||
new RegExp(config.password.allowedSymbols.value),
|
||||
{ message: config.password.allowedSymbols.message }
|
||||
);
|
||||
}
|
||||
|
||||
// Проверка наличия цифры
|
||||
if (config.password.requiredDigit?.value) {
|
||||
passwordSchema = passwordSchema.regex(
|
||||
/[0-9]/,
|
||||
{ message: config.password.requiredDigit.message }
|
||||
);
|
||||
}
|
||||
|
||||
// Проверка наличия буквы
|
||||
if (config.password.requiredLetter?.value) {
|
||||
passwordSchema = passwordSchema.regex(
|
||||
/[a-zA-Zа-яёА-ЯЁ]/,
|
||||
{ message: config.password.requiredLetter.message }
|
||||
);
|
||||
}
|
||||
|
||||
schema.password = passwordSchema;
|
||||
}
|
||||
|
||||
// Дополнительные поля для формы регистрации
|
||||
schema.confirm_password = z.string();
|
||||
|
||||
return z
|
||||
.object(schema)
|
||||
.refine((data) => data.password === data.confirm_password, {
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"message": "register-error-company-name-min"
|
||||
},
|
||||
"allowedSymbols": {
|
||||
"value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]+$",
|
||||
"value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]*$",
|
||||
"message": "register-error-company-name-not-allowed-symbols"
|
||||
}
|
||||
},
|
||||
@@ -80,12 +80,26 @@
|
||||
"message": "register-error-phone-max"
|
||||
},
|
||||
"minSize": {
|
||||
"value": 2,
|
||||
"value": 12,
|
||||
"message": "register-error-phone-min"
|
||||
},
|
||||
"allowedSymbols": {
|
||||
"value": "^[0-9\\+]+$",
|
||||
"message": "register-error-phone-not-allowed-symbols"
|
||||
}
|
||||
},
|
||||
"inn": {
|
||||
"maxSize": {
|
||||
"value": 10,
|
||||
"message": "register-error-inn-max"
|
||||
},
|
||||
"minSize": {
|
||||
"value": 10,
|
||||
"message": "register-error-inn-min"
|
||||
},
|
||||
"allowedSymbols": {
|
||||
"value": "^[0-9]+$",
|
||||
"message": "register-error-inn-not-allowed-symbols"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,10 @@
|
||||
color: v.$white;
|
||||
max-width: calc(100vw - var(--side-bar-width) - 60px);
|
||||
|
||||
@media (max-width: 440px) {
|
||||
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
@@ -73,6 +77,10 @@
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
align-items: center;
|
||||
|
||||
@media (max-width: 440px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.header-stat-item {
|
||||
@@ -101,6 +109,10 @@
|
||||
padding: 30px;
|
||||
background: v.$bg-light;
|
||||
min-height: 100vh;
|
||||
|
||||
@media (max-width: 440px) {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-containter {
|
||||
@@ -121,6 +133,10 @@
|
||||
&.auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -142,7 +158,12 @@
|
||||
color: v.$white;
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:hover {
|
||||
&:disabled {
|
||||
background: linear-gradient(135deg, v.$p-color-disabled, v.$s-color-disabled);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px v.$shadow-1;
|
||||
}
|
||||
@@ -244,6 +265,11 @@
|
||||
color: v.$text-p;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0px 4px 20px v.$shadow-1;
|
||||
|
||||
&:has(.modal-window-view-file) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +306,13 @@
|
||||
width: var(--side-bar-width);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.build-versions {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
color: #fffc;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-close-button {
|
||||
@@ -324,7 +357,7 @@
|
||||
}
|
||||
|
||||
.sub-link {
|
||||
padding: 12px 50px;
|
||||
padding: 12px 12px 12px 32px;
|
||||
font-size: 14px;
|
||||
border-radius: 0;
|
||||
margin-right: 0;
|
||||
@@ -428,6 +461,13 @@
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&-document {
|
||||
color: v.$white;
|
||||
background: v.$color-document;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.split-blocks {
|
||||
|
||||
@@ -14,11 +14,19 @@
|
||||
padding: 20px 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
.header-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
|
||||
@media (max-width: 440px) {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -36,6 +44,11 @@
|
||||
background: v.$bg-hover;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +81,20 @@
|
||||
@media (max-width: 490px) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.tokens-count {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
@media (max-width: 440px) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tokens-label {
|
||||
@@ -192,6 +213,11 @@
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
|
||||
@media (max-width: 440px) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
|
||||
@@ -41,8 +41,35 @@
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
&-confrim-window {
|
||||
.form-switcher {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&-button {
|
||||
padding: 5px 15px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +101,10 @@
|
||||
transition: all 0.3s;
|
||||
background: #f9fafb;
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&[type="password"]::-ms-reveal {
|
||||
display: none;
|
||||
}
|
||||
@@ -141,11 +172,12 @@
|
||||
margin-bottom: 18px;
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
background: linear-gradient(135deg, #414291, #4b3975);
|
||||
|
||||
&:hover {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,13 +34,15 @@
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.6rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@@ -214,6 +216,7 @@
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
max-width: 350px;
|
||||
|
||||
.plan-header {
|
||||
text-align: center;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
|
||||
@@ -12,6 +12,8 @@ $bg-darkening: #0000007a;
|
||||
$shadow-1: #0000001a;
|
||||
$white: #fff;
|
||||
$red: #dc2626;
|
||||
$p-color-disabled: #6365f18e;
|
||||
$s-color-disabled: #8a5cf663;
|
||||
|
||||
$color-image: #f08c00;
|
||||
$color-video: #2f9e44;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import TanstakFilesTable from '@/app/components/tanstakTable';
|
||||
import TanstakFilesTable from '@/app/components/TanstakTable';
|
||||
|
||||
type FilesTable = {
|
||||
fileType: string
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function DashboardFilesInfo() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-files-info">
|
||||
@@ -24,20 +14,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('images')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.images_quantity ? filesInfo?.images_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.images.quantity ? filesInfo?.images.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-image')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images_check ? filesInfo?.images_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images_violations ? filesInfo?.images_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.violation ? filesInfo?.images.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,20 +36,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('videos')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.videos_quantity ? filesInfo?.videos_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.videos.quantity ? filesInfo?.videos.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-videos')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos_check ? filesInfo?.videos_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos_violations ? filesInfo?.videos_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.violation ? filesInfo?.videos.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,20 +58,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('audios')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.audios_quantity ? filesInfo?.audios_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.audios.quantity ? filesInfo?.audios.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-audios')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios_check ? filesInfo?.audios_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios_violations ? filesInfo?.audios_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.violation ? filesInfo?.audios.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -90,20 +80,20 @@ export default function DashboardFilesInfo() {
|
||||
<div className="stat-type">
|
||||
{t('documents')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.documents_quantity ? filesInfo?.documents_quantity : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.documents.quantity ? filesInfo?.documents.quantity : 0}</div>
|
||||
<div className="stat-label">{t('your-documents')}</div>
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents_check ? convertBytes(filesInfo?.documents_check) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.check? convertBytes(filesInfo?.documents.check) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('checks')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents_check ? filesInfo?.documents_check : 0}</span>
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents_violations ? filesInfo?.documents_violations : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.violation ? filesInfo?.documents.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function DashboardLastCheck() {
|
||||
return (
|
||||
<div className="dashboard-last-check">
|
||||
@@ -7,12 +9,18 @@ export default function DashboardLastCheck() {
|
||||
17.10.2025 в 10:23</div>
|
||||
</div>
|
||||
<div className="last-check-actions">
|
||||
<button className="btn-report">
|
||||
<Link
|
||||
className="btn-report"
|
||||
href={'reports'}
|
||||
>
|
||||
📄 Отчёт
|
||||
</button>
|
||||
<button className="btn-new-search">
|
||||
</Link>
|
||||
<Link
|
||||
className="btn-new-search"
|
||||
href={'violations'}
|
||||
>
|
||||
🔍 Мониторинг нарушений
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,48 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesData } from '@/app/actions/fileEntity';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import Link from 'next/link';
|
||||
|
||||
type FileItem = {
|
||||
id: string;
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
size?: number | undefined;
|
||||
uploadDate?: number;
|
||||
protectStatus: string;
|
||||
supportId: number;
|
||||
};
|
||||
|
||||
type ApiFile = {
|
||||
id: string;
|
||||
originalFileName: string;
|
||||
mimeType: string;
|
||||
fileSize: number;
|
||||
updatedAt: string;
|
||||
protectStatus: string;
|
||||
supportId: number;
|
||||
};
|
||||
|
||||
type ApiResponse = {
|
||||
files?: ApiFile[];
|
||||
};
|
||||
|
||||
export default function DashboardUserFiles() {
|
||||
const {
|
||||
data: tableData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({
|
||||
queryKey: ['userFilesData', 1, 5],
|
||||
queryFn: () => getUserFilesData(1, 5),
|
||||
|
||||
select: (data: ApiResponse): FileItem[] => {
|
||||
if (!data?.files) return [];
|
||||
|
||||
return data.files.map((item: ApiFile) => {
|
||||
const newDate = new Date(item.updatedAt).getTime();
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
fileName: item.originalFileName,
|
||||
fileType: item.mimeType.toLocaleLowerCase(),
|
||||
size: item.fileSize,
|
||||
uploadDate: newDate,
|
||||
protectStatus: item.protectStatus,
|
||||
supportId: item.supportId,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
const [openDropDownList, setOpenDropDownList] = useState(false);
|
||||
const dropDownList = useRef(null);
|
||||
useClickOutside(
|
||||
dropDownList,
|
||||
() => {
|
||||
setOpenDropDownList(false)
|
||||
},
|
||||
openDropDownList
|
||||
);
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">Ваши файлы</h3>
|
||||
<a href="pages/marking-photo.php" className="view-all-link">Загрузить файл →</a>
|
||||
<div
|
||||
className="section-add-file btn view-all-link"
|
||||
onClick={() => {
|
||||
setOpenDropDownList(!openDropDownList);
|
||||
}}
|
||||
ref={dropDownList}
|
||||
>
|
||||
{t('add')}
|
||||
<div
|
||||
className={`section-drop-down-list ${openDropDownList ? 'opened' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col text-center"
|
||||
>
|
||||
<Link href='/pages/marking-images'>
|
||||
{t('photo-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-video'>
|
||||
{t('video-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-audio'>
|
||||
{t('audio-marking')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-files-list">
|
||||
<div className="file-item">
|
||||
<div className="file-icon video">
|
||||
🎬</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title="Запись экрана 2026-01-09 в 15.45.28.mov">
|
||||
Запись экрана 2026-01-09 в 15.45.28.mov</div>
|
||||
<div className="file-meta">
|
||||
11.3 МБ •
|
||||
11.01.2026</div>
|
||||
|
||||
{tableData?.length ? (
|
||||
tableData?.map((file) => {
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className="file-item"
|
||||
>
|
||||
<div className="file-icon">
|
||||
<FileTypeIcon type={file.fileType} />
|
||||
</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title={file.fileName}>
|
||||
{file.fileName}
|
||||
</div>
|
||||
<div className="file-meta">
|
||||
{convertBytes(file.size ? file.size : 0)} • {formatDate(file.uploadDate ? file.uploadDate : 0)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-badge protected">
|
||||
{file.protectStatus ? t(file.protectStatus) : t('error')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
<div className="file-badge protected">✓ Защищено</div>
|
||||
</div>
|
||||
<div className="file-item">
|
||||
<div className="file-icon video">
|
||||
🎬</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title="SOUNE_миграция.mov">
|
||||
SOUNE_миграция.mov</div>
|
||||
<div className="file-meta">
|
||||
17.1 МБ •
|
||||
11.01.2026</div>
|
||||
</div>
|
||||
<div className="file-badge protected">✓ Защищено</div>
|
||||
</div>
|
||||
<div className="file-item">
|
||||
<div className="file-icon video">
|
||||
🎬</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title="SOUNE_миграция.mov">
|
||||
SOUNE_миграция.mov</div>
|
||||
<div className="file-meta">
|
||||
17.1 МБ •
|
||||
11.01.2026</div>
|
||||
</div>
|
||||
<div className="file-badge protected">✓ Защищено</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function DashboardUserStats() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-user-stats">
|
||||
|
||||
<div className="stat-card projects">
|
||||
<div className="stat-type">ЗАЩИЩЁННЫЕ ФАЙЛЫ</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-value">{filesInfo?.total.protected ? filesInfo?.total.protected : 0}</div>
|
||||
<div className="stat-label">Файлов под защитой</div>
|
||||
<div className="stat-icon">🛡️</div>
|
||||
</div>
|
||||
@@ -37,7 +27,7 @@ export default function DashboardUserStats() {
|
||||
|
||||
<div className="stat-card violations">
|
||||
<div className="stat-type">НАРУШЕНИЯ</div>
|
||||
<div className="stat-value">{filesInfo?.all_files_violations ? filesInfo?.all_files_violations : 0}</div>
|
||||
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div>
|
||||
<div className="stat-label">Требуют внимания</div>
|
||||
<div className="stat-icon">⚠️</div>
|
||||
</div>
|
||||
@@ -45,7 +35,7 @@ export default function DashboardUserStats() {
|
||||
<div className="stat-card storage">
|
||||
<div className="stat-type">ХРАНИЛИЩЕ</div>
|
||||
<div className="stat-value">
|
||||
{filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}
|
||||
{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0}
|
||||
</div>
|
||||
<div className="stat-label">Использовано места</div>
|
||||
<div className="stat-icon">💾</div>
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { getProcents } from '@/app/lib/getProcents';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function DahboardLimitsSection() {
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-limits-section">
|
||||
<h3 className="section-title">💰 Использование лимитов</h3>
|
||||
@@ -9,13 +17,13 @@ export default function DahboardLimitsSection() {
|
||||
<span>🔍</span>
|
||||
<span>Проверки</span>
|
||||
</div>
|
||||
<div className="limit-value">520 / 10,000</div>
|
||||
<div className="limit-value">{filesInfo?.total.check ? filesInfo?.total.check : 0} / 10,000</div>
|
||||
</div>
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar low" style={{width: '44%'}}></div>
|
||||
<div className="progress-bar low" style={{ width: '0%' }}></div>
|
||||
</div>
|
||||
<div className="progress-bar-value">
|
||||
5%
|
||||
{getProcents(filesInfo?.total.check, 10000)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,13 +33,13 @@ export default function DahboardLimitsSection() {
|
||||
<span>💾</span>
|
||||
<span>Хранилище</span>
|
||||
</div>
|
||||
<div className="limit-value">790.2 / 1,024 МБ</div>
|
||||
<div className="limit-value">{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} / 1,024 МБ</div>
|
||||
</div>
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar medium" style={{width: '77%'}}></div>
|
||||
<div className="progress-bar medium" style={{ width: `${getProcents(filesInfo?.total.size, 1000000000)}%` }}></div>
|
||||
</div>
|
||||
<div className="progress-bar-value">
|
||||
77%
|
||||
{getProcents(filesInfo?.total.size, 1000000000)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="limit-item">
|
||||
@@ -40,13 +48,13 @@ export default function DahboardLimitsSection() {
|
||||
<span>📁</span>
|
||||
<span>Файлы</span>
|
||||
</div>
|
||||
<div className="limit-value">386 / 500</div>
|
||||
<div className="limit-value">{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0} / 500</div>
|
||||
</div>
|
||||
<div className="progress-bar-container">
|
||||
<div className="progress-bar medium" style={{width: '77%'}}></div>
|
||||
<div className="progress-bar medium" style={{ width: `${getProcents(filesInfo?.total.quantity, 500)}%` }}></div>
|
||||
</div>
|
||||
<div className="progress-bar-value">
|
||||
77%
|
||||
{getProcents(filesInfo?.total.quantity, 500)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,24 +7,33 @@ import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import testImage from '@/app/src/image-preview.png'
|
||||
import { useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ViolationsCarousel() {
|
||||
const paginationRef = useRef(null);
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">Нарушения</h3>
|
||||
<a href="pages/violations.php" className="view-all-link">Смотреть все →</a>
|
||||
<Link
|
||||
href="violations"
|
||||
className="view-all-link"
|
||||
>
|
||||
Смотреть все
|
||||
</Link>
|
||||
</div>
|
||||
<div className="violations-carousel">
|
||||
<div>
|
||||
{t('are-no-violations')}
|
||||
</div>
|
||||
{/* <div className="violations-carousel">
|
||||
<Swiper
|
||||
spaceBetween={50}
|
||||
slidesPerView={1}
|
||||
modules={[Pagination, Autoplay]}
|
||||
pagination={{
|
||||
/* dynamicBullets: true, */
|
||||
/* el: paginationRef.current, */
|
||||
clickable: true,
|
||||
el: '.custon-swiper-pagination'
|
||||
}}
|
||||
@@ -33,7 +42,6 @@ export default function ViolationsCarousel() {
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
/* width={300} */
|
||||
>
|
||||
<SwiperSlide>
|
||||
<div className="violation-slide">
|
||||
@@ -260,7 +268,7 @@ export default function ViolationsCarousel() {
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,23 +5,9 @@ import { PieChartComponent } from '@/app/components/PieChartComponent';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
|
||||
type FilesInfo = {
|
||||
totalSize: number;
|
||||
totalCount: number;
|
||||
imageQuantity: number;
|
||||
videoQuantity: number;
|
||||
audioQuantity: number;
|
||||
imageSize: number;
|
||||
videoSize: number;
|
||||
audioSize: number;
|
||||
documentSize: number;
|
||||
};
|
||||
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function ProtectionOverview() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -42,68 +28,23 @@ export default function ProtectionOverview() {
|
||||
openDropDownList
|
||||
);
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number,
|
||||
images_quantity: number,
|
||||
videos_quantity: number,
|
||||
audios_quantity: number,
|
||||
images_size: number,
|
||||
videos_size: number,
|
||||
audios_size: number,
|
||||
documents_size: number,
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0,
|
||||
imageQuantity: 0,
|
||||
videoQuantity: 0,
|
||||
audioQuantity: 0,
|
||||
imageSize: 0,
|
||||
videoSize: 0,
|
||||
audioSize: 0,
|
||||
documentSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize: data.all_files_size,
|
||||
totalCount: data.all_files_quantity,
|
||||
imageQuantity: data.images_quantity,
|
||||
videoQuantity: data.videos_quantity,
|
||||
audioQuantity: data.audios_quantity,
|
||||
imageSize: data.images_size,
|
||||
videoSize: data.videos_size,
|
||||
audioSize: data.audios_size,
|
||||
documentSize: data.documents_size,
|
||||
}
|
||||
},
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
useEffect(() => {
|
||||
setFilesCount([
|
||||
{
|
||||
name: 'images-few',
|
||||
value: filesInfo?.imageSize ? filesInfo?.imageSize : 0,
|
||||
value: filesInfo?.images.size ? filesInfo?.images.size : 0,
|
||||
color: '#f08c00'
|
||||
},
|
||||
{
|
||||
name: 'videos',
|
||||
value: filesInfo?.videoSize ? filesInfo?.videoSize : 0,
|
||||
value: filesInfo?.videos.size ? filesInfo?.videos.size : 0,
|
||||
color: '#2f9e44'
|
||||
},
|
||||
{
|
||||
name: 'audios',
|
||||
value: filesInfo?.audioSize ? filesInfo?.audioSize : 0,
|
||||
value: filesInfo?.audios.size ? filesInfo?.audios.size : 0,
|
||||
color: '#1971c2'
|
||||
},
|
||||
])
|
||||
@@ -134,14 +75,14 @@ export default function ProtectionOverview() {
|
||||
<div className={`protection-stats ${isOpen ? "opened" : ""}`}>
|
||||
|
||||
|
||||
{filesInfo?.totalCount ? (
|
||||
{filesInfo?.total.quantity ? (
|
||||
<>
|
||||
<div className="protection-stat total-files">
|
||||
<span className="protection-stat-value">
|
||||
{filesInfo?.totalCount ? filesInfo?.totalCount : 0}
|
||||
{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0}
|
||||
</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeFiles(filesInfo?.totalCount || 0)}
|
||||
{pluralizeFiles(filesInfo?.total.quantity || 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="protection-stat total-checks">
|
||||
@@ -180,7 +121,7 @@ export default function ProtectionOverview() {
|
||||
<div
|
||||
className="flex flex-col text-center"
|
||||
>
|
||||
<Link href='/pages/marking-photo'>
|
||||
<Link href='/pages/marking-images'>
|
||||
{t('photo-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-video'>
|
||||
@@ -196,18 +137,18 @@ export default function ProtectionOverview() {
|
||||
)}
|
||||
{isOpen ? (
|
||||
<div className="protection-stat total-usage">
|
||||
<PieChartComponent data={filesCount} show={filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} />
|
||||
<PieChartComponent data={filesCount} show={filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="protection-stat total-usage">
|
||||
<div className="protection-stat-value">{filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} / 0</div>
|
||||
<div className="protection-stat-value">{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} / 0</div>
|
||||
<div className="protection-stat-label">
|
||||
{t('disk-space-used')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!!filesInfo?.totalCount && (
|
||||
{!!filesInfo?.total.quantity && (
|
||||
<button
|
||||
className="protection-overview-switch cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
@@ -2,22 +2,12 @@
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function StatsGrid() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="block-wrapper stats-wrapper">
|
||||
@@ -46,10 +36,10 @@ export default function StatsGrid() {
|
||||
{t('images')}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
{filesInfo?.images_quantity ? filesInfo?.images_quantity : 0}
|
||||
{filesInfo?.images.quantity ? filesInfo?.images.quantity : 0}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}
|
||||
{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
@@ -63,10 +53,10 @@ export default function StatsGrid() {
|
||||
{t('videos')}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
{filesInfo?.videos_quantity ? filesInfo?.videos_quantity : 0}
|
||||
{filesInfo?.videos.quantity ? filesInfo?.videos.quantity : 0}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}
|
||||
{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
@@ -80,10 +70,10 @@ export default function StatsGrid() {
|
||||
{t('audios')}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
{filesInfo?.audios_quantity ? filesInfo?.audios_quantity : 0}
|
||||
{filesInfo?.audios.quantity ? filesInfo?.audios.quantity : 0}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}
|
||||
{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
|
||||
@@ -1,110 +1,5 @@
|
||||
/* deleted */
|
||||
|
||||
/* .violations-table {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f1f5f9;
|
||||
|
||||
.violation-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.violation-card {
|
||||
padding: 20px 25px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.violation-icon-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.photo {
|
||||
background: linear-gradient(135deg, #dbeafe, #bfdbfe);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.violation-content-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.violation-filename {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
font-size: 15px;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.violation-meta {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.violation-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.violation-status-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #fee2e2, #fecaca);
|
||||
color: #dc2626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.violation-sources-badge {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #fef3c7, #fde68a);
|
||||
color: #d97706;
|
||||
white-space: nowrap;
|
||||
}
|
||||
} */
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
import { useActionState } from 'react';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { resetPassword } from '@/app/actions/auth';
|
||||
|
||||
export default function ForgotPasswordForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
() => {
|
||||
console.log("test");
|
||||
},
|
||||
resetPassword,
|
||||
undefined,
|
||||
);
|
||||
const t = useTranslations('Login-register-form');
|
||||
@@ -27,26 +25,14 @@ export default function ForgotPasswordForm() {
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('enter-email')}
|
||||
/>
|
||||
{/* {state?.error?.email && (
|
||||
{state?.error === 'error' && (
|
||||
<p className={`${styles['form-error']}`}>
|
||||
{t(state?.error?.email)}
|
||||
{t('register-error-email-invalid')}
|
||||
</p>
|
||||
)} */}
|
||||
</div>
|
||||
{/* {state?.error.server && (
|
||||
<p className={`${styles['form-error']}`}>
|
||||
{t(state?.error.server)}
|
||||
</p>
|
||||
)} */}
|
||||
<div className={`${styles['form-options']}`}>
|
||||
<div className={`${styles['form-checkbox']}`}>
|
||||
<small>
|
||||
функционал не реализован
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('sign-in')}
|
||||
{t('recover-password')}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -96,8 +96,18 @@ export default function LoginForm() {
|
||||
{t('forgot-password')}?
|
||||
</a>
|
||||
</div>
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('sign-in')}
|
||||
<button
|
||||
type="submit"
|
||||
className={`${styles['btn']}`}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
) : (
|
||||
t("sign-in")
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
import { ChangeEvent, useActionState, useEffect, useRef, useState, MouseEvent } from 'react';
|
||||
import { ChangeEvent, useActionState, useEffect, useRef, useState } from 'react';
|
||||
import { registration, FormState } from '@/app/actions/auth';
|
||||
|
||||
|
||||
import PhoneInput from '@/app/ui/inputs/phone-input';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { IconEye } from '@/app/ui/icons/icons';
|
||||
import { SignupFormSchema } from '@/app/actions/definitions';
|
||||
import { getSignupFormSchema } from '@/app/actions/definitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import * as z from 'zod'
|
||||
import ConfirmMailModalWindow from '@/app/ui/forms/confirm-mail-modal-window';
|
||||
import { fetchINN } from '@/app/actions/action';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
|
||||
export default function RegisterForm() {
|
||||
const searchParams = useSearchParams();
|
||||
const referralCode = searchParams.get('ref');
|
||||
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
registration,
|
||||
undefined,
|
||||
@@ -23,6 +32,7 @@ export default function RegisterForm() {
|
||||
const [formState, setFormState] = useState<FormState | undefined>(undefined);
|
||||
const [showMailConfirmWindow, setShowMailConfirmWindow] = useState(false);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const [accountType, setAccountType] = useState<'b2c' | 'b2b'>('b2c');
|
||||
|
||||
useEffect(() => {
|
||||
setFormState(state);
|
||||
@@ -30,7 +40,40 @@ export default function RegisterForm() {
|
||||
setShowMailConfirmWindow(true);
|
||||
}
|
||||
|
||||
}, [state])
|
||||
}, [state]);
|
||||
|
||||
const debouncedFetchINN = useDebouncedCallback(
|
||||
async (value) => {
|
||||
const response = await fetchINN(value);
|
||||
|
||||
if (response.companyName) {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
...formState?.previousState,
|
||||
companyName: response.companyName
|
||||
},
|
||||
error: {
|
||||
...formState?.error,
|
||||
companyName: ''
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
companyName: response.companyName
|
||||
},
|
||||
error: {
|
||||
...formState?.error,
|
||||
companyName: '',
|
||||
inn: 'inn-invalid'
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
500
|
||||
);
|
||||
|
||||
function showPassowrd(target: 'password' | 'confirm-password') {
|
||||
if (target === 'password') {
|
||||
@@ -40,13 +83,38 @@ export default function RegisterForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function validateField(
|
||||
type SignupFormSchema = Awaited<ReturnType<typeof getSignupFormSchema>>;
|
||||
const [signupFormSchema, setSignupFormSchema] = useState<SignupFormSchema | null>(null);;
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
const loadSchema = async () => {
|
||||
const result = await getSignupFormSchema(accountType);
|
||||
if (isMounted) {
|
||||
setSignupFormSchema(result);
|
||||
}
|
||||
};
|
||||
|
||||
loadSchema();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, [accountType]);
|
||||
|
||||
async function validateField(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
) {
|
||||
switch (fieldName) {
|
||||
case 'confirm_password':
|
||||
const baseSchema = SignupFormSchema.shape.confirm_password;
|
||||
const baseSchema = signupFormSchema?.shape.confirm_password;
|
||||
|
||||
if (!baseSchema) {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
|
||||
const baseResult = baseSchema.safeParse(value);
|
||||
|
||||
if (!baseResult.success) {
|
||||
@@ -72,13 +140,25 @@ export default function RegisterForm() {
|
||||
case 'email':
|
||||
case 'fullName':
|
||||
case 'phone':
|
||||
case 'referralCode':
|
||||
case 'password':
|
||||
case 'companyName':
|
||||
case 'agree':
|
||||
const schema = SignupFormSchema.shape[fieldName];
|
||||
case 'inn':
|
||||
const schema = signupFormSchema?.shape[fieldName];
|
||||
if (!schema) {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
|
||||
if (fieldName === 'phone') {
|
||||
return schema.safeParse(value.replace(/[-\(\)\s]/g, ''));
|
||||
}
|
||||
if (fieldName === 'companyName' && accountType === 'b2c') {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
if (fieldName === 'referralCode') {
|
||||
return { success: true, data: value };
|
||||
}
|
||||
return schema.safeParse(value);
|
||||
|
||||
default:
|
||||
@@ -86,12 +166,45 @@ export default function RegisterForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeHandler(e: ChangeEvent<HTMLInputElement>): void {
|
||||
async function onChangeHandler(e: ChangeEvent<HTMLInputElement>): Promise<void> {
|
||||
const fieldName = e.target.name;
|
||||
if (fieldName !== 'phone') {
|
||||
e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, '');
|
||||
}
|
||||
const validatedField = validateField(fieldName, e.target.value);
|
||||
|
||||
if (fieldName === 'inn') {
|
||||
const currentValue = e.target.value;
|
||||
|
||||
const cleanedValue = currentValue.replace(/([^0-9])/g, '');
|
||||
|
||||
if (cleanedValue.length > 10) {
|
||||
e.target.value = cleanedValue.slice(0, 10);
|
||||
debouncedFetchINN(e.target.value);
|
||||
} else {
|
||||
e.target.value = cleanedValue;
|
||||
}
|
||||
|
||||
if (cleanedValue.length === 10) {
|
||||
debouncedFetchINN(cleanedValue);
|
||||
} else {
|
||||
if (formState) {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
...formState?.previousState,
|
||||
companyName: ''
|
||||
},
|
||||
error: {
|
||||
...formState?.error,
|
||||
companyName: '',
|
||||
inn: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validatedField = await validateField(fieldName, e.target.value);
|
||||
|
||||
if (validatedField.success) {
|
||||
setFormState(prevState => {
|
||||
@@ -109,6 +222,26 @@ export default function RegisterForm() {
|
||||
}
|
||||
}
|
||||
|
||||
function switchRegistrationTypeHandler(e: React.MouseEvent<HTMLButtonElement>, type: 'b2c' | 'b2b') {
|
||||
e.preventDefault();
|
||||
setAccountType(type);
|
||||
if (formState?.previousState) {
|
||||
setFormState({
|
||||
...formState,
|
||||
previousState: {
|
||||
...formState.previousState,
|
||||
companyName: '',
|
||||
inn: ''
|
||||
},
|
||||
error: {
|
||||
...formState.error,
|
||||
companyName: '',
|
||||
inn: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -131,6 +264,32 @@ export default function RegisterForm() {
|
||||
<form action={(e) => {
|
||||
formAction(e);
|
||||
}}>
|
||||
<div className={`${styles['form-switcher']}`}>
|
||||
<button
|
||||
className={`${styles['form-switcher-button']} ${accountType === 'b2c' ? styles['active'] : ''}`}
|
||||
onClick={(e) => {
|
||||
switchRegistrationTypeHandler(e, 'b2c');
|
||||
}}
|
||||
>
|
||||
{t('personal')}
|
||||
</button>
|
||||
<button
|
||||
className={`${styles['form-switcher-button']} ${accountType === 'b2b' ? styles['active'] : ''}`}
|
||||
onClick={(e) => {
|
||||
switchRegistrationTypeHandler(e, 'b2b');
|
||||
}}
|
||||
>
|
||||
{t('company')}
|
||||
</button>
|
||||
<input
|
||||
type="hidden"
|
||||
id="accountType"
|
||||
name="accountType"
|
||||
className={`${styles['form-input-hidden']}`}
|
||||
value={accountType}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']} ${styles['required']}`}
|
||||
@@ -162,6 +321,107 @@ export default function RegisterForm() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{accountType === 'b2b' ? (
|
||||
<>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']} ${styles['required']}`}
|
||||
>
|
||||
{t('INN')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="inn"
|
||||
name="inn"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('inn-placeholder')}
|
||||
defaultValue={formState?.previousState?.inn ? formState?.previousState?.inn : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formState?.error?.inn && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.inn.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']}`}
|
||||
>
|
||||
{t('company')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('field-filled-automatically')}
|
||||
value={formState?.previousState?.companyName ?? ''}
|
||||
readOnly={true}
|
||||
/* defaultValue={formState?.previousState?.companyName ? formState?.previousState?.companyName : ''} */
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formState?.error?.companyName && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.companyName.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']}`}>
|
||||
{t('referal-code')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="referralCode"
|
||||
name="referralCode"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('referal-code')}
|
||||
defaultValue={referralCode ? referralCode : ''}
|
||||
onChange={(e) => {
|
||||
e.target.value = e.target.value.toLocaleLowerCase();
|
||||
onChangeHandler(e)
|
||||
}}
|
||||
/>
|
||||
{formState?.error?.referralCode && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.referralCode.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('email-adress')}
|
||||
@@ -193,6 +453,7 @@ export default function RegisterForm() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('phone')}
|
||||
@@ -214,37 +475,6 @@ export default function RegisterForm() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label
|
||||
className={`${styles['form-label']}`}
|
||||
>
|
||||
{t('company')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('company-placeholder')}
|
||||
defaultValue={formState?.previousState?.companyName ? formState?.previousState?.companyName : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
{formState?.error?.companyName && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.companyName.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('password')}
|
||||
@@ -287,6 +517,7 @@ export default function RegisterForm() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('confirm-password')}
|
||||
@@ -350,8 +581,18 @@ export default function RegisterForm() {
|
||||
{t(formState?.error?.server)}
|
||||
</p>
|
||||
)}
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('create-an-account')}
|
||||
<button
|
||||
type="submit"
|
||||
className={`${styles['btn']}`}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
) : (
|
||||
t("create-an-account")
|
||||
)}
|
||||
</button>
|
||||
</form >
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
'use client'
|
||||
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
import { useActionState, useEffect, useRef, useState, ChangeEvent } from 'react';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { confirmPassword, FormStateConfirmPassword } from '@/app/actions/auth';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { IconEye } from '@/app/ui/icons/icons';
|
||||
import { SignupFormSchema } from '@/app/actions/definitions';
|
||||
import * as z from 'zod';
|
||||
|
||||
export default function ResetPasswordForm() {
|
||||
const [state, formAction, isPending] = useActionState(
|
||||
confirmPassword,
|
||||
undefined,
|
||||
);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [formState, setFormState] = useState<FormStateConfirmPassword | undefined>(undefined);
|
||||
const t = useTranslations('Login-register-form');
|
||||
const searchParams = useSearchParams();
|
||||
const email = searchParams.get('email');
|
||||
|
||||
useEffect(() => {
|
||||
if (state) {
|
||||
setFormState(state);
|
||||
}
|
||||
}, [state])
|
||||
|
||||
function showPassowrd(target: 'password' | 'confirm-password') {
|
||||
if (target === 'password') {
|
||||
setShowPassword(!showPassword);
|
||||
} else {
|
||||
setShowConfirmPassword(!showConfirmPassword);
|
||||
}
|
||||
}
|
||||
|
||||
function validateField(
|
||||
fieldName: string,
|
||||
value: string,
|
||||
) {
|
||||
switch (fieldName) {
|
||||
case 'confirm_password':
|
||||
const baseSchema = SignupFormSchema.shape.confirm_password;
|
||||
const baseResult = baseSchema.safeParse(value);
|
||||
|
||||
if (!baseResult.success) {
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
const currentPassword = passwordRef.current?.value || '';
|
||||
if (value !== currentPassword) {
|
||||
return {
|
||||
success: false,
|
||||
error: new z.ZodError([
|
||||
{
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'repeat-password',
|
||||
path: ['confirm_password']
|
||||
}
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: value };
|
||||
case 'password':
|
||||
const schema = SignupFormSchema.shape[fieldName];
|
||||
return schema.safeParse(value);
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown field: ${fieldName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeHandler(e: ChangeEvent<HTMLInputElement>): void {
|
||||
const fieldName = e.target.name;
|
||||
if (fieldName !== 'phone') {
|
||||
e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, '');
|
||||
}
|
||||
const validatedField = validateField(fieldName, e.target.value);
|
||||
|
||||
if (validatedField.success) {
|
||||
setFormState(prevState => {
|
||||
if (!prevState?.error || !prevState?.error[fieldName]) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const { [fieldName]: _, ...newError } = prevState.error;
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
error: newError
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<input
|
||||
type="hidden"
|
||||
id="email"
|
||||
name="email"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('enter-email')}
|
||||
defaultValue={email ? email : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('recovery-code')}
|
||||
</label>
|
||||
<input
|
||||
type="readOnly"
|
||||
id="verify-token"
|
||||
name="verify-token"
|
||||
className={`${styles['form-input']}`}
|
||||
placeholder={t('recovery-code')}
|
||||
defaultValue={formState?.previousState?.verifyToken ? formState?.previousState?.verifyToken : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('password')}
|
||||
</label>
|
||||
<div
|
||||
className={`${styles['password-wrapper']}`}
|
||||
>
|
||||
<input
|
||||
ref={passwordRef}
|
||||
type={showPassword ? "text" : "password"}
|
||||
id="password"
|
||||
name="password"
|
||||
className={`${styles['form-input']} ${styles['password']}`}
|
||||
placeholder={t('password-placeholder')}
|
||||
defaultValue={formState?.previousState?.password ? formState?.previousState?.password : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
showPassowrd('password')
|
||||
}}
|
||||
type="button"
|
||||
className={`show-password-button ${showPassword ? 'show' : ''}`}
|
||||
>
|
||||
<IconEye />
|
||||
</button>
|
||||
</div>
|
||||
{formState?.error?.password && (
|
||||
<p className="text-sm text-red-500">
|
||||
{
|
||||
formState?.error?.password.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className={`${styles['form-group']}`}>
|
||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||
{t('confirm-password')}
|
||||
</label>
|
||||
<div className={`${styles['password-wrapper']}`}>
|
||||
<input
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
className={`${styles['form-input']} ${styles['password']}`}
|
||||
placeholder={t('repeat-password')}
|
||||
defaultValue={formState?.previousState?.confirm_password ? formState?.previousState?.confirm_password : ''}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
showPassowrd('confirm-password');
|
||||
}}
|
||||
type="button"
|
||||
className={`show-password-button ${showConfirmPassword ? 'show' : ''}`}
|
||||
>
|
||||
<IconEye />
|
||||
</button>
|
||||
</div>
|
||||
{formState?.error?.confirm_password && (
|
||||
<p className="text-sm text-red-500">
|
||||
{t(formState?.error?.confirm_password)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* {state?.error.server && (
|
||||
<p className={`${styles['form-error']}`}>
|
||||
{t(state?.error.server)}
|
||||
</p>
|
||||
)} */}
|
||||
<button type="submit" className={`${styles['btn']}`}>
|
||||
{t('change-password')}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import NotificationsButton from './notificationsButton';
|
||||
import UserMenuButton from './userMenuButton';
|
||||
import Link from 'next/link';
|
||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import BurgerMenu from '@/app/ui/navigation/burger-menu';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function HeaderPanel() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
return (
|
||||
<header className="header">
|
||||
<BurgerMenu />
|
||||
<div className="header-action">
|
||||
<LanguageSwitcher />
|
||||
<Link
|
||||
href={'/pages/payment'}
|
||||
className="tokens-balance"
|
||||
>
|
||||
<div className="tokens-wrapper" title={t('current-balance-of-protection')}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: "16px", height: "16px" }}>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"></path>
|
||||
</svg>
|
||||
<span className="tokens-count">800</span>
|
||||
<span className="tokens-label">
|
||||
{t('tokens')}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
{/* <LanguageSwitcher /> */}
|
||||
{typeof userData?.tariffInfo?.tokens === 'number' && (
|
||||
<Link
|
||||
href={'/pages/payment'}
|
||||
className="tokens-balance"
|
||||
>
|
||||
<div className="tokens-wrapper" title={t('current-balance-of-protection')}>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: "16px", height: "16px" }}>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4z"></path>
|
||||
</svg>
|
||||
<span className="tokens-count">{userData?.tariffInfo?.tokens}</span>
|
||||
<span className="tokens-label">
|
||||
{t('tokens')}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<NotificationsButton />
|
||||
<UserMenuButton />
|
||||
</div>
|
||||
|
||||
@@ -3,27 +3,15 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import Link from 'next/link';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { logout } from '@/app/actions/auth';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function UserMenuButton() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const t = useTranslations("Global");
|
||||
const {
|
||||
data: userData,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['userData'],
|
||||
queryFn: () => {
|
||||
return getUserData();
|
||||
}
|
||||
})
|
||||
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
useClickOutside(
|
||||
menuRef,
|
||||
@@ -50,7 +38,7 @@ export default function UserMenuButton() {
|
||||
<div
|
||||
className="user-avatar"
|
||||
id="user-menu-btn"
|
||||
title={userData?.fullName ? userData.fullName : ''}
|
||||
title={userData?.fullName ?? ''}
|
||||
>
|
||||
{userData?.fullName ? userData.fullName[0] : ''}
|
||||
</div>
|
||||
@@ -61,10 +49,10 @@ export default function UserMenuButton() {
|
||||
</div>
|
||||
<div className="user-details">
|
||||
<div className="user-name">
|
||||
{userData?.fullName ? userData.fullName : ''}
|
||||
{userData?.fullName ?? ''}
|
||||
</div>
|
||||
<div className="user-email">
|
||||
{userData?.email ? userData.email : ''}
|
||||
{userData?.email ?? ''}
|
||||
</div>
|
||||
<div className="user-subscription">
|
||||
<span className="text-[14px]">{t('rate')}: </span>
|
||||
|
||||
@@ -103,9 +103,24 @@ export function IconArrowDown() {
|
||||
|
||||
export function IconFilter() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="currentColor" d="M11 18h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1m4 6h10c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1" /></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 16 16">
|
||||
<g transform="scale(0.6) translate(7, 7)">
|
||||
<path fill="currentColor" fillRule="evenodd" d="m4.5 1.529l2.838 2.603l-.676.737L5 3.344V14H4V3.344L2.338 4.869l-.676-.737zm6.454 10.745V1.91h1v10.363l1.662-1.525l.676.737l-2.838 2.603l-2.838-2.603l.676-.737z" clipRule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
/* <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
|
||||
<g transform="scale(0.6) translate(7, 7)">
|
||||
<path fill="currentColor" d="M11 18h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1M3 7c0 .55.45 1 1 1h16c.55 0 1-.45 1-1s-.45-1-1-1H4c-.55 0-1 .45-1 1m4 6h10c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1" />
|
||||
</g>
|
||||
</svg> */
|
||||
|
||||
{/* <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g transform="scale(0.6) translate(6, 6)">
|
||||
<path fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.758 19.414L9 21v-8.5L4.52 7.572A2 2 0 0 1 4 6.227V4h16v2.172a2 2 0 0 1-.586 1.414L15 12v1.5m7 8.5l-5-5m0 5l5-5" />
|
||||
</g>
|
||||
</svg> */}
|
||||
|
||||
export function IconNotification() {
|
||||
return (
|
||||
@@ -147,8 +162,23 @@ export function IconBurgerMenu() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function IconRefresh() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 20q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4q1.725 0 3.3.712T18 6.75V4h2v7h-7V9h4.2q-.8-1.4-2.187-2.2T12 6Q9.5 6 7.75 7.75T6 12t1.75 4.25T12 18q1.925 0 3.475-1.1T17.65 14h2.1q-.7 2.65-2.85 4.325T12 20" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconSearch() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5A6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5S14 7.01 14 9.5S11.99 14 9.5 14" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconDiscet() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon">
|
||||
<g fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 19V5a2 2 0 0 1 2-2h11.172a2 2 0 0 1 1.414.586l2.828 2.828A2 2 0 0 1 21 7.828V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" /><path d="M8.6 9h6.8a.6.6 0 0 0 .6-.6V3.6a.6.6 0 0 0-.6-.6H8.6a.6.6 0 0 0-.6.6v4.8a.6.6 0 0 0 .6.6ZM6 13.6V21h12v-7.4a.6.6 0 0 0-.6-.6H6.6a.6.6 0 0 0-.6.6Z" /></g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { useState, ChangeEvent } from 'react';
|
||||
import { useState, ChangeEvent, useEffect } from 'react';
|
||||
import { InputMask } from '@react-input/mask';
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
|
||||
interface PhoneInputProps {
|
||||
phoneState: string | undefined;
|
||||
validateHandler: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
clearCondition?: string | undefined | null
|
||||
}
|
||||
|
||||
export default function PhoneInput({ phoneState, validateHandler }: PhoneInputProps) {
|
||||
const [phone, setPhone] = useState(phoneState ? phoneState : '');
|
||||
export default function PhoneInput({ phoneState, validateHandler, clearCondition }: PhoneInputProps) {
|
||||
const [phone, setPhone] = useState<string | undefined>(phoneState ? phoneState : '');
|
||||
|
||||
useEffect(() => {
|
||||
if (clearCondition) {
|
||||
setPhone('');
|
||||
}
|
||||
}, [clearCondition])
|
||||
|
||||
return (
|
||||
<InputMask
|
||||
|
||||
@@ -1,75 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
|
||||
type FilesInfo = {
|
||||
totalSize: number;
|
||||
totalCount: number;
|
||||
};
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function ProtectionStatistic({ fileType }: { fileType: string }) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const getFileTypeFields = (fileType: string): { totalSize: string, totalCount: string } => {
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
const getFileTypeData = (fileType: string) => {
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
return {
|
||||
totalSize: 'images_size',
|
||||
totalCount: 'images_quantity'
|
||||
};
|
||||
return filesInfo?.images;
|
||||
case 'video':
|
||||
return {
|
||||
totalSize: 'videos_size',
|
||||
totalCount: 'videos_quantity'
|
||||
};
|
||||
return filesInfo?.videos;
|
||||
case 'audio':
|
||||
return {
|
||||
totalSize: 'audios_size',
|
||||
totalCount: 'audios_quantity'
|
||||
};
|
||||
return filesInfo?.audios;
|
||||
case 'document':
|
||||
return filesInfo?.documents;
|
||||
default:
|
||||
return {
|
||||
totalSize: 'all_files_size',
|
||||
totalCount: 'all_files_quantity'
|
||||
};
|
||||
return filesInfo?.total;
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
const fields = getFileTypeFields(fileType);
|
||||
|
||||
return {
|
||||
totalSize: data[fields.totalSize as keyof typeof data],
|
||||
totalCount: data[fields.totalCount as keyof typeof data]
|
||||
}
|
||||
}
|
||||
});
|
||||
const currentData = getFileTypeData(fileType);
|
||||
|
||||
return (
|
||||
<div className="protection-statistic">
|
||||
<div className="protection-statistic-stat-card">
|
||||
<div className="protection-statistic-stat-number">0</div>
|
||||
<div className="protection-statistic-stat-number">
|
||||
{currentData?.protected ?? 0}
|
||||
</div>
|
||||
<div className="protection-statistic-stat-label">{t(`${fileType}-protected`)}</div>
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
@@ -78,7 +40,7 @@ export default function ProtectionStatistic({ fileType }: { fileType: string })
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
<div className="protection-statistic-stat-number">
|
||||
{filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0}
|
||||
{convertBytes(currentData?.size ?? 0)}
|
||||
</div>
|
||||
<div className="protection-statistic-stat-label">{t(`${fileType}-occupy`)}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import {useTranslations, useLocale} from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { useStoreWithDevtools } from '@/app/stores/useStoreWithDevtools';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
type FilesInfo = {
|
||||
totalSize: number;
|
||||
@@ -15,62 +13,25 @@ type FilesInfo = {
|
||||
|
||||
export default function ProtectionSummary({ fileType }: { fileType: string }) {
|
||||
const t = useTranslations('Global');
|
||||
const CHECKS = useStoreWithDevtools(s => s.value1);
|
||||
const VIOLATIONS = useStoreWithDevtools(s => s.value2);
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
const getFileTypeFields = (fileType: string): { totalSize: string, totalCount: string } => {
|
||||
const getFileTypeData = (fileType: string) => {
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
return {
|
||||
totalSize: 'images_size',
|
||||
totalCount: 'images_quantity'
|
||||
};
|
||||
return filesInfo?.images;
|
||||
case 'video':
|
||||
return {
|
||||
totalSize: 'videos_size',
|
||||
totalCount: 'videos_quantity'
|
||||
};
|
||||
return filesInfo?.videos;
|
||||
case 'audio':
|
||||
return {
|
||||
totalSize: 'audios_size',
|
||||
totalCount: 'audios_quantity'
|
||||
};
|
||||
return filesInfo?.audios;
|
||||
case 'document':
|
||||
return filesInfo?.documents;
|
||||
default:
|
||||
return {
|
||||
totalSize: 'all_files_size',
|
||||
totalCount: 'all_files_quantity'
|
||||
};
|
||||
return filesInfo?.total;
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
const fields = getFileTypeFields(fileType);
|
||||
|
||||
return {
|
||||
totalSize: data[fields.totalSize as keyof typeof data],
|
||||
totalCount: data[fields.totalCount as keyof typeof data]
|
||||
}
|
||||
}
|
||||
});
|
||||
const currentData = getFileTypeData(fileType);
|
||||
|
||||
const locale = useLocale();
|
||||
|
||||
@@ -95,27 +56,27 @@ export default function ProtectionSummary({ fileType }: { fileType: string }) {
|
||||
<p>{t('current-status-of')}</p>
|
||||
<div className="protection-stats">
|
||||
<div className="protection-stat total-files">
|
||||
<span className="protection-stat-value">{filesInfo?.totalCount ? filesInfo?.totalCount : 0}</span>
|
||||
<span className="protection-stat-value">{currentData?.quantity ?? 0}</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeFilesName(filesInfo?.totalCount || 0)}
|
||||
{pluralizeFilesName(currentData?.quantity ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="protection-stat total-checks">
|
||||
<div>
|
||||
<span className="protection-stat-value">{CHECKS}</span>
|
||||
<span className="protection-stat-value">{currentData?.check ?? 0}</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeCheks(CHECKS)}
|
||||
{pluralizeCheks(currentData?.check ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="protection-stat-value">{VIOLATIONS}</span>
|
||||
<span className="protection-stat-value">{currentData?.violation ?? 0}</span>
|
||||
<span className="protection-stat-label">
|
||||
{pluralizeViolations(VIOLATIONS)}
|
||||
{pluralizeViolations(currentData?.violation ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="protection-stat total-usage">
|
||||
<div className="protection-stat-value">{filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} / 0</div>
|
||||
<div className="protection-stat-value">{convertBytes(currentData?.size ?? 0)} / 0</div>
|
||||
<div className="protection-stat-label">
|
||||
{t('disk-space-used')}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
export function FileInfoModalWindow({ fileInfo, setWindowClose, setWindowChildren }: any) {
|
||||
|
||||
const exampleTest = {
|
||||
ownerName: 'string',
|
||||
ownerMail: 'string',
|
||||
ownerCompany: 'string',
|
||||
fileName: 'string',
|
||||
fileSize: 'number',
|
||||
fileUploadDate: 'number',
|
||||
fileFormat: 'string',
|
||||
protectStatus: 'string',
|
||||
checksCount: 'number'
|
||||
}
|
||||
|
||||
function closeHandler() {
|
||||
setWindowChildren(null);
|
||||
setWindowClose(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='file-info-modal-window'>
|
||||
|
||||
<div
|
||||
className="flex justify-between"
|
||||
>
|
||||
<h3 className="text-2xl mb-4">
|
||||
Результат проверки
|
||||
</h3>
|
||||
<button
|
||||
className="file-info-modal-window-close-button"
|
||||
onClick={closeHandler}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="file-info-modal-window-section"
|
||||
>
|
||||
|
||||
<h4
|
||||
className="mb-2"
|
||||
>
|
||||
Информация о владельце
|
||||
</h4>
|
||||
<div className="file-info-modal-window-grid" >
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Имя
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.ownerName ? fileInfo.ownerName : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Email
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.ownerEmail ? fileInfo.ownerEmail : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Компания
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.ownerCompany ? fileInfo.ownerCompany : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4
|
||||
className="mb-2"
|
||||
>
|
||||
Информация о файле
|
||||
</h4>
|
||||
<div className="file-info-modal-window-grid" >
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Имя файла
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileName ? fileInfo.fileName : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Размер
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileSize ? convertBytes(fileInfo.fileSize) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Загружен
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileUploadDate ? fileInfo.fileUploadDate : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Формат
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.fileExtension ? fileInfo.fileExtension : '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4
|
||||
className="mb-2"
|
||||
>
|
||||
Детали защиты
|
||||
</h4>
|
||||
<div className="file-info-modal-window-grid" >
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Статус защиты
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.protectStatus ? fileInfo.protectStatus : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-info-modal-window-item">
|
||||
<div className="file-info-modal-window-title" >
|
||||
Количество проверок
|
||||
</div>
|
||||
<div className="file-info-modal-window-content" >
|
||||
{fileInfo.checksCount ? fileInfo.checksCount : '0'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { IconDocument, IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {useTranslations, useLocale} from 'next-intl';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function MyContentInfoBlock() {
|
||||
const locale = useLocale();
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
const fileTypeSizePercents = () => {
|
||||
if (!filesInfo?.all_files_size) {
|
||||
if (!filesInfo?.total.size) {
|
||||
return {
|
||||
images: 0,
|
||||
videos: 0,
|
||||
@@ -32,13 +23,13 @@ export default function MyContentInfoBlock() {
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
images: filesInfo?.images_size || 0,
|
||||
videos: filesInfo?.videos_size || 0,
|
||||
audios: filesInfo?.audios_size || 0,
|
||||
documents: filesInfo?.documents_size || 0,
|
||||
images: filesInfo?.images.size || 0,
|
||||
videos: filesInfo?.videos.size || 0,
|
||||
audios: filesInfo?.audios.size || 0,
|
||||
documents: filesInfo?.documents.size || 0,
|
||||
};
|
||||
|
||||
const total = filesInfo?.all_files_size;
|
||||
const total = filesInfo?.total.size;
|
||||
|
||||
const rawPercents = {
|
||||
images: (sizes.images / total) * 100,
|
||||
@@ -90,10 +81,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.images_quantity || 0} {pluralizeFiles(filesInfo?.images_quantity || 0)}
|
||||
{filesInfo?.images.quantity || 0} {pluralizeFiles(filesInfo?.images.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}
|
||||
{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,10 +101,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.videos_quantity || 0} {pluralizeFiles(filesInfo?.videos_quantity || 0)}
|
||||
{filesInfo?.videos.quantity || 0} {pluralizeFiles(filesInfo?.videos.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}
|
||||
{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,10 +121,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.audios_quantity || 0} {pluralizeFiles(filesInfo?.audios_quantity || 0)}
|
||||
{filesInfo?.audios.quantity || 0} {pluralizeFiles(filesInfo?.audios.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}
|
||||
{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,10 +141,10 @@ export default function MyContentInfoBlock() {
|
||||
</div>
|
||||
<div className="file-stat-details">
|
||||
<span className="file-stat-count">
|
||||
{filesInfo?.documents_quantity || 0} {pluralizeFiles(filesInfo?.documents_quantity || 0)}
|
||||
{filesInfo?.documents.quantity || 0} {pluralizeFiles(filesInfo?.documents.quantity || 0)}
|
||||
</span>
|
||||
<span className="file-stat-size">
|
||||
{filesInfo?.documents_size ? convertBytes(filesInfo?.documents_size) : 0}
|
||||
{filesInfo?.documents.size ? convertBytes(filesInfo?.documents.size) : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { PieChartComponent } from '@/app/components/PieChartComponent';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
|
||||
type FilesInfo = {
|
||||
@@ -28,76 +27,27 @@ export default function MyContentPieChart() {
|
||||
{ name: 'documents', value: 0, color: '#a561e6' },
|
||||
]);
|
||||
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<{
|
||||
all_files_size: number,
|
||||
all_files_quantity: number,
|
||||
images_quantity: number,
|
||||
videos_quantity: number,
|
||||
audios_quantity: number,
|
||||
documents_quantity: number,
|
||||
images_size: number,
|
||||
videos_size: number,
|
||||
audios_size: number,
|
||||
documents_size: number,
|
||||
}, Error, FilesInfo>({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo => {
|
||||
if (!data) {
|
||||
return {
|
||||
totalSize: 0,
|
||||
totalCount: 0,
|
||||
imageQuantity: 0,
|
||||
videoQuantity: 0,
|
||||
audioQuantity: 0,
|
||||
documentQuantity: 0,
|
||||
imageSize: 0,
|
||||
videoSize: 0,
|
||||
audioSize: 0,
|
||||
documentSize: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize: data.all_files_size,
|
||||
totalCount: data.all_files_quantity,
|
||||
imageQuantity: data.images_quantity,
|
||||
videoQuantity: data.videos_quantity,
|
||||
audioQuantity: data.audios_quantity,
|
||||
documentQuantity: data.documents_quantity,
|
||||
imageSize: data.images_size,
|
||||
videoSize: data.videos_size,
|
||||
audioSize: data.audios_size,
|
||||
documentSize: data.documents_size,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
useEffect(() => {
|
||||
setFilesCount([
|
||||
{
|
||||
name: 'images-few',
|
||||
value: filesInfo?.imageSize ? filesInfo?.imageSize : 0,
|
||||
value: filesInfo?.images.size ? filesInfo?.images.size : 0,
|
||||
color: '#f08c00'
|
||||
},
|
||||
{
|
||||
name: 'videos',
|
||||
value: filesInfo?.videoSize ? filesInfo?.videoSize : 0,
|
||||
value: filesInfo?.videos.size ? filesInfo?.videos.size : 0,
|
||||
color: '#2f9e44'
|
||||
},
|
||||
{
|
||||
name: 'audios',
|
||||
value: filesInfo?.audioSize ? filesInfo?.audioSize : 0,
|
||||
value: filesInfo?.audios.size ? filesInfo?.audios.size : 0,
|
||||
color: '#1971c2'
|
||||
},
|
||||
{
|
||||
name: 'documents',
|
||||
value: filesInfo?.documentSize ? filesInfo?.documentSize : 0,
|
||||
value: filesInfo?.documents.size ? filesInfo?.documents.size : 0,
|
||||
color: '#a561e6'
|
||||
},
|
||||
])
|
||||
@@ -105,7 +55,7 @@ export default function MyContentPieChart() {
|
||||
|
||||
return (
|
||||
<div className="protection-overview">
|
||||
<PieChartComponent data={filesCount} show={filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} />
|
||||
<PieChartComponent data={filesCount} show={convertBytes(filesInfo?.total.size ?? 0)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,14 @@
|
||||
//removed
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function MyContentPageTitleColorFrame({ title, description }: { title: string, description: string }) {
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="page-title-color-frame">
|
||||
@@ -30,7 +23,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
|
||||
</div>
|
||||
<div className="header-stats">
|
||||
<div className="header-stat-item">
|
||||
<div className="header-stat-value">{filesInfo?.all_files_quantity}</div>
|
||||
<div className="header-stat-value">{filesInfo?.total.quantity}</div>
|
||||
<div className="header-stat-label">Файлов</div>
|
||||
</div>
|
||||
<div className="header-stat-divider"></div>
|
||||
@@ -40,7 +33,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
|
||||
</div>
|
||||
<div className="header-stat-divider"></div>
|
||||
<div className="header-stat-item">
|
||||
<div className="header-stat-value">{filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}</div>
|
||||
<div className="header-stat-value">{convertBytes(filesInfo?.total.size ?? 0)}</div>
|
||||
<div className="header-stat-label">Хранилище</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
import {convertBytes} from '@/app/lib/convertBytes';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { IconDocument, IconShield, IconSearch, IconDiscet } from '@/app/ui/icons/icons';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function MyContentStatsOverview() {
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: filesInfo,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
});
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="my-content-stats-overview">
|
||||
<div className="stat-card files" data-icon="📄">
|
||||
<div className="stat-card files" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">📄</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconDocument />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.all_files_quantity}</div>
|
||||
<div className="stat-value">{filesInfo?.total.quantity}</div>
|
||||
<div className="stat-label">Всего файлов</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card protected" data-icon="🛡">
|
||||
<div className="stat-card protected" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">🛡</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconShield />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-value">{filesInfo?.total.protected}</div>
|
||||
<div className="stat-label">Защищено файлов</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card checks" data-icon="🔍">
|
||||
<div className="stat-card checks" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">🔍</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconSearch />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-label">Проверок защиты</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card storage" data-icon="💾">
|
||||
<div className="stat-card storage" data-icon="">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">💾</div>
|
||||
<div className="stat-trend">+0%</div>
|
||||
<div className="stat-icon">
|
||||
<IconDiscet />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}</div>
|
||||
<div className="stat-value">{convertBytes(filesInfo?.total.size ?? 0)}</div>
|
||||
<div className="stat-label">Хранилище</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,9 @@ import { usePathname } from 'next/navigation';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSideMenuStore } from '@/app/stores/sideMenuStore';
|
||||
|
||||
export default function DropDownList() {
|
||||
export default function DropDownList(
|
||||
{ referralLink }: { referralLink: string | null }
|
||||
) {
|
||||
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
||||
const [dropDownExpanded, setDropDownExpanded] = useState(false);
|
||||
const t = useTranslations('Global');
|
||||
@@ -17,16 +19,20 @@ export default function DropDownList() {
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: t('photo-marking'),
|
||||
href: '/pages/marking-photo'
|
||||
name: 'photo-marking',
|
||||
href: '/pages/marking-images'
|
||||
},
|
||||
{
|
||||
name: t('video-marking'),
|
||||
name: 'video-marking',
|
||||
href: '/pages/marking-video'
|
||||
},
|
||||
{
|
||||
name: t('audio-marking'),
|
||||
name: 'audio-marking',
|
||||
href: '/pages/marking-audio'
|
||||
},
|
||||
{
|
||||
name: 'document-marking',
|
||||
href: '/pages/marking-document'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -50,6 +56,9 @@ export default function DropDownList() {
|
||||
</div>
|
||||
<ul className={`sub-menu ${dropDownExpanded ? 'expanded' : ''}`}>
|
||||
{links.map((link) => {
|
||||
if (referralLink && link.name === 'document-marking') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={link.name}
|
||||
@@ -64,7 +73,7 @@ export default function DropDownList() {
|
||||
setMenuState(false)
|
||||
}}
|
||||
>
|
||||
<p className="">{link.name}</p>
|
||||
<p className="">{t(link.name)}</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,8 +11,70 @@ import navigation from './navigation.json';
|
||||
import { useSideMenuStore } from '@/app/stores/sideMenuStore';
|
||||
import { useRef } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { env } from 'process';
|
||||
import { getBuildData } from '@/app/actions/action';
|
||||
|
||||
export default function NavLinks() {
|
||||
const {
|
||||
data: referralLink,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => {
|
||||
return fetchReferralUserStats();
|
||||
},
|
||||
select: (data) => {
|
||||
if (data?.referralLink) {
|
||||
return data?.referralLink;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: frontendBuildDate,
|
||||
} = useQuery({
|
||||
queryKey: ['frontendBuildDate'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch('/build-info.json');
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
select: (data) => {
|
||||
if (data?.buildTime) {
|
||||
return data.buildTime;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
|
||||
const {
|
||||
data: backendBuildDate,
|
||||
} = useQuery({
|
||||
queryKey: ['backendBuildDate'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await getBuildData();
|
||||
return response.buildTimeBack ?? null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
|
||||
const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
||||
const t = useTranslations('Global');
|
||||
|
||||
@@ -66,9 +128,13 @@ export default function NavLinks() {
|
||||
<p className="">{t('home')}</p>
|
||||
</Link>
|
||||
|
||||
<DropDownList />
|
||||
<DropDownList referralLink={referralLink} />
|
||||
|
||||
{links.map((link) => {
|
||||
if (!referralLink && link.name === 'referral-program') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.name}
|
||||
@@ -108,6 +174,19 @@ export default function NavLinks() {
|
||||
<p className="">{t('exit')}</p>
|
||||
</Link>
|
||||
</ul>
|
||||
|
||||
<div className="build-versions">
|
||||
{frontendBuildDate && (
|
||||
<div className="">
|
||||
front: {frontendBuildDate}
|
||||
</div>
|
||||
)}
|
||||
{backendBuildDate && (
|
||||
<div className="">
|
||||
back: {backendBuildDate}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
"img": "M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.5 1.5 0 0 0 18.54 8H16c-.8 0-1.54.37-2 1l-3 4v2h2l2.54-3.82L16.5 18H20z"
|
||||
},
|
||||
{
|
||||
"name": "violations",
|
||||
"name": "violations-few",
|
||||
"href": "/pages/violations",
|
||||
"img": "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"href": "/pages/emptypage",
|
||||
"href": "/pages/search",
|
||||
"img": "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,122 +1,184 @@
|
||||
'use client'
|
||||
|
||||
import { fetchTariffs } from '@/app/actions/action';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { yooKasaCreatePayment } from '@/app/actions/yooKassa';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import ModalWindow from '@/app/components/ModalWindow';
|
||||
|
||||
export default function PaymentTabTariffs() {
|
||||
return (
|
||||
<div className="tab-content">
|
||||
<div className="billing-toggle">
|
||||
<div>
|
||||
<button className="billing-btn active">💳 Помесячно</button>
|
||||
<button className="billing-btn">
|
||||
🔥 Годовая
|
||||
<span>ВЫГОДНО</span>
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: userData,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['tariffData'],
|
||||
queryFn: () => {
|
||||
return fetchTariffs();
|
||||
}
|
||||
});
|
||||
|
||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||
const [selectedTariff, setSelectedTariff] = useState<{
|
||||
value: number;
|
||||
tariff: string;
|
||||
tokens: number;
|
||||
} | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
async function createPayment(value: number, tariff: string) {
|
||||
if (isProcessing) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
const response = await yooKasaCreatePayment(value, tariff);
|
||||
console.log(response);
|
||||
if (response) {
|
||||
window.open(response, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Payment error:', error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openPaymentWindow(value: number, tariff: string, tokens: number) {
|
||||
setSelectedTariff({ value, tariff, tokens });
|
||||
setIsProcessing(false);
|
||||
setOpenWindow(true);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
setOpenWindow(false);
|
||||
setSelectedTariff(null);
|
||||
setIsProcessing(false);
|
||||
}
|
||||
|
||||
function PaymentConfirmationModal() {
|
||||
if (!selectedTariff) return null;
|
||||
|
||||
const handleConfirm = () => {
|
||||
createPayment(selectedTariff.value, selectedTariff.tariff);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-window-confirm-payment">
|
||||
<h3 className="modal-window-confirm-payment-title">
|
||||
Подтверждение оплаты
|
||||
</h3>
|
||||
<h4 className="modal-window-confirm-payment-tariff">
|
||||
Подписка: {selectedTariff.tariff}
|
||||
</h4>
|
||||
<div className="modal-window-confirm-payment-description">
|
||||
Месячная подписка будет автоматически продлеваться
|
||||
<br />
|
||||
<strong>Включено {selectedTariff.tokens} токенов!</strong>
|
||||
</div>
|
||||
|
||||
<div className="modal-window-confirm-payment-price">
|
||||
<div>{selectedTariff.value}₽</div>
|
||||
<div>в месяц</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-4">
|
||||
<button
|
||||
className="btn-primary btn-cancel"
|
||||
onClick={handleCancel}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{t('no')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn-primary btn-confirm ${isProcessing ? 'btn-loading' : ''}`}
|
||||
onClick={handleConfirm}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Обработка...' : t('yes')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="subscription-grid">
|
||||
<div className="plan-card animate-card">
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">СТАРТ</h3>
|
||||
<div className="plan-price">900₽</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">100 токенов включено</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalWindow state={openWindow} callBack={setOpenWindow}>
|
||||
<PaymentConfirmationModal />
|
||||
</ModalWindow>
|
||||
|
||||
<div className="tab-content">
|
||||
<div className="billing-toggle" style={{ display: 'none' }}>
|
||||
<div>
|
||||
<button className="billing-btn active">💳 Помесячно</button>
|
||||
<button className="billing-btn">
|
||||
🔥 Годовая
|
||||
<span>ВЫГОДНО</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
100 файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
1 ГБ хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
До 10 МБ на файл
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="plan-card animate-card">
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">ПРЕМИУМ</h3>
|
||||
<div className="plan-price">3500₽</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">100 токенов включено</div>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
100 файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
1 ГБ хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
До 10 МБ на файл
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="plan-card animate-card">
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">ПРОФИ</h3>
|
||||
<div className="plan-price">9500₽</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">100 токенов включено</div>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
100 файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
1 ГБ хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
До 10 МБ на файл
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
<div className="subscription-grid">
|
||||
{userData?.map((item: {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
tokens: number;
|
||||
maxFilesCount: number;
|
||||
diskSize: number;
|
||||
}) => (
|
||||
<div className="plan-card animate-card" key={item.id}>
|
||||
<div className="plan-header">
|
||||
<div className="plan-icon">🚀</div>
|
||||
<h3 className="plan-name">
|
||||
{t.has(item.name) ? t(item.name) : item.name}
|
||||
</h3>
|
||||
<div className="plan-price">{item.price}</div>
|
||||
<div className="plan-period">в месяц</div>
|
||||
<div className="plan-tokens">{item.tokens} токенов включено</div>
|
||||
</div>
|
||||
<ul className="plan-features">
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
{item.maxFilesCount} файлов в месяц
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
{item.diskSize ? convertBytes(item.diskSize) : 0} хранилища
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Базовые модули защиты
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon">✓</span>
|
||||
Поддержка 24/7
|
||||
</li>
|
||||
<li className="plan-feature">
|
||||
<span className="feature-icon not">✗</span>
|
||||
API доступ
|
||||
</li>
|
||||
</ul>
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => openPaymentWindow(item.price, item.name, item.tokens)}
|
||||
>
|
||||
Выбрать план
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import { referralRefill } from '@/app/actions/referralsActions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function SecurePayments() {
|
||||
|
||||
return (
|
||||
<div className="secure-payments-wrapper">
|
||||
<h3>
|
||||
🔒 Безопасные платежи
|
||||
Безопасные платежи
|
||||
</h3>
|
||||
<div
|
||||
className='flex gap-4'
|
||||
>
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
onClick={async () => {
|
||||
const response = await referralRefill();
|
||||
console.log(response);
|
||||
if (response?.account === 'refill') {
|
||||
toast.success('refill');
|
||||
} else {
|
||||
toast.error(`error refill: ${response?.error?.message_desc}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
referralRefill
|
||||
</button>
|
||||
</div>
|
||||
<div className="secure-payments-grid">
|
||||
<div className="secure-payments-card">
|
||||
<div className="secure-payments-card-header">
|
||||
|
||||
@@ -1,38 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { getProcents } from '@/app/lib/getProcents';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export default function UserUsage() {
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
return (
|
||||
<div className="usage-stats">
|
||||
<h3>
|
||||
📊 Ваше использование
|
||||
Ваше использование
|
||||
</h3>
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">0 / 2,000</div>
|
||||
<div className="stat-value">
|
||||
{userData?.tariffInfo?.currentFileCounts} / {userData?.tariffInfo?.maxFileCounts}
|
||||
</div>
|
||||
<div className="stat-label">Файлов в месяц</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: '50%' }}
|
||||
style={{ width: `${getProcents(userData?.tariffInfo?.currentFileCounts, userData?.tariffInfo?.maxFileCounts)}%` }}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">0 / 20 ГБ</div>
|
||||
<div className="stat-value">
|
||||
{convertBytes(userData?.tariffInfo?.currentFileOnDisk ?? 0)} / {convertBytes(userData?.tariffInfo?.maxFileOnDisk ?? 0)}
|
||||
</div>
|
||||
<div className="stat-label">Объём хранилища</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: '50%' }}
|
||||
style={
|
||||
{ width: `${getProcents(userData?.tariffInfo?.currentFileOnDisk, userData?.tariffInfo?.maxFileOnDisk ?? 0)}%` }
|
||||
}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-value">
|
||||
{userData?.tariffInfo?.tokens ?? 0}
|
||||
</div>
|
||||
<div className="stat-label">Токенов на балансе</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">ПРОФИ</div>
|
||||
<div className="stat-value">
|
||||
{userData?.tariffInfo?.tariffName ? userData?.tariffInfo?.tariffName : 'ERROR'}
|
||||
</div>
|
||||
<div className="stat-label">Текущий план</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="earning-card success">
|
||||
<h4>💰 Доступно к выводу</h4>
|
||||
<p>
|
||||
135.00 Б
|
||||
0
|
||||
</p>
|
||||
<small>Минимальная сумма вывода: 100 Б</small>
|
||||
</div>
|
||||
@@ -18,7 +18,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="earning-card">
|
||||
<h4>📈 Всего заработано</h4>
|
||||
<p>
|
||||
135.00 Б
|
||||
0
|
||||
</p>
|
||||
<small>За все время участия в программе</small>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="earning-card warning">
|
||||
<h4>✅ Выплачено</h4>
|
||||
<p>
|
||||
0.00 Б
|
||||
0
|
||||
</p>
|
||||
<small>Успешно выплачено на ваши счета</small>
|
||||
</div>
|
||||
@@ -70,8 +70,8 @@ export default function IncomeAndPayments() {
|
||||
Обработано
|
||||
</div>
|
||||
|
||||
<div className="stats-item first-column second-row">
|
||||
100.00
|
||||
<div className="stats-item first-column left-bottom-corner">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
@@ -82,39 +82,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row last-column">
|
||||
0
|
||||
</div>
|
||||
|
||||
<div className="stats-item first-column">
|
||||
100.00
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-column">
|
||||
0
|
||||
</div>
|
||||
|
||||
<div className="stats-item last-row left-bottom-corner first-column">
|
||||
100.00
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row last-column right-bottom-corner">
|
||||
<div className="stats-item last-column right-bottom-corner">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchReferralsLevels, fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
|
||||
interface ReferralLevel {
|
||||
name: string;
|
||||
id: string;
|
||||
next: string | null;
|
||||
share: number;
|
||||
maxInvitee: number;
|
||||
minInvitee: number;
|
||||
}
|
||||
|
||||
interface ReferralsData {
|
||||
referrals: ReferralLevel[];
|
||||
}
|
||||
|
||||
export default function CommissionLevels() {
|
||||
|
||||
const {
|
||||
data: referralsLevels,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery<ReferralsData, Error, ReferralLevel[]>({
|
||||
queryKey: ['referralsLevels'],
|
||||
queryFn: () => fetchReferralsLevels(),
|
||||
select: (data) => {
|
||||
if (data?.referrals) {
|
||||
const itemsMap = new Map<string, ReferralLevel>();
|
||||
data.referrals.forEach(item => {
|
||||
itemsMap.set(item.id, item);
|
||||
});
|
||||
|
||||
const referencedIds = new Set(
|
||||
data.referrals
|
||||
.map(item => item.next)
|
||||
.filter((next): next is string => next !== null)
|
||||
);
|
||||
|
||||
const firstItem = data.referrals.find(item => !referencedIds.has(item.id));
|
||||
|
||||
if (!firstItem) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sortedReferrals: ReferralLevel[] = [];
|
||||
let currentItem = firstItem;
|
||||
|
||||
while (currentItem) {
|
||||
sortedReferrals.push(currentItem);
|
||||
|
||||
if (currentItem.next === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentItem = itemsMap.get(currentItem.next)!;
|
||||
|
||||
if (sortedReferrals.includes(currentItem)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sortedReferrals;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
data: activeInvites,
|
||||
error: activeInvitesError
|
||||
} = useQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => {
|
||||
return fetchReferralUserStats();
|
||||
},
|
||||
select: (data) => {
|
||||
if (data?.activeInvitee) {
|
||||
return data.activeInvitee
|
||||
} else {
|
||||
return 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
if (!referralsLevels?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getReferralIcon(referral: string) {
|
||||
switch (referral) {
|
||||
case 'bronze':
|
||||
return '🥉'
|
||||
case 'silver':
|
||||
return '🥈'
|
||||
case 'gold':
|
||||
return '🥇'
|
||||
case 'platinum':
|
||||
return '💎'
|
||||
default:
|
||||
return '🥉'
|
||||
}
|
||||
}
|
||||
|
||||
function isActiveReferralLevel(activeInvites: number, maxInvites: number, minInvites: number) {
|
||||
if (activeInvites <= maxInvites && activeInvites >= minInvites) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<h3 className="section-title">
|
||||
@@ -6,32 +120,28 @@ export default function CommissionLevels() {
|
||||
Уровни комиссии
|
||||
</h3>
|
||||
<div className="commission-levels">
|
||||
<div className="level-card active">
|
||||
<div className="level-number">🥉</div>
|
||||
<div className="level-commission">15%</div>
|
||||
<div className="level-subscribe">BRONZE</div>
|
||||
<div className="level-requirement">0-5 рефералов</div>
|
||||
</div>
|
||||
<div className="level-card ">
|
||||
<div className="level-number">🥈</div>
|
||||
<div className="level-commission">18%</div>
|
||||
<div className="level-subscribe">SILVER</div>
|
||||
<div className="level-requirement">6-15 рефералов</div>
|
||||
</div>
|
||||
|
||||
<div className="level-card ">
|
||||
<div className="level-number">🥇</div>
|
||||
<div className="level-commission">22%</div>
|
||||
<div className="level-subscribe">GOLD</div>
|
||||
<div className="level-requirement">16-50 рефералов</div>
|
||||
</div>
|
||||
|
||||
<div className="level-card ">
|
||||
<div className="level-number">💎</div>
|
||||
<div className="level-commission">25%</div>
|
||||
<div className="level-subscribe">PLATINUM</div>
|
||||
<div className="level-requirement">50+ рефералов</div>
|
||||
</div>
|
||||
{
|
||||
referralsLevels.map((item: ReferralLevel) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`level-card ${isActiveReferralLevel(activeInvites, item.maxInvitee, item.minInvitee) ? 'active' : ''}`
|
||||
}
|
||||
key={item.id}
|
||||
>
|
||||
<div className="level-number">
|
||||
{getReferralIcon(item.id)}
|
||||
</div>
|
||||
<div className="level-commission">{item.share}%</div>
|
||||
<div className="level-subscribe">{item.name}</div>
|
||||
<div className="level-requirement">{
|
||||
item.maxInvitee < 100 ? `${item.minInvitee}-${item.maxInvitee}` : `${item.minInvitee}+`
|
||||
} рефералов
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,77 +1,484 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getPaginationRowModel,
|
||||
getFilteredRowModel,
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
} from '@tanstack/react-table';
|
||||
import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } from '@/app/ui/icons/icons';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import DropDownList from '@/app/components/DropDownList';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { fetchReferralInvitees } from '@/app/actions/referralsActions';
|
||||
|
||||
type InviteesType = {
|
||||
status?: string;
|
||||
email?: string;
|
||||
registrationData: string;
|
||||
};
|
||||
|
||||
type InviteType = {
|
||||
active?: string;
|
||||
email?: string;
|
||||
regDate: string;
|
||||
};
|
||||
|
||||
export default function InvitationsTable() {
|
||||
const t = useTranslations('Global');
|
||||
const {
|
||||
data: referralInvitees,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<InviteType[], Error, InviteesType[], ['referralInvitees']>({
|
||||
queryKey: ['referralInvitees'],
|
||||
queryFn: () => {
|
||||
return fetchReferralInvitees();
|
||||
},
|
||||
|
||||
select: (data: InviteType[]): InviteesType[] => {
|
||||
if (!data) return [
|
||||
];
|
||||
|
||||
return data.map((item: InviteType) => {
|
||||
console.log(item);
|
||||
return {
|
||||
status: item?.active ? 'active' : 'not-active',
|
||||
email: item?.email,
|
||||
registrationData: item?.regDate
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Состояния
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [dateFilter, setDateFilter] = useState<string>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const t = useTranslations("Global");
|
||||
const locale = useLocale();
|
||||
|
||||
// Определение колонок
|
||||
const columns = useMemo<ColumnDef<InviteesType>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('email')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center table-item">
|
||||
{/* {row.original.size !== undefined ? convertBytes(row.original.size) : '-'} */}
|
||||
{row.original.email}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('status')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center font-semibold table-item">
|
||||
<span className="table-item-status">
|
||||
{row.original.status}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'registrationData',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('registration-date')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center table-item">
|
||||
{row.original.registrationData?.split('T')[0]}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableColumnFilter: false,
|
||||
}
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Фильтрация по типу файла и дате
|
||||
const filteredData = useMemo(() => {
|
||||
let result = referralInvitees;
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Фильтр по типу файла
|
||||
if (statusFilter !== 'all') {
|
||||
result = result.filter(item => item.status === statusFilter);
|
||||
}
|
||||
|
||||
// Фильтр по дате
|
||||
/* if (dateFilter !== 'all') {
|
||||
const now = Date.now();
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const sevenDays = 7 * oneDay;
|
||||
const thirtyDays = 30 * oneDay;
|
||||
|
||||
switch (dateFilter) {
|
||||
case 'today':
|
||||
const todayStart = new Date().setHours(0, 0, 0, 0);
|
||||
result = result.filter(item => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData >= todayStart
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'week':
|
||||
const weekAgo = now - sevenDays;
|
||||
result = result.filter(item => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData >= weekAgo;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'month':
|
||||
const monthAgo = now - thirtyDays;
|
||||
result = result.filter(item => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData >= monthAgo;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'older':
|
||||
const monthAgo2 = now - thirtyDays;
|
||||
result = result.filter(item => {
|
||||
if (item.registrationData) {
|
||||
return item.registrationData < monthAgo2;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
} */
|
||||
|
||||
return result;
|
||||
}, [referralInvitees, statusFilter, dateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPageRows = table.getRowModel().rows;
|
||||
const pageCount = table.getPageCount();
|
||||
|
||||
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
|
||||
table.setPageIndex(pagination.pageIndex - 1);
|
||||
}
|
||||
}, [filteredData, pagination.pageIndex]);
|
||||
|
||||
// Создание таблицы
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
pagination
|
||||
},
|
||||
autoResetPageIndex: false,
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pluralizeFiles = (number: number) => {
|
||||
const translate = [t('file'), t('files-few'), t('files')];
|
||||
return pluralize(number, translate[0], translate[1], translate[2], locale);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<h2 className="section-title">
|
||||
История приглашений
|
||||
</h2>
|
||||
<div
|
||||
className="block-wrapper"
|
||||
>
|
||||
<div className="tanstak-table-wrapper">
|
||||
<h3
|
||||
className="tanstak-table-title"
|
||||
>
|
||||
История приглашений
|
||||
</h3>
|
||||
{/* Фильтры */}
|
||||
<div className="tanstak-table-filtres" style={{ display: "none" }}>
|
||||
<div className="table-filtres-wrapper">
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{t('date-filter')}:</div>
|
||||
<DropDownList
|
||||
value={dateFilter}
|
||||
translatedValue={(() => {
|
||||
switch (dateFilter) {
|
||||
case 'all':
|
||||
return t('all-dates')
|
||||
case 'week':
|
||||
return t('for-a-week')
|
||||
case 'month':
|
||||
return t('for-a-month')
|
||||
case 'older':
|
||||
return t('older-than-a-month')
|
||||
default:
|
||||
return t('today')
|
||||
}
|
||||
})()}
|
||||
callBack={setDateFilter}
|
||||
>
|
||||
<li value="all">
|
||||
{t('all-dates')}
|
||||
</li>
|
||||
<li value="today">
|
||||
{t('today')}
|
||||
</li>
|
||||
<li value="week">
|
||||
{t('for-a-week')}
|
||||
</li>
|
||||
<li value="month">
|
||||
{t('for-a-month')}
|
||||
</li>
|
||||
<li value="older">
|
||||
{t('older-than-a-month')}
|
||||
</li>
|
||||
</DropDownList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stats-item left-top-corner stats-header">
|
||||
Email
|
||||
</div>
|
||||
<div className="stats-item stats-header">
|
||||
Статус
|
||||
</div>
|
||||
<div className="stats-item stats-header">
|
||||
Заработано
|
||||
</div>
|
||||
<div className="stats-item stats-header">
|
||||
Дата приглашения
|
||||
</div>
|
||||
<div className="stats-item last-column right-top-corner stats-header">
|
||||
Регистрация
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{t('items-per-page')}:</div>
|
||||
<DropDownList
|
||||
value={table.getState().pagination.pageSize}
|
||||
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
|
||||
callBack={table.setPageSize}
|
||||
>
|
||||
{[5, 10, 20, 50, 100].map(pageSize => (
|
||||
<li key={pageSize} value={pageSize}>
|
||||
{t('show')} {pageSize}
|
||||
</li>
|
||||
))}
|
||||
</DropDownList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-item first-column second-row">
|
||||
email@mail.com
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
{t('status')}
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item second-row last-column">
|
||||
0
|
||||
{/* Таблица */}
|
||||
<div className="tanstak-table-block">
|
||||
<table className="tanstak-table">
|
||||
<thead className="tanstak-table-head">
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: typeof header.column.columnDef.header === 'function'
|
||||
? header.column.columnDef.header(header.getContext())
|
||||
: header.column.columnDef.header as string}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="tanstak-table-body">
|
||||
{table.getRowModel().rows.length > 0 ? (
|
||||
table.getRowModel().rows.map(row => (
|
||||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>
|
||||
{typeof cell.column.columnDef.cell === 'function'
|
||||
? cell.column.columnDef.cell(cell.getContext())
|
||||
: cell.getValue() as string}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center">
|
||||
{t('no-data-for-selected-filters')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="stats-item first-column">
|
||||
email@mail.com
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
{t('status')}
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-column">
|
||||
0
|
||||
</div>
|
||||
{/* Пагинация */}
|
||||
<div className="tanstak-table-pagination">
|
||||
<div className="pagination-info">
|
||||
{/* <span className="pagination-info-pages">
|
||||
{t('page')}{' '}
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="pagination-info-files">
|
||||
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
|
||||
</span> */}
|
||||
</div>
|
||||
|
||||
<div className="stats-item last-row left-bottom-corner first-column">
|
||||
email@mail.com
|
||||
<div className="pagination-controls">
|
||||
{table.getCanPreviousPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.firstPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconDoubleArrowLeft />
|
||||
</button>
|
||||
)}
|
||||
{table.getCanPreviousPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="pagination-controls-pages">
|
||||
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
|
||||
const pageIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
table.getPageCount() - 5,
|
||||
table.getState().pagination.pageIndex - 2
|
||||
)
|
||||
) + i;
|
||||
|
||||
if (pageIndex < table.getPageCount()) {
|
||||
return (
|
||||
<button
|
||||
key={pageIndex}
|
||||
className={`${table.getState().pagination.pageIndex === pageIndex
|
||||
? 'current'
|
||||
: 'other'
|
||||
}`}
|
||||
onClick={() => table.setPageIndex(pageIndex)}
|
||||
>
|
||||
{pageIndex + 1}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{table.getCanNextPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconArrowRight />
|
||||
</button>
|
||||
)}
|
||||
{table.getCanNextPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.lastPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconDoubleArrowRight />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
{t('status')}
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row">
|
||||
0
|
||||
</div>
|
||||
<div className="stats-item last-row last-column right-bottom-corner">
|
||||
0
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export default function ReferralLinkSection() {
|
||||
const {
|
||||
data: referralUserStats,
|
||||
isLoading,
|
||||
isError,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => {
|
||||
return fetchReferralUserStats();
|
||||
},
|
||||
select: (data) => {
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations('Global')
|
||||
const [referralUrl, setReferralUrl] = useState<string>('');
|
||||
const refLink = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
setReferralUrl(`${window.location.origin}/register?ref=${referralUserStats?.referralLink}`);
|
||||
}
|
||||
}, [referralUserStats]);
|
||||
|
||||
const copyHandler = async () => {
|
||||
if (!refLink.current?.textContent) return;
|
||||
|
||||
const textToCopy = refLink.current.textContent;
|
||||
|
||||
if (typeof window === 'undefined' || !navigator.clipboard) {
|
||||
console.warn('Clipboard API не доступен');
|
||||
|
||||
const success = fallbackCopy(textToCopy);
|
||||
if (success) {
|
||||
toast.success(`${t('copied')}: ${textToCopy}`);
|
||||
} else {
|
||||
toast.error('Не удалось скопировать');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
toast.success(`${t('copied')}: ${textToCopy}`);
|
||||
} catch (err) {
|
||||
console.error('Ошибка копирования:', err);
|
||||
|
||||
const success = fallbackCopy(textToCopy);
|
||||
if (success) {
|
||||
toast.success(`${t('copied')}: ${textToCopy}`);
|
||||
} else {
|
||||
toast.error('Не удалось скопировать');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fallbackCopy = (text: string) => {
|
||||
try {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
|
||||
textArea.select();
|
||||
textArea.setSelectionRange(0, 99999);
|
||||
|
||||
const successful = document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
|
||||
return successful;
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (!referralUserStats?.referralLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="referral-link-section">
|
||||
<h3>🔗 Ваша реферальная ссылка</h3>
|
||||
<h3>Ваша реферальная ссылка</h3>
|
||||
<p>Поделитесь этой ссылкой с друзьями и получайте комиссию с каждой их покупки</p>
|
||||
<div className="referral-link-input">
|
||||
<span id="referral-link" >
|
||||
https://dashboard.no-copy.ru/register.php?ref=REFA0406AC8
|
||||
<span
|
||||
id="referral-link"
|
||||
ref={refLink}
|
||||
>
|
||||
{referralUrl}
|
||||
</span >
|
||||
<button className="copy-btn">📋 Копировать</button>
|
||||
<button
|
||||
className="copy-btn"
|
||||
onClick={copyHandler}
|
||||
>
|
||||
Копировать
|
||||
</button>
|
||||
</div>
|
||||
<p className="referral-link-footer">
|
||||
Код: <strong>REFA0406AC8</strong> •
|
||||
Кликов: <strong>21</strong> •
|
||||
Комиссия: <strong>15%</strong>
|
||||
Код: <strong>{referralUserStats?.referralLink}</strong> •
|
||||
Приглашений: <strong>
|
||||
{referralUserStats?.totalInvitee}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function SettingReferralProgram() {
|
||||
</div>
|
||||
|
||||
<form className="settings-form">
|
||||
<div>
|
||||
<div className="settings-form-wrapper">
|
||||
<h4>💳 Настройки выплат</h4>
|
||||
|
||||
<div className="form-group">
|
||||
@@ -32,7 +32,7 @@ export default function SettingReferralProgram() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="settings-form-wrapper">
|
||||
<h4>🔔 Уведомления</h4>
|
||||
|
||||
<div className="checkbox-group">
|
||||
@@ -40,11 +40,6 @@ export default function SettingReferralProgram() {
|
||||
<label>Email уведомления о новых рефералах</label>
|
||||
</div>
|
||||
|
||||
<div className="checkbox-group">
|
||||
<input type="checkbox" name="sms_notifications" defaultValue="1" />
|
||||
<label>SMS уведомления о выплатах</label>
|
||||
</div>
|
||||
|
||||
<div className="checkbox-group">
|
||||
<input type="checkbox" name="marketing_emails" defaultValue="1" />
|
||||
<label>Маркетинговые рассылки о программе</label>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
|
||||
export default function ReportsInfo() {
|
||||
const t = useTranslations("Global");
|
||||
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
return (
|
||||
<div className="dashboard-files-info">
|
||||
<div className="type-card images">
|
||||
<div className="stat-type">
|
||||
{t('all-content')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0}</div>
|
||||
</div>
|
||||
|
||||
<div className="type-card videos">
|
||||
<div className="stat-type">
|
||||
{t('violations-few')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div>
|
||||
</div>
|
||||
|
||||
<div className="type-card audio">
|
||||
<div className="stat-type">
|
||||
{t('monitoring')}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.check ? filesInfo?.total.check : 0}</div>
|
||||
</div>
|
||||
|
||||
<div className="type-card scripts">
|
||||
<div className="stat-type">
|
||||
{t('efficiency')}
|
||||
</div>
|
||||
<div className="stat-value">100%</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import { SearchedUserFilesList } from '@/app/ui/search/searched-user-files-list';
|
||||
import { SearchedGlobalFilesList } from '@/app/ui/search/searched-global-files-list';
|
||||
import { useCallback, useState, useImperativeHandle } from 'react';
|
||||
import { searchUserFiles, searchGlobalFiles } from '@/app/actions/searchActions';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Pagination } from '@/app/components/Pagination';
|
||||
|
||||
export interface SearchItem {
|
||||
url: string,
|
||||
pageTitle: string,
|
||||
height: number,
|
||||
width: number,
|
||||
host: string,
|
||||
pageUrl: string
|
||||
}
|
||||
|
||||
export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: any }) {
|
||||
const [searchedUserFiles, setSearchedUserFiles] = useState<string[]>([]);
|
||||
const [searchedUserFilesShowNull, setSearchedUserFilesShowNull] = useState<boolean>(false);
|
||||
const [searchedGlobalFiles, setSearchedGlobalFiles] = useState<SearchItem[]>([]);
|
||||
const [searchedGlobalFilesShowNull, setSearchedGlobalFilesShowNull] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [paginationLoading, setPaginationLoading] = useState<boolean>(false);
|
||||
const [showGlobalSearch, setShowGlobalSearch] = useState<boolean>(false);
|
||||
const queryClient = useQueryClient();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [searchTotalPages, setSearchTotalPages] = useState<number>(0);
|
||||
const [searchCurrentPages, setSearchCurrentPages] = useState<number>(1);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
clearList: () => {
|
||||
setSearchedUserFiles([]);
|
||||
setSearchedGlobalFiles([]);
|
||||
setSearchedUserFilesShowNull(false);
|
||||
setSearchedGlobalFilesShowNull(false);
|
||||
setShowGlobalSearch(false);
|
||||
setSearchCurrentPages(1);
|
||||
setSearchTotalPages(0);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
const handlerSearchUserFile = useCallback(async (fileId: string, page: number): Promise<void> => {
|
||||
setErrorMessage(null);
|
||||
|
||||
try {
|
||||
let result = await searchUserFiles(fileId);
|
||||
|
||||
if (result.content.length) {
|
||||
setSearchedUserFiles(result.content);
|
||||
setShowGlobalSearch(true);
|
||||
handlerSearchGlobalFile(fileId, page)
|
||||
} else {
|
||||
setSearchedUserFilesShowNull(true);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage('error');
|
||||
}
|
||||
|
||||
}, [fileId])
|
||||
|
||||
const handlerSearchGlobalFile = useCallback(async (fileId: string, page: number, actionFromPagination?: boolean): Promise<void> => {
|
||||
if (!actionFromPagination) {
|
||||
setLoading(true);
|
||||
setSearchedGlobalFilesShowNull(false);
|
||||
setSearchedGlobalFiles([]);
|
||||
} else {
|
||||
setPaginationLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
let result = await searchGlobalFiles(fileId, page);
|
||||
console.log('searchGlobalFiles');
|
||||
console.log(result);
|
||||
if (result.images.length) {
|
||||
setSearchedGlobalFiles(result.images);
|
||||
setSearchTotalPages(result.totalPages);
|
||||
setSearchCurrentPages(page);
|
||||
|
||||
} else {
|
||||
setSearchedGlobalFilesShowNull(true);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
await queryClient.invalidateQueries({ queryKey: ['userData'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userSearchData'] });
|
||||
setLoading(false);
|
||||
setPaginationLoading(false);
|
||||
}
|
||||
|
||||
}, [fileId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="mb-4"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary btn-search"
|
||||
disabled={fileId ? false : true}
|
||||
onClick={() => {
|
||||
if (fileId) {
|
||||
handlerSearchUserFile(fileId, 1)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Начать поиск
|
||||
</button>
|
||||
|
||||
{errorMessage && (
|
||||
<div className='mt-2 text-red-600 text-sm'>
|
||||
Ошибка поиска
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(searchedUserFiles.length !== 0) && (
|
||||
<SearchedUserFilesList list={searchedUserFiles} />
|
||||
)}
|
||||
|
||||
{searchedUserFilesShowNull && (
|
||||
<div
|
||||
className="user-file-search-results"
|
||||
>
|
||||
<div className="results-header">
|
||||
<div className="results-title">Результаты поиска</div>
|
||||
<div className="results-count">
|
||||
Найдено файлов: {searchedUserFiles.length}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="empty-results"
|
||||
>
|
||||
<h3>Похожих файлов не найдено</h3>
|
||||
<p>В вашей библиотеке нет файлов, похожих на загруженный</p>
|
||||
<div className="empty-results-hint">
|
||||
<strong>Совет:</strong> Убедитесь, что вы загружали этот файл ранее через страницу "Маркировка"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showGlobalSearch && (
|
||||
<div
|
||||
className="global-search-section"
|
||||
>
|
||||
<div className="global-search-header">
|
||||
<div className="global-search-title">
|
||||
Глобальный поиск изображений
|
||||
</div>
|
||||
<div className="">
|
||||
Найти где ещё используется ваше изображение в интернете
|
||||
</div>
|
||||
<div className="global-search-badge">
|
||||
Интернет поиск
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="global-search-content">
|
||||
<div className="global-search-info">
|
||||
<div className="global-search-info-title">
|
||||
Поиск по всему интернету
|
||||
</div>
|
||||
<div className="global-search-info-text">
|
||||
Используем продвинутые технологии обратного поиска по изображению для поиска копий вашего контента
|
||||
на сайтах, в социальных сетях и других источниках. Поможет обнаружить несанкционированное использование.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="search-counter" id="search-counter">
|
||||
<div className="counter-info">
|
||||
<div className="counter-icon" id="counter-icon"></div>
|
||||
<div className="counter-text">
|
||||
<div className="counter-label">Глобальных поисков сегодня</div>
|
||||
<div className="counter-value" id="counter-value">0 из 10</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
id="progress-fill"
|
||||
style={{ width: '0%' }}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`global-loading ${loading ? 'show' : ''}`} id="global-loading">
|
||||
<div className="global-spinner"></div>
|
||||
<div className="global-loading-text" id="global-loading-text">
|
||||
Поиск изображений в интернете...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`global-result ${searchedGlobalFiles.length ? 'show' : ''}`} id="global-result">
|
||||
<div className="global-result-card">
|
||||
<div className="global-result-header">
|
||||
<span>Найдено в интернете</span>
|
||||
{paginationLoading && (
|
||||
<div className="loading-animation">
|
||||
<span className="global-spinner"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="global-result-content">
|
||||
<SearchedGlobalFilesList list={searchedGlobalFiles} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{searchedGlobalFilesShowNull && (
|
||||
<div className="global-no-result" id="global-no-result">
|
||||
<div className="global-no-result-icon">‍</div>
|
||||
<h4>Похожие изображения в интернете не найдены</h4>
|
||||
<p>Это может означать, что ваше изображение уникально и не используется на других сайтах.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="global-error" id="global-error">
|
||||
<strong>Ошибка глобального поиска:</strong>
|
||||
<span id="global-error-text"></span>
|
||||
</div>
|
||||
|
||||
<Pagination totalPages={searchTotalPages} currentPage={searchCurrentPages} fileId={fileId} callBack={handlerSearchGlobalFile} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import { IconImageFile, IconDocument, IconAudioFile, IconVideoFile } from '@/app/ui/icons/icons';
|
||||
import { useUserSearchData } from '@/app/hooks/react-query/useUserSearchData';
|
||||
|
||||
export function RecentSearches() {
|
||||
const { data: userSearchData, isLoading, isError, error, } = useUserSearchData()
|
||||
|
||||
return (
|
||||
<div className="recent-searches">
|
||||
<div className="recent-title">
|
||||
Недавние поиски
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type document">
|
||||
<IconDocument /> document</span>
|
||||
<div>
|
||||
<div>{userSearchData?.document?.count ? userSearchData?.document?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type image">
|
||||
<IconImageFile /> image</span>
|
||||
<div>
|
||||
<div>{userSearchData?.image?.count ? userSearchData?.image?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type audio">
|
||||
<IconAudioFile /> audio</span>
|
||||
<div>
|
||||
<div>{userSearchData?.audio?.count ? userSearchData?.audio?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="recent-item">
|
||||
<span className="recent-type video">
|
||||
<IconVideoFile /> video</span>
|
||||
<div>
|
||||
<div>{userSearchData?.video?.count ? userSearchData?.video?.count : 0} поисков</div>
|
||||
<div className="recent-date">
|
||||
date
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { useUserSearchData } from '@/app/hooks/react-query/useUserSearchData';
|
||||
|
||||
export function SearchStats() {
|
||||
const { data: userSearchData, isLoading, isError, error, } = useUserSearchData()
|
||||
|
||||
return (
|
||||
<div className="stats-search">
|
||||
<div className="stats-title">
|
||||
Статистика поиска
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Изображения</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.image?.count ? userSearchData?.image?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Видео</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.video?.count ? userSearchData?.video?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Аудио</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.audio?.count ? userSearchData?.audio?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Документы</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.document?.count ? userSearchData?.document?.count : 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">Всего поисков</span>
|
||||
<span className="stat-value">
|
||||
{userSearchData?.allCountForPeriod ? userSearchData?.allCountForPeriod : 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { SearchItem } from './file-search-panel'
|
||||
|
||||
export function SearchedGlobalFilesList({ list }: { list: SearchItem[] }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{list.map(
|
||||
(
|
||||
item: SearchItem, index: number
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className="global-result-item"
|
||||
key={index}
|
||||
>
|
||||
<div className="global-result-image">
|
||||
<img src={item?.url} alt={item?.pageTitle} />
|
||||
</div>
|
||||
<div className="global-result-info">
|
||||
<div className="global-result-title">
|
||||
{item?.pageTitle}
|
||||
</div>
|
||||
<div className="global-result-meta">
|
||||
{item?.height & item?.width && (
|
||||
<div className="global-result-size">
|
||||
{item?.height}x{item?.width}
|
||||
</div>
|
||||
)}
|
||||
<div className="global-result-host">
|
||||
{item?.host}
|
||||
</div>
|
||||
</div>
|
||||
<div className="global-result-passage">
|
||||
{item?.pageTitle}
|
||||
</div>
|
||||
<a href={item?.pageUrl} target="_blank" className="global-result-url" title={item?.pageUrl}>
|
||||
{item?.pageUrl}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { toast } from 'sonner';
|
||||
import { useState, ReactNode } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import ModalWindow from '@/app/components/ModalWindow';
|
||||
import { IconEye, IconDownload } from '@/app/ui/icons/icons';
|
||||
|
||||
export function SearchedUserFilesList({ list }: { list: any }) {
|
||||
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const handlerDownload = async (fileId: string, fileName: string) => {
|
||||
setIsFileLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/download/${fileId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`error: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName || `file-${fileId}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success(`${t('file-is-downloading')} - ${fileName}`);
|
||||
} catch (error) {
|
||||
toast.error(t('failed-to-download-file'))
|
||||
} finally {
|
||||
setIsFileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerViewfile = async (e: any) => {
|
||||
setOpenWindowChildren(() => {
|
||||
return (
|
||||
<div
|
||||
className="modal-window-view-file"
|
||||
>
|
||||
<div className="modal-window-view-file-header">
|
||||
<div
|
||||
className="modal-window-view-file-title"
|
||||
>
|
||||
{e.originalFileName}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-window-view-file-content"
|
||||
>
|
||||
<div className="modal-window-view-file-content-wrapper">
|
||||
<div className="image-section">
|
||||
<div className="image-container">
|
||||
<img
|
||||
src={e.url ? e.url : '#'}
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="file-info-card"
|
||||
>
|
||||
<div className="info-header">
|
||||
<h4>Информация о файле</h4>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">ID в системе:</span>
|
||||
<span className="info-value">
|
||||
{e.supportId ? e.supportId : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Дата загрузки:</span>
|
||||
<span className="info-value">
|
||||
{e.uploadDate ? e.uploadDate : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Размер файла:</span>
|
||||
<span className="info-value">
|
||||
{convertBytes(e.fileSize)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">Статус защиты:</span>
|
||||
<span className="info-value">
|
||||
{e.status ? e.status : '#'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="user-file-search-results"
|
||||
>
|
||||
<div className="results-header">
|
||||
<div className="results-title">Результаты поиска</div>
|
||||
<div className="results-count">
|
||||
Найдено файлов: {list.length}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="results-list"
|
||||
>
|
||||
{list.map((e: any, index: number) => {
|
||||
if (e.similarityLevel !== 'DIFFERENT') {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
|
||||
>
|
||||
<div className="result-item">
|
||||
<div className="result-header">
|
||||
<div className="result-preview">
|
||||
{e.url ? (
|
||||
<img src={e.url} alt="Preview" />
|
||||
) : (
|
||||
<div className="preview-fallback">
|
||||
<span>📄</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="result-info">
|
||||
<div className="result-filename" title={e.originalFileName}>
|
||||
{e.originalFileName}
|
||||
</div>
|
||||
<div className="result-details">
|
||||
Загружен: {e.uploadDate ? e.uploadDate : '#'} •
|
||||
Размер: {convertBytes(e.fileSize)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="similarity-score">
|
||||
<div className="similarity-percentage">
|
||||
{e.similarityLevel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="result-badges">
|
||||
<span className={`badge ${e.status === 'PROTECTED' ? 'badge-protected' : 'badge-not-protected'}`}>
|
||||
{e.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="result-actions">
|
||||
<button
|
||||
className="btn btn-secondary gap-1"
|
||||
onClick={() => {
|
||||
handlerViewfile(e);
|
||||
}}
|
||||
>
|
||||
<IconEye />
|
||||
Просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-success gap-1"
|
||||
disabled={isFileLoading}
|
||||
onClick={() => handlerDownload(e.fileId, e.originalFileName)}
|
||||
>
|
||||
<IconDownload />
|
||||
Скачать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
+99
-141
@@ -1,20 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
|
||||
import { IconShieldAdd } from '@/app/ui/icons/icons';
|
||||
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;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface FileUploadInitResponse {
|
||||
@@ -25,13 +23,17 @@ interface FileUploadInitResponse {
|
||||
status: string
|
||||
}
|
||||
|
||||
interface UploadSectionFile {
|
||||
fileType: string
|
||||
allowedExtensions: string[]
|
||||
interface SectionSearchFile {
|
||||
allowedExtensions: {
|
||||
images: string[],
|
||||
videos: string[],
|
||||
audios: string[],
|
||||
documents: string[]
|
||||
}
|
||||
maxFileSize: number
|
||||
}
|
||||
|
||||
export default function UploadSectionFile({ fileType, allowedExtensions, maxFileSize }: UploadSectionFile) {
|
||||
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);
|
||||
@@ -39,22 +41,57 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
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 (!allowedExtensions || !Array.isArray(allowedExtensions)) {
|
||||
if (!allExtensions || !Array.isArray(allExtensions)) {
|
||||
return '';
|
||||
}
|
||||
return allowedExtensions.map(e => `.${e}`).join(', ');
|
||||
}, [allowedExtensions]);
|
||||
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 (!allowedExtensions.includes(file.type as string)) {
|
||||
if (!allExtensions.includes(file.type as string)) {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!extension || !allowedExtensions.includes(extension as string)) {
|
||||
if (!extension || !allExtensions.includes(extension as string)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('unsupported-file-format')
|
||||
@@ -74,37 +111,25 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
img.onload = () => {
|
||||
const dimensions = {
|
||||
width: img.width,
|
||||
height: img.height
|
||||
};
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
resolve(dimensions);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
reject(new Error(t('error-uploading-file')));
|
||||
};
|
||||
|
||||
img.src = objectUrl;
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -114,31 +139,21 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
return;
|
||||
}
|
||||
|
||||
let dimensions = undefined;
|
||||
if (fileType === "image") {
|
||||
try {
|
||||
dimensions = await getImageDimensions(file) as { width: number, height: number };
|
||||
if (dimensions.width < 100 || dimensions.height < 100 ||
|
||||
dimensions.width > 10000 || dimensions.height > 10000) {
|
||||
setError(`${t('image-size')}: ${dimensions.width}x${dimensions.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(t('error-reading-image'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedFile({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
dimensions,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
||||
});
|
||||
|
||||
console.log('Файл выбран:', file.name, file.size);
|
||||
}, [fileType, t]);
|
||||
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;
|
||||
@@ -172,29 +187,13 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
handleFileSelect(file);
|
||||
};
|
||||
|
||||
const handleClearFile = async (): Promise<void> => {
|
||||
if (selectedFile && selectedFile.preview) {
|
||||
URL.revokeObjectURL(selectedFile.preview);
|
||||
}
|
||||
|
||||
if (uploadId) {
|
||||
await cancelUpload(uploadId);
|
||||
}
|
||||
|
||||
isCancelledRef.current = true;
|
||||
|
||||
setIsFileUploaded(false);
|
||||
setSelectedFile(null);
|
||||
setError(null);
|
||||
setUploadProgress(0);
|
||||
};
|
||||
|
||||
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() || '';
|
||||
@@ -221,6 +220,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
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);
|
||||
@@ -229,14 +229,20 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
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.message_desc !== 'Chunk uploaded successfully') {
|
||||
throw new Error(`Chunk ${chunkIndex} upload failed`);
|
||||
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);
|
||||
@@ -256,7 +262,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
} finally {
|
||||
setUploadId(null);
|
||||
}
|
||||
}, [uploadId, fileType, t]);
|
||||
}, [uploadId, allowedExtensions]);
|
||||
|
||||
useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
@@ -296,9 +302,13 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
|
||||
return (
|
||||
<div className="upload-section">
|
||||
<h3>
|
||||
{t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })}
|
||||
</h3>
|
||||
<div className="search-info">
|
||||
<div className="search-info-title">Как работает поиск?</div>
|
||||
<div className="search-info-text">
|
||||
Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами,
|
||||
используя алгоритмы компьютерного зрения и цифровых отпечатков.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||
@@ -306,25 +316,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<IconShieldAdd />
|
||||
<IconSearch />
|
||||
<h4>
|
||||
{t('drag-the-file-here')}
|
||||
Выберите файл для поиска
|
||||
</h4>
|
||||
{/* <p className="mb-2.5 text-[#6b7280]">
|
||||
{t('or')}
|
||||
</p> */}
|
||||
|
||||
{fileType === "image" && (
|
||||
<p className="description">
|
||||
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
|
||||
</p>
|
||||
)}
|
||||
<p className="description">
|
||||
{t('file-format')}: {acceptString}
|
||||
</p>
|
||||
<p className="description">
|
||||
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
|
||||
</p>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@@ -339,8 +334,9 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
className="btn btn-primary"
|
||||
onClick={handleButtonClick}
|
||||
type="button"
|
||||
disabled={uploadProgress && uploadProgress !== 100 ? true : false}
|
||||
>
|
||||
{t('select-files-to-protect')}
|
||||
Выбрать файл
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
@@ -351,9 +347,9 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
|
||||
{selectedFile && (
|
||||
<div
|
||||
className={`selected-file ${isFileUploaded ? 'done' : ''}`}
|
||||
className={`selected-file`}
|
||||
>
|
||||
<div className="selected-file-file-info">
|
||||
<div className="selected-file-file-info relative">
|
||||
<div>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">
|
||||
@@ -365,52 +361,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
{t('size')}:
|
||||
</span> {selectedFile.size}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">{t('cost-of-protection')}:</span> 0
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">{t('current-balance')}:</span> 0
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{isFileUploaded && (
|
||||
<div className="font-medium text-green-600 text-2xl">
|
||||
{t('file-successfully-uploaded')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(fileType === 'image' && selectedFile.preview) && (
|
||||
<img
|
||||
src={selectedFile.preview}
|
||||
alt="Предпросмотр загруженного изображения"
|
||||
className="uploaded-image"
|
||||
onError={() => setError('Не удалось загрузить превью изображения')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex gap-3 justify-center relative">
|
||||
<button
|
||||
className="btn btn-cancel"
|
||||
onClick={handleClearFile}
|
||||
type="button"
|
||||
>
|
||||
{isFileUploaded ? t('close') : t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-confirm ${error || isFileUploaded || uploadId ? 'disabled' : ''}`}
|
||||
onClick={() => {
|
||||
handlerFileUpload(selectedFile);
|
||||
}}
|
||||
type="button"
|
||||
disabled={error || isFileUploaded || uploadId ? true : false}
|
||||
>
|
||||
{t('upload-file')}
|
||||
</button>
|
||||
{uploadProgress !== 0 && (
|
||||
<div
|
||||
className={`absolute top-5 right-5 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
|
||||
className={`absolute top-0 right-0 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
|
||||
>
|
||||
{uploadProgress}%
|
||||
</div>
|
||||
@@ -419,6 +373,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isFileUploaded && (
|
||||
<FileSearchPanel fileId={fileId} ref={childRef} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
|
||||
|
||||
interface supportedFormats {
|
||||
extensionVideo: string[]
|
||||
extensionAudio: string[]
|
||||
extensionImage: string[]
|
||||
extensionDocument: string[]
|
||||
}
|
||||
|
||||
export function SupportedFormats({ extensionVideo, extensionAudio, extensionImage, extensionDocument }: supportedFormats) {
|
||||
return (
|
||||
<div className="supported-formats">
|
||||
<div className="formats-title">
|
||||
Поддерживаемые форматы
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type image">
|
||||
<IconImageFile /> Изображения
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionImage.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type video">
|
||||
<IconVideoFile /> Видео
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionVideo.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type audio">
|
||||
<IconAudioFile /> Аудио
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionAudio.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="format-group">
|
||||
<div className="format-type document">
|
||||
<IconDocument /> Документы
|
||||
</div>
|
||||
<div className="format-list">
|
||||
{extensionDocument.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user