Compare commits
308
Commits
913a199d95
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6696cc1027 | ||
|
|
98cb5ab09b | ||
|
|
c501c2bbbf | ||
|
|
a74ea966ad | ||
|
|
0a7d29419f | ||
|
|
a9599d8a1f | ||
|
|
cbc1311e9d | ||
|
|
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 |
@@ -34,3 +34,11 @@ docker network inspect no-copy_app-network
|
||||
|
||||
docker volume ls - посмотреть все базы данных
|
||||
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
|
||||
Generated
+289
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.44.0",
|
||||
"version": "0.104.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.44.0",
|
||||
"version": "0.104.0",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@react-input/mask": "^2.0.4",
|
||||
@@ -15,11 +15,13 @@
|
||||
"@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",
|
||||
@@ -39,6 +41,7 @@
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"sass": "^1.94.2",
|
||||
"types-yoomoneycheckoutwidget": "^0.0.2",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
@@ -1127,6 +1130,256 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
|
||||
"integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.100"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
|
||||
"integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
|
||||
"integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -3415,6 +3668,15 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/card-validator": {
|
||||
"version": "10.0.4",
|
||||
"resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.4.tgz",
|
||||
"integrity": "sha512-1Q/tDCSvM5iL3psPo6j8KF/VoI63WA7Y4Zthy1E7l/H4Iul2e2X+1S0vp33n3OMRjLgGU2+gLaK2NsGtwArJkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"credit-card-type": "^10.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -3503,6 +3765,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/credit-card-type": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.1.2.tgz",
|
||||
"integrity": "sha512-fP9vlyeYSHfKbIdE6IOR63YivTXMy0ZqyEkBhHxuAbvpltepe+zJmQ34A2Va7zJ8J7ZaB5IMAdD+1YJOmsajJA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -6556,6 +6824,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.7.284",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz",
|
||||
"integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=22.13.0 || >=24"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.100"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -7732,6 +8012,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/types-yoomoneycheckoutwidget": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/types-yoomoneycheckoutwidget/-/types-yoomoneycheckoutwidget-0.0.2.tgz",
|
||||
"integrity": "sha512-Vab30GrWEE5qq65CbBcJW5deFjJ9jRVlmh+ZneaUsqnPKPlWHGwhA1ukx+o70LjqEdgK+HgMkxNlFsqchLX7ww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.48.0",
|
||||
"version": "0.122.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
@@ -16,11 +16,13 @@
|
||||
"@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",
|
||||
@@ -40,6 +42,7 @@
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"sass": "^1.94.2",
|
||||
"types-yoomoneycheckoutwidget": "^0.0.2",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ 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',
|
||||
@@ -51,7 +52,7 @@ export default function Page() {
|
||||
<VKLogin />
|
||||
</div>
|
||||
<div>
|
||||
<YandexAuthWithScript />
|
||||
<YandexLoginButton />
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,26 +3,11 @@ 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
|
||||
|
||||
@@ -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,21 +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,45 +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';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import SearchTitle from '@/app/ui/search/search-title';
|
||||
|
||||
export default async function Page() {
|
||||
const { file_extension: extensionVideo, max_file_size } = await getAllowedFilesExtensions('video');
|
||||
const { file_extension: extensionAudio } = await getAllowedFilesExtensions('audio');
|
||||
const { file_extension: extensionImage } = await getAllowedFilesExtensions('image');
|
||||
const { file_extension: extensionDocument } = await getAllowedFilesExtensions('document');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="block-wrapper">
|
||||
<h1 className="page-title">
|
||||
Поиск защищенного контента
|
||||
</h1>
|
||||
<p className="page-description">
|
||||
Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете
|
||||
</p>
|
||||
</div>
|
||||
<SearchTitle />
|
||||
<div className="search-grid">
|
||||
<SectionSearchFile
|
||||
maxFileSize={max_file_size}
|
||||
/* allowedExtensions={allExtensions} */
|
||||
allowedExtensions={{
|
||||
images: extensionImage,
|
||||
videos: extensionVideo,
|
||||
audios: extensionAudio,
|
||||
documents: extensionDocument
|
||||
}}
|
||||
/>
|
||||
<SectionSearchFile />
|
||||
<div className="search-sidebar">
|
||||
<SupportedFormats
|
||||
extensionVideo={extensionVideo}
|
||||
extensionAudio={extensionAudio}
|
||||
extensionImage={extensionImage}
|
||||
extensionDocument={extensionDocument}
|
||||
/>
|
||||
<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');
|
||||
@@ -24,6 +25,7 @@ export default function Page() {
|
||||
{/* 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,39 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useParams } from 'next/navigation'
|
||||
import ViolationPageTitle from '@/app/ui/violations/file-page/violation-page-title';
|
||||
import ViolationPageViolationInfo from '@/app/ui/violations/file-page/violation-page-violation-info';
|
||||
import ViolationPpageFileStatistic from '@/app/ui/violations/file-page/violation-page-file-statistic';
|
||||
import ViolationPageFileInfo from '@/app/ui/violations/file-page/violation-page-file-info';
|
||||
import ViolationPageActions from '@/app/ui/violations/file-page/violation-page-actions';
|
||||
import ViolationPageViolationsList from '@/app/ui/violations/file-page/violation-page-violations-list';
|
||||
import ViolationPageNote from '@/app/ui/violations/file-page/violation-page-note';
|
||||
|
||||
/* violation-details */
|
||||
export default function Page() {
|
||||
const params = useParams()
|
||||
const id = params.id
|
||||
|
||||
return (
|
||||
<div
|
||||
className="violation-details"
|
||||
>
|
||||
<ViolationPageTitle />
|
||||
<div
|
||||
className="grid grid-cols-[2fr_1fr] gap-3"
|
||||
>
|
||||
<div>
|
||||
<ViolationPageViolationInfo />
|
||||
<ViolationPageViolationsList />
|
||||
<ViolationPageNote />
|
||||
</div>
|
||||
<div>
|
||||
<ViolationPpageFileStatistic />
|
||||
<ViolationPageFileInfo />
|
||||
<ViolationPageActions />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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,151 @@
|
||||
'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;
|
||||
appealInfos?: any[]; //Не знаю что тут будет
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -44,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}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@ export async function getUserFilesInfo() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTariffs() {
|
||||
export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
@@ -81,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}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -107,6 +108,7 @@ export async function fetchTariffs() {
|
||||
}
|
||||
|
||||
export async function fetchTokensBundle() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
@@ -121,7 +123,8 @@ export async function fetchTokensBundle() {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,6 +145,7 @@ export async function fetchTokensBundle() {
|
||||
}
|
||||
|
||||
export async function getBuildData() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check/api/build`, {
|
||||
@@ -149,6 +153,7 @@ export async function getBuildData() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -164,7 +169,7 @@ export async function getBuildData() {
|
||||
|
||||
export async function fetchINN(inn: string) {
|
||||
try {
|
||||
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,
|
||||
|
||||
+55
-17
@@ -4,6 +4,7 @@ import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/action
|
||||
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');
|
||||
@@ -71,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,
|
||||
@@ -88,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 {
|
||||
@@ -150,9 +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';
|
||||
|
||||
const mail_verified = true;
|
||||
/* вернуть в 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 || '';
|
||||
@@ -163,6 +167,12 @@ export async function registration(
|
||||
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);
|
||||
|
||||
const dataToValidate = {
|
||||
@@ -225,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,
|
||||
@@ -237,6 +247,8 @@ export async function registration(
|
||||
password,
|
||||
accountType,
|
||||
mail_verified: mail_verified,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||
...(referralCode !== '' && { referralLink: referralCode })
|
||||
}
|
||||
@@ -253,6 +265,8 @@ export async function registration(
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||
...(referralCode !== '' && { referralLink: referralCode })
|
||||
})
|
||||
@@ -260,7 +274,7 @@ export async function registration(
|
||||
if (response.ok) {
|
||||
let parsed = await response.json() as {
|
||||
message_desc: string,
|
||||
message_body: { token: string, userId: number },
|
||||
message_body: { token: string, userId: number, fieldErrors: string },
|
||||
message_code: number
|
||||
};
|
||||
|
||||
@@ -387,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
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -418,14 +430,14 @@ export async function confirmEmail(userId: number, verifyToken: string) {
|
||||
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,
|
||||
verify_token: verifyToken
|
||||
user_id: userId
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -461,7 +473,6 @@ export async function confirmEmail(userId: number, verifyToken: string) {
|
||||
export async function resendConfirmEmail(userId: number) {
|
||||
console.log('resendConfirmEmail');
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -511,10 +522,9 @@ export async function resetPassword(
|
||||
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',
|
||||
@@ -598,6 +608,7 @@ export async function confirmPassword(
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${verifyToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -605,7 +616,6 @@ export async function confirmPassword(
|
||||
message_body: {
|
||||
email: email,
|
||||
password: password,
|
||||
verifyToken: verifyToken,
|
||||
action: "confirmVerification"
|
||||
}
|
||||
}),
|
||||
@@ -618,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,
|
||||
@@ -638,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,13 +95,15 @@ export async function companyUserRegistration(
|
||||
phone: phone,
|
||||
password,
|
||||
accountType: 'b2b',
|
||||
authToken: token,
|
||||
mail_verified: false
|
||||
mail_verified: false,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,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}`);
|
||||
@@ -118,7 +120,9 @@ export async function companyUserRegistration(
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password
|
||||
password,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -118,3 +140,66 @@ export async function viewFileInfo(fileId: string) {
|
||||
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,
|
||||
@@ -22,15 +20,11 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
|
||||
}
|
||||
const message = messageBody;
|
||||
message.action = 'init';
|
||||
message.token = token;
|
||||
|
||||
if (convertTo && convertTo !== 'reject') {
|
||||
message.convertTo = convertTo;
|
||||
}
|
||||
|
||||
// нужно будет добавить свойство convertTo в тело запроса он может прийти со значением reject тогда конвертировать не нужно.
|
||||
// console.log(convertTo);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -41,7 +35,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,6 +66,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
|
||||
}
|
||||
|
||||
export async function cancelUpload(udloadId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -84,7 +81,8 @@ export async function cancelUpload(udloadId: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,16 +114,30 @@ 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();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
@@ -161,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',
|
||||
@@ -174,7 +188,8 @@ export async function checkChunkStatus(upload_id: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -196,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',
|
||||
@@ -209,7 +226,8 @@ export async function getAllowedFilesExtensions(type: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -229,3 +247,47 @@ export async function getAllowedFilesExtensions(type: string) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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,14 +96,14 @@ 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}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,25 +137,18 @@ export async function referralRefill() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'refill',
|
||||
token: token,
|
||||
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: 2000,
|
||||
})
|
||||
let parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
@@ -359,15 +352,15 @@ export async function createPayoutRequest(
|
||||
const token = await getSessionData('token');
|
||||
const amount = formData.get('amount') as string || '';
|
||||
const payoutMethodId = formData.get('payoutMethodId') as string || '';
|
||||
|
||||
console.log('createPayoutRequest');
|
||||
console.log(token);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"amount": amount,
|
||||
"payoutMethodId": payoutMethodId
|
||||
'amount': amount,
|
||||
'payoutMethodId': payoutMethodId
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -375,13 +368,22 @@ export async function createPayoutRequest(
|
||||
}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
|
||||
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);
|
||||
@@ -393,12 +395,29 @@ export async function createPayoutRequest(
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'error-request';
|
||||
throw 'payment-error';
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
|
||||
if (error) {
|
||||
const typedError = error as string;
|
||||
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: {
|
||||
@@ -412,7 +431,7 @@ export async function createPayoutRequest(
|
||||
previousState: {
|
||||
amount
|
||||
},
|
||||
error: 'error-request'
|
||||
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,63 +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,
|
||||
} */
|
||||
|
||||
/* return {
|
||||
images: [],
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
totalPages: 0,
|
||||
totalResults: 0
|
||||
} */
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
@@ -84,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}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,13 +75,26 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
|
||||
}
|
||||
|
||||
} catch (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: 'error'
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +114,7 @@ export async function getSearchStats() {
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
return parsed;
|
||||
} else {
|
||||
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;
|
||||
@@ -99,7 +99,7 @@ export async function deleteSession() {
|
||||
console.log('delete session')
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session');
|
||||
cookieStore.delete('session_nc_db');
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
'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');
|
||||
console.log('fetchDocumentInfo');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/files/public/file-info/${fileId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
return parsed;
|
||||
} 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);
|
||||
|
||||
+19
-14
@@ -8,6 +8,7 @@ const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV
|
||||
|
||||
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());
|
||||
@@ -17,7 +18,10 @@ export async function makePaymentOperation(email: string, tariffId: number, oper
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/create`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -36,7 +40,6 @@ export async function makePaymentOperation(email: string, tariffId: number, oper
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -48,38 +51,39 @@ export async function yooKasaCreatePayment(amount: number, tariff: number, opera
|
||||
|
||||
const createPayload: ICreatePayment = {
|
||||
amount: {
|
||||
value: amount.toLocaleString().replace(/\s/g, ''),
|
||||
value: amount.toLocaleString().replace(/[,\s.]/g, ''),
|
||||
currency: 'RUB'
|
||||
},
|
||||
payment_method_data: {
|
||||
type: 'bank_card'
|
||||
},
|
||||
confirmation: {
|
||||
type: 'redirect',
|
||||
return_url: 'test'
|
||||
type: 'embedded',
|
||||
},
|
||||
metadata: {
|
||||
userMail: userEmail,
|
||||
tariff: tariff
|
||||
},
|
||||
capture: true
|
||||
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 (response && payment) {
|
||||
return payment?.confirmation.confirmation_url;
|
||||
|
||||
if (payment && response) {
|
||||
return payment?.confirmation.confirmation_token;
|
||||
} else {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
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`, {
|
||||
@@ -94,7 +98,8 @@ export async function fetchPaymentsHistory() {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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';
|
||||
@@ -113,8 +113,16 @@ export default function UploadSectionFile({
|
||||
setError(null);
|
||||
|
||||
const newFiles: SelectedFile[] = [];
|
||||
const selectedFileNames = new Set(selectedFiles.map(f => f.name));
|
||||
const currentFileNames = new Set<string>();
|
||||
const duplicateNames: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (selectedFileNames.has(file.name)) {
|
||||
duplicateNames.push(file.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
const validation = validateFile(file);
|
||||
|
||||
if (!validation.isValid) {
|
||||
@@ -141,6 +149,8 @@ export default function UploadSectionFile({
|
||||
}
|
||||
}
|
||||
|
||||
currentFileNames.add(file.name);
|
||||
|
||||
newFiles.push({
|
||||
file,
|
||||
name: file.name,
|
||||
@@ -152,8 +162,13 @@ export default function UploadSectionFile({
|
||||
});
|
||||
}
|
||||
|
||||
if (duplicateNames.length > 0) {
|
||||
const uniqueDuplicates = [...new Set(duplicateNames)];
|
||||
setError(`${t('duplicate-files')}: ${uniqueDuplicates.join(', ')}`);
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||
}, [fileType, t]);
|
||||
}, [fileType, selectedFiles]);
|
||||
|
||||
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
@@ -204,6 +219,7 @@ export default function UploadSectionFile({
|
||||
f.uploadId !== fileId && f.name !== fileId
|
||||
));
|
||||
} else {
|
||||
console.log('clear all')
|
||||
selectedFiles.forEach(file => {
|
||||
if (file.preview) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
@@ -223,10 +239,21 @@ export default function UploadSectionFile({
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const uploadSingleFile = async (fileInfo: SelectedFile): Promise<void> => {
|
||||
const uploadSingleFile = async (fileInfo: SelectedFile, uploadType: 'single' | 'multi'): Promise<void> => {
|
||||
const fileId = `${fileInfo.name}-${Date.now()}`;
|
||||
isCancelledRef.current.set(fileId, false);
|
||||
|
||||
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 {
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'uploading' } : f
|
||||
@@ -315,13 +342,20 @@ export default function UploadSectionFile({
|
||||
}, [selectedFiles]);
|
||||
|
||||
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)));
|
||||
await Promise.all(batch.map(file => uploadSingleFile(file, 'multi')));
|
||||
|
||||
if (selectedFilesRef.current.some(f => f.status === 'pending')) {
|
||||
await uploadNextBatch();
|
||||
@@ -329,6 +363,10 @@ export default function UploadSectionFile({
|
||||
};
|
||||
|
||||
await uploadNextBatch();
|
||||
} else {
|
||||
const errorMessage = `${t('there-are-not-enough-funds-to-protect-files', { count: priceCheck?.tokens_count })}`;
|
||||
setError(errorMessage);
|
||||
}
|
||||
}, [maxParallelUploads]);
|
||||
|
||||
const getTotalProgress = useMemo(() => {
|
||||
@@ -399,7 +437,6 @@ export default function UploadSectionFile({
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="upload-section" >
|
||||
<h3>
|
||||
@@ -565,15 +602,32 @@ export default function UploadSectionFile({
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<span className={
|
||||
`upload-progress ${file.status === 'completed' ? 'text-green-600' :
|
||||
file.status === 'uploading' ? 'text-blue-600' :
|
||||
'text-gray-600'
|
||||
}`
|
||||
}>
|
||||
{file.progress} %
|
||||
</span>
|
||||
<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)}
|
||||
@@ -607,7 +661,7 @@ export default function UploadSectionFile({
|
||||
<div className="mt-2 text-center" >
|
||||
<button
|
||||
className="btn btn-confirm"
|
||||
onClick={() => uploadSingleFile(file)
|
||||
onClick={() => uploadSingleFile(file, 'single')
|
||||
}
|
||||
type="button"
|
||||
disabled={uploadingCount >= maxParallelUploads}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -2,29 +2,29 @@
|
||||
|
||||
import { FileItem } from '@/app/components/tanstak-table/TanstakTable';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
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('without-monitoring');
|
||||
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('Global')
|
||||
const t = useTranslations('Monitoring-status');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
console.log('click');
|
||||
if (!dropdownRef?.current) {
|
||||
return;
|
||||
}
|
||||
@@ -49,10 +49,10 @@ export function MonitoringDropDown({ file }: {
|
||||
}, [dropdownRef, setIsOpen, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (file.status) {
|
||||
setCurrentMonitoringStatus(file.status === 'ACTIVE' ? 'without-monitoring' : 'without-monitoring');
|
||||
if (file.monitoring) {
|
||||
setCurrentMonitoringStatus(file.monitoring ? file.monitoring : 'NONE');
|
||||
}
|
||||
}, [file]);
|
||||
}, [file.monitoring]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && buttonRef.current) {
|
||||
@@ -66,10 +66,17 @@ export function MonitoringDropDown({ file }: {
|
||||
}, [isOpen]);
|
||||
|
||||
async function clickHandler(monitoring: string) {
|
||||
console.log('click', file.id);
|
||||
setCurrentMonitoringStatus(monitoring);
|
||||
|
||||
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 (
|
||||
@@ -113,36 +120,36 @@ export function MonitoringDropDown({ file }: {
|
||||
>
|
||||
<ul className="dropdown-list" role="listbox">
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'without-monitoring' ? 'current' : ''}`}
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'NONE' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('without-monitoring')
|
||||
clickHandler('NONE')
|
||||
}}
|
||||
>
|
||||
{t('without-monitoring')}
|
||||
{t('NONE')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'every-day' ? 'current' : ''}`}
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_DAILY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('every-day')
|
||||
clickHandler('MONITORING_DAILY')
|
||||
}}
|
||||
>
|
||||
{t('every-day')}
|
||||
{t('MONITORING_DAILY')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'every-week' ? 'current' : ''}`}
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_WEEKLY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('every-week')
|
||||
clickHandler('MONITORING_WEEKLY')
|
||||
}}
|
||||
>
|
||||
{t('every-week')}
|
||||
{t('MONITORING_WEEKLY')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'every-month' ? 'current' : ''}`}
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_MONTHLY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('every-month')
|
||||
clickHandler('MONITORING_MONTHLY')
|
||||
}}
|
||||
>
|
||||
{t('every-month')}
|
||||
{t('MONITORING_MONTHLY')}
|
||||
</li>
|
||||
</ul>
|
||||
</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,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
|
||||
});
|
||||
};
|
||||
@@ -39,6 +39,7 @@ export interface UserData {
|
||||
tariffs: Tariff[];
|
||||
tariffInfo: TariffInfo;
|
||||
permission: number;
|
||||
verifiedStatus?: 'unknow' | 'NOT_VERIFIED' | 'VERIFICATION_IN_PROGRESS' | 'VERIFIED' | 'VERIFICATION_FAILED';
|
||||
}
|
||||
|
||||
export const useUserProfile = () => {
|
||||
@@ -72,6 +73,7 @@ export const useUserProfile = () => {
|
||||
maxFileCounts: 0,
|
||||
},
|
||||
permission: 0,
|
||||
verifiedStatus: 'unknow'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ export const useUserPaymentsData = () => {
|
||||
|
||||
return data;
|
||||
},
|
||||
refetchInterval: 10000,
|
||||
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;
|
||||
}
|
||||
@@ -2,6 +2,34 @@ import { QueryClient } from '@tanstack/react-query';
|
||||
import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action';
|
||||
import { getUserFilesData } from '@/app/actions/fileEntity';
|
||||
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
|
||||
}),
|
||||
@@ -35,5 +71,10 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
|
||||
}
|
||||
}
|
||||
}),
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['allFilesExtensions'],
|
||||
queryFn: () => getAllFilesExtensions(),
|
||||
staleTime: 5 * 60 * 1000
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -348,7 +410,7 @@
|
||||
svg {
|
||||
margin-right: 15px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@@ -711,3 +773,74 @@
|
||||
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;
|
||||
} */
|
||||
}
|
||||
@@ -53,3 +53,6 @@ path:focus {
|
||||
}
|
||||
|
||||
@import "tailwindcss";
|
||||
@import 'swiper/css';
|
||||
@import 'swiper/css/navigation';
|
||||
@import 'swiper/css/pagination';
|
||||
+111
-27
@@ -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,64 +150,136 @@
|
||||
font-weight: 600;
|
||||
color: v.$text-p;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-count {
|
||||
font-size: 12px;
|
||||
color: v.$p-color;
|
||||
&-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;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 400px;
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,15 @@
|
||||
&.password {
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.valid-card {
|
||||
border: 2px solid #2f9e44;
|
||||
background: #ecf8ee;
|
||||
}
|
||||
}
|
||||
|
||||
.form-options {
|
||||
@@ -190,6 +199,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,9 +207,10 @@
|
||||
min-width: 500px;
|
||||
|
||||
&-button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
|
||||
+1428
-515
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 {
|
||||
@@ -298,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 {
|
||||
@@ -324,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,17 +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;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import TanstakFilesTable from '@/app/components/tanstak-table/TanstakTable';
|
||||
|
||||
type FilesTable = {
|
||||
fileType: string
|
||||
fileType: string;
|
||||
showFileLink?: boolean;
|
||||
}
|
||||
|
||||
export default function FilesTable({ fileType }: FilesTable) {
|
||||
export default function FilesTable({ fileType, showFileLink }: FilesTable) {
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<TanstakFilesTable fileType={fileType} />
|
||||
<TanstakFilesTable fileType={fileType} showFileLink={showFileLink}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -25,10 +25,6 @@ export default function DashboardFilesInfo() {
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.violation ? filesInfo?.images.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,10 +43,6 @@ export default function DashboardFilesInfo() {
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.violation ? filesInfo?.videos.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,10 +61,6 @@ export default function DashboardFilesInfo() {
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.violation ? filesInfo?.audios.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,16 +73,12 @@ export default function DashboardFilesInfo() {
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.check? convertBytes(filesInfo?.documents.check) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.size? convertBytes(filesInfo?.documents.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.violation ? filesInfo?.documents.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { fetchLastMonitoringCheck } from '@/app/actions/monitoringActions';
|
||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function DashboardLastCheck() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['lastMonitoringCheck'],
|
||||
queryFn: () => {
|
||||
return fetchLastMonitoringCheck()
|
||||
},
|
||||
select: (data) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div className="dashboard-last-check">
|
||||
<div className="last-check-info">
|
||||
<div className="last-check-title">Последняя проверка контента</div>
|
||||
<div className="last-check-title">
|
||||
{t('last-content-check')}
|
||||
</div>
|
||||
<div
|
||||
className="relative"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="last-check-date">
|
||||
17.10.2025 в 10:23</div>
|
||||
{data?.recent_searches[0]?.created_at ? `${formatDate(data.recent_searches[0].created_at)} ${t('in')} ${formatDateTime(data.recent_searches[0].created_at)}` : '---'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="last-check-actions">
|
||||
<Link
|
||||
className="btn-report"
|
||||
href={'reports'}
|
||||
href={'/pages/reports'}
|
||||
>
|
||||
📄 Отчёт
|
||||
{t('reports')}
|
||||
</Link>
|
||||
<Link
|
||||
{/* <Link
|
||||
className="btn-new-search"
|
||||
href={'violations'}
|
||||
>
|
||||
🔍 Мониторинг нарушений
|
||||
</Link>
|
||||
Мониторинг нарушений
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesData } from '@/app/actions/fileEntity';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import Link from 'next/link';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
|
||||
type FileItem = {
|
||||
id: string;
|
||||
@@ -42,7 +39,15 @@ export default function DashboardUserFiles() {
|
||||
error,
|
||||
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({
|
||||
queryKey: ['userFilesData', 1, 5],
|
||||
queryFn: () => getUserFilesData(1, 5),
|
||||
queryFn: () => getUserFilesData({
|
||||
page: 1,
|
||||
pageSize: 5,
|
||||
sortBy: 'updatedAt',
|
||||
sortOrder: 'desc',
|
||||
type: '',
|
||||
date: '',
|
||||
query: ''
|
||||
}),
|
||||
|
||||
select: (data: ApiResponse): FileItem[] => {
|
||||
if (!data?.files) return [];
|
||||
@@ -64,15 +69,6 @@ export default function DashboardUserFiles() {
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
const [openDropDownList, setOpenDropDownList] = useState(false);
|
||||
const dropDownList = useRef(null);
|
||||
useClickOutside(
|
||||
dropDownList,
|
||||
() => {
|
||||
setOpenDropDownList(false)
|
||||
},
|
||||
openDropDownList
|
||||
);
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
@@ -86,36 +82,29 @@ export default function DashboardUserFiles() {
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">Ваши файлы</h3>
|
||||
<h3 className="section-title">
|
||||
{t('your-files')}
|
||||
</h3>
|
||||
<div
|
||||
className="section-add-file btn view-all-link"
|
||||
onClick={() => {
|
||||
setOpenDropDownList(!openDropDownList);
|
||||
}}
|
||||
ref={dropDownList}
|
||||
className="btn view-all-link"
|
||||
>
|
||||
{t('add')}
|
||||
<div
|
||||
className={`section-drop-down-list ${openDropDownList ? 'opened' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col text-center"
|
||||
>
|
||||
<Link href='/pages/marking-images'>
|
||||
{t('photo-marking')}
|
||||
<Link href='/pages/my-content'>
|
||||
{t('all')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-video'>
|
||||
{t('video-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-audio'>
|
||||
{t('audio-marking')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-files-list">
|
||||
{isLoading && (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tableData?.length ? (
|
||||
tableData?.map((file) => {
|
||||
@@ -142,9 +131,11 @@ export default function DashboardUserFiles() {
|
||||
)
|
||||
})
|
||||
) : (
|
||||
!isLoading && (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,50 +3,152 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
import { useEffect } from 'react';
|
||||
import { useViolationStatistic } from '@/app/hooks/react-query/useViolationStatistic';
|
||||
import { IconDocument, IconSearch, IconWarning, IconGraph, IconDiscet, IconShield } from '@/app/ui/icons/icons';
|
||||
|
||||
export default function DashboardUserStats() {
|
||||
const t = useTranslations("Global");
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
const { data: violationStatistic, isLoading: violationStatisticLoading } = useViolationStatistic();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dashboard-user-stats">
|
||||
|
||||
<div className="stat-card projects">
|
||||
<div className="stat-type">ЗАЩИЩЁННЫЕ ФАЙЛЫ</div>
|
||||
<div className="stat-value">{filesInfo?.total.protected ? filesInfo?.total.protected : 0}</div>
|
||||
<div className="stat-label">Файлов под защитой</div>
|
||||
<div className="stat-icon">🛡️</div>
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">
|
||||
<IconShield />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.protected ? filesInfo?.total.protected : 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('files-under-protection')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card matches">
|
||||
<div className="stat-type">СОВПАДЕНИЯ</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-label">Найдено совпадений</div>
|
||||
<div className="stat-icon">🔍</div>
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">
|
||||
<IconSearch />
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.check ? filesInfo?.total.check : 0
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{violationStatisticLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
violationStatistic?.total_violations ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('matches-found')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card violations">
|
||||
<div className="stat-type">НАРУШЕНИЯ</div>
|
||||
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div>
|
||||
<div className="stat-label">Требуют внимания</div>
|
||||
<div className="stat-icon">⚠️</div>
|
||||
<div className="stat-card violations hidden">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">
|
||||
<IconWarning />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{violationStatisticLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
violationStatistic?.total_violations ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('matches')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card storage">
|
||||
<div className="stat-type">ХРАНИЛИЩЕ</div>
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">
|
||||
<IconDiscet />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">
|
||||
{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0}
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('disk-space-used')}
|
||||
</div>
|
||||
<div className="stat-label">Использовано места</div>
|
||||
<div className="stat-icon">💾</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card legal">
|
||||
<div className="stat-type">ЖАЛОБЫ</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-label">Отправлено юристам</div>
|
||||
<div className="stat-icon">⚖️</div>
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">
|
||||
<IconDocument />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{violationStatisticLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
violationStatistic?.in_progress_violations ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('violations-in-progress')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesData } from '@/app/actions/fileEntity';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import Link from 'next/link';
|
||||
import { getViolationFilesArray } from '@/app/actions/violationActions';
|
||||
import { IconEye } from '@/app/ui/icons/icons';
|
||||
|
||||
interface FileViolation {
|
||||
createdAt: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
latestViolationDate: string;
|
||||
mimeType: string;
|
||||
status: string;
|
||||
supportId: number;
|
||||
violationCount: number;
|
||||
}
|
||||
|
||||
interface ViolationData {
|
||||
content: FileViolation[];
|
||||
}
|
||||
|
||||
export default function DashboardUserViolations() {
|
||||
const {
|
||||
data: violationData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<ViolationData>({
|
||||
queryKey: ['violationData', { page: 0, size: 5 }],
|
||||
queryFn: () => getViolationFilesArray({
|
||||
page: 0,
|
||||
size: 5,
|
||||
}),
|
||||
|
||||
select: (data) => {
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
const formatDate = (timestamp: number | string) => {
|
||||
const date = new Date(timestamp);
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">
|
||||
{t('violation')}
|
||||
</h3>
|
||||
<div
|
||||
className="btn view-all-link"
|
||||
>
|
||||
<Link href='/pages/violations'>
|
||||
{t('all')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-files-list">
|
||||
{isLoading && (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{violationData?.content?.length ? (
|
||||
violationData?.content.map((file) => {
|
||||
return (
|
||||
<div
|
||||
key={file.fileId}
|
||||
className="file-item"
|
||||
>
|
||||
<div className="file-icon violent">
|
||||
<Link
|
||||
href={`/pages/file/${file.fileId}`}
|
||||
className="bg-violet-500 hover:bg-violet-600"
|
||||
title={t('view')}
|
||||
>
|
||||
<IconEye />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title={file.fileName}>
|
||||
{file.fileName}
|
||||
</div>
|
||||
<div className="file-meta">
|
||||
{file.createdAt ? formatDate(file.createdAt) : 0} • {t('violations-found')}: {file.violationCount ? file.violationCount : 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
!isLoading && (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Pagination, Autoplay } from 'swiper/modules';
|
||||
import Image from 'next/image';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import testImage from '@/app/src/image-preview.png'
|
||||
import { useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function ViolationsCarousel() {
|
||||
const paginationRef = useRef(null);
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">Нарушения</h3>
|
||||
<Link
|
||||
href="violations"
|
||||
className="view-all-link"
|
||||
>
|
||||
Смотреть все
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{t('are-no-violations')}
|
||||
</div>
|
||||
{/* <div className="violations-carousel">
|
||||
<Swiper
|
||||
spaceBetween={50}
|
||||
slidesPerView={1}
|
||||
modules={[Pagination, Autoplay]}
|
||||
pagination={{
|
||||
clickable: true,
|
||||
el: '.custon-swiper-pagination'
|
||||
}}
|
||||
loop={true}
|
||||
autoplay={{
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
>
|
||||
<SwiperSlide>
|
||||
<div className="violation-slide">
|
||||
<div className="violation-image-container">
|
||||
<Image src={testImage} alt="Preview" className="violation-image" />
|
||||
<div className="violation-badge">НАРУШЕНИЕ</div>
|
||||
<div className="violation-status-badge">ОБРАБОТАНО</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-content">
|
||||
<div className="violation-title-row">
|
||||
<div className="violation-filename">
|
||||
🖼️banner111.png</div>
|
||||
<div className="violation-type-badge photo">
|
||||
Фото
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-grid">
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">
|
||||
🌍 Регион
|
||||
</div>
|
||||
<div className="violation-info-value">
|
||||
<span className="region-flag">🇺🇸</span>
|
||||
<span>США</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📅 Обнаружено</div>
|
||||
<div className="violation-info-value">
|
||||
17.10.2025</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">🌐 Домен</div>
|
||||
<div className="violation-info-value">
|
||||
m.apkpure.com </div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📊 Категория</div>
|
||||
<div className="violation-info-value">
|
||||
Единичное
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📁 Тип</div>
|
||||
<div className="violation-info-value">
|
||||
Изображение</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">⚡ Статус</div>
|
||||
<div className="violation-info-value">
|
||||
✅ Обработано</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-actions-row">
|
||||
<button className="btn-visit-site">
|
||||
🔗 Перейти на сайт
|
||||
</button>
|
||||
<button className="violation-action">
|
||||
🚨 Открыть дело
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="violation-slide">
|
||||
<div className="violation-image-container">
|
||||
<Image src={testImage} alt="Preview" className="violation-image" />
|
||||
|
||||
<div className="violation-badge">НАРУШЕНИЕ</div>
|
||||
<div className="violation-status-badge">ОБРАБОТАНО</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-content">
|
||||
<div className="violation-title-row">
|
||||
<div className="violation-filename">
|
||||
🖼️banner111.png</div>
|
||||
<div className="violation-type-badge photo">
|
||||
Фото
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-grid">
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">
|
||||
🌍 Регион
|
||||
</div>
|
||||
<div className="violation-info-value">
|
||||
<span className="region-flag">🇺🇸</span>
|
||||
<span>США</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📅 Обнаружено</div>
|
||||
<div className="violation-info-value">
|
||||
17.10.2025</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">🌐 Домен</div>
|
||||
<div className="violation-info-value">
|
||||
m.apkpure.com </div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📊 Категория</div>
|
||||
<div className="violation-info-value">
|
||||
Единичное
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📁 Тип</div>
|
||||
<div className="violation-info-value">
|
||||
Изображение</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">⚡ Статус</div>
|
||||
<div className="violation-info-value">
|
||||
✅ Обработано</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-actions-row">
|
||||
<button className="btn-visit-site">
|
||||
🔗 Перейти на сайт
|
||||
</button>
|
||||
<button className="violation-action">
|
||||
🚨 Открыть дело
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
<SwiperSlide>
|
||||
<div className="violation-slide">
|
||||
<div className="violation-image-container">
|
||||
<Image src={testImage} alt="Preview" className="violation-image" />
|
||||
|
||||
<div className="violation-badge">НАРУШЕНИЕ</div>
|
||||
<div className="violation-status-badge">ОБРАБОТАНО</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-content">
|
||||
<div className="violation-title-row">
|
||||
<div className="violation-filename">
|
||||
🖼️banner111.png</div>
|
||||
<div className="violation-type-badge photo">
|
||||
Фото
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-grid">
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">
|
||||
🌍 Регион
|
||||
</div>
|
||||
<div className="violation-info-value">
|
||||
<span className="region-flag">🇺🇸</span>
|
||||
<span>США</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📅 Обнаружено</div>
|
||||
<div className="violation-info-value">
|
||||
17.10.2025</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">🌐 Домен</div>
|
||||
<div className="violation-info-value">
|
||||
m.apkpure.com </div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📊 Категория</div>
|
||||
<div className="violation-info-value">
|
||||
Единичное
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📁 Тип</div>
|
||||
<div className="violation-info-value">
|
||||
Изображение</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">⚡ Статус</div>
|
||||
<div className="violation-info-value">
|
||||
✅ Обработано</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-actions-row">
|
||||
<button className="btn-visit-site">
|
||||
🔗 Перейти на сайт
|
||||
</button>
|
||||
<button className="violation-action">
|
||||
🚨 Открыть дело
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
</Swiper>
|
||||
<div
|
||||
className="flex justify-center mt-6"
|
||||
>
|
||||
<div
|
||||
ref={paginationRef}
|
||||
className="flex justify-center gap-2 custon-swiper-pagination"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface FaqItemProps {
|
||||
question: string;
|
||||
children?: ReactNode | null;
|
||||
}
|
||||
|
||||
export default function FaqItem({ question, children }: FaqItemProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [height, setHeight] = useState(0);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const scrollHeight = contentRef.current.scrollHeight;
|
||||
if (isOpen) {
|
||||
setHeight(scrollHeight);
|
||||
} else {
|
||||
setHeight(0);
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
function toggleHandler() {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="faq-item">
|
||||
<div className="faq-question" onClick={toggleHandler}>
|
||||
<span>{question}</span>
|
||||
<div className="faq-toggle">
|
||||
<svg className={`dropdown-arrow ${isOpen? 'expanded' : ''}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M7 10l5 -5 5 5z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="faq-answer"
|
||||
style={{ height: `${height}px` }}
|
||||
>
|
||||
<div ref={contentRef}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
export default function FaqContact() {
|
||||
return (
|
||||
<>
|
||||
<div className="support-block" >
|
||||
<h3 >Не нашли ответ на свой вопрос?</h3>
|
||||
<p >
|
||||
Наша команда поддержки работает 24/7 и готова помочь вам
|
||||
<br />
|
||||
с любыми вопросами по использованию платформы NO COPY.
|
||||
</p>
|
||||
<div className="support-buttons" >
|
||||
<button className="btn btn-primary">
|
||||
Написать в поддержку
|
||||
</button>
|
||||
<a href="tel:+78003021090" className="btn btn-outline">
|
||||
📞 Позвонить: 8 (800) 302-10-90
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="support-schedule">
|
||||
<h4 >Время работы поддержки:</h4>
|
||||
<div className="support-schedule-grid">
|
||||
<div>📧 E-mail: круглосуточно</div>
|
||||
<div>📞 Телефон: 9:30 - 18:00 МСК</div>
|
||||
<div>💬 Чат: 9:30 - 18:00 МСК</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-mini-wrapper">
|
||||
<div className="stats-mini-content">
|
||||
<div className="stats-mini-item">
|
||||
<div className="stats-mini-value" style={{ 'color': '#6366f1' }}>
|
||||
18
|
||||
</div>
|
||||
<div className="stats-mini-label">Вопросов в базе</div>
|
||||
</div>
|
||||
<div className="stats-mini-item">
|
||||
<div className="stats-mini-value" style={{ 'color': '#10b981' }}>
|
||||
99%
|
||||
</div>
|
||||
<div className="stats-mini-label">Находят ответ</div>
|
||||
</div>
|
||||
<div className="stats-mini-item">
|
||||
<div className="stats-mini-value" style={{ 'color': '#f59e0b' }}>
|
||||
24/7
|
||||
</div>
|
||||
<div className="stats-mini-label">Поддержка</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import FaqItem from '@/app/ui/faq/component/faq-item';
|
||||
|
||||
export default function FrequentlyAskedQuestions() {
|
||||
return (
|
||||
<div
|
||||
className="faq-wrapper"
|
||||
>
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Защита контента
|
||||
<span>(2 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Как скрытый водяной знак защищает контент?"
|
||||
>
|
||||
Скрытый водяной знак встраивается незаметно в ваш контент и позволяет отслеживать его использование в интернете. Технология работает на основе скрытого водяного знака No Copy и не влияет на качество изображений, видео или аудио.
|
||||
</FaqItem >
|
||||
|
||||
|
||||
<FaqItem
|
||||
question="Насколько надежна защита?"
|
||||
>
|
||||
Наша многослойная защита обеспечивает 99.8% точность определения авторства даже после обработки файлов.
|
||||
</FaqItem >
|
||||
</section>
|
||||
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Общие вопросы
|
||||
<span>(2 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Что такое NO COPY?"
|
||||
>
|
||||
NO COPY - это платформа для защиты авторских прав с помощью невидимых водяных знаков для изображений, видео, аудио и документов.
|
||||
</FaqItem >
|
||||
|
||||
|
||||
<FaqItem
|
||||
question="Как работает защита?"
|
||||
>
|
||||
Мы встраиваем невидимые цифровые метки непосредственно в структуру файлов. Эти метки сохраняются даже при сжатии и конвертации.
|
||||
</FaqItem >
|
||||
</section>
|
||||
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Лимиты и ограничения
|
||||
<span>(3 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Сколько фотографий я могу промаркировать?"
|
||||
>
|
||||
Вы можете промаркировать столько фотографий, сколько прописано в выбранном Вами тарифном плане. Минимально в месяц можно маркировать от 100 фотографий на платных тарифных планах No Copy.
|
||||
</FaqItem >
|
||||
|
||||
|
||||
<FaqItem
|
||||
question="Сколько файлов можно загрузить?"
|
||||
>
|
||||
Количество файлов зависит от вашего тарифного плана. Для примера, на тарифе СТАРТ: ~100 фото / ~20 видео / ~35 аудио / ~60 pdf
|
||||
</FaqItem >
|
||||
|
||||
<FaqItem
|
||||
question="Что происходит при превышении лимитов?"
|
||||
>
|
||||
При превышении лимитов подписки вы можете использовать токены для продолжения работы или обновить тариф. Без токенов новые файлы нельзя будет защитить до начала следующего месяца или смены тарифа.
|
||||
</FaqItem >
|
||||
</section>
|
||||
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Хранение данных
|
||||
<span>(1 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Где хранится весь промаркированный контент?"
|
||||
>
|
||||
Весь промаркированный контент хранится на защищенных серверах No Copy с многоуровневой системой резервного копирования согласно закону ФЗ-152. Доступ к вашим файлам имеете только вы.
|
||||
</FaqItem >
|
||||
</section>
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Правовые вопросы
|
||||
<span>(1 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Могу ли я маркировать чужой контент?"
|
||||
>
|
||||
Нет, маркировать можно только контент, авторские права на который принадлежат вам. Это важное условие использования платформы No Copy.
|
||||
</FaqItem >
|
||||
</section>
|
||||
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Поддержка
|
||||
<span>(1 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Я сам нашёл нарушение контента, куда заявить?"
|
||||
>
|
||||
Если вы обнаружили нарушение самостоятельно, свяжитесь с нашей службой поддержки через раздел "Поддержка" или отправьте письмо на info@no-copy.ru.
|
||||
</FaqItem >
|
||||
</section>
|
||||
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Технические вопросы
|
||||
<span>(1 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Какие форматы поддерживаются?"
|
||||
>
|
||||
Изображения: JPG, JPEG, PNG, BMP. Видео: MP4, AVI, MOV, WMV. Аудио: WAV. Документы: PDF.
|
||||
</FaqItem >
|
||||
</section>
|
||||
|
||||
<section className="faq-section">
|
||||
<h2>
|
||||
<span></span>
|
||||
Оплата и подписка
|
||||
<span>(7 вопросов)</span>
|
||||
</h2>
|
||||
|
||||
<FaqItem
|
||||
question="Какие способы оплаты поддерживаются?"
|
||||
>
|
||||
Основной вид оплаты - это оплата российскими(выпущенные в РФ) банковскими картами (Visa, MasterCard, МИР).
|
||||
По запросу можно выбрать другой способ оплаты: электронными кошельками (Yoo.Money), через систему быстрых платежей (СБП) и другими способами.
|
||||
</FaqItem >
|
||||
<FaqItem
|
||||
question="Как работает автопродление подписки?"
|
||||
>
|
||||
Подписка автоматически продлевается каждый месяц с вашей карты. Вы можете отменить автопродление в любой момент в настройках аккаунта. Уведомление о списании приходит за 3 дня до продления.
|
||||
</FaqItem >
|
||||
<FaqItem
|
||||
question="Что такое токены и зачем они нужны?"
|
||||
>
|
||||
Токены - это внутренняя валюта платформы для оплаты защиты файлов и других действий на платформе. Например круглосуточный мониторинг совпадений контента, подача жалоб DMCA, фиксация нарушений(протокол) или подключение юристов на платформе и создание дела.
|
||||
</FaqItem >
|
||||
<FaqItem
|
||||
question="Можно ли получить возврат средств?"
|
||||
>
|
||||
Возврат возможен только в день оформления подписки при не использовании токенов, защиты файлов, поиска и других функций платформы. Для подачи уведомления обратитесь в техническую поддержку через форму обратной связи или E-mail: support@no-copy.ru.
|
||||
</FaqItem >
|
||||
<FaqItem
|
||||
question="Как изменить или отменить подписку?"
|
||||
>
|
||||
Вы можете повысить тариф в любой момент - доплата рассчитывается пропорционально. Понижение тарифа вступает в силу со следующего периода оплаты. Отменить подписку можно в настройках аккаунта.
|
||||
</FaqItem >
|
||||
<FaqItem
|
||||
question="Безопасна ли оплата через ваш сайт?"
|
||||
>
|
||||
Да, все платежи обрабатываются через ЮKassa с использованием защищенного SSL-соединения. Мы не храним данные ваших банковских карт. Платформа соответствует стандартам безопасности PCI DSS.
|
||||
</FaqItem >
|
||||
<FaqItem
|
||||
question="Как получить чек об оплате?"
|
||||
>
|
||||
Электронный чек автоматически отправляется на email, указанный при оплате.
|
||||
</FaqItem >
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export default function PopularAskedQuestions() {
|
||||
return (
|
||||
<div className="popular-questions">
|
||||
<h2>
|
||||
Популярные вопросы
|
||||
</h2>
|
||||
<div className="popular-grid">
|
||||
<div>
|
||||
<h4>Как работает защита?</h4>
|
||||
<p>Мы встраиваем невидимые водяные знаки в ваш контент</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Сколько файлов можно загрузить?</h4>
|
||||
<p>Зависит от вашего тарифного плана</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Безопасность данных</h4>
|
||||
<p>Все файлы хранятся на защищенных серверах</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export default function ViolationPageActions() {
|
||||
export default function FilePageActions() {
|
||||
return (
|
||||
<div
|
||||
className="block-wrapper"
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
|
||||
import { FileDetails } from '@/app/[locale]/pages/file/[id]/page';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { formatDate } from '@/app/lib/formatDate';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function FilenPageFileInfo({ fileInfo }: { fileInfo: FileDetails }) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="block-wrapper"
|
||||
>
|
||||
<div className="content-card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{t('file-information')}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="info-row">
|
||||
<span className="info-label">
|
||||
ID
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{fileInfo ? fileInfo.supportId : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">
|
||||
{t('file-type')}
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{fileInfo ? fileInfo.mimeType : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">
|
||||
{t('size')}
|
||||
</span>
|
||||
<span className="info-value">{fileInfo ? convertBytes(fileInfo.fileSize) : '#'}</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">
|
||||
{t('loaded')}
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{fileInfo ? formatDate(fileInfo.createdAt) : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="info-row">
|
||||
<span className="info-label">
|
||||
{t('monitoring')}
|
||||
</span>
|
||||
<span className="info-value">
|
||||
{fileInfo ? fileInfo.monitoring : '#'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user