Merge branch 'main' into NCFRONT-72

This commit is contained in:
smanylov
2026-02-17 18:20:50 +07:00
107 changed files with 7351 additions and 1431 deletions
+1 -1
View File
@@ -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
+78 -2
View File
@@ -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
View File
@@ -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"
+3
View File
@@ -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 />
-2
View File
@@ -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>
+2 -2
View File
@@ -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 />
+7 -2
View File
@@ -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>
+2
View File
@@ -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,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';
+2 -2
View File
@@ -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 />
+1 -1
View File
@@ -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 />
+3 -1
View File
@@ -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 />
+48
View File
@@ -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>
</>
)
}
+4 -2
View File
@@ -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 />
</> </>
-4
View File
@@ -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 />
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+199
View File
@@ -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
};
}
}
+14 -12
View File
@@ -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);
+44 -5
View File
@@ -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'
} }
}); });
@@ -79,3 +80,41 @@ export async function removeUserFile(fileId: string) {
return error return error
} }
} }
export async function viewFileInfo(fileId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'file_info',
file_id: fileId,
token: token
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
+45 -18
View File
@@ -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'
} }
}); });
+181
View File
@@ -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
}
}
}
+126
View File
@@ -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;
}
}
+8 -3
View File
@@ -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');
}
} }
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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) {
+43
View File
@@ -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);
}
}
+24
View File
@@ -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>
);
}
+22
View File
@@ -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>;
}
};
+69
View File
@@ -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>
+637
View File
@@ -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
});
};
+4
View File
@@ -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';
+18
View File
@@ -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;
}
+17 -1
View File
@@ -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;
}
}
}),
]); ]);
} }
+88 -59
View File
@@ -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,73 +163,33 @@ 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;
} }
// Дополнительные поля для формы регистрации // Дополнительные поля для формы регистрации
@@ -227,3 +203,56 @@ export function createValidationSchema(config: ValidationConfig) {
path: ['confirm_password'], path: ['confirm_password'],
}); });
} }
export function createValidationSchemaForPassword(config: ValidationConfig) {
const schema: Record<string, any> = {};
// password
if (config.password) {
let passwordSchema = z
.string()
.min(config.password.minSize?.value || 8, {
error: config.password.minSize?.message || 'register-error-password-symbols'
})
.max(config.password.maxSize?.value || 124, {
error: config.password.maxSize?.message || 'register-error-max-password'
})
.trim();
// Проверка допустимых символов
if (config.password.allowedSymbols?.value) {
passwordSchema = passwordSchema.regex(
new RegExp(config.password.allowedSymbols.value),
{ message: config.password.allowedSymbols.message }
);
}
// Проверка наличия цифры
if (config.password.requiredDigit?.value) {
passwordSchema = passwordSchema.regex(
/[0-9]/,
{ message: config.password.requiredDigit.message }
);
}
// Проверка наличия буквы
if (config.password.requiredLetter?.value) {
passwordSchema = passwordSchema.regex(
/[a-zA-Zа-яёА-ЯЁ]/,
{ message: config.password.requiredLetter.message }
);
}
schema.password = passwordSchema;
}
// Дополнительные поля для формы регистрации
schema.confirm_password = z.string();
return z
.object(schema)
.refine((data) => data.password === data.confirm_password, {
message: 'repeat-password',
path: ['confirm_password'],
});
}
+16 -2
View File
@@ -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"
}
} }
} }
+42 -2
View File
@@ -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 {
+26
View File
@@ -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 {
+34 -2
View File
@@ -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
+4 -1
View File
@@ -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;
-1
View File
@@ -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 {
+2
View File
@@ -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 -1
View File
@@ -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>
) )
+138 -36
View File
@@ -1,48 +1,150 @@
'use client'
import { useQuery } from '@tanstack/react-query';
import { getUserFilesData } from '@/app/actions/fileEntity';
import { useEffect, useState, useRef } from 'react';
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
import { convertBytes } from '@/app/lib/convertBytes';
import { useTranslations } from 'next-intl';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import Link from 'next/link';
type FileItem = {
id: string;
fileName: string;
fileType: string;
size?: number | undefined;
uploadDate?: number;
protectStatus: string;
supportId: number;
};
type ApiFile = {
id: string;
originalFileName: string;
mimeType: string;
fileSize: number;
updatedAt: string;
protectStatus: string;
supportId: number;
};
type ApiResponse = {
files?: ApiFile[];
};
export default function DashboardUserFiles() { 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>
) )
} }
+12 -71
View File
@@ -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={() => {
+9 -19
View File
@@ -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
-105
View File
@@ -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';
+6 -20
View File
@@ -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>
) )
+12 -2
View File
@@ -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>
) )
+282 -41
View File
@@ -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 >
</> </>
+207
View File
@@ -0,0 +1,207 @@
'use client'
import styles from '@/app/styles/module/login.module.scss'
import { useActionState, useEffect, useRef, useState, ChangeEvent } from 'react';
import { useTranslations } from 'next-intl';
import { confirmPassword, FormStateConfirmPassword } from '@/app/actions/auth';
import { useSearchParams } from 'next/navigation';
import { IconEye } from '@/app/ui/icons/icons';
import { SignupFormSchema } from '@/app/actions/definitions';
import * as z from 'zod';
export default function ResetPasswordForm() {
const [state, formAction, isPending] = useActionState(
confirmPassword,
undefined,
);
const passwordRef = useRef<HTMLInputElement>(null);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [formState, setFormState] = useState<FormStateConfirmPassword | undefined>(undefined);
const t = useTranslations('Login-register-form');
const searchParams = useSearchParams();
const email = searchParams.get('email');
useEffect(() => {
if (state) {
setFormState(state);
}
}, [state])
function showPassowrd(target: 'password' | 'confirm-password') {
if (target === 'password') {
setShowPassword(!showPassword);
} else {
setShowConfirmPassword(!showConfirmPassword);
}
}
function validateField(
fieldName: string,
value: string,
) {
switch (fieldName) {
case 'confirm_password':
const baseSchema = SignupFormSchema.shape.confirm_password;
const baseResult = baseSchema.safeParse(value);
if (!baseResult.success) {
return baseResult;
}
const currentPassword = passwordRef.current?.value || '';
if (value !== currentPassword) {
return {
success: false,
error: new z.ZodError([
{
code: z.ZodIssueCode.custom,
message: 'repeat-password',
path: ['confirm_password']
}
])
};
}
return { success: true, data: value };
case 'password':
const schema = SignupFormSchema.shape[fieldName];
return schema.safeParse(value);
default:
throw new Error(`Unknown field: ${fieldName}`);
}
}
function onChangeHandler(e: ChangeEvent<HTMLInputElement>): void {
const fieldName = e.target.name;
if (fieldName !== 'phone') {
e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, '');
}
const validatedField = validateField(fieldName, e.target.value);
if (validatedField.success) {
setFormState(prevState => {
if (!prevState?.error || !prevState?.error[fieldName]) {
return prevState;
}
const { [fieldName]: _, ...newError } = prevState.error;
return {
...prevState,
error: newError
};
});
}
}
return (
<form action={formAction}>
<div className={`${styles['form-group']}`}>
<input
type="hidden"
id="email"
name="email"
className={`${styles['form-input']}`}
placeholder={t('enter-email')}
defaultValue={email ? email : ''}
/>
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('recovery-code')}
</label>
<input
type="readOnly"
id="verify-token"
name="verify-token"
className={`${styles['form-input']}`}
placeholder={t('recovery-code')}
defaultValue={formState?.previousState?.verifyToken ? formState?.previousState?.verifyToken : ''}
/>
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('password')}
</label>
<div
className={`${styles['password-wrapper']}`}
>
<input
ref={passwordRef}
type={showPassword ? "text" : "password"}
id="password"
name="password"
className={`${styles['form-input']} ${styles['password']}`}
placeholder={t('password-placeholder')}
defaultValue={formState?.previousState?.password ? formState?.previousState?.password : ''}
onChange={onChangeHandler}
/>
<button
onClick={() => {
showPassowrd('password')
}}
type="button"
className={`show-password-button ${showPassword ? 'show' : ''}`}
>
<IconEye />
</button>
</div>
{formState?.error?.password && (
<p className="text-sm text-red-500">
{
formState?.error?.password.split('&').map((e, index) => {
return (
<span key={index}>
{t(e)}
<br />
</span>
)
})
}
</p>
)}
</div>
<div className={`${styles['form-group']}`}>
<label className={`${styles['form-label']} ${styles['required']}`}>
{t('confirm-password')}
</label>
<div className={`${styles['password-wrapper']}`}>
<input
type={showConfirmPassword ? "text" : "password"}
id="confirm_password"
name="confirm_password"
className={`${styles['form-input']} ${styles['password']}`}
placeholder={t('repeat-password')}
defaultValue={formState?.previousState?.confirm_password ? formState?.previousState?.confirm_password : ''}
onChange={onChangeHandler}
/>
<button
onClick={() => {
showPassowrd('confirm-password');
}}
type="button"
className={`show-password-button ${showConfirmPassword ? 'show' : ''}`}
>
<IconEye />
</button>
</div>
{formState?.error?.confirm_password && (
<p className="text-sm text-red-500">
{t(formState?.error?.confirm_password)}
</p>
)}
</div>
{/* {state?.error.server && (
<p className={`${styles['form-error']}`}>
{t(state?.error.server)}
</p>
)} */}
<button type="submit" className={`${styles['btn']}`}>
{t('change-password')}
</button>
</form>
)
}
+21 -16
View File
@@ -1,32 +1,37 @@
'use client'
import NotificationsButton from './notificationsButton'; import 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>
+5 -17
View File
@@ -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>
+31 -1
View File
@@ -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>
)
}
+10 -3
View File
@@ -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>
+18 -57
View File
@@ -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>
)
}
+17 -26
View File
@@ -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>
+7 -57
View File
@@ -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>
+15 -6
View File
@@ -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>
); );
})} })}
+80 -1
View File
@@ -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>
); );
} }
+2 -2
View File
@@ -20,13 +20,13 @@
"img": "M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.5 1.5 0 0 0 18.54 8H16c-.8 0-1.54.37-2 1l-3 4v2h2l2.54-3.82L16.5 18H20z" "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"
}, },
{ {
+175 -113
View File
@@ -1,122 +1,184 @@
'use client'
import { fetchTariffs } from '@/app/actions/action';
import { useQuery } from '@tanstack/react-query';
import { convertBytes } from '@/app/lib/convertBytes';
import { useTranslations } from 'next-intl';
import { yooKasaCreatePayment } from '@/app/actions/yooKassa';
import { useState, ReactNode } from 'react';
import ModalWindow from '@/app/components/ModalWindow';
export default function PaymentTabTariffs() { 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> </>
) );
} }
+25 -1
View File
@@ -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">
+25 -7
View File
@@ -1,38 +1,56 @@
'use client'
import { convertBytes } from '@/app/lib/convertBytes';
import { getProcents } from '@/app/lib/getProcents';
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
export default function UserUsage() { 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>
+136 -26
View File
@@ -1,4 +1,118 @@
'use client'
import { useQuery } from '@tanstack/react-query';
import { fetchReferralsLevels, fetchReferralUserStats } from '@/app/actions/referralsActions';
interface ReferralLevel {
name: string;
id: string;
next: string | null;
share: number;
maxInvitee: number;
minInvitee: number;
}
interface ReferralsData {
referrals: ReferralLevel[];
}
export default function CommissionLevels() { 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>
) )
+473 -66
View File
@@ -1,77 +1,484 @@
import { useTranslations } from 'next-intl'; 'use client'
import { useState, useMemo, useEffect } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
getFilteredRowModel,
ColumnDef,
SortingState,
ColumnFiltersState,
} from '@tanstack/react-table';
import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } from '@/app/ui/icons/icons';
import { useTranslations, useLocale } from 'next-intl';
import DropDownList from '@/app/components/DropDownList';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pluralize } from '@/app/lib/pluralize';
import { fetchReferralInvitees } from '@/app/actions/referralsActions';
type InviteesType = {
status?: string;
email?: string;
registrationData: string;
};
type InviteType = {
active?: string;
email?: string;
regDate: string;
};
export default function InvitationsTable() { 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>
+43
View File
@@ -0,0 +1,43 @@
'use client'
import { useTranslations } from 'next-intl';
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
export default function ReportsInfo() {
const t = useTranslations("Global");
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
return (
<div className="dashboard-files-info">
<div className="type-card images">
<div className="stat-type">
{t('all-content')}
</div>
<div className="stat-value">{filesInfo?.total.quantity ? filesInfo?.total.quantity : 0}</div>
</div>
<div className="type-card videos">
<div className="stat-type">
{t('violations-few')}
</div>
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div>
</div>
<div className="type-card audio">
<div className="stat-type">
{t('monitoring')}
</div>
<div className="stat-value">{filesInfo?.total.check ? filesInfo?.total.check : 0}</div>
</div>
<div className="type-card scripts">
<div className="stat-type">
{t('efficiency')}
</div>
<div className="stat-value">100%</div>
</div>
</div>
)
}
+234
View File
@@ -0,0 +1,234 @@
import { SearchedUserFilesList } from '@/app/ui/search/searched-user-files-list';
import { SearchedGlobalFilesList } from '@/app/ui/search/searched-global-files-list';
import { useCallback, useState, useImperativeHandle } from 'react';
import { searchUserFiles, searchGlobalFiles } from '@/app/actions/searchActions';
import { useQueryClient } from '@tanstack/react-query';
import { Pagination } from '@/app/components/Pagination';
export interface SearchItem {
url: string,
pageTitle: string,
height: number,
width: number,
host: string,
pageUrl: string
}
export function FileSearchPanel({ fileId, ref }: { fileId: string | null, ref: any }) {
const [searchedUserFiles, setSearchedUserFiles] = useState<string[]>([]);
const [searchedUserFilesShowNull, setSearchedUserFilesShowNull] = useState<boolean>(false);
const [searchedGlobalFiles, setSearchedGlobalFiles] = useState<SearchItem[]>([]);
const [searchedGlobalFilesShowNull, setSearchedGlobalFilesShowNull] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [paginationLoading, setPaginationLoading] = useState<boolean>(false);
const [showGlobalSearch, setShowGlobalSearch] = useState<boolean>(false);
const queryClient = useQueryClient();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [searchTotalPages, setSearchTotalPages] = useState<number>(0);
const [searchCurrentPages, setSearchCurrentPages] = useState<number>(1);
useImperativeHandle(ref, () => ({
clearList: () => {
setSearchedUserFiles([]);
setSearchedGlobalFiles([]);
setSearchedUserFilesShowNull(false);
setSearchedGlobalFilesShowNull(false);
setShowGlobalSearch(false);
setSearchCurrentPages(1);
setSearchTotalPages(0);
}
}));
const handlerSearchUserFile = useCallback(async (fileId: string, page: number): Promise<void> => {
setErrorMessage(null);
try {
let result = await searchUserFiles(fileId);
if (result.content.length) {
setSearchedUserFiles(result.content);
setShowGlobalSearch(true);
handlerSearchGlobalFile(fileId, page)
} else {
setSearchedUserFilesShowNull(true);
}
} catch (error) {
setErrorMessage('error');
}
}, [fileId])
const handlerSearchGlobalFile = useCallback(async (fileId: string, page: number, actionFromPagination?: boolean): Promise<void> => {
if (!actionFromPagination) {
setLoading(true);
setSearchedGlobalFilesShowNull(false);
setSearchedGlobalFiles([]);
} else {
setPaginationLoading(true);
}
try {
let result = await searchGlobalFiles(fileId, page);
console.log('searchGlobalFiles');
console.log(result);
if (result.images.length) {
setSearchedGlobalFiles(result.images);
setSearchTotalPages(result.totalPages);
setSearchCurrentPages(page);
} else {
setSearchedGlobalFilesShowNull(true);
}
} catch (error) {
} finally {
await queryClient.invalidateQueries({ queryKey: ['userData'] });
await queryClient.invalidateQueries({ queryKey: ['userSearchData'] });
setLoading(false);
setPaginationLoading(false);
}
}, [fileId])
return (
<>
<div
className="mb-4"
>
<button
className="btn btn-primary btn-search"
disabled={fileId ? false : true}
onClick={() => {
if (fileId) {
handlerSearchUserFile(fileId, 1)
}
}}
>
Начать поиск
</button>
{errorMessage && (
<div className='mt-2 text-red-600 text-sm'>
Ошибка поиска
</div>
)}
</div>
{(searchedUserFiles.length !== 0) && (
<SearchedUserFilesList list={searchedUserFiles} />
)}
{searchedUserFilesShowNull && (
<div
className="user-file-search-results"
>
<div className="results-header">
<div className="results-title">Результаты поиска</div>
<div className="results-count">
Найдено файлов: {searchedUserFiles.length}
</div>
</div>
<div
className="empty-results"
>
<h3>Похожих файлов не найдено</h3>
<p>В вашей библиотеке нет файлов, похожих на загруженный</p>
<div className="empty-results-hint">
<strong>Совет:</strong> Убедитесь, что вы загружали этот файл ранее через страницу "Маркировка"
</div>
</div>
</div>
)}
{showGlobalSearch && (
<div
className="global-search-section"
>
<div className="global-search-header">
<div className="global-search-title">
Глобальный поиск изображений
</div>
<div className="">
Найти где ещё используется ваше изображение в интернете
</div>
<div className="global-search-badge">
Интернет поиск
</div>
</div>
<div className="global-search-content">
<div className="global-search-info">
<div className="global-search-info-title">
Поиск по всему интернету
</div>
<div className="global-search-info-text">
Используем продвинутые технологии обратного поиска по изображению для поиска копий вашего контента
на сайтах, в социальных сетях и других источниках. Поможет обнаружить несанкционированное использование.
</div>
</div>
<div className="search-counter" id="search-counter">
<div className="counter-info">
<div className="counter-icon" id="counter-icon"></div>
<div className="counter-text">
<div className="counter-label">Глобальных поисков сегодня</div>
<div className="counter-value" id="counter-value">0 из 10</div>
</div>
</div>
<div className="progress-bar">
<div
className="progress-fill"
id="progress-fill"
style={{ width: '0%' }}
>
</div>
</div>
</div>
<div className={`global-loading ${loading ? 'show' : ''}`} id="global-loading">
<div className="global-spinner"></div>
<div className="global-loading-text" id="global-loading-text">
Поиск изображений в интернете...
</div>
</div>
<div className={`global-result ${searchedGlobalFiles.length ? 'show' : ''}`} id="global-result">
<div className="global-result-card">
<div className="global-result-header">
<span>Найдено в интернете</span>
{paginationLoading && (
<div className="loading-animation">
<span className="global-spinner"></span>
</div>
)}
</div>
<div className="global-result-content">
<SearchedGlobalFilesList list={searchedGlobalFiles} />
</div>
</div>
</div>
{searchedGlobalFilesShowNull && (
<div className="global-no-result" id="global-no-result">
<div className="global-no-result-icon">&zwj;</div>
<h4>Похожие изображения в интернете не найдены</h4>
<p>Это может означать, что ваше изображение уникально и не используется на других сайтах.</p>
</div>
)}
<div className="global-error" id="global-error">
<strong>Ошибка глобального поиска:</strong>
<span id="global-error-text"></span>
</div>
<Pagination totalPages={searchTotalPages} currentPage={searchCurrentPages} fileId={fileId} callBack={handlerSearchGlobalFile} />
</div>
</div>
)}
</>
)
}
+56
View File
@@ -0,0 +1,56 @@
'use client'
import { IconImageFile, IconDocument, IconAudioFile, IconVideoFile } from '@/app/ui/icons/icons';
import { useUserSearchData } from '@/app/hooks/react-query/useUserSearchData';
export function RecentSearches() {
const { data: userSearchData, isLoading, isError, error, } = useUserSearchData()
return (
<div className="recent-searches">
<div className="recent-title">
Недавние поиски
</div>
<div className="recent-item">
<span className="recent-type document">
<IconDocument /> document</span>
<div>
<div>{userSearchData?.document?.count ? userSearchData?.document?.count : 0} поисков</div>
<div className="recent-date">
date
</div>
</div>
</div>
<div className="recent-item">
<span className="recent-type image">
<IconImageFile /> image</span>
<div>
<div>{userSearchData?.image?.count ? userSearchData?.image?.count : 0} поисков</div>
<div className="recent-date">
date
</div>
</div>
</div>
<div className="recent-item">
<span className="recent-type audio">
<IconAudioFile /> audio</span>
<div>
<div>{userSearchData?.audio?.count ? userSearchData?.audio?.count : 0} поисков</div>
<div className="recent-date">
date
</div>
</div>
</div>
<div className="recent-item">
<span className="recent-type video">
<IconVideoFile /> video</span>
<div>
<div>{userSearchData?.video?.count ? userSearchData?.video?.count : 0} поисков</div>
<div className="recent-date">
date
</div>
</div>
</div>
</div>
)
}
+50
View File
@@ -0,0 +1,50 @@
'use client'
import { useUserSearchData } from '@/app/hooks/react-query/useUserSearchData';
export function SearchStats() {
const { data: userSearchData, isLoading, isError, error, } = useUserSearchData()
return (
<div className="stats-search">
<div className="stats-title">
Статистика поиска
</div>
<div className="stat-item">
<span className="stat-label">Изображения</span>
<span className="stat-value">
{userSearchData?.image?.count ? userSearchData?.image?.count : 0}
</span>
</div>
<div className="stat-item">
<span className="stat-label">Видео</span>
<span className="stat-value">
{userSearchData?.video?.count ? userSearchData?.video?.count : 0}
</span>
</div>
<div className="stat-item">
<span className="stat-label">Аудио</span>
<span className="stat-value">
{userSearchData?.audio?.count ? userSearchData?.audio?.count : 0}
</span>
</div>
<div className="stat-item">
<span className="stat-label">Документы</span>
<span className="stat-value">
{userSearchData?.document?.count ? userSearchData?.document?.count : 0}
</span>
</div>
<div className="stat-item">
<span className="stat-label">Всего поисков</span>
<span className="stat-value">
{userSearchData?.allCountForPeriod ? userSearchData?.allCountForPeriod : 0}
</span>
</div>
</div>
)
}
@@ -0,0 +1,45 @@
import { SearchItem } from './file-search-panel'
export function SearchedGlobalFilesList({ list }: { list: SearchItem[] }) {
return (
<>
{list.map(
(
item: SearchItem, index: number
) => {
return (
<div
className="global-result-item"
key={index}
>
<div className="global-result-image">
<img src={item?.url} alt={item?.pageTitle} />
</div>
<div className="global-result-info">
<div className="global-result-title">
{item?.pageTitle}
</div>
<div className="global-result-meta">
{item?.height & item?.width && (
<div className="global-result-size">
{item?.height}x{item?.width}
</div>
)}
<div className="global-result-host">
{item?.host}
</div>
</div>
<div className="global-result-passage">
{item?.pageTitle}
</div>
<a href={item?.pageUrl} target="_blank" className="global-result-url" title={item?.pageUrl}>
{item?.pageUrl}
</a>
</div>
</div>
)
})}
</>
)
}
@@ -0,0 +1,189 @@
import { convertBytes } from '@/app/lib/convertBytes';
import { toast } from 'sonner';
import { useState, ReactNode } from 'react';
import { useTranslations } from 'next-intl';
import ModalWindow from '@/app/components/ModalWindow';
import { IconEye, IconDownload } from '@/app/ui/icons/icons';
export function SearchedUserFilesList({ list }: { list: any }) {
const [isFileLoading, setIsFileLoading] = useState(false);
const [openWindow, setOpenWindow] = useState<boolean>(false);
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
const t = useTranslations('Global');
const handlerDownload = async (fileId: string, fileName: string) => {
setIsFileLoading(true);
try {
const response = await fetch(`/api/download/${fileId}`);
if (!response.ok) {
throw new Error(`error: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName || `file-${fileId}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - ${fileName}`);
} catch (error) {
toast.error(t('failed-to-download-file'))
} finally {
setIsFileLoading(false);
}
};
const handlerViewfile = async (e: any) => {
setOpenWindowChildren(() => {
return (
<div
className="modal-window-view-file"
>
<div className="modal-window-view-file-header">
<div
className="modal-window-view-file-title"
>
{e.originalFileName}
</div>
</div>
<div
className="modal-window-view-file-content"
>
<div className="modal-window-view-file-content-wrapper">
<div className="image-section">
<div className="image-container">
<img
src={e.url ? e.url : '#'}
alt="img"
/>
</div>
</div>
<div
className="file-info-card"
>
<div className="info-header">
<h4>Информация о файле</h4>
</div>
<div className="info-item">
<span className="info-label">ID в системе:</span>
<span className="info-value">
{e.supportId ? e.supportId : '#'}
</span>
</div>
<div className="info-item">
<span className="info-label">Дата загрузки:</span>
<span className="info-value">
{e.uploadDate ? e.uploadDate : '#'}
</span>
</div>
<div className="info-item">
<span className="info-label">Размер файла:</span>
<span className="info-value">
{convertBytes(e.fileSize)}
</span>
</div>
<div className="info-item">
<span className="info-label">Статус защиты:</span>
<span className="info-value">
{e.status ? e.status : '#'}
</span>
</div>
</div>
</div>
</div>
</div>
)
})
setOpenWindow(true);
};
return (
<>
<div
className="user-file-search-results"
>
<div className="results-header">
<div className="results-title">Результаты поиска</div>
<div className="results-count">
Найдено файлов: {list.length}
</div>
</div>
<div
className="results-list"
>
{list.map((e: any, index: number) => {
if (e.similarityLevel !== 'DIFFERENT') {
return (
<div
key={index}
>
<div className="result-item">
<div className="result-header">
<div className="result-preview">
{e.url ? (
<img src={e.url} alt="Preview" />
) : (
<div className="preview-fallback">
<span>📄</span>
</div>
)}
</div>
<div className="result-info">
<div className="result-filename" title={e.originalFileName}>
{e.originalFileName}
</div>
<div className="result-details">
Загружен: {e.uploadDate ? e.uploadDate : '#'}
Размер: {convertBytes(e.fileSize)}
</div>
</div>
<div className="similarity-score">
<div className="similarity-percentage">
{e.similarityLevel}
</div>
</div>
</div>
<div className="result-badges">
<span className={`badge ${e.status === 'PROTECTED' ? 'badge-protected' : 'badge-not-protected'}`}>
{e.status}
</span>
</div>
<div className="result-actions">
<button
className="btn btn-secondary gap-1"
onClick={() => {
handlerViewfile(e);
}}
>
<IconEye />
Просмотр
</button>
<button
className="btn btn-success gap-1"
disabled={isFileLoading}
onClick={() => handlerDownload(e.fileId, e.originalFileName)}
>
<IconDownload />
Скачать
</button>
</div>
</div>
</div>)
}
})}
</div>
</div>
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
</>
)
}
@@ -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>
); );
} }
+54
View File
@@ -0,0 +1,54 @@
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
interface supportedFormats {
extensionVideo: string[]
extensionAudio: string[]
extensionImage: string[]
extensionDocument: string[]
}
export function SupportedFormats({ extensionVideo, extensionAudio, extensionImage, extensionDocument }: supportedFormats) {
return (
<div className="supported-formats">
<div className="formats-title">
Поддерживаемые форматы
</div>
<div className="format-group">
<div className="format-type image">
<IconImageFile /> Изображения
</div>
<div className="format-list">
{extensionImage.join(', ')}
</div>
</div>
<div className="format-group">
<div className="format-type video">
<IconVideoFile /> Видео
</div>
<div className="format-list">
{extensionVideo.join(', ')}
</div>
</div>
<div className="format-group">
<div className="format-type audio">
<IconAudioFile /> Аудио
</div>
<div className="format-list">
{extensionAudio.join(', ')}
</div>
</div>
<div className="format-group">
<div className="format-type document">
<IconDocument /> Документы
</div>
<div className="format-list">
{extensionDocument.join(', ')}
</div>
</div>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More