NCFRONT-72 #4
+1
-1
@@ -8,7 +8,7 @@ WORKDIR /no-copy-frontend
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=dependencies /no-copy-frontend/node_modules ./node_modules
|
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
|
FROM node:22.20.0 AS runner
|
||||||
WORKDIR /no-copy-frontend
|
WORKDIR /no-copy-frontend
|
||||||
|
|||||||
Generated
+78
-2
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "no-copy-frontend",
|
"name": "no-copy-frontend",
|
||||||
"version": "0.17.0",
|
"version": "0.40.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "no-copy-frontend",
|
"name": "no-copy-frontend",
|
||||||
"version": "0.17.0",
|
"version": "0.40.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@a2seven/yoo-checkout": "^1.1.4",
|
||||||
"@react-input/mask": "^2.0.4",
|
"@react-input/mask": "^2.0.4",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.0.3",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
"use-debounce": "^10.1.0",
|
||||||
"validator": "^13.15.26",
|
"validator": "^13.15.26",
|
||||||
"zod": "^4.1.13",
|
"zod": "^4.1.13",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.9"
|
||||||
@@ -40,6 +42,20 @@
|
|||||||
"typescript": "^5"
|
"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": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
@@ -2313,6 +2329,16 @@
|
|||||||
"tslib": "^2.4.0"
|
"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": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
@@ -3288,6 +3314,15 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
@@ -4653,6 +4688,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -7822,6 +7877,18 @@
|
|||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/use-intl": {
|
||||||
"version": "4.5.8",
|
"version": "4.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz",
|
"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"
|
"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": {
|
"node_modules/validator": {
|
||||||
"version": "13.15.26",
|
"version": "13.15.26",
|
||||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz",
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "no-copy-frontend",
|
"name": "no-copy-frontend",
|
||||||
"version": "0.23.0",
|
"version": "0.42.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 2999",
|
"dev": "next dev -p 2999",
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@a2seven/yoo-checkout": "^1.1.4",
|
||||||
"@react-input/mask": "^2.0.4",
|
"@react-input/mask": "^2.0.4",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.0.3",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
"use-debounce": "^10.1.0",
|
||||||
"validator": "^13.15.26",
|
"validator": "^13.15.26",
|
||||||
"zod": "^4.1.13",
|
"zod": "^4.1.13",
|
||||||
"zustand": "^5.0.9"
|
"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 LogoIcon from '@/app/ui/logo-icon';
|
||||||
import styles from '@/app/styles/module/login.module.scss';
|
import styles from '@/app/styles/module/login.module.scss';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
|
||||||
import ForgotPasswordForm from '@/app/ui/forms/forgot-password-form';
|
import ForgotPasswordForm from '@/app/ui/forms/forgot-password-form';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -10,9 +9,6 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles['login-container-wrapper']}`}>
|
<div className={`${styles['login-container-wrapper']}`}>
|
||||||
<div className={`${styles['login-container-language']}`}>
|
|
||||||
<LanguageSwitcher />
|
|
||||||
</div>
|
|
||||||
<div className={`${styles['login-container']}`}>
|
<div className={`${styles['login-container']}`}>
|
||||||
<div className={`${styles['logo']}`}>
|
<div className={`${styles['logo']}`}>
|
||||||
<LogoIcon />
|
<LogoIcon />
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { routing } from '@/i18n/routing';
|
|||||||
import { ToastProvider } from '@/app/providers/ToastProvider';
|
import { ToastProvider } from '@/app/providers/ToastProvider';
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
import "../styles/global-styles.scss";
|
import "../styles/global-styles.scss";
|
||||||
import ActivityTracker from '../components/ActivityTracker';
|
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return routing.locales.map((locale: any) => ({ locale }));
|
return routing.locales.map((locale: any) => ({ locale }));
|
||||||
@@ -35,7 +34,6 @@ export default async function RootLayout({
|
|||||||
<NextIntlClientProvider>
|
<NextIntlClientProvider>
|
||||||
<Providers>
|
<Providers>
|
||||||
{children}
|
{children}
|
||||||
<ActivityTracker/>
|
|
||||||
<ToastProvider />
|
<ToastProvider />
|
||||||
</Providers>
|
</Providers>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles['login-container-wrapper']}`}>
|
<div className={`${styles['login-container-wrapper']}`}>
|
||||||
<div className={`${styles['login-container-language']}`}>
|
{/* <div className={`${styles['login-container-language']}`}>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
</div>
|
</div> */}
|
||||||
<div className={`${styles['login-container']}`}>
|
<div className={`${styles['login-container']}`}>
|
||||||
<div className={`${styles['logo']}`}>
|
<div className={`${styles['logo']}`}>
|
||||||
<LogoIcon />
|
<LogoIcon />
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function Page() {
|
|||||||
<div className="dashboard-main-grid">
|
<div className="dashboard-main-grid">
|
||||||
<div className="left-column">
|
<div className="left-column">
|
||||||
<DashboardUserFiles />
|
<DashboardUserFiles />
|
||||||
<DashboardPlatformsList />
|
{/* <DashboardPlatformsList /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="right-column">
|
<div className="right-column">
|
||||||
<ViolationsCarousel />
|
<ViolationsCarousel />
|
||||||
@@ -34,8 +34,13 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<DashboardFilesInfo />
|
<DashboardFilesInfo />
|
||||||
<div className="dashboard-main-grid">
|
<div className="dashboard-main-grid">
|
||||||
|
|
||||||
|
{/* <DahboardGeographySection /> */}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
<DahboardLimitsSection />
|
<DahboardLimitsSection />
|
||||||
<DahboardGeographySection />
|
|
||||||
</div>
|
</div>
|
||||||
<DashboardLastCheck />
|
<DashboardLastCheck />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import HeaderPanel from '@/app/ui/header/headerPanel';
|
|||||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||||
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||||
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
||||||
|
import ActivityTracker from '@/app/components/ActivityTracker';
|
||||||
|
|
||||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const queryClient = getQueryClient();
|
const queryClient = getQueryClient();
|
||||||
@@ -20,6 +21,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</HydrationBoundary>
|
</HydrationBoundary>
|
||||||
|
<ActivityTracker/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||||
import PageTitle from '@/app/ui/page-title';
|
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 { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||||
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
||||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
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 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 { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||||
import PageTitle from '@/app/ui/page-title';
|
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 { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||||
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
||||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
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 MyContentInfoBlock from '@/app/ui/my-content/my-content-info-block';
|
||||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||||
import MyContentPieChart from '@/app/ui/my-content/my-content-pie-chart';
|
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 MyContentStatsOverview from '@/app/ui/my-content/new/my-content-stats-overview';
|
||||||
|
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MyContentPageTitleColorFrame title="my-content" description="my-content-description" />
|
<PageTitleColorFrame title="my-content" description="my-content-description"/>
|
||||||
<MyContentStatsOverview />
|
<MyContentStatsOverview />
|
||||||
{/* <div className="split-blocks">
|
{/* <div className="split-blocks">
|
||||||
<MyContentInfoBlock />
|
<MyContentInfoBlock />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function Page() {
|
|||||||
<Suspense fallback={<>...</>}>
|
<Suspense fallback={<>...</>}>
|
||||||
<PageTitleColorFrame title="refferal-program" description="referral-program-description" />
|
<PageTitleColorFrame title="refferal-program" description="referral-program-description" />
|
||||||
<CommissionLevels />
|
<CommissionLevels />
|
||||||
<ReferralStats />
|
{/* <ReferralStats /> */}
|
||||||
<ReferralLinkSection />
|
<ReferralLinkSection />
|
||||||
<InvitationsTable />
|
<InvitationsTable />
|
||||||
<IncomeAndPayments />
|
<IncomeAndPayments />
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month';
|
|||||||
import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography';
|
import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography';
|
||||||
import ActivityByContentType from '@/app/ui/reports/activity-by-content-type';
|
import ActivityByContentType from '@/app/ui/reports/activity-by-content-type';
|
||||||
import PageTitle from '@/app/ui/page-title';
|
import PageTitle from '@/app/ui/page-title';
|
||||||
|
import ReportsInfo from '@/app/ui/reports/reports-info';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PageTitle title="reports-and-analytics" />
|
<PageTitle title="reports-and-analytics" />
|
||||||
<StatsGrid />
|
{/* <StatsGrid /> */}
|
||||||
|
<ReportsInfo />
|
||||||
<ActivityByContentType />
|
<ActivityByContentType />
|
||||||
<div className="analytics-grid">
|
<div className="analytics-grid">
|
||||||
<AnalyticsCardMonth />
|
<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 DangerZone from '@/app/ui/settings/danger-zone';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import PageTitle from '@/app/ui/page-title';
|
import PageTitle from '@/app/ui/page-title';
|
||||||
|
import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
@@ -16,11 +17,12 @@ export default function Page() {
|
|||||||
<PageTitle title="account-settings" />
|
<PageTitle title="account-settings" />
|
||||||
<div className="settings-grid">
|
<div className="settings-grid">
|
||||||
<PersonalDataSettings />
|
<PersonalDataSettings />
|
||||||
<NotificationsMonitoringSettings />
|
{/* <NotificationsMonitoringSettings /> */}
|
||||||
<SafetySetting />
|
<SafetySetting />
|
||||||
<SubscriptionSettings />
|
<SubscriptionSettings />
|
||||||
<APISettings />
|
{/* <APISettings /> */}
|
||||||
<LastActivitySettings />
|
<LastActivitySettings />
|
||||||
|
<CompanyUsersSettingPanel />
|
||||||
</div>
|
</div>
|
||||||
<DangerZone />
|
<DangerZone />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import LogoIcon from '@/app/ui/logo-icon';
|
|||||||
import styles from '@/app/styles/module/login.module.scss'
|
import styles from '@/app/styles/module/login.module.scss'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import RegisterForm from '@/app/ui/forms/register-form';
|
import RegisterForm from '@/app/ui/forms/register-form';
|
||||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
|
||||||
import {useTranslations} from 'next-intl';
|
import {useTranslations} from 'next-intl';
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -10,9 +9,6 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles['login-container-wrapper']}`}>
|
<div className={`${styles['login-container-wrapper']}`}>
|
||||||
<div className={`${styles['login-container-language']}`}>
|
|
||||||
<LanguageSwitcher />
|
|
||||||
</div>
|
|
||||||
<div className={`${styles['login-container']}`}>
|
<div className={`${styles['login-container']}`}>
|
||||||
<div className={`${styles['logo']}`}>
|
<div className={`${styles['logo']}`}>
|
||||||
<LogoIcon />
|
<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'
|
'use server'
|
||||||
|
|
||||||
import { getSessionData, deleteSession } from '@/app/actions/session';
|
import { getSessionData, deleteSession } from '@/app/actions/session';
|
||||||
import { testUserData, API_BASE_URL } from '@/app/actions/definitions';
|
import { testUserData, API_BASE_URL } from '@/app/actions/definitions';
|
||||||
import { tokenLifeExtension } from '@/app/actions/auth';
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
export async function getUserData() {
|
export async function getUserData() {
|
||||||
const userEmail = await getSessionData('email');
|
const userEmail = await getSessionData('email');
|
||||||
const token = await getSessionData('token');
|
const token = await getSessionData('token');
|
||||||
|
|
||||||
/* для теста */
|
/* для теста */
|
||||||
if (userEmail === "test" && token === "1111") {
|
if (userEmail === 'test' && token === '1111') {
|
||||||
return testUserData;
|
return testUserData;
|
||||||
}
|
}
|
||||||
/* для теста */
|
/* для теста */
|
||||||
@@ -18,17 +17,19 @@ export async function getUserData() {
|
|||||||
const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, {
|
const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json",
|
'Accept': 'application/json',
|
||||||
"Authorization": `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('error');
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,13 +43,13 @@ export async function getUserFilesInfo() {
|
|||||||
version: 1,
|
version: 1,
|
||||||
msg_id: 20005,
|
msg_id: 20005,
|
||||||
message_body: {
|
message_body: {
|
||||||
action: "user_files_info",
|
action: 'user_files_info',
|
||||||
token: token
|
token: token
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json"
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,8 +62,100 @@ export async function getUserFilesInfo() {
|
|||||||
} else {
|
} else {
|
||||||
throw parsed.message_code;
|
throw parsed.message_code;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
|
||||||
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
|
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
import { fetchINN } from '@/app/actions/action';
|
||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
const token = await getSessionData('token');
|
const token = await getSessionData('token');
|
||||||
@@ -11,9 +12,9 @@ export async function logout() {
|
|||||||
await fetch(`${API_BASE_URL}/v1/api/auth/logout`, {
|
await fetch(`${API_BASE_URL}/v1/api/auth/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json",
|
'Accept': 'application/json',
|
||||||
"Authorization": `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -36,8 +37,8 @@ export async function authorization(
|
|||||||
const password = formData.get('password');
|
const password = formData.get('password');
|
||||||
|
|
||||||
/* для теста */
|
/* для теста */
|
||||||
if (email === "test" && password === "test") {
|
if (email === 'test' && password === 'test') {
|
||||||
await createSession("1111", "test");
|
await createSession('1111', 'test');
|
||||||
redirect('/pages/dashboard');
|
redirect('/pages/dashboard');
|
||||||
}
|
}
|
||||||
/* для теста */
|
/* для теста */
|
||||||
@@ -81,13 +82,13 @@ export async function authorization(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json"
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
let parsed = await response.json();
|
let parsed = await response.json();
|
||||||
if (parsed.message_desc === 'Operation successful') {
|
if (parsed.message_code === 0) {
|
||||||
await createSession(parsed.message_body.token, email);
|
await createSession(parsed.message_body.token, email);
|
||||||
} else {
|
} else {
|
||||||
throw (`${parsed.message_code}`);
|
throw (`${parsed.message_code}`);
|
||||||
@@ -130,14 +131,16 @@ export async function authorization(
|
|||||||
|
|
||||||
export type FormState = {
|
export type FormState = {
|
||||||
previousState: {
|
previousState: {
|
||||||
fullName: string;
|
fullName?: string | undefined;
|
||||||
email: string;
|
email?: string | undefined;
|
||||||
password: string;
|
password?: string | undefined;
|
||||||
confirm_password: string;
|
confirm_password?: string | undefined;
|
||||||
phone: string;
|
phone?: string | undefined;
|
||||||
companyName: string;
|
companyName?: string | undefined;
|
||||||
agree: string;
|
agree?: string | undefined;
|
||||||
},
|
referralCode?: string | undefined;
|
||||||
|
inn?: string | undefined;
|
||||||
|
} | undefined,
|
||||||
mailConfirm?: boolean,
|
mailConfirm?: boolean,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
error: Record<string, string> | null
|
error: Record<string, string> | null
|
||||||
@@ -147,6 +150,8 @@ export async function registration(
|
|||||||
state: FormState | undefined,
|
state: FormState | undefined,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
): Promise<FormState> {
|
): Promise<FormState> {
|
||||||
|
const accountType = formData.get('accountType') as 'b2b' | 'b2c';
|
||||||
|
|
||||||
const fullName = formData.get('fullName') as string || '';
|
const fullName = formData.get('fullName') as string || '';
|
||||||
const email = formData.get('email') as string || '';
|
const email = formData.get('email') as string || '';
|
||||||
const password = formData.get('password') 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 phone = formData.get('phone') as string || '';
|
||||||
const companyName = formData.get('companyName') as string || '';
|
const companyName = formData.get('companyName') as string || '';
|
||||||
const agree = formData.get('agree') 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 = {
|
const dataToValidate = {
|
||||||
fullName,
|
fullName,
|
||||||
@@ -164,23 +171,41 @@ export async function registration(
|
|||||||
password,
|
password,
|
||||||
confirm_password,
|
confirm_password,
|
||||||
agree,
|
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> = {}
|
const errors: Record<string, string> = {}
|
||||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
if (validatedFields?.error?.message) {
|
||||||
message: string,
|
JSON?.parse(validatedFields?.error?.message).forEach((obj: {
|
||||||
path: string
|
message: string,
|
||||||
}) => {
|
path: string
|
||||||
if (errors[obj.path[0]]) {
|
}) => {
|
||||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
if (errors[obj.path[0]]) {
|
||||||
} else {
|
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||||
errors[obj.path[0]] = obj.message;
|
} else {
|
||||||
}
|
errors[obj.path[0]] = obj.message;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmedCompanyName) {
|
||||||
|
errors.inn = 'inn-invalid'
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
previousState: {
|
previousState: {
|
||||||
@@ -189,8 +214,8 @@ export async function registration(
|
|||||||
password,
|
password,
|
||||||
confirm_password,
|
confirm_password,
|
||||||
phone,
|
phone,
|
||||||
companyName,
|
agree,
|
||||||
agree
|
...(accountType === 'b2b' && { companyName, inn })
|
||||||
},
|
},
|
||||||
error: errors
|
error: errors
|
||||||
};
|
};
|
||||||
@@ -208,25 +233,47 @@ export async function registration(
|
|||||||
email,
|
email,
|
||||||
phone: phone,
|
phone: phone,
|
||||||
password,
|
password,
|
||||||
companyName: companyName,
|
accountType,
|
||||||
|
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||||
|
...(referralCode !== '' && { referralLink: referralCode })
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "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) {
|
if (response.ok) {
|
||||||
let parsed = await response.json() as {
|
let parsed = await response.json() as {
|
||||||
message_desc: string,
|
message_desc: string,
|
||||||
message_body: { token: string, userId: number },
|
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); */
|
/* await createSession(parsed.message_body.token, email as string); */
|
||||||
|
|
||||||
return {
|
return {
|
||||||
previousState: {
|
previousState: {
|
||||||
fullName,
|
fullName,
|
||||||
@@ -234,14 +281,17 @@ export async function registration(
|
|||||||
password,
|
password,
|
||||||
confirm_password,
|
confirm_password,
|
||||||
phone,
|
phone,
|
||||||
companyName,
|
agree,
|
||||||
agree
|
...(referralCode !== '' && { referralLink: referralCode }),
|
||||||
|
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
|
||||||
},
|
},
|
||||||
mailConfirm: true,
|
mailConfirm: true,
|
||||||
userId: parsed.message_body.userId,
|
userId: parsed.message_body.userId,
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
console.log(parsed);
|
||||||
throw parsed;
|
throw parsed;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -249,26 +299,35 @@ export async function registration(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
const errors: Record<string, string> = {};
|
||||||
if (error) {
|
if (error) {
|
||||||
const typedError = error as any;
|
const typedError = error as any;
|
||||||
const errors: Record<string, string> = {};
|
|
||||||
|
|
||||||
switch (typedError.message_code) {
|
switch (typedError.message_code) {
|
||||||
case 1:
|
case 1:
|
||||||
errors['server'] = 'email-or-already-registered'
|
if (typedError.message_desc === 'Refferal link is not exist') {
|
||||||
break;
|
errors['server'] = 'referral-link-is-not-exist'
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
errors['server'] = 'email-or-already-registered'
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 2:
|
case 2:
|
||||||
typedError.message_body.fieldErrors.forEach((obj: {
|
if (typedError.message_body?.fieldErrors) {
|
||||||
code: string,
|
typedError.message_body?.fieldErrors?.forEach((obj: {
|
||||||
field: string
|
code: string,
|
||||||
}) => {
|
field: string
|
||||||
if (errors[obj.field]) {
|
}) => {
|
||||||
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
|
if (errors[obj.field]) {
|
||||||
} else {
|
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
|
||||||
errors[obj.field] = obj.code.replaceAll('.', '-');
|
} else {
|
||||||
}
|
errors[obj.field] = obj.code.replaceAll('.', '-');
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
errors['server'] = 'unknown-error'
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -284,14 +343,27 @@ export async function registration(
|
|||||||
confirm_password,
|
confirm_password,
|
||||||
phone,
|
phone,
|
||||||
companyName,
|
companyName,
|
||||||
agree
|
agree,
|
||||||
|
inn
|
||||||
},
|
},
|
||||||
mailConfirm: false,
|
mailConfirm: false,
|
||||||
error: errors
|
error: errors
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
return {
|
||||||
|
previousState: {
|
||||||
|
fullName,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
confirm_password,
|
||||||
|
phone,
|
||||||
|
companyName,
|
||||||
|
agree,
|
||||||
|
inn
|
||||||
|
},
|
||||||
|
error: errors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect('/pages/dashboard');
|
redirect('/pages/dashboard');
|
||||||
@@ -306,8 +378,8 @@ export async function tokenLifeExtension() {
|
|||||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json",
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -331,11 +403,12 @@ export async function tokenLifeExtension() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmEmail(
|
export async function confirmEmail(
|
||||||
state: {error: string} | undefined,
|
state: { error: string } | undefined,
|
||||||
formData: FormData
|
formData: FormData
|
||||||
) {
|
) {
|
||||||
console.log('confirmEmail');
|
console.log('confirmEmail');
|
||||||
const emailConfirm = formData.get('emailConfirm') as string || '';
|
const emailConfirm = formData.get('emailConfirm') as string || '';
|
||||||
|
console.log(emailConfirm);
|
||||||
const userId = formData.get('userId') as string || '';
|
const userId = formData.get('userId') as string || '';
|
||||||
|
|
||||||
try {
|
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 * 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';
|
import validationConfig from '@/app/lib/validation-config.json';
|
||||||
|
|
||||||
export type FormState =
|
export type FormState =
|
||||||
@@ -35,30 +35,31 @@ interface ValidationConfig {
|
|||||||
|
|
||||||
const defaultConfig = validationConfig as ValidationConfig;
|
const defaultConfig = validationConfig as ValidationConfig;
|
||||||
|
|
||||||
let cachedSignupSchema: any = null;
|
async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise<any> {
|
||||||
let schemaPromise: Promise<any> | null = null;
|
|
||||||
|
|
||||||
async function loadConfigFromBackend(): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/validation-config');
|
const response = await fetch('/api/validation-config');
|
||||||
if (!response.ok) throw new Error('Failed to load config');
|
if (!response.ok) throw new Error('Failed to load config');
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Using default validation config:', error);
|
//сейчас выполняем это пока бека нету
|
||||||
|
/* console.error('Using default validation config:', error); */
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSignupFormSchema() {
|
export async function getSignupFormSchema(schemaType?: 'b2b' | 'b2c' | 'resetPassword') {
|
||||||
console.log('get scheme');
|
let schemaPromise: Promise<any> | null = null;
|
||||||
if (cachedSignupSchema) {
|
let cachedSignupSchema: any = null;
|
||||||
return cachedSignupSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!schemaPromise) {
|
if (!schemaPromise) {
|
||||||
schemaPromise = (async () => {
|
schemaPromise = (async () => {
|
||||||
const config = await loadConfigFromBackend();
|
const config = await loadConfigFromBackend();
|
||||||
cachedSignupSchema = createValidationSchema(config);
|
if (schemaType === 'resetPassword') {
|
||||||
|
cachedSignupSchema = createValidationSchemaForPassword(config);
|
||||||
|
} else {
|
||||||
|
cachedSignupSchema = createValidationSchema(config, schemaType);
|
||||||
|
}
|
||||||
|
|
||||||
return cachedSignupSchema;
|
return cachedSignupSchema;
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
@@ -66,6 +67,7 @@ export async function getSignupFormSchema() {
|
|||||||
return await schemaPromise;
|
return await schemaPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// для валидации в реально времени на клиентской странице
|
||||||
export const SignupFormSchema = createValidationSchema(defaultConfig);
|
export const SignupFormSchema = createValidationSchema(defaultConfig);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export async function getUserFilesData(page: number, pageSize: number) {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "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');
|
const token = await getSessionData('token');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -54,12 +54,13 @@ export async function removeUserFile(fileId: string) {
|
|||||||
message_body: {
|
message_body: {
|
||||||
action: 'delete_file',
|
action: 'delete_file',
|
||||||
file_id: fileId,
|
file_id: fileId,
|
||||||
|
full_delete: fullDelete,
|
||||||
token: token
|
token: token
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json"
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,6 +76,44 @@ export async function removeUserFile(fileId: string) {
|
|||||||
throw (`${response.status}`);
|
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) {
|
} catch (error) {
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||||
import { getSessionData } from '@/app/actions/session';
|
import { getSessionData } from '@/app/actions/session';
|
||||||
|
import { FormState } from '@/app/actions/auth';
|
||||||
|
import { unknown } from 'zod';
|
||||||
|
|
||||||
interface initMessageBody {
|
interface initMessageBody {
|
||||||
file_name: string,
|
file_name: string,
|
||||||
@@ -12,15 +14,18 @@ interface initMessageBody {
|
|||||||
token?: string
|
token?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fileUpload(messageBody: initMessageBody) {
|
export async function fileUpload(messageBody: initMessageBody, convertTo: string | null | undefined) {
|
||||||
const token = await getSessionData('token');
|
const token = await getSessionData('token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const message = messageBody;
|
const message = messageBody;
|
||||||
message.action = "init";
|
message.action = 'init';
|
||||||
message.token = token;
|
message.token = token;
|
||||||
|
|
||||||
|
// нужно будет добавить свойство convertTo в тело запроса он может прийти со значением reject тогда конвертировать не нужно.
|
||||||
|
// console.log(convertTo);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -30,8 +35,8 @@ export async function fileUpload(messageBody: initMessageBody) {
|
|||||||
message_body: message
|
message_body: message
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json"
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,13 +73,13 @@ export async function cancelUpload(udloadId: string) {
|
|||||||
version: 1,
|
version: 1,
|
||||||
msg_id: 20004,
|
msg_id: 20004,
|
||||||
message_body: {
|
message_body: {
|
||||||
action: "cancel",
|
action: 'cancel',
|
||||||
upload_id: udloadId,
|
upload_id: udloadId,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "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 {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, {
|
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -111,17 +121,34 @@ export async function chunkUpload(formData: FormData) {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
let parsed = await response.json();
|
let parsed = await response.json();
|
||||||
|
|
||||||
if (parsed.message_desc === 'Upload cancelled successfully') {
|
if (parsed.message_code === 0) {
|
||||||
return parsed.message_body;
|
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 {
|
} else {
|
||||||
throw parsed;
|
throw 'error'
|
||||||
}
|
}
|
||||||
} else {
|
} 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: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json"
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,8 +200,8 @@ export async function getAllowedFilesExtensions(type: string) {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "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() {
|
export async function deleteSession() {
|
||||||
console.log('delete session')
|
console.log('delete session')
|
||||||
const cookieStore = await cookies()
|
try {
|
||||||
cookieStore.delete('session');
|
const cookieStore = await cookies()
|
||||||
redirect('/login');
|
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}`, {
|
const response = await fetch(`${API_BASE_URL}/api/v1/vk/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json"
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
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}`, {
|
const response = await fetch(`${API_BASE_URL}/api/v1/yandex/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
"Accept": "application/json"
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (response.ok) {
|
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 { useEffect, useRef, useCallback } from 'react';
|
||||||
import { tokenLifeExtension } from '@/app/actions/auth';
|
import { tokenLifeExtension } from '@/app/actions/auth';
|
||||||
|
import { usePathname, useSearchParams } from 'next/navigation';
|
||||||
|
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||||
|
|
||||||
export default function ActivityTracker() {
|
export default function ActivityTracker() {
|
||||||
const lastRefreshTime = useRef(Date.now());
|
const lastRefreshTime = useRef(Date.now());
|
||||||
const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
|
const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
|
||||||
const isRefreshing = useRef(false);
|
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 функция
|
// Debounce функция
|
||||||
const debounce = useCallback((func: () => void, delay: number) => {
|
const debounce = useCallback((func: () => void, delay: number) => {
|
||||||
if (activityTimeoutRef.current) {
|
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,
|
SortingState,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
} from '@tanstack/react-table';
|
} 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 { 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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { getUserFilesData, removeUserFile } from '@/app/actions/fileEntity';
|
import { getUserFilesData, removeUserFile, viewFileInfo } from '@/app/actions/fileEntity';
|
||||||
import ModalWindow from '@/app/components/modalWindow';
|
import ModalWindow from '@/app/components/ModalWindow';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { pluralize } from '@/app/lib/pluralize';
|
import { pluralize } from '@/app/lib/pluralize';
|
||||||
import { convertBytes } from '@/app/lib/convertBytes';
|
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';
|
type FileType = 'image' | 'video' | 'audio';
|
||||||
|
|
||||||
@@ -31,7 +33,9 @@ type FileItem = {
|
|||||||
size?: number | undefined;
|
size?: number | undefined;
|
||||||
uploadDate?: number;
|
uploadDate?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
protectStatus: string;
|
||||||
_original?: ApiFile;
|
_original?: ApiFile;
|
||||||
|
supportId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ApiFile = {
|
type ApiFile = {
|
||||||
@@ -42,32 +46,15 @@ type ApiFile = {
|
|||||||
fileSize: number;
|
fileSize: number;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
protectStatus: string;
|
||||||
|
supportId: number;
|
||||||
|
fileName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ApiResponse = {
|
type ApiResponse = {
|
||||||
files?: ApiFile[];
|
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
|
// Форматирование даты из timestamp
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (timestamp: number) => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
@@ -130,10 +117,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
if (!data?.files) return [];
|
if (!data?.files) return [];
|
||||||
|
|
||||||
return data.files.map((item: ApiFile) => {
|
return data.files.map((item: ApiFile) => {
|
||||||
const [datePart, timePart] = item.updatedAt.split(' ');
|
const newDate = new Date(item.updatedAt).getTime();
|
||||||
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();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -143,6 +127,8 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
size: item.fileSize,
|
size: item.fileSize,
|
||||||
uploadDate: newDate,
|
uploadDate: newDate,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
|
protectStatus: item.protectStatus,
|
||||||
|
supportId: item.supportId,
|
||||||
_original: item
|
_original: item
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -171,6 +157,41 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
// Определение колонок
|
// Определение колонок
|
||||||
const columns = useMemo<ColumnDef<FileItem>[]>(
|
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',
|
accessorKey: 'fileName',
|
||||||
header: ({ column }) => (
|
header: ({ column }) => (
|
||||||
@@ -207,7 +228,9 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
{cutFileName(row.original.fileName)}
|
{cutFileName(row.original.fileName)}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,7 +387,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
return (
|
return (
|
||||||
<div className="text-center font-semibold table-item">
|
<div className="text-center font-semibold table-item">
|
||||||
<span className="table-item-status">
|
<span className="table-item-status">
|
||||||
{t('status')}
|
{row.original.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -382,31 +405,13 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="actions-group" style={{ display: "none" }}>
|
<div className="actions-group" style={{ display: "none" }}>
|
||||||
<button
|
|
||||||
onClick={() => handleView(row.original)}
|
|
||||||
className="bg-blue-500 hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
<IconEye />
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleProtect(row.original)}
|
onClick={() => handleProtect(row.original)}
|
||||||
className="bg-violet-500 hover:bg-violet-600"
|
className="bg-violet-500 hover:bg-violet-600"
|
||||||
>
|
>
|
||||||
<IconShieldExclamation />
|
<IconShieldExclamation />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
{/* <button
|
||||||
<div className="actions-group">
|
|
||||||
<button
|
|
||||||
onClick={() => handleDownload(row.original)}
|
|
||||||
disabled={isFileLoading}
|
|
||||||
className="table-action-download"
|
|
||||||
>
|
|
||||||
<IconFileDownload />
|
|
||||||
<span>
|
|
||||||
{t('download')}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleOpenWindowForRemove(row.original)}
|
onClick={() => handleOpenWindowForRemove(row.original)}
|
||||||
className="table-action-delete"
|
className="table-action-delete"
|
||||||
>
|
>
|
||||||
@@ -414,6 +419,31 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
<span>
|
<span>
|
||||||
{t('delete')}
|
{t('delete')}
|
||||||
</span>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -426,9 +456,19 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Обработчики действий
|
const handleView = async (file: FileItem) => {
|
||||||
const handleView = (file: FileItem) => {
|
|
||||||
console.log(`Просмотр файла: ${file.fileName}`);
|
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) => {
|
const handleDownload = async (file: FileItem) => {
|
||||||
@@ -445,7 +485,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = file.fileName || `file-${file.id}`;
|
a.download = file._original?.fileName || file.fileName || `file-${file.id}`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
@@ -474,7 +514,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
<div className="flex justify-center gap-4">
|
<div className="flex justify-center gap-4">
|
||||||
<button className="btn-primary btn-modal"
|
<button className="btn-primary btn-modal"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteMutation.mutate(file.id)
|
deleteMutation.mutate({
|
||||||
|
fileId: file.id,
|
||||||
|
removeParam: 1,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
disabled={deleteMutation.isPending}
|
disabled={deleteMutation.isPending}
|
||||||
>
|
>
|
||||||
@@ -496,9 +539,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
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'] });
|
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
|
||||||
|
|
||||||
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
|
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
|
||||||
@@ -515,7 +559,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
toast.error(t('error'));
|
toast.error(t('error'));
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: (response, fileId) => {
|
onSuccess: (response, { fileId }) => {
|
||||||
if (response === fileId) {
|
if (response === fileId) {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['userFilesData'],
|
queryKey: ['userFilesData'],
|
||||||
@@ -651,7 +695,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
|||||||
</h3>
|
</h3>
|
||||||
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
|
<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-wrapper">
|
||||||
<div className="table-filtres-item">
|
<div className="table-filtres-item">
|
||||||
<div className="table-filtres-label">{t('date-filter')}:</div>
|
<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) {
|
export function convertBytes(bytes: number) {
|
||||||
|
if (!bytes) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
|
||||||
if (bytes === 0) return '0 B';
|
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 { 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 { getUserFilesData } from '@/app/actions/fileEntity';
|
||||||
|
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||||
|
|
||||||
export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -19,5 +20,20 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
|||||||
queryKey: ['userFilesInfo'],
|
queryKey: ['userFilesInfo'],
|
||||||
queryFn: () => getUserFilesInfo()
|
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; */
|
/* 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> = {};
|
const schema: Record<string, any> = {};
|
||||||
|
|
||||||
// fullName
|
// fullName
|
||||||
@@ -38,6 +38,22 @@ export function createValidationSchema(config: ValidationConfig) {
|
|||||||
.trim();
|
.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
|
// email
|
||||||
if (config.email) {
|
if (config.email) {
|
||||||
let emailSchema = z
|
let emailSchema = z
|
||||||
@@ -147,79 +163,92 @@ export function createValidationSchema(config: ValidationConfig) {
|
|||||||
schema.password = passwordSchema;
|
schema.password = passwordSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
// companyName (опциональное поле)
|
// inn
|
||||||
if (config.companyName) {
|
if (config.inn && accountType !== 'b2c') {
|
||||||
let companySchema = z.string();
|
let innSchema = z.string();
|
||||||
|
|
||||||
// Если поле опциональное, добавляем .optional().nullable()
|
// Минимальная длина
|
||||||
if (config.companyName.optional?.value) {
|
if (config.inn.minSize) {
|
||||||
companySchema = companySchema
|
innSchema = innSchema.min(config.inn.minSize.value, {
|
||||||
.optional()
|
error: config.inn.minSize.message
|
||||||
.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.companyName.maxSize) {
|
if (config.inn.maxSize) {
|
||||||
companySchema = companySchema.refine(
|
innSchema = innSchema.max(config.inn.maxSize.value, {
|
||||||
(value) => {
|
error: config.inn.maxSize.message
|
||||||
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.companyName.allowedSymbols?.value) {
|
if (config.inn.allowedSymbols) {
|
||||||
companySchema = companySchema.refine(
|
innSchema = innSchema.regex(
|
||||||
(value) => {
|
new RegExp(config.inn.allowedSymbols.value),
|
||||||
if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) {
|
{ message: config.inn.allowedSymbols.message }
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const regex = new RegExp(config.companyName.allowedSymbols!.value);
|
|
||||||
return regex.test(value || '');
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: config.companyName.allowedSymbols.message
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
companySchema = companySchema.transform((val) => val?.trim() || '') as unknown as z.ZodString;
|
schema.inn = innSchema;
|
||||||
|
|
||||||
schema.companyName = companySchema;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Дополнительные поля для формы регистрации
|
// Дополнительные поля для формы регистрации
|
||||||
schema.confirm_password = z.string();
|
schema.confirm_password = z.string();
|
||||||
schema.agree = z.string().min(2, { error: 'register-error-agree' });
|
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
|
return z
|
||||||
.object(schema)
|
.object(schema)
|
||||||
.refine((data) => data.password === data.confirm_password, {
|
.refine((data) => data.password === data.confirm_password, {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
"message": "register-error-company-name-min"
|
"message": "register-error-company-name-min"
|
||||||
},
|
},
|
||||||
"allowedSymbols": {
|
"allowedSymbols": {
|
||||||
"value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]+$",
|
"value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]*$",
|
||||||
"message": "register-error-company-name-not-allowed-symbols"
|
"message": "register-error-company-name-not-allowed-symbols"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -80,12 +80,26 @@
|
|||||||
"message": "register-error-phone-max"
|
"message": "register-error-phone-max"
|
||||||
},
|
},
|
||||||
"minSize": {
|
"minSize": {
|
||||||
"value": 2,
|
"value": 12,
|
||||||
"message": "register-error-phone-min"
|
"message": "register-error-phone-min"
|
||||||
},
|
},
|
||||||
"allowedSymbols": {
|
"allowedSymbols": {
|
||||||
"value": "^[0-9\\+]+$",
|
"value": "^[0-9\\+]+$",
|
||||||
"message": "register-error-phone-not-allowed-symbols"
|
"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;
|
color: v.$white;
|
||||||
max-width: calc(100vw - var(--side-bar-width) - 60px);
|
max-width: calc(100vw - var(--side-bar-width) - 60px);
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -73,6 +77,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 28px;
|
gap: 28px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-stat-item {
|
.header-stat-item {
|
||||||
@@ -101,6 +109,10 @@
|
|||||||
padding: 30px;
|
padding: 30px;
|
||||||
background: v.$bg-light;
|
background: v.$bg-light;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-containter {
|
.main-containter {
|
||||||
@@ -121,6 +133,10 @@
|
|||||||
&.auto {
|
&.auto {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@@ -142,7 +158,12 @@
|
|||||||
color: v.$white;
|
color: v.$white;
|
||||||
border: 2px solid transparent;
|
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);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 25px v.$shadow-1;
|
box-shadow: 0 8px 25px v.$shadow-1;
|
||||||
}
|
}
|
||||||
@@ -244,6 +265,11 @@
|
|||||||
color: v.$text-p;
|
color: v.$text-p;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0px 4px 20px v.$shadow-1;
|
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);
|
width: var(--side-bar-width);
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.build-versions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
color: #fffc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-close-button {
|
.sidebar-close-button {
|
||||||
@@ -324,7 +357,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sub-link {
|
.sub-link {
|
||||||
padding: 12px 50px;
|
padding: 12px 12px 12px 32px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
@@ -428,6 +461,13 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-document {
|
||||||
|
color: v.$white;
|
||||||
|
background: v.$color-document;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-blocks {
|
.split-blocks {
|
||||||
|
|||||||
@@ -14,11 +14,19 @@
|
|||||||
padding: 20px 20px;
|
padding: 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
padding: 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.header-action {
|
.header-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -36,6 +44,11 @@
|
|||||||
background: v.$bg-hover;
|
background: v.$bg-hover;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,12 +81,20 @@
|
|||||||
@media (max-width: 490px) {
|
@media (max-width: 490px) {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tokens-count {
|
.tokens-count {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tokens-label {
|
.tokens-label {
|
||||||
@@ -192,6 +213,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-dropdown {
|
.user-dropdown {
|
||||||
|
|||||||
@@ -41,8 +41,35 @@
|
|||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 18px;
|
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;
|
transition: all 0.3s;
|
||||||
background: #f9fafb;
|
background: #f9fafb;
|
||||||
|
|
||||||
|
&-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
&[type="password"]::-ms-reveal {
|
&[type="password"]::-ms-reveal {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -141,11 +172,12 @@
|
|||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
margin: 20px 0 0 0;
|
margin: 20px 0 0 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
background: linear-gradient(135deg, #414291, #4b3975);
|
background: linear-gradient(135deg, #414291, #4b3975);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -34,13 +34,15 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 1.6rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
@@ -214,6 +216,7 @@
|
|||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
max-width: 350px;
|
||||||
|
|
||||||
.plan-header {
|
.plan-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ $bg-darkening: #0000007a;
|
|||||||
$shadow-1: #0000001a;
|
$shadow-1: #0000001a;
|
||||||
$white: #fff;
|
$white: #fff;
|
||||||
$red: #dc2626;
|
$red: #dc2626;
|
||||||
|
$p-color-disabled: #6365f18e;
|
||||||
|
$s-color-disabled: #8a5cf663;
|
||||||
|
|
||||||
$color-image: #f08c00;
|
$color-image: #f08c00;
|
||||||
$color-video: #2f9e44;
|
$color-video: #2f9e44;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import TanstakFilesTable from '@/app/components/tanstakTable';
|
import TanstakFilesTable from '@/app/components/TanstakTable';
|
||||||
|
|
||||||
type FilesTable = {
|
type FilesTable = {
|
||||||
fileType: string
|
fileType: string
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
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 {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
|
|
||||||
export default function DashboardFilesInfo() {
|
export default function DashboardFilesInfo() {
|
||||||
const t = useTranslations("Global");
|
const t = useTranslations("Global");
|
||||||
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
const {
|
|
||||||
data: filesInfo,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userFilesInfo'],
|
|
||||||
queryFn: getUserFilesInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-files-info">
|
<div className="dashboard-files-info">
|
||||||
@@ -24,20 +14,20 @@ export default function DashboardFilesInfo() {
|
|||||||
<div className="stat-type">
|
<div className="stat-type">
|
||||||
{t('images')}
|
{t('images')}
|
||||||
</div>
|
</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="stat-label">{t('your-image')}</div>
|
||||||
<div className="type-card-stats">
|
<div className="type-card-stats">
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('size')}</span>
|
<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>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('checks')}</span>
|
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||||
<span className="type-card-stat-value">{filesInfo?.images_check ? filesInfo?.images_check : 0}</span>
|
<span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('violations')}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,20 +36,20 @@ export default function DashboardFilesInfo() {
|
|||||||
<div className="stat-type">
|
<div className="stat-type">
|
||||||
{t('videos')}
|
{t('videos')}
|
||||||
</div>
|
</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="stat-label">{t('your-videos')}</div>
|
||||||
<div className="type-card-stats">
|
<div className="type-card-stats">
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('size')}</span>
|
<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>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('checks')}</span>
|
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||||
<span className="type-card-stat-value">{filesInfo?.videos_check ? filesInfo?.videos_check : 0}</span>
|
<span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('violations')}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,20 +58,20 @@ export default function DashboardFilesInfo() {
|
|||||||
<div className="stat-type">
|
<div className="stat-type">
|
||||||
{t('audios')}
|
{t('audios')}
|
||||||
</div>
|
</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="stat-label">{t('your-audios')}</div>
|
||||||
<div className="type-card-stats">
|
<div className="type-card-stats">
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('size')}</span>
|
<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>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('checks')}</span>
|
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||||
<span className="type-card-stat-value">{filesInfo?.audios_check ? filesInfo?.audios_check : 0}</span>
|
<span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('violations')}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,20 +80,20 @@ export default function DashboardFilesInfo() {
|
|||||||
<div className="stat-type">
|
<div className="stat-type">
|
||||||
{t('documents')}
|
{t('documents')}
|
||||||
</div>
|
</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="stat-label">{t('your-documents')}</div>
|
||||||
<div className="type-card-stats">
|
<div className="type-card-stats">
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('size')}</span>
|
<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>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('checks')}</span>
|
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||||
<span className="type-card-stat-value">{filesInfo?.documents_check ? filesInfo?.documents_check : 0}</span>
|
<span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="type-card-stat-row">
|
<div className="type-card-stat-row">
|
||||||
<span className="type-card-stat-label">{t('violations')}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function DashboardLastCheck() {
|
export default function DashboardLastCheck() {
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-last-check">
|
<div className="dashboard-last-check">
|
||||||
@@ -7,12 +9,18 @@ export default function DashboardLastCheck() {
|
|||||||
17.10.2025 в 10:23</div>
|
17.10.2025 в 10:23</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="last-check-actions">
|
<div className="last-check-actions">
|
||||||
<button className="btn-report">
|
<Link
|
||||||
|
className="btn-report"
|
||||||
|
href={'reports'}
|
||||||
|
>
|
||||||
📄 Отчёт
|
📄 Отчёт
|
||||||
</button>
|
</Link>
|
||||||
<button className="btn-new-search">
|
<Link
|
||||||
|
className="btn-new-search"
|
||||||
|
href={'violations'}
|
||||||
|
>
|
||||||
🔍 Мониторинг нарушений
|
🔍 Мониторинг нарушений
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</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() {
|
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 (
|
return (
|
||||||
<div className="content-section">
|
<div className="content-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">Ваши файлы</h3>
|
<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>
|
||||||
|
|
||||||
<div className="user-files-list">
|
<div className="user-files-list">
|
||||||
<div className="file-item">
|
|
||||||
<div className="file-icon video">
|
{tableData?.length ? (
|
||||||
🎬</div>
|
tableData?.map((file) => {
|
||||||
<div className="file-info">
|
return (
|
||||||
<div className="file-name" title="Запись экрана 2026-01-09 в 15.45.28.mov">
|
<div
|
||||||
Запись экрана 2026-01-09 в 15.45.28.mov</div>
|
key={file.id}
|
||||||
<div className="file-meta">
|
className="file-item"
|
||||||
11.3 МБ •
|
>
|
||||||
11.01.2026</div>
|
<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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,29 +1,19 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
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 {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
|
|
||||||
export default function DashboardUserStats() {
|
export default function DashboardUserStats() {
|
||||||
const t = useTranslations("Global");
|
const t = useTranslations("Global");
|
||||||
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
const {
|
|
||||||
data: filesInfo,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userFilesInfo'],
|
|
||||||
queryFn: getUserFilesInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-user-stats">
|
<div className="dashboard-user-stats">
|
||||||
|
|
||||||
<div className="stat-card projects">
|
<div className="stat-card projects">
|
||||||
<div className="stat-type">ЗАЩИЩЁННЫЕ ФАЙЛЫ</div>
|
<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-label">Файлов под защитой</div>
|
||||||
<div className="stat-icon">🛡️</div>
|
<div className="stat-icon">🛡️</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +27,7 @@ export default function DashboardUserStats() {
|
|||||||
|
|
||||||
<div className="stat-card violations">
|
<div className="stat-card violations">
|
||||||
<div className="stat-type">НАРУШЕНИЯ</div>
|
<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-label">Требуют внимания</div>
|
||||||
<div className="stat-icon">⚠️</div>
|
<div className="stat-icon">⚠️</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +35,7 @@ export default function DashboardUserStats() {
|
|||||||
<div className="stat-card storage">
|
<div className="stat-card storage">
|
||||||
<div className="stat-type">ХРАНИЛИЩЕ</div>
|
<div className="stat-type">ХРАНИЛИЩЕ</div>
|
||||||
<div className="stat-value">
|
<div className="stat-value">
|
||||||
{filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}
|
{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-label">Использовано места</div>
|
<div className="stat-label">Использовано места</div>
|
||||||
<div className="stat-icon">💾</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() {
|
export default function DahboardLimitsSection() {
|
||||||
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-limits-section">
|
<div className="dashboard-limits-section">
|
||||||
<h3 className="section-title">💰 Использование лимитов</h3>
|
<h3 className="section-title">💰 Использование лимитов</h3>
|
||||||
@@ -9,13 +17,13 @@ export default function DahboardLimitsSection() {
|
|||||||
<span>🔍</span>
|
<span>🔍</span>
|
||||||
<span>Проверки</span>
|
<span>Проверки</span>
|
||||||
</div>
|
</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>
|
||||||
<div className="progress-bar-container">
|
<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>
|
||||||
<div className="progress-bar-value">
|
<div className="progress-bar-value">
|
||||||
5%
|
{getProcents(filesInfo?.total.check, 10000)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -25,13 +33,13 @@ export default function DahboardLimitsSection() {
|
|||||||
<span>💾</span>
|
<span>💾</span>
|
||||||
<span>Хранилище</span>
|
<span>Хранилище</span>
|
||||||
</div>
|
</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>
|
||||||
<div className="progress-bar-container">
|
<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>
|
||||||
<div className="progress-bar-value">
|
<div className="progress-bar-value">
|
||||||
77%
|
{getProcents(filesInfo?.total.size, 1000000000)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="limit-item">
|
<div className="limit-item">
|
||||||
@@ -40,13 +48,13 @@ export default function DahboardLimitsSection() {
|
|||||||
<span>📁</span>
|
<span>📁</span>
|
||||||
<span>Файлы</span>
|
<span>Файлы</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="limit-value">386 / 500</div>
|
<div className="limit-value">{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0} / 500</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="progress-bar-container">
|
<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>
|
||||||
<div className="progress-bar-value">
|
<div className="progress-bar-value">
|
||||||
77%
|
{getProcents(filesInfo?.total.quantity, 500)}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,24 +7,33 @@ import 'swiper/css';
|
|||||||
import 'swiper/css/pagination';
|
import 'swiper/css/pagination';
|
||||||
import testImage from '@/app/src/image-preview.png'
|
import testImage from '@/app/src/image-preview.png'
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function ViolationsCarousel() {
|
export default function ViolationsCarousel() {
|
||||||
const paginationRef = useRef(null);
|
const paginationRef = useRef(null);
|
||||||
|
const t = useTranslations('Global');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-section">
|
<div className="content-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">Нарушения</h3>
|
<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>
|
||||||
<div className="violations-carousel">
|
<div>
|
||||||
|
{t('are-no-violations')}
|
||||||
|
</div>
|
||||||
|
{/* <div className="violations-carousel">
|
||||||
<Swiper
|
<Swiper
|
||||||
spaceBetween={50}
|
spaceBetween={50}
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
modules={[Pagination, Autoplay]}
|
modules={[Pagination, Autoplay]}
|
||||||
pagination={{
|
pagination={{
|
||||||
/* dynamicBullets: true, */
|
|
||||||
/* el: paginationRef.current, */
|
|
||||||
clickable: true,
|
clickable: true,
|
||||||
el: '.custon-swiper-pagination'
|
el: '.custon-swiper-pagination'
|
||||||
}}
|
}}
|
||||||
@@ -33,7 +42,6 @@ export default function ViolationsCarousel() {
|
|||||||
delay: 3000,
|
delay: 3000,
|
||||||
disableOnInteraction: false,
|
disableOnInteraction: false,
|
||||||
}}
|
}}
|
||||||
/* width={300} */
|
|
||||||
>
|
>
|
||||||
<SwiperSlide>
|
<SwiperSlide>
|
||||||
<div className="violation-slide">
|
<div className="violation-slide">
|
||||||
@@ -260,7 +268,7 @@ export default function ViolationsCarousel() {
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -5,23 +5,9 @@ import { PieChartComponent } from '@/app/components/PieChartComponent';
|
|||||||
import { useTranslations, useLocale } from 'next-intl';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
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 { convertBytes } from '@/app/lib/convertBytes';
|
||||||
import { pluralize } from '@/app/lib/pluralize';
|
import { pluralize } from '@/app/lib/pluralize';
|
||||||
|
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
type FilesInfo = {
|
|
||||||
totalSize: number;
|
|
||||||
totalCount: number;
|
|
||||||
imageQuantity: number;
|
|
||||||
videoQuantity: number;
|
|
||||||
audioQuantity: number;
|
|
||||||
imageSize: number;
|
|
||||||
videoSize: number;
|
|
||||||
audioSize: number;
|
|
||||||
documentSize: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default function ProtectionOverview() {
|
export default function ProtectionOverview() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -42,68 +28,23 @@ export default function ProtectionOverview() {
|
|||||||
openDropDownList
|
openDropDownList
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilesCount([
|
setFilesCount([
|
||||||
{
|
{
|
||||||
name: 'images-few',
|
name: 'images-few',
|
||||||
value: filesInfo?.imageSize ? filesInfo?.imageSize : 0,
|
value: filesInfo?.images.size ? filesInfo?.images.size : 0,
|
||||||
color: '#f08c00'
|
color: '#f08c00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'videos',
|
name: 'videos',
|
||||||
value: filesInfo?.videoSize ? filesInfo?.videoSize : 0,
|
value: filesInfo?.videos.size ? filesInfo?.videos.size : 0,
|
||||||
color: '#2f9e44'
|
color: '#2f9e44'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'audios',
|
name: 'audios',
|
||||||
value: filesInfo?.audioSize ? filesInfo?.audioSize : 0,
|
value: filesInfo?.audios.size ? filesInfo?.audios.size : 0,
|
||||||
color: '#1971c2'
|
color: '#1971c2'
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -134,14 +75,14 @@ export default function ProtectionOverview() {
|
|||||||
<div className={`protection-stats ${isOpen ? "opened" : ""}`}>
|
<div className={`protection-stats ${isOpen ? "opened" : ""}`}>
|
||||||
|
|
||||||
|
|
||||||
{filesInfo?.totalCount ? (
|
{filesInfo?.total.quantity ? (
|
||||||
<>
|
<>
|
||||||
<div className="protection-stat total-files">
|
<div className="protection-stat total-files">
|
||||||
<span className="protection-stat-value">
|
<span className="protection-stat-value">
|
||||||
{filesInfo?.totalCount ? filesInfo?.totalCount : 0}
|
{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0}
|
||||||
</span>
|
</span>
|
||||||
<span className="protection-stat-label">
|
<span className="protection-stat-label">
|
||||||
{pluralizeFiles(filesInfo?.totalCount || 0)}
|
{pluralizeFiles(filesInfo?.total.quantity || 0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="protection-stat total-checks">
|
<div className="protection-stat total-checks">
|
||||||
@@ -180,7 +121,7 @@ export default function ProtectionOverview() {
|
|||||||
<div
|
<div
|
||||||
className="flex flex-col text-center"
|
className="flex flex-col text-center"
|
||||||
>
|
>
|
||||||
<Link href='/pages/marking-photo'>
|
<Link href='/pages/marking-images'>
|
||||||
{t('photo-marking')}
|
{t('photo-marking')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href='/pages/marking-video'>
|
<Link href='/pages/marking-video'>
|
||||||
@@ -196,18 +137,18 @@ export default function ProtectionOverview() {
|
|||||||
)}
|
)}
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<div className="protection-stat total-usage">
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="protection-stat total-usage">
|
<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">
|
<div className="protection-stat-label">
|
||||||
{t('disk-space-used')}
|
{t('disk-space-used')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!!filesInfo?.totalCount && (
|
{!!filesInfo?.total.quantity && (
|
||||||
<button
|
<button
|
||||||
className="protection-overview-switch cursor-pointer"
|
className="protection-overview-switch cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -2,22 +2,12 @@
|
|||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
import { IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { convertBytes } from '@/app/lib/convertBytes';
|
||||||
import { getUserFilesInfo } from '@/app/actions/action';
|
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
import {convertBytes} from '@/app/lib/convertBytes';
|
|
||||||
|
|
||||||
export default function StatsGrid() {
|
export default function StatsGrid() {
|
||||||
const t = useTranslations("Global");
|
const t = useTranslations("Global");
|
||||||
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
const {
|
|
||||||
data: filesInfo,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userFilesInfo'],
|
|
||||||
queryFn: getUserFilesInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block-wrapper stats-wrapper">
|
<div className="block-wrapper stats-wrapper">
|
||||||
@@ -46,10 +36,10 @@ export default function StatsGrid() {
|
|||||||
{t('images')}
|
{t('images')}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item second-row">
|
<div className="stats-item second-row">
|
||||||
{filesInfo?.images_quantity ? filesInfo?.images_quantity : 0}
|
{filesInfo?.images.quantity ? filesInfo?.images.quantity : 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item second-row">
|
<div className="stats-item second-row">
|
||||||
{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}
|
{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item second-row">
|
<div className="stats-item second-row">
|
||||||
0
|
0
|
||||||
@@ -63,10 +53,10 @@ export default function StatsGrid() {
|
|||||||
{t('videos')}
|
{t('videos')}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item">
|
<div className="stats-item">
|
||||||
{filesInfo?.videos_quantity ? filesInfo?.videos_quantity : 0}
|
{filesInfo?.videos.quantity ? filesInfo?.videos.quantity : 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item">
|
<div className="stats-item">
|
||||||
{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}
|
{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item">
|
<div className="stats-item">
|
||||||
0
|
0
|
||||||
@@ -80,10 +70,10 @@ export default function StatsGrid() {
|
|||||||
{t('audios')}
|
{t('audios')}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item last-row">
|
<div className="stats-item last-row">
|
||||||
{filesInfo?.audios_quantity ? filesInfo?.audios_quantity : 0}
|
{filesInfo?.audios.quantity ? filesInfo?.audios.quantity : 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item last-row">
|
<div className="stats-item last-row">
|
||||||
{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}
|
{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item last-row">
|
<div className="stats-item last-row">
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -1,110 +1,5 @@
|
|||||||
/* deleted */
|
/* 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 Link from 'next/link';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
|
|
||||||
import styles from '@/app/styles/module/login.module.scss'
|
import styles from '@/app/styles/module/login.module.scss'
|
||||||
import { useActionState } from 'react';
|
import { useActionState } from 'react';
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { resetPassword } from '@/app/actions/auth';
|
||||||
|
|
||||||
export default function ForgotPasswordForm() {
|
export default function ForgotPasswordForm() {
|
||||||
const [state, formAction, isPending] = useActionState(
|
const [state, formAction, isPending] = useActionState(
|
||||||
() => {
|
resetPassword,
|
||||||
console.log("test");
|
|
||||||
},
|
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const t = useTranslations('Login-register-form');
|
const t = useTranslations('Login-register-form');
|
||||||
@@ -27,26 +25,14 @@ export default function ForgotPasswordForm() {
|
|||||||
className={`${styles['form-input']}`}
|
className={`${styles['form-input']}`}
|
||||||
placeholder={t('enter-email')}
|
placeholder={t('enter-email')}
|
||||||
/>
|
/>
|
||||||
{/* {state?.error?.email && (
|
{state?.error === 'error' && (
|
||||||
<p className={`${styles['form-error']}`}>
|
<p className={`${styles['form-error']}`}>
|
||||||
{t(state?.error?.email)}
|
{t('register-error-email-invalid')}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
<button type="submit" className={`${styles['btn']}`}>
|
<button type="submit" className={`${styles['btn']}`}>
|
||||||
{t('sign-in')}
|
{t('recover-password')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,8 +96,18 @@ export default function LoginForm() {
|
|||||||
{t('forgot-password')}?
|
{t('forgot-password')}?
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" className={`${styles['btn']}`}>
|
<button
|
||||||
{t('sign-in')}
|
type="submit"
|
||||||
|
className={`${styles['btn']}`}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<div className="loading-animation">
|
||||||
|
<div className="global-spinner"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t("sign-in")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import styles from '@/app/styles/module/login.module.scss';
|
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 { registration, FormState } from '@/app/actions/auth';
|
||||||
|
|
||||||
|
|
||||||
import PhoneInput from '@/app/ui/inputs/phone-input';
|
import PhoneInput from '@/app/ui/inputs/phone-input';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { IconEye } from '@/app/ui/icons/icons';
|
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 * as z from 'zod'
|
||||||
import ConfirmMailModalWindow from '@/app/ui/forms/confirm-mail-modal-window';
|
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() {
|
export default function RegisterForm() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const referralCode = searchParams.get('ref');
|
||||||
|
|
||||||
const [state, formAction, isPending] = useActionState(
|
const [state, formAction, isPending] = useActionState(
|
||||||
registration,
|
registration,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -23,6 +32,7 @@ export default function RegisterForm() {
|
|||||||
const [formState, setFormState] = useState<FormState | undefined>(undefined);
|
const [formState, setFormState] = useState<FormState | undefined>(undefined);
|
||||||
const [showMailConfirmWindow, setShowMailConfirmWindow] = useState(false);
|
const [showMailConfirmWindow, setShowMailConfirmWindow] = useState(false);
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [accountType, setAccountType] = useState<'b2c' | 'b2b'>('b2c');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormState(state);
|
setFormState(state);
|
||||||
@@ -30,7 +40,40 @@ export default function RegisterForm() {
|
|||||||
setShowMailConfirmWindow(true);
|
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') {
|
function showPassowrd(target: 'password' | 'confirm-password') {
|
||||||
if (target === '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,
|
fieldName: string,
|
||||||
value: string,
|
value: string,
|
||||||
) {
|
) {
|
||||||
switch (fieldName) {
|
switch (fieldName) {
|
||||||
case 'confirm_password':
|
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);
|
const baseResult = baseSchema.safeParse(value);
|
||||||
|
|
||||||
if (!baseResult.success) {
|
if (!baseResult.success) {
|
||||||
@@ -72,13 +140,25 @@ export default function RegisterForm() {
|
|||||||
case 'email':
|
case 'email':
|
||||||
case 'fullName':
|
case 'fullName':
|
||||||
case 'phone':
|
case 'phone':
|
||||||
|
case 'referralCode':
|
||||||
case 'password':
|
case 'password':
|
||||||
case 'companyName':
|
case 'companyName':
|
||||||
case 'agree':
|
case 'agree':
|
||||||
const schema = SignupFormSchema.shape[fieldName];
|
case 'inn':
|
||||||
|
const schema = signupFormSchema?.shape[fieldName];
|
||||||
|
if (!schema) {
|
||||||
|
return { success: true, data: value };
|
||||||
|
}
|
||||||
|
|
||||||
if (fieldName === 'phone') {
|
if (fieldName === 'phone') {
|
||||||
return schema.safeParse(value.replace(/[-\(\)\s]/g, ''));
|
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);
|
return schema.safeParse(value);
|
||||||
|
|
||||||
default:
|
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;
|
const fieldName = e.target.name;
|
||||||
if (fieldName !== 'phone') {
|
if (fieldName !== 'phone') {
|
||||||
e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, '');
|
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) {
|
if (validatedField.success) {
|
||||||
setFormState(prevState => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -131,6 +264,32 @@ export default function RegisterForm() {
|
|||||||
<form action={(e) => {
|
<form action={(e) => {
|
||||||
formAction(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']}`}>
|
<div className={`${styles['form-group']}`}>
|
||||||
<label
|
<label
|
||||||
className={`${styles['form-label']} ${styles['required']}`}
|
className={`${styles['form-label']} ${styles['required']}`}
|
||||||
@@ -162,6 +321,107 @@ export default function RegisterForm() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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']}`}>
|
<div className={`${styles['form-group']}`}>
|
||||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||||
{t('email-adress')}
|
{t('email-adress')}
|
||||||
@@ -193,6 +453,7 @@ export default function RegisterForm() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${styles['form-group']}`}>
|
<div className={`${styles['form-group']}`}>
|
||||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||||
{t('phone')}
|
{t('phone')}
|
||||||
@@ -214,37 +475,6 @@ export default function RegisterForm() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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']}`}>
|
<div className={`${styles['form-group']}`}>
|
||||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||||
{t('password')}
|
{t('password')}
|
||||||
@@ -287,6 +517,7 @@ export default function RegisterForm() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={`${styles['form-group']}`}>
|
<div className={`${styles['form-group']}`}>
|
||||||
<label className={`${styles['form-label']} ${styles['required']}`}>
|
<label className={`${styles['form-label']} ${styles['required']}`}>
|
||||||
{t('confirm-password')}
|
{t('confirm-password')}
|
||||||
@@ -350,8 +581,18 @@ export default function RegisterForm() {
|
|||||||
{t(formState?.error?.server)}
|
{t(formState?.error?.server)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<button type="submit" className={`${styles['btn']}`}>
|
<button
|
||||||
{t('create-an-account')}
|
type="submit"
|
||||||
|
className={`${styles['btn']}`}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? (
|
||||||
|
<div className="loading-animation">
|
||||||
|
<div className="global-spinner"></div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t("create-an-account")
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form >
|
</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 NotificationsButton from './notificationsButton';
|
||||||
import UserMenuButton from './userMenuButton';
|
import UserMenuButton from './userMenuButton';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import BurgerMenu from '@/app/ui/navigation/burger-menu';
|
import BurgerMenu from '@/app/ui/navigation/burger-menu';
|
||||||
|
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||||
|
|
||||||
export default function HeaderPanel() {
|
export default function HeaderPanel() {
|
||||||
const t = useTranslations("Global");
|
const t = useTranslations("Global");
|
||||||
|
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||||
return (
|
return (
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<BurgerMenu />
|
<BurgerMenu />
|
||||||
<div className="header-action">
|
<div className="header-action">
|
||||||
<LanguageSwitcher />
|
{/* <LanguageSwitcher /> */}
|
||||||
<Link
|
{typeof userData?.tariffInfo?.tokens === 'number' && (
|
||||||
href={'/pages/payment'}
|
<Link
|
||||||
className="tokens-balance"
|
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" }}>
|
<div className="tokens-wrapper" title={t('current-balance-of-protection')}>
|
||||||
<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 viewBox="0 0 24 24" fill="currentColor" style={{ width: "16px", height: "16px" }}>
|
||||||
</svg>
|
<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>
|
||||||
<span className="tokens-count">800</span>
|
</svg>
|
||||||
<span className="tokens-label">
|
<span className="tokens-count">{userData?.tariffInfo?.tokens}</span>
|
||||||
{t('tokens')}
|
<span className="tokens-label">
|
||||||
</span>
|
{t('tokens')}
|
||||||
</div>
|
</span>
|
||||||
</Link>
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
<NotificationsButton />
|
<NotificationsButton />
|
||||||
<UserMenuButton />
|
<UserMenuButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,27 +3,15 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { getUserData } from '@/app/actions/action';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { logout } from '@/app/actions/auth';
|
import { logout } from '@/app/actions/auth';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||||
|
|
||||||
export default function UserMenuButton() {
|
export default function UserMenuButton() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const t = useTranslations("Global");
|
const t = useTranslations("Global");
|
||||||
const {
|
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||||
data: userData,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userData'],
|
|
||||||
queryFn: () => {
|
|
||||||
return getUserData();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
useClickOutside(
|
useClickOutside(
|
||||||
menuRef,
|
menuRef,
|
||||||
@@ -50,7 +38,7 @@ export default function UserMenuButton() {
|
|||||||
<div
|
<div
|
||||||
className="user-avatar"
|
className="user-avatar"
|
||||||
id="user-menu-btn"
|
id="user-menu-btn"
|
||||||
title={userData?.fullName ? userData.fullName : ''}
|
title={userData?.fullName ?? ''}
|
||||||
>
|
>
|
||||||
{userData?.fullName ? userData.fullName[0] : ''}
|
{userData?.fullName ? userData.fullName[0] : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -61,10 +49,10 @@ export default function UserMenuButton() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="user-details">
|
<div className="user-details">
|
||||||
<div className="user-name">
|
<div className="user-name">
|
||||||
{userData?.fullName ? userData.fullName : ''}
|
{userData?.fullName ?? ''}
|
||||||
</div>
|
</div>
|
||||||
<div className="user-email">
|
<div className="user-email">
|
||||||
{userData?.email ? userData.email : ''}
|
{userData?.email ?? ''}
|
||||||
</div>
|
</div>
|
||||||
<div className="user-subscription">
|
<div className="user-subscription">
|
||||||
<span className="text-[14px]">{t('rate')}: </span>
|
<span className="text-[14px]">{t('rate')}: </span>
|
||||||
|
|||||||
@@ -103,9 +103,24 @@ export function IconArrowDown() {
|
|||||||
|
|
||||||
export function IconFilter() {
|
export function IconFilter() {
|
||||||
return (
|
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() {
|
export function IconNotification() {
|
||||||
return (
|
return (
|
||||||
@@ -147,8 +162,23 @@ export function IconBurgerMenu() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function IconRefresh() {
|
export function IconRefresh() {
|
||||||
return (
|
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>
|
<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'
|
'use client'
|
||||||
|
|
||||||
import { useState, ChangeEvent } from 'react';
|
import { useState, ChangeEvent, useEffect } from 'react';
|
||||||
import { InputMask } from '@react-input/mask';
|
import { InputMask } from '@react-input/mask';
|
||||||
import styles from '@/app/styles/module/login.module.scss';
|
import styles from '@/app/styles/module/login.module.scss';
|
||||||
|
|
||||||
interface PhoneInputProps {
|
interface PhoneInputProps {
|
||||||
phoneState: string | undefined;
|
phoneState: string | undefined;
|
||||||
validateHandler: (e: ChangeEvent<HTMLInputElement>) => void;
|
validateHandler: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
clearCondition?: string | undefined | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PhoneInput({ phoneState, validateHandler }: PhoneInputProps) {
|
export default function PhoneInput({ phoneState, validateHandler, clearCondition }: PhoneInputProps) {
|
||||||
const [phone, setPhone] = useState(phoneState ? phoneState : '');
|
const [phone, setPhone] = useState<string | undefined>(phoneState ? phoneState : '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (clearCondition) {
|
||||||
|
setPhone('');
|
||||||
|
}
|
||||||
|
}, [clearCondition])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputMask
|
<InputMask
|
||||||
|
|||||||
@@ -1,75 +1,37 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
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 { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
type FilesInfo = {
|
|
||||||
totalSize: number;
|
|
||||||
totalCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ProtectionStatistic({ fileType }: { fileType: string }) {
|
export default function ProtectionStatistic({ fileType }: { fileType: string }) {
|
||||||
const t = useTranslations('Global');
|
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) {
|
switch (fileType) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return {
|
return filesInfo?.images;
|
||||||
totalSize: 'images_size',
|
|
||||||
totalCount: 'images_quantity'
|
|
||||||
};
|
|
||||||
case 'video':
|
case 'video':
|
||||||
return {
|
return filesInfo?.videos;
|
||||||
totalSize: 'videos_size',
|
|
||||||
totalCount: 'videos_quantity'
|
|
||||||
};
|
|
||||||
case 'audio':
|
case 'audio':
|
||||||
return {
|
return filesInfo?.audios;
|
||||||
totalSize: 'audios_size',
|
case 'document':
|
||||||
totalCount: 'audios_quantity'
|
return filesInfo?.documents;
|
||||||
};
|
|
||||||
default:
|
default:
|
||||||
return {
|
return filesInfo?.total;
|
||||||
totalSize: 'all_files_size',
|
|
||||||
totalCount: 'all_files_quantity'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const currentData = getFileTypeData(fileType);
|
||||||
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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="protection-statistic">
|
<div className="protection-statistic">
|
||||||
<div className="protection-statistic-stat-card">
|
<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 className="protection-statistic-stat-label">{t(`${fileType}-protected`)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="protection-statistic-stat-card">
|
<div className="protection-statistic-stat-card">
|
||||||
@@ -78,7 +40,7 @@ export default function ProtectionStatistic({ fileType }: { fileType: string })
|
|||||||
</div>
|
</div>
|
||||||
<div className="protection-statistic-stat-card">
|
<div className="protection-statistic-stat-card">
|
||||||
<div className="protection-statistic-stat-number">
|
<div className="protection-statistic-stat-number">
|
||||||
{filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0}
|
{convertBytes(currentData?.size ?? 0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="protection-statistic-stat-label">{t(`${fileType}-occupy`)}</div>
|
<div className="protection-statistic-stat-label">{t(`${fileType}-occupy`)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import {useTranslations, useLocale} from 'next-intl';
|
import { useTranslations, useLocale } 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 { pluralize } from '@/app/lib/pluralize';
|
import { pluralize } from '@/app/lib/pluralize';
|
||||||
import { useStoreWithDevtools } from '@/app/stores/useStoreWithDevtools';
|
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
|
|
||||||
type FilesInfo = {
|
type FilesInfo = {
|
||||||
totalSize: number;
|
totalSize: number;
|
||||||
@@ -15,62 +13,25 @@ type FilesInfo = {
|
|||||||
|
|
||||||
export default function ProtectionSummary({ fileType }: { fileType: string }) {
|
export default function ProtectionSummary({ fileType }: { fileType: string }) {
|
||||||
const t = useTranslations('Global');
|
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) {
|
switch (fileType) {
|
||||||
case 'image':
|
case 'image':
|
||||||
return {
|
return filesInfo?.images;
|
||||||
totalSize: 'images_size',
|
|
||||||
totalCount: 'images_quantity'
|
|
||||||
};
|
|
||||||
case 'video':
|
case 'video':
|
||||||
return {
|
return filesInfo?.videos;
|
||||||
totalSize: 'videos_size',
|
|
||||||
totalCount: 'videos_quantity'
|
|
||||||
};
|
|
||||||
case 'audio':
|
case 'audio':
|
||||||
return {
|
return filesInfo?.audios;
|
||||||
totalSize: 'audios_size',
|
case 'document':
|
||||||
totalCount: 'audios_quantity'
|
return filesInfo?.documents;
|
||||||
};
|
|
||||||
default:
|
default:
|
||||||
return {
|
return filesInfo?.total;
|
||||||
totalSize: 'all_files_size',
|
|
||||||
totalCount: 'all_files_quantity'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const currentData = getFileTypeData(fileType);
|
||||||
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 locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
@@ -95,27 +56,27 @@ export default function ProtectionSummary({ fileType }: { fileType: string }) {
|
|||||||
<p>{t('current-status-of')}</p>
|
<p>{t('current-status-of')}</p>
|
||||||
<div className="protection-stats">
|
<div className="protection-stats">
|
||||||
<div className="protection-stat total-files">
|
<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">
|
<span className="protection-stat-label">
|
||||||
{pluralizeFilesName(filesInfo?.totalCount || 0)}
|
{pluralizeFilesName(currentData?.quantity ?? 0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="protection-stat total-checks">
|
<div className="protection-stat total-checks">
|
||||||
<div>
|
<div>
|
||||||
<span className="protection-stat-value">{CHECKS}</span>
|
<span className="protection-stat-value">{currentData?.check ?? 0}</span>
|
||||||
<span className="protection-stat-label">
|
<span className="protection-stat-label">
|
||||||
{pluralizeCheks(CHECKS)}
|
{pluralizeCheks(currentData?.check ?? 0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="protection-stat-value">{VIOLATIONS}</span>
|
<span className="protection-stat-value">{currentData?.violation ?? 0}</span>
|
||||||
<span className="protection-stat-label">
|
<span className="protection-stat-label">
|
||||||
{pluralizeViolations(VIOLATIONS)}
|
{pluralizeViolations(currentData?.violation ?? 0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="protection-stat total-usage">
|
<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">
|
<div className="protection-stat-label">
|
||||||
{t('disk-space-used')}
|
{t('disk-space-used')}
|
||||||
</div>
|
</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'
|
'use client'
|
||||||
|
|
||||||
import { IconDocument, IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
import { IconDocument, IconImageFile, IconVideoFile, IconAudioFile } from '@/app/ui/icons/icons';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import { getUserFilesInfo } from '@/app/actions/action';
|
|
||||||
import {useTranslations, useLocale} from 'next-intl';
|
|
||||||
import { convertBytes } from '@/app/lib/convertBytes';
|
import { convertBytes } from '@/app/lib/convertBytes';
|
||||||
import { pluralize } from '@/app/lib/pluralize';
|
import { pluralize } from '@/app/lib/pluralize';
|
||||||
|
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
|
|
||||||
export default function MyContentInfoBlock() {
|
export default function MyContentInfoBlock() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
|
|
||||||
const {
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
data: filesInfo,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userFilesInfo'],
|
|
||||||
queryFn: getUserFilesInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileTypeSizePercents = () => {
|
const fileTypeSizePercents = () => {
|
||||||
if (!filesInfo?.all_files_size) {
|
if (!filesInfo?.total.size) {
|
||||||
return {
|
return {
|
||||||
images: 0,
|
images: 0,
|
||||||
videos: 0,
|
videos: 0,
|
||||||
@@ -32,13 +23,13 @@ export default function MyContentInfoBlock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sizes = {
|
const sizes = {
|
||||||
images: filesInfo?.images_size || 0,
|
images: filesInfo?.images.size || 0,
|
||||||
videos: filesInfo?.videos_size || 0,
|
videos: filesInfo?.videos.size || 0,
|
||||||
audios: filesInfo?.audios_size || 0,
|
audios: filesInfo?.audios.size || 0,
|
||||||
documents: filesInfo?.documents_size || 0,
|
documents: filesInfo?.documents.size || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const total = filesInfo?.all_files_size;
|
const total = filesInfo?.total.size;
|
||||||
|
|
||||||
const rawPercents = {
|
const rawPercents = {
|
||||||
images: (sizes.images / total) * 100,
|
images: (sizes.images / total) * 100,
|
||||||
@@ -90,10 +81,10 @@ export default function MyContentInfoBlock() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="file-stat-details">
|
<div className="file-stat-details">
|
||||||
<span className="file-stat-count">
|
<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>
|
||||||
<span className="file-stat-size">
|
<span className="file-stat-size">
|
||||||
{filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0}
|
{filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,10 +101,10 @@ export default function MyContentInfoBlock() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="file-stat-details">
|
<div className="file-stat-details">
|
||||||
<span className="file-stat-count">
|
<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>
|
||||||
<span className="file-stat-size">
|
<span className="file-stat-size">
|
||||||
{filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0}
|
{filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,10 +121,10 @@ export default function MyContentInfoBlock() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="file-stat-details">
|
<div className="file-stat-details">
|
||||||
<span className="file-stat-count">
|
<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>
|
||||||
<span className="file-stat-size">
|
<span className="file-stat-size">
|
||||||
{filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0}
|
{filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,10 +141,10 @@ export default function MyContentInfoBlock() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="file-stat-details">
|
<div className="file-stat-details">
|
||||||
<span className="file-stat-count">
|
<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>
|
||||||
<span className="file-stat-size">
|
<span className="file-stat-size">
|
||||||
{filesInfo?.documents_size ? convertBytes(filesInfo?.documents_size) : 0}
|
{filesInfo?.documents.size ? convertBytes(filesInfo?.documents.size) : 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { PieChartComponent } from '@/app/components/PieChartComponent';
|
import { PieChartComponent } from '@/app/components/PieChartComponent';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { getUserFilesInfo } from '@/app/actions/action';
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { convertBytes } from '@/app/lib/convertBytes';
|
import { convertBytes } from '@/app/lib/convertBytes';
|
||||||
|
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
|
|
||||||
|
|
||||||
type FilesInfo = {
|
type FilesInfo = {
|
||||||
@@ -28,76 +27,27 @@ export default function MyContentPieChart() {
|
|||||||
{ name: 'documents', value: 0, color: '#a561e6' },
|
{ name: 'documents', value: 0, color: '#a561e6' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const {
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
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,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilesCount([
|
setFilesCount([
|
||||||
{
|
{
|
||||||
name: 'images-few',
|
name: 'images-few',
|
||||||
value: filesInfo?.imageSize ? filesInfo?.imageSize : 0,
|
value: filesInfo?.images.size ? filesInfo?.images.size : 0,
|
||||||
color: '#f08c00'
|
color: '#f08c00'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'videos',
|
name: 'videos',
|
||||||
value: filesInfo?.videoSize ? filesInfo?.videoSize : 0,
|
value: filesInfo?.videos.size ? filesInfo?.videos.size : 0,
|
||||||
color: '#2f9e44'
|
color: '#2f9e44'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'audios',
|
name: 'audios',
|
||||||
value: filesInfo?.audioSize ? filesInfo?.audioSize : 0,
|
value: filesInfo?.audios.size ? filesInfo?.audios.size : 0,
|
||||||
color: '#1971c2'
|
color: '#1971c2'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'documents',
|
name: 'documents',
|
||||||
value: filesInfo?.documentSize ? filesInfo?.documentSize : 0,
|
value: filesInfo?.documents.size ? filesInfo?.documents.size : 0,
|
||||||
color: '#a561e6'
|
color: '#a561e6'
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -105,7 +55,7 @@ export default function MyContentPieChart() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="protection-overview">
|
<div className="protection-overview">
|
||||||
<PieChartComponent data={filesCount} show={filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} />
|
<PieChartComponent data={filesCount} show={convertBytes(filesInfo?.total.size ?? 0)} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,14 @@
|
|||||||
|
//removed
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
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 { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
|
|
||||||
export default function MyContentPageTitleColorFrame({ title, description }: { title: string, description: string }) {
|
export default function MyContentPageTitleColorFrame({ title, description }: { title: string, description: string }) {
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
const {
|
|
||||||
data: filesInfo,
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userFilesInfo'],
|
|
||||||
queryFn: getUserFilesInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-title-color-frame">
|
<div className="page-title-color-frame">
|
||||||
@@ -30,7 +23,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
|
|||||||
</div>
|
</div>
|
||||||
<div className="header-stats">
|
<div className="header-stats">
|
||||||
<div className="header-stat-item">
|
<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 className="header-stat-label">Файлов</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="header-stat-divider"></div>
|
<div className="header-stat-divider"></div>
|
||||||
@@ -40,7 +33,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
|
|||||||
</div>
|
</div>
|
||||||
<div className="header-stat-divider"></div>
|
<div className="header-stat-divider"></div>
|
||||||
<div className="header-stat-item">
|
<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 className="header-stat-label">Хранилище</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { convertBytes } from '@/app/lib/convertBytes';
|
||||||
import { getUserFilesInfo } from '@/app/actions/action';
|
import { IconDocument, IconShield, IconSearch, IconDiscet } from '@/app/ui/icons/icons';
|
||||||
import {convertBytes} from '@/app/lib/convertBytes';
|
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||||
|
|
||||||
export default function MyContentStatsOverview() {
|
export default function MyContentStatsOverview() {
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
const {
|
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||||
data: filesInfo,
|
|
||||||
isLoading,
|
|
||||||
isError,
|
|
||||||
error,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['userFilesInfo'],
|
|
||||||
queryFn: getUserFilesInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-content-stats-overview">
|
<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-header">
|
||||||
<div className="stat-icon">📄</div>
|
<div className="stat-icon">
|
||||||
<div className="stat-trend">+0%</div>
|
<IconDocument />
|
||||||
|
</div>
|
||||||
|
{/* <div className="stat-trend">+0%</div> */}
|
||||||
</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 className="stat-label">Всего файлов</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card protected" data-icon="🛡">
|
<div className="stat-card protected" data-icon="">
|
||||||
<div className="stat-header">
|
<div className="stat-header">
|
||||||
<div className="stat-icon">🛡</div>
|
<div className="stat-icon">
|
||||||
<div className="stat-trend">+0%</div>
|
<IconShield />
|
||||||
|
</div>
|
||||||
|
{/* <div className="stat-trend">+0%</div> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-value">0</div>
|
<div className="stat-value">{filesInfo?.total.protected}</div>
|
||||||
<div className="stat-label">Защищено файлов</div>
|
<div className="stat-label">Защищено файлов</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card checks" data-icon="🔍">
|
<div className="stat-card checks" data-icon="">
|
||||||
<div className="stat-header">
|
<div className="stat-header">
|
||||||
<div className="stat-icon">🔍</div>
|
<div className="stat-icon">
|
||||||
<div className="stat-trend">+0%</div>
|
<IconSearch />
|
||||||
|
</div>
|
||||||
|
{/* <div className="stat-trend">+0%</div> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-value">0</div>
|
<div className="stat-value">0</div>
|
||||||
<div className="stat-label">Проверок защиты</div>
|
<div className="stat-label">Проверок защиты</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stat-card storage" data-icon="💾">
|
<div className="stat-card storage" data-icon="">
|
||||||
<div className="stat-header">
|
<div className="stat-header">
|
||||||
<div className="stat-icon">💾</div>
|
<div className="stat-icon">
|
||||||
<div className="stat-trend">+0%</div>
|
<IconDiscet />
|
||||||
|
</div>
|
||||||
|
{/* <div className="stat-trend">+0%</div> */}
|
||||||
</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 className="stat-label">Хранилище</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { usePathname } from 'next/navigation';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useSideMenuStore } from '@/app/stores/sideMenuStore';
|
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 pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
||||||
const [dropDownExpanded, setDropDownExpanded] = useState(false);
|
const [dropDownExpanded, setDropDownExpanded] = useState(false);
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
@@ -17,16 +19,20 @@ export default function DropDownList() {
|
|||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
name: t('photo-marking'),
|
name: 'photo-marking',
|
||||||
href: '/pages/marking-photo'
|
href: '/pages/marking-images'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('video-marking'),
|
name: 'video-marking',
|
||||||
href: '/pages/marking-video'
|
href: '/pages/marking-video'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: t('audio-marking'),
|
name: 'audio-marking',
|
||||||
href: '/pages/marking-audio'
|
href: '/pages/marking-audio'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'document-marking',
|
||||||
|
href: '/pages/marking-document'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -50,6 +56,9 @@ export default function DropDownList() {
|
|||||||
</div>
|
</div>
|
||||||
<ul className={`sub-menu ${dropDownExpanded ? 'expanded' : ''}`}>
|
<ul className={`sub-menu ${dropDownExpanded ? 'expanded' : ''}`}>
|
||||||
{links.map((link) => {
|
{links.map((link) => {
|
||||||
|
if (referralLink && link.name === 'document-marking') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={link.name}
|
key={link.name}
|
||||||
@@ -64,7 +73,7 @@ export default function DropDownList() {
|
|||||||
setMenuState(false)
|
setMenuState(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="">{link.name}</p>
|
<p className="">{t(link.name)}</p>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -11,8 +11,70 @@ import navigation from './navigation.json';
|
|||||||
import { useSideMenuStore } from '@/app/stores/sideMenuStore';
|
import { useSideMenuStore } from '@/app/stores/sideMenuStore';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
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() {
|
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 pathname = usePathname().replace('/en/', '/').replace('/ru/', '/');
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
|
|
||||||
@@ -66,9 +128,13 @@ export default function NavLinks() {
|
|||||||
<p className="">{t('home')}</p>
|
<p className="">{t('home')}</p>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<DropDownList />
|
<DropDownList referralLink={referralLink} />
|
||||||
|
|
||||||
{links.map((link) => {
|
{links.map((link) => {
|
||||||
|
if (!referralLink && link.name === 'referral-program') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={link.name}
|
key={link.name}
|
||||||
@@ -108,6 +174,19 @@ export default function NavLinks() {
|
|||||||
<p className="">{t('exit')}</p>
|
<p className="">{t('exit')}</p>
|
||||||
</Link>
|
</Link>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div className="build-versions">
|
||||||
|
{frontendBuildDate && (
|
||||||
|
<div className="">
|
||||||
|
front: {frontendBuildDate}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{backendBuildDate && (
|
||||||
|
<div className="">
|
||||||
|
back: {backendBuildDate}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</nav>
|
</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"
|
"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",
|
"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"
|
"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",
|
"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"
|
"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() {
|
export default function PaymentTabTariffs() {
|
||||||
return (
|
const t = useTranslations('Global');
|
||||||
<div className="tab-content">
|
const {
|
||||||
<div className="billing-toggle">
|
data: userData,
|
||||||
<div>
|
isLoading,
|
||||||
<button className="billing-btn active">💳 Помесячно</button>
|
isError,
|
||||||
<button className="billing-btn">
|
error
|
||||||
🔥 Годовая
|
} = useQuery({
|
||||||
<span>ВЫГОДНО</span>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="subscription-grid">
|
);
|
||||||
<div className="plan-card animate-card">
|
}
|
||||||
<div className="plan-header">
|
|
||||||
<div className="plan-icon">🚀</div>
|
return (
|
||||||
<h3 className="plan-name">СТАРТ</h3>
|
<>
|
||||||
<div className="plan-price">900₽</div>
|
<ModalWindow state={openWindow} callBack={setOpenWindow}>
|
||||||
<div className="plan-period">в месяц</div>
|
<PaymentConfirmationModal />
|
||||||
<div className="plan-tokens">100 токенов включено</div>
|
</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>
|
</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>
|
||||||
<div className="plan-card animate-card">
|
<div className="subscription-grid">
|
||||||
<div className="plan-header">
|
{userData?.map((item: {
|
||||||
<div className="plan-icon">🚀</div>
|
id: number;
|
||||||
<h3 className="plan-name">ПРЕМИУМ</h3>
|
name: string;
|
||||||
<div className="plan-price">3500₽</div>
|
price: number;
|
||||||
<div className="plan-period">в месяц</div>
|
tokens: number;
|
||||||
<div className="plan-tokens">100 токенов включено</div>
|
maxFilesCount: number;
|
||||||
</div>
|
diskSize: number;
|
||||||
<ul className="plan-features">
|
}) => (
|
||||||
<li className="plan-feature">
|
<div className="plan-card animate-card" key={item.id}>
|
||||||
<span className="feature-icon">✓</span>
|
<div className="plan-header">
|
||||||
100 файлов в месяц
|
<div className="plan-icon">🚀</div>
|
||||||
</li>
|
<h3 className="plan-name">
|
||||||
<li className="plan-feature">
|
{t.has(item.name) ? t(item.name) : item.name}
|
||||||
<span className="feature-icon">✓</span>
|
</h3>
|
||||||
1 ГБ хранилища
|
<div className="plan-price">{item.price}</div>
|
||||||
</li>
|
<div className="plan-period">в месяц</div>
|
||||||
<li className="plan-feature">
|
<div className="plan-tokens">{item.tokens} токенов включено</div>
|
||||||
<span className="feature-icon">✓</span>
|
</div>
|
||||||
До 10 МБ на файл
|
<ul className="plan-features">
|
||||||
</li>
|
<li className="plan-feature">
|
||||||
<li className="plan-feature">
|
<span className="feature-icon">✓</span>
|
||||||
<span className="feature-icon">✓</span>
|
{item.maxFilesCount} файлов в месяц
|
||||||
Базовые модули защиты
|
</li>
|
||||||
</li>
|
<li className="plan-feature">
|
||||||
<li className="plan-feature">
|
<span className="feature-icon">✓</span>
|
||||||
<span className="feature-icon">✓</span>
|
{item.diskSize ? convertBytes(item.diskSize) : 0} хранилища
|
||||||
Поддержка 24/7
|
</li>
|
||||||
</li>
|
<li className="plan-feature">
|
||||||
<li className="plan-feature">
|
<span className="feature-icon">✓</span>
|
||||||
<span className="feature-icon not">✗</span>
|
Базовые модули защиты
|
||||||
API доступ
|
</li>
|
||||||
</li>
|
<li className="plan-feature">
|
||||||
</ul>
|
<span className="feature-icon">✓</span>
|
||||||
</div>
|
Поддержка 24/7
|
||||||
<div className="plan-card animate-card">
|
</li>
|
||||||
<div className="plan-header">
|
<li className="plan-feature">
|
||||||
<div className="plan-icon">🚀</div>
|
<span className="feature-icon not">✗</span>
|
||||||
<h3 className="plan-name">ПРОФИ</h3>
|
API доступ
|
||||||
<div className="plan-price">9500₽</div>
|
</li>
|
||||||
<div className="plan-period">в месяц</div>
|
</ul>
|
||||||
<div className="plan-tokens">100 токенов включено</div>
|
<div className="flex justify-center">
|
||||||
</div>
|
<button
|
||||||
<ul className="plan-features">
|
className="btn btn-primary"
|
||||||
<li className="plan-feature">
|
onClick={() => openPaymentWindow(item.price, item.name, item.tokens)}
|
||||||
<span className="feature-icon">✓</span>
|
>
|
||||||
100 файлов в месяц
|
Выбрать план
|
||||||
</li>
|
</button>
|
||||||
<li className="plan-feature">
|
</div>
|
||||||
<span className="feature-icon">✓</span>
|
</div>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,33 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { referralRefill } from '@/app/actions/referralsActions';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export default function SecurePayments() {
|
export default function SecurePayments() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="secure-payments-wrapper">
|
<div className="secure-payments-wrapper">
|
||||||
<h3>
|
<h3>
|
||||||
🔒 Безопасные платежи
|
Безопасные платежи
|
||||||
</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-grid">
|
||||||
<div className="secure-payments-card">
|
<div className="secure-payments-card">
|
||||||
<div className="secure-payments-card-header">
|
<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() {
|
export default function UserUsage() {
|
||||||
|
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="usage-stats">
|
<div className="usage-stats">
|
||||||
<h3>
|
<h3>
|
||||||
📊 Ваше использование
|
Ваше использование
|
||||||
</h3>
|
</h3>
|
||||||
<div className="stats-grid">
|
<div className="stats-grid">
|
||||||
<div className="stat-card">
|
<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="stat-label">Файлов в месяц</div>
|
||||||
<div className="progress-bar">
|
<div className="progress-bar">
|
||||||
<div
|
<div
|
||||||
className="progress-fill"
|
className="progress-fill"
|
||||||
style={{ width: '50%' }}
|
style={{ width: `${getProcents(userData?.tariffInfo?.currentFileCounts, userData?.tariffInfo?.maxFileCounts)}%` }}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<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="stat-label">Объём хранилища</div>
|
||||||
<div className="progress-bar">
|
<div className="progress-bar">
|
||||||
<div
|
<div
|
||||||
className="progress-fill"
|
className="progress-fill"
|
||||||
style={{ width: '50%' }}
|
style={
|
||||||
|
{ width: `${getProcents(userData?.tariffInfo?.currentFileOnDisk, userData?.tariffInfo?.maxFileOnDisk ?? 0)}%` }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<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 className="stat-label">Токенов на балансе</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<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 className="stat-label">Текущий план</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function IncomeAndPayments() {
|
|||||||
<div className="earning-card success">
|
<div className="earning-card success">
|
||||||
<h4>💰 Доступно к выводу</h4>
|
<h4>💰 Доступно к выводу</h4>
|
||||||
<p>
|
<p>
|
||||||
135.00 Б
|
0
|
||||||
</p>
|
</p>
|
||||||
<small>Минимальная сумма вывода: 100 Б</small>
|
<small>Минимальная сумма вывода: 100 Б</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,7 +18,7 @@ export default function IncomeAndPayments() {
|
|||||||
<div className="earning-card">
|
<div className="earning-card">
|
||||||
<h4>📈 Всего заработано</h4>
|
<h4>📈 Всего заработано</h4>
|
||||||
<p>
|
<p>
|
||||||
135.00 Б
|
0
|
||||||
</p>
|
</p>
|
||||||
<small>За все время участия в программе</small>
|
<small>За все время участия в программе</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +26,7 @@ export default function IncomeAndPayments() {
|
|||||||
<div className="earning-card warning">
|
<div className="earning-card warning">
|
||||||
<h4>✅ Выплачено</h4>
|
<h4>✅ Выплачено</h4>
|
||||||
<p>
|
<p>
|
||||||
0.00 Б
|
0
|
||||||
</p>
|
</p>
|
||||||
<small>Успешно выплачено на ваши счета</small>
|
<small>Успешно выплачено на ваши счета</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,8 +70,8 @@ export default function IncomeAndPayments() {
|
|||||||
Обработано
|
Обработано
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stats-item first-column second-row">
|
<div className="stats-item first-column left-bottom-corner">
|
||||||
100.00
|
0
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item second-row">
|
<div className="stats-item second-row">
|
||||||
0
|
0
|
||||||
@@ -82,39 +82,7 @@ export default function IncomeAndPayments() {
|
|||||||
<div className="stats-item second-row">
|
<div className="stats-item second-row">
|
||||||
0
|
0
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-item second-row last-column">
|
<div className="stats-item last-column right-bottom-corner">
|
||||||
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">
|
|
||||||
0
|
0
|
||||||
</div>
|
</div>
|
||||||
</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() {
|
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 (
|
return (
|
||||||
<div className="block-wrapper">
|
<div className="block-wrapper">
|
||||||
<h3 className="section-title">
|
<h3 className="section-title">
|
||||||
@@ -6,32 +120,28 @@ export default function CommissionLevels() {
|
|||||||
Уровни комиссии
|
Уровни комиссии
|
||||||
</h3>
|
</h3>
|
||||||
<div className="commission-levels">
|
<div className="commission-levels">
|
||||||
<div className="level-card active">
|
{
|
||||||
<div className="level-number">🥉</div>
|
referralsLevels.map((item: ReferralLevel) => {
|
||||||
<div className="level-commission">15%</div>
|
return (
|
||||||
<div className="level-subscribe">BRONZE</div>
|
<div
|
||||||
<div className="level-requirement">0-5 рефералов</div>
|
className={
|
||||||
</div>
|
`level-card ${isActiveReferralLevel(activeInvites, item.maxInvitee, item.minInvitee) ? 'active' : ''}`
|
||||||
<div className="level-card ">
|
}
|
||||||
<div className="level-number">🥈</div>
|
key={item.id}
|
||||||
<div className="level-commission">18%</div>
|
>
|
||||||
<div className="level-subscribe">SILVER</div>
|
<div className="level-number">
|
||||||
<div className="level-requirement">6-15 рефералов</div>
|
{getReferralIcon(item.id)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="level-commission">{item.share}%</div>
|
||||||
<div className="level-card ">
|
<div className="level-subscribe">{item.name}</div>
|
||||||
<div className="level-number">🥇</div>
|
<div className="level-requirement">{
|
||||||
<div className="level-commission">22%</div>
|
item.maxInvitee < 100 ? `${item.minInvitee}-${item.maxInvitee}` : `${item.minInvitee}+`
|
||||||
<div className="level-subscribe">GOLD</div>
|
} рефералов
|
||||||
<div className="level-requirement">16-50 рефералов</div>
|
</div>
|
||||||
</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>
|
|
||||||
</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() {
|
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 (
|
return (
|
||||||
<div className="block-wrapper">
|
<div
|
||||||
<h2 className="section-title">
|
className="block-wrapper"
|
||||||
История приглашений
|
>
|
||||||
</h2>
|
<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="table-filtres-item">
|
||||||
<div className="stats-item left-top-corner stats-header">
|
<div className="table-filtres-label">{t('items-per-page')}:</div>
|
||||||
Email
|
<DropDownList
|
||||||
</div>
|
value={table.getState().pagination.pageSize}
|
||||||
<div className="stats-item stats-header">
|
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
|
||||||
Статус
|
callBack={table.setPageSize}
|
||||||
</div>
|
>
|
||||||
<div className="stats-item stats-header">
|
{[5, 10, 20, 50, 100].map(pageSize => (
|
||||||
Заработано
|
<li key={pageSize} value={pageSize}>
|
||||||
</div>
|
{t('show')} {pageSize}
|
||||||
<div className="stats-item stats-header">
|
</li>
|
||||||
Дата приглашения
|
))}
|
||||||
</div>
|
</DropDownList>
|
||||||
<div className="stats-item last-column right-top-corner stats-header">
|
</div>
|
||||||
Регистрация
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stats-item first-column second-row">
|
{/* Таблица */}
|
||||||
email@mail.com
|
<div className="tanstak-table-block">
|
||||||
</div>
|
<table className="tanstak-table">
|
||||||
<div className="stats-item second-row">
|
<thead className="tanstak-table-head">
|
||||||
{t('status')}
|
{table.getHeaderGroups().map(headerGroup => (
|
||||||
</div>
|
<tr key={headerGroup.id}>
|
||||||
<div className="stats-item second-row">
|
{headerGroup.headers.map(header => (
|
||||||
0
|
<th
|
||||||
</div>
|
key={header.id}
|
||||||
<div className="stats-item second-row">
|
>
|
||||||
0
|
{header.isPlaceholder
|
||||||
</div>
|
? null
|
||||||
<div className="stats-item second-row last-column">
|
: typeof header.column.columnDef.header === 'function'
|
||||||
0
|
? 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>
|
||||||
|
|
||||||
<div className="stats-item first-column">
|
{/* Пагинация */}
|
||||||
email@mail.com
|
<div className="tanstak-table-pagination">
|
||||||
</div>
|
<div className="pagination-info">
|
||||||
<div className="stats-item">
|
{/* <span className="pagination-info-pages">
|
||||||
{t('status')}
|
{t('page')}{' '}
|
||||||
</div>
|
<strong>
|
||||||
<div className="stats-item">
|
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
|
||||||
0
|
</strong>
|
||||||
</div>
|
</span>
|
||||||
<div className="stats-item">
|
<span className="pagination-info-files">
|
||||||
0
|
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
|
||||||
</div>
|
</span> */}
|
||||||
<div className="stats-item last-column">
|
</div>
|
||||||
0
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stats-item last-row left-bottom-corner first-column">
|
<div className="pagination-controls">
|
||||||
email@mail.com
|
{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>
|
||||||
<div className="stats-item last-row">
|
</div >
|
||||||
{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() {
|
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 (
|
return (
|
||||||
<div className="referral-link-section">
|
<div className="referral-link-section">
|
||||||
<h3>🔗 Ваша реферальная ссылка</h3>
|
<h3>Ваша реферальная ссылка</h3>
|
||||||
<p>Поделитесь этой ссылкой с друзьями и получайте комиссию с каждой их покупки</p>
|
<p>Поделитесь этой ссылкой с друзьями и получайте комиссию с каждой их покупки</p>
|
||||||
<div className="referral-link-input">
|
<div className="referral-link-input">
|
||||||
<span id="referral-link" >
|
<span
|
||||||
https://dashboard.no-copy.ru/register.php?ref=REFA0406AC8
|
id="referral-link"
|
||||||
|
ref={refLink}
|
||||||
|
>
|
||||||
|
{referralUrl}
|
||||||
</span >
|
</span >
|
||||||
<button className="copy-btn">📋 Копировать</button>
|
<button
|
||||||
|
className="copy-btn"
|
||||||
|
onClick={copyHandler}
|
||||||
|
>
|
||||||
|
Копировать
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="referral-link-footer">
|
<p className="referral-link-footer">
|
||||||
Код: <strong>REFA0406AC8</strong> •
|
Код: <strong>{referralUserStats?.referralLink}</strong> •
|
||||||
Кликов: <strong>21</strong> •
|
Приглашений: <strong>
|
||||||
Комиссия: <strong>15%</strong>
|
{referralUserStats?.totalInvitee}
|
||||||
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function SettingReferralProgram() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="settings-form">
|
<form className="settings-form">
|
||||||
<div>
|
<div className="settings-form-wrapper">
|
||||||
<h4>💳 Настройки выплат</h4>
|
<h4>💳 Настройки выплат</h4>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
@@ -32,7 +32,7 @@ export default function SettingReferralProgram() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="settings-form-wrapper">
|
||||||
<h4>🔔 Уведомления</h4>
|
<h4>🔔 Уведомления</h4>
|
||||||
|
|
||||||
<div className="checkbox-group">
|
<div className="checkbox-group">
|
||||||
@@ -40,11 +40,6 @@ export default function SettingReferralProgram() {
|
|||||||
<label>Email уведомления о новых рефералах</label>
|
<label>Email уведомления о новых рефералах</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="checkbox-group">
|
|
||||||
<input type="checkbox" name="sms_notifications" defaultValue="1" />
|
|
||||||
<label>SMS уведомления о выплатах</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="checkbox-group">
|
<div className="checkbox-group">
|
||||||
<input type="checkbox" name="marketing_emails" defaultValue="1" />
|
<input type="checkbox" name="marketing_emails" defaultValue="1" />
|
||||||
<label>Маркетинговые рассылки о программе</label>
|
<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'
|
'use client'
|
||||||
|
|
||||||
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
|
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 { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||||
|
import { FileSearchPanel } from '@/app/ui/search/file-search-panel';
|
||||||
interface SelectedFile {
|
interface SelectedFile {
|
||||||
file: File;
|
file: File;
|
||||||
preview: string | undefined;
|
preview: string | undefined;
|
||||||
name: string;
|
name: string;
|
||||||
size: string;
|
size: string;
|
||||||
dimensions?: {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileUploadInitResponse {
|
interface FileUploadInitResponse {
|
||||||
@@ -25,13 +23,17 @@ interface FileUploadInitResponse {
|
|||||||
status: string
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadSectionFile {
|
interface SectionSearchFile {
|
||||||
fileType: string
|
allowedExtensions: {
|
||||||
allowedExtensions: string[]
|
images: string[],
|
||||||
|
videos: string[],
|
||||||
|
audios: string[],
|
||||||
|
documents: string[]
|
||||||
|
}
|
||||||
maxFileSize: number
|
maxFileSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UploadSectionFile({ fileType, allowedExtensions, maxFileSize }: UploadSectionFile) {
|
export default function SectionSearchFile({ allowedExtensions, maxFileSize }: SectionSearchFile) {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||||
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
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 [uploadId, setUploadId] = useState<string | null>(null);
|
||||||
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
||||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||||
|
const [fileId, setFileId] = useState<string | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isCancelledRef = useRef(false);
|
const isCancelledRef = useRef(false);
|
||||||
|
|
||||||
|
const childRef = useRef(null);
|
||||||
|
|
||||||
const t = useTranslations('Global');
|
const t = useTranslations('Global');
|
||||||
|
|
||||||
|
const allExtensions = useMemo(() => { return [...allowedExtensions.images, ...allowedExtensions.videos, ...allowedExtensions.audios, ...allowedExtensions.documents] }, []);
|
||||||
|
|
||||||
const acceptString = useMemo(() => {
|
const acceptString = useMemo(() => {
|
||||||
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
|
if (!allExtensions || !Array.isArray(allExtensions)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return allowedExtensions.map(e => `.${e}`).join(', ');
|
return allExtensions.map(e => `.${e}`).join(', ');
|
||||||
}, [allowedExtensions]);
|
}, [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 } => {
|
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();
|
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||||
if (!extension || !allowedExtensions.includes(extension as string)) {
|
if (!extension || !allExtensions.includes(extension as string)) {
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
errorMessage: t('unsupported-file-format')
|
errorMessage: t('unsupported-file-format')
|
||||||
@@ -74,37 +111,25 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
return { isValid: true };
|
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> => {
|
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
setError(null);
|
setError(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileId) {
|
||||||
|
await removeUserFile(fileId, 1);
|
||||||
|
setFileId(null);
|
||||||
|
}
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsFileUploaded(false);
|
setIsFileUploaded(false);
|
||||||
|
|
||||||
|
if (childRef.current) {
|
||||||
|
//@ts-ignore
|
||||||
|
childRef.current.clearList();
|
||||||
|
}
|
||||||
|
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
|
|
||||||
const validation = validateFile(file);
|
const validation = validateFile(file);
|
||||||
@@ -114,31 +139,21 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
return;
|
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({
|
setSelectedFile({
|
||||||
file,
|
file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||||
dimensions,
|
|
||||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Файл выбран:', file.name, file.size);
|
handlerFileUpload({
|
||||||
}, [fileType, t]);
|
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 handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
const file = event.target.files?.[0] || null;
|
const file = event.target.files?.[0] || null;
|
||||||
@@ -172,29 +187,13 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
handleFileSelect(file);
|
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> => {
|
const handlerFileUpload = useCallback(async (fileInfo: SelectedFile): Promise<void> => {
|
||||||
if (uploadId) return;
|
if (uploadId) return;
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
isCancelledRef.current = false;
|
isCancelledRef.current = false;
|
||||||
const file = fileInfo.file;
|
const file = fileInfo.file;
|
||||||
|
const fileType = getFileType(fileInfo.file.name, allowedExtensions);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const extension = file.name.split('.').pop() || '';
|
const extension = file.name.split('.').pop() || '';
|
||||||
@@ -221,6 +220,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLastChunk = chunkIndex === totalChunks - 1;
|
||||||
const start = chunkIndex * CHUNK_SIZE;
|
const start = chunkIndex * CHUNK_SIZE;
|
||||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||||
const chunk = file.slice(start, end);
|
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('upload_id', response.upload_id);
|
||||||
formData.append('chunk_number', chunkIndex.toString());
|
formData.append('chunk_number', chunkIndex.toString());
|
||||||
formData.append('chunk', chunk);
|
formData.append('chunk', chunk);
|
||||||
|
formData.append('findSimilar', '1');
|
||||||
|
|
||||||
const chunkResponse = await chunkUpload(formData);
|
const chunkResponse = await chunkUpload(formData);
|
||||||
|
|
||||||
if (chunkResponse.message_desc !== 'Chunk uploaded successfully') {
|
if (chunkResponse.errorMesage !== null) {
|
||||||
throw new Error(`Chunk ${chunkIndex} upload failed`);
|
throw chunkResponse.errorMesage;
|
||||||
}
|
}
|
||||||
|
|
||||||
setUploadProgress(Math.floor((chunkIndex + 1) / totalChunks * 100));
|
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);
|
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||||
@@ -256,7 +262,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
} finally {
|
} finally {
|
||||||
setUploadId(null);
|
setUploadId(null);
|
||||||
}
|
}
|
||||||
}, [uploadId, fileType, t]);
|
}, [uploadId, allowedExtensions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Обработка закрытия вкладки
|
// Обработка закрытия вкладки
|
||||||
@@ -296,9 +302,13 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="upload-section">
|
<div className="upload-section">
|
||||||
<h3>
|
<div className="search-info">
|
||||||
{t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })}
|
<div className="search-info-title">Как работает поиск?</div>
|
||||||
</h3>
|
<div className="search-info-text">
|
||||||
|
Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами,
|
||||||
|
используя алгоритмы компьютерного зрения и цифровых отпечатков.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||||
@@ -306,25 +316,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<IconShieldAdd />
|
<IconSearch />
|
||||||
<h4>
|
<h4>
|
||||||
{t('drag-the-file-here')}
|
Выберите файл для поиска
|
||||||
</h4>
|
</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
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
@@ -339,8 +334,9 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={uploadProgress && uploadProgress !== 100 ? true : false}
|
||||||
>
|
>
|
||||||
{t('select-files-to-protect')}
|
Выбрать файл
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -351,9 +347,9 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
|
|
||||||
{selectedFile && (
|
{selectedFile && (
|
||||||
<div
|
<div
|
||||||
className={`selected-file ${isFileUploaded ? 'done' : ''}`}
|
className={`selected-file`}
|
||||||
>
|
>
|
||||||
<div className="selected-file-file-info">
|
<div className="selected-file-file-info relative">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-700">
|
<p className="text-gray-700">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
@@ -365,52 +361,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
{t('size')}:
|
{t('size')}:
|
||||||
</span> {selectedFile.size}
|
</span> {selectedFile.size}
|
||||||
</p>
|
</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>
|
||||||
<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 && (
|
{uploadProgress !== 0 && (
|
||||||
<div
|
<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}%
|
{uploadProgress}%
|
||||||
</div>
|
</div>
|
||||||
@@ -419,6 +373,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isFileUploaded && (
|
||||||
|
<FileSearchPanel fileId={fileId} ref={childRef} />
|
||||||
|
)}
|
||||||
</div>
|
</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