Compare commits
400
Commits
062c3e3539
...
NCBACK-168
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
587d7ceead | ||
|
|
ac8ef8caa2 | ||
|
|
7d0fb265d4 | ||
|
|
743db57bb3 | ||
|
|
d7d2e36573 | ||
|
|
8b157b8e87 | ||
|
|
0935e8cbc7 | ||
|
|
f04ce348ae | ||
|
|
508e0d182c | ||
|
|
dbd5d43d65 | ||
|
|
7d8a725e34 | ||
|
|
491adb4090 | ||
|
|
1a3f5277d9 | ||
|
|
ffcd5fc535 | ||
|
|
65e0c2ed62 | ||
|
|
36b4a3b481 | ||
|
|
e0455b200a | ||
|
|
348a5398d0 | ||
|
|
af87bfc7b6 | ||
|
|
beefe86ffd | ||
|
|
3a7c609588 | ||
|
|
aeeee04166 | ||
|
|
288d6782a4 | ||
|
|
55915af9df | ||
|
|
bfc457470c | ||
|
|
210145f5ca | ||
|
|
d0793a281e | ||
|
|
6994edd216 | ||
|
|
3a0ae93846 | ||
|
|
13a5db6313 | ||
|
|
9c011b6416 | ||
|
|
294e16e1a6 | ||
|
|
71f7120239 | ||
|
|
0e28986dc7 | ||
|
|
51a3ed7d18 | ||
|
|
c15867a41c | ||
|
|
c4e4d775f5 | ||
|
|
cd07502128 | ||
|
|
1ef2cc227a | ||
|
|
7226285746 | ||
|
|
dd93cd302c | ||
|
|
37b7ccb52e | ||
|
|
b8a76bd6a9 | ||
|
|
07e1237f05 | ||
|
|
29546740c7 | ||
|
|
42cc96a28a | ||
|
|
84b07af246 | ||
|
|
8a72dbaf68 | ||
|
|
af6b997ceb | ||
|
|
e97dd392c0 | ||
|
|
ae0526a58a | ||
|
|
d419c1c2cb | ||
|
|
48e03b4237 | ||
|
|
2fad519b7e | ||
|
|
84b73b912f | ||
|
|
a3cbaaa2e7 | ||
|
|
881f3295c0 | ||
|
|
0cc9230672 | ||
|
|
010a8f3a5a | ||
|
|
4952e0daa1 | ||
|
|
455b39bb1e | ||
|
|
34772bd3b4 | ||
|
|
2296044280 | ||
|
|
2d201bfcde | ||
|
|
17931191f5 | ||
|
|
4e8289e976 | ||
|
|
41c56e026f | ||
|
|
1e8bf782ca | ||
|
|
e4eedf464b | ||
|
|
c274cb134d | ||
|
|
c0fe6cce48 | ||
|
|
9e89a37b4d | ||
|
|
a704e410c5 | ||
|
|
d24884f7e6 | ||
|
|
2cada9ba27 | ||
|
|
a7c203d7a4 | ||
|
|
abc07a5e2f | ||
|
|
904c615665 | ||
|
|
e9af59769d | ||
|
|
e933d64790 | ||
|
|
c07fe860e5 | ||
|
|
1b6dd3c40b | ||
|
|
7c82f940fc | ||
|
|
a69636d7e3 | ||
|
|
83f535c2e9 | ||
|
|
47bf0b3e77 | ||
|
|
e09cd3a77c | ||
|
|
5d521358f7 | ||
|
|
cfb2f42e96 | ||
|
|
cbbfb1c6ad | ||
|
|
86263bd235 | ||
|
|
fc9dc836e7 | ||
|
|
a508ede373 | ||
|
|
fd4da24b1d | ||
|
|
2f0acfc07f | ||
|
|
f8cf3618f7 | ||
|
|
0ad1ce98ad | ||
|
|
04c27a9b7f | ||
|
|
12fb7104a1 | ||
|
|
f5548f39da | ||
|
|
b16d0949a4 | ||
|
|
5dfc3573e5 | ||
|
|
7800499c72 | ||
|
|
045eef24c0 | ||
|
|
3a8a1f9c08 | ||
|
|
f55f4cedfe | ||
|
|
85b77aeb91 | ||
|
|
1b4ad64ef6 | ||
|
|
c7939b8923 | ||
|
|
a155a90fba | ||
|
|
31d9307fe1 | ||
|
|
86f56b4735 | ||
|
|
1f44211088 | ||
|
|
fcc1f3bec9 | ||
|
|
cbf30604af | ||
|
|
839dd8cf94 | ||
|
|
365aeded43 | ||
|
|
ad247fb269 | ||
|
|
1e0267c03d | ||
|
|
47a16c0c2b | ||
|
|
6dcf3de29e | ||
|
|
8974d00078 | ||
|
|
d0263e7252 | ||
|
|
0d2cb18745 | ||
|
|
00084d769a | ||
|
|
c1271d9c34 | ||
|
|
4d957ea271 | ||
|
|
9b4fb796f9 | ||
|
|
c4aefdb8b9 | ||
|
|
35b7f82ad9 | ||
|
|
25ad9b0350 | ||
|
|
f22ea522b8 | ||
|
|
9f317d9878 | ||
|
|
aaa46e7d3a | ||
|
|
11692c8537 | ||
|
|
d0901961be | ||
|
|
d8d1eec3ed | ||
|
|
0182616050 | ||
|
|
8da7056cd6 | ||
|
|
50d2497288 | ||
|
|
02b378612b | ||
|
|
90903a67ac | ||
|
|
a551893322 | ||
|
|
ea9d57cc65 | ||
|
|
e141545035 | ||
|
|
62cd597994 | ||
|
|
1eff2d2c0a | ||
|
|
2af7c12cf6 | ||
|
|
3a6034a460 | ||
|
|
9c9bf201c4 | ||
|
|
cb8389bb4a | ||
|
|
77ad126530 | ||
|
|
e152013e2a | ||
|
|
0c3ccb2806 | ||
|
|
eb2615c303 | ||
|
|
29b6120f80 | ||
|
|
90315a112a | ||
|
|
50f45a133b | ||
|
|
c57685065a | ||
|
|
8048c0ff0e | ||
|
|
10c2ca5bc3 | ||
|
|
f688aa210f | ||
|
|
b377c11c41 | ||
|
|
1fe49c24e8 | ||
|
|
27ca7ddf8e | ||
|
|
b6c5278282 | ||
|
|
6cf7bd766d | ||
|
|
e75fcd7bff | ||
|
|
eb14c5cb21 | ||
|
|
0097f768ef | ||
|
|
d6dee3361e | ||
|
|
9b83ba3176 | ||
|
|
2878f546ee | ||
|
|
e71b65d593 | ||
|
|
1d5925e178 | ||
|
|
45c5137565 | ||
|
|
3544b2af81 | ||
|
|
ea003ea73c | ||
|
|
dd483cb628 | ||
|
|
b89e5b4c6b | ||
|
|
a09ce94da1 | ||
|
|
1256334805 | ||
|
|
5ff450c3bc | ||
|
|
8bb6097d90 | ||
|
|
89506c222b | ||
|
|
0887ccde13 | ||
|
|
d68a35903c | ||
|
|
b05661e120 | ||
|
|
d40e3d563b | ||
|
|
f31713a929 | ||
|
|
f4b0a63e23 | ||
|
|
884fb8dc50 | ||
|
|
4abcc9689c | ||
|
|
154e9cc5c1 | ||
|
|
534e18bbd4 | ||
|
|
0df34bdb1c | ||
|
|
11d65410e6 | ||
|
|
f4a0222e0e | ||
|
|
022943d2af | ||
|
|
3a84394847 | ||
|
|
70717f88fb | ||
|
|
95319e51ff | ||
|
|
6a1a3adce5 | ||
|
|
aff0b7a080 | ||
|
|
9128c0e298 | ||
|
|
6453e8402e | ||
|
|
7ae0fecb61 | ||
|
|
c9f230a76f | ||
|
|
17692d6497 | ||
|
|
a9861967ef | ||
|
|
4d5bae0812 | ||
|
|
845a3b479d | ||
|
|
010c39e869 | ||
|
|
e9c2e2f0aa | ||
|
|
cbb01de3a2 | ||
|
|
6ed75bd08c | ||
|
|
3c73dc40dc | ||
|
|
4986455919 | ||
|
|
f4b4f4bafb | ||
|
|
2982741558 | ||
|
|
18e700bb6e | ||
|
|
0955d66931 | ||
|
|
39c3760161 | ||
|
|
b5ec4ccbcd | ||
|
|
c5c4687063 | ||
|
|
b7865acbe3 | ||
|
|
3a3a514825 | ||
|
|
7cdcb3a69a | ||
|
|
d7899d0dd5 | ||
|
|
88a383f85e | ||
|
|
e5ed1e213d | ||
|
|
46ee8ac4a6 | ||
|
|
5ea3823946 | ||
|
|
9d90c81f64 | ||
|
|
38bfc94125 | ||
|
|
261d1300e7 | ||
|
|
e0fde479ad | ||
|
|
1af861aa94 | ||
|
|
ad76b06e8a | ||
|
|
75bab70856 | ||
|
|
f65513b1cb | ||
|
|
ba0b47bec5 | ||
|
|
f2428d7fde | ||
|
|
07eb00c705 | ||
|
|
9907213dd8 | ||
|
|
89e288c591 | ||
|
|
75874ab46f | ||
|
|
333dd5d3c5 | ||
|
|
949038979e | ||
|
|
c61598010c | ||
|
|
f56e6ba6d1 | ||
|
|
13ac3a6543 | ||
|
|
c3de5727fe | ||
|
|
2774b7eab6 | ||
|
|
d54a8bfe60 | ||
|
|
8464616c37 | ||
|
|
be06d7b833 | ||
|
|
f7879485ef | ||
|
|
00c4f4062d | ||
|
|
8c52839c02 | ||
|
|
7a1b45895f | ||
|
|
d36b724d4f | ||
|
|
6e648a522a | ||
|
|
8b56e3da21 | ||
|
|
9fbb74ba60 | ||
|
|
e8f4f9939f | ||
|
|
fa60a79241 | ||
|
|
413daf37d6 | ||
|
|
e24b0b1dc8 | ||
|
|
df81c1e232 | ||
|
|
5bc74d874c | ||
|
|
98f0571d1f | ||
|
|
04ff892e86 | ||
|
|
83f644cb03 | ||
|
|
0c03bb9254 | ||
|
|
1280505297 | ||
|
|
1efdb06d3b | ||
|
|
88c356bdf2 | ||
|
|
060b684e57 | ||
|
|
c739f2e7b1 | ||
|
|
7f85d64167 | ||
|
|
6873daa5de | ||
|
|
1a17683897 | ||
|
|
5a6dcf33fa | ||
|
|
57ceddd9ee | ||
|
|
b79d8b8879 | ||
|
|
6d6d0e102b | ||
|
|
33f5c15e45 | ||
|
|
640f167e4b | ||
|
|
0351c561b5 | ||
|
|
b8c439d2d7 | ||
|
|
a11abdbb56 | ||
|
|
617984e587 | ||
|
|
b1f731fdde | ||
|
|
9899741c00 | ||
|
|
a53c1b4b49 | ||
|
|
3e3a5df34d | ||
|
|
2227a842ae | ||
|
|
3b1509edf0 | ||
|
|
72be27e3c1 | ||
|
|
35f74791c9 | ||
|
|
913a199d95 | ||
|
|
90514e2327 | ||
|
|
3e0f7f077a | ||
|
|
8cea6ccfbb | ||
|
|
c66ee30940 | ||
|
|
60427dbb4f | ||
|
|
a24ccd9cbe | ||
|
|
e547c561e3 | ||
|
|
1b878faf2b | ||
|
|
10c3a2326b | ||
|
|
55acfe7efe | ||
|
|
c4272bf1fe | ||
|
|
2e2d8e897b | ||
|
|
18716cf97f | ||
|
|
1a7bad29d1 | ||
|
|
3b3e18f22d | ||
|
|
d2161fe221 | ||
|
|
4895dcbcc4 | ||
|
|
6990f78f69 | ||
|
|
64d606a791 | ||
|
|
3b72aaa7c2 | ||
|
|
de7199a3df | ||
|
|
c8f2233e24 | ||
|
|
d18071e533 | ||
|
|
bc1d5d5337 | ||
|
|
9da0a095cb | ||
|
|
92bd3fdcdb | ||
|
|
021ca69e6b | ||
|
|
454ec6f865 | ||
|
|
97b24cfadf | ||
|
|
332788a77a | ||
|
|
a33ccf4180 | ||
|
|
1b9b843f1a | ||
|
|
82e27a0ce4 | ||
|
|
a3ac3f6535 | ||
|
|
c1d5f9eebf | ||
|
|
70d6dea264 | ||
|
|
891c3325c9 | ||
|
|
9709b938f2 | ||
|
|
79e3a5b902 | ||
|
|
6248909003 | ||
|
|
c88646b64c | ||
|
|
4f7b464bed | ||
|
|
9d03968bc1 | ||
|
|
9ab78a55c3 | ||
|
|
9f276657c3 | ||
|
|
aeb61936ce | ||
|
|
a3ea25c1e6 | ||
|
|
13d68f93d9 | ||
|
|
81c31547ac | ||
|
|
b5d53e0831 | ||
|
|
22bc8fdcbe | ||
|
|
c7c78e303c | ||
|
|
048e1269ad | ||
|
|
13128d282b | ||
|
|
498424cf27 | ||
|
|
67bf12c11f | ||
|
|
c658721333 | ||
|
|
906b3a554c | ||
|
|
f4a13e2a8e | ||
|
|
1f61ba7edd | ||
|
|
38b262a3f0 | ||
|
|
868ed7ca7e | ||
|
|
7e6760e769 | ||
|
|
62b91f0ce9 | ||
|
|
fad2a525ea | ||
|
|
0c20109254 | ||
|
|
84bf085453 | ||
|
|
57c99bd981 | ||
|
|
bcbe64807b | ||
|
|
198c8b452d | ||
|
|
1fec3b2584 | ||
|
|
7aa29bd398 | ||
|
|
1f6301c531 | ||
|
|
edc6e56c69 | ||
|
|
d584e68e3a | ||
|
|
e36eb4d422 | ||
|
|
3946e7a0e7 | ||
|
|
a3da6b8530 | ||
|
|
f3e8ef3f65 | ||
|
|
03920524d3 | ||
|
|
4958d3ca57 | ||
|
|
5f12c58cc8 | ||
|
|
6ccdfc0129 | ||
|
|
c089de52e7 | ||
|
|
166de1bf15 | ||
|
|
726c1dc16c | ||
|
|
a52be724c0 | ||
|
|
be1f310595 | ||
|
|
953f2f22a9 | ||
|
|
74579fa53e | ||
|
|
930e811232 | ||
|
|
6ca112f3e3 | ||
|
|
659d307a12 | ||
|
|
459035529c | ||
|
|
0ad2b03f2b | ||
|
|
bf9926f004 | ||
|
|
4166cb1d80 | ||
|
|
396d3888db |
@@ -33,4 +33,12 @@ docker network connect no-copy_app-network no-copy-frontend
|
||||
docker network inspect no-copy_app-network
|
||||
|
||||
docker volume ls - посмотреть все базы данных
|
||||
docker volume prune - удалить неиспользуемые базы данных
|
||||
docker volume prune - удалить неиспользуемые базы данных
|
||||
|
||||
Юкаса
|
||||
эндпоинт для бека.
|
||||
{URL}/api/payments/webhook/yookassa
|
||||
|
||||
https://dev-workspace.not-copy.com/api/payments/webhook/yookassa
|
||||
|
||||
https://mutely-profound-sandfish.cloudpub.ru/api/payments/webhook/yookassa
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
const { version } = require('./package.json');
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
@@ -7,6 +8,9 @@ const nextConfig: NextConfig = {
|
||||
bodySizeLimit: '10mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
APP_VERSION: version,
|
||||
},
|
||||
};
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
Generated
+1097
-732
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.36.0",
|
||||
"version": "0.120.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
@@ -9,17 +9,20 @@
|
||||
"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",
|
||||
"@tanstack/react-query": "^5.90.11",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@vkid/sdk": "^2.6.2",
|
||||
"card-validator": "^10.0.4",
|
||||
"clsx": "^2.1.1",
|
||||
"jose": "^6.1.2",
|
||||
"next": "^16.0.7",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"next-intl": "^4.5.8",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-slick": "^0.31.0",
|
||||
@@ -27,6 +30,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"
|
||||
@@ -38,6 +42,7 @@
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"sass": "^1.94.2",
|
||||
"types-yoomoneycheckoutwidget": "^0.0.2",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"buildTime": null
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,9 +1,11 @@
|
||||
import Providers from '@/app/providers/getQueryServer'
|
||||
import type { Metadata } from "next";
|
||||
import { NextIntlClientProvider, hasLocale } from 'next-intl';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { ToastProvider } from '@/app/providers/ToastProvider';
|
||||
import Providers from '@/app/providers/getQueryServer';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/global-styles.scss";
|
||||
|
||||
@@ -29,9 +31,9 @@ export default async function RootLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<html>
|
||||
<body>
|
||||
<NextIntlClientProvider>
|
||||
<NextIntlClientProvider locale={locale}>
|
||||
<Providers>
|
||||
{children}
|
||||
<ToastProvider />
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
|
||||
export default function YandexCallback() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams.get('code');
|
||||
const error = searchParams.get('error');
|
||||
|
||||
if (error) {
|
||||
console.error('Ошибка авторизации:', error);
|
||||
router.push('/login?error=yandex_denied');
|
||||
return;
|
||||
}
|
||||
|
||||
if (code) {
|
||||
console.log('получаю код');
|
||||
console.log(code);
|
||||
fetch('/api/auth/yandex/callback', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
console.log(data);
|
||||
console.log('all ok');
|
||||
/* router.push('/dashboard'); */
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [searchParams, router]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles['login-container-wrapper']}`}
|
||||
>
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner large"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import VKLogin from '@/app/components/VKLogin';
|
||||
import YandexAuthWithScript from '@/app/components/YandexLogin';
|
||||
import { getHealt } from '@/app/actions/action';
|
||||
import { Suspense } from 'react';
|
||||
import {YandexLoginButton} from '@/app/components/YandexLoginButton';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Login',
|
||||
@@ -15,57 +18,69 @@ export const metadata: Metadata = {
|
||||
export default function Page() {
|
||||
const t = useTranslations('Login-register-form');
|
||||
|
||||
async function HealthData() {
|
||||
const data = await getHealt();
|
||||
if (!data) {
|
||||
return <div>Сервер временно не работает</div>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
{/* <div className={`${styles['login-container-language']}`}>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<HealthData />
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
{/* <div className={`${styles['login-container-language']}`}>
|
||||
<LanguageSwitcher />
|
||||
</div> */}
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
<h1>NO COPY</h1>
|
||||
<p>
|
||||
{t('copyright-protection')}
|
||||
</p>
|
||||
</div>
|
||||
<LoginForm />
|
||||
<div className="divider">
|
||||
<span>
|
||||
{t('or-log-in-via')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<VKLogin />
|
||||
</div>
|
||||
<div>
|
||||
<YandexAuthWithScript />
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
<Link href="register"> {t('register')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${styles['features']}`}>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{t('protecting-watermark')}
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
<h1>NO COPY</h1>
|
||||
<p>
|
||||
{t('copyright-protection')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
{t('monitoring-violations')}
|
||||
<LoginForm />
|
||||
<div className="divider">
|
||||
<span>
|
||||
{t('or-log-in-via')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path>
|
||||
</svg>
|
||||
{t('safe-storage')}
|
||||
<div className="mb-4">
|
||||
<VKLogin />
|
||||
</div>
|
||||
<div>
|
||||
<YandexLoginButton />
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
<Link href="register"> {t('register')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${styles['features']}`}>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{t('protecting-watermark')}
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
{t('monitoring-violations')}
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path>
|
||||
</svg>
|
||||
{t('safe-storage')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import DashboardFilesInfo from '@/app/ui/dashboard/new/dashboard-files-info';
|
||||
import DahboardLimitsSection from '@/app/ui/dashboard/new/dashborad-limits-section';
|
||||
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
|
||||
import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check';
|
||||
import ViolationsCarousel from '@/app/ui/dashboard/new/violations-carousel';
|
||||
import DashboardUserViolations from '@/app/ui/dashboard/new/dashboard-user-violations';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard',
|
||||
@@ -24,13 +24,16 @@ export default function Page() {
|
||||
<PageTitle title="statistics" />
|
||||
<DashboardUserStats />
|
||||
<div className="dashboard-main-grid">
|
||||
{/* <DashboardUserFiles />
|
||||
<DashboardUserViolations /> */}
|
||||
|
||||
<div className="left-column">
|
||||
<DashboardUserFiles />
|
||||
{/* <DashboardPlatformsList /> */}
|
||||
</div>
|
||||
<div className="right-column">
|
||||
<ViolationsCarousel />
|
||||
<DashboardUserViolations />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<DashboardFilesInfo />
|
||||
<div className="dashboard-main-grid">
|
||||
@@ -40,7 +43,7 @@ export default function Page() {
|
||||
<div
|
||||
className="mb-4"
|
||||
>
|
||||
<DahboardLimitsSection />
|
||||
{/* <DahboardLimitsSection /> */}
|
||||
</div>
|
||||
<DashboardLastCheck />
|
||||
</Suspense>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import PopularAskedQuestions from '@/app/ui/faq/popular-asked-questions';
|
||||
import FrequentlyAskedQuestions from '@/app/ui/faq/frequently-asked-questions';
|
||||
import FaqContact from '@/app/ui/faq/faq-contact';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="frequently-asked-questions" />
|
||||
<PopularAskedQuestions />
|
||||
<FrequentlyAskedQuestions />
|
||||
<FaqContact />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// app/files/[id]/page.tsx
|
||||
'use server'
|
||||
|
||||
import FilePageTitle from '@/app/ui/file-page/file-page-title';
|
||||
import FilePageViolationInfo from '@/app/ui/file-page/file-page-violation-info';
|
||||
import ViolationPpageFileStatistic from '@/app/ui/file-page/file-page-file-statistic';
|
||||
import FilePageFileInfo from '@/app/ui/file-page/file-page-file-info';
|
||||
import FilePageActions from '@/app/ui/file-page/file-page-actions';
|
||||
import FilePageViolationsList from '@/app/ui/file-page/violation-table/file-page-violations-list';
|
||||
import FilePageNote from '@/app/ui/file-page/file-page-note';
|
||||
import { viewFileInfo } from '@/app/actions/fileEntity';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import FilePageNotFound from '@/app/ui/file-page/file-page-file-not-found';
|
||||
|
||||
type MonitoringStatus = 'NONE' | 'MONITORING_DAILY' | 'MONITORING_WEEKLY' | 'MONITORING_MONTHLY';
|
||||
type MimeType = 'image' | 'video' | 'audio' | 'document';
|
||||
|
||||
export interface FileDetails {
|
||||
id: string;
|
||||
userId: number;
|
||||
originalFileName: string;
|
||||
storedFileName: string;
|
||||
filePath: string;
|
||||
protectedFilePath: string;
|
||||
fileSize: number;
|
||||
mimeType: MimeType;
|
||||
fileExtension: string;
|
||||
checksum: string;
|
||||
uploadSessionId: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
formattedSize: string;
|
||||
downloadUrl: string;
|
||||
existsOnDisk: boolean;
|
||||
supportId: number | null;
|
||||
protectStatus: string;
|
||||
ownerName: string;
|
||||
ownerEmail: string;
|
||||
ownerCompany: string | null;
|
||||
fileName: string;
|
||||
fileFormat: string | null;
|
||||
checksCount: number;
|
||||
fileUploadDate: string;
|
||||
monitoring: MonitoringStatus;
|
||||
thumbnailFileUrl: string;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ page?: string, status?: string, violationId?: string, caseType?: 'complaint' | 'claim', startDate: string, endDate: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
searchParams
|
||||
}: PageProps) {
|
||||
const { id } = await params;
|
||||
const { page, status, violationId, caseType, startDate, endDate } = await searchParams;
|
||||
const currentPage = Number(page) || 1;
|
||||
|
||||
try {
|
||||
const fileInfo: FileDetails = await viewFileInfo(id);
|
||||
|
||||
return (
|
||||
<div className="violation-details">
|
||||
<FilePageTitle fileInfo={fileInfo} />
|
||||
<div>
|
||||
<div
|
||||
className="grid grid-cols-[2fr_1fr] gap-3"
|
||||
>
|
||||
<FilePageFileInfo fileInfo={fileInfo} />
|
||||
{/* <FilePageActions /> */}
|
||||
<ViolationPpageFileStatistic id={id} />
|
||||
</div>
|
||||
<div>
|
||||
<FilePageViolationsList
|
||||
currentPage={currentPage}
|
||||
status={status}
|
||||
fileId={id}
|
||||
violationId={violationId}
|
||||
caseType={caseType}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
{/* <FilePageNote /> */}
|
||||
{/* <FilePageViolationInfo /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
return (
|
||||
<FilePageNotFound />
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,31 +3,16 @@ import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "document";
|
||||
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
|
||||
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
try {
|
||||
const data = await queryClient.fetchQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: fetchReferralUserStats,
|
||||
});
|
||||
if (data?.referralLink) {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitleColorFrame
|
||||
title="audio-protection"
|
||||
description="multi-layered-protection-for-your-audios"
|
||||
title="document-protection"
|
||||
description="multi-layered-protection-for-your-document"
|
||||
/>
|
||||
<ProtectionStatistic fileType={FILE_TYPE} />
|
||||
<UploadSectionFile
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import { NotificationsList } from '@/app/ui/notifications/notifications-list';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title='notifications' />
|
||||
|
||||
<NotificationsList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import UserUsage from '@/app/ui/payment/user-usage';
|
||||
import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats';
|
||||
import PaymentTabs from '@/app/ui/payment/payment-tabs';
|
||||
import SecurePayments from '@/app/ui/payment/secure-payments';
|
||||
import PaymentUserCards from '@/app/ui/payment/payment-user-cards';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'payment',
|
||||
@@ -22,6 +23,7 @@ export default function Page() {
|
||||
<UserUsage />
|
||||
<TokensSpendStats />
|
||||
<PaymentTabs />
|
||||
<PaymentUserCards />
|
||||
<SecurePayments />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Suspense } from 'react';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import ReferralStats from '@/app/ui/referral-page/referral-stats';
|
||||
import ReferralLinkSection from '@/app/ui/referral-page/referral-link-section';
|
||||
import CommissionLevels from '@/app/ui/referral-page/commission-levels';
|
||||
import InvitationsTable from '@/app/ui/referral-page/invitations-table';
|
||||
import InvitationsTable from '@/app/ui/referral-page/table/invitations-table';
|
||||
import IncomeAndPayments from '@/app/ui/referral-page/Income-and-payments';
|
||||
import SettingReferralProgram from '@/app/ui/referral-page/setting-referral-program';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import StatsGrid from '@/app/ui/dashboard/stats-grid';
|
||||
import ViolationsTable from '@/app/ui/dashboard/violations-table';
|
||||
import ViolationsTable from '@/app/ui/violations/violations-table';
|
||||
import FinalSummary from '@/app/ui/reports/final-summary';
|
||||
import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month';
|
||||
import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography';
|
||||
import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-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';
|
||||
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="reports-and-analytics" />
|
||||
<StatsGrid />
|
||||
<ReportsInfo />
|
||||
<ActivityByContentType />
|
||||
<div className="analytics-grid">
|
||||
<AnalyticsCardMonth />
|
||||
{/* <AnalyticsCardMonth /> */}
|
||||
<AnalyticsCardDistribution />
|
||||
<AnalyticsCardGeography />
|
||||
</div>
|
||||
<ViolationsTable />
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
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';
|
||||
export default function Page() {
|
||||
import SearchTitle from '@/app/ui/search/search-title';
|
||||
|
||||
export default async function Page() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="block-wrapper">
|
||||
<h1 className="page-title">
|
||||
Поиск защищенного контента
|
||||
</h1>
|
||||
<p className="page-description">
|
||||
Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете
|
||||
</p>
|
||||
</div>
|
||||
<SearchTitle />
|
||||
<div className="search-grid">
|
||||
<SectionSearchFile
|
||||
maxFileSize={1024000000}
|
||||
allowedExtensions={['jpg', 'jpeg', 'png', 'gif', 'bmp']}
|
||||
fileType="image"
|
||||
/>
|
||||
<SectionSearchFile />
|
||||
<div className="search-sidebar">
|
||||
<SupportedFormats />
|
||||
<SearchStats />
|
||||
<RecentSearches />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import SettingUserVerification from '@/app/ui/settings/setting-user-verification';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Global');
|
||||
@@ -21,8 +22,10 @@ export default function Page() {
|
||||
<SafetySetting />
|
||||
<SubscriptionSettings />
|
||||
{/* <APISettings /> */}
|
||||
<LastActivitySettings />
|
||||
{/* LastActivitySettings возможно нужно будет вернуть */}
|
||||
{/* <LastActivitySettings /> */}
|
||||
<CompanyUsersSettingPanel />
|
||||
<SettingUserVerification />
|
||||
</div>
|
||||
<DangerZone />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
'use server'
|
||||
|
||||
import { TrackingStatistic } from '@/app/ui/tracking-page/tracking-statistic';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import { TrackingHistoryView } from '@/app/ui/tracking-page/tracking-history-view';
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "document";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tracking-page"
|
||||
>
|
||||
<TrackingStatistic />
|
||||
<FilesTable fileType={FILE_TYPE} showFileLink={true} />
|
||||
<TrackingHistoryView />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
|
||||
import DashboardPlatformsList from '@/app/ui/dashboard/new/dashboard-platforms-list';
|
||||
import ViolationsMyCases from '@/app/ui/violations/violations-my-cases';
|
||||
import ViolationsMyComplaint from '@/app/ui/violations/violations-my-complaint';
|
||||
import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section';
|
||||
import ViolationsTable from '@/app/ui/violations/violations-table';
|
||||
import ViolationsStatistic from '@/app/ui/violations/violations-statistic';
|
||||
export default function Page() {
|
||||
const FILE_TYPE = "all";
|
||||
import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
|
||||
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
|
||||
import ViolationsMyClaims from '@/app/ui/violations/violations-my-claims';
|
||||
import ViolationsLastCombinedCases from '@/app/ui/violations/violations-last-combined-cases';
|
||||
import { ViolationsMyComplaintsTable } from '@/app/ui/violations/tables/violations-my-complaints-table';
|
||||
import { ViolationsMyClaimsTable } from '@/app/ui/violations/tables/violations-my-claims-table';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitleColorFrame
|
||||
@@ -15,12 +18,18 @@ export default function Page() {
|
||||
description="monitoring-copyright-infringements-description"
|
||||
/>
|
||||
<ViolationsStatistic />
|
||||
<ViolationsMyCases />
|
||||
<ViolationsCheckAllSection />
|
||||
<ViolationsTable />
|
||||
<div className="dashboard-main-grid">
|
||||
<DashboardPlatformsList />
|
||||
<DahboardGeographySection />
|
||||
<ViolationsLastCombinedCases />
|
||||
{/* <ViolationsMyComplaint />
|
||||
<ViolationsMyClaims /> */}
|
||||
<div className="analytics-grid">
|
||||
<AnalyticsCardDistribution />
|
||||
<AnalyticsCardGeography />
|
||||
</div>
|
||||
<div className="analytics-grid">
|
||||
<ViolationsMyClaimsTable />
|
||||
<ViolationsMyComplaintsTable />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { PdfProvider } from '@/app/contexts/PdfContext';
|
||||
import { WatchDocProviders } from '@/app/providers/WatchDocProviders';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<WatchDocProviders>
|
||||
<PdfProvider>
|
||||
{children}
|
||||
</PdfProvider>
|
||||
</WatchDocProviders>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
'use server';
|
||||
|
||||
import Link from 'next/link';
|
||||
import PdfViewer from '@/app/components/PdfViewer';
|
||||
import { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
|
||||
import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
|
||||
import { fetchDocumentInfo } from '@/app/actions/trackingActions';
|
||||
import { LoginUserBage } from '@/app/components/document-viewer/LoginUserBage';
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ docid: string; title?: string }>;
|
||||
}
|
||||
|
||||
interface DocumentInfo {
|
||||
checksCount?: number;
|
||||
checksum?: string;
|
||||
createdAt?: string;
|
||||
downloadUrl?: string;
|
||||
existsOnDisk?: boolean;
|
||||
fileExtension?: string;
|
||||
fileFormat?: string | null;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
fileSize?: number;
|
||||
fileUploadDate?: string;
|
||||
formattedSize?: string;
|
||||
id: string;
|
||||
mimeType?: string;
|
||||
monitoring?: string;
|
||||
originalFileName?: string;
|
||||
ownerCompany?: string | null;
|
||||
ownerEmail?: string;
|
||||
ownerName?: string;
|
||||
permissions?: {
|
||||
DOWNLOAD?: boolean;
|
||||
};
|
||||
protectStatus?: string;
|
||||
protectedFilePath?: string;
|
||||
status?: string;
|
||||
storedFileName?: string;
|
||||
supportId?: number;
|
||||
thumbnailFileUrl?: string;
|
||||
updatedAt?: string;
|
||||
uploadSessionId?: string;
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const { docid, title } = await searchParams;
|
||||
|
||||
let response: DocumentInfo | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
response = await fetchDocumentInfo(docid);
|
||||
} catch (err) {
|
||||
error = 'Не удалось загрузить информацию о документе';
|
||||
}
|
||||
|
||||
const documentTitle = response?.fileName || 'FileName';
|
||||
|
||||
if (error || !response) {
|
||||
return (
|
||||
<div className="watch-doc-wrapper">
|
||||
<div className="watch-doc-page">
|
||||
<header className="watch-doc-header">
|
||||
<Link href="#" className="header-logo">NoCopy</Link>
|
||||
<LoginUserBage />
|
||||
</header>
|
||||
<div className="error-container">
|
||||
<h2>Ошибка загрузки документа</h2>
|
||||
<p>{error || 'Документ не найден'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="watch-doc-wrapper">
|
||||
<div className="watch-doc-page">
|
||||
<header className="watch-doc-header">
|
||||
<Link href="#" className="header-logo">NoCopy</Link>
|
||||
<LoginUserBage />
|
||||
</header>
|
||||
|
||||
<div className="watch-doc-status">
|
||||
<div className="st-title">Защита подтверждена</div>
|
||||
<div className="st-sub">
|
||||
Документ <strong>{response?.fileName ? response?.fileName : '#'}</strong> зарегистрирован и защищён платформой NoCopy.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-grid">
|
||||
<div
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<div className="watch-doc-card">
|
||||
<div className="card-head">Правообладатель</div>
|
||||
<div className="card-body">
|
||||
<div className="owner-row">
|
||||
<div className="owner-av">
|
||||
{response?.fileName ? response?.fileName[0] : ''}
|
||||
</div>
|
||||
<div>
|
||||
{response?.ownerName && (
|
||||
<div className="owner-name">{response?.ownerName}</div>
|
||||
)}
|
||||
{response?.ownerCompany && (
|
||||
<div className="owner-name">{response?.ownerCompany}</div>
|
||||
)}
|
||||
{response?.ownerName && (
|
||||
<div className="owner-meta">Зарегистрирован в NoCopy</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-actions">
|
||||
{response?.permissions?.DOWNLOAD && (
|
||||
<DownloadButton fileId={docid} fileName={response?.fileName}/>
|
||||
)}
|
||||
<CopyLinkButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="watch-doc-card">
|
||||
<div className="card-head">Предпросмотр</div>
|
||||
<div className="card-body">
|
||||
<PdfViewer
|
||||
fileId={docid}
|
||||
fileName={documentTitle}
|
||||
showDownload={response.permissions?.DOWNLOAD}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-footer">
|
||||
<Link href="#">NoCopy</Link>
|
||||
<Link href="/terms-of-use">Условия</Link>
|
||||
<Link href="/privacy-policy">Конфиденциальность</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function getSearchSettings() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/search/settings`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.message_body) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function putSearchSettings(proxy: boolean, yandex: boolean, google: boolean) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/search/settings`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
engines: {
|
||||
yandex: yandex,
|
||||
google: google
|
||||
},
|
||||
proxyEnabled: proxy
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.message_desc === 'Settings updated') {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+128
-9
@@ -2,7 +2,6 @@
|
||||
|
||||
import { getSessionData, deleteSession } from '@/app/actions/session';
|
||||
import { testUserData, API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export async function getUserData() {
|
||||
const userEmail = await getSessionData('email');
|
||||
@@ -26,9 +25,11 @@ export async function getUserData() {
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error');
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +44,12 @@ export async function getUserFilesInfo() {
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'user_files_info',
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,13 +62,15 @@ export async function getUserFilesInfo() {
|
||||
} else {
|
||||
throw parsed.message_code;
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`error: ${error}`);
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTariffs() {
|
||||
export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
@@ -78,12 +81,13 @@ export async function fetchTariffs() {
|
||||
msg_id: 30001,
|
||||
message_body: {
|
||||
action: 'get',
|
||||
user_token: token
|
||||
tariff_term: tariffTerm
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,8 +99,123 @@ export async function fetchTariffs() {
|
||||
return []
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error');
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTokensBundle() {
|
||||
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: 'tariffs_by_type',
|
||||
type: 'token'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed.message_body?.token_tariffs;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBuildData() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check/api/build`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
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/auth`, {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHealt() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check/api/healt`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+223
-73
@@ -3,6 +3,8 @@
|
||||
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';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function logout() {
|
||||
const token = await getSessionData('token');
|
||||
@@ -70,7 +72,7 @@ export async function authorization(
|
||||
|
||||
try {
|
||||
const { email, password } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -87,6 +89,7 @@ export async function authorization(
|
||||
});
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.token, email);
|
||||
} else {
|
||||
@@ -130,16 +133,18 @@ export async function authorization(
|
||||
|
||||
export type FormState = {
|
||||
previousState: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
phone: string;
|
||||
companyName?: string;
|
||||
agree: string;
|
||||
referralCode?: string
|
||||
},
|
||||
fullName?: string | undefined;
|
||||
email?: string | undefined;
|
||||
password?: string | undefined;
|
||||
confirm_password?: string | undefined;
|
||||
phone?: string | undefined;
|
||||
companyName?: string | undefined;
|
||||
agree?: string | undefined;
|
||||
referralCode?: string | undefined;
|
||||
inn?: string | undefined;
|
||||
} | undefined,
|
||||
mailConfirm?: boolean,
|
||||
userId?: number,
|
||||
error: Record<string, string> | null
|
||||
};
|
||||
|
||||
@@ -147,8 +152,11 @@ export async function registration(
|
||||
state: FormState | undefined,
|
||||
formData: FormData,
|
||||
): Promise<FormState> {
|
||||
const headersList = await headers();
|
||||
const accountType = formData.get('accountType') as 'b2b' | 'b2c';
|
||||
|
||||
/* вернуть в true когда проект выйдет из дева */
|
||||
const mail_verified = false;
|
||||
const fullName = formData.get('fullName') as string || '';
|
||||
const email = formData.get('email') as string || '';
|
||||
const password = formData.get('password') as string || '';
|
||||
@@ -156,7 +164,14 @@ 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 referralCode = formData.get('referralCode') as string || '';
|
||||
const inn = formData.get('inn') as string || '';
|
||||
|
||||
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
const SignupFormSchema = await getSignupFormSchema(accountType);
|
||||
|
||||
@@ -167,25 +182,41 @@ export async function registration(
|
||||
password,
|
||||
confirm_password,
|
||||
agree,
|
||||
...(accountType === 'b2b' && { companyName })
|
||||
...(accountType === 'b2b' && { companyName, inn })
|
||||
};
|
||||
|
||||
const validatedFields = await SignupFormSchema.safeParse(dataToValidate);
|
||||
|
||||
console.log(`try to register ${accountType}`);
|
||||
|
||||
if (!validatedFields.success) {
|
||||
let confirmedCompanyName: boolean = true;
|
||||
if (accountType === 'b2b') {
|
||||
try {
|
||||
const response = await fetchINN(inn);
|
||||
confirmedCompanyName = response?.companyName ? true : false;
|
||||
} catch (error) {
|
||||
confirmedCompanyName = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validatedFields.success || !confirmedCompanyName) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
if (validatedFields?.error?.message) {
|
||||
JSON?.parse(validatedFields?.error?.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!confirmedCompanyName) {
|
||||
errors.inn = 'inn-invalid'
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
@@ -195,7 +226,8 @@ export async function registration(
|
||||
confirm_password,
|
||||
phone,
|
||||
agree,
|
||||
...(accountType === 'b2b' && { companyName })
|
||||
...(referralCode !== '' && { referralCode }),
|
||||
...(accountType === 'b2b' && { companyName, inn })
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
@@ -203,7 +235,7 @@ export async function registration(
|
||||
|
||||
try {
|
||||
const { fullName, email, password, phone } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -214,7 +246,10 @@ export async function registration(
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
...(accountType === 'b2b' && { companyName: companyName }),
|
||||
mail_verified: mail_verified,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||
...(referralCode !== '' && { referralLink: referralCode })
|
||||
}
|
||||
}),
|
||||
@@ -230,19 +265,19 @@ export async function registration(
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
...(accountType === 'b2b' && { companyName: companyName }),
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
...(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 },
|
||||
message_body: { token: string, userId: number, fieldErrors: string },
|
||||
message_code: number
|
||||
};
|
||||
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
console.log(`registration - ${accountType}`);
|
||||
console.log({
|
||||
@@ -251,10 +286,30 @@ export async function registration(
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
referralLink: 'fds324',
|
||||
...(accountType === 'b2b' && { companyName: companyName })
|
||||
...(referralCode !== '' && { referralLink: referralCode }),
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
|
||||
});
|
||||
await createSession(parsed.message_body.token, email as string);
|
||||
|
||||
if (mail_verified) {
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
agree,
|
||||
...(referralCode !== '' && { referralLink: referralCode }),
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
|
||||
},
|
||||
mailConfirm: true,
|
||||
userId: parsed.message_body.userId,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
await createSession(parsed.message_body.token, email as string);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
@@ -262,19 +317,6 @@ export async function registration(
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
/* return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree
|
||||
},
|
||||
mailConfirm: true,
|
||||
error: null
|
||||
}; */
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
if (error) {
|
||||
@@ -302,6 +344,8 @@ export async function registration(
|
||||
}
|
||||
});
|
||||
|
||||
} else if (typedError.message_desc === 'Company already exists') {
|
||||
errors['inn'] = 'company-already-registered'
|
||||
} else {
|
||||
errors['server'] = 'unknown-error'
|
||||
}
|
||||
@@ -320,8 +364,10 @@ export async function registration(
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree
|
||||
agree,
|
||||
inn
|
||||
},
|
||||
mailConfirm: false,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
@@ -334,7 +380,8 @@ export async function registration(
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree
|
||||
agree,
|
||||
inn
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
@@ -354,13 +401,11 @@ export async function tokenLifeExtension() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20008,
|
||||
message_body: {
|
||||
token: token
|
||||
}
|
||||
msg_id: 20008
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -376,24 +421,110 @@ export async function tokenLifeExtension() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function confirmEmail(
|
||||
state: any | undefined,
|
||||
formData: any,
|
||||
) {
|
||||
export async function confirmEmail(userId: number, verifyToken: string) {
|
||||
console.log('confirmEmail');
|
||||
const emailConfirm = formData.get('emailConfirm') as string || '';
|
||||
console.log(emailConfirm);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
'Authorization': `Bearer ${verifyToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20009,
|
||||
message_body: {
|
||||
resend: 0,
|
||||
user_id: userId
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.token, parsed.message_body.email as string);
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'entered-code-is-incorrect'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'server-error'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'network-error'
|
||||
};
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
}
|
||||
|
||||
export async function resendConfirmEmail(userId: number) {
|
||||
console.log('resendConfirmEmail');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20009,
|
||||
message_body: {
|
||||
resend: 1,
|
||||
user_id: userId,
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
fine: true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
fine: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type FormStateConfirmPassword = {
|
||||
previousState: {
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
verifyToken: string;
|
||||
},
|
||||
error: Record<string, string> | null
|
||||
};
|
||||
|
||||
export async function resetPassword(
|
||||
state: any | undefined,
|
||||
formData: FormData,
|
||||
) {
|
||||
const email = formData.get('email');
|
||||
console.log('resetPassword');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -427,15 +558,6 @@ export async function resetPassword(
|
||||
redirect(`/reset-password?email=${email}`);
|
||||
}
|
||||
|
||||
export type FormStateConfirmPassword = {
|
||||
previousState: {
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
verifyToken: string;
|
||||
},
|
||||
error: Record<string, string> | null
|
||||
};
|
||||
|
||||
export async function confirmPassword(
|
||||
state: FormStateConfirmPassword | undefined,
|
||||
@@ -486,6 +608,7 @@ export async function confirmPassword(
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${verifyToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -493,7 +616,6 @@ export async function confirmPassword(
|
||||
message_body: {
|
||||
email: email,
|
||||
password: password,
|
||||
verifyToken: verifyToken,
|
||||
action: "confirmVerification"
|
||||
}
|
||||
}),
|
||||
@@ -506,16 +628,35 @@ export async function confirmPassword(
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.authToken, email as string);
|
||||
} else {
|
||||
throw parsed.message_body;
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const typedError = error as any;
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
|
||||
switch (typedError.message_code) {
|
||||
case 1:
|
||||
errors['server'] = 'unknown-error'
|
||||
break
|
||||
|
||||
case 2:
|
||||
if (typedError.message_desc === 'Token not found or time expired') {
|
||||
errors['server'] = 'token-not-found-or-time-expired'
|
||||
} else {
|
||||
errors['server'] = 'unknown-error'
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errors['server'] = 'request-ended-with-an-error'
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
email,
|
||||
@@ -526,7 +667,16 @@ export async function confirmPassword(
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
verifyToken
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export type CompanyUserRegistrationFormState = {
|
||||
previousState: {
|
||||
@@ -24,6 +25,8 @@ export async function companyUserRegistration(
|
||||
return;
|
||||
}
|
||||
|
||||
const headersList = await headers();
|
||||
|
||||
const fullName = formData.get('fullName') as string || '';
|
||||
const email = formData.get('email') as string || '';
|
||||
const password = formData.get('password') as string || '';
|
||||
@@ -32,6 +35,11 @@ export async function companyUserRegistration(
|
||||
|
||||
const SignupFormSchema = await getSignupFormSchema('b2c');
|
||||
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
const dataToValidate = {
|
||||
fullName,
|
||||
email,
|
||||
@@ -87,12 +95,15 @@ export async function companyUserRegistration(
|
||||
phone: phone,
|
||||
password,
|
||||
accountType: 'b2b',
|
||||
authToken: token
|
||||
mail_verified: false,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,14 +113,6 @@ export async function companyUserRegistration(
|
||||
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}`);
|
||||
@@ -117,7 +120,9 @@ export async function companyUserRegistration(
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password
|
||||
password,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as z from 'zod'
|
||||
import {createValidationSchema, createValidationSchemaForPassword} 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 =
|
||||
@@ -83,6 +83,30 @@ export const loginFormSchema = z
|
||||
.trim(),
|
||||
})
|
||||
|
||||
|
||||
export const createComplaintSchema = z
|
||||
.object({
|
||||
textArea: z
|
||||
.string()
|
||||
.min(1, { error: 'text-area-error-fill-complaint-text' })
|
||||
.max(2000, {error: 'text-area-error-complaint-text-too-long'})
|
||||
.trim(),
|
||||
})
|
||||
|
||||
export const createClaimSchema = z
|
||||
.object({
|
||||
textArea: z
|
||||
.string()
|
||||
.min(1, { error: 'fill-text-of-the-case' })
|
||||
.max(2000, {error: 'claim-text-must-not-exceed-2000-characters'})
|
||||
.trim(),
|
||||
caseTitle: z
|
||||
.string()
|
||||
.min(1, { error: 'fill-title-of-the-case' })
|
||||
.max(300, {error: 'claim-title-must-not-exceed-300-characters'})
|
||||
.trim(),
|
||||
})
|
||||
|
||||
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
|
||||
|
||||
export const API_BASE_URL = process.env.NODE_ENV === 'development'
|
||||
@@ -93,4 +117,12 @@ export const testUserData = {
|
||||
fullName: 'test',
|
||||
email: 'test@mail.com',
|
||||
subscriptionType: 'base'
|
||||
}
|
||||
}
|
||||
|
||||
export const bankCardFormSchema = z
|
||||
.object({
|
||||
bankCard: z
|
||||
.string()
|
||||
.min(19, { error: 'error-bank-card' })
|
||||
.trim()
|
||||
})
|
||||
@@ -4,7 +4,23 @@ import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
|
||||
export async function getUserFilesData(page: number, pageSize: number) {
|
||||
export async function getUserFilesData({
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
type,
|
||||
date,
|
||||
query = ''
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc' | string;
|
||||
type?: string;
|
||||
date?: string;
|
||||
query?: string;
|
||||
}) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
@@ -14,31 +30,37 @@ export async function getUserFilesData(page: number, pageSize: number) {
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'user_files',
|
||||
action: 'search_files',
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
token: token
|
||||
query: query,
|
||||
sort_by: sortBy || '', // protectStatus, fileName, fileSize, createdAt, updatedAt, supportId
|
||||
type: type || '', // image, video, document, audio
|
||||
date: date || '', // today, week, month
|
||||
sort_order: sortOrder
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
throw parsed.message_text ? parsed.message_text : 'error';
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
throw `${response.status}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +77,12 @@ export async function removeUserFile(fileId: string, fullDelete: number) {
|
||||
action: 'delete_file',
|
||||
file_id: fileId,
|
||||
full_delete: fullDelete,
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -92,13 +114,13 @@ export async function viewFileInfo(fileId: string) {
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'file_info',
|
||||
file_id: fileId,
|
||||
token: token
|
||||
file_id: fileId
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -114,6 +136,69 @@ export async function viewFileInfo(fileId: string) {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFile(fileId: string, fileName: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/download/${fileId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw 'failed-to-download-file';
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const base64 = Buffer.from(arrayBuffer).toString('base64')
|
||||
|
||||
return {
|
||||
data: base64,
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
fileName: fileName
|
||||
}
|
||||
}
|
||||
|
||||
export async function FileAnAppeal(fileId: string, appealReason: string, additionalInfo: 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: 'submit_appeal',
|
||||
file_id: fileId,
|
||||
appeal_reason: appealReason,
|
||||
additional_info: additionalInfo
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
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,
|
||||
@@ -11,17 +9,21 @@ interface initMessageBody {
|
||||
extension: string,
|
||||
file_size: number | string,
|
||||
action?: string,
|
||||
token?: string
|
||||
token?: string,
|
||||
convertTo?: string | undefined | null
|
||||
}
|
||||
|
||||
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.token = token;
|
||||
|
||||
if (convertTo && convertTo !== 'reject') {
|
||||
message.convertTo = convertTo;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
@@ -33,7 +35,8 @@ export async function fileUpload(messageBody: initMessageBody) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -63,6 +66,8 @@ export async function fileUpload(messageBody: initMessageBody) {
|
||||
}
|
||||
|
||||
export async function cancelUpload(udloadId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -76,7 +81,8 @@ export async function cancelUpload(udloadId: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,12 +114,27 @@ export type ChunkResponse = {
|
||||
fileId?: string
|
||||
};
|
||||
|
||||
export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
|
||||
export async function chunkUpload(formData: FormData, itPrivate?: boolean): Promise<ChunkResponse> {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, {
|
||||
const url = itPrivate
|
||||
? `${API_BASE_URL}/api/v1/private/files/chunk`
|
||||
: `${API_BASE_URL}/api/v1/files/chunk`;
|
||||
|
||||
|
||||
/* if (token && itPrivate) {
|
||||
formData.append('token', token);
|
||||
} */
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
/* ...(token && itPrivate && { 'Authorization': `Bearer ${token}` }) возможно нужно будет условно отправлять токен*/
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
@@ -124,25 +145,27 @@ export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
|
||||
fileId: parsed.message_body.file_id
|
||||
}
|
||||
} else if (parsed.message_desc === 'Duplicate file upload') {
|
||||
throw 'duplicate-file'
|
||||
throw 'error-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 if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: Failed to upload chunk: .wav file not have RIFF header') {
|
||||
throw 'error-upload-chunk-wav-file-not-have-riff'
|
||||
} else {
|
||||
throw 'error'
|
||||
throw 'error-uploading-file'
|
||||
}
|
||||
} else {
|
||||
throw 'error'
|
||||
throw 'error-uploading-file'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
const typedError = error as string;
|
||||
|
||||
return {
|
||||
errorMesage: typedError
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
errorMesage: 'error'
|
||||
errorMesage: 'error-uploading-file'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +173,8 @@ export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
|
||||
}
|
||||
|
||||
export async function checkChunkStatus(upload_id: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -163,7 +188,8 @@ export async function checkChunkStatus(upload_id: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -185,6 +211,8 @@ export async function checkChunkStatus(upload_id: string) {
|
||||
}
|
||||
|
||||
export async function getAllowedFilesExtensions(type: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -198,7 +226,8 @@ export async function getAllowedFilesExtensions(type: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -217,4 +246,48 @@ export async function getAllowedFilesExtensions(type: string) {
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkOperationPrice(action: string, filesCount: number, fileType: 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: 30008,
|
||||
message_body: {
|
||||
action: action,
|
||||
file_type: fileType,
|
||||
count_files: filesCount
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
cost: 0,
|
||||
success: false,
|
||||
tokens_count: 0,
|
||||
error: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function setMonitoring(fileId: string, monitoringType: 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: 30007,
|
||||
message_body: {
|
||||
monitoring_type: monitoringType,
|
||||
file_id: fileId
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
success: true,
|
||||
errorMessage: null
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface MonitoringStats {
|
||||
total_searches: number;
|
||||
searches_by_status: {
|
||||
processing: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
};
|
||||
recent_searches: any[];
|
||||
}
|
||||
|
||||
export async function fetchMonitoringStats(): Promise<MonitoringStats | null> {
|
||||
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: 30011
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed?.message_body?.searches_by_status) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLastMonitoringCheck() {
|
||||
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: 30011,
|
||||
message_body: {
|
||||
limit: 1
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body?.searches_by_status) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMonitoringInfo() {
|
||||
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: 30027
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export interface NotificationBody {
|
||||
createdAt: string | null,
|
||||
id: number,
|
||||
message: string,
|
||||
status: string | null,
|
||||
type: string,
|
||||
context: Record<string, string>
|
||||
}
|
||||
|
||||
export async function fetchActiveNotifications() {
|
||||
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: 30015,
|
||||
message_body: {
|
||||
action: 'active'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllNotifications(page: number = 0, size: number = 10) {
|
||||
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: 30015,
|
||||
message_body: {
|
||||
action: 'list',
|
||||
page: page,
|
||||
size: size,
|
||||
sortBy: "createdAt",
|
||||
sortDirection: "DESC", //DESC, ASC
|
||||
types: [], //"SEARCH_RESULT", "MONITORING_RESULT"
|
||||
statuses: ["NEW", "READIED"] //"NEW"
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
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 false
|
||||
}
|
||||
}
|
||||
|
||||
export async function notificationsMarkAsRead(ids: number[]) {
|
||||
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: 30015,
|
||||
message_body: {
|
||||
action: 'markRead',
|
||||
notificationIds: ids
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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 {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function notificationsDelete(ids: number[]) {
|
||||
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: 30015,
|
||||
message_body: {
|
||||
action: 'delete',
|
||||
notificationIds: ids
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log('parsed');
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use server'
|
||||
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
|
||||
export async function getPaymentsMethods() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/methods`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
return parsed;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePaymentsMethods(cardId: string): Promise<boolean | undefined> {
|
||||
if (!cardId) {
|
||||
return
|
||||
}
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/methods/${cardId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.message === 'Payment method deleted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { API_BASE_URL, bankCardFormSchema } from '@/app/actions/definitions';
|
||||
|
||||
export async function fetchReferralsLevels() {
|
||||
const token = await getSessionData('token');
|
||||
@@ -14,12 +14,12 @@ export async function fetchReferralsLevels() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'levels',
|
||||
token: token,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -57,12 +57,12 @@ export async function fetchReferralUserStats() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'userStats',
|
||||
token: token,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,26 +96,19 @@ export async function fetchReferralInvitees() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'invitees',
|
||||
token: token,
|
||||
pageSize: 100,
|
||||
pageNumber: 0
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -144,25 +137,18 @@ export async function referralRefill() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'refill',
|
||||
token: token,
|
||||
amount: 1,
|
||||
amount: 2000,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -178,4 +164,274 @@ export async function referralRefill() {
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PayoutMethods {
|
||||
cardNumber: string,
|
||||
id: number,
|
||||
type: string,
|
||||
displayName: string,
|
||||
maskedDetails: string,
|
||||
default: boolean,
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export async function getPayoutMethods(): Promise<{
|
||||
methods: PayoutMethods[],
|
||||
error: string | null
|
||||
}> {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/user/payout-methods`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
return {
|
||||
methods: parsed,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
methods: [],
|
||||
error: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserPayments() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/requests`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
return {
|
||||
payments: parsed,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
payments: [],
|
||||
error: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type PayoutMethodFormState = {
|
||||
previousState: {
|
||||
bankCard?: string | undefined;
|
||||
paymentsType?: 'bank-card' | 'bank-transfer' | 'on-account-balance';
|
||||
} | undefined,
|
||||
success?: boolean,
|
||||
error: Record<string, string | null> | null
|
||||
};
|
||||
|
||||
export async function addPayoutMethod(
|
||||
state: PayoutMethodFormState | undefined,
|
||||
formData: FormData,
|
||||
): Promise<PayoutMethodFormState> {
|
||||
const token = await getSessionData('token');
|
||||
console.log('addPayoutMethod');
|
||||
|
||||
const bankCard = formData.get('bankCard') as string || '';
|
||||
const paymentsType = formData.get('paymentsType') as 'bank-card' | 'bank-transfer' | 'on-account-balance';
|
||||
|
||||
const validatedFields = bankCardFormSchema.safeParse({
|
||||
bankCard
|
||||
});
|
||||
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
bankCard
|
||||
},
|
||||
success: false,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { bankCard } = validatedFields.data;
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/payout-method/card`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"cardNumber": bankCard.replaceAll(' ', '')
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
bankCard,
|
||||
paymentsType
|
||||
},
|
||||
success: true,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'error-save';
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
errors['server'] = 'error-save'
|
||||
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
bankCard,
|
||||
paymentsType
|
||||
},
|
||||
success: false,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type PayoutRequest = {
|
||||
previousState: {
|
||||
amount?: string | undefined;
|
||||
} | undefined,
|
||||
error: string | null
|
||||
};
|
||||
|
||||
export async function createPayoutRequest(
|
||||
state: PayoutRequest | undefined,
|
||||
formData: FormData,
|
||||
): Promise<PayoutRequest> {
|
||||
const token = await getSessionData('token');
|
||||
const amount = formData.get('amount') as string || '';
|
||||
const payoutMethodId = formData.get('payoutMethodId') as string || '';
|
||||
|
||||
console.log('createPayoutRequest');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
'amount': amount,
|
||||
'payoutMethodId': payoutMethodId
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 400) {
|
||||
const responseText = await response.text();
|
||||
throw JSON.parse(responseText)?.message;
|
||||
}
|
||||
|
||||
if (response.status === 409) {
|
||||
const responseText = await response.text();
|
||||
if (responseText.includes('Not have money for payout')) {
|
||||
throw 'insufficient-funds-to-receive-payment'
|
||||
}
|
||||
|
||||
if (responseText.includes('Not found payout method')) {
|
||||
throw 'not-found-payout-method'
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
amount
|
||||
},
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'payment-error';
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
|
||||
if (error) {
|
||||
let typedError: string;
|
||||
|
||||
switch (error) {
|
||||
case 'Sum of pay is required':
|
||||
typedError = 'sum-of-pay-is-required';
|
||||
break;
|
||||
case 'Min sum for pay 1000':
|
||||
typedError = 'min-sum-for-payout-1000';
|
||||
break;
|
||||
case 'insufficient-funds-to-receive-payment':
|
||||
typedError = 'insufficient-funds-to-receive-payment';
|
||||
break;
|
||||
case 'not-found-payout-method':
|
||||
typedError = 'not-found-payout-method';
|
||||
break
|
||||
default:
|
||||
typedError = 'payment-error';
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
amount
|
||||
},
|
||||
error: typedError
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
amount
|
||||
},
|
||||
error: 'payment-error'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@ 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'
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -17,55 +20,28 @@ export async function searchUserFiles(fileId: string) {
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
throw parsed.message_desc;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
|
||||
if (error === 'File for search unsupported') {
|
||||
return {
|
||||
errorMessage: 'file-for-search-unsupported'
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
errorMessage: '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`, {
|
||||
@@ -76,11 +52,13 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
page: currentPage,
|
||||
check_similar_first: true,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -93,11 +71,31 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
throw 'error';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
let errorMessage: string = 'error';
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
switch (typedError.message_desc) {
|
||||
case 'No results found':
|
||||
errorMessage = 'no-similar-files-title'
|
||||
break;
|
||||
default:
|
||||
errorMessage = 'error'
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
images: [],
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
totalPages: 0,
|
||||
totalResults: 0,
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,14 +112,14 @@ export async function getSearchStats() {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
return parsed;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export async function createSession(token: string, email: string) {
|
||||
});
|
||||
const cookieStore = await cookies();
|
||||
|
||||
cookieStore.set('session', session, {
|
||||
cookieStore.set('session_nc_db', session, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
expires: expiresAt,
|
||||
@@ -48,7 +48,7 @@ export async function createSession(token: string, email: string) {
|
||||
|
||||
export async function updateSession(newToken: string) {
|
||||
const cookieStore = await cookies();
|
||||
const session = cookieStore.get('session')?.value;
|
||||
const session = cookieStore.get('session_nc_db')?.value;
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
@@ -68,7 +68,7 @@ export async function updateSession(newToken: string) {
|
||||
|
||||
const updatedSession = await encrypt(updatedPayload);
|
||||
|
||||
cookieStore.set('session', updatedSession, {
|
||||
cookieStore.set('session_nc_db', updatedSession, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
expires: updatedPayload.expiresAt,
|
||||
@@ -79,7 +79,7 @@ export async function updateSession(newToken: string) {
|
||||
|
||||
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get('session')?.value;
|
||||
const sessionCookie = cookieStore.get('session_nc_db')?.value;
|
||||
|
||||
if (!sessionCookie) {
|
||||
return null;
|
||||
@@ -97,7 +97,12 @@ export async function getSessionData(type: 'token' | 'email'): Promise<string |
|
||||
|
||||
export async function deleteSession() {
|
||||
console.log('delete session')
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session');
|
||||
redirect('/login');
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session_nc_db');
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
redirect('/login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
'use server';
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function requestFileWithTracking(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
const headersList = await headers();
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
file_id: fileId,
|
||||
ip: ip,
|
||||
user_agent: userAgent
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
return data.message_body;
|
||||
} else {
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> {
|
||||
const token = await getSessionData('token');
|
||||
const headersList = await headers();
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
file_id: fileId,
|
||||
ip: ip,
|
||||
user_agent: userAgent
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
const fileUrl = data.message_body;
|
||||
const fileResponse = await fetch(fileUrl);
|
||||
if (!fileResponse.ok) {
|
||||
throw new Error(`Failed to fetch file: ${fileResponse.status}`);
|
||||
}
|
||||
|
||||
return await fileResponse.arrayBuffer();
|
||||
}
|
||||
else {
|
||||
return await response.arrayBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDocumentInfo(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: {
|
||||
file_id: fileId,
|
||||
action: 'file_info'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function filePermisionChange(fileId: string, permissions: boolean) {
|
||||
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: {
|
||||
file_id: fileId,
|
||||
action: 'change_permission',
|
||||
permissions: {
|
||||
DOWNLOAD: permissions
|
||||
}
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchHistoryView(
|
||||
page: number,
|
||||
size: number,
|
||||
locale: string,
|
||||
sortDirection?: 'asc' | 'desc',
|
||||
sortBy?: 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: 30028,
|
||||
message_body: {
|
||||
action: 'history_view',
|
||||
page: page,
|
||||
size: size,
|
||||
locale: locale,
|
||||
sort_direction: sortDirection,
|
||||
sort_by: sortBy
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTrackingStats() {
|
||||
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: 30028,
|
||||
message_body: {
|
||||
action: 'view_stats'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadTrackingFile(fileId: string, fileName?: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/public-download/${fileId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw 'failed-to-download-file';
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const base64 = Buffer.from(arrayBuffer).toString('base64')
|
||||
|
||||
return {
|
||||
data: base64,
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
fileName: fileName ? fileName : 'fileName'
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,719 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL, createComplaintSchema, createClaimSchema } from '@/app/actions/definitions';
|
||||
|
||||
export async function getViolationSearchStatus() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/actual-task`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const text = await response.text();
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
|
||||
return {
|
||||
status: text === 'Task not found' ? 'task-not-found' : text
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function startGlobalMonitoring(monitoringType: 'monitoring' | 'all_files') {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/start`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
searchType: monitoringType,
|
||||
...(monitoringType === 'all_files' && { fileIds: [] })
|
||||
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed: {
|
||||
taskId: string,
|
||||
status: string,
|
||||
totalFiles: number
|
||||
} = await response.json();
|
||||
|
||||
if (parsed?.status === 'ACCEPTED') {
|
||||
return parsed
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getViolationFilesArray(props: {
|
||||
page?: number,
|
||||
size?: number,
|
||||
start_date?: string,
|
||||
end_date?: string,
|
||||
file_name?: string
|
||||
}) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, start_date, end_date, file_name } = props;
|
||||
|
||||
try {
|
||||
const body: Record<string, any> = {};
|
||||
|
||||
if (page !== undefined) body.page = page;
|
||||
if (size !== undefined) body.size = size;
|
||||
if (start_date !== undefined) body.start_date = start_date;
|
||||
if (end_date !== undefined) body.end_date = end_date;
|
||||
if (file_name !== undefined) body.file_name = file_name;
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/violation-summary-with-files`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.content.length) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileMatches(fileId: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: 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: 30009,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
page: (page > 0 ? page - 1 : 0),
|
||||
size: 5,
|
||||
sort_direction: 'desc',
|
||||
status: status,
|
||||
...(violationId && { action: 'getByViolationId' }),
|
||||
...(violationId && { violation_id: violationId }),
|
||||
...((startDate && endDate) && { start_date: startDate, end_date: endDate })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_body) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationAnalyticStatistic(group: 'domain' | 'tld') {
|
||||
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: 30009,
|
||||
message_body: {
|
||||
group_by: group,
|
||||
action: "group",
|
||||
sort_direction: 'desc'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed?.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationStatistic(id?: 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: 30010,
|
||||
message_body: {
|
||||
...(id && { file_id: id })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed?.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationFinalSummaryStatistic() {
|
||||
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: 30023
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface complainBody {
|
||||
previousText: string,
|
||||
violationID: string | null,
|
||||
success: boolean,
|
||||
errorMessage?: Record<string, string> | null
|
||||
}
|
||||
|
||||
export async function createComplaint(
|
||||
state: complainBody | undefined,
|
||||
formData: FormData
|
||||
): Promise<complainBody> {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
const violationId = formData.get('violationId') as string || '';
|
||||
const textArea = formData.get('note') as string || '';
|
||||
|
||||
const validatedFields = createComplaintSchema.safeParse({
|
||||
textArea
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: 'create',
|
||||
violation_id: violationId,
|
||||
text: textArea,
|
||||
email: email
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: true
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (error && typeof error === 'object' && 'message_code' in error) {
|
||||
const err = error as { message_code: number };
|
||||
if (err.message_code === 2) {
|
||||
errors['server'] = 'the-complaint-has-not-been-registered'
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
errors['server'] = 'error-save';
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchComplainInfo(id: number) {
|
||||
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: 30013,
|
||||
message_body: {
|
||||
action: 'get_by_violation',
|
||||
violation_id: id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
body: parsed?.message_body
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
errorMessage: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MatchStatus = 'NEW' | 'SHOWED' | 'LEGAL_IN_WORK' | 'COMPLAINT_IN_WORK' | 'COMPLAINT_AND_LEGAL_IN_WORK' | 'AUTHORIZED_USE' | 'NOT_OWNER_FILE';
|
||||
|
||||
export async function updateMatchStatus(id: number, status: MatchStatus) {
|
||||
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: 30009,
|
||||
message_body: {
|
||||
action: 'updateStatus',
|
||||
violation_id: id,
|
||||
status: status
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body.status) {
|
||||
return true
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchClaimInfo(id: number) {
|
||||
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: 30017,
|
||||
message_body: {
|
||||
action: 'get_all',
|
||||
pageSize: 1,
|
||||
pageNumber: 0,
|
||||
sortBy: "createdAt",
|
||||
sortDir: "desc",
|
||||
violation_id: id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
body: parsed?.message_body
|
||||
}
|
||||
} else if (parsed?.message_code !== 0 && parsed?.message_desc === 'Law case not found') {
|
||||
return {
|
||||
body: null
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
errorMessage: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface caseBody {
|
||||
previousText: string,
|
||||
previousCaseTitle: string,
|
||||
previousAmount: string,
|
||||
violationID: string | null,
|
||||
success: boolean,
|
||||
errorMessage?: Record<string, string> | null
|
||||
}
|
||||
|
||||
export async function createClaim(
|
||||
state: caseBody | undefined,
|
||||
formData: FormData
|
||||
): Promise<caseBody> {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
const violationId = formData.get('violationId') as string || '';
|
||||
const textArea = formData.get('note') as string || '';
|
||||
const caseTitle = formData.get('caseTitle') as string || '';
|
||||
const amount = formData.get('amount') as string || '';
|
||||
|
||||
const validatedFields = createClaimSchema.safeParse({
|
||||
textArea,
|
||||
caseTitle
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: 'create',
|
||||
violation_id: violationId,
|
||||
description: textArea,
|
||||
email: email,
|
||||
name: caseTitle,
|
||||
priority: 'HIGH',
|
||||
amount: Number(amount)
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousText: textArea,
|
||||
previousAmount: amount,
|
||||
success: true
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (error && typeof error === 'object' && 'message_code' in error) {
|
||||
const err = error as { message_code: number };
|
||||
if (err.message_code === 2) {
|
||||
errors['server'] = 'the-complaint-has-not-been-registered'
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
errors['server'] = 'error-save';
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComplaintsCaseQueryParams {
|
||||
page?: number,
|
||||
size?: number,
|
||||
sort_by?: string,
|
||||
sort_direction?: 'desc' | 'asc'
|
||||
}
|
||||
|
||||
export async function fetchLastClaims(params: ComplaintsCaseQueryParams) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, sort_by, sort_direction } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: "get_all",
|
||||
pageNumber: page,
|
||||
size,
|
||||
sort_by,
|
||||
sort_direction
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLastComplaint(params: ComplaintsCaseQueryParams) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, sort_by, sort_direction } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: "get_all",
|
||||
page,
|
||||
size,
|
||||
sort_by,
|
||||
sort_direction
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { createSession } from '@/app/actions/session';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
|
||||
export async function YandexAuthorization(data: any, codeVerifier: string) {
|
||||
|
||||
console.log(data);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
'use server'
|
||||
|
||||
import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout';
|
||||
import { getSessionData, deleteSession } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4' });
|
||||
|
||||
export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string, operationType: 'TARIFF' | 'TOKEN') {
|
||||
const formData = new FormData();
|
||||
const token = await getSessionData('token');
|
||||
|
||||
formData.append('email', email);
|
||||
formData.append('tariffId', tariffId.toString());
|
||||
formData.append('operationType', operationType);
|
||||
formData.append('operationUuid', operationUuid);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/create`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.paymentUuid) {
|
||||
return true
|
||||
} else {
|
||||
throw 'error'
|
||||
}
|
||||
} else {
|
||||
throw 'error'
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function yooKasaCreatePayment(amount: number, tariff: number, operationType: 'TARIFF' | 'TOKEN') {
|
||||
const userEmail = await getSessionData('email');
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 10);
|
||||
const idempotenceKey = `${userEmail}-${timestamp}-${amount}-${tariff}-${random}`;
|
||||
|
||||
if (!userEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createPayload: ICreatePayment = {
|
||||
amount: {
|
||||
value: amount.toLocaleString().replace(/[,\s.]/g, ''),
|
||||
currency: 'RUB'
|
||||
},
|
||||
confirmation: {
|
||||
type: 'embedded',
|
||||
},
|
||||
metadata: {
|
||||
userMail: userEmail,
|
||||
tariff: tariff
|
||||
},
|
||||
merchant_customer_id: userEmail,
|
||||
capture: true,
|
||||
save_payment_method: operationType === 'TOKEN' ? false : true,
|
||||
/* ...(operationType === 'TOKEN' && { save_payment_method: false }) */
|
||||
};
|
||||
|
||||
try {
|
||||
const payment = await checkout.createPayment(createPayload, idempotenceKey);
|
||||
const response = await makePaymentOperation(userEmail, tariff, payment.id, operationType);
|
||||
|
||||
if (payment && response) {
|
||||
return payment?.confirmation.confirmation_token;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPaymentsHistory() {
|
||||
const email = await getSessionData('email');
|
||||
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: 30005,
|
||||
message_body: {
|
||||
action: 'user_payments',
|
||||
email: email
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
payments: parsed.message_body.payments,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw parsed.message_code;
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'need authorization' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/download/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Authorization": token
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`backend error: ${response.status}`);
|
||||
}
|
||||
|
||||
const fileBlob = await response.blob();
|
||||
const headers = new Headers(response.headers);
|
||||
|
||||
return new NextResponse(fileBlob, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': headers.get('content-type') || 'application/octet-stream',
|
||||
'Content-Disposition': headers.get('content-disposition') || `attachment; filename="file-${id}"`,
|
||||
'Content-Length': headers.get('content-length') || String(fileBlob.size),
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('download error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'download file error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,36 +3,12 @@
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { tokenLifeExtension } from '@/app/actions/auth';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import { removeUserFile } from '@/app/actions/fileEntity';
|
||||
|
||||
export default function ActivityTracker() {
|
||||
const lastRefreshTime = useRef(Date.now());
|
||||
const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
|
||||
const isRefreshing = useRef(false);
|
||||
|
||||
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const selectedId = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('searchedFileId='))
|
||||
?.split('=')[1];
|
||||
|
||||
const handleCookie = async () => {
|
||||
if (selectedId) {
|
||||
await removeUserFile(selectedId, 1);
|
||||
document.cookie = 'searchedFileId=; path=/; max-age=0';
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedId) {
|
||||
handleCookie();
|
||||
}
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
// Debounce функция
|
||||
const debounce = useCallback((func: () => void, delay: number) => {
|
||||
if (activityTimeoutRef.current) {
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function DropDownList({ children, value, callBack, translatedValu
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
className="dropdown-button"
|
||||
type="button"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { ReactNode, useState, useRef } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { SelectedFile } from './UploadSectionFile';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface DropDownItem {
|
||||
file: SelectedFile;
|
||||
convertTo: string;
|
||||
}
|
||||
|
||||
interface DropDownListProps {
|
||||
children: ReactNode;
|
||||
value: string | null | undefined;
|
||||
callBack: (value: DropDownItem) => void;
|
||||
translatedValue?: string | null | undefined;
|
||||
selectedFiles: SelectedFile[];
|
||||
}
|
||||
|
||||
interface LiElementProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
||||
'data-file'?: string;
|
||||
'data-convert-to'?: string;
|
||||
}
|
||||
|
||||
export default function DropDownListForImageConvert({
|
||||
children,
|
||||
value,
|
||||
callBack,
|
||||
translatedValue,
|
||||
selectedFiles
|
||||
}: DropDownListProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const t = useTranslations('Global')
|
||||
|
||||
useClickOutside(dropdownRef,
|
||||
() => {
|
||||
setIsOpen(false);
|
||||
},
|
||||
isOpen
|
||||
);
|
||||
|
||||
const getDisplayValue = (): string => {
|
||||
if (translatedValue) return translatedValue;
|
||||
return value ? value : t('do-not-convert');
|
||||
};
|
||||
|
||||
const findFileByName = (fileName: string): SelectedFile | undefined => {
|
||||
return selectedFiles.find(file => file.name === fileName);
|
||||
};
|
||||
|
||||
const enhancedChildren = React.Children.map(children, (child, index) => {
|
||||
if (React.isValidElement<LiElementProps>(child)) {
|
||||
const fileName = child.props['data-file'];
|
||||
const convertTo = child.props['data-convert-to'];
|
||||
|
||||
const isCurrent = value === convertTo;
|
||||
|
||||
return React.cloneElement(child, {
|
||||
onClick: () => {
|
||||
if (fileName && convertTo) {
|
||||
const file = findFileByName(fileName);
|
||||
if (file) {
|
||||
callBack({ file, convertTo });
|
||||
} else {
|
||||
console.error(`Файл с именем ${fileName} не найден`);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
className: `dropdown-item ${isCurrent ? "current" : ""}`,
|
||||
key: index,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dropdown-wrapper" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="dropdown-button"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">
|
||||
{getDisplayValue()}
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="dropdown-list">
|
||||
<ul
|
||||
className=""
|
||||
role="listbox"
|
||||
>
|
||||
{enhancedChildren}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface NotificationTextHandlerProps {
|
||||
notificationType: string
|
||||
notificationMessage: string
|
||||
notificationContext: Record<string, string>
|
||||
}
|
||||
|
||||
export function NotificationTextHandler({
|
||||
notificationType,
|
||||
notificationMessage,
|
||||
notificationContext
|
||||
}: NotificationTextHandlerProps) {
|
||||
const t = useTranslations('Notifications');
|
||||
|
||||
const getNotificationText = () => {
|
||||
if (!notificationType) {
|
||||
return t.has(notificationMessage) ? t(notificationMessage) : notificationMessage
|
||||
}
|
||||
|
||||
switch (notificationType) {
|
||||
case 'USER_VERIFIED':
|
||||
return t('user-verified', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
updateDate: notificationContext?.update_date || t('not-specified-date')
|
||||
});
|
||||
|
||||
case 'MONITORING_RESULT':
|
||||
return t('monitoring-result', {
|
||||
message: notificationContext?.message || t('message-missing')
|
||||
});
|
||||
|
||||
case 'COMPLAINT_STATUS_CHANGED':
|
||||
return t('complaint-status-changed', {
|
||||
newStatus: notificationContext?.new_status || t('not-specified'),
|
||||
oldStatus: notificationContext?.old_status || t('not-specified'),
|
||||
violation: notificationContext?.violation || t('not-specified'),
|
||||
file: notificationContext?.file || t('not-specified')
|
||||
});
|
||||
|
||||
case 'FILE_ADDED_TO_SYSTEM':
|
||||
return t('file-added-to-system', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
fileName: notificationContext?.file_name || t('not-specified-file-name')
|
||||
});
|
||||
|
||||
case 'FILE_MODERATION_EVENT':
|
||||
return t('file-moderation-event', {
|
||||
fileName: notificationContext?.file_name || t('not-specified-file-name'),
|
||||
oldStatus: notificationContext?.old_status || t('not-specified'),
|
||||
newStatus: notificationContext?.new_status || t('not-specified'),
|
||||
comment: notificationContext?.comment || t('no-comment')
|
||||
});
|
||||
|
||||
case 'PAYMENT_RESULT':
|
||||
return t('payment-result', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
amount: notificationContext?.amount || '0'
|
||||
});
|
||||
|
||||
case 'REFERRAL_REGISTERED':
|
||||
return t('referral-registered', {
|
||||
referralUser: notificationContext?.referral_user || t('unknown-user')
|
||||
});
|
||||
|
||||
case 'SEARCH_RESULT':
|
||||
return t('search-result', {
|
||||
startDate: notificationContext?.start_date || t('not-specified-date'),
|
||||
endDate: notificationContext?.end_date || t('not-specified-date'),
|
||||
searchType: notificationContext?.search_type || t('not-specified'),
|
||||
count: notificationContext?.count || '0'
|
||||
});
|
||||
|
||||
case 'SEARCH_OPERATION_FAILED':
|
||||
return t('search-operation-failed', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
errorMessage: notificationContext?.error_message || t('error-not-described'),
|
||||
message: notificationContext?.message || t('message-missing'),
|
||||
reason: notificationContext?.reason || t('reason-not-specified'),
|
||||
nextRun: notificationContext?.next_run || t('not-scheduled')
|
||||
});
|
||||
|
||||
case 'TOKEN_NOT_FOUND':
|
||||
return t('token-not-found', {
|
||||
currentTokens: notificationContext?.current_tokens || '0',
|
||||
needTokens: notificationContext?.need_tokens || t('not-specified'),
|
||||
message: notificationContext?.message || t('message-missing'),
|
||||
nextRun: notificationContext?.next_run || t('not-scheduled')
|
||||
});
|
||||
|
||||
default:
|
||||
return notificationMessage || t('notification-without-text');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{getNotificationText()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { usePdf } from '@/app/contexts/PdfContext';
|
||||
import { requestFileAsBuffer } from '@/app/actions/trackingActions';
|
||||
|
||||
interface PdfViewerProps {
|
||||
fileId: string;
|
||||
fileName?: string;
|
||||
showDownload?: boolean;
|
||||
}
|
||||
|
||||
export default function PdfViewer({ fileId, fileName = 'document.pdf', showDownload }: PdfViewerProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const pdfDocumentRef = useRef<any>(null);
|
||||
const { setPdfData } = usePdf();
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let objectUrl: string | null = null;
|
||||
|
||||
const loadAndDisplayPdf = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const pdfData = await requestFileAsBuffer(fileId);
|
||||
|
||||
if (!pdfData) {
|
||||
throw new Error('Failed to load document');
|
||||
}
|
||||
|
||||
//это нужно для скачивания так-как иначе pdfData теряется для скачивания
|
||||
if (showDownload) {
|
||||
const pdfDataForDownload = pdfData.slice(0);
|
||||
setPdfData(pdfDataForDownload, fileName);
|
||||
}
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: pdfData,
|
||||
useSystemFonts: true,
|
||||
disableFontFace: false,
|
||||
});
|
||||
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
pdfDocumentRef.current = pdfDocument;
|
||||
setTotalPages(pdfDocument.numPages);
|
||||
setCurrentPage(1);
|
||||
|
||||
await renderPage(pdfDocument, 1);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading PDF:', err);
|
||||
if (isMounted) {
|
||||
setError('Не удалось загрузить документ');
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAndDisplayPdf();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (pdfDocumentRef.current) {
|
||||
pdfDocumentRef.current.destroy();
|
||||
}
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [fileId, setPdfData]);
|
||||
|
||||
const renderPage = async (pdfDocument: any, pageNum: number) => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
try {
|
||||
const page = await pdfDocument.getPage(pageNum);
|
||||
|
||||
const container = canvasRef.current.parentElement;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const containerWidth = container.clientWidth || 800;
|
||||
|
||||
// Если ширина контейнера 0, откладываем рендер
|
||||
if (containerWidth === 0) {
|
||||
console.log('Container width is 0, waiting...');
|
||||
setTimeout(() => renderPage(pdfDocument, pageNum), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const originalViewport = page.getViewport({ scale: 1 });
|
||||
const scale = containerWidth / originalViewport.width;
|
||||
const viewport = page.getViewport({ scale: Math.min(scale, 1.5) });
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
} catch (err) {
|
||||
console.log('Error rendering page:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!pdfDocumentRef.current || loading) return;
|
||||
|
||||
const checkContainer = () => {
|
||||
const container = canvasRef.current?.parentElement;
|
||||
if (container && container.clientWidth > 0) {
|
||||
renderPage(pdfDocumentRef.current, currentPage);
|
||||
} else {
|
||||
setTimeout(checkContainer, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkContainer();
|
||||
}, [pdfDocumentRef.current, loading]);
|
||||
|
||||
const goToPrevPage = async () => {
|
||||
if (pdfDocumentRef.current && currentPage > 1) {
|
||||
const newPage = currentPage - 1;
|
||||
setCurrentPage(newPage);
|
||||
await renderPage(pdfDocumentRef.current, newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const goToNextPage = async () => {
|
||||
if (pdfDocumentRef.current && currentPage < totalPages) {
|
||||
const newPage = currentPage + 1;
|
||||
setCurrentPage(newPage);
|
||||
await renderPage(pdfDocumentRef.current, newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
containerRef.current.requestFullscreen();
|
||||
setFullscreen(true);
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
setFullscreen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setFullscreen(!!document.fullscreenElement);
|
||||
if (pdfDocumentRef.current && !document.fullscreenElement) {
|
||||
setTimeout(() => renderPage(pdfDocumentRef.current, currentPage), 100);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||
}, [currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (pdfDocumentRef.current && !loading) {
|
||||
renderPage(pdfDocumentRef.current, currentPage);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [currentPage, loading]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="prev-wrap">
|
||||
<div className="pdf-loading">Загрузка документа...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="prev-wrap">
|
||||
<div className="pdf-error">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="pdf-container">
|
||||
<div className="prev-wrap">
|
||||
<canvas ref={canvasRef} className="pdf-canvas" />
|
||||
</div>
|
||||
{totalPages > 0 && (
|
||||
<div className="document-controls">
|
||||
<div className="document-controls__nav">
|
||||
<button
|
||||
className="document-controls__prev"
|
||||
onClick={goToPrevPage}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
← Пред.
|
||||
</button>
|
||||
<span className="document-controls__current-page">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
className="document-controls__next"
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
След. →
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="document-controls__fullscreen"
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
{fullscreen ? 'Выйти' : 'На весь экран'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,919 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo, useEffect, ReactNode } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getPaginationRowModel,
|
||||
getFilteredRowModel,
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
} from '@tanstack/react-table';
|
||||
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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
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';
|
||||
|
||||
type FileItem = {
|
||||
id: string;
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
violations?: number | undefined;
|
||||
size?: number | undefined;
|
||||
uploadDate?: number;
|
||||
status?: string;
|
||||
protectStatus: string;
|
||||
_original?: ApiFile;
|
||||
supportId: number;
|
||||
};
|
||||
|
||||
type ApiFile = {
|
||||
id: string;
|
||||
originalFileName: string;
|
||||
mimeType: string;
|
||||
violations: number;
|
||||
fileSize: number;
|
||||
updatedAt: string;
|
||||
status: string;
|
||||
protectStatus: string;
|
||||
supportId: number;
|
||||
};
|
||||
|
||||
type ApiResponse = {
|
||||
files?: ApiFile[];
|
||||
};
|
||||
|
||||
// Форматирование даты из timestamp
|
||||
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}`;
|
||||
};
|
||||
|
||||
const formatDateTime = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const cutFileName = (fileName: string) => {
|
||||
const MAX_FILE_NAME_LENGTH = 26;
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
|
||||
if (lastDotIndex <= 0) {
|
||||
return fileName.length > MAX_FILE_NAME_LENGTH ? fileName.substring(0, MAX_FILE_NAME_LENGTH - 3) + '...' : fileName;
|
||||
}
|
||||
|
||||
const nameWithoutExtension = fileName.substring(0, lastDotIndex);
|
||||
const extension = fileName.substring(lastDotIndex);
|
||||
|
||||
if (fileName.length <= MAX_FILE_NAME_LENGTH) {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3;
|
||||
|
||||
if (maxNameLength <= 0) {
|
||||
return '...' /* + extension */;
|
||||
}
|
||||
|
||||
return nameWithoutExtension.substring(0, maxNameLength) + '...' /* + extension */;
|
||||
}
|
||||
|
||||
const cutFileExtension = (fileName: string) => {
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
const extension = fileName.substring(lastDotIndex + 1);
|
||||
return extension;
|
||||
}
|
||||
|
||||
export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
const {
|
||||
data: tableData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({
|
||||
queryKey: ['userFilesData', 1, 10000],
|
||||
queryFn: () => getUserFilesData(1, 10000),
|
||||
|
||||
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(),
|
||||
violations: item.violations,
|
||||
size: item.fileSize,
|
||||
uploadDate: newDate,
|
||||
status: item.status,
|
||||
protectStatus: item.protectStatus,
|
||||
supportId: item.supportId,
|
||||
_original: item
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Состояния
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [dateFilter, setDateFilter] = useState<string>('all');
|
||||
const [typeFilter, setTypeFilter] = useState<string>(fileType);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
|
||||
|
||||
const [isFileLoading, setIsFileLoading] = useState(false);
|
||||
|
||||
const t = useTranslations("Global");
|
||||
const locale = useLocale();
|
||||
|
||||
// Определение колонок
|
||||
const columns = useMemo<ColumnDef<FileItem>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
ID
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center table-item table-item-id">
|
||||
{row.original.supportId ? row.original.supportId : '-'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'fileName',
|
||||
header: ({ column }) => (
|
||||
<div className="column start">
|
||||
<span>
|
||||
{t('file')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center space-x-2 table-item">
|
||||
<FileTypeIcon type={row.original.fileType} />
|
||||
<div>
|
||||
<div className="font-medium w-full truncate" title={row.original.fileName}>
|
||||
<span className="font-semibold">
|
||||
{cutFileName(row.original.fileName)}
|
||||
</span>
|
||||
<br />
|
||||
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <span className="table-item-protected">
|
||||
{row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
enableColumnFilter: false,
|
||||
},
|
||||
/* {
|
||||
accessorKey: 'violations',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('violations')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
let classCollor = () => {
|
||||
let result = ''
|
||||
if (row.original.violations !== undefined) {
|
||||
result = row.original.violations > 0 ? 'text-red-600' : 'text-green-600'
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return (
|
||||
<div className={`text-center font-semibold ${classCollor()}`}>
|
||||
{row.original.violations !== undefined ? row.original.violations : '-'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}, */
|
||||
{
|
||||
accessorKey: 'size',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('size')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center table-item">
|
||||
{row.original.size !== undefined ? convertBytes(row.original.size) : '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'uploadDate',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('upload-date')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center table-item">
|
||||
{row.original.uploadDate ? (
|
||||
<>
|
||||
{formatDate(row.original.uploadDate)}
|
||||
<br />
|
||||
{formatDateTime(row.original.uploadDate)}
|
||||
</>
|
||||
) : (
|
||||
<div>-</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableColumnFilter: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
{t('status')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
||||
className="sort-button"
|
||||
>
|
||||
{
|
||||
column.getIsSorted() === 'asc' ?
|
||||
<span>
|
||||
<IconArrowUp />
|
||||
</span>
|
||||
: column.getIsSorted() === 'desc' ?
|
||||
<span>
|
||||
<IconArrowDown />
|
||||
</span>
|
||||
: <span>
|
||||
<IconFilter />
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center font-semibold table-item">
|
||||
<span className="table-item-status">
|
||||
{row.original.status}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => {
|
||||
return (
|
||||
<div className="column">
|
||||
{t('actions')}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="actions">
|
||||
<div className="actions-group" style={{ display: "none" }}>
|
||||
<button
|
||||
onClick={() => handleProtect(row.original)}
|
||||
className="bg-violet-500 hover:bg-violet-600"
|
||||
>
|
||||
<IconShieldExclamation />
|
||||
</button>
|
||||
{/* <button
|
||||
onClick={() => handleOpenWindowForRemove(row.original)}
|
||||
className="table-action-delete"
|
||||
>
|
||||
<IconDelete />
|
||||
<span>
|
||||
{t('delete')}
|
||||
</span>
|
||||
</button> */}
|
||||
</div>
|
||||
<div className="actions-group">
|
||||
{row.original.protectStatus === 'PROTECTED' && (
|
||||
<button
|
||||
onClick={() => handleDownload(row.original)}
|
||||
disabled={isFileLoading}
|
||||
className="table-action-download"
|
||||
>
|
||||
<IconFileDownload />
|
||||
<span>
|
||||
{t('download')}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
handleView(row.original)
|
||||
}}
|
||||
className="table-action-view"
|
||||
>
|
||||
<IconEye />
|
||||
<span>
|
||||
{t('make-check')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableColumnFilter: false,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const handleView = async (file: FileItem) => {
|
||||
console.log(`Просмотр файла: ${file.fileName}`);
|
||||
console.log(file);
|
||||
|
||||
const fileInfo = await viewFileInfo(file.id);
|
||||
console.log(fileInfo);
|
||||
|
||||
setOpenWindowChildren(() => {
|
||||
return (
|
||||
<FileInfoModalWindow fileInfo={fileInfo} setWindowClose={setOpenWindow} setWindowChildren={setOpenWindowChildren} />
|
||||
)
|
||||
})
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
const handleDownload = async (file: FileItem) => {
|
||||
setIsFileLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/download/${file.id}`);
|
||||
|
||||
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 = file.fileName || `file-${file.id}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success(`${t('file-is-downloading')} - ${file.fileName}`);
|
||||
} catch (error) {
|
||||
toast.error(t('failed-to-download-file'))
|
||||
} finally {
|
||||
setIsFileLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProtect = (file: FileItem) => {
|
||||
console.log(`Щиток: ${file.fileName}`);
|
||||
};
|
||||
|
||||
const handleOpenWindowForRemove = async (file: FileItem) => {
|
||||
|
||||
setOpenWindowChildren(() => {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl">
|
||||
{t('you-sure-you-want-to-delete')}
|
||||
</h3>
|
||||
<div className="mt-2 mb-8 text-center">{file.fileName}</div>
|
||||
<div className="flex justify-center gap-4">
|
||||
<button className="btn-primary btn-modal"
|
||||
onClick={() => {
|
||||
deleteMutation.mutate({
|
||||
fileId: file.id,
|
||||
removeParam: 1,
|
||||
})
|
||||
}}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
{deleteMutation.isPending ? '...' : t('yes')}
|
||||
</button>
|
||||
<button className="btn-primary btn-modal"
|
||||
onClick={() => {
|
||||
setOpenWindow(false);
|
||||
setOpenWindowChildren(null);
|
||||
}}
|
||||
>
|
||||
{t('no')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: ({ fileId, removeParam }: { fileId: string; removeParam: number }) =>
|
||||
removeUserFile(fileId, removeParam),
|
||||
|
||||
onMutate: async ({ fileId, removeParam }: { fileId: string; removeParam: number }) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
|
||||
|
||||
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
|
||||
if (!old?.files) return old;
|
||||
return {
|
||||
...old,
|
||||
files: old.files.filter(file => file.id !== fileId)
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onError: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
toast.error(t('error'));
|
||||
},
|
||||
|
||||
onSuccess: (response, { fileId }) => {
|
||||
if (response === fileId) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['userFilesData'],
|
||||
refetchType: 'active'
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['userFilesInfo']
|
||||
});
|
||||
|
||||
setOpenWindow(false);
|
||||
setOpenWindowChildren(null);
|
||||
toast.success(t('file-has-been-deleted'));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Фильтрация по типу файла и дате
|
||||
const filteredData = useMemo(() => {
|
||||
let result = tableData;
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Фильтр по типу файла
|
||||
if (typeFilter !== 'all') {
|
||||
result = result.filter(item => item.fileType === typeFilter);
|
||||
}
|
||||
|
||||
// Фильтр по дате
|
||||
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.uploadDate) {
|
||||
return item.uploadDate >= todayStart
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'week':
|
||||
const weekAgo = now - sevenDays;
|
||||
result = result.filter(item => {
|
||||
if (item.uploadDate) {
|
||||
return item.uploadDate >= weekAgo;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'month':
|
||||
const monthAgo = now - thirtyDays;
|
||||
result = result.filter(item => {
|
||||
if (item.uploadDate) {
|
||||
return item.uploadDate >= monthAgo;
|
||||
}
|
||||
});
|
||||
break;
|
||||
case 'older':
|
||||
const monthAgo2 = now - thirtyDays;
|
||||
result = result.filter(item => {
|
||||
if (item.uploadDate) {
|
||||
return item.uploadDate < monthAgo2;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [tableData, typeFilter, dateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPageRows = table.getRowModel().rows;
|
||||
const pageCount = table.getPageCount();
|
||||
|
||||
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
|
||||
table.setPageIndex(pagination.pageIndex - 1);
|
||||
}
|
||||
}, [filteredData, pagination.pageIndex]);
|
||||
|
||||
// Создание таблицы
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
pagination
|
||||
},
|
||||
autoResetPageIndex: false,
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pluralizeFiles = (number: number) => {
|
||||
const translate = [t('file'), t('files-few'), t('files')];
|
||||
return pluralize(number, translate[0], translate[1], translate[2], locale);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tanstak-table-wrapper">
|
||||
<h3
|
||||
className="tanstak-table-title"
|
||||
>
|
||||
{(() => {
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
return t('protected-image')
|
||||
case 'video':
|
||||
return t('protected-video')
|
||||
case 'audio':
|
||||
return t('protected-audio')
|
||||
default:
|
||||
return t('table-description')
|
||||
}
|
||||
})()}
|
||||
</h3>
|
||||
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
|
||||
{/* Фильтры */}
|
||||
<div className="tanstak-table-filtres" style={{ display: "none" }}>
|
||||
<div className="table-filtres-wrapper">
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{t('date-filter')}:</div>
|
||||
<DropDownList
|
||||
value={dateFilter}
|
||||
translatedValue={(() => {
|
||||
switch (dateFilter) {
|
||||
case 'all':
|
||||
return t('all-dates')
|
||||
case 'week':
|
||||
return t('for-a-week')
|
||||
case 'month':
|
||||
return t('for-a-month')
|
||||
case 'older':
|
||||
return t('older-than-a-month')
|
||||
default:
|
||||
return t('today')
|
||||
}
|
||||
})()}
|
||||
callBack={setDateFilter}
|
||||
>
|
||||
<li value="all">
|
||||
{t('all-dates')}
|
||||
</li>
|
||||
<li value="today">
|
||||
{t('today')}
|
||||
</li>
|
||||
<li value="week">
|
||||
{t('for-a-week')}
|
||||
</li>
|
||||
<li value="month">
|
||||
{t('for-a-month')}
|
||||
</li>
|
||||
<li value="older">
|
||||
{t('older-than-a-month')}
|
||||
</li>
|
||||
</DropDownList>
|
||||
</div>
|
||||
|
||||
{fileType === "all" && (
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{t('type-filter')}:</div>
|
||||
<DropDownList
|
||||
value={typeFilter}
|
||||
translatedValue={
|
||||
(() => {
|
||||
switch (typeFilter) {
|
||||
case 'image':
|
||||
return t('images')
|
||||
case 'video':
|
||||
return t('videos')
|
||||
case 'audio':
|
||||
return t('audios')
|
||||
default:
|
||||
return t('all-types')
|
||||
}
|
||||
})()
|
||||
}
|
||||
callBack={setTypeFilter}
|
||||
>
|
||||
<li value="all">
|
||||
{t('all-types')}
|
||||
</li>
|
||||
<li value="image">
|
||||
{t('images-few')}
|
||||
</li>
|
||||
<li value="video">
|
||||
{t('videos')}
|
||||
</li>
|
||||
<li value="audio">
|
||||
{t('audios')}
|
||||
</li>
|
||||
</DropDownList>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="table-filtres-item">
|
||||
<div className="table-filtres-label">{t('items-per-page')}:</div>
|
||||
<DropDownList
|
||||
value={table.getState().pagination.pageSize}
|
||||
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
|
||||
callBack={table.setPageSize}
|
||||
>
|
||||
{[5, 10, 20, 50, 100].map(pageSize => (
|
||||
<li key={pageSize} value={pageSize}>
|
||||
{t('show')} {pageSize}
|
||||
</li>
|
||||
))}
|
||||
</DropDownList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Таблица */}
|
||||
<div className="tanstak-table-block">
|
||||
<table className="tanstak-table">
|
||||
<thead className="tanstak-table-head">
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: typeof header.column.columnDef.header === 'function'
|
||||
? header.column.columnDef.header(header.getContext())
|
||||
: header.column.columnDef.header as string}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="tanstak-table-body">
|
||||
{table.getRowModel().rows.length > 0 ? (
|
||||
table.getRowModel().rows.map(row => (
|
||||
<tr key={row.id}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id}>
|
||||
{typeof cell.column.columnDef.cell === 'function'
|
||||
? cell.column.columnDef.cell(cell.getContext())
|
||||
: cell.getValue() as string}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center">
|
||||
{t('no-data-for-selected-filters')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Пагинация */}
|
||||
<div className="tanstak-table-pagination">
|
||||
<div className="pagination-info">
|
||||
<span className="pagination-info-pages">
|
||||
{t('page')}{' '}
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="pagination-info-files">
|
||||
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pagination-controls">
|
||||
{table.getCanPreviousPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.firstPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconDoubleArrowLeft />
|
||||
</button>
|
||||
)}
|
||||
{table.getCanPreviousPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="pagination-controls-pages">
|
||||
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
|
||||
const pageIndex = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
table.getPageCount() - 5,
|
||||
table.getState().pagination.pageIndex - 2
|
||||
)
|
||||
) + i;
|
||||
|
||||
if (pageIndex < table.getPageCount()) {
|
||||
return (
|
||||
<button
|
||||
key={pageIndex}
|
||||
className={`${table.getState().pagination.pageIndex === pageIndex
|
||||
? 'current'
|
||||
: 'other'
|
||||
}`}
|
||||
onClick={() => table.setPageIndex(pageIndex)}
|
||||
>
|
||||
{pageIndex + 1}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{table.getCanNextPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconArrowRight />
|
||||
</button>
|
||||
)}
|
||||
{table.getCanNextPage() && (
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => table.lastPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconDoubleArrowRight />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
|
||||
import { IconShieldAdd } from '@/app/ui/icons/icons';
|
||||
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload';
|
||||
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus, checkOperationPrice } from '@/app/actions/fileUpload';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
interface SelectedFile {
|
||||
import DropDownListForImageConvert from '@/app/components/DropDownListForImageConvert';
|
||||
|
||||
export interface SelectedFile {
|
||||
file: File;
|
||||
preview: string | undefined;
|
||||
name: string;
|
||||
@@ -15,32 +17,42 @@ interface SelectedFile {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
uploadId?: string;
|
||||
progress: number;
|
||||
status: 'pending' | 'uploading' | 'completed' | 'error' | 'cancelled';
|
||||
error?: string;
|
||||
convertTo?: string | null;
|
||||
}
|
||||
|
||||
interface FileUploadInitResponse {
|
||||
upload_id: string,
|
||||
file_name: string,
|
||||
total_chunks: number,
|
||||
chunk_size: number,
|
||||
status: string
|
||||
upload_id: string;
|
||||
file_name: string;
|
||||
total_chunks: number;
|
||||
chunk_size: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface UploadSectionFile {
|
||||
fileType: string
|
||||
allowedExtensions: string[]
|
||||
maxFileSize: number
|
||||
fileType: string;
|
||||
allowedExtensions: string[];
|
||||
maxFileSize: number;
|
||||
maxParallelUploads?: number;
|
||||
}
|
||||
|
||||
export default function UploadSectionFile({ fileType, allowedExtensions, maxFileSize }: UploadSectionFile) {
|
||||
export default function UploadSectionFile({
|
||||
fileType,
|
||||
allowedExtensions,
|
||||
maxFileSize,
|
||||
maxParallelUploads = 3
|
||||
}: UploadSectionFile) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
|
||||
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [uploadId, setUploadId] = useState<string | null>(null);
|
||||
const [isFileUploaded, setIsFileUploaded] = useState<boolean>(false);
|
||||
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
||||
const [activeUploads, setActiveUploads] = useState<Set<string>>(new Set()); // ID активных загрузок
|
||||
const queryClient = useQueryClient();
|
||||
const isCancelledRef = useRef(false);
|
||||
const isCancelledRef = useRef<Map<string, boolean>>(new Map()); // Для отслеживания отмен по файлам
|
||||
const selectedFilesRef = useRef<SelectedFile[]>(selectedFiles);
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
@@ -74,11 +86,6 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
function setErrorHandler(message: string | null): void {
|
||||
setError(message);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
|
||||
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
@@ -102,63 +109,76 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileSelect = useCallback(async (file: File | null): Promise<void> => {
|
||||
if (!file) {
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleFilesSelect = useCallback(async (files: File[]): Promise<void> => {
|
||||
setError(null);
|
||||
setIsFileUploaded(false);
|
||||
setUploadProgress(0);
|
||||
|
||||
const validation = validateFile(file);
|
||||
const newFiles: SelectedFile[] = [];
|
||||
const selectedFileNames = new Set(selectedFiles.map(f => f.name));
|
||||
const currentFileNames = new Set<string>();
|
||||
const duplicateNames: string[] = [];
|
||||
|
||||
if (!validation.isValid) {
|
||||
setError(validation.errorMessage || t('unknown-validation-error'));
|
||||
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) {
|
||||
setErrorHandler(`${t('image-size')}: ${dimensions.width}x${dimensions.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorHandler(t('error-reading-image'));
|
||||
return;
|
||||
for (const file of files) {
|
||||
if (selectedFileNames.has(file.name)) {
|
||||
duplicateNames.push(file.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
const validation = validateFile(file);
|
||||
|
||||
if (!validation.isValid) {
|
||||
setError(prev => prev ? `${prev}, ${validation.errorMessage}` : validation.errorMessage || t('unknown-validation-error'));
|
||||
continue;
|
||||
}
|
||||
|
||||
let dimensions: {
|
||||
width: number,
|
||||
height: number,
|
||||
} | undefined = undefined;
|
||||
if (fileType === "image") {
|
||||
try {
|
||||
dimensions = await getImageDimensions(file) as { width: number, height: number };
|
||||
if (dimensions?.width < 100 || dimensions?.height < 100 ||
|
||||
dimensions?.width > 10000 || dimensions?.height > 10000) {
|
||||
setError(prev => prev ? `${prev}, ${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.` :
|
||||
`${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(prev => prev ? `${prev}, ${t('error-reading-image')}` : t('error-reading-image'));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentFileNames.add(file.name);
|
||||
|
||||
newFiles.push({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
dimensions,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined,
|
||||
progress: 0,
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedFile({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
dimensions,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined
|
||||
});
|
||||
if (duplicateNames.length > 0) {
|
||||
const uniqueDuplicates = [...new Set(duplicateNames)];
|
||||
setError(`${t('duplicate-files')}: ${uniqueDuplicates.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log('Файл выбран:', file.name, file.size);
|
||||
}, [fileType, t]);
|
||||
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||
}, [fileType, selectedFiles]);
|
||||
|
||||
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const file = event.target.files?.[0] || null;
|
||||
handleFileSelect(file);
|
||||
const files = Array.from(event.target.files || []);
|
||||
handleFilesSelect(files);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = (): void => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
@@ -169,66 +189,107 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
handleFileSelect(file);
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
handleFilesSelect(files);
|
||||
};
|
||||
|
||||
const handleClearFile = async (): Promise<void> => {
|
||||
if (selectedFile && selectedFile.preview) {
|
||||
URL.revokeObjectURL(selectedFile.preview);
|
||||
const handleClearFile = async (fileId?: string): Promise<void> => {
|
||||
if (fileId) {
|
||||
const fileToClear = selectedFiles.find(f => f.uploadId === fileId || f.name === fileId);
|
||||
if (fileToClear?.preview) {
|
||||
URL.revokeObjectURL(fileToClear.preview);
|
||||
}
|
||||
|
||||
if (fileToClear?.uploadId) {
|
||||
await cancelUpload(fileToClear.uploadId);
|
||||
isCancelledRef.current.set(fileToClear.uploadId, true);
|
||||
setActiveUploads(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(fileToClear.uploadId!);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => prev.filter(f =>
|
||||
f.uploadId !== fileId && f.name !== fileId
|
||||
));
|
||||
} else {
|
||||
console.log('clear all')
|
||||
selectedFiles.forEach(file => {
|
||||
if (file.preview) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
});
|
||||
|
||||
const cancelPromises = Array.from(activeUploads).map(uploadId =>
|
||||
cancelUpload(uploadId)
|
||||
);
|
||||
await Promise.allSettled(cancelPromises);
|
||||
|
||||
isCancelledRef.current.clear();
|
||||
setActiveUploads(new Set());
|
||||
setSelectedFiles([]);
|
||||
}
|
||||
|
||||
if (uploadId) {
|
||||
await cancelUpload(uploadId);
|
||||
}
|
||||
|
||||
isCancelledRef.current = true;
|
||||
|
||||
setIsFileUploaded(false);
|
||||
setSelectedFile(null);
|
||||
setError(null);
|
||||
setUploadProgress(0);
|
||||
};
|
||||
|
||||
const handlerFileUpload = useCallback(async (fileInfo: SelectedFile): Promise<void> => {
|
||||
if (uploadId) return;
|
||||
const uploadSingleFile = async (fileInfo: SelectedFile, uploadType: 'single' | 'multi'): Promise<void> => {
|
||||
const fileId = `${fileInfo.name}-${Date.now()}`;
|
||||
isCancelledRef.current.set(fileId, false);
|
||||
|
||||
setError(null);
|
||||
isCancelledRef.current = false;
|
||||
const file = fileInfo.file;
|
||||
if (uploadType === 'single') {
|
||||
setError(null);
|
||||
const priceCheck = await checkOperationPrice('checkTokensForProtect', 1, fileType);
|
||||
|
||||
if (!priceCheck.success) {
|
||||
const errorMessage = `${t('there-are-not-enough-funds-to-protect-file', { count: priceCheck?.tokens_count })}`;
|
||||
setError(errorMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const extension = file.name.split('.').pop() || '';
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'uploading' } : f
|
||||
));
|
||||
|
||||
const extension = fileInfo.file.name.split('.').pop() || '';
|
||||
const initMessageBody = {
|
||||
file_name: file.name,
|
||||
file_name: fileInfo.file.name,
|
||||
file_type: fileType,
|
||||
extension: extension,
|
||||
file_size: file.size
|
||||
file_size: fileInfo.file.size
|
||||
};
|
||||
|
||||
const response = await fileUpload(initMessageBody) as FileUploadInitResponse;
|
||||
const response = await fileUpload(initMessageBody, fileInfo.convertTo) as FileUploadInitResponse;
|
||||
|
||||
if (!response?.upload_id) {
|
||||
throw new Error('Failed to get upload_id');
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, uploadId: response.upload_id } : f
|
||||
));
|
||||
|
||||
setActiveUploads(prev => new Set(prev).add(response.upload_id));
|
||||
|
||||
const CHUNK_SIZE = response.chunk_size;
|
||||
const totalChunks = response.total_chunks;
|
||||
|
||||
setUploadId(response.upload_id);
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
if (isCancelledRef.current) {
|
||||
if (isCancelledRef.current.get(fileId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = chunkIndex * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
const end = Math.min(start + CHUNK_SIZE, fileInfo.file.size);
|
||||
const chunk = fileInfo.file.slice(start, end);
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('upload_id', response.upload_id);
|
||||
@@ -241,12 +302,24 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
throw chunkResponse.errorMesage;
|
||||
}
|
||||
|
||||
setUploadProgress(Math.floor((chunkIndex + 1) / totalChunks * 100));
|
||||
const progress = Math.floor((chunkIndex + 1) / totalChunks * 100);
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, progress } : f
|
||||
));
|
||||
}
|
||||
|
||||
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||
setIsFileUploaded(true);
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'completed', progress: 100 } : f
|
||||
));
|
||||
|
||||
setActiveUploads(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(response.upload_id);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userData'] });
|
||||
@@ -255,53 +328,93 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current) {
|
||||
switch (error) {
|
||||
case 'duplicate-file':
|
||||
setErrorHandler(t('error-duplicate-file'));
|
||||
break;
|
||||
case 'user-not-have-tokens-for-protect':
|
||||
setErrorHandler(t('error-user-not-have-tokens-for-protect'));
|
||||
break;
|
||||
default:
|
||||
setErrorHandler(t('error-uploading-file'));
|
||||
break;
|
||||
}
|
||||
if (!isCancelledRef.current.get(fileId)) {
|
||||
const errorMessage = (typeof error === 'string') ? t(error) : t('error-uploading-file');
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'error', error: errorMessage } : f
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
setUploadId(null);
|
||||
}
|
||||
}, [uploadId, fileType, t]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
||||
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
||||
// но у него есть свои особенности и стандартные запросы не работают.
|
||||
const handleUnload = () => {
|
||||
if (uploadId) {
|
||||
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
|
||||
//const blob = new Blob([data], { type: 'application/json' });
|
||||
//navigator.sendBeacon('/api/v1/data/cancel', blob);
|
||||
console.log('sendBeacon');
|
||||
}
|
||||
};
|
||||
if (uploadId) {
|
||||
window.addEventListener('unload', handleUnload);
|
||||
}
|
||||
selectedFilesRef.current = selectedFiles;
|
||||
}, [selectedFiles]);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unload', handleUnload);
|
||||
};
|
||||
}, [uploadId]);
|
||||
const handleStartAllUploads = useCallback(async (): Promise<void> => {
|
||||
setError(null);
|
||||
if (selectedFilesRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
const priceCheck = await checkOperationPrice('checkTokensForProtect', selectedFilesRef.current.length, fileType);
|
||||
|
||||
if (priceCheck.success) {
|
||||
const uploadNextBatch = async () => {
|
||||
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
|
||||
const batch = currentPending.slice(0, maxParallelUploads);
|
||||
|
||||
if (batch.length === 0) return;
|
||||
|
||||
await Promise.all(batch.map(file => uploadSingleFile(file, 'multi')));
|
||||
|
||||
if (selectedFilesRef.current.some(f => f.status === 'pending')) {
|
||||
await uploadNextBatch();
|
||||
}
|
||||
};
|
||||
|
||||
await uploadNextBatch();
|
||||
} else {
|
||||
const errorMessage = `${t('there-are-not-enough-funds-to-protect-files', { count: priceCheck?.tokens_count })}`;
|
||||
setError(errorMessage);
|
||||
}
|
||||
}, [maxParallelUploads]);
|
||||
|
||||
const getTotalProgress = useMemo(() => {
|
||||
if (selectedFiles.length === 0) return 0;
|
||||
const total = selectedFiles.reduce((sum, file) => sum + file.progress, 0);
|
||||
return Math.floor(total / selectedFiles.length);
|
||||
}, [selectedFiles]);
|
||||
|
||||
const completedCount = useMemo(() =>
|
||||
selectedFiles.filter(f => f.status === 'completed').length,
|
||||
[selectedFiles]
|
||||
);
|
||||
|
||||
const uploadingCount = useMemo(() =>
|
||||
selectedFiles.filter(f => f.status === 'uploading').length,
|
||||
[selectedFiles]
|
||||
);
|
||||
|
||||
|
||||
/* useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
||||
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
||||
// но у него есть свои особенности и стандартные запросы не работают.
|
||||
const handleUnload = () => {
|
||||
if (activeUploads.size) {
|
||||
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
|
||||
//const blob = new Blob([data], { type: 'application/json' });
|
||||
//navigator.sendBeacon('/api/v1/data/cancel', blob);
|
||||
console.log('sendBeacon');
|
||||
}
|
||||
};
|
||||
if (activeUploads.size) {
|
||||
window.addEventListener('unload', handleUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unload', handleUnload);
|
||||
};
|
||||
}, [activeUploads.size]); */
|
||||
|
||||
useNavigationBlocker({
|
||||
shouldBlock: !!uploadId,
|
||||
shouldBlock: !!activeUploads.size,
|
||||
message: t('have-unsaved-changes'),
|
||||
onConfirm: async () => {
|
||||
console.log('User confirmed navigation');
|
||||
if (uploadId) {
|
||||
await cancelUpload(uploadId);
|
||||
if (activeUploads.size) {
|
||||
await handleClearFile();
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
@@ -309,13 +422,30 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
}
|
||||
});
|
||||
|
||||
function converButtonHandler(value: {
|
||||
convertTo: string;
|
||||
file: SelectedFile
|
||||
}): void {
|
||||
setSelectedFiles(selectedFiles.map(selectedFile => {
|
||||
if (selectedFile.name === value.file.name) {
|
||||
return {
|
||||
...selectedFile,
|
||||
convertTo: value.convertTo
|
||||
};
|
||||
}
|
||||
return selectedFile;
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="upload-section">
|
||||
<div className="upload-section" >
|
||||
<h3>
|
||||
{t('upload-for-protection', { fileType: t(fileType.toLocaleLowerCase()) })}
|
||||
{t('upload-for-protection', {
|
||||
fileType: t(fileType.toLocaleLowerCase())
|
||||
})}
|
||||
</h3>
|
||||
|
||||
<div
|
||||
< div
|
||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
@@ -325,115 +455,226 @@ export default function UploadSectionFile({ fileType, allowedExtensions, maxFile
|
||||
<h4>
|
||||
{t('drag-the-file-here')}
|
||||
</h4>
|
||||
{/* <p className="mb-2.5 text-[#6b7280]">
|
||||
{t('or')}
|
||||
</p> */}
|
||||
|
||||
{fileType === "image" && (
|
||||
<p className="description">
|
||||
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
|
||||
</p>
|
||||
)}
|
||||
<p className="description">
|
||||
{
|
||||
fileType === "image" && (
|
||||
<p className="description" >
|
||||
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<p className="description" >
|
||||
{t('file-format')}: {acceptString}
|
||||
</p>
|
||||
<p className="description">
|
||||
< p className="description" >
|
||||
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
|
||||
</p>
|
||||
|
||||
<input
|
||||
< input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={acceptString}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
aria-label="Выбор файла для защиты"
|
||||
multiple // Добавляем multiple для выбора нескольких файлов
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleButtonClick}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
type="button"
|
||||
disabled={uploadProgress && uploadProgress !== 100 ? true : false}
|
||||
disabled={uploadingCount > maxParallelUploads}
|
||||
>
|
||||
{t('select-files-to-protect')}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-600 font-medium">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
error && (
|
||||
<div className="error-message">
|
||||
<p className="error-message-text"> {error} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{selectedFile && (
|
||||
<div
|
||||
className={`selected-file ${isFileUploaded ? 'done' : ''}`}
|
||||
>
|
||||
<div className="selected-file-file-info">
|
||||
<div>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">
|
||||
{t('file')}:
|
||||
</span> {selectedFile.name}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">
|
||||
{t('size')}:
|
||||
</span> {selectedFile.size}
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">{t('cost-of-protection')}:</span> 0
|
||||
</p>
|
||||
<p className="text-gray-700">
|
||||
<span className="font-medium">{t('current-balance')}:</span> 0
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{isFileUploaded && (
|
||||
<div className="font-medium text-green-600 text-2xl">
|
||||
{t('file-successfully-uploaded')}
|
||||
{
|
||||
selectedFiles.length > 0 && (
|
||||
<div className="mt-6" >
|
||||
<div className="selected-file-action-panel" >
|
||||
<div>
|
||||
<p className="selected-file-action-info" >
|
||||
<span>{t('total-files')}: {selectedFiles.length}</span>
|
||||
<span>{t('uploading')}: {uploadingCount}</span>
|
||||
<span>{t('completed')}: {completedCount}</span>
|
||||
</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${getTotalProgress}%` }
|
||||
}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`btn btn-primary`}
|
||||
onClick={handleStartAllUploads}
|
||||
type="button"
|
||||
disabled={uploadingCount > 0 || selectedFiles.every(f => f.status !== 'pending')}
|
||||
>
|
||||
{t('upload-all')}
|
||||
</button>
|
||||
< button
|
||||
className="btn btn-primary"
|
||||
onClick={() => handleClearFile()}
|
||||
type="button"
|
||||
>
|
||||
{t('clear-all')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="selected-file-wrapper">
|
||||
{
|
||||
selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={`${file.name}-${index}`}
|
||||
className={`selected-file ${file.status === 'completed' ? 'done' : ''} ${file.status === 'error' ? 'error' : ''}`}
|
||||
>
|
||||
<div className="selected-file-file-info">
|
||||
<div>
|
||||
<p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('file')}: </span> {file.name}
|
||||
</p>
|
||||
< p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('size')}: </span> {file.size}
|
||||
</p>
|
||||
< p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('status')}: </span>
|
||||
<span className={`ml-2 ${file.status === 'completed' ? 'text-green-600' :
|
||||
file.status === 'uploading' ? 'text-blue-600' :
|
||||
file.status === 'error' ? 'text-red-600' :
|
||||
'text-gray-600'
|
||||
}`}>
|
||||
{
|
||||
file.status === 'completed' ? t('completed') :
|
||||
file.status === 'uploading' ? t('uploading') :
|
||||
file.status === 'error' ? t('error') :
|
||||
t('pending')
|
||||
}
|
||||
</span>
|
||||
</p>
|
||||
{
|
||||
file.error && (
|
||||
<p className="text-red-600 text-sm mt-1" > {file.error} </p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="selected-file-right-block">
|
||||
{
|
||||
(fileType === 'image' && file.status === 'pending') && (
|
||||
<>
|
||||
<DropDownListForImageConvert
|
||||
value={file.convertTo}
|
||||
translatedValue={file.convertTo === 'reject' ? t('do-not-convert') : file.convertTo}
|
||||
callBack={converButtonHandler}
|
||||
selectedFiles={selectedFiles}
|
||||
>
|
||||
<li data-file={file.name} data-convert-to="reject">
|
||||
{t('do-not-convert')}
|
||||
</li>
|
||||
{
|
||||
allowedExtensions && allowedExtensions.map(extension => {
|
||||
if (extension === 'gif') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={extension} data-file={file.name} data-convert-to={extension}>
|
||||
{t('convert-to')} {extension}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</DropDownListForImageConvert>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className="flex"
|
||||
>
|
||||
{file.status !== 'error' && (
|
||||
<div className="upload-progress-circle">
|
||||
<svg viewBox="0 0 36 36" className="circular-chart">
|
||||
<path
|
||||
className="circle-bg"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<path
|
||||
className="circle"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke={file.status === 'completed' ? '#10b981' : file.status === 'uploading' ? '#155dfc' : '#6b7280'}
|
||||
strokeWidth="4"
|
||||
strokeDasharray={`${file.progress}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className="circle-percentage">{file.progress}%</span>
|
||||
</div>
|
||||
)}
|
||||
< button
|
||||
className="btn btn-cancel"
|
||||
onClick={() => handleClearFile(file.uploadId || file.name)}
|
||||
type="button"
|
||||
>
|
||||
{t('remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
(fileType === 'image' && file.preview) && (
|
||||
<div
|
||||
className="image-preview-wrapper"
|
||||
>
|
||||
<img
|
||||
src={file.preview}
|
||||
alt="Предпросмотр загруженного изображения"
|
||||
className="uploaded-image"
|
||||
onError={() => setError('Не удалось загрузить превью изображения')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
file.status === 'pending' && (
|
||||
<div className="mt-2 text-center" >
|
||||
<button
|
||||
className="btn btn-confirm"
|
||||
onClick={() => uploadSingleFile(file, 'single')
|
||||
}
|
||||
type="button"
|
||||
disabled={uploadingCount >= maxParallelUploads}
|
||||
>
|
||||
{t('start-upload')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(fileType === 'image' && selectedFile.preview) && (
|
||||
<img
|
||||
src={selectedFile.preview}
|
||||
alt="Предпросмотр загруженного изображения"
|
||||
className="uploaded-image"
|
||||
onError={() => setErrorHandler('Не удалось загрузить превью изображения')}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mt-4 flex gap-3 justify-center relative">
|
||||
<button
|
||||
className="btn btn-cancel"
|
||||
onClick={handleClearFile}
|
||||
type="button"
|
||||
>
|
||||
{isFileUploaded ? t('close') : t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-confirm ${error || isFileUploaded || uploadId ? 'disabled' : ''}`}
|
||||
onClick={() => {
|
||||
handlerFileUpload(selectedFile);
|
||||
}}
|
||||
type="button"
|
||||
disabled={error || isFileUploaded || uploadId ? true : false}
|
||||
>
|
||||
{t('upload-file')}
|
||||
</button>
|
||||
{uploadProgress !== 0 && (
|
||||
<div
|
||||
className={`absolute top-5 right-5 font-medium ${isFileUploaded ? "text-green-600" : ""}`}
|
||||
>
|
||||
{uploadProgress}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,10 +14,6 @@ export default function YandexAuthWithScript() {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [showButton, setShowButton] = useState(true);
|
||||
|
||||
const handleLogin = (token: string) => {
|
||||
console.log('handleLogin');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return;
|
||||
|
||||
@@ -25,31 +21,28 @@ export default function YandexAuthWithScript() {
|
||||
{
|
||||
client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
|
||||
response_type: 'token',
|
||||
redirect_uri: `http://localhost:2999/ru/login`,
|
||||
redirect_uri: `https://fallibly-desired-moonfish.cloudpub.ru/en/login`,
|
||||
},
|
||||
'http://localhost:2999',
|
||||
'https://fallibly-desired-moonfish.cloudpub.ru',
|
||||
{
|
||||
parentId: buttonRef.current.id,
|
||||
view: 'button',
|
||||
buttonView: 'main',
|
||||
buttonTheme: 'light',
|
||||
buttonSize: 'm',
|
||||
buttonSize: 'l',
|
||||
buttonBorderRadius: 10
|
||||
}
|
||||
)
|
||||
.then((result: any) => {
|
||||
console.log('then');
|
||||
.then(function (result: any) {
|
||||
console.log('you at least try?')
|
||||
console.log(result);
|
||||
if (result?.access_token) {
|
||||
handleLogin(result.access_token);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setShowButton(false);
|
||||
}, 1000)
|
||||
}
|
||||
return result.handler();
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Yandex auth error:', error);
|
||||
.then(function (data: any) {
|
||||
console.log('Сообщение с токеном: ', data);
|
||||
})
|
||||
.catch(function (error: any) {
|
||||
console.log('Что-то пошло не так: ', error);
|
||||
});
|
||||
}, [isReady]);
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function YandexLoginButton() {
|
||||
const clientId = 'b356a7ceac0f4c50a3b434220f86bce0';
|
||||
const redirectUri = encodeURIComponent('https://fallibly-desired-moonfish.cloudpub.ru/login/callback');
|
||||
|
||||
const authUrl = `https://oauth.yandex.ru/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&force_confirm=yes`;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={authUrl}
|
||||
className="yandex-auth-button"
|
||||
>
|
||||
Войти через Яндекс
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import {
|
||||
YooMoneyCheckoutWidget,
|
||||
YooMoneyCheckoutWidgetConfig,
|
||||
YooMoneyErrorCallbackResult
|
||||
} from "types-yoomoneycheckoutwidget";
|
||||
|
||||
let isLoading = false;
|
||||
let isInited = false;
|
||||
export default function YooWidget({ config, onComplete, onSuccess, onFail, onModalClose }: {
|
||||
/**
|
||||
* Описание https://yookassa.ru/developers/payment-acceptance/integration-scenarios/widget/reference#initialization-parameters
|
||||
*/
|
||||
config: YooMoneyCheckoutWidgetConfig,
|
||||
/** после успешной оплаты*/
|
||||
onComplete?: () => void,
|
||||
/** после успешной оплаты, только если не подписались на onComplete*/
|
||||
onSuccess?: () => void,
|
||||
/** только если вы отключили обработку неуспешных попыток в виджете: пользователь выбрал любой способ оплаты, но платеж не прошел; закончился срок действия токена.*/
|
||||
onFail?: () => void,
|
||||
/** после закрытия модального окна платежной формы если customization.modal==true */
|
||||
onModalClose?: () => void,
|
||||
}) {
|
||||
const isModal = config?.customization?.modal;
|
||||
let checkout: YooMoneyCheckoutWidget;
|
||||
const destroy = () => {
|
||||
console.log('checkout.destroy');
|
||||
checkout.destroy();
|
||||
isInited = false;
|
||||
}
|
||||
|
||||
const initializeWidget = useCallback(
|
||||
() => {
|
||||
isInited = true;
|
||||
isLoading = false;
|
||||
if (!isModal) {
|
||||
const userErrorCallBack = config.error_callback;
|
||||
config.error_callback = (error: YooMoneyErrorCallbackResult) => {
|
||||
userErrorCallBack(error);
|
||||
isInited = false;
|
||||
};
|
||||
}
|
||||
checkout = new window.YooMoneyCheckoutWidget(config);
|
||||
checkout.on('complete', (result) => {
|
||||
console.log('on complete', result);
|
||||
onComplete && onComplete();
|
||||
destroy();
|
||||
});
|
||||
onSuccess && checkout.on('success', (result) => {
|
||||
console.log('on success', result);
|
||||
onSuccess();
|
||||
destroy();
|
||||
});
|
||||
onFail && checkout.on('fail', (result) => {
|
||||
console.warn('on fail', result);
|
||||
onFail();
|
||||
destroy();
|
||||
});
|
||||
|
||||
if (config.customization?.modal) {
|
||||
checkout.on('modal_close', (result) => {
|
||||
onModalClose && onModalClose();
|
||||
destroy();
|
||||
});
|
||||
}
|
||||
checkout.render(config.customization?.modal ? undefined : 'payment-form');
|
||||
|
||||
}, [config, onSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isInited) {
|
||||
if (!window.YooMoneyCheckoutWidget) {
|
||||
isLoading = true;
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://yookassa.ru/checkout-widget/v1/checkout-widget.js';
|
||||
script.async = true;
|
||||
script.onload = initializeWidget;
|
||||
document.head.appendChild(script);
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
} else {
|
||||
initializeWidget();
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!isInited && !isModal) {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isModal ? '' : <div id="payment-form" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CopyLinkButtonProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CopyLinkButton({ className = 'btn btn-out', children = 'Скопировать ссылку' }: CopyLinkButtonProps) {
|
||||
|
||||
const copyCurrentPageLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
toast.success('Ссылка скопирована');
|
||||
} catch (err) {
|
||||
toast.error('Не удалось скопировать ссылку');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={copyCurrentPageLink}
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { downloadTrackingFile } from '@/app/actions/trackingActions';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
interface DownloadButtonProps {
|
||||
fileId: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export function DownloadButton({ fileId, fileName }: DownloadButtonProps) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const result = await downloadTrackingFile(fileId, fileName);
|
||||
|
||||
if (!result) {
|
||||
throw 'failed-to-download-file'
|
||||
}
|
||||
|
||||
const byteCharacters = atob(result.data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: result.contentType });
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = result.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success(`${t('file-is-downloading')} - `);
|
||||
} catch (error) {
|
||||
toast.error(t('failed-to-download-file'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button className="btn btn-out" onClick={handleDownload}>
|
||||
Скачать документ
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export function LoginUserBage() {
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
if (!userData?.fullName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href="#" className="header-user">
|
||||
{userData?.fullName}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
'use client'
|
||||
|
||||
import { FileItem } from '@/app/components/tanstak-table/TanstakTable';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { setMonitoring } from '@/app/actions/monitoringActions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function MonitoringDropDown({ file }: {
|
||||
file: FileItem
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentMonitoringStatus, setCurrentMonitoringStatus] = useState(file.monitoring);
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const t = useTranslations('Monitoring-status');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!dropdownRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!buttonRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonRef?.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
};
|
||||
}, [dropdownRef, setIsOpen, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (file.monitoring) {
|
||||
setCurrentMonitoringStatus(file.monitoring ? file.monitoring : 'NONE');
|
||||
}
|
||||
}, [file.monitoring]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setDropdownPosition({
|
||||
top: rect.bottom + window.scrollY,
|
||||
left: rect.left + window.scrollX,
|
||||
width: rect.width
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
async function clickHandler(monitoring: string) {
|
||||
|
||||
const response = await setMonitoring(file.id, monitoring);
|
||||
|
||||
if (response.success) {
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
setIsOpen(false);
|
||||
toast.success(t('the-file-is monitored', { fileName: file.fileName }));
|
||||
setCurrentMonitoringStatus(monitoring);
|
||||
} else {
|
||||
toast.warning(t('failed-to-establish-monitoring'));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown-wrapper monitoring-dropdown">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="dropdown-button"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">
|
||||
{t(currentMonitoringStatus)}
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && createPortal(
|
||||
<div
|
||||
className="dropdown-list-portal"
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
width: dropdownPosition.width,
|
||||
zIndex: 99999
|
||||
}}
|
||||
>
|
||||
<ul className="dropdown-list" role="listbox">
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'NONE' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('NONE')
|
||||
}}
|
||||
>
|
||||
{t('NONE')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_DAILY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('MONITORING_DAILY')
|
||||
}}
|
||||
>
|
||||
{t('MONITORING_DAILY')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_WEEKLY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('MONITORING_WEEKLY')
|
||||
}}
|
||||
>
|
||||
{t('MONITORING_WEEKLY')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_MONTHLY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('MONITORING_MONTHLY')
|
||||
}}
|
||||
>
|
||||
{t('MONITORING_MONTHLY')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function SelectedFilesAction({ filesId, callBack }: { filesId: Set<string>, callBack: (value: Set<string>) => void }) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
if (filesId.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function downloadHandler() {
|
||||
if (filesId.size) {
|
||||
const filesArray = Array.from(filesId);
|
||||
const filesString = filesArray.join(', ');
|
||||
toast.success(`${t('download')}: ${filesString}`)
|
||||
}
|
||||
}
|
||||
|
||||
function clearHandler() {
|
||||
callBack(new Set());
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="selected-files-wrapper"
|
||||
>
|
||||
<div
|
||||
className="selected-files-count"
|
||||
>
|
||||
{t('selected-files')}: {filesId.size}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="selected-files-action"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
downloadHandler();
|
||||
}}
|
||||
>
|
||||
{t('download-all')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
clearHandler();
|
||||
}}
|
||||
>
|
||||
{t('deselect')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useRef, ReactNode } from 'react';
|
||||
|
||||
interface PdfContextType {
|
||||
downloadPdf: () => Promise<void>;
|
||||
setPdfData: (data: ArrayBuffer | null, fileName: string) => void;
|
||||
}
|
||||
|
||||
const PdfContext = createContext<PdfContextType | null>(null);
|
||||
|
||||
export function PdfProvider({ children }: { children: ReactNode }) {
|
||||
const pdfDataRef = useRef<ArrayBuffer | null>(null);
|
||||
const fileNameRef = useRef<string>('');
|
||||
|
||||
const setPdfData = (data: ArrayBuffer | null, fileName: string) => {
|
||||
pdfDataRef.current = data;
|
||||
fileNameRef.current = fileName;
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
if (!pdfDataRef.current) {
|
||||
console.log('No PDF data available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = new Blob([pdfDataRef.current], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileNameRef.current || 'document.pdf';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.log('Error downloading PDF:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PdfContext.Provider value={{ downloadPdf, setPdfData }}>
|
||||
{children}
|
||||
</PdfContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePdf() {
|
||||
const context = useContext(PdfContext);
|
||||
if (!context) {
|
||||
throw new Error('usePdf must be used within PdfProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchActiveNotifications, NotificationBody } from '@/app/actions/notificationActions';
|
||||
|
||||
export interface UseActiveNotifications {
|
||||
notifications: NotificationBody[],
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
export const useActiveNotifications = () => {
|
||||
return useQuery({
|
||||
queryKey: ['activeNotifications'],
|
||||
queryFn: () => {
|
||||
return fetchActiveNotifications()
|
||||
},
|
||||
select: (data: UseActiveNotifications | null) => {
|
||||
if (!data) {
|
||||
return {
|
||||
notifications: [],
|
||||
unreadCount: 0
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
refetchInterval: 10000
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getAllFilesExtensions } from '@/app/lib/prefetch-queries';
|
||||
|
||||
|
||||
export interface allFilesExtensions {
|
||||
videos: string[];
|
||||
audios: string[];
|
||||
images: string[];
|
||||
documents: string[];
|
||||
maxFileSize: number;
|
||||
}
|
||||
|
||||
const EMPTY_EXTENSIONS: allFilesExtensions = {
|
||||
videos: [],
|
||||
audios: [],
|
||||
images: [],
|
||||
documents: [],
|
||||
maxFileSize: 0
|
||||
};
|
||||
|
||||
export const useAllFilesExtensions = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ['allFilesExtensions'],
|
||||
queryFn: getAllFilesExtensions,
|
||||
retry: false,
|
||||
initialData: EMPTY_EXTENSIONS
|
||||
});
|
||||
|
||||
return {
|
||||
...query,
|
||||
data: query.data ?? EMPTY_EXTENSIONS
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchAllNotifications, NotificationBody } from '@/app/actions/notificationActions';
|
||||
|
||||
export interface UseAllNotifications {
|
||||
notifications: NotificationBody[],
|
||||
currentPage: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useAllNotifications = (page: number, size: number = 10) => {
|
||||
return useQuery({
|
||||
queryKey: ['allNotifications', page, size],
|
||||
queryFn: () => {
|
||||
return fetchAllNotifications(page, size)
|
||||
},
|
||||
select: (data: UseAllNotifications | null) => {
|
||||
if (!data) {
|
||||
return {
|
||||
notifications: [],
|
||||
currentPage: 0,
|
||||
totalElements: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
refetchInterval: 10000
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getFileMatches } from '@/app/actions/violationActions';
|
||||
|
||||
export interface ViolationFileDetail {
|
||||
id: number;
|
||||
url: string;
|
||||
page_url: string;
|
||||
page_title: string;
|
||||
host: string;
|
||||
status: string;
|
||||
created_date: string;
|
||||
file_id: string;
|
||||
country: string;
|
||||
country_code: string;
|
||||
}
|
||||
|
||||
interface ViolationFile {
|
||||
violations: ViolationFileDetail[];
|
||||
total_elements: number;
|
||||
total_pages: number;
|
||||
current_page: number;
|
||||
page_size: number;
|
||||
has_next: boolean;
|
||||
has_previous: boolean;
|
||||
}
|
||||
|
||||
export const useFileViolations = (id: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: string,) => {
|
||||
return useQuery({
|
||||
queryKey: ['fileViolations', id, page, status, violationId, startDate, endDate],
|
||||
queryFn: () => {
|
||||
return getFileMatches(id, page, status, violationId, startDate, endDate)
|
||||
},
|
||||
select: (data: ViolationFile | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserPayments } from '@/app/actions/referralsActions';
|
||||
|
||||
export interface ReferralPayments {
|
||||
amount: number,
|
||||
completedAt: string | null,
|
||||
createdAt: string,
|
||||
id: number,
|
||||
processAt: string | null
|
||||
rejectionReason: string | null,
|
||||
status: string,
|
||||
updatedAt: string,
|
||||
}
|
||||
|
||||
export interface UserReferralPayments {
|
||||
payments: ReferralPayments[],
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useGetReferralPayments = () => {
|
||||
return useQuery({
|
||||
queryKey: ['referralPayments'],
|
||||
queryFn: getUserPayments,
|
||||
select: (data): UserReferralPayments => {
|
||||
if (!data) {
|
||||
return {
|
||||
payments: [],
|
||||
error: 'error'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
payments: data.payments,
|
||||
error: data.error
|
||||
}
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchHistoryView } from '@/app/actions/trackingActions';
|
||||
|
||||
export interface HistoryViewItem {
|
||||
city: string;
|
||||
country: string;
|
||||
viewer: string;
|
||||
document: string;
|
||||
view_date: string;
|
||||
}
|
||||
|
||||
export interface UseHistoryView {
|
||||
documentsCount: number;
|
||||
history: HistoryViewItem[];
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
uniqueCityCount: number;
|
||||
uniqueCountryCount: number;
|
||||
}
|
||||
|
||||
export const useHistoryView = (
|
||||
page: number,
|
||||
size: number,
|
||||
locale: string,
|
||||
sortDirection?: 'asc' | 'desc',
|
||||
sortBy?: string
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ['violationStatistic', page, size, locale, sortDirection, sortBy],
|
||||
queryFn: () => {
|
||||
return fetchHistoryView(page, size, locale, sortDirection, sortBy)
|
||||
},
|
||||
select: (data: UseHistoryView | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
placeholderData: (previousData) => previousData // для оптимистичных обновлений
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchLastClaims, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
|
||||
|
||||
export interface Case {
|
||||
amount: number;
|
||||
content: null | string;
|
||||
created_at: string;
|
||||
description: string;
|
||||
id: number;
|
||||
lawyer: null | string;
|
||||
name: string;
|
||||
page_number: number;
|
||||
page_size: number;
|
||||
priority: "HIGH" | "MEDIUM" | "LOW";
|
||||
total_elements: number;
|
||||
total_pages: number;
|
||||
type: "ACTIVE" | "INACTIVE" | string;
|
||||
updated_at: string;
|
||||
violation_id: number;
|
||||
file_id: string;
|
||||
}
|
||||
export interface useLastCase {
|
||||
content: Case[],
|
||||
page: number,
|
||||
size: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useLastCases = (params: ComplaintsCaseQueryParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['lastCases', params.page, params.size, params.sort_by, params.sort_direction],
|
||||
queryFn: () => {
|
||||
return fetchLastClaims(params)
|
||||
},
|
||||
select: (data: useLastCase | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchLastComplaint, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
|
||||
|
||||
export interface Complaint {
|
||||
complaint_text: string;
|
||||
created_at: string;
|
||||
email: string;
|
||||
id: number;
|
||||
not_moderated: boolean;
|
||||
status: "CREATED" | string;
|
||||
updated_at: string;
|
||||
violation_id: number;
|
||||
file_id: string;
|
||||
}
|
||||
export interface useLastComplaint {
|
||||
content: Complaint[],
|
||||
page: number,
|
||||
size: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useLastComplaints = (params: ComplaintsCaseQueryParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['lastComplaints', params.page, params.size, params.sort_by, params.sort_direction],
|
||||
queryFn: () => {
|
||||
return fetchLastComplaint(params)
|
||||
},
|
||||
select: (data: useLastComplaint | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchMonitoringInfo } from '@/app/actions/monitoringActions';
|
||||
|
||||
interface MonitoringCount {
|
||||
day: number;
|
||||
month: number;
|
||||
weekly: number;
|
||||
}
|
||||
|
||||
export const useMonitoringCount = () => {
|
||||
return useQuery({
|
||||
queryKey: ['monitoringCount'],
|
||||
queryFn: () => {
|
||||
return fetchMonitoringInfo()
|
||||
},
|
||||
select: (data: MonitoringCount | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchMonitoringStats } from '@/app/actions/monitoringActions';
|
||||
|
||||
interface MonitoringStats {
|
||||
total_searches: number;
|
||||
searches_by_status: {
|
||||
processing: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
};
|
||||
recent_searches: any[];
|
||||
}
|
||||
|
||||
export const useMonitoringStats = () => {
|
||||
return useQuery({
|
||||
queryKey: ['monitoringStats'],
|
||||
queryFn: () => {
|
||||
return fetchMonitoringStats()
|
||||
},
|
||||
select: (data: MonitoringStats | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPaymentsMethods } from '@/app/actions/paymentAction';
|
||||
|
||||
export interface PaymentsMethods {
|
||||
cardType: string,
|
||||
expiryMonth: string,
|
||||
expiryYear: string,
|
||||
lastFour: string,
|
||||
paymentMethodId: string
|
||||
}
|
||||
|
||||
export const usePaymentsMethods = () => {
|
||||
return useQuery({
|
||||
queryKey: ['paymentsMethods'],
|
||||
queryFn: () => {
|
||||
return getPaymentsMethods()
|
||||
},
|
||||
select: (data: PaymentsMethods[] | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchTrackingStats } from '@/app/actions/trackingActions';
|
||||
|
||||
export interface useTrackingStats {
|
||||
totalViews: number,
|
||||
uniqueIps: number,
|
||||
uniqueUsers: number,
|
||||
documentsCount: number,
|
||||
uniqueCountryCount: number,
|
||||
uniqueCityCount: number,
|
||||
firstViewDate: string,
|
||||
lastViewDate: string
|
||||
}
|
||||
|
||||
export const useTrackingStats = () => {
|
||||
return useQuery({
|
||||
queryKey: ['trackingStats'],
|
||||
queryFn: () => {
|
||||
return fetchTrackingStats()
|
||||
},
|
||||
select: (data: useTrackingStats | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
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;
|
||||
verifiedStatus?: 'unknow' | 'NOT_VERIFIED' | 'VERIFICATION_IN_PROGRESS' | 'VERIFIED' | 'VERIFICATION_FAILED';
|
||||
}
|
||||
|
||||
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,
|
||||
verifiedStatus: 'unknow'
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
|
||||
export interface FilesInfo {
|
||||
total: {
|
||||
size: number;
|
||||
quantity: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
images: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
videos: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
audios: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
documents: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const useUserFilesInfo = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo | null => {
|
||||
if (!data) {
|
||||
return {
|
||||
total: { size: 0, quantity: 0, check: 0, violation: 0, protected: 0 },
|
||||
images: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
videos: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
audios: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
documents: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
total: {
|
||||
size: data.all_files_size,
|
||||
quantity: data.all_files_quantity,
|
||||
check: data.all_files_check,
|
||||
violation: data.all_files_violation,
|
||||
protected: data.protected_files_count,
|
||||
},
|
||||
images: {
|
||||
quantity: data.images_quantity,
|
||||
size: data.images_size,
|
||||
check: data.images_check,
|
||||
violation: data.images_violations,
|
||||
protected: data.protected_image_files_count,
|
||||
},
|
||||
videos: {
|
||||
quantity: data.videos_quantity,
|
||||
size: data.videos_size,
|
||||
check: data.videos_check,
|
||||
violation: data.videos_violations,
|
||||
protected: data.protected_video_files_count,
|
||||
},
|
||||
audios: {
|
||||
quantity: data.audios_quantity,
|
||||
size: data.audios_size,
|
||||
check: data.audios_check,
|
||||
violation: data.audios_violations,
|
||||
protected: data.protected_audio_files_count,
|
||||
},
|
||||
documents: {
|
||||
quantity: data.document_quantity,
|
||||
size: data.document_size,
|
||||
check: data.document_check,
|
||||
violation: data.document_violations,
|
||||
protected: data.protected_document_files_count,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchPaymentsHistory } from '@/app/actions/yooKassa';
|
||||
|
||||
export interface PaymentsData {
|
||||
amount: number,
|
||||
cancellationReason: string | null,
|
||||
createdAt: string,
|
||||
id: number,
|
||||
operationType: string,
|
||||
paymentUuid: string,
|
||||
status: string,
|
||||
updatedAt: string,
|
||||
}
|
||||
|
||||
|
||||
export interface UserPaymentsData {
|
||||
payments: PaymentsData[],
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useUserPaymentsData = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userPaymentsData'],
|
||||
queryFn: fetchPaymentsHistory,
|
||||
select: (data): UserPaymentsData => {
|
||||
if (!data) {
|
||||
return {
|
||||
payments: [],
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
refetchInterval: 10000,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchViolationAnalyticStatistic } from '@/app/actions/violationActions';
|
||||
|
||||
export interface UseViolationStatistic {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export const useViolationAnalyticStatistic = (group: 'domain' | 'tld') => {
|
||||
return useQuery({
|
||||
queryKey: ['violationAnalyticStatistic', group],
|
||||
queryFn: () => {
|
||||
return fetchViolationAnalyticStatistic(group)
|
||||
},
|
||||
select: (data: UseViolationStatistic | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchViolationFinalSummaryStatistic } from '@/app/actions/violationActions';
|
||||
|
||||
export interface UseViolationFinalSummaryStatistic {
|
||||
complaints_without_case: number;
|
||||
total_complaints: number;
|
||||
total_law_cases: number;
|
||||
total_violations: number;
|
||||
violations_with_complaint: number;
|
||||
violations_without_complaint: number;
|
||||
}
|
||||
|
||||
export const useViolationFinalSummaryStatistic = () => {
|
||||
return useQuery({
|
||||
queryKey: ['violationFinalSummaryStatistic'],
|
||||
queryFn: () => {
|
||||
return fetchViolationFinalSummaryStatistic()
|
||||
},
|
||||
select: (data: UseViolationFinalSummaryStatistic | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchViolationStatistic } from '@/app/actions/violationActions';
|
||||
|
||||
export interface UseViolationStatistic {
|
||||
total_violations: number,
|
||||
new_violations: number,
|
||||
in_progress_violations: number,
|
||||
resolved_violations: number,
|
||||
total_complaints: number,
|
||||
total_law_cases: number
|
||||
}
|
||||
|
||||
export const useViolationStatistic = (id?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['violationStatistic', id],
|
||||
queryFn: () => {
|
||||
return fetchViolationStatistic(id)
|
||||
},
|
||||
select: (data: UseViolationStatistic | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const MOBILE = 'MOBILE'
|
||||
export const TABLET = 'TABLET'
|
||||
export const DESKTOP = 'DESKTOP'
|
||||
|
||||
const getDevice = (width: number) => {
|
||||
if (width < 768) return MOBILE
|
||||
else if (width < 992) return TABLET
|
||||
else return DESKTOP
|
||||
}
|
||||
|
||||
export function useViewport() {
|
||||
const [viewport, setViewport] = useState({
|
||||
width: 0,
|
||||
device: 'DESKTOP'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setViewport({
|
||||
width: window.innerWidth,
|
||||
device: getDevice(window.innerWidth)
|
||||
});
|
||||
};
|
||||
|
||||
handleResize();
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return viewport;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Форматирование даты из timestamp
|
||||
export const formatDate = (timestamp: number | string) => {
|
||||
if (!timestamp) {
|
||||
return '---'
|
||||
}
|
||||
|
||||
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}`;
|
||||
};
|
||||
|
||||
export const formatDateTime = (timestamp: number | string) => {
|
||||
if (!timestamp) {
|
||||
return '---'
|
||||
}
|
||||
|
||||
const date = new Date(timestamp);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
export 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';
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
export function getProcents(
|
||||
currentNumber: number,
|
||||
totalNumber: number,
|
||||
currentNumber: number | undefined,
|
||||
totalNumber: number | undefined,
|
||||
decimals: number = 0
|
||||
): number {
|
||||
if (!currentNumber && !totalNumber) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (totalNumber === 0) {
|
||||
if (totalNumber === 0 || typeof currentNumber === 'undefined' || typeof totalNumber === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
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';
|
||||
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
|
||||
export interface allFilesExtensions {
|
||||
videos: string[];
|
||||
audios: string[];
|
||||
images: string[];
|
||||
documents: string[];
|
||||
maxFileSize: number;
|
||||
}
|
||||
|
||||
export async function getAllFilesExtensions(): Promise<allFilesExtensions> {
|
||||
const [video, audio, image, document] = await Promise.all([
|
||||
getAllowedFilesExtensions('video'),
|
||||
getAllowedFilesExtensions('audio'),
|
||||
getAllowedFilesExtensions('image'),
|
||||
getAllowedFilesExtensions('document')
|
||||
]);
|
||||
|
||||
const result: allFilesExtensions = {
|
||||
videos: video?.file_extension ? video.file_extension : [],
|
||||
audios: audio?.file_extension ? audio.file_extension : [],
|
||||
images: image?.file_extension ? image.file_extension : [],
|
||||
documents: document?.file_extension ? document.file_extension : [],
|
||||
maxFileSize: video?.max_file_size ? video.max_file_size : 0
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||
await Promise.all([
|
||||
@@ -12,7 +40,15 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['userFilesData'],
|
||||
queryFn: () => {
|
||||
return getUserFilesData(1, 10000);
|
||||
return getUserFilesData({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
sortBy: '',
|
||||
sortOrder: '',
|
||||
type: '',
|
||||
date: '',
|
||||
query: ''
|
||||
});
|
||||
},
|
||||
staleTime: 30 * 1000
|
||||
}),
|
||||
@@ -24,5 +60,21 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => fetchReferralUserStats()
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['backendBuildDate'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await getBuildData();
|
||||
return response.buildTimeBack ?? null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['allFilesExtensions'],
|
||||
queryFn: () => getAllFilesExtensions(),
|
||||
staleTime: 5 * 60 * 1000
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -163,6 +163,35 @@ export function createValidationSchema(config: ValidationConfig, accountType?: '
|
||||
schema.password = passwordSchema;
|
||||
}
|
||||
|
||||
// inn
|
||||
if (config.inn && accountType !== 'b2c') {
|
||||
let innSchema = z.string();
|
||||
|
||||
// Минимальная длина
|
||||
if (config.inn.minSize) {
|
||||
innSchema = innSchema.min(config.inn.minSize.value, {
|
||||
error: config.inn.minSize.message
|
||||
});
|
||||
}
|
||||
|
||||
// Максимальная длина
|
||||
if (config.inn.maxSize) {
|
||||
innSchema = innSchema.max(config.inn.maxSize.value, {
|
||||
error: config.inn.maxSize.message
|
||||
});
|
||||
}
|
||||
|
||||
// Допустимые символы
|
||||
if (config.inn.allowedSymbols) {
|
||||
innSchema = innSchema.regex(
|
||||
new RegExp(config.inn.allowedSymbols.value),
|
||||
{ message: config.inn.allowedSymbols.message }
|
||||
);
|
||||
}
|
||||
|
||||
schema.inn = innSchema;
|
||||
}
|
||||
|
||||
// Дополнительные поля для формы регистрации
|
||||
schema.confirm_password = z.string();
|
||||
schema.agree = z.string().min(2, { error: 'register-error-agree' });
|
||||
|
||||
@@ -87,5 +87,19 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import Providers from './getQueryServer';
|
||||
|
||||
export function WatchDocProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
isServer,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||
|
||||
/* export function makeQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let browserQueryClient: QueryClient | undefined = undefined
|
||||
|
||||
function getQueryClient() {
|
||||
if (isServer) {
|
||||
return makeQueryClient()
|
||||
} else {
|
||||
if (!browserQueryClient) browserQueryClient = makeQueryClient()
|
||||
return browserQueryClient
|
||||
}
|
||||
} */
|
||||
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
@use './variable.scss' as v;
|
||||
|
||||
.loading-animation {
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
&.large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
&::before {
|
||||
border: 10px solid #e5e7eb;
|
||||
border-top: 10px solid #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* min-width: 80px;
|
||||
min-height: 80px; */
|
||||
|
||||
&.start {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.loading-animation {
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.global-spinner {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
@use './reports.scss';
|
||||
@use './privacy-and-terms-pages.scss';
|
||||
@use './VKLogin.scss';
|
||||
@use './animation.scss';
|
||||
@use './violation-details.scss';
|
||||
@use './watch-doc.scss';
|
||||
@use './tracking-statistic.scss';
|
||||
|
||||
.page-title {
|
||||
font-size: 30px;
|
||||
@@ -151,10 +155,50 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.btn,
|
||||
.btn-s,
|
||||
.btn-l {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.btn-s {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-l {
|
||||
padding: 16px 32px;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, v.$p-color, v.$s-color);
|
||||
/* background: linear-gradient(135deg, v.$p-color, v.$s-color); */
|
||||
background: #6366f1;
|
||||
color: v.$white;
|
||||
border: 2px solid transparent;
|
||||
|
||||
@@ -179,6 +223,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: v.$bg-hover;
|
||||
color: v.$text-p;
|
||||
border: 2px solid v.$border-color-1;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid v.$p-color;
|
||||
@@ -190,6 +244,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: v.$color-warning;
|
||||
|
||||
&:hover {
|
||||
background: v.$color-warning-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
@@ -306,6 +368,14 @@
|
||||
width: var(--side-bar-width);
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.build-versions {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
color: #fffc;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-close-button {
|
||||
@@ -340,7 +410,7 @@
|
||||
svg {
|
||||
margin-right: 15px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@@ -545,10 +615,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-list-portal {
|
||||
.dropdown-list {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
background-color: #ffffff;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #dbeafe;
|
||||
}
|
||||
|
||||
&.current {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.monitoring-dropdown {
|
||||
max-width: 184px;
|
||||
margin: 0 auto;
|
||||
|
||||
.dropdown-button {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dropdown-list {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: auto;
|
||||
z-index: 9999;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
|
||||
transform-origin: top left;
|
||||
|
||||
background-color: #ffffff;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -627,4 +759,88 @@
|
||||
|
||||
.yaPreloadingSuggestBlockContainer {
|
||||
height: 52px !important;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.error-message-text {
|
||||
color: #dc2626;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.yandex-auth-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: v.$black;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:hover {
|
||||
background: #1a1a1a;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
|
||||
/* transform: translateY(-2px); */
|
||||
}
|
||||
}
|
||||
|
||||
.search-settings {
|
||||
&-wrapper {
|
||||
margin-bottom: 20px;
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
font-weight: 600;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
margin-left: 4px;
|
||||
color: v.$red;
|
||||
|
||||
&.active {
|
||||
color: v.$green;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* &-action-panel {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
} */
|
||||
}
|
||||
@@ -52,4 +52,7 @@ path:focus {
|
||||
outline-style: none;
|
||||
}
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@import 'swiper/css';
|
||||
@import 'swiper/css/navigation';
|
||||
@import 'swiper/css/pagination';
|
||||
+113
-29
@@ -4,11 +4,12 @@
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
background: v.$white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 20px v.$shadow-1;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto 30px;
|
||||
|
||||
@media (max-width: 490px) {
|
||||
padding: 20px 20px;
|
||||
@@ -49,6 +50,10 @@
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
/* &.active {
|
||||
background: v.$color-warning;
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +90,11 @@
|
||||
@media (max-width: 440px) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: v.$p-color;
|
||||
color: v.$white;
|
||||
}
|
||||
}
|
||||
|
||||
.tokens-count {
|
||||
@@ -110,7 +120,7 @@
|
||||
background: v.$white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 10px 10px 20px 10px v.$shadow-1;
|
||||
min-width: 320px;
|
||||
min-width: 350px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
@@ -124,8 +134,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
.notification-header {
|
||||
.notification {
|
||||
/* &-dropdown {} */
|
||||
|
||||
&-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid v.$b-color-1;
|
||||
display: flex;
|
||||
@@ -138,65 +150,137 @@
|
||||
font-weight: 600;
|
||||
color: v.$text-p;
|
||||
}
|
||||
|
||||
.notification-count {
|
||||
font-size: 12px;
|
||||
color: v.$p-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 400px;
|
||||
&-count {
|
||||
font-size: 10px;
|
||||
background: v.$p-color;
|
||||
padding: 5px;
|
||||
color: v.$white;
|
||||
font-weight: 500;
|
||||
border-radius: 5px;
|
||||
margin-left: 5px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
&-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 15px 20px;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid v.$b-color-1;
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
&-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: v.$text-p;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
&-content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
&-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 13px;
|
||||
color: v.$text-s;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 12px;
|
||||
&-data {
|
||||
font-size: 10px;
|
||||
color: v.$text-m;
|
||||
}
|
||||
|
||||
.notification-footer {
|
||||
padding: 12px 20px;
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
height: max-content;
|
||||
color: v.$p-color;
|
||||
align-self: start;
|
||||
padding-top: 5px;
|
||||
|
||||
svg {
|
||||
fill: v.$p-color;
|
||||
|
||||
&:hover {
|
||||
fill: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&-mark-all-as-read {
|
||||
font-size: 10px;
|
||||
color: v.$p-color;
|
||||
text-align: end;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
padding: 8px 20px;
|
||||
border-top: 1px solid v.$b-color-1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
.notification-link {
|
||||
color: v.$p-color;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-unread-count {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
/* width: 22px;
|
||||
height: 22px; */
|
||||
border-radius: 15px;
|
||||
background: v.$red;
|
||||
color: v.$white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
|
||||
@@ -112,6 +112,15 @@
|
||||
&.password {
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.valid-card {
|
||||
border: 2px solid #2f9e44;
|
||||
background: #ecf8ee;
|
||||
}
|
||||
}
|
||||
|
||||
.form-options {
|
||||
@@ -172,16 +181,47 @@
|
||||
margin-bottom: 18px;
|
||||
margin: 20px 0 0 0;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
background: linear-gradient(135deg, #414291, #4b3975);
|
||||
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.refresh {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group-confrim-window {
|
||||
min-width: 500px;
|
||||
|
||||
&-button-group {
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
min-width: auto;
|
||||
|
||||
&-button-group {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
+1750
-310
File diff suppressed because it is too large
Load Diff
@@ -201,10 +201,18 @@
|
||||
}
|
||||
|
||||
.subscription-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
/* display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-columns: repeat(4, auto);
|
||||
gap: 20px;
|
||||
margin-bottom: 25px;
|
||||
justify-content: center; */
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 278px));
|
||||
gap: 18px;
|
||||
margin-bottom: 25px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
@@ -216,7 +224,7 @@
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
max-width: 350px;
|
||||
max-width: 278px;
|
||||
|
||||
.plan-header {
|
||||
text-align: center;
|
||||
@@ -249,6 +257,7 @@
|
||||
.plan-period {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.plan-tokens {
|
||||
@@ -260,6 +269,11 @@
|
||||
font-weight: 600;
|
||||
margin: 12px auto;
|
||||
display: inline-block;
|
||||
|
||||
&.header {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-features {
|
||||
@@ -293,6 +307,97 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.token-package {
|
||||
background: #f8fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 12px 30px rgba(99, 102, 241, 0.2);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: #10b981;
|
||||
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.token-package-amount {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.token-package-text {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.token-package-price {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.token-package-detail {
|
||||
color: #64748b;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-button {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
padding: 12px 18px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.selected-token-info {
|
||||
margin-top: 8px;
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.secure-payments-wrapper {
|
||||
@@ -319,16 +424,38 @@
|
||||
|
||||
.secure-payments-card {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
/* &:has(.yookassa) {
|
||||
align-self: flex-end;
|
||||
} */
|
||||
|
||||
.secure-payments-card-header {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 8px;
|
||||
height: 44px;
|
||||
|
||||
&.yookassa {
|
||||
max-width: 88px;
|
||||
margin: 0 auto 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #1e293b;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.95rem;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
max-width: 88px;
|
||||
margin: 0 auto 8px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
+38
-15
@@ -7,6 +7,7 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 30px;
|
||||
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||
|
||||
.enhanced-chart-title {
|
||||
font-size: 22px;
|
||||
@@ -14,12 +15,7 @@
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '🔍';
|
||||
margin-right: 12px;
|
||||
font-size: 28px;
|
||||
}
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-stats-grid {
|
||||
@@ -49,7 +45,13 @@
|
||||
.search-stat-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 15px;
|
||||
display: block;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-stat-value {
|
||||
@@ -68,11 +70,19 @@
|
||||
}
|
||||
|
||||
.analytics-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
.tanstak-table-wrapper .tanstak-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1370px) {
|
||||
flex-direction: column;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.analytics-card {
|
||||
background: white;
|
||||
@@ -80,10 +90,12 @@
|
||||
padding: 30px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
flex: 1;
|
||||
min-width: 350px;
|
||||
max-width: calc(50% - 15px);
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 970px) {
|
||||
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
.analytics-header {
|
||||
@@ -152,6 +164,7 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 20px 40px rgba(30, 58, 138, 0.4);
|
||||
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
@@ -189,10 +202,14 @@
|
||||
|
||||
.summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
gap: 30px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
@media (max-width: 940px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-stat {
|
||||
@@ -204,6 +221,12 @@
|
||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||
transition: all 0.4s ease;
|
||||
|
||||
&.center {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
|
||||
@@ -219,6 +219,64 @@
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.verefication-section {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.verefication-block {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.drag-drop-zone {
|
||||
border: 2px dashed #6366f1;
|
||||
border-radius: .75rem;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.selected-file-action-panel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.selected-file-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: v.$text-s;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: v.$p-color;
|
||||
color: v.$white;
|
||||
|
||||
background: linear-gradient(135deg, v.$p-color, v.$s-color);
|
||||
color: v.$white;
|
||||
border: 2px solid transparent;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
border: 2px solid v.$p-color;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
@use './variable.scss' as v;
|
||||
|
||||
:root {
|
||||
--p: #6366f1;
|
||||
--pd: #4f46e5;
|
||||
--pl: #e0e7ff;
|
||||
--g: #10b981;
|
||||
--a: #f59e0b;
|
||||
--r: #f43f5e;
|
||||
--s: #0ea5e9;
|
||||
--bg: #f1f5f9;
|
||||
--sur: #fff;
|
||||
--brd: #e2e8f0;
|
||||
--tx: #1e293b;
|
||||
--mu: #64748b;
|
||||
--rad: 14px;
|
||||
--sh: 0 2px 10px rgba(0, 0, 0, .07);
|
||||
}
|
||||
|
||||
.tracking-page {
|
||||
.general-statistic {
|
||||
background: linear-gradient(135deg, v.$p-color 55%, v.$s-color);
|
||||
border-radius: var(--rad);
|
||||
padding: 32px;
|
||||
color: #fff;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
&-sub {
|
||||
font-size: 13px;
|
||||
opacity: .75;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
background: rgba(255, 255, 255, .13);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid rgba(255, 255, 255, .15);
|
||||
|
||||
&-ico {
|
||||
font-size: 20px;
|
||||
margin-bottom: 6px;
|
||||
svg {
|
||||
fill: v.$white;
|
||||
color: v.$white;
|
||||
}
|
||||
}
|
||||
|
||||
&-lbl {
|
||||
font-size: 11px;
|
||||
opacity: .8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&-val {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
$p-color: #6366f1;
|
||||
$p-color-hover: #1e0ff0;
|
||||
$s-color: #8b5cf6;
|
||||
$text-p: #1f2937;
|
||||
$text-s: #4b5563;
|
||||
@@ -10,16 +11,31 @@ $bg-hover: #e9ecef;
|
||||
$bg-light: #f8f9ff;
|
||||
$bg-darkening: #0000007a;
|
||||
$shadow-1: #0000001a;
|
||||
$shadow-2: #8686861a;
|
||||
$white: #fff;
|
||||
$black: #000;
|
||||
$red: #dc2626;
|
||||
$green: #00a63e;
|
||||
$green-2: #10b981;
|
||||
$grey: #5a6e8a;
|
||||
$p-color-disabled: #6365f18e;
|
||||
$s-color-disabled: #8a5cf663;
|
||||
$border-color-1: #e2e8f0;
|
||||
$border-color-1-hover: #b1b7be;
|
||||
$color-warning: #fab005;
|
||||
$color-warning-hover: #fa9805;
|
||||
$color-warning-2: #f08c00;
|
||||
|
||||
$color-image: #f08c00;
|
||||
$color-video: #2f9e44;
|
||||
$color-audio: #1971c2;
|
||||
$color-document: #a561e6;
|
||||
|
||||
$status-new: #fab005;
|
||||
$status-showed: #5a6e8a;
|
||||
$status-in-work: #3b82f6;
|
||||
$authorized-use: #2ecc71;
|
||||
|
||||
:root {
|
||||
--side-bar-width: 280px;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,319 @@
|
||||
@use './variable.scss' as v;
|
||||
|
||||
:root {
|
||||
--bg: #f5f6fa;
|
||||
--card: #fff;
|
||||
--border: #e2e5ef;
|
||||
--border2: #d0d4e4;
|
||||
--text: #0f1221;
|
||||
--text2: #4b5270;
|
||||
--muted: #8890ad;
|
||||
--accent: #5b47e0;
|
||||
--accent2: #7c5cf6;
|
||||
--green: #059669;
|
||||
--green-bg: #ecfdf5;
|
||||
--green-bd: #a7f3d0;
|
||||
--red: #dc2626;
|
||||
--red-bg: #fef2f2;
|
||||
--amber: #b45309;
|
||||
--shadow: 0 2px 12px rgba(15, 18, 33, .07);
|
||||
}
|
||||
|
||||
.watch-doc {
|
||||
&-wrapper {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
|
||||
|
||||
.btn-acc {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 3px 12px rgba(91, 71, 224, .28);
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent2);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-out {
|
||||
background: var(--card);
|
||||
color: var(--text2);
|
||||
border: 1px solid var(--border2);
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.document-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
gap: 10px;
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__next,
|
||||
&__prev {
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent);
|
||||
background: none;
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 6px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
display: inline;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__current-page {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&__fullscreen {
|
||||
font-size: .75rem;
|
||||
color: var(--accent);
|
||||
background: none;
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 6px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prev-wrap {
|
||||
min-height: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-canvas {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.pdf-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.pdf-container:fullscreen {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.prev-wrap {
|
||||
flex: 1;
|
||||
min-height: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.pdf-canvas {
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 120px);
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.document-controls {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
z-index: 1000;
|
||||
min-width: 300px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-page {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px 60px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 0 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 28px;
|
||||
|
||||
.header-logo {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: v.$p-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.header-user {
|
||||
font-size: .83rem;
|
||||
color: var(--text2);
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 7px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&-status {
|
||||
background: var(--green-bg);
|
||||
border: 1.5px solid var(--green-bd);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
margin-bottom: 22px;
|
||||
padding: 20px 22px;
|
||||
border-radius: 14px;
|
||||
|
||||
.st-title {
|
||||
color: var(--green);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.st-sub {
|
||||
font-size: .83rem;
|
||||
color: var(--text2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
&-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
text-align: center;
|
||||
padding: 28px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: .76rem;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
height: 100%;
|
||||
|
||||
.card-head {
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
|
||||
font-size: .78rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .6px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 14px 20px;
|
||||
|
||||
.owner-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.owner-av {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.owner-name {
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.owner-meta {
|
||||
color: var(--muted);
|
||||
}
|
||||
}
|
||||
|
||||
.download-description {
|
||||
font-size: .82rem;
|
||||
color: var(--text2);
|
||||
margin-bottom: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user