diff --git a/Dockerfile b/Dockerfile index 93db878..4329947 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /no-copy-frontend COPY . . COPY --from=dependencies /no-copy-frontend/node_modules ./node_modules -RUN npm run build +RUN rm -rf .next && npm run build --force FROM node:22.20.0 AS runner WORKDIR /no-copy-frontend diff --git a/package-lock.json b/package-lock.json index 66e0bfe..17a27a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { "name": "no-copy-frontend", - "version": "0.17.0", + "version": "0.40.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "no-copy-frontend", - "version": "0.17.0", + "version": "0.40.0", "dependencies": { + "@a2seven/yoo-checkout": "^1.1.4", "@react-input/mask": "^2.0.4", "@tailwindcss/postcss": "^4.1.17", "@tanstack/match-sorter-utils": "^8.19.4", @@ -26,6 +27,7 @@ "sonner": "^2.0.7", "swiper": "^12.0.3", "tailwindcss": "^4.1.17", + "use-debounce": "^10.1.0", "validator": "^13.15.26", "zod": "^4.1.13", "zustand": "^5.0.9" @@ -40,6 +42,20 @@ "typescript": "^5" } }, + "node_modules/@a2seven/yoo-checkout": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@a2seven/yoo-checkout/-/yoo-checkout-1.1.4.tgz", + "integrity": "sha512-Xn8E6fJKVUpSqTTEgigoi0Woyvmc1/UuHukwpQUTEtxD2l4skTEeqPlhd0J4xum6lQ/0eGzeJP+/MgpkE9Ak9g==", + "license": "MIT", + "dependencies": { + "@types/axios": "^0.14.0", + "axios": "^0.21.1", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -2313,6 +2329,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/axios": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz", + "integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==", + "deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "axios": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -3288,6 +3314,15 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -4653,6 +4688,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -7822,6 +7877,18 @@ "punycode": "^2.1.0" } }, + "node_modules/use-debounce": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.0.tgz", + "integrity": "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-intl": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz", @@ -7845,6 +7912,15 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validator": { "version": "13.15.26", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", diff --git a/package.json b/package.json index bc01c80..6429837 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "no-copy-frontend", - "version": "0.23.0", + "version": "0.42.0", "private": true, "scripts": { "dev": "next dev -p 2999", @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@a2seven/yoo-checkout": "^1.1.4", "@react-input/mask": "^2.0.4", "@tailwindcss/postcss": "^4.1.17", "@tanstack/match-sorter-utils": "^8.19.4", @@ -27,6 +28,7 @@ "sonner": "^2.0.7", "swiper": "^12.0.3", "tailwindcss": "^4.1.17", + "use-debounce": "^10.1.0", "validator": "^13.15.26", "zod": "^4.1.13", "zustand": "^5.0.9" diff --git a/public/build-info.json b/public/build-info.json new file mode 100644 index 0000000..37d74ed --- /dev/null +++ b/public/build-info.json @@ -0,0 +1,3 @@ +{ + "buildTime": null +} \ No newline at end of file diff --git a/src/app/[locale]/forgot-password/page.tsx b/src/app/[locale]/forgot-password/page.tsx index 1aff5a0..34d5374 100644 --- a/src/app/[locale]/forgot-password/page.tsx +++ b/src/app/[locale]/forgot-password/page.tsx @@ -2,7 +2,6 @@ import { useTranslations } from 'next-intl' import LogoIcon from '@/app/ui/logo-icon'; import styles from '@/app/styles/module/login.module.scss'; import Link from 'next/link'; -import LanguageSwitcher from '@/app/components/LanguageSwitcher'; import ForgotPasswordForm from '@/app/ui/forms/forgot-password-form'; export default function Page() { @@ -10,9 +9,6 @@ export default function Page() { return (
-
- -
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 33d2e07..d684eaf 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -6,7 +6,6 @@ import { routing } from '@/i18n/routing'; import { ToastProvider } from '@/app/providers/ToastProvider'; import "../styles/globals.css"; import "../styles/global-styles.scss"; -import ActivityTracker from '../components/ActivityTracker'; export function generateStaticParams() { return routing.locales.map((locale: any) => ({ locale })); @@ -35,7 +34,6 @@ export default async function RootLayout({ {children} - diff --git a/src/app/[locale]/login/page.tsx b/src/app/[locale]/login/page.tsx index 1f9376d..fa94248 100644 --- a/src/app/[locale]/login/page.tsx +++ b/src/app/[locale]/login/page.tsx @@ -17,9 +17,9 @@ export default function Page() { return (
-
+{/*
-
+
*/}
diff --git a/src/app/[locale]/pages/dashboard/page.tsx b/src/app/[locale]/pages/dashboard/page.tsx index 93962e9..5615b5b 100644 --- a/src/app/[locale]/pages/dashboard/page.tsx +++ b/src/app/[locale]/pages/dashboard/page.tsx @@ -26,7 +26,7 @@ export default function Page() {
- + {/* */}
@@ -34,8 +34,13 @@ export default function Page() {
+ + {/* */} +
+
-
diff --git a/src/app/[locale]/pages/layout.tsx b/src/app/[locale]/pages/layout.tsx index 23866bb..decb59c 100644 --- a/src/app/[locale]/pages/layout.tsx +++ b/src/app/[locale]/pages/layout.tsx @@ -3,6 +3,7 @@ import HeaderPanel from '@/app/ui/header/headerPanel'; import { getQueryClient } from '@/app/providers/getQueryClient'; import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries'; +import ActivityTracker from '@/app/components/ActivityTracker'; export default async function Layout({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient(); @@ -20,6 +21,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
+
); } \ No newline at end of file diff --git a/src/app/[locale]/pages/marking-audio/page.tsx b/src/app/[locale]/pages/marking-audio/page.tsx index 02a3364..ad46029 100644 --- a/src/app/[locale]/pages/marking-audio/page.tsx +++ b/src/app/[locale]/pages/marking-audio/page.tsx @@ -1,7 +1,7 @@ import ProtectionSummary from '@/app/ui/marking-page/protection-summary'; import FilesTable from '@/app/ui/dashboard/files-table'; import PageTitle from '@/app/ui/page-title'; -import UploadSectionFile from '@/app/components/upload-section-file'; +import UploadSectionFile from '@/app/components/UploadSectionFile'; import { getAllowedFilesExtensions } from '@/app/actions/fileUpload'; import { StackedBarChart } from '@/app/components/StackedBarChart'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; diff --git a/src/app/[locale]/pages/marking-document/page.tsx b/src/app/[locale]/pages/marking-document/page.tsx new file mode 100644 index 0000000..9e4b571 --- /dev/null +++ b/src/app/[locale]/pages/marking-document/page.tsx @@ -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 ( +
+ + + + +
+ ); +} \ No newline at end of file diff --git a/src/app/[locale]/pages/marking-photo/page.tsx b/src/app/[locale]/pages/marking-images/page.tsx similarity index 94% rename from src/app/[locale]/pages/marking-photo/page.tsx rename to src/app/[locale]/pages/marking-images/page.tsx index 963960e..9eba8da 100644 --- a/src/app/[locale]/pages/marking-photo/page.tsx +++ b/src/app/[locale]/pages/marking-images/page.tsx @@ -1,5 +1,5 @@ import FilesTable from '@/app/ui/dashboard/files-table'; -import UploadSectionFile from '@/app/components/upload-section-file'; +import UploadSectionFile from '@/app/components/UploadSectionFile'; import { getAllowedFilesExtensions } from '@/app/actions/fileUpload'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic'; diff --git a/src/app/[locale]/pages/marking-video/page.tsx b/src/app/[locale]/pages/marking-video/page.tsx index 8ee95b9..e2968f3 100644 --- a/src/app/[locale]/pages/marking-video/page.tsx +++ b/src/app/[locale]/pages/marking-video/page.tsx @@ -1,7 +1,7 @@ import ProtectionSummary from '@/app/ui/marking-page/protection-summary'; import FilesTable from '@/app/ui/dashboard/files-table'; import PageTitle from '@/app/ui/page-title'; -import UploadSectionFile from '@/app/components/upload-section-file'; +import UploadSectionFile from '@/app/components/UploadSectionFile'; import { getAllowedFilesExtensions } from '@/app/actions/fileUpload'; import { StackedBarChart } from '@/app/components/StackedBarChart'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; diff --git a/src/app/[locale]/pages/my-content/page.tsx b/src/app/[locale]/pages/my-content/page.tsx index d7393ba..b126dc1 100644 --- a/src/app/[locale]/pages/my-content/page.tsx +++ b/src/app/[locale]/pages/my-content/page.tsx @@ -1,13 +1,13 @@ import MyContentInfoBlock from '@/app/ui/my-content/my-content-info-block'; import FilesTable from '@/app/ui/dashboard/files-table'; import MyContentPieChart from '@/app/ui/my-content/my-content-pie-chart'; -import MyContentPageTitleColorFrame from '@/app/ui/my-content/new/my-content-page-title'; import MyContentStatsOverview from '@/app/ui/my-content/new/my-content-stats-overview'; +import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; export default function Page() { return ( <> - + {/*
diff --git a/src/app/[locale]/pages/refferals/page.tsx b/src/app/[locale]/pages/refferals/page.tsx index 2f30864..7cc1a5f 100644 --- a/src/app/[locale]/pages/refferals/page.tsx +++ b/src/app/[locale]/pages/refferals/page.tsx @@ -16,7 +16,7 @@ export default function Page() { ...}> - + {/* */} diff --git a/src/app/[locale]/pages/reports/page.tsx b/src/app/[locale]/pages/reports/page.tsx index dabb6fd..edf6d9c 100644 --- a/src/app/[locale]/pages/reports/page.tsx +++ b/src/app/[locale]/pages/reports/page.tsx @@ -5,12 +5,14 @@ import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month'; import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography'; import ActivityByContentType from '@/app/ui/reports/activity-by-content-type'; import PageTitle from '@/app/ui/page-title'; +import ReportsInfo from '@/app/ui/reports/reports-info'; export default function Page() { return (
- + {/* */} +
diff --git a/src/app/[locale]/pages/search/page.tsx b/src/app/[locale]/pages/search/page.tsx new file mode 100644 index 0000000..bae66e5 --- /dev/null +++ b/src/app/[locale]/pages/search/page.tsx @@ -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 ( + <> +
+

+ Поиск защищенного контента +

+

+ Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете +

+
+
+ +
+ + + +
+
+ + + ) +} \ No newline at end of file diff --git a/src/app/[locale]/pages/settings/page.tsx b/src/app/[locale]/pages/settings/page.tsx index cfa3abb..c6cd245 100644 --- a/src/app/[locale]/pages/settings/page.tsx +++ b/src/app/[locale]/pages/settings/page.tsx @@ -7,6 +7,7 @@ import LastActivitySettings from '@/app/ui/settings/last-activity-settings'; import DangerZone from '@/app/ui/settings/danger-zone'; import { useTranslations } from 'next-intl'; import PageTitle from '@/app/ui/page-title'; +import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel'; export default function Page() { const t = useTranslations('Global'); @@ -16,11 +17,12 @@ export default function Page() {
- + {/* */} - + {/* */} +
diff --git a/src/app/[locale]/register/page.tsx b/src/app/[locale]/register/page.tsx index 7293a9e..0b4eb3b 100644 --- a/src/app/[locale]/register/page.tsx +++ b/src/app/[locale]/register/page.tsx @@ -2,7 +2,6 @@ import LogoIcon from '@/app/ui/logo-icon'; import styles from '@/app/styles/module/login.module.scss' import Link from 'next/link'; import RegisterForm from '@/app/ui/forms/register-form'; -import LanguageSwitcher from '@/app/components/LanguageSwitcher'; import {useTranslations} from 'next-intl'; export default function Page() { @@ -10,9 +9,6 @@ export default function Page() { return (
-
- -
diff --git a/src/app/[locale]/reset-password/page.tsx b/src/app/[locale]/reset-password/page.tsx new file mode 100644 index 0000000..eac8849 --- /dev/null +++ b/src/app/[locale]/reset-password/page.tsx @@ -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 ( +
+
+
+ +

NO COPY

+

+ {t('recover-password')} +

+
+ + + +
+

{t('already-have-an-account')}? {t('login')}

+
+
+

{t('no-account')}? + {t('register')} +

+
+
+
+ ) +} \ No newline at end of file diff --git a/src/app/actions/action.ts b/src/app/actions/action.ts index 360af1a..71458cc 100644 --- a/src/app/actions/action.ts +++ b/src/app/actions/action.ts @@ -1,15 +1,14 @@ 'use server' + import { getSessionData, deleteSession } from '@/app/actions/session'; import { testUserData, API_BASE_URL } from '@/app/actions/definitions'; -import { tokenLifeExtension } from '@/app/actions/auth'; -import { redirect } from 'next/navigation'; export async function getUserData() { const userEmail = await getSessionData('email'); const token = await getSessionData('token'); /* для теста */ - if (userEmail === "test" && token === "1111") { + if (userEmail === 'test' && token === '1111') { return testUserData; } /* для теста */ @@ -18,17 +17,19 @@ export async function getUserData() { const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, { method: 'GET', headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}`, } }); if (response.ok) { return await response.json(); + } else { + return null } } catch (error) { - console.error('error'); + return null } } @@ -42,13 +43,13 @@ export async function getUserFilesInfo() { version: 1, msg_id: 20005, message_body: { - action: "user_files_info", + action: 'user_files_info', token: token } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); @@ -61,8 +62,100 @@ export async function getUserFilesInfo() { } else { throw parsed.message_code; } + } else { + return null } } catch (error) { - console.error(`error: ${error}`); + return null + } +} + +export async function fetchTariffs() { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30001, + message_body: { + action: 'get', + user_token: token + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + const parsed = await response.json() + if (parsed?.message_code === 0) { + return parsed.message_body?.tariffs; + } else { + return [] + } + + } else { + return null + } + } catch (error) { + return null + } +} + +export async function getBuildData() { + + try { + const response = await fetch(`${API_BASE_URL}/check/api/build`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + }); + + if (response.ok) { + return await response.json(); + } else { + return null + } + } catch (error) { + return null; + } +} + +export async function fetchINN(inn: string) { + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30004, + message_body: { + inn: inn, + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + const parsed = await response.json() + if (parsed?.message_code === 0) { + return parsed.message_body + } else { + return null + } + + } else { + return null + } + } catch (error) { + return null } } \ No newline at end of file diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index f9432f8..823d3ac 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -3,6 +3,7 @@ import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions'; import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session'; import { redirect } from 'next/navigation'; +import { fetchINN } from '@/app/actions/action'; export async function logout() { const token = await getSessionData('token'); @@ -11,9 +12,9 @@ export async function logout() { await fetch(`${API_BASE_URL}/v1/api/auth/logout`, { method: 'POST', headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "Authorization": `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': `Bearer ${token}`, } }); } catch (error) { @@ -36,8 +37,8 @@ export async function authorization( const password = formData.get('password'); /* для теста */ - if (email === "test" && password === "test") { - await createSession("1111", "test"); + if (email === 'test' && password === 'test') { + await createSession('1111', 'test'); redirect('/pages/dashboard'); } /* для теста */ @@ -81,13 +82,13 @@ export async function authorization( } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); if (response.ok) { let parsed = await response.json(); - if (parsed.message_desc === 'Operation successful') { + if (parsed.message_code === 0) { await createSession(parsed.message_body.token, email); } else { throw (`${parsed.message_code}`); @@ -130,14 +131,16 @@ export async function authorization( export type FormState = { previousState: { - fullName: string; - email: string; - password: string; - confirm_password: string; - phone: string; - companyName: string; - agree: string; - }, + fullName?: string | undefined; + email?: string | undefined; + password?: string | undefined; + confirm_password?: string | undefined; + phone?: string | undefined; + companyName?: string | undefined; + agree?: string | undefined; + referralCode?: string | undefined; + inn?: string | undefined; + } | undefined, mailConfirm?: boolean, userId?: number, error: Record | null @@ -147,6 +150,8 @@ export async function registration( state: FormState | undefined, formData: FormData, ): Promise { + const accountType = formData.get('accountType') as 'b2b' | 'b2c'; + const fullName = formData.get('fullName') as string || ''; const email = formData.get('email') as string || ''; const password = formData.get('password') as string || ''; @@ -154,8 +159,10 @@ export async function registration( const phone = formData.get('phone') as string || ''; const companyName = formData.get('companyName') as string || ''; const agree = formData.get('agree') as string || ''; + const referralCode = formData.get('referralCode'); + const inn = formData.get('inn') as string || ''; - const SignupFormSchema = await getSignupFormSchema(); + const SignupFormSchema = await getSignupFormSchema(accountType); const dataToValidate = { fullName, @@ -164,23 +171,41 @@ export async function registration( password, confirm_password, agree, - companyName + ...(accountType === 'b2b' && { companyName, inn }) }; - const validatedFields = SignupFormSchema.safeParse(dataToValidate); + const validatedFields = await SignupFormSchema.safeParse(dataToValidate); - if (!validatedFields.success) { + console.log(`try to register ${accountType}`); + + let confirmedCompanyName: boolean = true; + if (accountType === 'b2b') { + try { + const response = await fetchINN(inn); + confirmedCompanyName = response?.companyName ? true : false; + } catch (error) { + confirmedCompanyName = false; + } + } + + if (!validatedFields.success || !confirmedCompanyName) { const errors: Record = {} - JSON.parse(validatedFields.error.message).forEach((obj: { - message: string, - path: string - }) => { - if (errors[obj.path[0]]) { - errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`; - } else { - errors[obj.path[0]] = obj.message; - } - }); + if (validatedFields?.error?.message) { + JSON?.parse(validatedFields?.error?.message).forEach((obj: { + message: string, + path: string + }) => { + if (errors[obj.path[0]]) { + errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`; + } else { + errors[obj.path[0]] = obj.message; + } + }); + } + + if (!confirmedCompanyName) { + errors.inn = 'inn-invalid' + } return { previousState: { @@ -189,8 +214,8 @@ export async function registration( password, confirm_password, phone, - companyName, - agree + agree, + ...(accountType === 'b2b' && { companyName, inn }) }, error: errors }; @@ -208,25 +233,47 @@ export async function registration( email, phone: phone, password, - companyName: companyName, + accountType, + ...(accountType === 'b2b' && { companyName: companyName, inn: inn }), + ...(referralCode !== '' && { referralLink: referralCode }) } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); + console.log({ + fullName: fullName, + email, + phone: phone, + password, + accountType, + ...(accountType === 'b2b' && { companyName: companyName, inn: inn }), + ...(referralCode !== '' && { referralLink: referralCode }) + }) + if (response.ok) { let parsed = await response.json() as { message_desc: string, message_body: { token: string, userId: number }, - message_code: string + message_code: number }; - console.log(parsed); - if (parsed.message_desc === 'Operation successful') { + if (parsed.message_code === 0) { + console.log(`registration - ${accountType}`); + console.log({ + fullName: fullName, + email, + phone: phone, + password, + accountType, + ...(referralCode !== '' && { referralLink: referralCode }), + ...(accountType === 'b2b' && { companyName: companyName, inn: inn }) + }); /* await createSession(parsed.message_body.token, email as string); */ + return { previousState: { fullName, @@ -234,14 +281,17 @@ export async function registration( password, confirm_password, phone, - companyName, - agree + agree, + ...(referralCode !== '' && { referralLink: referralCode }), + ...(accountType === 'b2b' && { companyName: companyName, inn: inn }) }, mailConfirm: true, userId: parsed.message_body.userId, error: null }; + } else { + console.log(parsed); throw parsed; } } else { @@ -249,26 +299,35 @@ export async function registration( } } catch (error: unknown) { + const errors: Record = {}; if (error) { const typedError = error as any; - const errors: Record = {}; switch (typedError.message_code) { case 1: - errors['server'] = 'email-or-already-registered' - break; - + if (typedError.message_desc === 'Refferal link is not exist') { + errors['server'] = 'referral-link-is-not-exist' + break; + } else { + errors['server'] = 'email-or-already-registered' + break; + } case 2: - typedError.message_body.fieldErrors.forEach((obj: { - code: string, - field: string - }) => { - if (errors[obj.field]) { - errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`; - } else { - errors[obj.field] = obj.code.replaceAll('.', '-'); - } - }); + if (typedError.message_body?.fieldErrors) { + typedError.message_body?.fieldErrors?.forEach((obj: { + code: string, + field: string + }) => { + if (errors[obj.field]) { + errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`; + } else { + errors[obj.field] = obj.code.replaceAll('.', '-'); + } + }); + + } else { + errors['server'] = 'unknown-error' + } break; default: @@ -284,14 +343,27 @@ export async function registration( confirm_password, phone, companyName, - agree + agree, + inn }, mailConfirm: false, error: errors }; } - throw error; + return { + previousState: { + fullName, + email, + password, + confirm_password, + phone, + companyName, + agree, + inn + }, + error: errors + }; } redirect('/pages/dashboard'); @@ -306,8 +378,8 @@ export async function tokenLifeExtension() { const response = await fetch(`${API_BASE_URL}/api/v1/data`, { method: 'POST', headers: { - "Content-Type": "application/json", - "Accept": "application/json", + 'Content-Type': 'application/json', + 'Accept': 'application/json', }, body: JSON.stringify({ version: 1, @@ -331,11 +403,12 @@ export async function tokenLifeExtension() { } export async function confirmEmail( - state: {error: string} | undefined, + state: { error: string } | undefined, formData: FormData ) { console.log('confirmEmail'); const emailConfirm = formData.get('emailConfirm') as string || ''; + console.log(emailConfirm); const userId = formData.get('userId') as string || ''; try { diff --git a/src/app/actions/companyActions.ts b/src/app/actions/companyActions.ts new file mode 100644 index 0000000..c6257da --- /dev/null +++ b/src/app/actions/companyActions.ts @@ -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 | null +} | undefined; + +export async function companyUserRegistration( + state: CompanyUserRegistrationFormState | undefined, + formData: FormData, +): Promise { + 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 = {} + 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 = {}; + 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 + }; + } +} \ No newline at end of file diff --git a/src/app/actions/definitions.ts b/src/app/actions/definitions.ts index cbd260d..fd774a0 100644 --- a/src/app/actions/definitions.ts +++ b/src/app/actions/definitions.ts @@ -1,5 +1,5 @@ import * as z from 'zod' -import { createValidationSchema } from "@/app/lib/validation-config-parser"; +import {createValidationSchema, createValidationSchemaForPassword} from '@/app/lib/validation-config-parser'; import validationConfig from '@/app/lib/validation-config.json'; export type FormState = @@ -35,30 +35,31 @@ interface ValidationConfig { const defaultConfig = validationConfig as ValidationConfig; -let cachedSignupSchema: any = null; -let schemaPromise: Promise | null = null; - -async function loadConfigFromBackend(): Promise { +async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise { try { const response = await fetch('/api/validation-config'); if (!response.ok) throw new Error('Failed to load config'); return await response.json(); } catch (error) { - console.error('Using default validation config:', error); + //сейчас выполняем это пока бека нету + /* console.error('Using default validation config:', error); */ return defaultConfig; } } -export async function getSignupFormSchema() { - console.log('get scheme'); - if (cachedSignupSchema) { - return cachedSignupSchema; - } +export async function getSignupFormSchema(schemaType?: 'b2b' | 'b2c' | 'resetPassword') { + let schemaPromise: Promise | null = null; + let cachedSignupSchema: any = null; if (!schemaPromise) { schemaPromise = (async () => { const config = await loadConfigFromBackend(); - cachedSignupSchema = createValidationSchema(config); + if (schemaType === 'resetPassword') { + cachedSignupSchema = createValidationSchemaForPassword(config); + } else { + cachedSignupSchema = createValidationSchema(config, schemaType); + } + return cachedSignupSchema; })(); } @@ -66,6 +67,7 @@ export async function getSignupFormSchema() { return await schemaPromise; } +// для валидации в реально времени на клиентской странице export const SignupFormSchema = createValidationSchema(defaultConfig); diff --git a/src/app/actions/fileEntity.ts b/src/app/actions/fileEntity.ts index a42edc2..2dfc5f5 100644 --- a/src/app/actions/fileEntity.ts +++ b/src/app/actions/fileEntity.ts @@ -21,8 +21,8 @@ export async function getUserFilesData(page: number, pageSize: number) { } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); @@ -42,7 +42,7 @@ export async function getUserFilesData(page: number, pageSize: number) { } } -export async function removeUserFile(fileId: string) { +export async function removeUserFile(fileId: string, fullDelete: number) { const token = await getSessionData('token'); try { @@ -54,12 +54,13 @@ export async function removeUserFile(fileId: string) { message_body: { action: 'delete_file', file_id: fileId, + full_delete: fullDelete, token: token } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); @@ -75,6 +76,44 @@ export async function removeUserFile(fileId: string) { throw (`${response.status}`); } + } catch (error) { + return error + } +} + +export async function viewFileInfo(fileId: string) { + const token = await getSessionData('token'); + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 20005, + message_body: { + action: 'file_info', + file_id: fileId, + token: token + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + let parsed = await response.json(); + + if (parsed.message_code === 0) { + return parsed.message_body; + } else { + throw parsed; + } + } else { + throw (`${response.status}`); + } + } catch (error) { return error } diff --git a/src/app/actions/fileUpload.ts b/src/app/actions/fileUpload.ts index cc304d0..776e1c4 100644 --- a/src/app/actions/fileUpload.ts +++ b/src/app/actions/fileUpload.ts @@ -2,6 +2,8 @@ import { API_BASE_URL } from '@/app/actions/definitions'; import { getSessionData } from '@/app/actions/session'; +import { FormState } from '@/app/actions/auth'; +import { unknown } from 'zod'; interface initMessageBody { file_name: string, @@ -12,15 +14,18 @@ interface initMessageBody { token?: string } -export async function fileUpload(messageBody: initMessageBody) { +export async function fileUpload(messageBody: initMessageBody, convertTo: string | null | undefined) { const token = await getSessionData('token'); if (!token) { return; } const message = messageBody; - message.action = "init"; + message.action = 'init'; message.token = token; + // нужно будет добавить свойство convertTo в тело запроса он может прийти со значением reject тогда конвертировать не нужно. + // console.log(convertTo); + try { const response = await fetch(`${API_BASE_URL}/api/v1/data`, { method: 'POST', @@ -30,8 +35,8 @@ export async function fileUpload(messageBody: initMessageBody) { message_body: message }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); @@ -68,13 +73,13 @@ export async function cancelUpload(udloadId: string) { version: 1, msg_id: 20004, message_body: { - action: "cancel", + action: 'cancel', upload_id: udloadId, } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); @@ -101,7 +106,12 @@ export async function cancelUpload(udloadId: string) { } } -export async function chunkUpload(formData: FormData) { +export type ChunkResponse = { + errorMesage: string | null + fileId?: string +}; + +export async function chunkUpload(formData: FormData): Promise { try { const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, { method: 'POST', @@ -111,17 +121,34 @@ export async function chunkUpload(formData: FormData) { if (response.ok) { let parsed = await response.json(); - if (parsed.message_desc === 'Upload cancelled successfully') { - return parsed.message_body; + if (parsed.message_code === 0) { + return { + errorMesage: null, + fileId: parsed.message_body.file_id + } + } else if (parsed.message_desc === 'Duplicate file upload') { + throw 'duplicate-file' + } else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: User not have tokens for protect') { + throw 'user-not-have-tokens-for-protect' } else { - throw parsed; + throw 'error' } } else { - throw (`${response.status}`); + throw 'error' + } + } catch (error: unknown) { + if (error) { + const typedError = error as any; + + return { + errorMesage: typedError + }; + } else { + return { + errorMesage: 'error' + }; } - } catch (error) { - return error } } @@ -138,8 +165,8 @@ export async function checkChunkStatus(upload_id: string) { } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); @@ -173,8 +200,8 @@ export async function getAllowedFilesExtensions(type: string) { } }), headers: { - "Content-Type": "application/json", - "Accept": "application/json" + 'Content-Type': 'application/json', + 'Accept': 'application/json' } }); diff --git a/src/app/actions/referralsActions.ts b/src/app/actions/referralsActions.ts new file mode 100644 index 0000000..01a6020 --- /dev/null +++ b/src/app/actions/referralsActions.ts @@ -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 + } + } +} \ No newline at end of file diff --git a/src/app/actions/searchActions.ts b/src/app/actions/searchActions.ts new file mode 100644 index 0000000..ac481d8 --- /dev/null +++ b/src/app/actions/searchActions.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/app/actions/session.ts b/src/app/actions/session.ts index 76a2191..dbd8b07 100644 --- a/src/app/actions/session.ts +++ b/src/app/actions/session.ts @@ -97,7 +97,12 @@ export async function getSessionData(type: 'token' | 'email'): Promise(null); const isRefreshing = useRef(false); + + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + const selectedId = document.cookie + .split('; ') + .find(row => row.startsWith('searchedFileId=')) + ?.split('=')[1]; + + const handleCookie = async () => { + if (selectedId) { + await removeUserFile(selectedId, 1); + document.cookie = 'searchedFileId=; path=/; max-age=0'; + } + }; + + if (selectedId) { + handleCookie(); + } + }, [pathname, searchParams]); + // Debounce функция const debounce = useCallback((func: () => void, delay: number) => { if (activityTimeoutRef.current) { diff --git a/src/app/components/dropDownList.tsx b/src/app/components/DropDownList.tsx similarity index 100% rename from src/app/components/dropDownList.tsx rename to src/app/components/DropDownList.tsx diff --git a/src/app/components/DropDownListForImageConvert.tsx b/src/app/components/DropDownListForImageConvert.tsx new file mode 100644 index 0000000..51c75c2 --- /dev/null +++ b/src/app/components/DropDownListForImageConvert.tsx @@ -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 { + '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(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(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 ( +
+ + {isOpen && ( +
+
    + {enhancedChildren} +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/components/FileTypeIcon.tsx b/src/app/components/FileTypeIcon.tsx new file mode 100644 index 0000000..3b57350 --- /dev/null +++ b/src/app/components/FileTypeIcon.tsx @@ -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 + + ; + case 'video': + return + + ; + case 'audio': + return + + ; + default: + return + + ; + } +}; \ No newline at end of file diff --git a/src/app/components/modalWindow.tsx b/src/app/components/ModalWindow.tsx similarity index 100% rename from src/app/components/modalWindow.tsx rename to src/app/components/ModalWindow.tsx diff --git a/src/app/components/Pagination.tsx b/src/app/components/Pagination.tsx new file mode 100644 index 0000000..fc2f25c --- /dev/null +++ b/src/app/components/Pagination.tsx @@ -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 ( +
+ {visiblePages[0] > 1 && ( + <> + + {visiblePages[0] > 2 && ...} + + )} + + {visiblePages.map(page => ( + + ))} + + {visiblePages[visiblePages.length - 1] < totalPages && ( + <> + {visiblePages[visiblePages.length - 1] < totalPages - 1 && ...} + + + )} +
+ ); +} \ No newline at end of file diff --git a/src/app/components/tanstakTable.tsx b/src/app/components/TanstakTable.tsx similarity index 87% rename from src/app/components/tanstakTable.tsx rename to src/app/components/TanstakTable.tsx index a334407..45a7b8c 100644 --- a/src/app/components/tanstakTable.tsx +++ b/src/app/components/TanstakTable.tsx @@ -11,15 +11,17 @@ import { SortingState, ColumnFiltersState, } from '@tanstack/react-table'; -import { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } from '@/app/ui/icons/icons'; +import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation } from '@/app/ui/icons/icons'; import { useTranslations, useLocale } from 'next-intl'; -import DropDownList from '@/app/components/dropDownList'; +import DropDownList from '@/app/components/DropDownList'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { getUserFilesData, removeUserFile } from '@/app/actions/fileEntity'; -import ModalWindow from '@/app/components/modalWindow'; +import { getUserFilesData, removeUserFile, viewFileInfo } from '@/app/actions/fileEntity'; +import ModalWindow from '@/app/components/ModalWindow'; import { toast } from 'sonner'; import { pluralize } from '@/app/lib/pluralize'; import { convertBytes } from '@/app/lib/convertBytes'; +import { FileInfoModalWindow } from '@/app/ui/modal-windows/file-info-modal-window'; +import { FileTypeIcon } from '@/app/components/FileTypeIcon'; type FileType = 'image' | 'video' | 'audio'; @@ -31,7 +33,9 @@ type FileItem = { size?: number | undefined; uploadDate?: number; status?: string; + protectStatus: string; _original?: ApiFile; + supportId: number; }; type ApiFile = { @@ -42,32 +46,15 @@ type ApiFile = { fileSize: number; updatedAt: string; status: string; + protectStatus: string; + supportId: number; + fileName: string; }; type ApiResponse = { files?: ApiFile[]; }; -// Иконки для типов файлов -const FileTypeIcon = ({ type }: { type: string }) => { - switch (type) { - case 'image': - return - - ; - case 'video': - return - - ; - case 'audio': - return - - ; - default: - return 📄; - } -}; - // Форматирование даты из timestamp const formatDate = (timestamp: number) => { const date = new Date(timestamp); @@ -130,10 +117,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { if (!data?.files) return []; return data.files.map((item: ApiFile) => { - const [datePart, timePart] = item.updatedAt.split(' '); - const [day, month, year] = datePart.split('-').map(Number); - const [hours, minutes, seconds] = timePart.split(':').map(Number); - const newDate = new Date(year, month - 1, day, hours, minutes, seconds).getTime(); + const newDate = new Date(item.updatedAt).getTime(); return { id: item.id, @@ -143,6 +127,8 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { size: item.fileSize, uploadDate: newDate, status: item.status, + protectStatus: item.protectStatus, + supportId: item.supportId, _original: item }; }); @@ -171,6 +157,41 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { // Определение колонок const columns = useMemo[]>( () => [ + { + accessorKey: 'id', + header: ({ column }) => ( +
+ + ID + + +
+ ), + cell: ({ row }) => { + return ( +
+ {row.original.supportId ? row.original.supportId : '-'} +
+ ) + }, + }, { accessorKey: 'fileName', header: ({ column }) => ( @@ -207,7 +228,9 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { {cutFileName(row.original.fileName)}
- {cutFileExtension(row.original.fileName)} {row.original?.status} + {cutFileExtension(row.original.fileName)} + {row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')} +
@@ -364,7 +387,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { return (
- {t('status')} + {row.original.status}
) @@ -382,31 +405,13 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { cell: ({ row }) => (
- -
-
- - */} +
+
+ {row.original.protectStatus === 'PROTECTED' && ( + + )} +
@@ -426,9 +456,19 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { [] ); - // Обработчики действий - const handleView = (file: FileItem) => { + const handleView = async (file: FileItem) => { console.log(`Просмотр файла: ${file.fileName}`); + console.log(file); + + const fileInfo = await viewFileInfo(file.id); + console.log(fileInfo); + + setOpenWindowChildren(() => { + return ( + + ) + }) + setOpenWindow(true); }; const handleDownload = async (file: FileItem) => { @@ -445,7 +485,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = file.fileName || `file-${file.id}`; + a.download = file._original?.fileName || file.fileName || `file-${file.id}`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); @@ -474,7 +514,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
+ + { + error && ( +
+

{error}

+
+ ) + } + + { + selectedFiles.length > 0 && ( +
+
+
+

+ {t('total-files')}: {selectedFiles.length} + {t('uploading')}: {uploadingCount} + {t('completed')}: {completedCount} +

+
+
+
+
+
+ +
+ + < button + className="btn btn-primary" + onClick={() => handleClearFile()} + type="button" + > + {t('clear-all')} + +
+
+ +
+ { + selectedFiles.map((file, index) => ( +
+
+
+

+ {t('file')}: {file.name} +

+ < p className="text-gray-700" > + {t('size')}: {file.size} +

+ < p className="text-gray-700" > + {t('status')}: + + { + file.status === 'completed' ? t('completed') : + file.status === 'uploading' ? t('uploading') : + file.status === 'error' ? t('error') : + t('pending') + } + +

+ { + file.error && ( +

{file.error}

+ ) + } +
+
+ { + (fileType === 'image' && file.status === 'pending') && ( + <> + +
  • + {t('do-not-convert')} +
  • + { + allowedExtensions && allowedExtensions.map(extension => { + if (extension === 'gif') { + return null + } + + return ( +
  • + {t('convert-to')} {extension} +
  • + ) + }) + } +
    + + ) + } +
    + + {file.progress} % + + < button + className="btn btn-cancel" + onClick={() => handleClearFile(file.uploadId || file.name)} + type="button" + > + {t('remove')} + +
    +
    +
    + + { + (fileType === 'image' && file.preview) && ( +
    + Предпросмотр загруженного изображения setError('Не удалось загрузить превью изображения') + } + /> +
    + + ) + } + + { + file.status === 'pending' && ( +
    + +
    + )} +
    + ))} +
    +
    + )} +
    +
    + ); +} \ No newline at end of file diff --git a/src/app/hooks/react-query/useUserDataInfo.ts b/src/app/hooks/react-query/useUserDataInfo.ts new file mode 100644 index 0000000..30452c4 --- /dev/null +++ b/src/app/hooks/react-query/useUserDataInfo.ts @@ -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; + }, + }); +}; \ No newline at end of file diff --git a/src/app/hooks/react-query/useUserFilesInfo.ts b/src/app/hooks/react-query/useUserFilesInfo.ts new file mode 100644 index 0000000..56783eb --- /dev/null +++ b/src/app/hooks/react-query/useUserFilesInfo.ts @@ -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, + }, + }; + }, + }); +}; \ No newline at end of file diff --git a/src/app/hooks/react-query/useUserSearchData.ts b/src/app/hooks/react-query/useUserSearchData.ts new file mode 100644 index 0000000..92d6e04 --- /dev/null +++ b/src/app/hooks/react-query/useUserSearchData.ts @@ -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 + }); +}; \ No newline at end of file diff --git a/src/app/lib/convertBytes.ts b/src/app/lib/convertBytes.ts index 3516c98..b1886fe 100644 --- a/src/app/lib/convertBytes.ts +++ b/src/app/lib/convertBytes.ts @@ -1,4 +1,8 @@ export function convertBytes(bytes: number) { + if (!bytes) { + return 0; + } + const units = ['B', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) return '0 B'; diff --git a/src/app/lib/getProcents.ts b/src/app/lib/getProcents.ts new file mode 100644 index 0000000..686bb8a --- /dev/null +++ b/src/app/lib/getProcents.ts @@ -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; +} \ No newline at end of file diff --git a/src/app/lib/prefetch-queries.ts b/src/app/lib/prefetch-queries.ts index 791c71c..71c5d57 100644 --- a/src/app/lib/prefetch-queries.ts +++ b/src/app/lib/prefetch-queries.ts @@ -1,6 +1,7 @@ import { QueryClient } from '@tanstack/react-query'; -import { getUserData, getUserFilesInfo } from '@/app/actions/action'; +import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action'; import { getUserFilesData } from '@/app/actions/fileEntity'; +import { fetchReferralUserStats } from '@/app/actions/referralsActions'; export async function prefetchLayoutQueries(queryClient: QueryClient) { await Promise.all([ @@ -19,5 +20,20 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) { queryKey: ['userFilesInfo'], queryFn: () => getUserFilesInfo() }), + queryClient.prefetchQuery({ + queryKey: ['referralUserStats'], + queryFn: () => fetchReferralUserStats() + }), + queryClient.prefetchQuery({ + queryKey: ['backendBuildDate'], + queryFn: async () => { + try { + const response = await getBuildData(); + return response.buildTimeBack ?? null; + } catch (error) { + return null; + } + } + }), ]); } \ No newline at end of file diff --git a/src/app/lib/validation-config-parser.ts b/src/app/lib/validation-config-parser.ts index 0051173..b964ad5 100644 --- a/src/app/lib/validation-config-parser.ts +++ b/src/app/lib/validation-config-parser.ts @@ -19,7 +19,7 @@ interface ValidationConfig { /* const EMAIL_REGEX = /^([a-z0-9.\-_]+|[а-яё0-9.\-_]+)@(([a-z0-9.\-]+)\.([a-z]{2,})|([a-z0-9.\-]+)\.([а-яё]{2,})|([а-яё0-9.\-]+)\.([a-z]{2,})|([а-яё0-9.\-]+)\.([а-яё]{2,}))$/i; */ -export function createValidationSchema(config: ValidationConfig) { +export function createValidationSchema(config: ValidationConfig, accountType?: 'b2b' | 'b2c') { const schema: Record = {}; // fullName @@ -38,6 +38,22 @@ export function createValidationSchema(config: ValidationConfig) { .trim(); } + // fullName + if (config.companyName && accountType !== 'b2c') { + schema.companyName = z + .string() + .trim() + .min(config.companyName.minSize?.value || 1, { + error: config.companyName.minSize?.message || 'register-error-company-name-min' + }) + .max(config.companyName.maxSize?.value || 100, { + error: config.companyName.maxSize?.message || 'register-error-company-name-max' + }) + .regex(new RegExp(config.companyName.allowedSymbols?.value || '.*'), { + message: config.companyName.allowedSymbols?.message || 'register-error-company-name-not-allowed-symbols' + }); + } + // email if (config.email) { let emailSchema = z @@ -147,79 +163,92 @@ export function createValidationSchema(config: ValidationConfig) { schema.password = passwordSchema; } - // companyName (опциональное поле) - if (config.companyName) { - let companySchema = z.string(); + // inn + if (config.inn && accountType !== 'b2c') { + let innSchema = z.string(); - // Если поле опциональное, добавляем .optional().nullable() - if (config.companyName.optional?.value) { - companySchema = companySchema - .optional() - .nullable() - .transform((val) => (val === null || val === undefined ? '' : val)) - .refine( - (value) => { - // Для пустого значения пропускаем проверки - if (!value || value.trim().length === 0) return true; - - // Проверка минимальной длины - const minLength = config.companyName?.minSize?.value || 2; - return value.trim().length >= minLength; - }, - { - message: config.companyName.minSize?.message || 'register-error-company-name-min' - } - ) as unknown as z.ZodString; - } else { - // Если поле обязательное - if (config.companyName.minSize) { - companySchema = companySchema.min(config.companyName.minSize.value, { - error: config.companyName.minSize.message - }); - } + // Минимальная длина + if (config.inn.minSize) { + innSchema = innSchema.min(config.inn.minSize.value, { + error: config.inn.minSize.message + }); } - // Максимальная длина (применяется всегда) - if (config.companyName.maxSize) { - companySchema = companySchema.refine( - (value) => { - if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) { - return true; - } - const maxLength = config.companyName?.maxSize?.value || 200; - return (value || '').trim().length <= maxLength; - }, - { - message: config.companyName.maxSize.message - } - ); + // Максимальная длина + if (config.inn.maxSize) { + innSchema = innSchema.max(config.inn.maxSize.value, { + error: config.inn.maxSize.message + }); } // Допустимые символы - if (config.companyName.allowedSymbols?.value) { - companySchema = companySchema.refine( - (value) => { - if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) { - return true; - } - const regex = new RegExp(config.companyName.allowedSymbols!.value); - return regex.test(value || ''); - }, - { - message: config.companyName.allowedSymbols.message - } + if (config.inn.allowedSymbols) { + innSchema = innSchema.regex( + new RegExp(config.inn.allowedSymbols.value), + { message: config.inn.allowedSymbols.message } ); } - companySchema = companySchema.transform((val) => val?.trim() || '') as unknown as z.ZodString; - - schema.companyName = companySchema; + schema.inn = innSchema; } // Дополнительные поля для формы регистрации schema.confirm_password = z.string(); schema.agree = z.string().min(2, { error: 'register-error-agree' }); + return z + .object(schema) + .refine((data) => data.password === data.confirm_password, { + message: 'repeat-password', + path: ['confirm_password'], + }); +} + +export function createValidationSchemaForPassword(config: ValidationConfig) { + const schema: Record = {}; + + // 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, { diff --git a/src/app/lib/validation-config.json b/src/app/lib/validation-config.json index b4620de..bd9f6f2 100644 --- a/src/app/lib/validation-config.json +++ b/src/app/lib/validation-config.json @@ -70,7 +70,7 @@ "message": "register-error-company-name-min" }, "allowedSymbols": { - "value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]+$", + "value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]*$", "message": "register-error-company-name-not-allowed-symbols" } }, @@ -80,12 +80,26 @@ "message": "register-error-phone-max" }, "minSize": { - "value": 2, + "value": 12, "message": "register-error-phone-min" }, "allowedSymbols": { "value": "^[0-9\\+]+$", "message": "register-error-phone-not-allowed-symbols" } + }, + "inn": { + "maxSize": { + "value": 10, + "message": "register-error-inn-max" + }, + "minSize": { + "value": 10, + "message": "register-error-inn-min" + }, + "allowedSymbols": { + "value": "^[0-9]+$", + "message": "register-error-inn-not-allowed-symbols" + } } } \ No newline at end of file diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index c2f5c4b..4dd2171 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -41,6 +41,10 @@ color: v.$white; max-width: calc(100vw - var(--side-bar-width) - 60px); + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } + h1 { font-size: 2rem; font-weight: 700; @@ -73,6 +77,10 @@ display: flex; gap: 28px; align-items: center; + + @media (max-width: 440px) { + flex-direction: column; + } } .header-stat-item { @@ -101,6 +109,10 @@ padding: 30px; background: v.$bg-light; min-height: 100vh; + + @media (max-width: 440px) { + padding: 15px; + } } .main-containter { @@ -121,6 +133,10 @@ &.auto { width: auto; } + + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } } .btn { @@ -142,7 +158,12 @@ color: v.$white; border: 2px solid transparent; - &:hover { + &:disabled { + background: linear-gradient(135deg, v.$p-color-disabled, v.$s-color-disabled); + cursor: default; + } + + &:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 25px v.$shadow-1; } @@ -244,6 +265,11 @@ color: v.$text-p; border-radius: 20px; box-shadow: 0px 4px 20px v.$shadow-1; + + &:has(.modal-window-view-file) { + padding: 0; + background: transparent; + } } } @@ -280,6 +306,13 @@ width: var(--side-bar-width); transform: translateX(0); } + + .build-versions { + position: absolute; + bottom: 10px; + left: 10px; + color: #fffc; + } } .sidebar-close-button { @@ -324,7 +357,7 @@ } .sub-link { - padding: 12px 50px; + padding: 12px 12px 12px 32px; font-size: 14px; border-radius: 0; margin-right: 0; @@ -428,6 +461,13 @@ padding: 8px; border-radius: 6px; } + + &-document { + color: v.$white; + background: v.$color-document; + padding: 8px; + border-radius: 6px; + } } .split-blocks { diff --git a/src/app/styles/header.scss b/src/app/styles/header.scss index e534826..e978741 100644 --- a/src/app/styles/header.scss +++ b/src/app/styles/header.scss @@ -14,11 +14,19 @@ padding: 20px 20px; } + @media (max-width: 440px) { + padding: 10px 10px; + } + .header-action { display: flex; align-items: center; gap: 15px; + @media (max-width: 440px) { + gap: 10px; + } + .icon-btn { width: 40px; height: 40px; @@ -36,6 +44,11 @@ background: v.$bg-hover; transform: translateY(-2px); } + + @media (max-width: 440px) { + width: 30px; + height: 30px; + } } } @@ -68,12 +81,20 @@ @media (max-width: 490px) { padding: 8px; } + + @media (max-width: 440px) { + padding: 8px; + } } .tokens-count { font-weight: 700; font-size: 14px; letter-spacing: 0.5px; + + @media (max-width: 440px) { + font-size: 12px; + } } .tokens-label { @@ -192,6 +213,11 @@ cursor: pointer; transition: all 0.3s; text-transform: uppercase; + + @media (max-width: 440px) { + width: 30px; + height: 30px; + } } .user-dropdown { diff --git a/src/app/styles/module/login.module.scss b/src/app/styles/module/login.module.scss index a868274..aa108e6 100644 --- a/src/app/styles/module/login.module.scss +++ b/src/app/styles/module/login.module.scss @@ -41,8 +41,35 @@ .form-group { margin-bottom: 18px; +} - &-confrim-window { +.form-switcher { + display: flex; + justify-content: center; + gap: 10px; + margin-bottom: 10px; + + &-button { + padding: 5px 15px; + background: linear-gradient(135deg, #6366f1, #8b5cf6); + color: white; + border: none; + border-radius: 10px; + transition: all 0.3s; + opacity: 0.6; + cursor: pointer; + + &.active { + opacity: 1; + } + + &:disabled { + opacity: 0.5; + } + + &:hover:not(:disabled) { + transform: translateY(-2px); + } } } @@ -74,6 +101,10 @@ transition: all 0.3s; background: #f9fafb; + &-hidden { + display: none; + } + &[type="password"]::-ms-reveal { display: none; } @@ -141,11 +172,12 @@ margin-bottom: 18px; margin: 20px 0 0 0; - &:hover { + &:hover:not(:disabled) { transform: translateY(-2px); } &:disabled { + opacity: 0.5; background: linear-gradient(135deg, #414291, #4b3975); &:hover { diff --git a/src/app/styles/pages-styles.scss b/src/app/styles/pages-styles.scss index 8adc088..1639155 100644 --- a/src/app/styles/pages-styles.scss +++ b/src/app/styles/pages-styles.scss @@ -236,6 +236,7 @@ border-bottom: 1px solid v.$b-color-1; align-items: center; overflow: auto; + border-radius: 10px; .stats-item { border: 1px solid v.$b-color-1; @@ -371,6 +372,11 @@ background: v.$p-color; margin-bottom: 10px; margin-top: 20px; + + &:disabled { + background: linear-gradient(135deg, v.$p-color-disabled, v.$s-color-disabled); + cursor: default; + } } } @@ -389,6 +395,11 @@ text-align: left; display: flex; justify-content: space-between; + + @media (max-width: 500px) { + flex-direction: column; + gap: 10px; + } } .uploaded-image { @@ -399,6 +410,25 @@ border: 1px solid #d1d5db; } + .btn-convert { + background: v.$white; + color: v.$p-color; + border: 2px solid v.$p-color; + padding: 5px 15px; + border-radius: 12px; + cursor: pointer; + font-weight: 600; + font-size: 16px; + transition: 0.3s; + min-width: 120px; + + &.active { + background: #00a63e; + border: 2px solid #00a63e; + color: v.$white; + } + } + .btn-confirm { background: linear-gradient(45deg, v.$p-color 50%, v.$s-color 100%); color: v.$white; @@ -433,12 +463,86 @@ font-size: 16px; transition: 0.3s; min-width: 120px; + + @media (max-width: 500px) { + max-height: 34px; + } } .btn:hover { transform: translateY(-2px); box-shadow: 0 8px 25px v.$shadow-1; } + + .dropdown-wrapper { + .dropdown-button { + border: 2px solid v.$p-color; + color: v.$p-color; + min-width: 200px; + } + + .dropdown-list { + border: 1px solid v.$p-color; + color: v.$p-color; + + li { + &:first-child { + border-radius: 12px 12px 0 0; + } + + &:last-child { + border-radius: 0 0 12px 12px; + } + } + } + } + } + + .selected-file-action-panel { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + @media (max-width: 900px) { + flex-direction: column; + } + } + + .selected-file-right-block { + display: flex; + flex-direction: column; + gap: 10px; + align-items: end; + /* align-items: center; */ + + @media (max-width: 500px) { + flex-direction: column-reverse; + gap: 8px; + } + + .upload-progress { + font-weight: 500; + font-size: 18px; + margin-right: 16px; + + @media (max-width: 500px) { + margin-right: 0px; + } + } + } + + .selected-file-action-info { + color: #374151; + font-weight: 500; + display: flex; + gap: 10px; + } + + .selected-file-wrapper { + display: flex; + flex-direction: column; + gap: 16px; } } @@ -695,6 +799,10 @@ &:last-child { padding-right: 15px; } + + &:has(.table-item-id) { + width: 20px; + } } .table-item { @@ -727,7 +835,8 @@ .table-item-status { padding: 2px 10px; border-radius: 15px; - background-color: v.$b-color-2; + background-color: #dcfce7; + color: #166534; } .actions { @@ -764,10 +873,15 @@ } } - .table-action-download { + .table-action-download, + .table-action-delete, + .table-action-view { display: flex; - background-color: #3b72e0; align-items: center; + } + + .table-action-download { + background-color: #3b72e0; &:hover { background-color: #2d56a8; @@ -775,14 +889,20 @@ } .table-action-delete { - display: flex; background-color: #e80a14; - align-items: center; &:hover { background-color: #9f0712; } } + + .table-action-view { + background-color: #2b7fff; + + &:hover { + background-color: #155dfc; + } + } } } } @@ -941,6 +1061,10 @@ grid-template-columns: auto; } + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } + .stat-card { background: white; padding: 18px; @@ -1008,6 +1132,10 @@ text-align: center; max-width: calc(100vw - var(--side-bar-width) - 60px); + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } + .referral-link-input { display: flex; gap: 15px; @@ -1032,6 +1160,10 @@ align-items: center; justify-content: center; white-space: nowrap; + + @media (max-width: 490px) { + font-size: 10px; + } } .copy-btn { @@ -1276,6 +1408,16 @@ margin-bottom: 30px; border: none; + + @media (max-width: 440px) { + padding: 0; + box-shadow: none; + } + + .settings-form-wrapper { + width: 100%; + } + .form-group { margin-bottom: 20px; @@ -1284,6 +1426,10 @@ margin-bottom: 8px; font-weight: 600; color: v.$text-s; + + @media (max-width: 440px) { + font-size: 12px; + } } input, @@ -1294,6 +1440,11 @@ border-radius: 10px; font-size: 14px; transition: border-color 0.3s; + + @media (max-width: 440px) { + font-size: 12px; + padding: 10px; + } } } @@ -1302,6 +1453,10 @@ align-items: center; gap: 10px; margin-bottom: 15px; + + @media (max-width: 440px) { + font-size: 14px; + } } .btn-primary { @@ -1322,6 +1477,7 @@ grid-template-columns: repeat(5, 1fr); gap: 20px; margin-bottom: 40px; + max-width: calc(100vw - var(--side-bar-width) - 60px); .stat-card.matches { --card-color-1: #3b82f6; @@ -1420,6 +1576,11 @@ @media (max-width: 860px) { grid-template-columns: repeat(2, 1fr); } + + @media (max-width: 440px) { + grid-template-columns: auto; + max-width: calc(100vw - var(--side-bar-width) - 30px); + } } .dashboard-main-grid { @@ -1453,6 +1614,14 @@ max-width: calc(100vw - var(--side-bar-width) - 60px); } } + + @media (max-width: 440px) { + + .left-column, + .right-column { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } + } } .content-section { @@ -1591,6 +1760,8 @@ grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 40px; + width: 100%; + max-width: calc(100vw - var(--side-bar-width) - 60px); .type-card.images { --card-color-1: #ff6b8a; @@ -1705,6 +1876,11 @@ @media (max-width: 1360px) { grid-template-columns: repeat(2, 1fr); } + + @media (max-width: 440px) { + grid-template-columns: auto; + max-width: calc(100vw - var(--side-bar-width) - 30px); + } } .dashboard-limits-section { @@ -1891,6 +2067,12 @@ display: flex; justify-content: space-between; align-items: center; + max-width: calc(100vw - var(--side-bar-width) - 60px); + + @media (max-width: 440px) { + flex-direction: column; + max-width: calc(100vw - var(--side-bar-width) - 30px); + } .last-check-title { font-size: 14px; @@ -2143,6 +2325,10 @@ margin-bottom: 2rem; max-width: calc(100vw - var(--side-bar-width) - 60px); + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } + &-stat-card { background: white; padding: 1.5rem; @@ -2221,7 +2407,9 @@ } .stat-icon { - font-size: 36px; + .icon { + transform: scale(1.5); + } } .stat-header { @@ -2262,6 +2450,10 @@ box-shadow: 0 10px 40px rgba(99, 102, 241, 0.1); max-width: calc(100vw - var(--side-bar-width) - 60px); + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } + .my-cases-header { display: flex; align-items: center; @@ -2419,6 +2611,11 @@ gap: 24px; max-width: calc(100vw - var(--side-bar-width) - 60px); + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + flex-direction: column; + } + .check-all-left { flex: 1; position: relative; @@ -2474,6 +2671,10 @@ margin-bottom: 24px; max-width: calc(100vw - var(--side-bar-width) - 60px); + @media (max-width: 440px) { + max-width: calc(100vw - var(--side-bar-width) - 30px); + } + .section-header { display: flex; align-items: center; @@ -2675,4 +2876,1222 @@ } } } +} + +.search-grid { + display: grid; + grid-template-columns: 1fr 300px; + gap: 24px; + margin-bottom: 24px; + max-width: calc(100vw - var(--side-bar-width) - 60px); + + @media (max-width: 440px) { + grid-template-columns: auto; + max-width: calc(100vw - var(--side-bar-width) - 30px); + } +} + +.search-info { + background: #f0f9ff; + border: 1px solid #bfdbfe; + border-radius: 12px; + padding: 16px; + margin-bottom: 20px; + + &-title { + font-weight: 600; + color: #1e40af; + margin-bottom: 8px; + } + + &-text { + color: #3730a3; + font-size: 14px; + } +} + +.btn-primary.btn-search { + background: linear-gradient(135deg, #667eea 50%, #764ba2 100%); + border: none +} + +.global-search-section { + background: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%); + padding: 0; + box-shadow: 0 10px 40px rgba(99, 102, 241, 0.1); + border-radius: 20px; + position: relative; + overflow: hidden; + transform: translateY(0); + transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + + .global-search-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + padding: 24px; + color: white; + position: relative; + overflow: hidden; + + .global-search-title { + font-size: 1.4rem; + font-weight: 700; + margin: 0; + display: flex; + align-items: center; + gap: 16px; + position: relative; + z-index: 2; + } + + .global-search-badge { + background: rgba(255, 255, 255, 0.9); + color: #667eea; + padding: 8px 16px; + border-radius: 20px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + animation: pulse-glow 2s ease-in-out infinite; + } + } + + .global-search-content { + padding: 24px; + border: 3px solid transparent; + border-radius: 0 0 20px 20px; + background: + linear-gradient(#fff, #fff) padding-box, + linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%) border-box; + background-clip: padding-box, border-box; + + .global-search-info { + background: linear-gradient(135deg, #f0f4ff 0%, #e0e7ff 100%); + border: 2px solid #c7d2fe; + border-radius: 16px; + padding: 20px; + margin-bottom: 24px; + position: relative; + overflow: hidden; + + .global-search-info-title { + font-weight: 700; + color: #3730a3; + margin-bottom: 12px; + display: flex; + align-items: center; + gap: 10px; + font-size: 16px; + position: relative; + z-index: 2; + } + + .global-search-info-text { + color: #4338ca; + font-size: 14px; + line-height: 1.6; + position: relative; + z-index: 2; + } + } + + .search-counter { + display: flex; + align-items: center; + justify-content: space-between; + background: white; + border: 2px solid #e5e7eb; + border-radius: 12px; + padding: 16px 20px; + margin-bottom: 20px; + transition: all 0.3s ease; + + .counter-info { + display: flex; + align-items: center; + gap: 12px; + } + + .progress-bar { + width: 80px; + height: 8px; + background: #f3f4f6; + border-radius: 4px; + overflow: hidden; + position: relative; + + .progress-fill { + height: 100%; + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + border-radius: 4px; + transition: all 0.5s ease; + position: relative; + overflow: hidden; + } + } + } + + .global-search-btn { + width: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 16px 24px; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + position: relative; + overflow: hidden; + + &:disabled { + opacity: 0.5; + cursor: default; + } + + &:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 12px 35px rgba(102, 126, 234, 0.4); + } + } + + .global-loading { + display: none; + text-align: center; + padding: 40px; + background: #f8fafc; + border-radius: 16px; + margin: 20px 0; + + &.show { + display: block; + } + + .global-spinner { + width: 50px; + height: 50px; + margin: 0 auto 20px; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 4px solid #e5e7eb; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + } + + &::after { + content: ''; + position: absolute; + top: 8px; + left: 8px; + width: calc(100% - 16px); + height: calc(100% - 16px); + border: 2px solid transparent; + border-top: 2px solid #764ba2; + border-radius: 50%; + animation: spin 2s linear infinite reverse; + } + } + + .global-loading-text { + color: #4b5563; + font-weight: 500; + font-size: 16px; + } + } + + .global-result { + display: none; + margin: 20px 0; + animation: slideUp 0.5s ease; + + &.show { + display: block; + } + + .global-result-card { + background: white; + border: 2px solid #e5e7eb; + border-radius: 16px; + overflow: hidden; + transition: all 0.3s ease; + + .global-result-header { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); + color: white; + padding: 16px 20px; + font-weight: 600; + display: flex; + align-items: center; + gap: 12px; + } + + .global-result-content { + padding: 24px; + } + + .global-result-item { + display: flex; + gap: 20px; + align-items: flex-start; + margin-bottom: 30px; + + .global-result-image { + width: 140px; + height: 140px; + border-radius: 12px; + overflow: hidden; + flex-shrink: 0; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + position: relative; + border: 2px solid #e5e7eb; + transition: all 0.3s ease; + + img { + width: 100%; + height: 100%; + object-fit: cover; + cursor: pointer; + transition: all 0.3s ease; + } + } + + .global-result-info { + flex: 1; + } + + .global-result-title { + font-weight: 600; + color: #1e293b; + margin-bottom: 12px; + font-size: 18px; + line-height: 1.4; + } + + .global-result-meta { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 16px; + font-size: 14px; + } + + .global-result-size, + .global-result-host { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 8px; + font-weight: 500; + } + + .global-result-size { + background: #f1f5f9; + color: #64748b; + } + + .global-result-host { + background: #eff6ff; + color: #2563eb; + border: 1px solid #bfdbfe; + } + + .global-result-passage { + color: #6b7280; + font-size: 14px; + line-height: 1.6; + margin-bottom: 20px; + padding: 16px; + background: #f8fafc; + border-radius: 12px; + border-left: 4px solid #667eea; + } + + .global-result-url { + color: #667eea; + text-decoration: none; + font-size: 14px; + font-weight: 500; + word-break: break-all; + display: block; + padding: 12px 16px; + background: #f8fafc; + border-radius: 10px; + transition: all 0.3s ease; + border: 1px solid #e5e7eb; + position: relative; + overflow: hidden; + + &:hover { + background: #eff6ff; + border-color: #667eea; + text-decoration: none; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15); + } + } + } + } + } + + .global-no-result { + text-align: center; + padding: 60px 20px; + background: #f8fafc; + border-radius: 16px; + margin: 20px 0; + } + + .global-error { + display: none; + background: #fef2f2; + border: 2px solid #fecaca; + border-radius: 12px; + padding: 20px; + color: #991b1b; + margin: 20px 0; + } + } +} + +@keyframes slideUp { + 0% { + opacity: 1; + transform: translateY(0); + } + + 100% { + opacity: 0; + transform: translateY(-20px); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +@keyframes pulse-glow { + + 0%, + 100% { + box-shadow: 0 0 5px rgba(255, 255, 255, 0.5); + } + + 50% { + box-shadow: 0 0 20px rgba(255, 255, 255, 0.8); + } +} + +.user-file-search-results { + background: white; + border-radius: 16px; + padding: 24px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + margin-bottom: 20px; + + .results-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + + .results-title { + font-size: 1.3rem; + font-weight: 600; + color: #1e293b; + } + } + + .empty-results { + text-align: center; + padding: 60px 20px; + color: #64748b; + + &-hint { + margin-top: 16px; + padding: 12px; + background: #f0f9ff; + border-radius: 8px; + font-size: 14px; + } + } + + .results-list { + display: flex; + flex-direction: column; + gap: 16px; + + .result-item { + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 20px; + transition: all 0.2s ease; + + &:hover { + border-color: #6366f1; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15); + } + + .result-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 12px; + } + + .result-preview { + width: 60px; + height: 60px; + border-radius: 10px; + overflow: hidden; + position: relative; + flex-shrink: 0; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: opacity 0.3s ease; + } + } + + .preview-fallback { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + background: #f3f4f6; + } + + .result-info { + flex: 1; + } + + .result-filename { + font-weight: 600; + color: #1e293b; + margin-bottom: 4px; + } + + .result-details { + color: #64748b; + font-size: 14px; + } + + .similarity-score { + text-align: center; + min-width: 80px; + } + + .similarity-percentage { + font-size: 1.5rem; + font-weight: 700; + color: #10b981; + margin-bottom: 4px; + } + + .similarity-label { + font-size: 12px; + color: #64748b; + text-transform: uppercase; + } + + .result-badges { + display: flex; + gap: 8px; + margin-top: 12px; + margin-bottom: 12px; + flex-wrap: wrap; + + .badge { + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + } + + .badge-protected { + background: #d1fae5; + color: #065f46; + } + + .badge-not-protected { + background: v.$b-color-2; + } + } + + .result-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 12px; + + .btn-secondary { + background: #6b7280; + color: white; + font-size: 12px; + padding: 6px 12px; + + &:hover { + background: #4b5563; + } + } + + .btn-success { + background: #10b981; + color: white; + font-size: 12px; + padding: 6px 12px; + + &:hover { + background: #059669; + } + } + } + } + } +} + +.modal-window-view-file { + /* position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 90%; + max-width: 1200px; + max-height: 90%; + background: white; + border-radius: 16px; + padding: 0; + overflow: hidden; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); */ + border-radius: 20px; + overflow: hidden; + + &-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + } + + &-title { + margin: 0; + font-size: 18px; + font-weight: 600; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: 16px; + } + + &-content { + padding: 20px; + max-height: calc(90vh - 80px); + overflow-y: auto; + background: v.$white; + + &-wrapper { + display: grid; + grid-template-columns: 1fr 300px; + gap: 20px; + height: 100%; + max-height: 80vh; + } + + .image-section { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; + background: #fafafa; + border-radius: 12px; + position: relative; + } + + .image-container { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + border-radius: 8px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); + } + } + + .file-info-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 0; + overflow: hidden; + + .info-header { + background: #f8fafc; + padding: 16px 20px; + border-bottom: 1px solid #e2e8f0; + + h4 { + margin: 0; + font-size: 16px; + font-weight: 600; + } + } + + .info-item { + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 12px 20px; + border-bottom: 1px solid #f1f5f9; + } + + .info-label { + font-size: 14px; + color: #64748b; + font-weight: 500; + flex-shrink: 0; + } + + .info-value { + font-weight: 600; + color: #1e293b; + text-align: right; + word-break: break-word; + max-width: 150px; + } + + .status-badges { + padding: 12px 20px; + border-bottom: 1px solid #f1f5f9; + } + } + } +} + +.file-info-modal-window { + width: 50vw; + min-width: 400px; + + &-close-button { + position: absolute; + top: 15px; + right: 20px; + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #64748b; + } + + &-section { + margin-bottom: 25px; + } + + &-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + } + + &-item { + background: v.$bg-light; + padding: 15px; + border-radius: 8px; + } + + &-title { + font-weight: 600; + color: v.$text-p; + } + + &-content { + color: v.$text-s; + } +} + +.section-add-file { + position: relative; + + .section-drop-down-list { + position: absolute; + left: 80%; + top: 50%; + max-height: 0; + color: v.$text-p; + font-size: 16px; + border-radius: 20px; + padding: 0; + border: none; + width: fit-content; + z-index: 2; + font-weight: 500; + + background: #ffffff00; + overflow: hidden; + transition: all 0.5s ease; + transform: translateY(-50%); + + &.opened { + max-height: 500px; + padding: 15px 10px; + box-shadow: 0 4px 20px #00000014; + background: v.$white; + } + + a { + white-space: nowrap; + color: v.$p-color; + opacity: 0.7; + } + + a:hover { + transition: all 0.3s ease; + opacity: 1; + } + } +} + +.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; + } +} + +.search-sidebar { + display: flex; + flex-direction: column; + gap: 20px; +} + +.supported-formats, +.stats-search, +.recent-searches { + background: v.$white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 6px v.$shadow-1; + + .formats-title, + .stats-title, + .recent-title { + font-weight: 600; + color: v.$text-p; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; + } + + .format-group { + margin-bottom: 16px; + } + + .format-type { + font-weight: 500; + color: v.$text-p; + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 8px; + + &.image { + svg { + color: v.$color-image; + } + } + + &.audio { + svg { + color: v.$color-audio; + } + } + + &.video { + svg { + color: v.$color-video; + } + } + + &.document { + svg { + color: v.$color-document; + } + } + } + + .format-list { + color: v.$text-s; + font-size: 14px; + line-height: 1.5; + text-transform: uppercase; + } + + .stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid v.$b-color-2; + + &:last-child { + border-bottom: none; + } + + .stat-label { + color: v.$text-s; + font-size: 14px; + opacity: 0.9; + position: relative; + z-index: 1; + } + + .stat-value { + font-weight: 600; + color: v.$text-p; + font-size: 32px; + margin-bottom: 5px; + position: relative; + z-index: 1; + } + } + + .recent-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid v.$b-color-2; + + .recent-type { + color: v.$text-p; + font-size: 14px; + display: flex; + + &.image { + svg { + color: v.$color-image; + } + } + + &.audio { + svg { + color: v.$color-audio; + } + } + + &.video { + svg { + color: v.$color-video; + } + } + + &.document { + svg { + color: v.$color-document; + } + } + } + + .recent-date { + color: v.$text-s; + font-size: 12px; + } + } +} + +.pagination-wrapper { + display: flex; + gap: 5px; + margin-top: 10px; + + .pagination-button { + border: 2px solid #e2e8f0; + padding: 0.25rem 0.75rem; + border-radius: 10px; + color: v.$p-color; + cursor: pointer; + + &.current-page { + background-color: v.$p-color; + color: v.$white; + } + + &:hover { + background-color: v.$bg-light; + } + } +} + +.loading-animation { + display: block; + text-align: center; + + .global-spinner { + display: block; + width: 24px; + height: 24px; + margin: 0 auto; + position: relative; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: 4px solid #e5e7eb; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + } + } +} + +.modal-window-confirm-payment { + background: white; + border-radius: 20px; + max-width: 480px; + margin: 20px; + text-align: center; + position: relative; + + &-title { + color: #1e293b; + margin-bottom: 12px; + font-size: 24px; + } + + &-tariff { + color: #1e293b; + margin-bottom: 8px; + font-size: 20px; + } + + &-description { + color: #64748b; + margin-bottom: 12px; + font-size: 16px; + } + + &-price { + background: #f8fafc; + padding: 12px; + border-radius: 10px; + margin-bottom: 30px; + + div { + &:first-child { + font-size: 26px; + font-weight: 700; + color: #6366f1; + } + + &:last-child { + color: #64748b; + font-size: 16px; + } + } + } + + .btn-primary { + width: 100%; + padding: 12px 18px; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + + &.btn-confirm { + background: linear-gradient(135deg, #6366f1, #8b5cf6); + color: white; + } + + &.btn-cancel { + background: transparent; + border: 2px solid #6366f1; + color: #6366f1; + } + + &.btn-confirm:disabled, + .btn-cancel:disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; + } + + .btn-loading { + position: relative; + /* Можно добавить анимацию загрузки */ + } + + .btn-loading::after { + content: ''; + position: absolute; + width: 16px; + height: 16px; + top: 50%; + right: 10px; + transform: translateY(-50%); + border: 2px solid transparent; + border-top-color: #ffffff; + border-radius: 50%; + animation: button-loading-spinner 1s ease infinite; + } + + @keyframes button-loading-spinner { + from { + transform: translateY(-50%) rotate(0turn); + } + + to { + transform: translateY(-50%) rotate(1turn); + } + } + } } \ No newline at end of file diff --git a/src/app/styles/payment-container.scss b/src/app/styles/payment-container.scss index ef88cb8..5d92c98 100644 --- a/src/app/styles/payment-container.scss +++ b/src/app/styles/payment-container.scss @@ -34,13 +34,15 @@ padding: 16px; text-align: center; border: 1px solid #e2e8f0; + height: 100%; } .stat-value { - font-size: 1.6rem; + font-size: 1.2rem; font-weight: 700; color: #1e293b; margin-bottom: 4px; + white-space: nowrap; } .stat-label { @@ -214,6 +216,7 @@ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer; overflow: visible; + max-width: 350px; .plan-header { text-align: center; diff --git a/src/app/styles/settings.scss b/src/app/styles/settings.scss index 4d655ed..e4b4086 100644 --- a/src/app/styles/settings.scss +++ b/src/app/styles/settings.scss @@ -7,7 +7,6 @@ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); border: 1px solid #e2e8f0; transition: all 0.3s ease; - overflow: hidden; } .form-section { diff --git a/src/app/styles/variable.scss b/src/app/styles/variable.scss index 24d51fb..535eb21 100644 --- a/src/app/styles/variable.scss +++ b/src/app/styles/variable.scss @@ -12,6 +12,8 @@ $bg-darkening: #0000007a; $shadow-1: #0000001a; $white: #fff; $red: #dc2626; +$p-color-disabled: #6365f18e; +$s-color-disabled: #8a5cf663; $color-image: #f08c00; $color-video: #2f9e44; diff --git a/src/app/ui/dashboard/files-table.tsx b/src/app/ui/dashboard/files-table.tsx index c1c82f6..a1185cf 100644 --- a/src/app/ui/dashboard/files-table.tsx +++ b/src/app/ui/dashboard/files-table.tsx @@ -1,4 +1,4 @@ -import TanstakFilesTable from '@/app/components/tanstakTable'; +import TanstakFilesTable from '@/app/components/TanstakTable'; type FilesTable = { fileType: string diff --git a/src/app/ui/dashboard/new/dashboard-files-info.tsx b/src/app/ui/dashboard/new/dashboard-files-info.tsx index cdee67a..7a7e2ef 100644 --- a/src/app/ui/dashboard/new/dashboard-files-info.tsx +++ b/src/app/ui/dashboard/new/dashboard-files-info.tsx @@ -1,22 +1,12 @@ 'use client' import { useTranslations } from 'next-intl'; -import { useQuery } from '@tanstack/react-query'; -import { getUserFilesInfo } from '@/app/actions/action'; import {convertBytes} from '@/app/lib/convertBytes'; +import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo'; export default function DashboardFilesInfo() { const t = useTranslations("Global"); - - const { - data: filesInfo, - isLoading, - isError, - error, - } = useQuery({ - queryKey: ['userFilesInfo'], - queryFn: getUserFilesInfo, - }); + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); return (
    @@ -24,20 +14,20 @@ export default function DashboardFilesInfo() {
    {t('images')}
    -
    {filesInfo?.images_quantity ? filesInfo?.images_quantity : 0}
    +
    {filesInfo?.images.quantity ? filesInfo?.images.quantity : 0}
    {t('your-image')}
    {t('size')} - {filesInfo?.images_size ? convertBytes(filesInfo?.images_size) : 0} + {filesInfo?.images.size ? convertBytes(filesInfo?.images.size) : 0}
    - {t('checks')} - {filesInfo?.images_check ? filesInfo?.images_check : 0} + {t('PROTECTED')} + {filesInfo?.images.protected ? filesInfo?.images.protected : 0}
    {t('violations')} - {filesInfo?.images_violations ? filesInfo?.images_violations : 0} + {filesInfo?.images.violation ? filesInfo?.images.violation : 0}
    @@ -46,20 +36,20 @@ export default function DashboardFilesInfo() {
    {t('videos')}
    -
    {filesInfo?.videos_quantity ? filesInfo?.videos_quantity : 0}
    +
    {filesInfo?.videos.quantity ? filesInfo?.videos.quantity : 0}
    {t('your-videos')}
    {t('size')} - {filesInfo?.videos_size ? convertBytes(filesInfo?.videos_size) : 0} + {filesInfo?.videos.size ? convertBytes(filesInfo?.videos.size) : 0}
    - {t('checks')} - {filesInfo?.videos_check ? filesInfo?.videos_check : 0} + {t('PROTECTED')} + {filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}
    {t('violations')} - {filesInfo?.videos_violations ? filesInfo?.videos_violations : 0} + {filesInfo?.videos.violation ? filesInfo?.videos.violation : 0}
    @@ -68,20 +58,20 @@ export default function DashboardFilesInfo() {
    {t('audios')}
    -
    {filesInfo?.audios_quantity ? filesInfo?.audios_quantity : 0}
    +
    {filesInfo?.audios.quantity ? filesInfo?.audios.quantity : 0}
    {t('your-audios')}
    {t('size')} - {filesInfo?.audios_size ? convertBytes(filesInfo?.audios_size) : 0} + {filesInfo?.audios.size ? convertBytes(filesInfo?.audios.size) : 0}
    - {t('checks')} - {filesInfo?.audios_check ? filesInfo?.audios_check : 0} + {t('PROTECTED')} + {filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}
    {t('violations')} - {filesInfo?.audios_violations ? filesInfo?.audios_violations : 0} + {filesInfo?.audios.violation ? filesInfo?.audios.violation : 0}
    @@ -90,20 +80,20 @@ export default function DashboardFilesInfo() {
    {t('documents')}
    -
    {filesInfo?.documents_quantity ? filesInfo?.documents_quantity : 0}
    +
    {filesInfo?.documents.quantity ? filesInfo?.documents.quantity : 0}
    {t('your-documents')}
    {t('size')} - {filesInfo?.documents_check ? convertBytes(filesInfo?.documents_check) : 0} + {filesInfo?.documents.check? convertBytes(filesInfo?.documents.check) : 0}
    - {t('checks')} - {filesInfo?.documents_check ? filesInfo?.documents_check : 0} + {t('PROTECTED')} + {filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}
    {t('violations')} - {filesInfo?.documents_violations ? filesInfo?.documents_violations : 0} + {filesInfo?.documents.violation ? filesInfo?.documents.violation : 0}
    diff --git a/src/app/ui/dashboard/new/dashboard-last-check.tsx b/src/app/ui/dashboard/new/dashboard-last-check.tsx index a000925..c8de769 100644 --- a/src/app/ui/dashboard/new/dashboard-last-check.tsx +++ b/src/app/ui/dashboard/new/dashboard-last-check.tsx @@ -1,3 +1,5 @@ +import Link from 'next/link'; + export default function DashboardLastCheck() { return (
    @@ -7,12 +9,18 @@ export default function DashboardLastCheck() { 17.10.2025 в 10:23
    - - +
    ) diff --git a/src/app/ui/dashboard/new/dashboard-user-files.tsx b/src/app/ui/dashboard/new/dashboard-user-files.tsx index ce3ee2f..09ff25b 100644 --- a/src/app/ui/dashboard/new/dashboard-user-files.tsx +++ b/src/app/ui/dashboard/new/dashboard-user-files.tsx @@ -1,48 +1,150 @@ +'use client' + +import { useQuery } from '@tanstack/react-query'; +import { getUserFilesData } from '@/app/actions/fileEntity'; +import { useEffect, useState, useRef } from 'react'; +import { FileTypeIcon } from '@/app/components/FileTypeIcon'; +import { convertBytes } from '@/app/lib/convertBytes'; +import { useTranslations } from 'next-intl'; +import { useClickOutside } from '@/app/hooks/useClickOutside'; +import Link from 'next/link'; + +type FileItem = { + id: string; + fileName: string; + fileType: string; + size?: number | undefined; + uploadDate?: number; + protectStatus: string; + supportId: number; +}; + +type ApiFile = { + id: string; + originalFileName: string; + mimeType: string; + fileSize: number; + updatedAt: string; + protectStatus: string; + supportId: number; +}; + +type ApiResponse = { + files?: ApiFile[]; +}; + export default function DashboardUserFiles() { + const { + data: tableData, + isLoading, + isError, + error, + } = useQuery({ + 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 (

    Ваши файлы

    - Загрузить файл → +
    { + setOpenDropDownList(!openDropDownList); + }} + ref={dropDownList} + > + {t('add')} +
    +
    + + {t('photo-marking')} + + + {t('video-marking')} + + + {t('audio-marking')} + +
    +
    +
    -
    -
    - 🎬
    -
    -
    - Запись экрана 2026-01-09 в 15.45.28.mov
    -
    - 11.3 МБ • - 11.01.2026
    + + {tableData?.length ? ( + tableData?.map((file) => { + return ( +
    +
    + +
    +
    +
    + {file.fileName} +
    +
    + {convertBytes(file.size ? file.size : 0)} • {formatDate(file.uploadDate ? file.uploadDate : 0)} +
    +
    +
    + {file.protectStatus ? t(file.protectStatus) : t('error')} +
    +
    + ) + }) + ) : ( +
    + {t('there-are-no-files-yet')}
    -
    ✓ Защищено
    -
    -
    -
    - 🎬
    -
    -
    - SOUNE_миграция.mov
    -
    - 17.1 МБ • - 11.01.2026
    -
    -
    ✓ Защищено
    -
    -
    -
    - 🎬
    -
    -
    - SOUNE_миграция.mov
    -
    - 17.1 МБ • - 11.01.2026
    -
    -
    ✓ Защищено
    -
    + )}
    ) diff --git a/src/app/ui/dashboard/new/dashboard-user-stats.tsx b/src/app/ui/dashboard/new/dashboard-user-stats.tsx index f02a15f..e6640aa 100644 --- a/src/app/ui/dashboard/new/dashboard-user-stats.tsx +++ b/src/app/ui/dashboard/new/dashboard-user-stats.tsx @@ -1,29 +1,19 @@ 'use client' import { useTranslations } from 'next-intl'; -import { useQuery } from '@tanstack/react-query'; -import { getUserFilesInfo } from '@/app/actions/action'; import {convertBytes} from '@/app/lib/convertBytes'; +import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo'; export default function DashboardUserStats() { const t = useTranslations("Global"); - - const { - data: filesInfo, - isLoading, - isError, - error, - } = useQuery({ - queryKey: ['userFilesInfo'], - queryFn: getUserFilesInfo, - }); + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); return (
    ЗАЩИЩЁННЫЕ ФАЙЛЫ
    -
    0
    +
    {filesInfo?.total.protected ? filesInfo?.total.protected : 0}
    Файлов под защитой
    🛡️
    @@ -37,7 +27,7 @@ export default function DashboardUserStats() {
    НАРУШЕНИЯ
    -
    {filesInfo?.all_files_violations ? filesInfo?.all_files_violations : 0}
    +
    {filesInfo?.total.violation ? filesInfo?.total.violation : 0}
    Требуют внимания
    ⚠️
    @@ -45,7 +35,7 @@ export default function DashboardUserStats() {
    ХРАНИЛИЩЕ
    - {filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0} + {filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0}
    Использовано места
    💾
    diff --git a/src/app/ui/dashboard/new/dashborad-limits-section.tsx b/src/app/ui/dashboard/new/dashborad-limits-section.tsx index 08465d8..f20de55 100644 --- a/src/app/ui/dashboard/new/dashborad-limits-section.tsx +++ b/src/app/ui/dashboard/new/dashborad-limits-section.tsx @@ -1,4 +1,12 @@ +'use client' + +import { convertBytes } from '@/app/lib/convertBytes'; +import { getProcents } from '@/app/lib/getProcents'; +import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo'; + export default function DahboardLimitsSection() { + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); + return (

    💰 Использование лимитов

    @@ -9,13 +17,13 @@ export default function DahboardLimitsSection() { 🔍 Проверки
    -
    520 / 10,000
    +
    {filesInfo?.total.check ? filesInfo?.total.check : 0} / 10,000
    -
    +
    - 5% + {getProcents(filesInfo?.total.check, 10000)}%
    @@ -25,13 +33,13 @@ export default function DahboardLimitsSection() { 💾 Хранилище
    -
    790.2 / 1,024 МБ
    +
    {filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} / 1,024 МБ
    -
    +
    - 77% + {getProcents(filesInfo?.total.size, 1000000000)}%
    @@ -40,13 +48,13 @@ export default function DahboardLimitsSection() { 📁 Файлы
    -
    386 / 500
    +
    {filesInfo?.total.quantity ? filesInfo?.total.quantity : 0} / 500
    -
    +
    - 77% + {getProcents(filesInfo?.total.quantity, 500)}%
    diff --git a/src/app/ui/dashboard/new/violations-carousel.tsx b/src/app/ui/dashboard/new/violations-carousel.tsx index 67a9860..c44bf7c 100644 --- a/src/app/ui/dashboard/new/violations-carousel.tsx +++ b/src/app/ui/dashboard/new/violations-carousel.tsx @@ -7,24 +7,33 @@ import 'swiper/css'; import 'swiper/css/pagination'; import testImage from '@/app/src/image-preview.png' import { useRef } from 'react'; +import { useTranslations } from 'next-intl'; +import Link from 'next/link'; export default function ViolationsCarousel() { const paginationRef = useRef(null); + const t = useTranslations('Global'); return (

    Нарушения

    - Смотреть все → + + Смотреть все +
    -
    +
    + {t('are-no-violations')} +
    + {/*
    @@ -260,7 +268,7 @@ export default function ViolationsCarousel() { >
    -
    +
    */} ) } \ No newline at end of file diff --git a/src/app/ui/dashboard/protection-overview.tsx b/src/app/ui/dashboard/protection-overview.tsx index 93b0d47..9477623 100644 --- a/src/app/ui/dashboard/protection-overview.tsx +++ b/src/app/ui/dashboard/protection-overview.tsx @@ -5,23 +5,9 @@ import { PieChartComponent } from '@/app/components/PieChartComponent'; import { useTranslations, useLocale } from 'next-intl'; import Link from 'next/link'; import { useClickOutside } from '@/app/hooks/useClickOutside'; -import { useQuery } from '@tanstack/react-query'; -import { getUserFilesInfo } from '@/app/actions/action'; import { convertBytes } from '@/app/lib/convertBytes'; import { pluralize } from '@/app/lib/pluralize'; - -type FilesInfo = { - totalSize: number; - totalCount: number; - imageQuantity: number; - videoQuantity: number; - audioQuantity: number; - imageSize: number; - videoSize: number; - audioSize: number; - documentSize: number; -}; - +import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo'; export default function ProtectionOverview() { const [isOpen, setIsOpen] = useState(false); @@ -42,68 +28,23 @@ export default function ProtectionOverview() { openDropDownList ); - const { - data: filesInfo, - isLoading, - isError, - error, - } = useQuery<{ - all_files_size: number, - all_files_quantity: number, - images_quantity: number, - videos_quantity: number, - audios_quantity: number, - images_size: number, - videos_size: number, - audios_size: number, - documents_size: number, - }, Error, FilesInfo>({ - queryKey: ['userFilesInfo'], - queryFn: getUserFilesInfo, - select: (data): FilesInfo => { - if (!data) { - return { - totalSize: 0, - totalCount: 0, - imageQuantity: 0, - videoQuantity: 0, - audioQuantity: 0, - imageSize: 0, - videoSize: 0, - audioSize: 0, - documentSize: 0, - } - } - - return { - totalSize: data.all_files_size, - totalCount: data.all_files_quantity, - imageQuantity: data.images_quantity, - videoQuantity: data.videos_quantity, - audioQuantity: data.audios_quantity, - imageSize: data.images_size, - videoSize: data.videos_size, - audioSize: data.audios_size, - documentSize: data.documents_size, - } - }, - }); + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); useEffect(() => { setFilesCount([ { name: 'images-few', - value: filesInfo?.imageSize ? filesInfo?.imageSize : 0, + value: filesInfo?.images.size ? filesInfo?.images.size : 0, color: '#f08c00' }, { name: 'videos', - value: filesInfo?.videoSize ? filesInfo?.videoSize : 0, + value: filesInfo?.videos.size ? filesInfo?.videos.size : 0, color: '#2f9e44' }, { name: 'audios', - value: filesInfo?.audioSize ? filesInfo?.audioSize : 0, + value: filesInfo?.audios.size ? filesInfo?.audios.size : 0, color: '#1971c2' }, ]) @@ -134,14 +75,14 @@ export default function ProtectionOverview() {
    - {filesInfo?.totalCount ? ( + {filesInfo?.total.quantity ? ( <>
    - {filesInfo?.totalCount ? filesInfo?.totalCount : 0} + {filesInfo?.total.quantity ? filesInfo?.total.quantity : 0} - {pluralizeFiles(filesInfo?.totalCount || 0)} + {pluralizeFiles(filesInfo?.total.quantity || 0)}
    @@ -180,7 +121,7 @@ export default function ProtectionOverview() {
    - + {t('photo-marking')} @@ -196,18 +137,18 @@ export default function ProtectionOverview() { )} {isOpen ? (
    - +
    ) : (
    -
    {filesInfo?.totalSize ? convertBytes(filesInfo?.totalSize) : 0} / 0
    +
    {filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} / 0
    {t('disk-space-used')}
    )}
    - {!!filesInfo?.totalCount && ( + {!!filesInfo?.total.quantity && ( ) diff --git a/src/app/ui/forms/login-form.tsx b/src/app/ui/forms/login-form.tsx index 8f25c76..d97c2fa 100644 --- a/src/app/ui/forms/login-form.tsx +++ b/src/app/ui/forms/login-form.tsx @@ -96,8 +96,18 @@ export default function LoginForm() { {t('forgot-password')}?
    - ) diff --git a/src/app/ui/forms/register-form.tsx b/src/app/ui/forms/register-form.tsx index dd1c0f9..6c65b6b 100644 --- a/src/app/ui/forms/register-form.tsx +++ b/src/app/ui/forms/register-form.tsx @@ -1,17 +1,26 @@ 'use client' import styles from '@/app/styles/module/login.module.scss'; -import { ChangeEvent, useActionState, useEffect, useRef, useState, MouseEvent } from 'react'; +import { ChangeEvent, useActionState, useEffect, useRef, useState } from 'react'; import { registration, FormState } from '@/app/actions/auth'; + + import PhoneInput from '@/app/ui/inputs/phone-input'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { IconEye } from '@/app/ui/icons/icons'; -import { SignupFormSchema } from '@/app/actions/definitions'; +import { getSignupFormSchema } from '@/app/actions/definitions'; +import { useSearchParams } from 'next/navigation'; import * as z from 'zod' import ConfirmMailModalWindow from '@/app/ui/forms/confirm-mail-modal-window'; +import { fetchINN } from '@/app/actions/action'; +import { useDebouncedCallback } from 'use-debounce'; + export default function RegisterForm() { + const searchParams = useSearchParams(); + const referralCode = searchParams.get('ref'); + const [state, formAction, isPending] = useActionState( registration, undefined, @@ -23,6 +32,7 @@ export default function RegisterForm() { const [formState, setFormState] = useState(undefined); const [showMailConfirmWindow, setShowMailConfirmWindow] = useState(false); const passwordRef = useRef(null); + const [accountType, setAccountType] = useState<'b2c' | 'b2b'>('b2c'); useEffect(() => { setFormState(state); @@ -30,7 +40,40 @@ export default function RegisterForm() { setShowMailConfirmWindow(true); } - }, [state]) + }, [state]); + + const debouncedFetchINN = useDebouncedCallback( + async (value) => { + const response = await fetchINN(value); + + if (response.companyName) { + setFormState({ + ...formState, + previousState: { + ...formState?.previousState, + companyName: response.companyName + }, + error: { + ...formState?.error, + companyName: '' + } + }); + } else { + setFormState({ + ...formState, + previousState: { + companyName: response.companyName + }, + error: { + ...formState?.error, + companyName: '', + inn: 'inn-invalid' + } + }); + } + }, + 500 + ); function showPassowrd(target: 'password' | 'confirm-password') { if (target === 'password') { @@ -40,13 +83,38 @@ export default function RegisterForm() { } } - function validateField( + type SignupFormSchema = Awaited>; + const [signupFormSchema, setSignupFormSchema] = useState(null);; + + useEffect(() => { + let isMounted = true; + + const loadSchema = async () => { + const result = await getSignupFormSchema(accountType); + if (isMounted) { + setSignupFormSchema(result); + } + }; + + loadSchema(); + + return () => { + isMounted = false; + }; + }, [accountType]); + + async function validateField( fieldName: string, value: string, ) { switch (fieldName) { case 'confirm_password': - const baseSchema = SignupFormSchema.shape.confirm_password; + const baseSchema = signupFormSchema?.shape.confirm_password; + + if (!baseSchema) { + return { success: true, data: value }; + } + const baseResult = baseSchema.safeParse(value); if (!baseResult.success) { @@ -72,13 +140,25 @@ export default function RegisterForm() { case 'email': case 'fullName': case 'phone': + case 'referralCode': case 'password': case 'companyName': case 'agree': - const schema = SignupFormSchema.shape[fieldName]; + case 'inn': + const schema = signupFormSchema?.shape[fieldName]; + if (!schema) { + return { success: true, data: value }; + } + if (fieldName === 'phone') { return schema.safeParse(value.replace(/[-\(\)\s]/g, '')); } + if (fieldName === 'companyName' && accountType === 'b2c') { + return { success: true, data: value }; + } + if (fieldName === 'referralCode') { + return { success: true, data: value }; + } return schema.safeParse(value); default: @@ -86,12 +166,45 @@ export default function RegisterForm() { } } - function onChangeHandler(e: ChangeEvent): void { + async function onChangeHandler(e: ChangeEvent): Promise { const fieldName = e.target.name; if (fieldName !== 'phone') { e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, ''); } - const validatedField = validateField(fieldName, e.target.value); + + if (fieldName === 'inn') { + const currentValue = e.target.value; + + const cleanedValue = currentValue.replace(/([^0-9])/g, ''); + + if (cleanedValue.length > 10) { + e.target.value = cleanedValue.slice(0, 10); + debouncedFetchINN(e.target.value); + } else { + e.target.value = cleanedValue; + } + + if (cleanedValue.length === 10) { + debouncedFetchINN(cleanedValue); + } else { + if (formState) { + setFormState({ + ...formState, + previousState: { + ...formState?.previousState, + companyName: '' + }, + error: { + ...formState?.error, + companyName: '', + inn: '' + } + }); + } + } + } + + const validatedField = await validateField(fieldName, e.target.value); if (validatedField.success) { setFormState(prevState => { @@ -109,6 +222,26 @@ export default function RegisterForm() { } } + function switchRegistrationTypeHandler(e: React.MouseEvent, type: 'b2c' | 'b2b') { + e.preventDefault(); + setAccountType(type); + if (formState?.previousState) { + setFormState({ + ...formState, + previousState: { + ...formState.previousState, + companyName: '', + inn: '' + }, + error: { + ...formState.error, + companyName: '', + inn: '' + } + }); + } + } + return ( <> + + +
    +
    @@ -150,10 +141,10 @@ export default function MyContentInfoBlock() {
    - {filesInfo?.documents_quantity || 0} {pluralizeFiles(filesInfo?.documents_quantity || 0)} + {filesInfo?.documents.quantity || 0} {pluralizeFiles(filesInfo?.documents.quantity || 0)} - {filesInfo?.documents_size ? convertBytes(filesInfo?.documents_size) : 0} + {filesInfo?.documents.size ? convertBytes(filesInfo?.documents.size) : 0}
    diff --git a/src/app/ui/my-content/my-content-pie-chart.tsx b/src/app/ui/my-content/my-content-pie-chart.tsx index d6ae2d7..caf060e 100644 --- a/src/app/ui/my-content/my-content-pie-chart.tsx +++ b/src/app/ui/my-content/my-content-pie-chart.tsx @@ -1,10 +1,9 @@ 'use client' import { PieChartComponent } from '@/app/components/PieChartComponent'; -import { useQuery } from '@tanstack/react-query'; -import { getUserFilesInfo } from '@/app/actions/action'; import { useState, useEffect } from 'react'; import { convertBytes } from '@/app/lib/convertBytes'; +import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo'; type FilesInfo = { @@ -28,76 +27,27 @@ export default function MyContentPieChart() { { name: 'documents', value: 0, color: '#a561e6' }, ]); - const { - data: filesInfo, - isLoading, - isError, - error, - } = useQuery<{ - all_files_size: number, - all_files_quantity: number, - images_quantity: number, - videos_quantity: number, - audios_quantity: number, - documents_quantity: number, - images_size: number, - videos_size: number, - audios_size: number, - documents_size: number, - }, Error, FilesInfo>({ - queryKey: ['userFilesInfo'], - queryFn: getUserFilesInfo, - select: (data): FilesInfo => { - if (!data) { - return { - totalSize: 0, - totalCount: 0, - imageQuantity: 0, - videoQuantity: 0, - audioQuantity: 0, - documentQuantity: 0, - imageSize: 0, - videoSize: 0, - audioSize: 0, - documentSize: 0, - } - } - - return { - totalSize: data.all_files_size, - totalCount: data.all_files_quantity, - imageQuantity: data.images_quantity, - videoQuantity: data.videos_quantity, - audioQuantity: data.audios_quantity, - documentQuantity: data.documents_quantity, - imageSize: data.images_size, - videoSize: data.videos_size, - audioSize: data.audios_size, - documentSize: data.documents_size, - } - }, - }); - + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); useEffect(() => { setFilesCount([ { name: 'images-few', - value: filesInfo?.imageSize ? filesInfo?.imageSize : 0, + value: filesInfo?.images.size ? filesInfo?.images.size : 0, color: '#f08c00' }, { name: 'videos', - value: filesInfo?.videoSize ? filesInfo?.videoSize : 0, + value: filesInfo?.videos.size ? filesInfo?.videos.size : 0, color: '#2f9e44' }, { name: 'audios', - value: filesInfo?.audioSize ? filesInfo?.audioSize : 0, + value: filesInfo?.audios.size ? filesInfo?.audios.size : 0, color: '#1971c2' }, { name: 'documents', - value: filesInfo?.documentSize ? filesInfo?.documentSize : 0, + value: filesInfo?.documents.size ? filesInfo?.documents.size : 0, color: '#a561e6' }, ]) @@ -105,7 +55,7 @@ export default function MyContentPieChart() { return (
    - +
    ) } \ No newline at end of file diff --git a/src/app/ui/my-content/new/my-content-page-title.tsx b/src/app/ui/my-content/new/my-content-page-title.tsx index afb6691..7f45584 100644 --- a/src/app/ui/my-content/new/my-content-page-title.tsx +++ b/src/app/ui/my-content/new/my-content-page-title.tsx @@ -1,21 +1,14 @@ +//removed 'use client' import { useTranslations } from 'next-intl'; -import { useQuery } from '@tanstack/react-query'; -import { getUserFilesInfo } from '@/app/actions/action'; import { convertBytes } from '@/app/lib/convertBytes'; +import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo'; export default function MyContentPageTitleColorFrame({ title, description }: { title: string, description: string }) { const t = useTranslations('Global'); - const { - data: filesInfo, - isLoading, - isError, - error, - } = useQuery({ - queryKey: ['userFilesInfo'], - queryFn: getUserFilesInfo, - }); + + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); return (
    @@ -30,7 +23,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
    -
    {filesInfo?.all_files_quantity}
    +
    {filesInfo?.total.quantity}
    Файлов
    @@ -40,7 +33,7 @@ export default function MyContentPageTitleColorFrame({ title, description }: { t
    -
    {filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}
    +
    {convertBytes(filesInfo?.total.size ?? 0)}
    Хранилище
    diff --git a/src/app/ui/my-content/new/my-content-stats-overview.tsx b/src/app/ui/my-content/new/my-content-stats-overview.tsx index 277567a..deffedf 100644 --- a/src/app/ui/my-content/new/my-content-stats-overview.tsx +++ b/src/app/ui/my-content/new/my-content-stats-overview.tsx @@ -1,57 +1,57 @@ 'use client' import { useTranslations } from 'next-intl'; -import { useQuery } from '@tanstack/react-query'; -import { getUserFilesInfo } from '@/app/actions/action'; -import {convertBytes} from '@/app/lib/convertBytes'; +import { convertBytes } from '@/app/lib/convertBytes'; +import { IconDocument, IconShield, IconSearch, IconDiscet } from '@/app/ui/icons/icons'; +import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo'; export default function MyContentStatsOverview() { const t = useTranslations('Global'); - const { - data: filesInfo, - isLoading, - isError, - error, - } = useQuery({ - queryKey: ['userFilesInfo'], - queryFn: getUserFilesInfo, - }); + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); return (
    -
    +
    -
    📄
    -
    +0%
    +
    + +
    + {/*
    +0%
    */}
    -
    {filesInfo?.all_files_quantity}
    +
    {filesInfo?.total.quantity}
    Всего файлов
    -
    +
    -
    🛡
    -
    +0%
    +
    + +
    + {/*
    +0%
    */}
    -
    0
    +
    {filesInfo?.total.protected}
    Защищено файлов
    -
    +
    -
    🔍
    -
    +0%
    +
    + +
    + {/*
    +0%
    */}
    0
    Проверок защиты
    -
    +
    -
    💾
    -
    +0%
    +
    + +
    + {/*
    +0%
    */}
    -
    {filesInfo?.all_files_size ? convertBytes(filesInfo?.all_files_size) : 0}
    +
    {convertBytes(filesInfo?.total.size ?? 0)}
    Хранилище
    diff --git a/src/app/ui/navigation/nav-link-dropdown.tsx b/src/app/ui/navigation/nav-link-dropdown.tsx index 817db8a..7c899ad 100644 --- a/src/app/ui/navigation/nav-link-dropdown.tsx +++ b/src/app/ui/navigation/nav-link-dropdown.tsx @@ -5,7 +5,9 @@ import { usePathname } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { useSideMenuStore } from '@/app/stores/sideMenuStore'; -export default function DropDownList() { +export default function DropDownList( + { referralLink }: { referralLink: string | null } +) { const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/'); const [dropDownExpanded, setDropDownExpanded] = useState(false); const t = useTranslations('Global'); @@ -17,16 +19,20 @@ export default function DropDownList() { const links = [ { - name: t('photo-marking'), - href: '/pages/marking-photo' + name: 'photo-marking', + href: '/pages/marking-images' }, { - name: t('video-marking'), + name: 'video-marking', href: '/pages/marking-video' }, { - name: t('audio-marking'), + name: 'audio-marking', href: '/pages/marking-audio' + }, + { + name: 'document-marking', + href: '/pages/marking-document' } ]; @@ -50,6 +56,9 @@ export default function DropDownList() {
      {links.map((link) => { + if (referralLink && link.name === 'document-marking') { + return null; + } return ( -

      {link.name}

      +

      {t(link.name)}

      ); })} diff --git a/src/app/ui/navigation/nav-links.tsx b/src/app/ui/navigation/nav-links.tsx index fc466b2..1fd1be0 100644 --- a/src/app/ui/navigation/nav-links.tsx +++ b/src/app/ui/navigation/nav-links.tsx @@ -11,8 +11,70 @@ import navigation from './navigation.json'; import { useSideMenuStore } from '@/app/stores/sideMenuStore'; import { useRef } from 'react'; import { useClickOutside } from '@/app/hooks/useClickOutside'; +import { fetchReferralUserStats } from '@/app/actions/referralsActions'; +import { useQuery } from '@tanstack/react-query'; +import { env } from 'process'; +import { getBuildData } from '@/app/actions/action'; export default function NavLinks() { + const { + data: referralLink, + isLoading, + isError, + error + } = useQuery({ + queryKey: ['referralUserStats'], + queryFn: () => { + return fetchReferralUserStats(); + }, + select: (data) => { + if (data?.referralLink) { + return data?.referralLink; + } else { + return null; + } + } + }); + + const { + data: frontendBuildDate, + } = useQuery({ + queryKey: ['frontendBuildDate'], + queryFn: async () => { + try { + const response = await fetch('/build-info.json'); + if (!response.ok) { + return null; + } + return response.json(); + } catch (error) { + return null; + } + }, + select: (data) => { + if (data?.buildTime) { + return data.buildTime; + } + return null; + }, + retry: false + }); + + const { + data: backendBuildDate, + } = useQuery({ + queryKey: ['backendBuildDate'], + queryFn: async () => { + try { + const response = await getBuildData(); + return response.buildTimeBack ?? null; + } catch (error) { + return null; + } + }, + retry: false + }); + const pathname = usePathname().replace('/en/', '/').replace('/ru/', '/'); const t = useTranslations('Global'); @@ -66,9 +128,13 @@ export default function NavLinks() {

      {t('home')}

      - + {links.map((link) => { + if (!referralLink && link.name === 'referral-program') { + return null; + } + return ( {t('exit')}

    + +
    + {frontendBuildDate && ( +
    + front: {frontendBuildDate} +
    + )} + {backendBuildDate && ( +
    + back: {backendBuildDate} +
    + )} +
    ); } diff --git a/src/app/ui/navigation/navigation.json b/src/app/ui/navigation/navigation.json index da6ad54..068034c 100644 --- a/src/app/ui/navigation/navigation.json +++ b/src/app/ui/navigation/navigation.json @@ -20,13 +20,13 @@ "img": "M16 4c0-1.11.89-2 2-2s2 .89 2 2-.89 2-2 2-2-.89-2-2zm4 18v-6h2.5l-2.54-7.63A1.5 1.5 0 0 0 18.54 8H16c-.8 0-1.54.37-2 1l-3 4v2h2l2.54-3.82L16.5 18H20z" }, { - "name": "violations", + "name": "violations-few", "href": "/pages/violations", "img": "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }, { "name": "search", - "href": "/pages/emptypage", + "href": "/pages/search", "img": "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" }, { diff --git a/src/app/ui/payment/payment-tab-tariffs.tsx b/src/app/ui/payment/payment-tab-tariffs.tsx index 2dfd7dc..01335b2 100644 --- a/src/app/ui/payment/payment-tab-tariffs.tsx +++ b/src/app/ui/payment/payment-tab-tariffs.tsx @@ -1,122 +1,184 @@ +'use client' + +import { fetchTariffs } from '@/app/actions/action'; +import { useQuery } from '@tanstack/react-query'; +import { convertBytes } from '@/app/lib/convertBytes'; +import { useTranslations } from 'next-intl'; +import { yooKasaCreatePayment } from '@/app/actions/yooKassa'; +import { useState, ReactNode } from 'react'; +import ModalWindow from '@/app/components/ModalWindow'; + export default function PaymentTabTariffs() { - return ( -
    -
    -
    - - +
    -
    -
    -
    -
    🚀
    -

    СТАРТ

    -
    900₽
    -
    в месяц
    -
    100 токенов включено
    + ); + } + + return ( + <> + + + + +
    +
    +
    + +
    -
      -
    • - - 100 файлов в месяц -
    • -
    • - - 1 ГБ хранилища -
    • -
    • - - До 10 МБ на файл -
    • -
    • - - Базовые модули защиты -
    • -
    • - - Поддержка 24/7 -
    • -
    • - - API доступ -
    • -
    -
    -
    -
    🚀
    -

    ПРЕМИУМ

    -
    3500₽
    -
    в месяц
    -
    100 токенов включено
    -
    -
      -
    • - - 100 файлов в месяц -
    • -
    • - - 1 ГБ хранилища -
    • -
    • - - До 10 МБ на файл -
    • -
    • - - Базовые модули защиты -
    • -
    • - - Поддержка 24/7 -
    • -
    • - - API доступ -
    • -
    -
    -
    -
    -
    🚀
    -

    ПРОФИ

    -
    9500₽
    -
    в месяц
    -
    100 токенов включено
    -
    -
      -
    • - - 100 файлов в месяц -
    • -
    • - - 1 ГБ хранилища -
    • -
    • - - До 10 МБ на файл -
    • -
    • - - Базовые модули защиты -
    • -
    • - - Поддержка 24/7 -
    • -
    • - - API доступ -
    • -
    +
    + {userData?.map((item: { + id: number; + name: string; + price: number; + tokens: number; + maxFilesCount: number; + diskSize: number; + }) => ( +
    +
    +
    🚀
    +

    + {t.has(item.name) ? t(item.name) : item.name} +

    +
    {item.price}
    +
    в месяц
    +
    {item.tokens} токенов включено
    +
    +
      +
    • + + {item.maxFilesCount} файлов в месяц +
    • +
    • + + {item.diskSize ? convertBytes(item.diskSize) : 0} хранилища +
    • +
    • + + Базовые модули защиты +
    • +
    • + + Поддержка 24/7 +
    • +
    • + + API доступ +
    • +
    +
    + +
    +
    + ))}
    -
    - ) + + ); } \ No newline at end of file diff --git a/src/app/ui/payment/secure-payments.tsx b/src/app/ui/payment/secure-payments.tsx index d530eaa..2bd2eaa 100644 --- a/src/app/ui/payment/secure-payments.tsx +++ b/src/app/ui/payment/secure-payments.tsx @@ -1,9 +1,33 @@ +'use client' + +import { referralRefill } from '@/app/actions/referralsActions'; +import { toast } from 'sonner'; + export default function SecurePayments() { + return (

    - 🔒 Безопасные платежи + Безопасные платежи

    +
    + +
    diff --git a/src/app/ui/payment/user-usage.tsx b/src/app/ui/payment/user-usage.tsx index 0669b1c..2e5a443 100644 --- a/src/app/ui/payment/user-usage.tsx +++ b/src/app/ui/payment/user-usage.tsx @@ -1,38 +1,56 @@ +'use client' + +import { convertBytes } from '@/app/lib/convertBytes'; +import { getProcents } from '@/app/lib/getProcents'; +import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo'; + export default function UserUsage() { + const { data: userData, isLoading, isError, error } = useUserProfile(); + return (

    - 📊 Ваше использование + Ваше использование

    -
    0 / 2,000
    +
    + {userData?.tariffInfo?.currentFileCounts} / {userData?.tariffInfo?.maxFileCounts} +
    Файлов в месяц
    -
    0 / 20 ГБ
    +
    + {convertBytes(userData?.tariffInfo?.currentFileOnDisk ?? 0)} / {convertBytes(userData?.tariffInfo?.maxFileOnDisk ?? 0)} +
    Объём хранилища
    -
    0
    +
    + {userData?.tariffInfo?.tokens ?? 0} +
    Токенов на балансе
    -
    ПРОФИ
    +
    + {userData?.tariffInfo?.tariffName ? userData?.tariffInfo?.tariffName : 'ERROR'} +
    Текущий план
    diff --git a/src/app/ui/referral-page/Income-and-payments.tsx b/src/app/ui/referral-page/Income-and-payments.tsx index db4820f..c196ec9 100644 --- a/src/app/ui/referral-page/Income-and-payments.tsx +++ b/src/app/ui/referral-page/Income-and-payments.tsx @@ -10,7 +10,7 @@ export default function IncomeAndPayments() {

    💰 Доступно к выводу

    - 135.00 Б + 0

    Минимальная сумма вывода: 100 Б
    @@ -18,7 +18,7 @@ export default function IncomeAndPayments() {

    📈 Всего заработано

    - 135.00 Б + 0

    За все время участия в программе
    @@ -26,7 +26,7 @@ export default function IncomeAndPayments() {

    ✅ Выплачено

    - 0.00 Б + 0

    Успешно выплачено на ваши счета
    @@ -70,8 +70,8 @@ export default function IncomeAndPayments() { Обработано
    -
    - 100.00 +
    + 0
    0 @@ -82,39 +82,7 @@ export default function IncomeAndPayments() {
    0
    -
    - 0 -
    - -
    - 100.00 -
    -
    - 0 -
    -
    - 0 -
    -
    - 0 -
    -
    - 0 -
    - -
    - 100.00 -
    -
    - 0 -
    -
    - 0 -
    -
    - 0 -
    -
    +
    0
    diff --git a/src/app/ui/referral-page/commission-levels.tsx b/src/app/ui/referral-page/commission-levels.tsx index 82183b7..8100367 100644 --- a/src/app/ui/referral-page/commission-levels.tsx +++ b/src/app/ui/referral-page/commission-levels.tsx @@ -1,4 +1,118 @@ +'use client' + +import { useQuery } from '@tanstack/react-query'; +import { fetchReferralsLevels, fetchReferralUserStats } from '@/app/actions/referralsActions'; + +interface ReferralLevel { + name: string; + id: string; + next: string | null; + share: number; + maxInvitee: number; + minInvitee: number; +} + +interface ReferralsData { + referrals: ReferralLevel[]; +} + export default function CommissionLevels() { + + const { + data: referralsLevels, + isLoading, + isError, + error + } = useQuery({ + queryKey: ['referralsLevels'], + queryFn: () => fetchReferralsLevels(), + select: (data) => { + if (data?.referrals) { + const itemsMap = new Map(); + 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 (

    @@ -6,32 +120,28 @@ export default function CommissionLevels() { Уровни комиссии

    -
    -
    🥉
    -
    15%
    -
    BRONZE
    -
    0-5 рефералов
    -
    -
    -
    🥈
    -
    18%
    -
    SILVER
    -
    6-15 рефералов
    -
    - -
    -
    🥇
    -
    22%
    -
    GOLD
    -
    16-50 рефералов
    -
    - -
    -
    💎
    -
    25%
    -
    PLATINUM
    -
    50+ рефералов
    -
    + { + referralsLevels.map((item: ReferralLevel) => { + return ( +
    +
    + {getReferralIcon(item.id)} +
    +
    {item.share}%
    +
    {item.name}
    +
    { + item.maxInvitee < 100 ? `${item.minInvitee}-${item.maxInvitee}` : `${item.minInvitee}+` + } рефералов +
    +
    + ) + }) + }
    ) diff --git a/src/app/ui/referral-page/invitations-table.tsx b/src/app/ui/referral-page/invitations-table.tsx index c051c40..33f3bbe 100644 --- a/src/app/ui/referral-page/invitations-table.tsx +++ b/src/app/ui/referral-page/invitations-table.tsx @@ -1,77 +1,484 @@ -import { useTranslations } from 'next-intl'; +'use client' + +import { useState, useMemo, useEffect } from 'react'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + ColumnDef, + SortingState, + ColumnFiltersState, +} from '@tanstack/react-table'; +import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter } from '@/app/ui/icons/icons'; +import { useTranslations, useLocale } from 'next-intl'; +import DropDownList from '@/app/components/DropDownList'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { pluralize } from '@/app/lib/pluralize'; +import { fetchReferralInvitees } from '@/app/actions/referralsActions'; + +type InviteesType = { + status?: string; + email?: string; + registrationData: string; +}; + +type InviteType = { + active?: string; + email?: string; + regDate: string; +}; + export default function InvitationsTable() { - const t = useTranslations('Global'); + const { + data: referralInvitees, + isLoading, + isError, + error, + } = useQuery({ + 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([]); + const [columnFilters, setColumnFilters] = useState([]); + const [dateFilter, setDateFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('all'); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const t = useTranslations("Global"); + const locale = useLocale(); + + // Определение колонок + const columns = useMemo[]>( + () => [ + { + accessorKey: 'email', + header: ({ column }) => ( +
    + + {t('email')} + + +
    + ), + cell: ({ row }) => ( +
    + {/* {row.original.size !== undefined ? convertBytes(row.original.size) : '-'} */} + {row.original.email} +
    + ), + }, + { + accessorKey: 'status', + header: ({ column }) => ( +
    + + {t('status')} + + +
    + ), + cell: ({ row }) => { + return ( +
    + + {row.original.status} + +
    + ) + }, + }, + { + accessorKey: 'registrationData', + header: ({ column }) => ( +
    + + {t('registration-date')} + + +
    + ), + cell: ({ row }) => { + return ( +
    + {row.original.registrationData?.split('T')[0]} +
    + ) + }, + 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 ( -
    -

    - История приглашений -

    +
    +
    +

    + История приглашений +

    + {/* Фильтры */} +
    +
    +
    +
    {t('date-filter')}:
    + { + 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} + > +
  • + {t('all-dates')} +
  • +
  • + {t('today')} +
  • +
  • + {t('for-a-week')} +
  • +
  • + {t('for-a-month')} +
  • +
  • + {t('older-than-a-month')} +
  • +
    +
    +
    -
    -
    - Email -
    -
    - Статус -
    -
    - Заработано -
    -
    - Дата приглашения -
    -
    - Регистрация +
    +
    {t('items-per-page')}:
    + + {[5, 10, 20, 50, 100].map(pageSize => ( +
  • + {t('show')} {pageSize} +
  • + ))} +
    +
    -
    - email@mail.com -
    -
    - {t('status')} -
    -
    - 0 -
    -
    - 0 -
    -
    - 0 + {/* Таблица */} +
    + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + ))} + + )) + ) : ( + + + + )} + +
    + {header.isPlaceholder + ? null + : typeof header.column.columnDef.header === 'function' + ? header.column.columnDef.header(header.getContext()) + : header.column.columnDef.header as string} +
    + {typeof cell.column.columnDef.cell === 'function' + ? cell.column.columnDef.cell(cell.getContext()) + : cell.getValue() as string} +
    + {t('no-data-for-selected-filters')} +
    -
    - email@mail.com -
    -
    - {t('status')} -
    -
    - 0 -
    -
    - 0 -
    -
    - 0 -
    + {/* Пагинация */} +
    +
    + {/* + {t('page')}{' '} + + {table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1} + + + + | {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)} + */} +
    -
    - email@mail.com +
    + {table.getCanPreviousPage() && ( + + )} + {table.getCanPreviousPage() && ( + + )} + +
    + {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 ( + + ); + } + return null; + })} +
    + + {table.getCanNextPage() && ( + + )} + {table.getCanNextPage() && ( + + )} +
    -
    - {t('status')} -
    -
    - 0 -
    -
    - 0 -
    -
    - 0 -
    -
    +
    - ) + ); } \ No newline at end of file diff --git a/src/app/ui/referral-page/referral-link-section.tsx b/src/app/ui/referral-page/referral-link-section.tsx index e406f43..4da87f7 100644 --- a/src/app/ui/referral-page/referral-link-section.tsx +++ b/src/app/ui/referral-page/referral-link-section.tsx @@ -1,18 +1,118 @@ +'use client' + +import { fetchReferralUserStats } from '@/app/actions/referralsActions'; +import { useQuery } from '@tanstack/react-query'; +import { useTranslations } from 'next-intl'; +import { useEffect, useRef, useState } from 'react'; +import { toast } from 'sonner'; + export default function ReferralLinkSection() { + const { + data: referralUserStats, + isLoading, + isError, + error + } = useQuery({ + queryKey: ['referralUserStats'], + queryFn: () => { + return fetchReferralUserStats(); + }, + select: (data) => { + return data; + } + }); + + const t = useTranslations('Global') + const [referralUrl, setReferralUrl] = useState(''); + const refLink = useRef(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 (
    -

    🔗 Ваша реферальная ссылка

    +

    Ваша реферальная ссылка

    Поделитесь этой ссылкой с друзьями и получайте комиссию с каждой их покупки

    - - https://dashboard.no-copy.ru/register.php?ref=REFA0406AC8 + + {referralUrl} - +

    - Код: REFA0406AC8 • - Кликов: 21 • - Комиссия: 15% + Код: {referralUserStats?.referralLink} • + Приглашений: + {referralUserStats?.totalInvitee} +

    ) diff --git a/src/app/ui/referral-page/setting-referral-program.tsx b/src/app/ui/referral-page/setting-referral-program.tsx index 6c4e1fe..38cbab1 100644 --- a/src/app/ui/referral-page/setting-referral-program.tsx +++ b/src/app/ui/referral-page/setting-referral-program.tsx @@ -8,7 +8,7 @@ export default function SettingReferralProgram() {
    -
    +

    💳 Настройки выплат

    @@ -32,7 +32,7 @@ export default function SettingReferralProgram() {
    -
    +

    🔔 Уведомления

    @@ -40,11 +40,6 @@ export default function SettingReferralProgram() {
    -
    - - -
    -
    diff --git a/src/app/ui/reports/reports-info.tsx b/src/app/ui/reports/reports-info.tsx new file mode 100644 index 0000000..9b96ef6 --- /dev/null +++ b/src/app/ui/reports/reports-info.tsx @@ -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 ( +
    +
    +
    + {t('all-content')} +
    +
    {filesInfo?.total.quantity ? filesInfo?.total.quantity : 0}
    +
    + +
    +
    + {t('violations-few')} +
    +
    {filesInfo?.total.violation ? filesInfo?.total.violation : 0}
    +
    + +
    +
    + {t('monitoring')} +
    +
    {filesInfo?.total.check ? filesInfo?.total.check : 0}
    +
    + +
    +
    + {t('efficiency')} +
    +
    100%
    +
    +
    + ) +} \ No newline at end of file diff --git a/src/app/ui/search/file-search-panel.tsx b/src/app/ui/search/file-search-panel.tsx new file mode 100644 index 0000000..a21defb --- /dev/null +++ b/src/app/ui/search/file-search-panel.tsx @@ -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([]); + const [searchedUserFilesShowNull, setSearchedUserFilesShowNull] = useState(false); + const [searchedGlobalFiles, setSearchedGlobalFiles] = useState([]); + const [searchedGlobalFilesShowNull, setSearchedGlobalFilesShowNull] = useState(false); + const [loading, setLoading] = useState(false); + const [paginationLoading, setPaginationLoading] = useState(false); + const [showGlobalSearch, setShowGlobalSearch] = useState(false); + const queryClient = useQueryClient(); + const [errorMessage, setErrorMessage] = useState(null); + + const [searchTotalPages, setSearchTotalPages] = useState(0); + const [searchCurrentPages, setSearchCurrentPages] = useState(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 => { + 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 => { + 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 ( + <> +
    + + + {errorMessage && ( +
    + Ошибка поиска +
    + )} +
    + + {(searchedUserFiles.length !== 0) && ( + + )} + + {searchedUserFilesShowNull && ( +
    +
    +
    Результаты поиска
    +
    + Найдено файлов: {searchedUserFiles.length} +
    +
    +
    +

    Похожих файлов не найдено

    +

    В вашей библиотеке нет файлов, похожих на загруженный

    +
    + Совет: Убедитесь, что вы загружали этот файл ранее через страницу "Маркировка" +
    +
    + +
    + )} + + {showGlobalSearch && ( +
    +
    +
    + Глобальный поиск изображений +
    +
    + Найти где ещё используется ваше изображение в интернете +
    +
    + Интернет поиск +
    +
    + + +
    +
    +
    + Поиск по всему интернету +
    +
    + Используем продвинутые технологии обратного поиска по изображению для поиска копий вашего контента + на сайтах, в социальных сетях и других источниках. Поможет обнаружить несанкционированное использование. +
    +
    + +
    +
    +
    +
    +
    Глобальных поисков сегодня
    +
    0 из 10
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + Поиск изображений в интернете... +
    +
    + +
    +
    +
    + Найдено в интернете + {paginationLoading && ( +
    + +
    + )} +
    +
    + +
    +
    +
    + + {searchedGlobalFilesShowNull && ( +
    +
    +

    Похожие изображения в интернете не найдены

    +

    Это может означать, что ваше изображение уникально и не используется на других сайтах.

    +
    + )} + +
    + Ошибка глобального поиска: + +
    + + +
    +
    + )} + + ) +} \ No newline at end of file diff --git a/src/app/ui/search/recent-searches.tsx b/src/app/ui/search/recent-searches.tsx new file mode 100644 index 0000000..54e0135 --- /dev/null +++ b/src/app/ui/search/recent-searches.tsx @@ -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 ( +
    +
    + Недавние поиски +
    +
    + + document +
    +
    {userSearchData?.document?.count ? userSearchData?.document?.count : 0} поисков
    +
    + date +
    +
    +
    +
    + + image +
    +
    {userSearchData?.image?.count ? userSearchData?.image?.count : 0} поисков
    +
    + date +
    +
    +
    +
    + + audio +
    +
    {userSearchData?.audio?.count ? userSearchData?.audio?.count : 0} поисков
    +
    + date +
    +
    +
    +
    + + video +
    +
    {userSearchData?.video?.count ? userSearchData?.video?.count : 0} поисков
    +
    + date +
    +
    +
    +
    + ) +} \ No newline at end of file diff --git a/src/app/ui/search/search-stats.tsx b/src/app/ui/search/search-stats.tsx new file mode 100644 index 0000000..387ee8d --- /dev/null +++ b/src/app/ui/search/search-stats.tsx @@ -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 ( +
    +
    + Статистика поиска +
    + +
    + Изображения + + {userSearchData?.image?.count ? userSearchData?.image?.count : 0} + +
    + +
    + Видео + + {userSearchData?.video?.count ? userSearchData?.video?.count : 0} + +
    + +
    + Аудио + + {userSearchData?.audio?.count ? userSearchData?.audio?.count : 0} + +
    + +
    + Документы + + {userSearchData?.document?.count ? userSearchData?.document?.count : 0} + +
    + +
    + Всего поисков + + {userSearchData?.allCountForPeriod ? userSearchData?.allCountForPeriod : 0} + +
    +
    + ) +} \ No newline at end of file diff --git a/src/app/ui/search/searched-global-files-list.tsx b/src/app/ui/search/searched-global-files-list.tsx new file mode 100644 index 0000000..194c71f --- /dev/null +++ b/src/app/ui/search/searched-global-files-list.tsx @@ -0,0 +1,45 @@ +import { SearchItem } from './file-search-panel' + +export function SearchedGlobalFilesList({ list }: { list: SearchItem[] }) { + + return ( + <> + {list.map( + ( + item: SearchItem, index: number + ) => { + return ( +
    +
    + {item?.pageTitle} +
    +
    +
    + {item?.pageTitle} +
    +
    + {item?.height & item?.width && ( +
    + {item?.height}x{item?.width} +
    + )} +
    + {item?.host} +
    +
    +
    + {item?.pageTitle} +
    + + {item?.pageUrl} + +
    +
    + ) + })} + + ) +} \ No newline at end of file diff --git a/src/app/ui/search/searched-user-files-list.tsx b/src/app/ui/search/searched-user-files-list.tsx new file mode 100644 index 0000000..d5f35ed --- /dev/null +++ b/src/app/ui/search/searched-user-files-list.tsx @@ -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(false); + const [openWindowChildren, setOpenWindowChildren] = useState(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 ( +
    +
    +
    + {e.originalFileName} +
    +
    +
    +
    +
    +
    + img +
    +
    +
    +
    +

    Информация о файле

    +
    +
    + ID в системе: + + {e.supportId ? e.supportId : '#'} + +
    +
    + Дата загрузки: + + {e.uploadDate ? e.uploadDate : '#'} + +
    +
    + Размер файла: + + {convertBytes(e.fileSize)} + +
    +
    + Статус защиты: + + {e.status ? e.status : '#'} + +
    +
    +
    +
    +
    + ) + }) + setOpenWindow(true); + }; + + return ( + <> +
    +
    +
    Результаты поиска
    +
    + Найдено файлов: {list.length} +
    +
    +
    + {list.map((e: any, index: number) => { + if (e.similarityLevel !== 'DIFFERENT') { + return ( +
    +
    +
    +
    + {e.url ? ( + Preview + ) : ( +
    + 📄 +
    + )} +
    +
    +
    + {e.originalFileName} +
    +
    + Загружен: {e.uploadDate ? e.uploadDate : '#'} • + Размер: {convertBytes(e.fileSize)} +
    +
    +
    +
    + {e.similarityLevel} +
    +
    +
    + +
    + + {e.status} + +
    + + +
    + + +
    +
    +
    ) + } + })} +
    +
    + + + + ) +} \ No newline at end of file diff --git a/src/app/components/upload-section-file.tsx b/src/app/ui/search/section-search-file.tsx similarity index 62% rename from src/app/components/upload-section-file.tsx rename to src/app/ui/search/section-search-file.tsx index f929c2e..b89f212 100644 --- a/src/app/components/upload-section-file.tsx +++ b/src/app/ui/search/section-search-file.tsx @@ -1,20 +1,18 @@ 'use client' import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react'; -import { IconShieldAdd } from '@/app/ui/icons/icons'; +import { IconSearch } from '@/app/ui/icons/icons'; import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload'; import { useTranslations } from 'next-intl'; import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker'; import { useQueryClient } from '@tanstack/react-query'; +import { removeUserFile } from '@/app/actions/fileEntity'; +import { FileSearchPanel } from '@/app/ui/search/file-search-panel'; interface SelectedFile { file: File; preview: string | undefined; name: string; size: string; - dimensions?: { - width: number; - height: number; - }; } interface FileUploadInitResponse { @@ -25,13 +23,17 @@ interface FileUploadInitResponse { status: string } -interface UploadSectionFile { - fileType: string - allowedExtensions: string[] +interface SectionSearchFile { + allowedExtensions: { + images: string[], + videos: string[], + audios: string[], + documents: string[] + } maxFileSize: number } -export default function UploadSectionFile({ fileType, allowedExtensions, maxFileSize }: UploadSectionFile) { +export default function SectionSearchFile({ allowedExtensions, maxFileSize }: SectionSearchFile) { const fileInputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [selectedFile, setSelectedFile] = useState(null); @@ -39,22 +41,57 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile const [uploadId, setUploadId] = useState(null); const [isFileUploaded, setIsFileUploaded] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); + const [fileId, setFileId] = useState(null); const queryClient = useQueryClient(); const isCancelledRef = useRef(false); + const childRef = useRef(null); + const t = useTranslations('Global'); + const allExtensions = useMemo(() => { return [...allowedExtensions.images, ...allowedExtensions.videos, ...allowedExtensions.audios, ...allowedExtensions.documents] }, []); + const acceptString = useMemo(() => { - if (!allowedExtensions || !Array.isArray(allowedExtensions)) { + if (!allExtensions || !Array.isArray(allExtensions)) { return ''; } - return allowedExtensions.map(e => `.${e}`).join(', '); - }, [allowedExtensions]); + return allExtensions.map(e => `.${e}`).join(', '); + }, [allExtensions]); + + + const getFileType = (fileName: string, allowedExtensions: { + images: string[], + videos: string[], + audios: string[], + documents: string[] + }): 'image' | 'video' | 'audio' | 'document' | 'unknown' => { + + const lastDotIndex = fileName.lastIndexOf('.'); + const extension = fileName.substring(lastDotIndex + 1).toLowerCase(); + + if (allowedExtensions.images.includes(extension)) { + return 'image'; + } + + if (allowedExtensions.videos.includes(extension)) { + return 'video'; + } + + if (allowedExtensions.audios.includes(extension)) { + return 'audio'; + } + + if (allowedExtensions.documents.includes(extension)) { + return 'document'; + } + + return 'unknown'; + } const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => { - if (!allowedExtensions.includes(file.type as string)) { + if (!allExtensions.includes(file.type as string)) { const extension = file.name.split('.').pop()?.toLowerCase(); - if (!extension || !allowedExtensions.includes(extension as string)) { + if (!extension || !allExtensions.includes(extension as string)) { return { isValid: false, errorMessage: t('unsupported-file-format') @@ -74,37 +111,25 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile return { isValid: true }; }; - const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => { - return new Promise((resolve, reject) => { - const img = new Image(); - const objectUrl = URL.createObjectURL(file); - - img.onload = () => { - const dimensions = { - width: img.width, - height: img.height - }; - URL.revokeObjectURL(objectUrl); - resolve(dimensions); - }; - - img.onerror = () => { - URL.revokeObjectURL(objectUrl); - reject(new Error(t('error-uploading-file'))); - }; - - img.src = objectUrl; - }); - }; - const handleFileSelect = useCallback(async (file: File | null): Promise => { if (!file) { setError(null); return; } + if (fileId) { + await removeUserFile(fileId, 1); + setFileId(null); + } + setError(null); setIsFileUploaded(false); + + if (childRef.current) { + //@ts-ignore + childRef.current.clearList(); + } + setUploadProgress(0); const validation = validateFile(file); @@ -114,31 +139,21 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile return; } - let dimensions = undefined; - if (fileType === "image") { - try { - dimensions = await getImageDimensions(file) as { width: number, height: number }; - if (dimensions.width < 100 || dimensions.height < 100 || - dimensions.width > 10000 || dimensions.height > 10000) { - setError(`${t('image-size')}: ${dimensions.width}x${dimensions.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`); - return; - } - } catch (err) { - setError(t('error-reading-image')); - return; - } - } - setSelectedFile({ file, name: file.name, size: `${(file.size / 1024 / 1024).toFixed(2)} MB`, - dimensions, preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined }); - console.log('Файл выбран:', file.name, file.size); - }, [fileType, t]); + handlerFileUpload({ + file, + name: file.name, + size: `${(file.size / 1024 / 1024).toFixed(2)} MB`, + preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined + }); + + }, [t]); const handleFileInputChange = (event: ChangeEvent): void => { const file = event.target.files?.[0] || null; @@ -172,29 +187,13 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile handleFileSelect(file); }; - const handleClearFile = async (): Promise => { - 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 => { if (uploadId) return; setError(null); isCancelledRef.current = false; const file = fileInfo.file; + const fileType = getFileType(fileInfo.file.name, allowedExtensions); try { const extension = file.name.split('.').pop() || ''; @@ -221,6 +220,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile return; } + const isLastChunk = chunkIndex === totalChunks - 1; const start = chunkIndex * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); @@ -229,14 +229,20 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile formData.append('upload_id', response.upload_id); formData.append('chunk_number', chunkIndex.toString()); formData.append('chunk', chunk); + formData.append('findSimilar', '1'); const chunkResponse = await chunkUpload(formData); - if (chunkResponse.message_desc !== 'Chunk uploaded successfully') { - throw new Error(`Chunk ${chunkIndex} upload failed`); + if (chunkResponse.errorMesage !== null) { + throw chunkResponse.errorMesage; } setUploadProgress(Math.floor((chunkIndex + 1) / totalChunks * 100)); + + if (isLastChunk && chunkResponse.fileId) { + setFileId(chunkResponse.fileId); + document.cookie = `searchedFileId=${chunkResponse.fileId}` + } } const chunkStatus = await checkChunkStatus(response.upload_id); @@ -256,7 +262,7 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile } finally { setUploadId(null); } - }, [uploadId, fileType, t]); + }, [uploadId, allowedExtensions]); useEffect(() => { // Обработка закрытия вкладки @@ -296,9 +302,13 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile return (
    -

    - {t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })} -

    +
    +
    Как работает поиск?
    +
    + Наша система анализирует загруженный файл и сравнивает его с вашими защищенными файлами, + используя алгоритмы компьютерного зрения и цифровых отпечатков. +
    +
    - +

    - {t('drag-the-file-here')} + Выберите файл для поиска

    - {/*

    - {t('or')} -

    */} - - {fileType === "image" && ( -

    - {t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')} -

    - )} -

    - {t('file-format')}: {acceptString} -

    -

    - {t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')} -

    - {t('select-files-to-protect')} + Выбрать файл {error && ( @@ -351,9 +347,9 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile {selectedFile && (
    -
    +

    @@ -365,52 +361,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile {t('size')}: {selectedFile.size}

    -

    - {t('cost-of-protection')}: 0 -

    -

    - {t('current-balance')}: 0 -

    -
    - {isFileUploaded && ( -
    - {t('file-successfully-uploaded')} -
    - )} -
    -
    - - {(fileType === 'image' && selectedFile.preview) && ( - Предпросмотр загруженного изображения setError('Не удалось загрузить превью изображения')} - /> - )} - -
    - - {uploadProgress !== 0 && (
    {uploadProgress}%
    @@ -419,6 +373,10 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
    )}
    + + {isFileUploaded && ( + + )}
    ); } \ No newline at end of file diff --git a/src/app/ui/search/supported-formats.tsx b/src/app/ui/search/supported-formats.tsx new file mode 100644 index 0000000..ce9b7ae --- /dev/null +++ b/src/app/ui/search/supported-formats.tsx @@ -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 ( +
    +
    + Поддерживаемые форматы +
    + +
    +
    + Изображения +
    +
    + {extensionImage.join(', ')} +
    +
    + +
    +
    + Видео +
    +
    + {extensionVideo.join(', ')} +
    +
    + +
    +
    + Аудио +
    +
    + {extensionAudio.join(', ')} +
    +
    + +
    +
    + Документы +
    +
    + {extensionDocument.join(', ')} +
    +
    +
    + ) +} \ No newline at end of file diff --git a/src/app/ui/settings/company-users-setting-panel.tsx b/src/app/ui/settings/company-users-setting-panel.tsx new file mode 100644 index 0000000..3ac5c0b --- /dev/null +++ b/src/app/ui/settings/company-users-setting-panel.tsx @@ -0,0 +1,315 @@ +'use client' + +import styles from '@/app/styles/module/login.module.scss'; +import { useTranslations } from 'next-intl'; +import { useActionState, useEffect, useRef, useState, ChangeEvent } from 'react'; +import { getUserData } from '@/app/actions/action'; +import { useQuery } from '@tanstack/react-query'; +import { companyUserRegistration } from '@/app/actions/companyActions'; +import { CompanyUserRegistrationFormState } from '@/app/actions/companyActions'; +import { SignupFormSchema } from '@/app/actions/definitions'; +import * as z from 'zod'; +import PhoneInput from '@/app/ui/inputs/phone-input'; +import { IconEye } from '@/app/ui/icons/icons'; +import { toast } from 'sonner'; +import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo'; + +export function CompanyUsersSettingPanel() { + const { data: userData, isLoading, isError, error } = useUserProfile(); + + if (userData?.permission !== 3) { + return null + } + + const [state, formAction, isPending] = useActionState( + companyUserRegistration, + undefined + ); + + const t = useTranslations('Login-register-form'); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [formState, setFormState] = useState(undefined); + const passwordRef = useRef(null); + + useEffect(() => { + if (state) { + setFormState(state); + if (state.createdUser) { + toast.success(`Создан пользователь - ${state.createdUser}`); + } + } + + }, [state]) + + function validateField( + fieldName: string, + value: string, + ) { + switch (fieldName) { + case 'confirm_password': + const baseSchema = SignupFormSchema.shape.confirm_password; + const baseResult = baseSchema.safeParse(value); + + if (!baseResult.success) { + return baseResult; + } + + const currentPassword = passwordRef.current?.value || ''; + if (value !== currentPassword) { + return { + success: false, + error: new z.ZodError([ + { + code: z.ZodIssueCode.custom, + message: 'repeat-password', + path: ['confirm_password'] + } + ]) + }; + } + + return { success: true, data: value }; + + case 'email': + case 'fullName': + case 'phone': + case 'password': + case 'companyName': + case 'agree': + const schema = SignupFormSchema.shape[fieldName]; + if (fieldName === 'phone') { + return schema.safeParse(value.replace(/[-\(\)\s]/g, '')); + } + if (fieldName === 'companyName') { + return { success: true, data: value }; + } + return schema.safeParse(value); + + default: + throw new Error(`Unknown field: ${fieldName}`); + } + } + + function onChangeHandler(e: ChangeEvent): void { + const fieldName = e.target.name; + if (fieldName !== 'phone') { + e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, ''); + } + const validatedField = validateField(fieldName, e.target.value); + + if (validatedField.success) { + setFormState(prevState => { + if (!prevState?.error || !prevState?.error[fieldName]) { + return prevState; + } + + const { [fieldName]: _, ...newError } = prevState.error; + + return { + ...prevState, + error: newError + }; + }); + } + } + + function showPassowrd(target: 'password' | 'confirm-password') { + if (target === 'password') { + setShowPassword(!showPassword); + } else { + setShowConfirmPassword(!showConfirmPassword); + } + } + + return ( +
    +
    +

    + Добавление пользователей +

    + { + formAction(e); + }}> +
    + + + {formState?.error?.fullName && ( +

    + { + formState?.error?.fullName.split('&').map((e, index) => { + return ( + + {t(e)} +
    +
    + ) + }) + } +

    + )} + +
    +
    + + { + e.target.value = e.target.value.toLocaleLowerCase(); + onChangeHandler(e) + }} + /> + {formState?.error?.email && ( +

    + { + formState?.error?.email.split('&').map((e, index) => { + return ( + + {t(e)} +
    +
    + ) + }) + } +

    + )} +
    +
    + + + {formState?.error?.phone && ( +

    + { + formState?.error?.phone.split('&').map((e, index) => { + return ( + + {t(e)} +
    +
    + ) + }) + } +

    + )} +
    +
    + +
    + + +
    + {formState?.error?.password && ( +

    + { + formState?.error?.password.split('&').map((e, index) => { + return ( + + {t(e)} +
    +
    + ) + }) + } +

    + )} +
    + +
    + +
    + + +
    + {formState?.error?.confirm_password && ( +

    + {t(formState?.error?.confirm_password)} +

    + )} +
    + + {formState?.error?.server && ( +

    + {t(formState?.error?.server)} +

    + )} + + + +
    +
    + ) +} \ No newline at end of file diff --git a/src/app/ui/settings/personal-data-settings.tsx b/src/app/ui/settings/personal-data-settings.tsx index 61128e8..37cd0c1 100644 --- a/src/app/ui/settings/personal-data-settings.tsx +++ b/src/app/ui/settings/personal-data-settings.tsx @@ -1,7 +1,10 @@ 'use client' import { useTranslations } from 'next-intl'; -import { useActionState, useState } from 'react' +import { useActionState, useState, useEffect } from 'react'; +import { getUserData } from '@/app/actions/action'; +import { useQuery } from '@tanstack/react-query'; +import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo'; export default function PersonalDataSettings() { const [errorMessage, formAction, isPending] = useActionState( @@ -11,7 +14,11 @@ export default function PersonalDataSettings() { undefined ); - const [selectedValue, setSelectedValue] = useState('male'); + const { data: userData, isLoading, isError, error } = useUserProfile(); + + useEffect(() => { + console.log(userData) + }, [userData]); const t = useTranslations('Global'); @@ -26,19 +33,37 @@ export default function PersonalDataSettings() { - -
    -
    - - +
    + {userData?.company && ( +
    + + +
    + )}
    - + {t('email-cant-change')} @@ -47,45 +72,13 @@ export default function PersonalDataSettings() { - -
    -
    - - + defaultValue={userData?.phone} + />
    -
    - - - - {t('used-to-send-congratulations')} - -
    -
    - - - - {t('automatically-filled-in-based-on-date-of-birth')} - -
    - diff --git a/src/app/ui/settings/subscription-settings.tsx b/src/app/ui/settings/subscription-settings.tsx index a984483..beaef02 100644 --- a/src/app/ui/settings/subscription-settings.tsx +++ b/src/app/ui/settings/subscription-settings.tsx @@ -1,7 +1,27 @@ +'use client' + import { useTranslations } from 'next-intl'; +import { getUserData } from '@/app/actions/action'; +import { useQuery } from '@tanstack/react-query'; +import { convertBytes } from '@/app/lib/convertBytes'; +import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo'; export default function SubscriptionSettings() { const t = useTranslations('Global'); + const { data: userData, isLoading, isError, error } = useUserProfile(); + + function dateHandler(date: string | undefined) { + if (!date) { + return 'error' + } + + const newDate = new Date(date); + const day = newDate.getDate().toString().padStart(2, '0'); + const month = (newDate.getMonth() + 1).toString().padStart(2, '0'); + const year = newDate.getFullYear(); + + return `${day}.${month}.${year}`; + } return (
    @@ -12,18 +32,18 @@ export default function SubscriptionSettings() {
    -

    🎯 Подписка "ПРОФИ"

    -

    📅 Активна до 10.06.2028

    +

    Подписка {userData?.tariffInfo?.tariffName}

    +

    📅 Активна до {dateHandler(userData?.tariffInfo?.endTariff)}

    -
    2,000
    +
    {userData?.tariffInfo?.maxFileCounts}

    файлов в месяц

    💾 Хранилище
    - 20 ГБ + {userData?.tariffInfo?.maxFileOnDisk ? convertBytes(userData?.tariffInfo?.maxFileOnDisk) : 0}
    🔌 API доступ
    @@ -36,7 +56,7 @@ export default function SubscriptionSettings() {
    -
    + {/*
    @@ -46,7 +66,7 @@ export default function SubscriptionSettings() { -
    +
    */}
    ) diff --git a/src/app/ui/test.tsx b/src/app/ui/test.tsx index b1c4047..534facc 100644 --- a/src/app/ui/test.tsx +++ b/src/app/ui/test.tsx @@ -1,20 +1,9 @@ 'use client'; -import { useQuery } from '@tanstack/react-query'; -import { getUserData } from '@/app/actions/action'; +import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo'; export default function HomePage() { - const { - data: userData, - isLoading, - isError, - error, - } = useQuery({ - queryKey: ['userData'], - queryFn: () => { - return getUserData(); - }, - }); + const { data: userData, isLoading, isError, error } = useUserProfile(); if (isLoading) return
    Loading...
    ; if (isError) return
    Error: {error.message}
    ; @@ -23,7 +12,7 @@ export default function HomePage() {

    user email

    - {userData.email} + {userData?.email}
    ); diff --git a/src/app/ui/violations/violations-statistic.tsx b/src/app/ui/violations/violations-statistic.tsx index b3e2541..e37dc79 100644 --- a/src/app/ui/violations/violations-statistic.tsx +++ b/src/app/ui/violations/violations-statistic.tsx @@ -1,51 +1,25 @@ 'use client' import { useTranslations } from 'next-intl'; -import { useQuery } from '@tanstack/react-query'; -import { getUserFilesInfo } from '@/app/actions/action'; - -type FilesInfo = { - totalSize: number; - totalCount: number; -}; +import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo'; export default function ViolationsStatistic() { const t = useTranslations('Global'); - const { - data: filesInfo, - isLoading, - isError, - error, - } = useQuery<{ - all_files_size: number, - all_files_quantity: number - }, Error, FilesInfo>({ - queryKey: ['userFilesInfo'], - queryFn: getUserFilesInfo, - select: (data): FilesInfo => { - if (!data) { - return { - totalSize: 0, - totalCount: 0 - } - } - - return { - totalSize: data.all_files_size, - totalCount: data.all_files_quantity - } - } - }); + const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); return (
    -
    0
    +
    + {filesInfo?.total.violation} +
    {t(`total-violations`)}
    -
    0
    +
    + {filesInfo?.total.violation} +
    {t(`new`)}
    diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 9479042..2ecd534 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -18,8 +18,10 @@ "violations-few": "Violations", "violations": "Violations", "check": "Check", + "make-check": "Check", "checks": "Checks", "checks-few": "Checks", + "document": "Document", "documents": "Documents", "video": "video", "videos": "Videos", @@ -41,6 +43,7 @@ "photo-marking": "Photo marking", "video-marking": "Video marking", "audio-marking": "Audio marking", + "document-marking": "Document marking", "tokens": "tokens", "current-balance-of-protection": "Current file protection token balance", "rate": "Rate", @@ -83,6 +86,7 @@ "save-changes": "Save changes", "video-protection": "Video protection", "audio-protection": "Audio protection", + "document-protection": "Document protection", "notifications-and-monitoring": "Notifications and Monitoring", "email-notifications": "Email Notifications", "receive-violation-notifications-on-email": "Receive violation notifications via email", @@ -143,6 +147,7 @@ "pixels": "pixels", "error-processing-file": "Error processing file", "error-uploading-file": "Error uploading file", + "error-duplicate-file": "This file has already been uploaded to the system", "unknown-validation-error": "Unknown validation error", "unsupported-file-format": "Unsupported file format.", "the-file-is-too-large": "The file is too large", @@ -170,6 +175,7 @@ "multi-layered-protection-for-your-images": "Multi-layered image content protection with invisible watermarks", "multi-layered-protection-for-your-audios": "Multi-layered audio content protection with invisible watermarks", "multi-layered-protection-for-your-videos": "Multi-layered video content protection with invisible watermarks", + "multi-layered-protection-for-your-document": "Multi-layered PDF document content protection with invisible watermarks", "my-content-description": "Managing protected files and copyright control", "monitoring-copyright-infringements": "Monitoring copyright infringements", "monitoring-copyright-infringements-description": "Monitor and manage copyright infringement of your protected content online", @@ -183,19 +189,45 @@ "protected-audio": "Protected audio", "image-protected": "Images Protected", "video-protected": "Videos Protected", - "audio-protected": "Audio Protected", + "audio-protected": "Audios Protected", + "document-protected": "Documents Protected", "image-checked": "Images Checked", "video-checked": "Videos Checked", - "audio-checked": "Audio Checked", + "audio-checked": "Audios Checked", + "document-checked": "Documents Checked", "image-occupy": "Occupied by Images", "video-occupy": "Occupied by Videos", - "audio-occupy": "Occupied by Audio", + "audio-occupy": "Occupied by Audios", + "document-occupy": "Occupied by Documents", "violations-found": "Violations found", "total-violations": "Total violations", "new": "new", "in-progress": "In progress", "it-decided": "It's decided", - "API": "API" + "API": "API", + "NOT_PROTECTED": "Not protected", + "PROCESSING": "Processing", + "PROTECTED": "Protected", + "FAILED": "Failed", + "FAILED_SAVE": "Failed save", + "view": "View", + "are-no-violations": "There are no violations", + "error-reading-image": "Error reading image", + "error-user-not-have-tokens-for-protect": "User not have tokens for protect", + "registration-date": "Registration date", + "copied": "Copied", + "uploading": "Uploading", + "clear-all": "Clear all", + "upload-all": "Upload all", + "remove": "Remove", + "start-upload": "Start upload", + "completed": "Completed", + "pending": "Pending", + "all-content": "All content", + "monitoring": "Monitoring", + "efficiency": "Efficiency", + "convert-to": "Convert to", + "do-not-convert": "Do not convert" }, "Login-register-form": { "and": "and", @@ -234,8 +266,9 @@ "register-error-password-one-letter": "Password must contain at least 1 letter.", "register-error-password-min": "Password must be at least 8 characters long.", "register-error-phone-min": "Please enter a phone number.", + "register-error-phone-max": "The phone length must not exceed 14 characters", "register-error-company-name-min": "The company name must contain at least 2 characters.", - "register-error-company-name-max": "The company name must exceed 200 characters.", + "register-error-company-name-max": "The company name must not exceed 200 characters.", "remember-me": "Remember me", "repeat-password": "Repeat password", "request-ended-with-an-error": "The request ended with an error", @@ -260,12 +293,26 @@ "register-error-company-name-not-allowed-symbols": "The company name must not contain symbols.", "password-too-common": "This password is too common, please choose another.", "register-error-email-min": "Email length must be at least 7 characters.", - "register-error-phone-not-allowed-symbols": "", - "register-error-password-not-allowed-symbols": "Password contains invalid characters.", + "register-error-phone-not-allowed-symbols": "Phone contains invalid characters.", + "register-error-password-not-allowed-symbols": "Password contains invalid characters. Can be used @.!#$%&'*+/=?^_{|}~-", "confirmation-code": "Confirmation code", "enter-confirmation-code": "Enter the confirmation code sent to your email:", "confirm": "Confirm", "entered-code-is-incorrect": "The code you entered is incorrect.", - "resend-code": "Resend code" + "resend-code": "Resend code", + "personal": "Personal", + "unknown-error": "Unknown error", + "change-password": "Change Password", + "recovery-code": "Recovery code", + "referal-code": "Referal code", + "email-invalid": "Invalid email address", + "referral-link-is-not-exist": "This referral link does not exist.", + "register-error-inn-max": "INN must not exceed 10 characters.", + "register-error-inn-min": "INN must contain at least 10 characters.", + "register-error-inn-not-allowed-symbols": "INN contains invalid characters.", + "field-filled-automatically": "The field is filled in automatically", + "inn-invalid": "Invalid INN", + "inn-placeholder": "Enter 10 digits", + "INN": "INN" } } \ No newline at end of file diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 5c5f60f..dc4e879 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -18,8 +18,10 @@ "violations-few": "Нарушения", "violations": "Нарушений", "check": "Проверка", + "make-check": "Проверить", "checks": "Проверок", "checks-few": "Проверки", + "document": "документ", "documents": "Документы", "video": "видео", "videos": "Видео", @@ -41,6 +43,7 @@ "photo-marking": "Маркировка фото", "video-marking": "Маркировка видео", "audio-marking": "Маркировка аудио", + "document-marking": "Маркировка документов", "tokens": "токенов", "current-balance-of-protection": "Текущий баланс токенов для защиты файлов", "rate": "Тариф", @@ -83,6 +86,7 @@ "save-changes": "Сохранить изменения", "video-protection": "Защита видео", "audio-protection": "Защита аудио", + "document-protection": "Защита документов", "notifications-and-monitoring": "Уведомления и мониторинг", "email-notifications": "Email уведомления", "receive-violation-notifications-on-email": "Получать уведомления о нарушениях на email", @@ -143,6 +147,7 @@ "pixels": "пикселей", "error-processing-file": "Ошибка при обработке файла", "error-uploading-file": "Ошибка при загрузке файла", + "error-duplicate-file": "Такой файл уже загружен в систему", "unknown-validation-error": "Неизвестная ошибка валидации", "unsupported-file-format": "Неподдерживаемый формат файла.", "the-file-is-too-large": "Файл слишком большой", @@ -158,7 +163,7 @@ "you-sure-you-want-to-delete": "Уверены, что хотите удалить?", "reports-and-analytics": "Отчёты и аналитика", "file-has-been-deleted": "Файл удален", - "error": "Ошбка", + "error": "Ошибка", "failed-to-download-file": "Не удалось скачать файл", "file-is-downloading": "Скачивается файл", "status": "статус", @@ -170,6 +175,7 @@ "multi-layered-protection-for-your-images": "Многослойная защита изображений с невидимыми водяными знаками", "multi-layered-protection-for-your-audios": "Многослойная защита аудиоконтента с невидимыми водяными знаками", "multi-layered-protection-for-your-videos": "Многослойная защита видеоконтента с невидимыми водяными знаками", + "multi-layered-protection-for-your-document": "Многослойная защита PDF документов с невидимыми водяными знаками", "my-content-description": "Управление защищенными файлами и контролем авторских прав", "monitoring-copyright-infringements": "Мониторинг нарушений авторских прав", "monitoring-copyright-infringements-description": "Отслеживание и управление нарушениями авторских прав на ваш защищенный контент в интернете", @@ -184,18 +190,44 @@ "image-protected": "Изображений защищено", "video-protected": "Видео защищено", "audio-protected": "Аудио защищено", + "document-protected": "Документов защищено", "image-checked": "Изображений проверялось", "video-checked": "Видео проверялось", "audio-checked": "Аудио проверялось", + "document-checked": "Документов проверялось", "image-occupy": "Занимают изображения", "video-occupy": "Занимают видео", "audio-occupy": "Занимают аудио", + "document-occupy": "Занимают документы", "violations-found": "Найдено нарушений", "total-violations": "Всего нарушений", "new": "Новые", "in-progress": "В работе", "it-decided": "Решено", - "API": "API" + "API": "API", + "NOT_PROTECTED": "Не защищено", + "PROCESSING": "Обработка", + "PROTECTED": "Защищено", + "FAILED": "Ошибка", + "FAILED_SAVE": "Сохранение не удалось", + "view": "Посмотреть", + "are-no-violations": "Нарушений нет", + "error-reading-image": "Ошибка чтения изображения", + "error-user-not-have-tokens-for-protect": "У пользователя нет токенов для защиты.", + "registration-date": "Дата регистрации", + "copied": "Скопировано", + "uploading": "Загрузка", + "clear-all": "Очистить всё", + "upload-all": "Загрузить всё", + "remove": "Удалить", + "start-upload": "Начать загрузку", + "completed": "Завершено", + "pending": "В ожидании", + "all-content": "Весь контент", + "monitoring": "Мониторинг", + "efficiency": "Эффективность", + "convert-to": "Конвертировать в", + "do-not-convert": "Не конвертировать" }, "Login-register-form": { "and": "и", @@ -234,8 +266,9 @@ "register-error-password-one-letter": "Пароль должен содержать как минимум 1 букву.", "register-error-password-min": "Длина пароля должна быть не менее 8 символов.", "register-error-phone-min": "Пожалуйста, введите телефонный номер.", + "register-error-phone-max": "Длина телефона не должна превышать 14 символов", "register-error-company-name-min": "Название компании должно содержать не менее 2 символов.", - "register-error-company-name-max": "Название компании должно превышать 200 символов.", + "register-error-company-name-max": "Название компании не должно превышать 200 символов.", "remember-me": "Запомнить меня", "repeat-password": "Повторите пароль", "request-ended-with-an-error": "Запрос завершился с ошибкой", @@ -260,12 +293,26 @@ "register-error-company-name-not-allowed-symbols": "Название компании не должно содержать символов.", "password-too-common": "Этот пароль слишком простой, пожалуйста, выберите другой.", "register-error-email-min": "Длина Email не должна быть меньше 7 символов.", - "register-error-phone-not-allowed-symbols": "", - "register-error-password-not-allowed-symbols": "Пароль содержит недопустимые символы.", + "register-error-phone-not-allowed-symbols": "Телефон содержит недопустимые символы.", + "register-error-password-not-allowed-symbols": "Пароль содержит недопустимые символы. Можно использовать @.!#$%&'*+/=?^_{|}~-", "confirmation-code": "Код подтверждения", "enter-confirmation-code": "Введите код подтверждения высланный на почту:", "confirm": "Подтвердить", "entered-code-is-incorrect": "Введенный код не верен.", - "resend-code": "Выслать код заново" + "resend-code": "Выслать код заново", + "personal": "Личный", + "unknown-error": "Неизвестная ошибка", + "change-password": "Изменить пароль", + "recovery-code": "Код восстановления", + "referal-code": "Реферальный код", + "email-invalid": "Неверный адрес электронной почты", + "referral-link-is-not-exist": "Такой реферальной ссылки не существует.", + "register-error-inn-max": "ИНН не должн превышать 10 символов.", + "register-error-inn-min": "ИНН должн содержать не менее 10 символов.", + "register-error-inn-not-allowed-symbols": "ИНН содержит недопустимые символы.", + "field-filled-automatically": "Поле заполняется автоматически", + "inn-invalid": "Неверный ИНН", + "inn-placeholder": "Введите 10 цифр", + "INN": "ИНН" } } \ No newline at end of file