Compare commits
628
Commits
8541916e4c
...
NCBACK-168
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
587d7ceead | ||
|
|
ac8ef8caa2 | ||
|
|
7d0fb265d4 | ||
|
|
743db57bb3 | ||
|
|
d7d2e36573 | ||
|
|
8b157b8e87 | ||
|
|
0935e8cbc7 | ||
|
|
f04ce348ae | ||
|
|
508e0d182c | ||
|
|
dbd5d43d65 | ||
|
|
7d8a725e34 | ||
|
|
491adb4090 | ||
|
|
1a3f5277d9 | ||
|
|
ffcd5fc535 | ||
|
|
65e0c2ed62 | ||
|
|
36b4a3b481 | ||
|
|
e0455b200a | ||
|
|
348a5398d0 | ||
|
|
af87bfc7b6 | ||
|
|
beefe86ffd | ||
|
|
3a7c609588 | ||
|
|
aeeee04166 | ||
|
|
288d6782a4 | ||
|
|
55915af9df | ||
|
|
bfc457470c | ||
|
|
210145f5ca | ||
|
|
d0793a281e | ||
|
|
6994edd216 | ||
|
|
3a0ae93846 | ||
|
|
13a5db6313 | ||
|
|
9c011b6416 | ||
|
|
294e16e1a6 | ||
|
|
71f7120239 | ||
|
|
0e28986dc7 | ||
|
|
51a3ed7d18 | ||
|
|
c15867a41c | ||
|
|
c4e4d775f5 | ||
|
|
cd07502128 | ||
|
|
1ef2cc227a | ||
|
|
7226285746 | ||
|
|
dd93cd302c | ||
|
|
37b7ccb52e | ||
|
|
b8a76bd6a9 | ||
|
|
07e1237f05 | ||
|
|
29546740c7 | ||
|
|
42cc96a28a | ||
|
|
84b07af246 | ||
|
|
8a72dbaf68 | ||
|
|
af6b997ceb | ||
|
|
e97dd392c0 | ||
|
|
ae0526a58a | ||
|
|
d419c1c2cb | ||
|
|
48e03b4237 | ||
|
|
2fad519b7e | ||
|
|
84b73b912f | ||
|
|
a3cbaaa2e7 | ||
|
|
881f3295c0 | ||
|
|
0cc9230672 | ||
|
|
010a8f3a5a | ||
|
|
4952e0daa1 | ||
|
|
455b39bb1e | ||
|
|
34772bd3b4 | ||
|
|
2296044280 | ||
|
|
2d201bfcde | ||
|
|
17931191f5 | ||
|
|
4e8289e976 | ||
|
|
41c56e026f | ||
|
|
1e8bf782ca | ||
|
|
e4eedf464b | ||
|
|
c274cb134d | ||
|
|
c0fe6cce48 | ||
|
|
9e89a37b4d | ||
|
|
a704e410c5 | ||
|
|
d24884f7e6 | ||
|
|
2cada9ba27 | ||
|
|
a7c203d7a4 | ||
|
|
abc07a5e2f | ||
|
|
904c615665 | ||
|
|
e9af59769d | ||
|
|
e933d64790 | ||
|
|
c07fe860e5 | ||
|
|
1b6dd3c40b | ||
|
|
7c82f940fc | ||
|
|
a69636d7e3 | ||
|
|
83f535c2e9 | ||
|
|
47bf0b3e77 | ||
|
|
e09cd3a77c | ||
|
|
5d521358f7 | ||
|
|
cfb2f42e96 | ||
|
|
cbbfb1c6ad | ||
|
|
86263bd235 | ||
|
|
fc9dc836e7 | ||
|
|
a508ede373 | ||
|
|
fd4da24b1d | ||
|
|
2f0acfc07f | ||
|
|
f8cf3618f7 | ||
|
|
0ad1ce98ad | ||
|
|
04c27a9b7f | ||
|
|
12fb7104a1 | ||
|
|
f5548f39da | ||
|
|
b16d0949a4 | ||
|
|
5dfc3573e5 | ||
|
|
7800499c72 | ||
|
|
045eef24c0 | ||
|
|
3a8a1f9c08 | ||
|
|
f55f4cedfe | ||
|
|
85b77aeb91 | ||
|
|
1b4ad64ef6 | ||
|
|
c7939b8923 | ||
|
|
a155a90fba | ||
|
|
31d9307fe1 | ||
|
|
86f56b4735 | ||
|
|
1f44211088 | ||
|
|
fcc1f3bec9 | ||
|
|
cbf30604af | ||
|
|
839dd8cf94 | ||
|
|
365aeded43 | ||
|
|
ad247fb269 | ||
|
|
1e0267c03d | ||
|
|
47a16c0c2b | ||
|
|
6dcf3de29e | ||
|
|
8974d00078 | ||
|
|
d0263e7252 | ||
|
|
0d2cb18745 | ||
|
|
00084d769a | ||
|
|
c1271d9c34 | ||
|
|
4d957ea271 | ||
|
|
9b4fb796f9 | ||
|
|
c4aefdb8b9 | ||
|
|
35b7f82ad9 | ||
|
|
25ad9b0350 | ||
|
|
f22ea522b8 | ||
|
|
9f317d9878 | ||
|
|
aaa46e7d3a | ||
|
|
11692c8537 | ||
|
|
d0901961be | ||
|
|
d8d1eec3ed | ||
|
|
0182616050 | ||
|
|
8da7056cd6 | ||
|
|
50d2497288 | ||
|
|
02b378612b | ||
|
|
90903a67ac | ||
|
|
a551893322 | ||
|
|
ea9d57cc65 | ||
|
|
e141545035 | ||
|
|
62cd597994 | ||
|
|
1eff2d2c0a | ||
|
|
2af7c12cf6 | ||
|
|
3a6034a460 | ||
|
|
9c9bf201c4 | ||
|
|
cb8389bb4a | ||
|
|
77ad126530 | ||
|
|
e152013e2a | ||
|
|
0c3ccb2806 | ||
|
|
eb2615c303 | ||
|
|
29b6120f80 | ||
|
|
90315a112a | ||
|
|
50f45a133b | ||
|
|
c57685065a | ||
|
|
8048c0ff0e | ||
|
|
10c2ca5bc3 | ||
|
|
f688aa210f | ||
|
|
b377c11c41 | ||
|
|
1fe49c24e8 | ||
|
|
27ca7ddf8e | ||
|
|
b6c5278282 | ||
|
|
6cf7bd766d | ||
|
|
e75fcd7bff | ||
|
|
eb14c5cb21 | ||
|
|
0097f768ef | ||
|
|
d6dee3361e | ||
|
|
9b83ba3176 | ||
|
|
2878f546ee | ||
|
|
e71b65d593 | ||
|
|
1d5925e178 | ||
|
|
45c5137565 | ||
|
|
3544b2af81 | ||
|
|
ea003ea73c | ||
|
|
dd483cb628 | ||
|
|
b89e5b4c6b | ||
|
|
a09ce94da1 | ||
|
|
1256334805 | ||
|
|
5ff450c3bc | ||
|
|
8bb6097d90 | ||
|
|
89506c222b | ||
|
|
0887ccde13 | ||
|
|
d68a35903c | ||
|
|
b05661e120 | ||
|
|
d40e3d563b | ||
|
|
f31713a929 | ||
|
|
f4b0a63e23 | ||
|
|
884fb8dc50 | ||
|
|
4abcc9689c | ||
|
|
154e9cc5c1 | ||
|
|
534e18bbd4 | ||
|
|
0df34bdb1c | ||
|
|
11d65410e6 | ||
|
|
f4a0222e0e | ||
|
|
022943d2af | ||
|
|
3a84394847 | ||
|
|
70717f88fb | ||
|
|
95319e51ff | ||
|
|
6a1a3adce5 | ||
|
|
aff0b7a080 | ||
|
|
9128c0e298 | ||
|
|
6453e8402e | ||
|
|
7ae0fecb61 | ||
|
|
c9f230a76f | ||
|
|
17692d6497 | ||
|
|
a9861967ef | ||
|
|
4d5bae0812 | ||
|
|
845a3b479d | ||
|
|
010c39e869 | ||
|
|
e9c2e2f0aa | ||
|
|
cbb01de3a2 | ||
|
|
6ed75bd08c | ||
|
|
3c73dc40dc | ||
|
|
4986455919 | ||
|
|
f4b4f4bafb | ||
|
|
2982741558 | ||
|
|
18e700bb6e | ||
|
|
0955d66931 | ||
|
|
39c3760161 | ||
|
|
b5ec4ccbcd | ||
|
|
c5c4687063 | ||
|
|
b7865acbe3 | ||
|
|
3a3a514825 | ||
|
|
7cdcb3a69a | ||
|
|
d7899d0dd5 | ||
|
|
88a383f85e | ||
|
|
e5ed1e213d | ||
|
|
46ee8ac4a6 | ||
|
|
5ea3823946 | ||
|
|
9d90c81f64 | ||
|
|
38bfc94125 | ||
|
|
261d1300e7 | ||
|
|
e0fde479ad | ||
|
|
1af861aa94 | ||
|
|
ad76b06e8a | ||
|
|
75bab70856 | ||
|
|
f65513b1cb | ||
|
|
ba0b47bec5 | ||
|
|
f2428d7fde | ||
|
|
07eb00c705 | ||
|
|
9907213dd8 | ||
|
|
89e288c591 | ||
|
|
75874ab46f | ||
|
|
333dd5d3c5 | ||
|
|
949038979e | ||
|
|
c61598010c | ||
|
|
f56e6ba6d1 | ||
|
|
13ac3a6543 | ||
|
|
c3de5727fe | ||
|
|
2774b7eab6 | ||
|
|
d54a8bfe60 | ||
|
|
8464616c37 | ||
|
|
be06d7b833 | ||
|
|
f7879485ef | ||
|
|
00c4f4062d | ||
|
|
8c52839c02 | ||
|
|
7a1b45895f | ||
|
|
d36b724d4f | ||
|
|
6e648a522a | ||
|
|
8b56e3da21 | ||
|
|
9fbb74ba60 | ||
|
|
e8f4f9939f | ||
|
|
fa60a79241 | ||
|
|
413daf37d6 | ||
|
|
e24b0b1dc8 | ||
|
|
df81c1e232 | ||
|
|
5bc74d874c | ||
|
|
98f0571d1f | ||
|
|
04ff892e86 | ||
|
|
83f644cb03 | ||
|
|
0c03bb9254 | ||
|
|
1280505297 | ||
|
|
1efdb06d3b | ||
|
|
88c356bdf2 | ||
|
|
060b684e57 | ||
|
|
c739f2e7b1 | ||
|
|
7f85d64167 | ||
|
|
6873daa5de | ||
|
|
1a17683897 | ||
|
|
5a6dcf33fa | ||
|
|
57ceddd9ee | ||
|
|
b79d8b8879 | ||
|
|
6d6d0e102b | ||
|
|
33f5c15e45 | ||
|
|
640f167e4b | ||
|
|
0351c561b5 | ||
|
|
b8c439d2d7 | ||
|
|
a11abdbb56 | ||
|
|
617984e587 | ||
|
|
b1f731fdde | ||
|
|
9899741c00 | ||
|
|
a53c1b4b49 | ||
|
|
3e3a5df34d | ||
|
|
2227a842ae | ||
|
|
3b1509edf0 | ||
|
|
72be27e3c1 | ||
|
|
35f74791c9 | ||
|
|
913a199d95 | ||
|
|
90514e2327 | ||
|
|
3e0f7f077a | ||
|
|
8cea6ccfbb | ||
|
|
c66ee30940 | ||
|
|
60427dbb4f | ||
|
|
a24ccd9cbe | ||
|
|
e547c561e3 | ||
|
|
1b878faf2b | ||
|
|
10c3a2326b | ||
|
|
55acfe7efe | ||
|
|
c4272bf1fe | ||
|
|
2e2d8e897b | ||
|
|
18716cf97f | ||
|
|
1a7bad29d1 | ||
|
|
3b3e18f22d | ||
|
|
d2161fe221 | ||
|
|
4895dcbcc4 | ||
|
|
6990f78f69 | ||
|
|
64d606a791 | ||
|
|
3b72aaa7c2 | ||
|
|
de7199a3df | ||
|
|
c8f2233e24 | ||
|
|
d18071e533 | ||
|
|
bc1d5d5337 | ||
|
|
9da0a095cb | ||
|
|
92bd3fdcdb | ||
|
|
021ca69e6b | ||
|
|
454ec6f865 | ||
|
|
97b24cfadf | ||
|
|
332788a77a | ||
|
|
a33ccf4180 | ||
|
|
1b9b843f1a | ||
|
|
82e27a0ce4 | ||
|
|
a3ac3f6535 | ||
|
|
c1d5f9eebf | ||
|
|
70d6dea264 | ||
|
|
891c3325c9 | ||
|
|
9709b938f2 | ||
|
|
79e3a5b902 | ||
|
|
6248909003 | ||
|
|
c88646b64c | ||
|
|
4f7b464bed | ||
|
|
9d03968bc1 | ||
|
|
9ab78a55c3 | ||
|
|
9f276657c3 | ||
|
|
aeb61936ce | ||
|
|
a3ea25c1e6 | ||
|
|
13d68f93d9 | ||
|
|
81c31547ac | ||
|
|
b5d53e0831 | ||
|
|
22bc8fdcbe | ||
|
|
c7c78e303c | ||
|
|
048e1269ad | ||
|
|
13128d282b | ||
|
|
498424cf27 | ||
|
|
67bf12c11f | ||
|
|
c658721333 | ||
|
|
906b3a554c | ||
|
|
f4a13e2a8e | ||
|
|
1f61ba7edd | ||
|
|
38b262a3f0 | ||
|
|
868ed7ca7e | ||
|
|
7e6760e769 | ||
|
|
62b91f0ce9 | ||
|
|
fad2a525ea | ||
|
|
0c20109254 | ||
|
|
84bf085453 | ||
|
|
57c99bd981 | ||
|
|
bcbe64807b | ||
|
|
198c8b452d | ||
|
|
1fec3b2584 | ||
|
|
7aa29bd398 | ||
|
|
1f6301c531 | ||
|
|
edc6e56c69 | ||
|
|
d584e68e3a | ||
|
|
e36eb4d422 | ||
|
|
3946e7a0e7 | ||
|
|
a3da6b8530 | ||
|
|
f3e8ef3f65 | ||
|
|
03920524d3 | ||
|
|
4958d3ca57 | ||
|
|
5f12c58cc8 | ||
|
|
6ccdfc0129 | ||
|
|
c089de52e7 | ||
|
|
166de1bf15 | ||
|
|
726c1dc16c | ||
|
|
a52be724c0 | ||
|
|
be1f310595 | ||
|
|
953f2f22a9 | ||
|
|
74579fa53e | ||
|
|
930e811232 | ||
|
|
6ca112f3e3 | ||
|
|
659d307a12 | ||
|
|
459035529c | ||
|
|
0ad2b03f2b | ||
|
|
062c3e3539 | ||
|
|
78c606ba9e | ||
|
|
ffeeff8be3 | ||
|
|
9a9ec39ff6 | ||
|
|
61ed604b3e | ||
|
|
40e6172c0b | ||
|
|
0898ea9aa0 | ||
|
|
63a241f3b5 | ||
|
|
54529fdbb5 | ||
|
|
7d6882a006 | ||
|
|
1b57d589fb | ||
|
|
b6c5c59919 | ||
|
|
da99d7e2a8 | ||
|
|
82d795c1c5 | ||
|
|
e8b9b5e1b9 | ||
|
|
e36d67be17 | ||
|
|
0ec7834cc5 | ||
|
|
7572c1ada2 | ||
|
|
256ecbe31f | ||
|
|
037640fc70 | ||
|
|
f6ca797802 | ||
|
|
c4de107fba | ||
|
|
6d9710e7cd | ||
|
|
963b649aac | ||
|
|
4f294e8b90 | ||
|
|
a684e1dd94 | ||
|
|
777555c187 | ||
|
|
8890cfb067 | ||
|
|
6993dd3eb8 | ||
|
|
23317807f6 | ||
|
|
c24a4febc9 | ||
|
|
c7a0131624 | ||
|
|
2f3c3696d6 | ||
|
|
de0a8d0c39 | ||
|
|
8bf5111d7c | ||
|
|
1037347e06 | ||
|
|
a84cd39737 | ||
|
|
be130520d9 | ||
|
|
81614b028c | ||
|
|
c0c89dba77 | ||
|
|
97b846deec | ||
|
|
e778cf7e33 | ||
|
|
40fa737208 | ||
|
|
5df5e340e5 | ||
|
|
7c757a3c59 | ||
|
|
2c26a8cd85 | ||
|
|
584f0dda2f | ||
|
|
362df2f609 | ||
|
|
f99e352693 | ||
|
|
17a7672d4e | ||
|
|
1aedd8bc29 | ||
|
|
3d601cc154 | ||
|
|
e8c57729be | ||
|
|
e0606c5486 | ||
|
|
61e3d08136 | ||
|
|
1e199de7b2 | ||
|
|
2adf66baa2 | ||
|
|
1ee55f503d | ||
|
|
8d10aa77c8 | ||
|
|
ab0b68a7b8 | ||
|
|
7789f19055 | ||
|
|
19e807a7f6 | ||
|
|
b4249c7f38 | ||
|
|
c5baec6d1d | ||
|
|
82c7bb3635 | ||
|
|
c01499f4f1 | ||
|
|
f87be6d2c6 | ||
|
|
5c252cdcb1 | ||
|
|
42100761c1 | ||
|
|
4f6648ead8 | ||
|
|
c65314e7cd | ||
|
|
8b04485533 | ||
|
|
fc81d471ff | ||
|
|
55e0b0723e | ||
|
|
63d380f0f7 | ||
|
|
fc9a7afada | ||
|
|
27d8e75986 | ||
|
|
8b68e37b89 | ||
|
|
832c4b0e76 | ||
|
|
c4ab78b89b | ||
|
|
a6a493fe97 | ||
|
|
efae068bac | ||
|
|
2129fe1727 | ||
|
|
f7f53d5af3 | ||
|
|
9a6491b76b | ||
|
|
1db333a21b | ||
|
|
e9e9f41f2a | ||
|
|
ab652117c8 | ||
|
|
6ef23862e1 | ||
|
|
2be8f81a5b | ||
|
|
bf9926f004 | ||
|
|
bd869798be | ||
|
|
4166cb1d80 | ||
|
|
396d3888db | ||
|
|
f8ab107724 | ||
|
|
38ce69047d | ||
|
|
8f8302f76f | ||
|
|
9366f531d1 | ||
|
|
f5302f2b4b | ||
|
|
fc7efbfa07 | ||
|
|
fb661df8a1 | ||
|
|
3589487e39 | ||
|
|
7d7213b457 | ||
|
|
2b7ba7255a | ||
|
|
7a03b0bc2e | ||
|
|
a9c85570b7 | ||
|
|
4697eb6fda | ||
|
|
b763e438b8 | ||
|
|
7213e40b96 | ||
|
|
719a3e3582 | ||
|
|
049f404b20 | ||
|
|
18a48236b5 | ||
|
|
3d38729004 | ||
|
|
ede6ad179c | ||
|
|
f790a95e71 | ||
|
|
e53a85caa2 | ||
|
|
e4d6df71a1 | ||
|
|
afb6bf1760 | ||
|
|
2e928fb91a | ||
|
|
d0918b8c0b | ||
|
|
77ca4775b1 | ||
|
|
9ac254aa40 | ||
|
|
5cc822a535 | ||
|
|
75b505464c | ||
|
|
4c239f12f9 | ||
|
|
cb5a005041 | ||
|
|
6b9279f802 | ||
|
|
13ab4bfcfe | ||
|
|
09e9d2df40 | ||
|
|
73a349e01d | ||
|
|
31af7036a3 | ||
|
|
b5754b75a8 | ||
|
|
e2174df41b | ||
|
|
b674be212b | ||
|
|
54cb433278 | ||
|
|
24e96d2f8a | ||
|
|
2350ac8a5d | ||
|
|
9f880f71d1 | ||
|
|
d3a2bd1497 | ||
|
|
a8ae39751e | ||
|
|
5541d76574 | ||
|
|
da47a7a974 | ||
|
|
46f11e081c | ||
|
|
d81ed71e1d | ||
|
|
3c0fea3eb0 | ||
|
|
0e35c5e601 | ||
|
|
b2952c3594 | ||
|
|
9547ead7aa | ||
|
|
46bd4f7855 | ||
|
|
71cc51b3cf | ||
|
|
a590514e05 | ||
|
|
f6b04d6e33 | ||
|
|
d3bbc593a6 | ||
|
|
fe5fd79d85 | ||
|
|
be98bcba3f | ||
|
|
e19b559a19 | ||
|
|
aa0f9398eb | ||
|
|
5597fe42a2 | ||
|
|
bde030d5b5 | ||
|
|
052c66fd7c | ||
|
|
0044d3c703 | ||
|
|
9406004ee4 | ||
|
|
94261cde7a | ||
|
|
9d4604ac4e | ||
|
|
c438ae3fb9 | ||
|
|
9f41386b65 | ||
|
|
630a108968 | ||
|
|
3e9c87ec46 | ||
|
|
d0de025d96 | ||
|
|
eff7fbc171 | ||
|
|
6c46a1a655 | ||
|
|
edcac25b02 | ||
|
|
3537bd1f3a | ||
|
|
a085685d33 | ||
|
|
f16e527a32 | ||
|
|
1032c5420b | ||
|
|
bd0169bd61 | ||
|
|
198a01d026 | ||
|
|
fffd38a812 | ||
|
|
0c1aad1932 | ||
|
|
6d9313cbb8 | ||
|
|
bd2ec273a9 | ||
|
|
f8bbf56f47 | ||
|
|
aaa95091d2 | ||
|
|
ab6e385984 | ||
|
|
a3bcccdf28 | ||
|
|
aa67d722c3 | ||
|
|
2c54d3e375 | ||
|
|
2209bfa48c | ||
|
|
164e2568b9 | ||
|
|
d52dfb7ab8 | ||
|
|
eeef5ac9f9 | ||
|
|
49088e5e5f | ||
|
|
e82d96a44c | ||
|
|
e0420fa6d3 | ||
|
|
87b8d68f41 | ||
|
|
fe77b4ad36 | ||
|
|
8058be42c1 | ||
|
|
c38b7ee558 | ||
|
|
3376fee99a | ||
|
|
79ca9ac540 | ||
|
|
2bf6fcac2f | ||
|
|
e26a8c4742 | ||
|
|
02ebf8aa07 | ||
|
|
a7467d486d | ||
|
|
c26c2ebc7f | ||
|
|
35659a2878 | ||
|
|
7540dd57de | ||
|
|
a538fe1684 | ||
|
|
9b8cb3b583 | ||
|
|
fd59fb3e5e | ||
|
|
61cd288359 | ||
|
|
58192b2959 | ||
|
|
6ad888d399 | ||
|
|
8748a4b3ea | ||
|
|
70ebf06663 | ||
|
|
c42d5eae27 | ||
|
|
6e4dceda0c | ||
|
|
3b32b38586 | ||
|
|
169d0d33a0 | ||
|
|
c9784bf18f | ||
|
|
e94b0bf041 | ||
|
|
668eed9c64 | ||
|
|
3d57486030 | ||
|
|
ee1575cac7 | ||
|
|
79df200ccb | ||
|
|
b0d1b954dd | ||
|
|
7fb6f7418f | ||
|
|
72bd21c648 | ||
|
|
3334059df7 | ||
|
|
7aacea387e |
@@ -0,0 +1,2 @@
|
||||
SESSION_SECRET=odW+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo=
|
||||
DEV_URL=http://localhost
|
||||
+1
-1
@@ -31,7 +31,7 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
# .env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ WORKDIR /no-copy-frontend
|
||||
|
||||
COPY . .
|
||||
COPY --from=dependencies /no-copy-frontend/node_modules ./node_modules
|
||||
RUN npm run build
|
||||
RUN rm -rf .next && npm run build --force
|
||||
|
||||
FROM node:22.20.0 AS runner
|
||||
WORKDIR /no-copy-frontend
|
||||
|
||||
@@ -6,6 +6,10 @@ npm run dev
|
||||
|
||||
Open [http://localhost:2999](http://localhost:2999)
|
||||
|
||||
## Смена портов
|
||||
в .env меняешь "DEV_URL" это урл до бека
|
||||
в package.json "-p 2999" - это порт на котором стартанет приложение.
|
||||
|
||||
|
||||
## Docker
|
||||
docker-compose down -v --rmi all
|
||||
@@ -26,4 +30,15 @@ docker network ls
|
||||
|
||||
docker network connect no-copy_app-network no-copy-frontend
|
||||
|
||||
docker network inspect no-copy_app-network
|
||||
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
|
||||
+9
-1
@@ -1,8 +1,16 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
const { version } = require('./package.json');
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* output: 'export' */
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '10mb',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
APP_VERSION: version,
|
||||
},
|
||||
};
|
||||
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
Generated
+1269
-723
File diff suppressed because it is too large
Load Diff
+14
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.1.1",
|
||||
"version": "0.120.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
@@ -9,20 +9,31 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@react-input/mask": "^2.0.4",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tanstack/match-sorter-utils": "^8.19.4",
|
||||
"@tanstack/react-query": "^5.90.11",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@vkid/sdk": "^2.6.2",
|
||||
"card-validator": "^10.0.4",
|
||||
"clsx": "^2.1.1",
|
||||
"jose": "^6.1.2",
|
||||
"next": "^16.0.7",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"next-intl": "^4.5.8",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-slick": "^0.31.0",
|
||||
"recharts": "^3.5.1",
|
||||
"sonner": "^2.0.7",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwindcss": "^4.1.17",
|
||||
"zod": "^4.1.13"
|
||||
"use-debounce": "^10.1.0",
|
||||
"validator": "^13.15.26",
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
@@ -31,6 +42,7 @@
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.0.3",
|
||||
"sass": "^1.94.2",
|
||||
"types-yoomoneycheckoutwidget": "^0.0.2",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"buildTime": null
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,34 @@
|
||||
import { useTranslations } from 'next-intl'
|
||||
import LogoIcon from '@/app/ui/logo-icon';
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
import Link from 'next/link';
|
||||
import ForgotPasswordForm from '@/app/ui/forms/forgot-password-form';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Login-register-form');
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
<h1>NO COPY</h1>
|
||||
<p>
|
||||
{t('recover-password')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ForgotPasswordForm/>
|
||||
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
<Link href="register"> {t('register')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +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";
|
||||
|
||||
@@ -28,11 +31,12 @@ export default async function RootLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<html>
|
||||
<body>
|
||||
<NextIntlClientProvider>
|
||||
<NextIntlClientProvider locale={locale}>
|
||||
<Providers>
|
||||
{children}
|
||||
<ToastProvider />
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Metadata } from 'next';
|
||||
import styles from '@/app/styles/login.module.scss'
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
import LogoIcon from '@/app/ui/logo-icon';
|
||||
import Link from 'next/link';
|
||||
import LoginForm from '@/app/ui/login-form';
|
||||
import LoginForm from '@/app/ui/forms/login-form';
|
||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import VKLogin from '@/app/components/VKLogin';
|
||||
import YandexAuthWithScript from '@/app/components/YandexLogin';
|
||||
import { getHealt } from '@/app/actions/action';
|
||||
import { Suspense } from 'react';
|
||||
import {YandexLoginButton} from '@/app/components/YandexLoginButton';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Login',
|
||||
@@ -13,51 +18,69 @@ export const metadata: Metadata = {
|
||||
export default function Page() {
|
||||
const t = useTranslations('Login-register-form');
|
||||
|
||||
async function HealthData() {
|
||||
const data = await getHealt();
|
||||
if (!data) {
|
||||
return <div>Сервер временно не работает</div>
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container-language']}`}>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<HealthData />
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
{/* <div className={`${styles['login-container-language']}`}>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
<h1>NO COPY</h1>
|
||||
<p>
|
||||
{t('copyright-protection')}
|
||||
</p>
|
||||
</div>
|
||||
<LoginForm />
|
||||
<div className="divider">
|
||||
<span>
|
||||
{t('or-log-in-via')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
<Link href="register"> {t('register')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${styles['features']}`}>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{t('protecting-watermark')}
|
||||
</div> */}
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
<h1>NO COPY</h1>
|
||||
<p>
|
||||
{t('copyright-protection')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
{t('monitoring-violations')}
|
||||
<LoginForm />
|
||||
<div className="divider">
|
||||
<span>
|
||||
{t('or-log-in-via')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path>
|
||||
</svg>
|
||||
{t('safe-storage')}
|
||||
<div className="mb-4">
|
||||
<VKLogin />
|
||||
</div>
|
||||
<div>
|
||||
<YandexLoginButton />
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
<Link href="register"> {t('register')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className={`${styles['features']}`}>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{t('protecting-watermark')}
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
{t('monitoring-violations')}
|
||||
</div>
|
||||
<div className={`${styles['feature']}`}>
|
||||
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path>
|
||||
</svg>
|
||||
{t('safe-storage')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { Metadata } from 'next';
|
||||
import { Suspense } from 'react';
|
||||
import ProtectionOverview from '@/app/ui/dashboard/protection-overview';
|
||||
import StatsGrid from '@/app/ui/dashboard/stats-grid';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import DashboardUserStats from '@/app/ui/dashboard/new/dashboard-user-stats';
|
||||
import DashboardUserFiles from '@/app/ui/dashboard/new/dashboard-user-files';
|
||||
import DashboardPlatformsList from '@/app/ui/dashboard/new/dashboard-platforms-list';
|
||||
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 DashboardUserViolations from '@/app/ui/dashboard/new/dashboard-user-violations';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard',
|
||||
@@ -13,15 +20,32 @@ export default function Page() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Suspense fallback={<>...</>}>
|
||||
<h1 className="page-title">
|
||||
{t("statistics")}
|
||||
</h1>
|
||||
<ProtectionOverview />
|
||||
<StatsGrid />
|
||||
<FilesTable />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Suspense fallback={<>...</>}>
|
||||
<PageTitle title="statistics" />
|
||||
<DashboardUserStats />
|
||||
<div className="dashboard-main-grid">
|
||||
{/* <DashboardUserFiles />
|
||||
<DashboardUserViolations /> */}
|
||||
|
||||
<div className="left-column">
|
||||
<DashboardUserFiles />
|
||||
</div>
|
||||
<div className="right-column">
|
||||
<DashboardUserViolations />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<DashboardFilesInfo />
|
||||
<div className="dashboard-main-grid">
|
||||
|
||||
{/* <DahboardGeographySection /> */}
|
||||
</div>
|
||||
<div
|
||||
className="mb-4"
|
||||
>
|
||||
{/* <DahboardLimitsSection /> */}
|
||||
</div>
|
||||
<DashboardLastCheck />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Metadata } from 'next';
|
||||
import { Suspense } from 'react';
|
||||
import ProtectionOverview from '@/app/ui/dashboard/protection-overview';
|
||||
import StatsGrid from '@/app/ui/dashboard/stats-grid';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard',
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<>...</>}>
|
||||
<PageTitle title="statistics" />
|
||||
<div className="split-blocks">
|
||||
<ProtectionOverview />
|
||||
<StatsGrid />
|
||||
</div>
|
||||
<FilesTable fileType={'all'} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import PopularAskedQuestions from '@/app/ui/faq/popular-asked-questions';
|
||||
import FrequentlyAskedQuestions from '@/app/ui/faq/frequently-asked-questions';
|
||||
import FaqContact from '@/app/ui/faq/faq-contact';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title="frequently-asked-questions" />
|
||||
<PopularAskedQuestions />
|
||||
<FrequentlyAskedQuestions />
|
||||
<FaqContact />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// app/files/[id]/page.tsx
|
||||
'use server'
|
||||
|
||||
import FilePageTitle from '@/app/ui/file-page/file-page-title';
|
||||
import FilePageViolationInfo from '@/app/ui/file-page/file-page-violation-info';
|
||||
import ViolationPpageFileStatistic from '@/app/ui/file-page/file-page-file-statistic';
|
||||
import FilePageFileInfo from '@/app/ui/file-page/file-page-file-info';
|
||||
import FilePageActions from '@/app/ui/file-page/file-page-actions';
|
||||
import FilePageViolationsList from '@/app/ui/file-page/violation-table/file-page-violations-list';
|
||||
import FilePageNote from '@/app/ui/file-page/file-page-note';
|
||||
import { viewFileInfo } from '@/app/actions/fileEntity';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import FilePageNotFound from '@/app/ui/file-page/file-page-file-not-found';
|
||||
|
||||
type MonitoringStatus = 'NONE' | 'MONITORING_DAILY' | 'MONITORING_WEEKLY' | 'MONITORING_MONTHLY';
|
||||
type MimeType = 'image' | 'video' | 'audio' | 'document';
|
||||
|
||||
export interface FileDetails {
|
||||
id: string;
|
||||
userId: number;
|
||||
originalFileName: string;
|
||||
storedFileName: string;
|
||||
filePath: string;
|
||||
protectedFilePath: string;
|
||||
fileSize: number;
|
||||
mimeType: MimeType;
|
||||
fileExtension: string;
|
||||
checksum: string;
|
||||
uploadSessionId: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
formattedSize: string;
|
||||
downloadUrl: string;
|
||||
existsOnDisk: boolean;
|
||||
supportId: number | null;
|
||||
protectStatus: string;
|
||||
ownerName: string;
|
||||
ownerEmail: string;
|
||||
ownerCompany: string | null;
|
||||
fileName: string;
|
||||
fileFormat: string | null;
|
||||
checksCount: number;
|
||||
fileUploadDate: string;
|
||||
monitoring: MonitoringStatus;
|
||||
thumbnailFileUrl: string;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ page?: string, status?: string, violationId?: string, caseType?: 'complaint' | 'claim', startDate: string, endDate: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
searchParams
|
||||
}: PageProps) {
|
||||
const { id } = await params;
|
||||
const { page, status, violationId, caseType, startDate, endDate } = await searchParams;
|
||||
const currentPage = Number(page) || 1;
|
||||
|
||||
try {
|
||||
const fileInfo: FileDetails = await viewFileInfo(id);
|
||||
|
||||
return (
|
||||
<div className="violation-details">
|
||||
<FilePageTitle fileInfo={fileInfo} />
|
||||
<div>
|
||||
<div
|
||||
className="grid grid-cols-[2fr_1fr] gap-3"
|
||||
>
|
||||
<FilePageFileInfo fileInfo={fileInfo} />
|
||||
{/* <FilePageActions /> */}
|
||||
<ViolationPpageFileStatistic id={id} />
|
||||
</div>
|
||||
<div>
|
||||
<FilePageViolationsList
|
||||
currentPage={currentPage}
|
||||
status={status}
|
||||
fileId={id}
|
||||
violationId={violationId}
|
||||
caseType={caseType}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
{/* <FilePageNote /> */}
|
||||
{/* <FilePageViolationInfo /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
return (
|
||||
<FilePageNotFound />
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,27 @@
|
||||
import NavLinks from '@/app/ui/nav-links'
|
||||
import styles from '@/app/styles/page.module.scss'
|
||||
import NavLinks from '@/app/ui/navigation/nav-links'
|
||||
import HeaderPanel from '@/app/ui/header/headerPanel';
|
||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
|
||||
import ActivityTracker from '@/app/components/ActivityTracker';
|
||||
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
await Promise.all([
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['userData'],
|
||||
queryFn: () => getUserData()
|
||||
})
|
||||
])
|
||||
await prefetchLayoutQueries(queryClient);
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<NavLinks />
|
||||
<div className={`${styles['main-containter-wrapper']}`}>
|
||||
<div className="main-containter-wrapper">
|
||||
<HeaderPanel />
|
||||
<main className={`${styles['main-containter']}`}>
|
||||
<main className="main-containter">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</HydrationBoundary>
|
||||
<ActivityTracker/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,64 @@
|
||||
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
|
||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||
import TestSection from '@/app/ui/marking-page/test-section';
|
||||
import { IconAudioFile } from '@/app/ui/icons/icons';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'File A',
|
||||
checks: 4,
|
||||
violations: 2
|
||||
},
|
||||
{
|
||||
name: 'File B',
|
||||
checks: 8,
|
||||
violations: 4
|
||||
},
|
||||
{
|
||||
name: 'File C',
|
||||
checks: 2,
|
||||
violations: 1
|
||||
},
|
||||
{
|
||||
name: 'File D',
|
||||
checks: 7,
|
||||
violations: 1
|
||||
},
|
||||
{
|
||||
name: 'File E',
|
||||
checks: 5,
|
||||
violations: 4
|
||||
},
|
||||
{
|
||||
name: 'File F',
|
||||
checks: 5,
|
||||
violations: 3
|
||||
}
|
||||
];
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "audio";
|
||||
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<div className="page-title">
|
||||
<h1 >Защита аудио</h1>
|
||||
</div>
|
||||
<ProtectionSummary />
|
||||
<TestSection />
|
||||
<ProtectedFilesTable />
|
||||
<PageTitleColorFrame
|
||||
title="audio-protection"
|
||||
description="multi-layered-protection-for-your-audios"
|
||||
/>
|
||||
<ProtectionStatistic fileType={FILE_TYPE} />
|
||||
{/* <div className="split-blocks">
|
||||
<ProtectionSummary fileType={FILE_TYPE} />
|
||||
<div className="protection-overview">
|
||||
<StackedBarChart data={data} />
|
||||
</div>
|
||||
</div> */}
|
||||
<UploadSectionFile fileType={FILE_TYPE} allowedExtensions={file_extension} maxFileSize={max_file_size} />
|
||||
<FilesTable fileType={FILE_TYPE} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "document";
|
||||
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitleColorFrame
|
||||
title="document-protection"
|
||||
description="multi-layered-protection-for-your-document"
|
||||
/>
|
||||
<ProtectionStatistic fileType={FILE_TYPE} />
|
||||
<UploadSectionFile
|
||||
fileType={FILE_TYPE}
|
||||
allowedExtensions={file_extension}
|
||||
maxFileSize={max_file_size}
|
||||
/>
|
||||
<FilesTable fileType={FILE_TYPE} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'File A',
|
||||
checks: 4,
|
||||
violations: 2
|
||||
},
|
||||
{
|
||||
name: 'File B',
|
||||
checks: 8,
|
||||
violations: 4
|
||||
},
|
||||
{
|
||||
name: 'File C',
|
||||
checks: 2,
|
||||
violations: 1
|
||||
},
|
||||
{
|
||||
name: 'File D',
|
||||
checks: 7,
|
||||
violations: 1
|
||||
},
|
||||
{
|
||||
name: 'File E',
|
||||
checks: 5,
|
||||
violations: 4
|
||||
},
|
||||
];
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "image";
|
||||
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitleColorFrame
|
||||
title="image-protection"
|
||||
description="multi-layered-protection-for-your-images"
|
||||
/>
|
||||
<ProtectionStatistic fileType={FILE_TYPE}/>
|
||||
{/* <div className="split-blocks">
|
||||
<ProtectionSummary fileType={FILE_TYPE} />
|
||||
<div className="protection-overview">
|
||||
<StackedBarChart data={data} />
|
||||
</div>
|
||||
</div> */}
|
||||
<UploadSectionFile fileType={FILE_TYPE} allowedExtensions={file_extension} maxFileSize={max_file_size} />
|
||||
<FilesTable fileType={FILE_TYPE} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
|
||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||
import TestSection from '@/app/ui/marking-page/test-section';
|
||||
import UploadSectionImage from '@/app/ui/marking-page/upload-section-image';
|
||||
import { IconImageFile } from '@/app/ui/icons/icons';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<div className="page-title">
|
||||
<h1 >Защита изображений</h1>
|
||||
</div>
|
||||
<ProtectionSummary />
|
||||
<UploadSectionImage />
|
||||
<TestSection />
|
||||
<ProtectedFilesTable />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,64 @@
|
||||
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
|
||||
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
|
||||
import TestSection from '@/app/ui/marking-page/test-section';
|
||||
import UploadSectionVideo from '@/app/ui/marking-page/upload-section-video';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import UploadSectionFile from '@/app/components/UploadSectionFile';
|
||||
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
|
||||
import { StackedBarChart } from '@/app/components/StackedBarChart';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: 'File A',
|
||||
checks: 4,
|
||||
violations: 2
|
||||
},
|
||||
{
|
||||
name: 'File B',
|
||||
checks: 8,
|
||||
violations: 4
|
||||
},
|
||||
{
|
||||
name: 'File C',
|
||||
checks: 2,
|
||||
violations: 1
|
||||
},
|
||||
{
|
||||
name: 'File D',
|
||||
checks: 7,
|
||||
violations: 1
|
||||
},
|
||||
{
|
||||
name: 'File E',
|
||||
checks: 5,
|
||||
violations: 4
|
||||
},
|
||||
{
|
||||
name: 'File F',
|
||||
checks: 5,
|
||||
violations: 3
|
||||
}
|
||||
];
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "video";
|
||||
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<div className="page-title">
|
||||
<h1 >Защита видео</h1>
|
||||
</div>
|
||||
<ProtectionSummary />
|
||||
<UploadSectionVideo />
|
||||
<TestSection />
|
||||
<ProtectedFilesTable />
|
||||
<PageTitleColorFrame
|
||||
title="video-protection"
|
||||
description="multi-layered-protection-for-your-videos"
|
||||
/>
|
||||
<ProtectionStatistic fileType={FILE_TYPE} />
|
||||
{/* <div className="split-blocks">
|
||||
<ProtectionSummary fileType={FILE_TYPE} />
|
||||
<div className="protection-overview">
|
||||
<StackedBarChart data={data} />
|
||||
</div>
|
||||
</div> */}
|
||||
<UploadSectionFile fileType={FILE_TYPE} allowedExtensions={file_extension} maxFileSize={max_file_size} />
|
||||
<FilesTable fileType={FILE_TYPE} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import MyContentInfoBlock from '@/app/ui/my-content/my-content-info-block';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import MyContentPieChart from '@/app/ui/my-content/my-content-pie-chart';
|
||||
import MyContentStatsOverview from '@/app/ui/my-content/new/my-content-stats-overview';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<>
|
||||
<PageTitleColorFrame title="my-content" description="my-content-description"/>
|
||||
<MyContentStatsOverview />
|
||||
{/* <div className="split-blocks">
|
||||
<MyContentInfoBlock />
|
||||
<MyContentPieChart />
|
||||
</div> */}
|
||||
<FilesTable fileType={'all'} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Suspense } from 'react';
|
||||
import ReferralStats from '@/app/ui/referral-page/referral-stats';
|
||||
import ReferralLinkSection from '@/app/ui/referral-page/referral-link-section';
|
||||
import CommissionLevels from '@/app/ui/referral-page/commission-levels';
|
||||
import InvitationsTable from '@/app/ui/referral-page/table/invitations-table';
|
||||
import IncomeAndPayments from '@/app/ui/referral-page/Income-and-payments';
|
||||
import SettingReferralProgram from '@/app/ui/referral-page/setting-referral-program';
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<Suspense fallback={<>...</>}>
|
||||
<PageTitleColorFrame title="refferal-program" description="referral-program-description" />
|
||||
<CommissionLevels />
|
||||
{/* <ReferralStats /> */}
|
||||
<ReferralLinkSection />
|
||||
<InvitationsTable />
|
||||
<IncomeAndPayments />
|
||||
<SettingReferralProgram />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +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>
|
||||
<h1 className="page-title">Отчёты и аналитика</h1>
|
||||
<StatsGrid />
|
||||
<PageTitle title="reports-and-analytics" />
|
||||
<ReportsInfo />
|
||||
<ActivityByContentType />
|
||||
<div className="analytics-grid">
|
||||
<AnalyticsCardMonth />
|
||||
{/* <AnalyticsCardMonth /> */}
|
||||
<AnalyticsCardDistribution />
|
||||
<AnalyticsCardGeography />
|
||||
</div>
|
||||
<ViolationsTable />
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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 SearchTitle from '@/app/ui/search/search-title';
|
||||
|
||||
export default async function Page() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchTitle />
|
||||
<div className="search-grid">
|
||||
<SectionSearchFile />
|
||||
<div className="search-sidebar">
|
||||
<SupportedFormats />
|
||||
<SearchStats />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -5,20 +5,29 @@ import SubscriptionSettings from '@/app/ui/settings/subscription-settings';
|
||||
import APISettings from '@/app/ui/settings/api-settings';
|
||||
import LastActivitySettings from '@/app/ui/settings/last-activity-settings';
|
||||
import DangerZone from '@/app/ui/settings/danger-zone';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel';
|
||||
import SettingUserVerification from '@/app/ui/settings/setting-user-verification';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="page-title">⚙️ Настройки аккаунта</h1>
|
||||
<PageTitle title="account-settings" />
|
||||
<div className="settings-grid">
|
||||
<PersonalDataSettings />
|
||||
<NotificationsMonitoringSettings/>
|
||||
<SafetySetting/>
|
||||
<SubscriptionSettings/>
|
||||
<APISettings/>
|
||||
<LastActivitySettings/>
|
||||
{/* <NotificationsMonitoringSettings /> */}
|
||||
<SafetySetting />
|
||||
<SubscriptionSettings />
|
||||
{/* <APISettings /> */}
|
||||
{/* LastActivitySettings возможно нужно будет вернуть */}
|
||||
{/* <LastActivitySettings /> */}
|
||||
<CompanyUsersSettingPanel />
|
||||
<SettingUserVerification />
|
||||
</div>
|
||||
<DangerZone/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
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';
|
||||
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
|
||||
title="monitoring-copyright-infringements"
|
||||
description="monitoring-copyright-infringements-description"
|
||||
/>
|
||||
<ViolationsStatistic />
|
||||
<ViolationsCheckAllSection />
|
||||
<ViolationsTable />
|
||||
<ViolationsLastCombinedCases />
|
||||
{/* <ViolationsMyComplaint />
|
||||
<ViolationsMyClaims /> */}
|
||||
<div className="analytics-grid">
|
||||
<AnalyticsCardDistribution />
|
||||
<AnalyticsCardGeography />
|
||||
</div>
|
||||
<div className="analytics-grid">
|
||||
<ViolationsMyClaimsTable />
|
||||
<ViolationsMyComplaintsTable />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import LogoIcon from '@/app/ui/logo-icon';
|
||||
import styles from '@/app/styles/login.module.scss'
|
||||
import styles from '@/app/styles/module/login.module.scss'
|
||||
import Link from 'next/link';
|
||||
import RegisterForm from '@/app/ui/register-form';
|
||||
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
|
||||
import RegisterForm from '@/app/ui/forms/register-form';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
export default function Page() {
|
||||
@@ -10,9 +9,6 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container-language']}`}>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslations } from 'next-intl'
|
||||
import LogoIcon from '@/app/ui/logo-icon';
|
||||
import styles from '@/app/styles/module/login.module.scss';
|
||||
import Link from 'next/link';
|
||||
import ResetPasswordForm from '@/app/ui/forms/reset-password-form';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Login-register-form');
|
||||
|
||||
return (
|
||||
<div className={`${styles['login-container-wrapper']}`}>
|
||||
<div className={`${styles['login-container']}`}>
|
||||
<div className={`${styles['logo']}`}>
|
||||
<LogoIcon />
|
||||
<h1>NO COPY</h1>
|
||||
<p>
|
||||
{t('recover-password')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ResetPasswordForm />
|
||||
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
|
||||
</div>
|
||||
<div className={`${styles['register-link']}`}>
|
||||
<p>{t('no-account')}?
|
||||
<Link href="register"> {t('register')}</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { PdfProvider } from '@/app/contexts/PdfContext';
|
||||
import { WatchDocProviders } from '@/app/providers/WatchDocProviders';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<WatchDocProviders>
|
||||
<PdfProvider>
|
||||
{children}
|
||||
</PdfProvider>
|
||||
</WatchDocProviders>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
'use server';
|
||||
|
||||
import Link from 'next/link';
|
||||
import PdfViewer from '@/app/components/PdfViewer';
|
||||
import { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
|
||||
import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
|
||||
import { fetchDocumentInfo } from '@/app/actions/trackingActions';
|
||||
import { LoginUserBage } from '@/app/components/document-viewer/LoginUserBage';
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ docid: string; title?: string }>;
|
||||
}
|
||||
|
||||
interface DocumentInfo {
|
||||
checksCount?: number;
|
||||
checksum?: string;
|
||||
createdAt?: string;
|
||||
downloadUrl?: string;
|
||||
existsOnDisk?: boolean;
|
||||
fileExtension?: string;
|
||||
fileFormat?: string | null;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
fileSize?: number;
|
||||
fileUploadDate?: string;
|
||||
formattedSize?: string;
|
||||
id: string;
|
||||
mimeType?: string;
|
||||
monitoring?: string;
|
||||
originalFileName?: string;
|
||||
ownerCompany?: string | null;
|
||||
ownerEmail?: string;
|
||||
ownerName?: string;
|
||||
permissions?: {
|
||||
DOWNLOAD?: boolean;
|
||||
};
|
||||
protectStatus?: string;
|
||||
protectedFilePath?: string;
|
||||
status?: string;
|
||||
storedFileName?: string;
|
||||
supportId?: number;
|
||||
thumbnailFileUrl?: string;
|
||||
updatedAt?: string;
|
||||
uploadSessionId?: string;
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const { docid, title } = await searchParams;
|
||||
|
||||
let response: DocumentInfo | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
response = await fetchDocumentInfo(docid);
|
||||
} catch (err) {
|
||||
error = 'Не удалось загрузить информацию о документе';
|
||||
}
|
||||
|
||||
const documentTitle = response?.fileName || 'FileName';
|
||||
|
||||
if (error || !response) {
|
||||
return (
|
||||
<div className="watch-doc-wrapper">
|
||||
<div className="watch-doc-page">
|
||||
<header className="watch-doc-header">
|
||||
<Link href="#" className="header-logo">NoCopy</Link>
|
||||
<LoginUserBage />
|
||||
</header>
|
||||
<div className="error-container">
|
||||
<h2>Ошибка загрузки документа</h2>
|
||||
<p>{error || 'Документ не найден'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="watch-doc-wrapper">
|
||||
<div className="watch-doc-page">
|
||||
<header className="watch-doc-header">
|
||||
<Link href="#" className="header-logo">NoCopy</Link>
|
||||
<LoginUserBage />
|
||||
</header>
|
||||
|
||||
<div className="watch-doc-status">
|
||||
<div className="st-title">Защита подтверждена</div>
|
||||
<div className="st-sub">
|
||||
Документ <strong>{response?.fileName ? response?.fileName : '#'}</strong> зарегистрирован и защищён платформой NoCopy.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-grid">
|
||||
<div
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<div className="watch-doc-card">
|
||||
<div className="card-head">Правообладатель</div>
|
||||
<div className="card-body">
|
||||
<div className="owner-row">
|
||||
<div className="owner-av">
|
||||
{response?.fileName ? response?.fileName[0] : ''}
|
||||
</div>
|
||||
<div>
|
||||
{response?.ownerName && (
|
||||
<div className="owner-name">{response?.ownerName}</div>
|
||||
)}
|
||||
{response?.ownerCompany && (
|
||||
<div className="owner-name">{response?.ownerCompany}</div>
|
||||
)}
|
||||
{response?.ownerName && (
|
||||
<div className="owner-meta">Зарегистрирован в NoCopy</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-actions">
|
||||
{response?.permissions?.DOWNLOAD && (
|
||||
<DownloadButton fileId={docid} fileName={response?.fileName}/>
|
||||
)}
|
||||
<CopyLinkButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="watch-doc-card">
|
||||
<div className="card-head">Предпросмотр</div>
|
||||
<div className="card-body">
|
||||
<PdfViewer
|
||||
fileId={docid}
|
||||
fileName={documentTitle}
|
||||
showDownload={response.permissions?.DOWNLOAD}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-footer">
|
||||
<Link href="#">NoCopy</Link>
|
||||
<Link href="/terms-of-use">Условия</Link>
|
||||
<Link href="/privacy-policy">Конфиденциальность</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function getSearchSettings() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/search/settings`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.message_body) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function putSearchSettings(proxy: boolean, yandex: boolean, google: boolean) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/search/settings`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
engines: {
|
||||
yandex: yandex,
|
||||
google: google
|
||||
},
|
||||
proxyEnabled: proxy
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.message_desc === 'Settings updated') {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+196
-35
@@ -1,42 +1,14 @@
|
||||
'use server'
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { localDevelopmentUrl, testUserData } from '@/app/actions/definitions';
|
||||
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'development'
|
||||
? localDevelopmentUrl
|
||||
: 'http://app:8080';
|
||||
|
||||
const FRUITS_URL = 'https://www.fruityvice.com/api/fruit/';
|
||||
const ALL = 'all';
|
||||
|
||||
export async function fetchFruits() {
|
||||
try {
|
||||
const response = await fetch(FRUITS_URL + ALL, { 'method': 'GET' });
|
||||
let parsed = await response.json();
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
throw new Error('Failed to fetch fruits data.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchFruit(id: number) {
|
||||
try {
|
||||
const response = await fetch(FRUITS_URL + id, { 'method': 'GET' });
|
||||
let parsed = await response.json();
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
throw new Error('Failed to fetch fruits data.');
|
||||
}
|
||||
}
|
||||
import { getSessionData, deleteSession } from '@/app/actions/session';
|
||||
import { testUserData, API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function getUserData() {
|
||||
const userEmail = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
|
||||
/* для теста */
|
||||
if (userEmail === "test" && token === "1111") {
|
||||
if (userEmail === 'test' && token === '1111') {
|
||||
return testUserData;
|
||||
}
|
||||
/* для теста */
|
||||
@@ -45,16 +17,205 @@ export async function getUserData() {
|
||||
const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('error');
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserFilesInfo() {
|
||||
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: 'user_files_info',
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else if (parsed.message_code === 2) {
|
||||
await deleteSession();
|
||||
} else {
|
||||
throw parsed.message_code;
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30001,
|
||||
message_body: {
|
||||
action: 'get',
|
||||
tariff_term: tariffTerm
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json()
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed.message_body?.tariffs;
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTokensBundle() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30001,
|
||||
message_body: {
|
||||
action: 'tariffs_by_type',
|
||||
type: 'token'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed.message_body?.token_tariffs;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBuildData() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check/api/build`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchINN(inn: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30004,
|
||||
message_body: {
|
||||
inn: inn,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json()
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed.message_body
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHealt() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check/api/healt`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+512
-64
@@ -1,12 +1,10 @@
|
||||
'use server'
|
||||
|
||||
import { SignupFormSchema, loginFormSchema, localDevelopmentUrl } from '@/app/actions/definitions';
|
||||
import { createSession, deleteSession, getSessionData } from '@/app/actions/session';
|
||||
import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
|
||||
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
const API_BASE_URL = process.env.NODE_ENV === 'development'
|
||||
? localDevelopmentUrl
|
||||
: 'http://app:8080';
|
||||
import { fetchINN } from '@/app/actions/action';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function logout() {
|
||||
const token = await getSessionData('token');
|
||||
@@ -15,17 +13,16 @@ export async function logout() {
|
||||
await fetch(`${API_BASE_URL}/v1/api/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
await deleteSession();
|
||||
}
|
||||
|
||||
await deleteSession();
|
||||
redirect('/login');
|
||||
}
|
||||
|
||||
export async function authorization(
|
||||
@@ -41,8 +38,8 @@ export async function authorization(
|
||||
const password = formData.get('password');
|
||||
|
||||
/* для теста */
|
||||
if (email === "test" && password === "test") {
|
||||
await createSession("1111", "test");
|
||||
if (email === 'test' && password === 'test') {
|
||||
await createSession('1111', 'test');
|
||||
redirect('/pages/dashboard');
|
||||
}
|
||||
/* для теста */
|
||||
@@ -75,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,
|
||||
@@ -86,20 +83,38 @@ export async function authorization(
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
await createSession(parsed.message_body.token, email);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.token, email);
|
||||
} else {
|
||||
throw (`${parsed.message_code}`);
|
||||
}
|
||||
} else {
|
||||
throw ('request-ended-with-an-error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
errors['server'] = `${error}`;
|
||||
|
||||
switch (error) {
|
||||
case '2':
|
||||
errors['server'] = 'password-does-not-match'
|
||||
break;
|
||||
|
||||
case '4':
|
||||
errors['server'] = 'email-not-found'
|
||||
break;
|
||||
|
||||
default:
|
||||
errors['server'] = 'request-ended-with-an-error'
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
@@ -116,39 +131,452 @@ export async function authorization(
|
||||
redirect('/pages/dashboard');
|
||||
}
|
||||
|
||||
type FormState = {
|
||||
export type FormState = {
|
||||
previousState: {
|
||||
full_name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
phone: string;
|
||||
company: string;
|
||||
agree: string;
|
||||
}
|
||||
error: Record<string, string>
|
||||
fullName?: string | undefined;
|
||||
email?: string | undefined;
|
||||
password?: string | undefined;
|
||||
confirm_password?: string | undefined;
|
||||
phone?: string | undefined;
|
||||
companyName?: string | undefined;
|
||||
agree?: string | undefined;
|
||||
referralCode?: string | undefined;
|
||||
inn?: string | undefined;
|
||||
} | undefined,
|
||||
mailConfirm?: boolean,
|
||||
userId?: number,
|
||||
error: Record<string, string> | null
|
||||
};
|
||||
|
||||
export async function registration(
|
||||
state: FormState | undefined,
|
||||
formData: FormData,
|
||||
): Promise<FormState> {
|
||||
const full_name = formData.get('full_name') as string || '';
|
||||
const headersList = await headers();
|
||||
const accountType = formData.get('accountType') as 'b2b' | 'b2c';
|
||||
|
||||
/* вернуть в true когда проект выйдет из дева */
|
||||
const mail_verified = false;
|
||||
const fullName = formData.get('fullName') as string || '';
|
||||
const email = formData.get('email') as string || '';
|
||||
const password = formData.get('password') as string || '';
|
||||
const confirm_password = formData.get('confirm_password') as string || '';
|
||||
const phone = formData.get('phone') as string || '';
|
||||
const company = formData.get('company') as string || '';
|
||||
const companyName = formData.get('companyName') as string || '';
|
||||
const agree = formData.get('agree') as string || '';
|
||||
const referralCode = formData.get('referralCode') as string || '';
|
||||
const inn = formData.get('inn') as string || '';
|
||||
|
||||
const validatedFields = SignupFormSchema.safeParse({
|
||||
full_name,
|
||||
|
||||
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 = {
|
||||
fullName,
|
||||
email,
|
||||
phone: phone.replace(/[-\(\)\s]/g, ''),
|
||||
password,
|
||||
confirm_password,
|
||||
agree,
|
||||
});
|
||||
...(accountType === 'b2b' && { companyName, inn })
|
||||
};
|
||||
|
||||
const validatedFields = await SignupFormSchema.safeParse(dataToValidate);
|
||||
|
||||
console.log(`try to register ${accountType}`);
|
||||
|
||||
let confirmedCompanyName: boolean = true;
|
||||
if (accountType === 'b2b') {
|
||||
try {
|
||||
const response = await fetchINN(inn);
|
||||
confirmedCompanyName = response?.companyName ? true : false;
|
||||
} catch (error) {
|
||||
confirmedCompanyName = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validatedFields.success || !confirmedCompanyName) {
|
||||
const errors: Record<string, string> = {}
|
||||
if (validatedFields?.error?.message) {
|
||||
JSON?.parse(validatedFields?.error?.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!confirmedCompanyName) {
|
||||
errors.inn = 'inn-invalid'
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
agree,
|
||||
...(referralCode !== '' && { referralCode }),
|
||||
...(accountType === 'b2b' && { companyName, inn })
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { fullName, email, password, phone } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20002,
|
||||
message_body: {
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
mail_verified: mail_verified,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||
...(referralCode !== '' && { referralLink: referralCode })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
|
||||
...(referralCode !== '' && { referralLink: referralCode })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json() as {
|
||||
message_desc: string,
|
||||
message_body: { token: string, userId: number, fieldErrors: string },
|
||||
message_code: number
|
||||
};
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
console.log(`registration - ${accountType}`);
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType,
|
||||
...(referralCode !== '' && { referralLink: referralCode }),
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
|
||||
});
|
||||
|
||||
if (mail_verified) {
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
agree,
|
||||
...(referralCode !== '' && { referralLink: referralCode }),
|
||||
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
|
||||
},
|
||||
mailConfirm: true,
|
||||
userId: parsed.message_body.userId,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
await createSession(parsed.message_body.token, email as string);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
|
||||
switch (typedError.message_code) {
|
||||
case 1:
|
||||
if (typedError.message_desc === 'Refferal link is not exist') {
|
||||
errors['server'] = 'referral-link-is-not-exist'
|
||||
break;
|
||||
} else {
|
||||
errors['server'] = 'email-or-already-registered'
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
if (typedError.message_body?.fieldErrors) {
|
||||
typedError.message_body?.fieldErrors?.forEach((obj: {
|
||||
code: string,
|
||||
field: string
|
||||
}) => {
|
||||
if (errors[obj.field]) {
|
||||
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
|
||||
} else {
|
||||
errors[obj.field] = obj.code.replaceAll('.', '-');
|
||||
}
|
||||
});
|
||||
|
||||
} else if (typedError.message_desc === 'Company already exists') {
|
||||
errors['inn'] = 'company-already-registered'
|
||||
} else {
|
||||
errors['server'] = 'unknown-error'
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errors['server'] = 'request-ended-with-an-error'
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree,
|
||||
inn
|
||||
},
|
||||
mailConfirm: false,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
companyName,
|
||||
agree,
|
||||
inn
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
/* console.log('/v2');
|
||||
redirect('/v2'); */
|
||||
}
|
||||
|
||||
export async function tokenLifeExtension() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20008
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.message_code === 0) {
|
||||
await updateSession(parsed.message_body.token);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await deleteSession();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function confirmEmail(userId: number, verifyToken: string) {
|
||||
console.log('confirmEmail');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
'Authorization': `Bearer ${verifyToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20009,
|
||||
message_body: {
|
||||
resend: 0,
|
||||
user_id: userId
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.token, parsed.message_body.email as string);
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'entered-code-is-incorrect'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'server-error'
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'network-error'
|
||||
};
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
}
|
||||
|
||||
export async function resendConfirmEmail(userId: number) {
|
||||
console.log('resendConfirmEmail');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20009,
|
||||
message_body: {
|
||||
resend: 1,
|
||||
user_id: userId,
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
fine: true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
fine: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type FormStateConfirmPassword = {
|
||||
previousState: {
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
verifyToken: string;
|
||||
},
|
||||
error: Record<string, string> | null
|
||||
};
|
||||
|
||||
export async function resetPassword(
|
||||
state: any | undefined,
|
||||
formData: FormData,
|
||||
) {
|
||||
const email = formData.get('email');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20010,
|
||||
message_body: {
|
||||
email: email,
|
||||
action: "resetPassword"
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
if (parsed.message_code === 0) {
|
||||
|
||||
} else {
|
||||
throw 'error';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error
|
||||
};
|
||||
}
|
||||
console.log('redirect')
|
||||
redirect(`/reset-password?email=${email}`);
|
||||
}
|
||||
|
||||
|
||||
export async function confirmPassword(
|
||||
state: FormStateConfirmPassword | undefined,
|
||||
formData: any,
|
||||
): Promise<FormStateConfirmPassword | undefined> {
|
||||
const email = formData.get('email');
|
||||
const password = formData.get('password');
|
||||
const confirm_password = formData.get('confirm_password');
|
||||
const verifyToken = formData.get('verify-token');
|
||||
console.log('confirmPassword');
|
||||
|
||||
const SignupFormSchema = await getSignupFormSchema('resetPassword');
|
||||
|
||||
const dataToValidate = {
|
||||
password,
|
||||
confirm_password
|
||||
};
|
||||
|
||||
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
@@ -165,70 +593,90 @@ export async function registration(
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
full_name,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
company,
|
||||
agree
|
||||
verifyToken
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { full_name, email, password, phone } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${verifyToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20002,
|
||||
msg_id: 20010,
|
||||
message_body: {
|
||||
fullName: full_name,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
companyName: company,
|
||||
email: email,
|
||||
password: password,
|
||||
action: "confirmVerification"
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
console.log('--response--');
|
||||
console.log(response);
|
||||
console.log('--response--');
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log('--parsed--');
|
||||
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
await createSession(parsed.message_body.token, email);
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.authToken, email as string);
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
if (error) {
|
||||
const errors: Record<string, string> = {};
|
||||
errors['server'] = 'request-ended-with-an-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: {
|
||||
full_name,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone,
|
||||
company,
|
||||
agree
|
||||
verifyToken
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
verifyToken
|
||||
},
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
'use server'
|
||||
|
||||
import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export type CompanyUserRegistrationFormState = {
|
||||
previousState: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password: string;
|
||||
phone: string;
|
||||
},
|
||||
createdUser: string | null
|
||||
error: Record<string, string> | null
|
||||
} | undefined;
|
||||
|
||||
export async function companyUserRegistration(
|
||||
state: CompanyUserRegistrationFormState | undefined,
|
||||
formData: FormData,
|
||||
): Promise<CompanyUserRegistrationFormState> {
|
||||
const token = await getSessionData('token');
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headersList = await headers();
|
||||
|
||||
const fullName = formData.get('fullName') as string || '';
|
||||
const email = formData.get('email') as string || '';
|
||||
const password = formData.get('password') as string || '';
|
||||
const confirm_password = formData.get('confirm_password') as string || '';
|
||||
const phone = formData.get('phone') as string || '';
|
||||
|
||||
const SignupFormSchema = await getSignupFormSchema('b2c');
|
||||
|
||||
const 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,
|
||||
phone: phone.replace(/[-\(\)\s]/g, ''),
|
||||
password,
|
||||
confirm_password,
|
||||
agree: 'on'
|
||||
};
|
||||
|
||||
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
|
||||
|
||||
console.log(`try to register company users ${email}`);
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('registration error');
|
||||
console.log(errors);
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone
|
||||
},
|
||||
createdUser: null,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { fullName, email, password, phone } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20002,
|
||||
message_body: {
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType: 'b2b',
|
||||
mail_verified: false,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json() as {
|
||||
message_desc: string,
|
||||
message_body: { token: string },
|
||||
message_code: number
|
||||
};
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
console.log(`registration - ${email}`);
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
});
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
phone: ''
|
||||
},
|
||||
createdUser: fullName,
|
||||
error: null
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
|
||||
switch (typedError.message_code) {
|
||||
case 1:
|
||||
errors['server'] = 'email-or-already-registered'
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (typedError.message_body?.fieldErrors) {
|
||||
typedError.message_body?.fieldErrors?.forEach((obj: {
|
||||
code: string,
|
||||
field: string
|
||||
}) => {
|
||||
if (errors[obj.field]) {
|
||||
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
|
||||
} else {
|
||||
errors[obj.field] = obj.code.replaceAll('.', '-');
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
errors['server'] = 'unknown-error'
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errors['server'] = 'request-ended-with-an-error'
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone
|
||||
},
|
||||
createdUser: null,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
fullName,
|
||||
email,
|
||||
password,
|
||||
confirm_password,
|
||||
phone
|
||||
},
|
||||
createdUser: null,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as z from 'zod'
|
||||
import { createValidationSchema, createValidationSchemaForPassword } from '@/app/lib/validation-config-parser';
|
||||
import validationConfig from '@/app/lib/validation-config.json';
|
||||
|
||||
export type FormState =
|
||||
| {
|
||||
@@ -16,32 +18,58 @@ export type FormAction = (
|
||||
formData: FormData
|
||||
) => Promise<FormState>
|
||||
|
||||
export const SignupFormSchema = z
|
||||
.object({
|
||||
full_name: z
|
||||
.string()
|
||||
.min(3, { error: 'register-error-name' })
|
||||
.trim(),
|
||||
email: z.email({ error: 'register-error-valid-email' }).trim(),
|
||||
phone: z
|
||||
.string()
|
||||
.min(12, { error: 'register-error-phone' }),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, { error: 'register-error-password-symbols' })
|
||||
.regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter'})
|
||||
.regex(/[0-9]/, { error: 'register-error-password-one-digit' })
|
||||
.trim(),
|
||||
confirm_password: z
|
||||
.string(),
|
||||
agree: z
|
||||
.string()
|
||||
.min(2, { error: 'register-error-agree' })
|
||||
})
|
||||
.refine((data) => data.password === data.confirm_password, {
|
||||
message: 'repeat-password',
|
||||
path: ['confirm_password'],
|
||||
});
|
||||
interface FieldConfig {
|
||||
maxSize?: { value: number; message: string };
|
||||
minSize?: { value: number; message: string };
|
||||
allowedSymbols?: { value: string; message: string };
|
||||
optional?: { value: boolean };
|
||||
requiredDigit?: { value: boolean; message: string };
|
||||
requiredLetter?: { value: boolean; message: string };
|
||||
caseType?: { value: 'LOWER' | 'UPPER' | 'ANY'; message: string };
|
||||
emailValidation?: { value: boolean; message: string };
|
||||
}
|
||||
|
||||
interface ValidationConfig {
|
||||
[key: string]: FieldConfig;
|
||||
}
|
||||
|
||||
const defaultConfig = validationConfig as ValidationConfig;
|
||||
|
||||
async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('/api/validation-config');
|
||||
if (!response.ok) throw new Error('Failed to load config');
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
//сейчас выполняем это пока бека нету
|
||||
/* console.error('Using default validation config:', error); */
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSignupFormSchema(schemaType?: 'b2b' | 'b2c' | 'resetPassword') {
|
||||
let schemaPromise: Promise<any> | null = null;
|
||||
let cachedSignupSchema: any = null;
|
||||
|
||||
if (!schemaPromise) {
|
||||
schemaPromise = (async () => {
|
||||
const config = await loadConfigFromBackend();
|
||||
if (schemaType === 'resetPassword') {
|
||||
cachedSignupSchema = createValidationSchemaForPassword(config);
|
||||
} else {
|
||||
cachedSignupSchema = createValidationSchema(config, schemaType);
|
||||
}
|
||||
|
||||
return cachedSignupSchema;
|
||||
})();
|
||||
}
|
||||
|
||||
return await schemaPromise;
|
||||
}
|
||||
|
||||
// для валидации в реально времени на клиентской странице
|
||||
export const SignupFormSchema = createValidationSchema(defaultConfig);
|
||||
|
||||
|
||||
export const loginFormSchema = z
|
||||
.object({
|
||||
@@ -55,10 +83,46 @@ export const loginFormSchema = z
|
||||
.trim(),
|
||||
})
|
||||
|
||||
export const localDevelopmentUrl = 'http://localhost';
|
||||
|
||||
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'
|
||||
? localDevelopmentUrl
|
||||
: 'http://app:8080';
|
||||
|
||||
export const testUserData = {
|
||||
fullName: 'test',
|
||||
email: 'test@mail.com',
|
||||
subscriptionType: 'base'
|
||||
}
|
||||
}
|
||||
|
||||
export const bankCardFormSchema = z
|
||||
.object({
|
||||
bankCard: z
|
||||
.string()
|
||||
.min(19, { error: 'error-bank-card' })
|
||||
.trim()
|
||||
})
|
||||
@@ -0,0 +1,205 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
|
||||
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 {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'search_files',
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
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',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed.message_text ? parsed.message_text : 'error';
|
||||
}
|
||||
} else {
|
||||
throw `${response.status}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeUserFile(fileId: string, fullDelete: 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: 20005,
|
||||
message_body: {
|
||||
action: 'delete_file',
|
||||
file_id: fileId,
|
||||
full_delete: fullDelete,
|
||||
}
|
||||
}),
|
||||
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.file_id;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function viewFileInfo(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'file_info',
|
||||
file_id: fileId
|
||||
}
|
||||
}),
|
||||
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 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
'use server'
|
||||
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
|
||||
interface initMessageBody {
|
||||
file_name: string,
|
||||
file_type: string,
|
||||
extension: string,
|
||||
file_size: number | string,
|
||||
action?: string,
|
||||
token?: string,
|
||||
convertTo?: string | undefined | null
|
||||
}
|
||||
|
||||
export async function fileUpload(messageBody: initMessageBody, convertTo?: string | null | undefined) {
|
||||
const token = await getSessionData('token');
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const message = messageBody;
|
||||
message.action = 'init';
|
||||
|
||||
if (convertTo && convertTo !== 'reject') {
|
||||
message.convertTo = convertTo;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20004,
|
||||
message_body: message
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json() as {
|
||||
message_desc: string,
|
||||
message_body: {
|
||||
upload_id: string,
|
||||
total_chunks: number,
|
||||
chunk_size: number,
|
||||
status: string
|
||||
},
|
||||
message_code: string
|
||||
};
|
||||
|
||||
if (parsed.message_desc === 'Operation successful') {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelUpload(udloadId: 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: 20004,
|
||||
message_body: {
|
||||
action: 'cancel',
|
||||
upload_id: udloadId,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json() as {
|
||||
message_code: string,
|
||||
message_desc: string,
|
||||
message_body: {
|
||||
upload_id: string,
|
||||
message: string
|
||||
}
|
||||
};
|
||||
|
||||
if (parsed.message_desc === 'Upload cancelled successfully') {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
export type ChunkResponse = {
|
||||
errorMesage: string | null
|
||||
fileId?: string
|
||||
};
|
||||
|
||||
export async function chunkUpload(formData: FormData, itPrivate?: boolean): Promise<ChunkResponse> {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
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,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
/* ...(token && itPrivate && { 'Authorization': `Bearer ${token}` }) возможно нужно будет условно отправлять токен*/
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
errorMesage: null,
|
||||
fileId: parsed.message_body.file_id
|
||||
}
|
||||
} else if (parsed.message_desc === 'Duplicate file upload') {
|
||||
throw 'error-duplicate-file'
|
||||
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: User not have tokens for protect') {
|
||||
throw 'user-not-have-tokens-for-protect'
|
||||
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: Failed to upload chunk: .wav file not have RIFF header') {
|
||||
throw 'error-upload-chunk-wav-file-not-have-riff'
|
||||
} else {
|
||||
throw 'error-uploading-file'
|
||||
}
|
||||
} else {
|
||||
throw 'error-uploading-file'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error) {
|
||||
const typedError = error as string;
|
||||
|
||||
return {
|
||||
errorMesage: typedError
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
errorMesage: 'error-uploading-file'
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20004,
|
||||
message_body: {
|
||||
action: 'chunks',
|
||||
upload_id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_desc === 'Upload cancelled successfully') {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllowedFilesExtensions(type: 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: 20004,
|
||||
message_body: {
|
||||
action: 'file_extension',
|
||||
file_type: type
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_desc === 'Operation successful') {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkOperationPrice(action: string, filesCount: number, fileType: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30008,
|
||||
message_body: {
|
||||
action: action,
|
||||
file_type: fileType,
|
||||
count_files: filesCount
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
cost: 0,
|
||||
success: false,
|
||||
tokens_count: 0,
|
||||
error: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function setMonitoring(fileId: string, monitoringType: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30007,
|
||||
message_body: {
|
||||
monitoring_type: monitoringType,
|
||||
file_id: fileId
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
success: true,
|
||||
errorMessage: null
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface MonitoringStats {
|
||||
total_searches: number;
|
||||
searches_by_status: {
|
||||
processing: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
};
|
||||
recent_searches: any[];
|
||||
}
|
||||
|
||||
export async function fetchMonitoringStats(): Promise<MonitoringStats | null> {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30011
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed?.message_body?.searches_by_status) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLastMonitoringCheck() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30011,
|
||||
message_body: {
|
||||
limit: 1
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body?.searches_by_status) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMonitoringInfo() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30027
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export interface NotificationBody {
|
||||
createdAt: string | null,
|
||||
id: number,
|
||||
message: string,
|
||||
status: string | null,
|
||||
type: string,
|
||||
context: Record<string, string>
|
||||
}
|
||||
|
||||
export async function fetchActiveNotifications() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'active'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllNotifications(page: number = 0, size: number = 10) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'list',
|
||||
page: page,
|
||||
size: size,
|
||||
sortBy: "createdAt",
|
||||
sortDirection: "DESC", //DESC, ASC
|
||||
types: [], //"SEARCH_RESULT", "MONITORING_RESULT"
|
||||
statuses: ["NEW", "READIED"] //"NEW"
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function notificationsMarkAsRead(ids: number[]) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'markRead',
|
||||
notificationIds: ids
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function notificationsDelete(ids: number[]) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'delete',
|
||||
notificationIds: ids
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log('parsed');
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use server'
|
||||
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
|
||||
export async function getPaymentsMethods() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/methods`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
return parsed;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePaymentsMethods(cardId: string): Promise<boolean | undefined> {
|
||||
if (!cardId) {
|
||||
return
|
||||
}
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/methods/${cardId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.message === 'Payment method deleted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL, bankCardFormSchema } from '@/app/actions/definitions';
|
||||
|
||||
export async function fetchReferralsLevels() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'levels',
|
||||
}
|
||||
}),
|
||||
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 {
|
||||
referrals: parsed.message_body,
|
||||
error: null
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
referrals: [],
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReferralUserStats() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'userStats',
|
||||
}
|
||||
}),
|
||||
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 {
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchReferralInvitees() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'invitees',
|
||||
pageSize: 100,
|
||||
pageNumber: 0
|
||||
}
|
||||
}),
|
||||
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 {
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function referralRefill() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'refill',
|
||||
amount: 2000,
|
||||
}
|
||||
}),
|
||||
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 {
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PayoutMethods {
|
||||
cardNumber: string,
|
||||
id: number,
|
||||
type: string,
|
||||
displayName: string,
|
||||
maskedDetails: string,
|
||||
default: boolean,
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export async function getPayoutMethods(): Promise<{
|
||||
methods: PayoutMethods[],
|
||||
error: string | null
|
||||
}> {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/user/payout-methods`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
return {
|
||||
methods: parsed,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
methods: [],
|
||||
error: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserPayments() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/requests`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
return {
|
||||
payments: parsed,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'error';
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
payments: [],
|
||||
error: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type PayoutMethodFormState = {
|
||||
previousState: {
|
||||
bankCard?: string | undefined;
|
||||
paymentsType?: 'bank-card' | 'bank-transfer' | 'on-account-balance';
|
||||
} | undefined,
|
||||
success?: boolean,
|
||||
error: Record<string, string | null> | null
|
||||
};
|
||||
|
||||
export async function addPayoutMethod(
|
||||
state: PayoutMethodFormState | undefined,
|
||||
formData: FormData,
|
||||
): Promise<PayoutMethodFormState> {
|
||||
const token = await getSessionData('token');
|
||||
console.log('addPayoutMethod');
|
||||
|
||||
const bankCard = formData.get('bankCard') as string || '';
|
||||
const paymentsType = formData.get('paymentsType') as 'bank-card' | 'bank-transfer' | 'on-account-balance';
|
||||
|
||||
const validatedFields = bankCardFormSchema.safeParse({
|
||||
bankCard
|
||||
});
|
||||
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
bankCard
|
||||
},
|
||||
success: false,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { bankCard } = validatedFields.data;
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/payout-method/card`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"cardNumber": bankCard.replaceAll(' ', '')
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(response);
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
bankCard,
|
||||
paymentsType
|
||||
},
|
||||
success: true,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'error-save';
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errors: Record<string, string> = {};
|
||||
errors['server'] = 'error-save'
|
||||
|
||||
if (error) {
|
||||
const typedError = error as any;
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
bankCard,
|
||||
paymentsType
|
||||
},
|
||||
success: false,
|
||||
error: errors
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type PayoutRequest = {
|
||||
previousState: {
|
||||
amount?: string | undefined;
|
||||
} | undefined,
|
||||
error: string | null
|
||||
};
|
||||
|
||||
export async function createPayoutRequest(
|
||||
state: PayoutRequest | undefined,
|
||||
formData: FormData,
|
||||
): Promise<PayoutRequest> {
|
||||
const token = await getSessionData('token');
|
||||
const amount = formData.get('amount') as string || '';
|
||||
const payoutMethodId = formData.get('payoutMethodId') as string || '';
|
||||
|
||||
console.log('createPayoutRequest');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
'amount': amount,
|
||||
'payoutMethodId': payoutMethodId
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 400) {
|
||||
const responseText = await response.text();
|
||||
throw JSON.parse(responseText)?.message;
|
||||
}
|
||||
|
||||
if (response.status === 409) {
|
||||
const responseText = await response.text();
|
||||
if (responseText.includes('Not have money for payout')) {
|
||||
throw 'insufficient-funds-to-receive-payment'
|
||||
}
|
||||
|
||||
if (responseText.includes('Not found payout method')) {
|
||||
throw 'not-found-payout-method'
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
amount
|
||||
},
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw 'payment-error';
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
|
||||
if (error) {
|
||||
let typedError: string;
|
||||
|
||||
switch (error) {
|
||||
case 'Sum of pay is required':
|
||||
typedError = 'sum-of-pay-is-required';
|
||||
break;
|
||||
case 'Min sum for pay 1000':
|
||||
typedError = 'min-sum-for-payout-1000';
|
||||
break;
|
||||
case 'insufficient-funds-to-receive-payment':
|
||||
typedError = 'insufficient-funds-to-receive-payment';
|
||||
break;
|
||||
case 'not-found-payout-method':
|
||||
typedError = 'not-found-payout-method';
|
||||
break
|
||||
default:
|
||||
typedError = 'payment-error';
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
amount
|
||||
},
|
||||
error: typedError
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
previousState: {
|
||||
amount
|
||||
},
|
||||
error: 'payment-error'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export async function searchUserFiles(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw parsed.message_desc;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (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');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20007,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
page: currentPage,
|
||||
check_similar_first: true,
|
||||
}
|
||||
}),
|
||||
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 'error';
|
||||
}
|
||||
|
||||
} 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: errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSearchStats() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/check/file_stats`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
return parsed;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'server-only'
|
||||
'use server';
|
||||
|
||||
/* import 'server-only'; */
|
||||
import { cookies } from 'next/headers'
|
||||
import { SignJWT, jwtVerify } from 'jose'
|
||||
/* import { SessionPayload } from '@/app/lib/definitions' */
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
/* const secretKey = process.env.SESSION_SECRET */
|
||||
const secretKey = 'AAA+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo='
|
||||
@@ -27,7 +29,7 @@ export async function decrypt(session: string | undefined = '') {
|
||||
}
|
||||
|
||||
export async function createSession(token: string, email: string) {
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
const expiresAt = new Date(Date.now() + 50 * 60 * 1000);
|
||||
const session = await encrypt({
|
||||
token,
|
||||
email,
|
||||
@@ -35,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,
|
||||
@@ -44,9 +46,40 @@ export async function createSession(token: string, email: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSession(newToken: string) {
|
||||
const cookieStore = await cookies();
|
||||
const session = cookieStore.get('session_nc_db')?.value;
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = await decrypt(session);
|
||||
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatedPayload = {
|
||||
...payload,
|
||||
token: newToken,
|
||||
expiresAt: new Date(Date.now() + 50 * 60 * 1000)
|
||||
};
|
||||
|
||||
const updatedSession = await encrypt(updatedPayload);
|
||||
|
||||
cookieStore.set('session_nc_db', updatedSession, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
expires: updatedPayload.expiresAt,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -63,6 +96,13 @@ export async function getSessionData(type: 'token' | 'email'): Promise<string |
|
||||
|
||||
|
||||
export async function deleteSession() {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session');
|
||||
console.log('delete session')
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session_nc_db');
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
redirect('/login');
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'server-only'
|
||||
import { cookies } from 'next/headers'
|
||||
import { decrypt } from '@/app/actions/session'
|
||||
|
||||
export async function updateSession() {
|
||||
const session = (await cookies()).get('session')?.value
|
||||
const payload = await decrypt(session)
|
||||
|
||||
if (!session || !payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
||||
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('session', session, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
expires: expires,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
'use server';
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function requestFileWithTracking(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
const headersList = await headers();
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
file_id: fileId,
|
||||
ip: ip,
|
||||
user_agent: userAgent
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
return data.message_body;
|
||||
} else {
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> {
|
||||
const token = await getSessionData('token');
|
||||
const headersList = await headers();
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
file_id: fileId,
|
||||
ip: ip,
|
||||
user_agent: userAgent
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
const fileUrl = data.message_body;
|
||||
const fileResponse = await fetch(fileUrl);
|
||||
if (!fileResponse.ok) {
|
||||
throw new Error(`Failed to fetch file: ${fileResponse.status}`);
|
||||
}
|
||||
|
||||
return await fileResponse.arrayBuffer();
|
||||
}
|
||||
else {
|
||||
return await response.arrayBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDocumentInfo(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
action: 'file_info'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function filePermisionChange(fileId: string, permissions: boolean) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
action: 'change_permission',
|
||||
permissions: {
|
||||
DOWNLOAD: permissions
|
||||
}
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchHistoryView(
|
||||
page: number,
|
||||
size: number,
|
||||
locale: string,
|
||||
sortDirection?: 'asc' | 'desc',
|
||||
sortBy?: string
|
||||
) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30028,
|
||||
message_body: {
|
||||
action: 'history_view',
|
||||
page: page,
|
||||
size: size,
|
||||
locale: locale,
|
||||
sort_direction: sortDirection,
|
||||
sort_by: sortBy
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTrackingStats() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30028,
|
||||
message_body: {
|
||||
action: 'view_stats'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadTrackingFile(fileId: string, fileName?: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/public-download/${fileId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw 'failed-to-download-file';
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const base64 = Buffer.from(arrayBuffer).toString('base64')
|
||||
|
||||
return {
|
||||
data: base64,
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
fileName: fileName ? fileName : 'fileName'
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,719 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL, createComplaintSchema, createClaimSchema } from '@/app/actions/definitions';
|
||||
|
||||
export async function getViolationSearchStatus() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/actual-task`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const text = await response.text();
|
||||
|
||||
if (text) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
|
||||
return {
|
||||
status: text === 'Task not found' ? 'task-not-found' : text
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function startGlobalMonitoring(monitoringType: 'monitoring' | 'all_files') {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/start`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
searchType: monitoringType,
|
||||
...(monitoringType === 'all_files' && { fileIds: [] })
|
||||
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed: {
|
||||
taskId: string,
|
||||
status: string,
|
||||
totalFiles: number
|
||||
} = await response.json();
|
||||
|
||||
if (parsed?.status === 'ACCEPTED') {
|
||||
return parsed
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getViolationFilesArray(props: {
|
||||
page?: number,
|
||||
size?: number,
|
||||
start_date?: string,
|
||||
end_date?: string,
|
||||
file_name?: string
|
||||
}) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, start_date, end_date, file_name } = props;
|
||||
|
||||
try {
|
||||
const body: Record<string, any> = {};
|
||||
|
||||
if (page !== undefined) body.page = page;
|
||||
if (size !== undefined) body.size = size;
|
||||
if (start_date !== undefined) body.start_date = start_date;
|
||||
if (end_date !== undefined) body.end_date = end_date;
|
||||
if (file_name !== undefined) body.file_name = file_name;
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/violation-summary-with-files`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.content.length) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileMatches(fileId: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30009,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
page: (page > 0 ? page - 1 : 0),
|
||||
size: 5,
|
||||
sort_direction: 'desc',
|
||||
status: status,
|
||||
...(violationId && { action: 'getByViolationId' }),
|
||||
...(violationId && { violation_id: violationId }),
|
||||
...((startDate && endDate) && { start_date: startDate, end_date: endDate })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_body) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationAnalyticStatistic(group: 'domain' | 'tld') {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30009,
|
||||
message_body: {
|
||||
group_by: group,
|
||||
action: "group",
|
||||
sort_direction: 'desc'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed?.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationStatistic(id?: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30010,
|
||||
message_body: {
|
||||
...(id && { file_id: id })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed?.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationFinalSummaryStatistic() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30023
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface complainBody {
|
||||
previousText: string,
|
||||
violationID: string | null,
|
||||
success: boolean,
|
||||
errorMessage?: Record<string, string> | null
|
||||
}
|
||||
|
||||
export async function createComplaint(
|
||||
state: complainBody | undefined,
|
||||
formData: FormData
|
||||
): Promise<complainBody> {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
const violationId = formData.get('violationId') as string || '';
|
||||
const textArea = formData.get('note') as string || '';
|
||||
|
||||
const validatedFields = createComplaintSchema.safeParse({
|
||||
textArea
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: 'create',
|
||||
violation_id: violationId,
|
||||
text: textArea,
|
||||
email: email
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: true
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (error && typeof error === 'object' && 'message_code' in error) {
|
||||
const err = error as { message_code: number };
|
||||
if (err.message_code === 2) {
|
||||
errors['server'] = 'the-complaint-has-not-been-registered'
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
errors['server'] = 'error-save';
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchComplainInfo(id: number) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: 'get_by_violation',
|
||||
violation_id: id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
body: parsed?.message_body
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
errorMessage: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MatchStatus = 'NEW' | 'SHOWED' | 'LEGAL_IN_WORK' | 'COMPLAINT_IN_WORK' | 'COMPLAINT_AND_LEGAL_IN_WORK' | 'AUTHORIZED_USE' | 'NOT_OWNER_FILE';
|
||||
|
||||
export async function updateMatchStatus(id: number, status: MatchStatus) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30009,
|
||||
message_body: {
|
||||
action: 'updateStatus',
|
||||
violation_id: id,
|
||||
status: status
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body.status) {
|
||||
return true
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchClaimInfo(id: number) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: 'get_all',
|
||||
pageSize: 1,
|
||||
pageNumber: 0,
|
||||
sortBy: "createdAt",
|
||||
sortDir: "desc",
|
||||
violation_id: id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
body: parsed?.message_body
|
||||
}
|
||||
} else if (parsed?.message_code !== 0 && parsed?.message_desc === 'Law case not found') {
|
||||
return {
|
||||
body: null
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
errorMessage: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface caseBody {
|
||||
previousText: string,
|
||||
previousCaseTitle: string,
|
||||
previousAmount: string,
|
||||
violationID: string | null,
|
||||
success: boolean,
|
||||
errorMessage?: Record<string, string> | null
|
||||
}
|
||||
|
||||
export async function createClaim(
|
||||
state: caseBody | undefined,
|
||||
formData: FormData
|
||||
): Promise<caseBody> {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
const violationId = formData.get('violationId') as string || '';
|
||||
const textArea = formData.get('note') as string || '';
|
||||
const caseTitle = formData.get('caseTitle') as string || '';
|
||||
const amount = formData.get('amount') as string || '';
|
||||
|
||||
const validatedFields = createClaimSchema.safeParse({
|
||||
textArea,
|
||||
caseTitle
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: 'create',
|
||||
violation_id: violationId,
|
||||
description: textArea,
|
||||
email: email,
|
||||
name: caseTitle,
|
||||
priority: 'HIGH',
|
||||
amount: Number(amount)
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousText: textArea,
|
||||
previousAmount: amount,
|
||||
success: true
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (error && typeof error === 'object' && 'message_code' in error) {
|
||||
const err = error as { message_code: number };
|
||||
if (err.message_code === 2) {
|
||||
errors['server'] = 'the-complaint-has-not-been-registered'
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
errors['server'] = 'error-save';
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComplaintsCaseQueryParams {
|
||||
page?: number,
|
||||
size?: number,
|
||||
sort_by?: string,
|
||||
sort_direction?: 'desc' | 'asc'
|
||||
}
|
||||
|
||||
export async function fetchLastClaims(params: ComplaintsCaseQueryParams) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, sort_by, sort_direction } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: "get_all",
|
||||
pageNumber: page,
|
||||
size,
|
||||
sort_by,
|
||||
sort_direction
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLastComplaint(params: ComplaintsCaseQueryParams) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, sort_by, sort_direction } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: "get_all",
|
||||
page,
|
||||
size,
|
||||
sort_by,
|
||||
sort_direction
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
'use server'
|
||||
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { createSession } from '@/app/actions/session';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
|
||||
export async function vkAuthorization(data : any, codeVerifier: string) {
|
||||
|
||||
console.log(data);
|
||||
console.log(codeVerifier);
|
||||
const { code, state, device_id } = data
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/vk/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log(parsed);
|
||||
if (parsed.message_desc === 'Operation successful') {
|
||||
/* await createSession(parsed.message_body.token, email); */
|
||||
} else {
|
||||
throw (`${parsed.message_code}`);
|
||||
}
|
||||
} else {
|
||||
throw ('request-ended-with-an-error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
switch (error) {
|
||||
case '2':
|
||||
errors['server'] = 'password-does-not-match'
|
||||
break;
|
||||
|
||||
case '4':
|
||||
errors['server'] = 'email-not-found'
|
||||
break;
|
||||
|
||||
default:
|
||||
errors['server'] = 'request-ended-with-an-error'
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use server'
|
||||
|
||||
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);
|
||||
console.log(codeVerifier);
|
||||
const { code, state, device_id } = data
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/yandex/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log(parsed);
|
||||
if (parsed.message_desc === 'Operation successful') {
|
||||
/* await createSession(parsed.message_body.token, email); */
|
||||
} else {
|
||||
throw (`${parsed.message_code}`);
|
||||
}
|
||||
} else {
|
||||
throw ('request-ended-with-an-error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
switch (error) {
|
||||
case '2':
|
||||
errors['server'] = 'password-does-not-match'
|
||||
break;
|
||||
|
||||
case '4':
|
||||
errors['server'] = 'email-not-found'
|
||||
break;
|
||||
|
||||
default:
|
||||
errors['server'] = 'request-ended-with-an-error'
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
redirect('/pages/dashboard');
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use server'
|
||||
|
||||
import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout';
|
||||
import { getSessionData, deleteSession } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4' });
|
||||
|
||||
export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string, operationType: 'TARIFF' | 'TOKEN') {
|
||||
const formData = new FormData();
|
||||
const token = await getSessionData('token');
|
||||
|
||||
formData.append('email', email);
|
||||
formData.append('tariffId', tariffId.toString());
|
||||
formData.append('operationType', operationType);
|
||||
formData.append('operationUuid', operationUuid);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/create`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.paymentUuid) {
|
||||
return true
|
||||
} else {
|
||||
throw 'error'
|
||||
}
|
||||
} else {
|
||||
throw 'error'
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function yooKasaCreatePayment(amount: number, tariff: number, operationType: 'TARIFF' | 'TOKEN') {
|
||||
const userEmail = await getSessionData('email');
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 10);
|
||||
const idempotenceKey = `${userEmail}-${timestamp}-${amount}-${tariff}-${random}`;
|
||||
|
||||
if (!userEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const createPayload: ICreatePayment = {
|
||||
amount: {
|
||||
value: amount.toLocaleString().replace(/[,\s.]/g, ''),
|
||||
currency: 'RUB'
|
||||
},
|
||||
confirmation: {
|
||||
type: 'embedded',
|
||||
},
|
||||
metadata: {
|
||||
userMail: userEmail,
|
||||
tariff: tariff
|
||||
},
|
||||
merchant_customer_id: userEmail,
|
||||
capture: true,
|
||||
save_payment_method: operationType === 'TOKEN' ? false : true,
|
||||
/* ...(operationType === 'TOKEN' && { save_payment_method: false }) */
|
||||
};
|
||||
|
||||
try {
|
||||
const payment = await checkout.createPayment(createPayload, idempotenceKey);
|
||||
const response = await makePaymentOperation(userEmail, tariff, payment.id, operationType);
|
||||
|
||||
if (payment && response) {
|
||||
return payment?.confirmation.confirmation_token;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchPaymentsHistory() {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30005,
|
||||
message_body: {
|
||||
action: 'user_payments',
|
||||
email: email
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
payments: parsed.message_body.payments,
|
||||
error: null
|
||||
};
|
||||
} else {
|
||||
throw parsed.message_code;
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// components/ActivityTracker.tsx
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { tokenLifeExtension } from '@/app/actions/auth';
|
||||
|
||||
export default function ActivityTracker() {
|
||||
const lastRefreshTime = useRef(Date.now());
|
||||
const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
|
||||
const isRefreshing = useRef(false);
|
||||
|
||||
// Debounce функция
|
||||
const debounce = useCallback((func: () => void, delay: number) => {
|
||||
if (activityTimeoutRef.current) {
|
||||
clearTimeout(activityTimeoutRef.current);
|
||||
}
|
||||
|
||||
activityTimeoutRef.current = setTimeout(func, delay);
|
||||
}, []);
|
||||
|
||||
// Ваша функция обновления токена (адаптированная для клиента)
|
||||
const refreshTokenSilently = useCallback(async () => {
|
||||
if (isRefreshing.current) return false;
|
||||
|
||||
isRefreshing.current = true;
|
||||
|
||||
try {
|
||||
let response = tokenLifeExtension();
|
||||
|
||||
//@ts-ignore
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Silent token refresh failed:', error);
|
||||
return false;
|
||||
} finally {
|
||||
isRefreshing.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Проверка необходимости обновления
|
||||
const checkAndRefresh = useCallback(async () => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastRefresh = now - lastRefreshTime.current;
|
||||
|
||||
// Обновляем не чаще чем раз в 10 минут
|
||||
// и только если прошло больше 5 минут с последнего обновления
|
||||
if (timeSinceLastRefresh > 5 * 60 * 1000 && timeSinceLastRefresh > 30000) {
|
||||
const refreshed = await refreshTokenSilently();
|
||||
if (refreshed) {
|
||||
lastRefreshTime.current = now;
|
||||
console.log('Token silently refreshed at', new Date().toISOString());
|
||||
}
|
||||
}
|
||||
}, [refreshTokenSilently]);
|
||||
|
||||
// Обработчик активности
|
||||
const handleActivity = useCallback(() => {
|
||||
// Используем debounce чтобы не спамить запросами
|
||||
// Ждем 30 секунд после последнего действия пользователя
|
||||
debounce(checkAndRefresh, 30000);
|
||||
}, [debounce, checkAndRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
'mousedown', 'mousemove', 'wheel', 'keydown',
|
||||
'touchstart', 'touchend', 'scroll', 'click',
|
||||
'focus', 'blur', 'input', 'change'
|
||||
];
|
||||
|
||||
// Добавляем обработчики с passive: true для производительности
|
||||
events.forEach(event => {
|
||||
document.addEventListener(event, handleActivity, { passive: true });
|
||||
});
|
||||
|
||||
// Также обновляем при возвращении на вкладку
|
||||
const handleVisibilityChange = () => {
|
||||
if (!document.hidden) {
|
||||
handleActivity();
|
||||
// Немедленно проверяем при возвращении на вкладку
|
||||
setTimeout(checkAndRefresh, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Периодическая проверка (каждые 2 минуты)
|
||||
const intervalId = setInterval(checkAndRefresh, 2 * 60 * 1000);
|
||||
|
||||
// Инициализация - проверяем сразу если нужно
|
||||
const initialCheck = setTimeout(checkAndRefresh, 10000);
|
||||
|
||||
return () => {
|
||||
// Очистка
|
||||
events.forEach(event => {
|
||||
document.removeEventListener(event, handleActivity);
|
||||
});
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
clearInterval(intervalId);
|
||||
clearTimeout(initialCheck);
|
||||
if (activityTimeoutRef.current) {
|
||||
clearTimeout(activityTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [handleActivity, checkAndRefresh]);
|
||||
|
||||
return null; // Не рендерит ничего
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { ReactNode, useState, useRef } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
|
||||
interface DropDownListProps {
|
||||
children: ReactNode;
|
||||
value: string | number;
|
||||
callBack: (value: string) => void;
|
||||
translatedValue?: string;
|
||||
}
|
||||
|
||||
interface ChildProps {
|
||||
value?: string;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DropDownList({ children, value, callBack, translatedValue }: DropDownListProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
useClickOutside(dropdownRef,
|
||||
() => {
|
||||
setIsOpen(false);
|
||||
},
|
||||
isOpen
|
||||
);
|
||||
|
||||
const enhancedChildren = React.Children.map(children, (child, index) => {
|
||||
if (React.isValidElement<ChildProps>(child)) {
|
||||
const childValue = child.props.value || undefined;
|
||||
|
||||
return React.cloneElement<ChildProps>(child, {
|
||||
onClick: () => {
|
||||
callBack(childValue || "");
|
||||
setIsOpen(false);
|
||||
},
|
||||
className: `dropdown-item ${value === childValue ? "current" : ""}`,
|
||||
key: index,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dropdown-wrapper" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
className="dropdown-button"
|
||||
type="button"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">
|
||||
{translatedValue ? translatedValue : value}
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="dropdown-list">
|
||||
<ul
|
||||
className="py-1"
|
||||
role="listbox"
|
||||
aria-labelledby="language-selector"
|
||||
>
|
||||
{enhancedChildren}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { ReactNode, useState, useRef } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { SelectedFile } from './UploadSectionFile';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface DropDownItem {
|
||||
file: SelectedFile;
|
||||
convertTo: string;
|
||||
}
|
||||
|
||||
interface DropDownListProps {
|
||||
children: ReactNode;
|
||||
value: string | null | undefined;
|
||||
callBack: (value: DropDownItem) => void;
|
||||
translatedValue?: string | null | undefined;
|
||||
selectedFiles: SelectedFile[];
|
||||
}
|
||||
|
||||
interface LiElementProps extends React.LiHTMLAttributes<HTMLLIElement> {
|
||||
'data-file'?: string;
|
||||
'data-convert-to'?: string;
|
||||
}
|
||||
|
||||
export default function DropDownListForImageConvert({
|
||||
children,
|
||||
value,
|
||||
callBack,
|
||||
translatedValue,
|
||||
selectedFiles
|
||||
}: DropDownListProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const t = useTranslations('Global')
|
||||
|
||||
useClickOutside(dropdownRef,
|
||||
() => {
|
||||
setIsOpen(false);
|
||||
},
|
||||
isOpen
|
||||
);
|
||||
|
||||
const getDisplayValue = (): string => {
|
||||
if (translatedValue) return translatedValue;
|
||||
return value ? value : t('do-not-convert');
|
||||
};
|
||||
|
||||
const findFileByName = (fileName: string): SelectedFile | undefined => {
|
||||
return selectedFiles.find(file => file.name === fileName);
|
||||
};
|
||||
|
||||
const enhancedChildren = React.Children.map(children, (child, index) => {
|
||||
if (React.isValidElement<LiElementProps>(child)) {
|
||||
const fileName = child.props['data-file'];
|
||||
const convertTo = child.props['data-convert-to'];
|
||||
|
||||
const isCurrent = value === convertTo;
|
||||
|
||||
return React.cloneElement(child, {
|
||||
onClick: () => {
|
||||
if (fileName && convertTo) {
|
||||
const file = findFileByName(fileName);
|
||||
if (file) {
|
||||
callBack({ file, convertTo });
|
||||
} else {
|
||||
console.error(`Файл с именем ${fileName} не найден`);
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
className: `dropdown-item ${isCurrent ? "current" : ""}`,
|
||||
key: index,
|
||||
});
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="dropdown-wrapper" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="dropdown-button"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">
|
||||
{getDisplayValue()}
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="dropdown-list">
|
||||
<ul
|
||||
className=""
|
||||
role="listbox"
|
||||
>
|
||||
{enhancedChildren}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
|
||||
|
||||
export const FileTypeIcon = ({ type }: { type: string }) => {
|
||||
switch (type) {
|
||||
case 'image':
|
||||
return <span className="color-image">
|
||||
<IconImageFile />
|
||||
</span>;
|
||||
case 'video':
|
||||
return <span className="color-video">
|
||||
<IconVideoFile />
|
||||
</span>;
|
||||
case 'audio':
|
||||
return <span className="color-audio">
|
||||
<IconAudioFile />
|
||||
</span>;
|
||||
default:
|
||||
return <span className="color-document">
|
||||
<IconDocument />
|
||||
</span>;
|
||||
}
|
||||
};
|
||||
@@ -7,20 +7,50 @@ import { useState, useRef, useEffect } from 'react'
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
|
||||
export default function LanguageSwitcher() {
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const switchLanguage = (newLocale: string) => {
|
||||
const currentLang = document.documentElement.getAttribute('lang') || 'ru';
|
||||
router.replace(pathname, { locale: newLocale });
|
||||
setIsOpen(false);
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
const newLang = document.documentElement.getAttribute('lang')
|
||||
|
||||
if (newLang && newLang !== currentLang) {
|
||||
observer.disconnect();
|
||||
|
||||
if (window.location.protocol === 'http:' && process.env.NODE_ENV !== 'development') {
|
||||
setTimeout(() => window.location.reload(), 100)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (window.location.protocol === 'http:' && process.env.NODE_ENV !== 'development') {
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['lang', 'dir']
|
||||
})
|
||||
}
|
||||
|
||||
router.replace(pathname, { locale: newLocale })
|
||||
setIsOpen(false)
|
||||
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
useClickOutside(dropdownRef, () => {
|
||||
setIsOpen(false);
|
||||
});
|
||||
useClickOutside(
|
||||
dropdownRef,
|
||||
() => {
|
||||
setIsOpen(false);
|
||||
},
|
||||
isOpen
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
@@ -32,10 +62,10 @@ export default function LanguageSwitcher() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<div className="language-switcher" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center justify-between w-full px-4 py-2 text-sm font-medium text-[#6366f1] bg-white border-2 border-[#6366f1] rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
className="switcher-btn"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">{locale === 'ru' ? 'РУ' : locale.toUpperCase()}</span>
|
||||
@@ -55,7 +85,7 @@ export default function LanguageSwitcher() {
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 z-10 w-full mt-2 origin-top-right bg-white rounded-md shadow-lg border-2 border-[#6366f1] focus:outline-none">
|
||||
<div className="switcher-list">
|
||||
<ul
|
||||
className="py-1"
|
||||
role="listbox"
|
||||
@@ -65,7 +95,7 @@ export default function LanguageSwitcher() {
|
||||
<li key={lang} role="option">
|
||||
<button
|
||||
onClick={() => switchLanguage(lang)}
|
||||
className={`flex items-center w-full px-4 py-2 text-sm text-left hover:bg-blue-100 ${locale === lang ? 'bg-blue-50 text-blue-600 font-medium' : 'text-[#6366f1]'
|
||||
className={`language-btn ${locale === lang ? 'active' : ''
|
||||
}`}
|
||||
role="menuitem"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
interface ModalWindowtProps {
|
||||
children: ReactNode | null;
|
||||
state: boolean;
|
||||
callBack: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export default function ModalWindow({ children, state, callBack }: ModalWindowtProps) {
|
||||
return (
|
||||
<>
|
||||
{state && (
|
||||
<div
|
||||
className="modal-wrapper"
|
||||
onClick={() => {
|
||||
callBack(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal-window"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface NotificationTextHandlerProps {
|
||||
notificationType: string
|
||||
notificationMessage: string
|
||||
notificationContext: Record<string, string>
|
||||
}
|
||||
|
||||
export function NotificationTextHandler({
|
||||
notificationType,
|
||||
notificationMessage,
|
||||
notificationContext
|
||||
}: NotificationTextHandlerProps) {
|
||||
const t = useTranslations('Notifications');
|
||||
|
||||
const getNotificationText = () => {
|
||||
if (!notificationType) {
|
||||
return t.has(notificationMessage) ? t(notificationMessage) : notificationMessage
|
||||
}
|
||||
|
||||
switch (notificationType) {
|
||||
case 'USER_VERIFIED':
|
||||
return t('user-verified', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
updateDate: notificationContext?.update_date || t('not-specified-date')
|
||||
});
|
||||
|
||||
case 'MONITORING_RESULT':
|
||||
return t('monitoring-result', {
|
||||
message: notificationContext?.message || t('message-missing')
|
||||
});
|
||||
|
||||
case 'COMPLAINT_STATUS_CHANGED':
|
||||
return t('complaint-status-changed', {
|
||||
newStatus: notificationContext?.new_status || t('not-specified'),
|
||||
oldStatus: notificationContext?.old_status || t('not-specified'),
|
||||
violation: notificationContext?.violation || t('not-specified'),
|
||||
file: notificationContext?.file || t('not-specified')
|
||||
});
|
||||
|
||||
case 'FILE_ADDED_TO_SYSTEM':
|
||||
return t('file-added-to-system', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
fileName: notificationContext?.file_name || t('not-specified-file-name')
|
||||
});
|
||||
|
||||
case 'FILE_MODERATION_EVENT':
|
||||
return t('file-moderation-event', {
|
||||
fileName: notificationContext?.file_name || t('not-specified-file-name'),
|
||||
oldStatus: notificationContext?.old_status || t('not-specified'),
|
||||
newStatus: notificationContext?.new_status || t('not-specified'),
|
||||
comment: notificationContext?.comment || t('no-comment')
|
||||
});
|
||||
|
||||
case 'PAYMENT_RESULT':
|
||||
return t('payment-result', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
amount: notificationContext?.amount || '0'
|
||||
});
|
||||
|
||||
case 'REFERRAL_REGISTERED':
|
||||
return t('referral-registered', {
|
||||
referralUser: notificationContext?.referral_user || t('unknown-user')
|
||||
});
|
||||
|
||||
case 'SEARCH_RESULT':
|
||||
return t('search-result', {
|
||||
startDate: notificationContext?.start_date || t('not-specified-date'),
|
||||
endDate: notificationContext?.end_date || t('not-specified-date'),
|
||||
searchType: notificationContext?.search_type || t('not-specified'),
|
||||
count: notificationContext?.count || '0'
|
||||
});
|
||||
|
||||
case 'SEARCH_OPERATION_FAILED':
|
||||
return t('search-operation-failed', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
errorMessage: notificationContext?.error_message || t('error-not-described'),
|
||||
message: notificationContext?.message || t('message-missing'),
|
||||
reason: notificationContext?.reason || t('reason-not-specified'),
|
||||
nextRun: notificationContext?.next_run || t('not-scheduled')
|
||||
});
|
||||
|
||||
case 'TOKEN_NOT_FOUND':
|
||||
return t('token-not-found', {
|
||||
currentTokens: notificationContext?.current_tokens || '0',
|
||||
needTokens: notificationContext?.need_tokens || t('not-specified'),
|
||||
message: notificationContext?.message || t('message-missing'),
|
||||
nextRun: notificationContext?.next_run || t('not-scheduled')
|
||||
});
|
||||
|
||||
default:
|
||||
return notificationMessage || t('notification-without-text');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{getNotificationText()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client'
|
||||
|
||||
export function Pagination({ totalPages, currentPage, fileId, callBack }: any) {
|
||||
|
||||
if (totalPages === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getVisiblePages = () => {
|
||||
const pages = [];
|
||||
const maxVisible = 7;
|
||||
let start = Math.max(1, currentPage - 3);
|
||||
let end = Math.min(totalPages, currentPage + 3);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
if (start === 1) {
|
||||
end = Math.min(totalPages, start + maxVisible - 1);
|
||||
} else if (end === totalPages) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const visiblePages = getVisiblePages();
|
||||
|
||||
return (
|
||||
<div className="pagination-wrapper">
|
||||
{visiblePages[0] > 1 && (
|
||||
<>
|
||||
<button
|
||||
className={`pagination-button`}
|
||||
onClick={() => callBack(fileId, 1, true)}
|
||||
>
|
||||
1
|
||||
</button>
|
||||
{visiblePages[0] > 2 && <span>...</span>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{visiblePages.map(page => (
|
||||
<button
|
||||
key={page}
|
||||
onClick={() => callBack(fileId, page, true)}
|
||||
className={`pagination-button ${page === currentPage ? 'current-page' : ''}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
|
||||
{visiblePages[visiblePages.length - 1] < totalPages && (
|
||||
<>
|
||||
{visiblePages[visiblePages.length - 1] < totalPages - 1 && <span>...</span>}
|
||||
<button
|
||||
className={`pagination-button`}
|
||||
onClick={() => callBack(fileId, totalPages, true)}
|
||||
>
|
||||
{totalPages}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { usePdf } from '@/app/contexts/PdfContext';
|
||||
import { requestFileAsBuffer } from '@/app/actions/trackingActions';
|
||||
|
||||
interface PdfViewerProps {
|
||||
fileId: string;
|
||||
fileName?: string;
|
||||
showDownload?: boolean;
|
||||
}
|
||||
|
||||
export default function PdfViewer({ fileId, fileName = 'document.pdf', showDownload }: PdfViewerProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const pdfDocumentRef = useRef<any>(null);
|
||||
const { setPdfData } = usePdf();
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let objectUrl: string | null = null;
|
||||
|
||||
const loadAndDisplayPdf = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const pdfData = await requestFileAsBuffer(fileId);
|
||||
|
||||
if (!pdfData) {
|
||||
throw new Error('Failed to load document');
|
||||
}
|
||||
|
||||
//это нужно для скачивания так-как иначе pdfData теряется для скачивания
|
||||
if (showDownload) {
|
||||
const pdfDataForDownload = pdfData.slice(0);
|
||||
setPdfData(pdfDataForDownload, fileName);
|
||||
}
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: pdfData,
|
||||
useSystemFonts: true,
|
||||
disableFontFace: false,
|
||||
});
|
||||
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
pdfDocumentRef.current = pdfDocument;
|
||||
setTotalPages(pdfDocument.numPages);
|
||||
setCurrentPage(1);
|
||||
|
||||
await renderPage(pdfDocument, 1);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading PDF:', err);
|
||||
if (isMounted) {
|
||||
setError('Не удалось загрузить документ');
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAndDisplayPdf();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (pdfDocumentRef.current) {
|
||||
pdfDocumentRef.current.destroy();
|
||||
}
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [fileId, setPdfData]);
|
||||
|
||||
const renderPage = async (pdfDocument: any, pageNum: number) => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
try {
|
||||
const page = await pdfDocument.getPage(pageNum);
|
||||
|
||||
const container = canvasRef.current.parentElement;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const containerWidth = container.clientWidth || 800;
|
||||
|
||||
// Если ширина контейнера 0, откладываем рендер
|
||||
if (containerWidth === 0) {
|
||||
console.log('Container width is 0, waiting...');
|
||||
setTimeout(() => renderPage(pdfDocument, pageNum), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const originalViewport = page.getViewport({ scale: 1 });
|
||||
const scale = containerWidth / originalViewport.width;
|
||||
const viewport = page.getViewport({ scale: Math.min(scale, 1.5) });
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
} catch (err) {
|
||||
console.log('Error rendering page:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!pdfDocumentRef.current || loading) return;
|
||||
|
||||
const checkContainer = () => {
|
||||
const container = canvasRef.current?.parentElement;
|
||||
if (container && container.clientWidth > 0) {
|
||||
renderPage(pdfDocumentRef.current, currentPage);
|
||||
} else {
|
||||
setTimeout(checkContainer, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkContainer();
|
||||
}, [pdfDocumentRef.current, loading]);
|
||||
|
||||
const goToPrevPage = async () => {
|
||||
if (pdfDocumentRef.current && currentPage > 1) {
|
||||
const newPage = currentPage - 1;
|
||||
setCurrentPage(newPage);
|
||||
await renderPage(pdfDocumentRef.current, newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const goToNextPage = async () => {
|
||||
if (pdfDocumentRef.current && currentPage < totalPages) {
|
||||
const newPage = currentPage + 1;
|
||||
setCurrentPage(newPage);
|
||||
await renderPage(pdfDocumentRef.current, newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
containerRef.current.requestFullscreen();
|
||||
setFullscreen(true);
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
setFullscreen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setFullscreen(!!document.fullscreenElement);
|
||||
if (pdfDocumentRef.current && !document.fullscreenElement) {
|
||||
setTimeout(() => renderPage(pdfDocumentRef.current, currentPage), 100);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||
}, [currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (pdfDocumentRef.current && !loading) {
|
||||
renderPage(pdfDocumentRef.current, currentPage);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [currentPage, loading]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="prev-wrap">
|
||||
<div className="pdf-loading">Загрузка документа...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="prev-wrap">
|
||||
<div className="pdf-error">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="pdf-container">
|
||||
<div className="prev-wrap">
|
||||
<canvas ref={canvasRef} className="pdf-canvas" />
|
||||
</div>
|
||||
{totalPages > 0 && (
|
||||
<div className="document-controls">
|
||||
<div className="document-controls__nav">
|
||||
<button
|
||||
className="document-controls__prev"
|
||||
onClick={goToPrevPage}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
← Пред.
|
||||
</button>
|
||||
<span className="document-controls__current-page">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
className="document-controls__next"
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
След. →
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="document-controls__fullscreen"
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
{fullscreen ? 'Выйти' : 'На весь экран'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
|
||||
|
||||
export const PieChartComponent = ({ data }: {
|
||||
export const PieChartComponent = ({ data, show }: {
|
||||
data: {
|
||||
name: string,
|
||||
value: number,
|
||||
color: string
|
||||
}[]
|
||||
}[],
|
||||
show: number | string
|
||||
}) => {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
@@ -16,33 +19,34 @@ export const PieChartComponent = ({ data }: {
|
||||
>
|
||||
<ResponsiveContainer width={200} height={200}>
|
||||
<PieChart>
|
||||
<text x={100} y={80} className="pie-char-text">
|
||||
0
|
||||
<text x={100} y={75} className="pie-char-text">
|
||||
{show}
|
||||
</text>
|
||||
<text x={100} y={100} className="pie-char-text">
|
||||
{t('out-of')}
|
||||
</text>
|
||||
<text x={100} y={120} className="pie-char-text">
|
||||
<text x={100} y={125} className="pie-char-text">
|
||||
0
|
||||
</text>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={70}
|
||||
outerRadius={90}
|
||||
innerRadius={65}
|
||||
outerRadius={100}
|
||||
paddingAngle={0}
|
||||
dataKey="value"
|
||||
stroke="none"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
<Cell key={`cell-${index}`} fill={entry.color} stroke="none" />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex gap-6">
|
||||
<div className="flex gap-6 mt-4">
|
||||
{data.map((entry, index) => (
|
||||
<div key={`cell-${index}`} className="text-white">
|
||||
<div key={`cell-${index}`} className="">
|
||||
<span className="w-2 h-2 rounded-full font-bold inline-block mr-1.5" style={{ backgroundColor: entry.color }}></span>
|
||||
<span>{t(entry.name)}</span>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { useMemo } from 'react';
|
||||
import { BarChart, Bar, Cell, XAxis, Tooltip } from 'recharts';
|
||||
import { pluralize } from '@/app/lib/pluralize';
|
||||
import { useStoreWithDevtools } from '@/app/stores/useStoreWithDevtools';
|
||||
|
||||
type Files = {
|
||||
data: {
|
||||
name: string,
|
||||
checks: number,
|
||||
violations: number
|
||||
}[]
|
||||
}
|
||||
|
||||
interface BarShapeProps {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
fill: string;
|
||||
radius?: number[];
|
||||
}
|
||||
|
||||
export const StackedBarChart = ({ data }: Files) => {
|
||||
const locale = useLocale();
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const CHECKS_COLOR = '#6366f1';
|
||||
const VIOLATIONS_COLOR = '#f08c00'
|
||||
const BORDER_RADIUS = 5;
|
||||
const STROKE_COLOR = "#dfdede24";
|
||||
const STROKE_WIDTH = 1;
|
||||
|
||||
/* const { totalChecks, totalViolations } = useMemo(() => ({
|
||||
totalChecks: data.reduce((sum, item) => sum + (item.checks || 0), 0),
|
||||
totalViolations: data.reduce((sum, item) => sum + (item.violations || 0), 0)
|
||||
}), [data]); */
|
||||
|
||||
const totalChecks = useStoreWithDevtools(s => s.value1);
|
||||
const totalViolations = useStoreWithDevtools(s => s.value2);
|
||||
|
||||
const renderBarWithPartialBorder = (props: BarShapeProps) => {
|
||||
const { x, y, width, height, fill } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Основной прямоугольник с закругленными углами */}
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
rx={BORDER_RADIUS}
|
||||
ry={BORDER_RADIUS}
|
||||
/>
|
||||
|
||||
{/* Левая граница (центральная часть, без закруглений) */}
|
||||
<line
|
||||
x1={x}
|
||||
y1={y + BORDER_RADIUS}
|
||||
x2={x}
|
||||
y2={y + height - BORDER_RADIUS}
|
||||
/>
|
||||
|
||||
{/* Правая граница (центральная часть, без закруглений) */}
|
||||
<line
|
||||
x1={x + width}
|
||||
y1={y + BORDER_RADIUS}
|
||||
x2={x + width}
|
||||
y2={y + height - BORDER_RADIUS}
|
||||
/>
|
||||
|
||||
{/* Нижняя граница (прямая часть между закруглениями) */}
|
||||
<line
|
||||
x1={x + BORDER_RADIUS}
|
||||
y1={y + height}
|
||||
x2={x + width - BORDER_RADIUS}
|
||||
y2={y + height}
|
||||
/>
|
||||
|
||||
{/* Верхняя граница (прямая часть между закруглениями) */}
|
||||
<line
|
||||
x1={x + BORDER_RADIUS}
|
||||
y1={y}
|
||||
x2={x + width - BORDER_RADIUS}
|
||||
y2={y}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBarWithTopBorder = (props: BarShapeProps) => {
|
||||
const { x, y, width, height, fill } = props;
|
||||
|
||||
if (height <= 0 || width <= 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Основной прямоугольник с закругленными углами */}
|
||||
<rect
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={fill}
|
||||
rx={BORDER_RADIUS}
|
||||
ry={BORDER_RADIUS}
|
||||
/>
|
||||
|
||||
{/* Левая граница (центральная часть, без закруглений) */}
|
||||
<line
|
||||
x1={x}
|
||||
y1={y + BORDER_RADIUS}
|
||||
x2={x}
|
||||
y2={y + height - BORDER_RADIUS}
|
||||
stroke={STROKE_COLOR}
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
|
||||
{/* Правая граница (центральная часть, без закруглений) */}
|
||||
<line
|
||||
x1={x + width}
|
||||
y1={y + BORDER_RADIUS}
|
||||
x2={x + width}
|
||||
y2={y + height - BORDER_RADIUS}
|
||||
stroke={STROKE_COLOR}
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
|
||||
{/* Нижняя граница (прямая часть между закруглениями) */}
|
||||
<line
|
||||
x1={x + BORDER_RADIUS}
|
||||
y1={y + height}
|
||||
x2={x + width - BORDER_RADIUS}
|
||||
y2={y + height}
|
||||
stroke={STROKE_COLOR}
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
|
||||
{/* Верхняя граница (прямая часть между закруглениями) */}
|
||||
<line
|
||||
x1={x + BORDER_RADIUS}
|
||||
y1={y}
|
||||
x2={x + width - BORDER_RADIUS}
|
||||
y2={y}
|
||||
stroke={STROKE_COLOR}
|
||||
strokeWidth={STROKE_WIDTH}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const pluralizeCheks = (number: number) => {
|
||||
const translate = [t('check'), t('checks-few'), t('checks')];
|
||||
return pluralize(number, translate[0], translate[1], translate[2], locale);
|
||||
};
|
||||
|
||||
const pluralizeViolations = (number: number) => {
|
||||
const translate = [t('violation'), t('violations-few'), t('violations')];
|
||||
return pluralize(number, translate[0], translate[1], translate[2], locale);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="stacked-bar-chart-wrapper">
|
||||
<div className="stacked-bar-chart">
|
||||
<BarChart
|
||||
style={{ width: '100%', maxWidth: '600px', maxHeight: '210px', aspectRatio: 1.618 }}
|
||||
responsive
|
||||
data={data}
|
||||
barCategoryGap={15}
|
||||
margin={{
|
||||
top: 20,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
{/* <Tooltip /> */}
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke={CHECKS_COLOR}
|
||||
fontSize={12}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="checks"
|
||||
stackId="a"
|
||||
label={{ position: 'top', fill: CHECKS_COLOR, fontSize: 12 }}
|
||||
radius={5}
|
||||
shape={renderBarWithPartialBorder as any}
|
||||
>
|
||||
{data.map((_entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={CHECKS_COLOR} />
|
||||
))}
|
||||
</Bar>
|
||||
<Bar
|
||||
dataKey="violations"
|
||||
stackId="b"
|
||||
label={{ position: 'top', fill: CHECKS_COLOR, fontSize: 12 }}
|
||||
radius={5}
|
||||
shape={renderBarWithTopBorder as any}
|
||||
>
|
||||
{data.map((_entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={VIOLATIONS_COLOR} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</div>
|
||||
<div className="protection-stat-total-info">
|
||||
<div
|
||||
className={`font-bold`}
|
||||
style={{ color: CHECKS_COLOR }}
|
||||
>
|
||||
{totalChecks} {pluralizeCheks(totalChecks)}
|
||||
</div>
|
||||
<div
|
||||
className={`font-bold`}
|
||||
style={{ color: VIOLATIONS_COLOR }}
|
||||
>
|
||||
{totalViolations} {pluralizeViolations(totalViolations)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import { useStoreWithDevtools } from '@/app/stores/useStoreWithDevtools';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export function TestComponent() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const setText1 = useStoreWithDevtools(s => s.setValue1);
|
||||
const setText2 = useStoreWithDevtools(s => s.setValue2);
|
||||
const text1 = useStoreWithDevtools(s => s.value1);
|
||||
const text2 = useStoreWithDevtools(s => s.value2);
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className='mb-2'>
|
||||
<input
|
||||
className='border rounded-md mr-1'
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
setText1(Number(e.target.value))
|
||||
}}
|
||||
/>
|
||||
<span>{t('check')} {text1}</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
className='border rounded-md mr-1'
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
setText2(Number(e.target.value))
|
||||
}}
|
||||
/>
|
||||
<span>{t('violation')} {text2}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,681 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
|
||||
import { IconShieldAdd } from '@/app/ui/icons/icons';
|
||||
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';
|
||||
import DropDownListForImageConvert from '@/app/components/DropDownListForImageConvert';
|
||||
|
||||
export interface SelectedFile {
|
||||
file: File;
|
||||
preview: string | undefined;
|
||||
name: string;
|
||||
size: string;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
uploadId?: string;
|
||||
progress: number;
|
||||
status: 'pending' | 'uploading' | 'completed' | 'error' | 'cancelled';
|
||||
error?: string;
|
||||
convertTo?: string | null;
|
||||
}
|
||||
|
||||
interface FileUploadInitResponse {
|
||||
upload_id: string;
|
||||
file_name: string;
|
||||
total_chunks: number;
|
||||
chunk_size: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface UploadSectionFile {
|
||||
fileType: string;
|
||||
allowedExtensions: string[];
|
||||
maxFileSize: number;
|
||||
maxParallelUploads?: number;
|
||||
}
|
||||
|
||||
export default function UploadSectionFile({
|
||||
fileType,
|
||||
allowedExtensions,
|
||||
maxFileSize,
|
||||
maxParallelUploads = 3
|
||||
}: UploadSectionFile) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false);
|
||||
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeUploads, setActiveUploads] = useState<Set<string>>(new Set()); // ID активных загрузок
|
||||
const queryClient = useQueryClient();
|
||||
const isCancelledRef = useRef<Map<string, boolean>>(new Map()); // Для отслеживания отмен по файлам
|
||||
const selectedFilesRef = useRef<SelectedFile[]>(selectedFiles);
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const acceptString = useMemo(() => {
|
||||
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
|
||||
return '';
|
||||
}
|
||||
return allowedExtensions.map(e => `.${e}`).join(', ');
|
||||
}, [allowedExtensions]);
|
||||
|
||||
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
|
||||
if (!allowedExtensions.includes(file.type as string)) {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
if (!extension || !allowedExtensions.includes(extension as string)) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('unsupported-file-format')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_SIZE = maxFileSize;
|
||||
|
||||
if (file.size > MAX_SIZE) {
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: t('the-file-is-too-large')
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
};
|
||||
|
||||
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
|
||||
img.onload = () => {
|
||||
const dimensions = {
|
||||
width: img.width,
|
||||
height: img.height
|
||||
};
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
resolve(dimensions);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
reject(new Error(t('error-uploading-file')));
|
||||
};
|
||||
|
||||
img.src = objectUrl;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFilesSelect = useCallback(async (files: File[]): Promise<void> => {
|
||||
setError(null);
|
||||
|
||||
const newFiles: SelectedFile[] = [];
|
||||
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) {
|
||||
setError(prev => prev ? `${prev}, ${validation.errorMessage}` : validation.errorMessage || t('unknown-validation-error'));
|
||||
continue;
|
||||
}
|
||||
|
||||
let dimensions: {
|
||||
width: number,
|
||||
height: number,
|
||||
} | undefined = undefined;
|
||||
if (fileType === "image") {
|
||||
try {
|
||||
dimensions = await getImageDimensions(file) as { width: number, height: number };
|
||||
if (dimensions?.width < 100 || dimensions?.height < 100 ||
|
||||
dimensions?.width > 10000 || dimensions?.height > 10000) {
|
||||
setError(prev => prev ? `${prev}, ${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.` :
|
||||
`${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(prev => prev ? `${prev}, ${t('error-reading-image')}` : t('error-reading-image'));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentFileNames.add(file.name);
|
||||
|
||||
newFiles.push({
|
||||
file,
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||
dimensions,
|
||||
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined,
|
||||
progress: 0,
|
||||
status: 'pending'
|
||||
});
|
||||
}
|
||||
|
||||
if (duplicateNames.length > 0) {
|
||||
const uniqueDuplicates = [...new Set(duplicateNames)];
|
||||
setError(`${t('duplicate-files')}: ${uniqueDuplicates.join(', ')}`);
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => [...prev, ...newFiles]);
|
||||
}, [fileType, selectedFiles]);
|
||||
|
||||
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const files = Array.from(event.target.files || []);
|
||||
handleFilesSelect(files);
|
||||
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (): void => {
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
|
||||
event.preventDefault();
|
||||
setIsDragging(false);
|
||||
|
||||
const files = Array.from(event.dataTransfer.files);
|
||||
handleFilesSelect(files);
|
||||
};
|
||||
|
||||
const handleClearFile = async (fileId?: string): Promise<void> => {
|
||||
if (fileId) {
|
||||
const fileToClear = selectedFiles.find(f => f.uploadId === fileId || f.name === fileId);
|
||||
if (fileToClear?.preview) {
|
||||
URL.revokeObjectURL(fileToClear.preview);
|
||||
}
|
||||
|
||||
if (fileToClear?.uploadId) {
|
||||
await cancelUpload(fileToClear.uploadId);
|
||||
isCancelledRef.current.set(fileToClear.uploadId, true);
|
||||
setActiveUploads(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(fileToClear.uploadId!);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => prev.filter(f =>
|
||||
f.uploadId !== fileId && f.name !== fileId
|
||||
));
|
||||
} else {
|
||||
console.log('clear all')
|
||||
selectedFiles.forEach(file => {
|
||||
if (file.preview) {
|
||||
URL.revokeObjectURL(file.preview);
|
||||
}
|
||||
});
|
||||
|
||||
const cancelPromises = Array.from(activeUploads).map(uploadId =>
|
||||
cancelUpload(uploadId)
|
||||
);
|
||||
await Promise.allSettled(cancelPromises);
|
||||
|
||||
isCancelledRef.current.clear();
|
||||
setActiveUploads(new Set());
|
||||
setSelectedFiles([]);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
};
|
||||
|
||||
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
|
||||
));
|
||||
|
||||
const extension = fileInfo.file.name.split('.').pop() || '';
|
||||
const initMessageBody = {
|
||||
file_name: fileInfo.file.name,
|
||||
file_type: fileType,
|
||||
extension: extension,
|
||||
file_size: fileInfo.file.size
|
||||
};
|
||||
|
||||
const response = await fileUpload(initMessageBody, fileInfo.convertTo) as FileUploadInitResponse;
|
||||
|
||||
if (!response?.upload_id) {
|
||||
throw new Error('Failed to get upload_id');
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, uploadId: response.upload_id } : f
|
||||
));
|
||||
|
||||
setActiveUploads(prev => new Set(prev).add(response.upload_id));
|
||||
|
||||
const CHUNK_SIZE = response.chunk_size;
|
||||
const totalChunks = response.total_chunks;
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
if (isCancelledRef.current.get(fileId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const start = chunkIndex * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, fileInfo.file.size);
|
||||
const chunk = fileInfo.file.slice(start, end);
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('upload_id', response.upload_id);
|
||||
formData.append('chunk_number', chunkIndex.toString());
|
||||
formData.append('chunk', chunk);
|
||||
|
||||
const chunkResponse = await chunkUpload(formData);
|
||||
|
||||
if (chunkResponse.errorMesage !== null) {
|
||||
throw chunkResponse.errorMesage;
|
||||
}
|
||||
|
||||
const progress = Math.floor((chunkIndex + 1) / totalChunks * 100);
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, progress } : f
|
||||
));
|
||||
}
|
||||
|
||||
const chunkStatus = await checkChunkStatus(response.upload_id);
|
||||
if (chunkStatus.message_body.missing_chunks === 0) {
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'completed', progress: 100 } : f
|
||||
));
|
||||
|
||||
setActiveUploads(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(response.upload_id);
|
||||
return newSet;
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['userData'] });
|
||||
} else {
|
||||
throw new Error('Not all chunks were uploaded');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (!isCancelledRef.current.get(fileId)) {
|
||||
const errorMessage = (typeof error === 'string') ? t(error) : t('error-uploading-file');
|
||||
setSelectedFiles(prev => prev.map(f =>
|
||||
f.name === fileInfo.name ? { ...f, status: 'error', error: errorMessage } : f
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
selectedFilesRef.current = selectedFiles;
|
||||
}, [selectedFiles]);
|
||||
|
||||
const handleStartAllUploads = useCallback(async (): Promise<void> => {
|
||||
setError(null);
|
||||
if (selectedFilesRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
const priceCheck = await checkOperationPrice('checkTokensForProtect', selectedFilesRef.current.length, fileType);
|
||||
|
||||
if (priceCheck.success) {
|
||||
const uploadNextBatch = async () => {
|
||||
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
|
||||
const batch = currentPending.slice(0, maxParallelUploads);
|
||||
|
||||
if (batch.length === 0) return;
|
||||
|
||||
await Promise.all(batch.map(file => uploadSingleFile(file, 'multi')));
|
||||
|
||||
if (selectedFilesRef.current.some(f => f.status === 'pending')) {
|
||||
await uploadNextBatch();
|
||||
}
|
||||
};
|
||||
|
||||
await uploadNextBatch();
|
||||
} else {
|
||||
const errorMessage = `${t('there-are-not-enough-funds-to-protect-files', { count: priceCheck?.tokens_count })}`;
|
||||
setError(errorMessage);
|
||||
}
|
||||
}, [maxParallelUploads]);
|
||||
|
||||
const getTotalProgress = useMemo(() => {
|
||||
if (selectedFiles.length === 0) return 0;
|
||||
const total = selectedFiles.reduce((sum, file) => sum + file.progress, 0);
|
||||
return Math.floor(total / selectedFiles.length);
|
||||
}, [selectedFiles]);
|
||||
|
||||
const completedCount = useMemo(() =>
|
||||
selectedFiles.filter(f => f.status === 'completed').length,
|
||||
[selectedFiles]
|
||||
);
|
||||
|
||||
const uploadingCount = useMemo(() =>
|
||||
selectedFiles.filter(f => f.status === 'uploading').length,
|
||||
[selectedFiles]
|
||||
);
|
||||
|
||||
|
||||
/* useEffect(() => {
|
||||
// Обработка закрытия вкладки
|
||||
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
|
||||
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
|
||||
// но у него есть свои особенности и стандартные запросы не работают.
|
||||
const handleUnload = () => {
|
||||
if (activeUploads.size) {
|
||||
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
|
||||
//const blob = new Blob([data], { type: 'application/json' });
|
||||
//navigator.sendBeacon('/api/v1/data/cancel', blob);
|
||||
console.log('sendBeacon');
|
||||
}
|
||||
};
|
||||
if (activeUploads.size) {
|
||||
window.addEventListener('unload', handleUnload);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('unload', handleUnload);
|
||||
};
|
||||
}, [activeUploads.size]); */
|
||||
|
||||
useNavigationBlocker({
|
||||
shouldBlock: !!activeUploads.size,
|
||||
message: t('have-unsaved-changes'),
|
||||
onConfirm: async () => {
|
||||
console.log('User confirmed navigation');
|
||||
if (activeUploads.size) {
|
||||
await handleClearFile();
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('User cancelled navigation');
|
||||
}
|
||||
});
|
||||
|
||||
function converButtonHandler(value: {
|
||||
convertTo: string;
|
||||
file: SelectedFile
|
||||
}): void {
|
||||
setSelectedFiles(selectedFiles.map(selectedFile => {
|
||||
if (selectedFile.name === value.file.name) {
|
||||
return {
|
||||
...selectedFile,
|
||||
convertTo: value.convertTo
|
||||
};
|
||||
}
|
||||
return selectedFile;
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="upload-section" >
|
||||
<h3>
|
||||
{t('upload-for-protection', {
|
||||
fileType: t(fileType.toLocaleLowerCase())
|
||||
})}
|
||||
</h3>
|
||||
|
||||
< div
|
||||
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<IconShieldAdd />
|
||||
<h4>
|
||||
{t('drag-the-file-here')}
|
||||
</h4>
|
||||
|
||||
{
|
||||
fileType === "image" && (
|
||||
<p className="description" >
|
||||
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
<p className="description" >
|
||||
{t('file-format')}: {acceptString}
|
||||
</p>
|
||||
< p className="description" >
|
||||
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
|
||||
</p>
|
||||
|
||||
< input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept={acceptString}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileInputChange}
|
||||
aria-label="Выбор файла для защиты"
|
||||
multiple // Добавляем multiple для выбора нескольких файлов
|
||||
/>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
type="button"
|
||||
disabled={uploadingCount > maxParallelUploads}
|
||||
>
|
||||
{t('select-files-to-protect')}
|
||||
</button>
|
||||
|
||||
{
|
||||
error && (
|
||||
<div className="error-message">
|
||||
<p className="error-message-text"> {error} </p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
selectedFiles.length > 0 && (
|
||||
<div className="mt-6" >
|
||||
<div className="selected-file-action-panel" >
|
||||
<div>
|
||||
<p className="selected-file-action-info" >
|
||||
<span>{t('total-files')}: {selectedFiles.length}</span>
|
||||
<span>{t('uploading')}: {uploadingCount}</span>
|
||||
<span>{t('completed')}: {completedCount}</span>
|
||||
</p>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${getTotalProgress}%` }
|
||||
}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`btn btn-primary`}
|
||||
onClick={handleStartAllUploads}
|
||||
type="button"
|
||||
disabled={uploadingCount > 0 || selectedFiles.every(f => f.status !== 'pending')}
|
||||
>
|
||||
{t('upload-all')}
|
||||
</button>
|
||||
< button
|
||||
className="btn btn-primary"
|
||||
onClick={() => handleClearFile()}
|
||||
type="button"
|
||||
>
|
||||
{t('clear-all')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="selected-file-wrapper">
|
||||
{
|
||||
selectedFiles.map((file, index) => (
|
||||
<div
|
||||
key={`${file.name}-${index}`}
|
||||
className={`selected-file ${file.status === 'completed' ? 'done' : ''} ${file.status === 'error' ? 'error' : ''}`}
|
||||
>
|
||||
<div className="selected-file-file-info">
|
||||
<div>
|
||||
<p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('file')}: </span> {file.name}
|
||||
</p>
|
||||
< p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('size')}: </span> {file.size}
|
||||
</p>
|
||||
< p className="text-gray-700" >
|
||||
<span className="font-medium" > {t('status')}: </span>
|
||||
<span className={`ml-2 ${file.status === 'completed' ? 'text-green-600' :
|
||||
file.status === 'uploading' ? 'text-blue-600' :
|
||||
file.status === 'error' ? 'text-red-600' :
|
||||
'text-gray-600'
|
||||
}`}>
|
||||
{
|
||||
file.status === 'completed' ? t('completed') :
|
||||
file.status === 'uploading' ? t('uploading') :
|
||||
file.status === 'error' ? t('error') :
|
||||
t('pending')
|
||||
}
|
||||
</span>
|
||||
</p>
|
||||
{
|
||||
file.error && (
|
||||
<p className="text-red-600 text-sm mt-1" > {file.error} </p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="selected-file-right-block">
|
||||
{
|
||||
(fileType === 'image' && file.status === 'pending') && (
|
||||
<>
|
||||
<DropDownListForImageConvert
|
||||
value={file.convertTo}
|
||||
translatedValue={file.convertTo === 'reject' ? t('do-not-convert') : file.convertTo}
|
||||
callBack={converButtonHandler}
|
||||
selectedFiles={selectedFiles}
|
||||
>
|
||||
<li data-file={file.name} data-convert-to="reject">
|
||||
{t('do-not-convert')}
|
||||
</li>
|
||||
{
|
||||
allowedExtensions && allowedExtensions.map(extension => {
|
||||
if (extension === 'gif') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={extension} data-file={file.name} data-convert-to={extension}>
|
||||
{t('convert-to')} {extension}
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}
|
||||
</DropDownListForImageConvert>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className="flex"
|
||||
>
|
||||
{file.status !== 'error' && (
|
||||
<div className="upload-progress-circle">
|
||||
<svg viewBox="0 0 36 36" className="circular-chart">
|
||||
<path
|
||||
className="circle-bg"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<path
|
||||
className="circle"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke={file.status === 'completed' ? '#10b981' : file.status === 'uploading' ? '#155dfc' : '#6b7280'}
|
||||
strokeWidth="4"
|
||||
strokeDasharray={`${file.progress}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className="circle-percentage">{file.progress}%</span>
|
||||
</div>
|
||||
)}
|
||||
< button
|
||||
className="btn btn-cancel"
|
||||
onClick={() => handleClearFile(file.uploadId || file.name)}
|
||||
type="button"
|
||||
>
|
||||
{t('remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
(fileType === 'image' && file.preview) && (
|
||||
<div
|
||||
className="image-preview-wrapper"
|
||||
>
|
||||
<img
|
||||
src={file.preview}
|
||||
alt="Предпросмотр загруженного изображения"
|
||||
className="uploaded-image"
|
||||
onError={() => setError('Не удалось загрузить превью изображения')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
file.status === 'pending' && (
|
||||
<div className="mt-2 text-center" >
|
||||
<button
|
||||
className="btn btn-confirm"
|
||||
onClick={() => uploadSingleFile(file, 'single')
|
||||
}
|
||||
type="button"
|
||||
disabled={uploadingCount >= maxParallelUploads}
|
||||
>
|
||||
{t('start-upload')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as VKID from '@vkid/sdk';
|
||||
import { vkAuthorization } from '@/app/actions/vkAuth';
|
||||
|
||||
export default function VKLogin() {
|
||||
const [codeVerifier, setCodeVerifier] = useState<string>('');
|
||||
const [codeChallenge, setCodeChallenge] = useState<string>('');
|
||||
const [state, setState] = useState<string>('');
|
||||
|
||||
async function generateAuthString(minLength: number = 43, maxLength: number = 128) {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
|
||||
const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;
|
||||
|
||||
let result = '';
|
||||
const charsLength = chars.length;
|
||||
|
||||
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
||||
const randomValues = new Uint32Array(length);
|
||||
crypto.getRandomValues(randomValues);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(randomValues[i] % charsLength);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * charsLength));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function generateCodeChallenge(codeVerifier: string) {
|
||||
try {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(codeVerifier);
|
||||
const digest = await crypto.subtle.digest('SHA-256', data);
|
||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)));
|
||||
const base64url = base64
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
return base64url;
|
||||
} catch (error) {
|
||||
console.error('Error generating code challenge:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function clickHandler() {
|
||||
console.log('clickHandler');
|
||||
console.log('codeChallenge:', codeChallenge);
|
||||
alert("Кнопка отключена");
|
||||
return;
|
||||
|
||||
try {
|
||||
if (!codeVerifier) {
|
||||
const newCodeVerifier = await generateAuthString(43, 128);
|
||||
const newCodeChallenge = await generateCodeChallenge(newCodeVerifier);
|
||||
|
||||
setCodeVerifier(newCodeVerifier);
|
||||
setCodeChallenge(newCodeChallenge);
|
||||
|
||||
VKID.Config.update({
|
||||
codeChallenge: newCodeChallenge
|
||||
});
|
||||
}
|
||||
|
||||
const data = await VKID.Auth.login();
|
||||
vkidOnSuccess(data);
|
||||
} catch (error) {
|
||||
console.error('Authorization error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function vkidOnSuccess(data: any) {
|
||||
console.log('vkidOnSuccess');
|
||||
console.log('vk data:', data);
|
||||
console.log('codeVerifier :', codeVerifier);
|
||||
|
||||
vkAuthorization(data, codeVerifier);
|
||||
|
||||
const newCodeVerifier = await generateAuthString(43, 128);
|
||||
const newCodeChallenge = await generateCodeChallenge(newCodeVerifier);
|
||||
|
||||
setCodeVerifier(newCodeVerifier);
|
||||
setCodeChallenge(newCodeChallenge);
|
||||
|
||||
VKID.Config.update({
|
||||
codeChallenge: newCodeChallenge
|
||||
});
|
||||
|
||||
console.log('new codeChallenge:', newCodeChallenge);
|
||||
console.log('state: ', state)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return; //выключил кнопку пока нету бизне аккаунта, что бы не было ошибко на странице.
|
||||
async function initVKID() {
|
||||
try {
|
||||
const newCodeVerifier = await generateAuthString(43, 128);
|
||||
const newCodeChallenge = await generateCodeChallenge(newCodeVerifier);
|
||||
const newState = await generateAuthString(32, 128);
|
||||
setState(newState);
|
||||
|
||||
setCodeVerifier(newCodeVerifier);
|
||||
setCodeChallenge(newCodeChallenge);
|
||||
|
||||
VKID.Config.init({
|
||||
app: 54389559,
|
||||
redirectUrl: 'http://localhost',
|
||||
responseMode: VKID.ConfigResponseMode.Callback,
|
||||
source: VKID.ConfigSource.LOWCODE,
|
||||
scope: 'email phone vkid.personal_info',
|
||||
codeChallenge: newCodeChallenge,
|
||||
state: newState
|
||||
});
|
||||
|
||||
console.log('Generated codeVerifier:', newCodeVerifier);
|
||||
console.log('Generated codeChallenge:', newCodeChallenge);
|
||||
} catch (error) {
|
||||
console.error('Error initializing VKID:', error);
|
||||
}
|
||||
}
|
||||
|
||||
initVKID();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button id="VKIDSDKAuthButton" className="VkIdWebSdk__button VkIdWebSdk__button_reset">
|
||||
<div className="VkIdWebSdk__button_container">
|
||||
<div className="VkIdWebSdk__button_icon">
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M4.54648 4.54648C3 6.09295 3 8.58197 3 13.56V14.44C3 19.418 3 21.907 4.54648 23.4535C6.09295 25 8.58197 25 13.56 25H14.44C19.418 25 21.907 25 23.4535 23.4535C25 21.907 25
|
||||
19.418 25 14.44V13.56C25 8.58197 25 6.09295 23.4535 4.54648C21.907 3 19.418 3 14.44 3H13.56C8.58197 3 6.09295 3 4.54648 4.54648ZM6.79999 10.15C6.91798 15.8728 9.92951 19.31 14.8932 19.31H15.1812V16.05C16.989 16.2332 18.3371
|
||||
17.5682 18.8875 19.31H21.4939C20.7869 16.7044 18.9535 15.2604 17.8141 14.71C18.9526 14.0293 20.5641 12.3893 20.9436 10.15H18.5722C18.0747 11.971 16.5945 13.6233 15.1803 13.78V10.15H12.7711V16.5C11.305 16.1337 9.39237 14.3538 9.314 10.15H6.79999Z" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="VkIdWebSdk__button_text" onClick={clickHandler}>
|
||||
Войти с VK ID
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import Script from 'next/script';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
YaAuthSuggest: any;
|
||||
}
|
||||
}
|
||||
|
||||
export default function YandexAuthWithScript() {
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [showButton, setShowButton] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return;
|
||||
|
||||
window.YaAuthSuggest.init(
|
||||
{
|
||||
client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
|
||||
response_type: 'token',
|
||||
redirect_uri: `https://fallibly-desired-moonfish.cloudpub.ru/en/login`,
|
||||
},
|
||||
'https://fallibly-desired-moonfish.cloudpub.ru',
|
||||
{
|
||||
parentId: buttonRef.current.id,
|
||||
view: 'button',
|
||||
buttonView: 'main',
|
||||
buttonTheme: 'light',
|
||||
buttonSize: 'l',
|
||||
buttonBorderRadius: 10
|
||||
}
|
||||
)
|
||||
.then(function (result: any) {
|
||||
console.log('you at least try?')
|
||||
console.log(result);
|
||||
return result.handler();
|
||||
})
|
||||
.then(function (data: any) {
|
||||
console.log('Сообщение с токеном: ', data);
|
||||
})
|
||||
.catch(function (error: any) {
|
||||
console.log('Что-то пошло не так: ', error);
|
||||
});
|
||||
}, [isReady]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
src="https://yastatic.net/s3/passport-sdk/autofill/v1/sdk-suggest-with-polyfills-latest.js"
|
||||
strategy="lazyOnload"
|
||||
onLoad={() => setIsReady(true)}
|
||||
onError={() => console.error('Failed to load Yandex script')}
|
||||
/>
|
||||
|
||||
{showButton && (
|
||||
<div
|
||||
ref={buttonRef}
|
||||
id="yandex-auth-container"
|
||||
className="flex justify-center"
|
||||
>
|
||||
{!isReady && <div>Загрузка Яндекс авторизации...</div>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function YandexLoginButton() {
|
||||
const clientId = 'b356a7ceac0f4c50a3b434220f86bce0';
|
||||
const redirectUri = encodeURIComponent('https://fallibly-desired-moonfish.cloudpub.ru/login/callback');
|
||||
|
||||
const authUrl = `https://oauth.yandex.ru/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&force_confirm=yes`;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={authUrl}
|
||||
className="yandex-auth-button"
|
||||
>
|
||||
Войти через Яндекс
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import {
|
||||
YooMoneyCheckoutWidget,
|
||||
YooMoneyCheckoutWidgetConfig,
|
||||
YooMoneyErrorCallbackResult
|
||||
} from "types-yoomoneycheckoutwidget";
|
||||
|
||||
let isLoading = false;
|
||||
let isInited = false;
|
||||
export default function YooWidget({ config, onComplete, onSuccess, onFail, onModalClose }: {
|
||||
/**
|
||||
* Описание https://yookassa.ru/developers/payment-acceptance/integration-scenarios/widget/reference#initialization-parameters
|
||||
*/
|
||||
config: YooMoneyCheckoutWidgetConfig,
|
||||
/** после успешной оплаты*/
|
||||
onComplete?: () => void,
|
||||
/** после успешной оплаты, только если не подписались на onComplete*/
|
||||
onSuccess?: () => void,
|
||||
/** только если вы отключили обработку неуспешных попыток в виджете: пользователь выбрал любой способ оплаты, но платеж не прошел; закончился срок действия токена.*/
|
||||
onFail?: () => void,
|
||||
/** после закрытия модального окна платежной формы если customization.modal==true */
|
||||
onModalClose?: () => void,
|
||||
}) {
|
||||
const isModal = config?.customization?.modal;
|
||||
let checkout: YooMoneyCheckoutWidget;
|
||||
const destroy = () => {
|
||||
console.log('checkout.destroy');
|
||||
checkout.destroy();
|
||||
isInited = false;
|
||||
}
|
||||
|
||||
const initializeWidget = useCallback(
|
||||
() => {
|
||||
isInited = true;
|
||||
isLoading = false;
|
||||
if (!isModal) {
|
||||
const userErrorCallBack = config.error_callback;
|
||||
config.error_callback = (error: YooMoneyErrorCallbackResult) => {
|
||||
userErrorCallBack(error);
|
||||
isInited = false;
|
||||
};
|
||||
}
|
||||
checkout = new window.YooMoneyCheckoutWidget(config);
|
||||
checkout.on('complete', (result) => {
|
||||
console.log('on complete', result);
|
||||
onComplete && onComplete();
|
||||
destroy();
|
||||
});
|
||||
onSuccess && checkout.on('success', (result) => {
|
||||
console.log('on success', result);
|
||||
onSuccess();
|
||||
destroy();
|
||||
});
|
||||
onFail && checkout.on('fail', (result) => {
|
||||
console.warn('on fail', result);
|
||||
onFail();
|
||||
destroy();
|
||||
});
|
||||
|
||||
if (config.customization?.modal) {
|
||||
checkout.on('modal_close', (result) => {
|
||||
onModalClose && onModalClose();
|
||||
destroy();
|
||||
});
|
||||
}
|
||||
checkout.render(config.customization?.modal ? undefined : 'payment-form');
|
||||
|
||||
}, [config, onSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isInited) {
|
||||
if (!window.YooMoneyCheckoutWidget) {
|
||||
isLoading = true;
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://yookassa.ru/checkout-widget/v1/checkout-widget.js';
|
||||
script.async = true;
|
||||
script.onload = initializeWidget;
|
||||
document.head.appendChild(script);
|
||||
return () => {
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
} else {
|
||||
initializeWidget();
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!isInited && !isModal) {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isModal ? '' : <div id="payment-form" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CopyLinkButtonProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CopyLinkButton({ className = 'btn btn-out', children = 'Скопировать ссылку' }: CopyLinkButtonProps) {
|
||||
|
||||
const copyCurrentPageLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
toast.success('Ссылка скопирована');
|
||||
} catch (err) {
|
||||
toast.error('Не удалось скопировать ссылку');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={copyCurrentPageLink}
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { downloadTrackingFile } from '@/app/actions/trackingActions';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
interface DownloadButtonProps {
|
||||
fileId: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export function DownloadButton({ fileId, fileName }: DownloadButtonProps) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const result = await downloadTrackingFile(fileId, fileName);
|
||||
|
||||
if (!result) {
|
||||
throw 'failed-to-download-file'
|
||||
}
|
||||
|
||||
const byteCharacters = atob(result.data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: result.contentType });
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = result.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success(`${t('file-is-downloading')} - `);
|
||||
} catch (error) {
|
||||
toast.error(t('failed-to-download-file'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button className="btn btn-out" onClick={handleDownload}>
|
||||
Скачать документ
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export function LoginUserBage() {
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
if (!userData?.fullName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href="#" className="header-user">
|
||||
{userData?.fullName}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
'use client'
|
||||
|
||||
import { FileItem } from '@/app/components/tanstak-table/TanstakTable';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { setMonitoring } from '@/app/actions/monitoringActions';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function MonitoringDropDown({ file }: {
|
||||
file: FileItem
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentMonitoringStatus, setCurrentMonitoringStatus] = useState(file.monitoring);
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const t = useTranslations('Monitoring-status');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!dropdownRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!buttonRef?.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonRef?.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClick);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClick);
|
||||
};
|
||||
}, [dropdownRef, setIsOpen, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (file.monitoring) {
|
||||
setCurrentMonitoringStatus(file.monitoring ? file.monitoring : 'NONE');
|
||||
}
|
||||
}, [file.monitoring]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setDropdownPosition({
|
||||
top: rect.bottom + window.scrollY,
|
||||
left: rect.left + window.scrollX,
|
||||
width: rect.width
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
async function clickHandler(monitoring: string) {
|
||||
|
||||
const response = await setMonitoring(file.id, monitoring);
|
||||
|
||||
if (response.success) {
|
||||
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
setIsOpen(false);
|
||||
toast.success(t('the-file-is monitored', { fileName: file.fileName }));
|
||||
setCurrentMonitoringStatus(monitoring);
|
||||
} else {
|
||||
toast.warning(t('failed-to-establish-monitoring'));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dropdown-wrapper monitoring-dropdown">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
className="dropdown-button"
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<span className="mr-2">
|
||||
{t(currentMonitoringStatus)}
|
||||
</span>
|
||||
</span>
|
||||
<svg
|
||||
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && createPortal(
|
||||
<div
|
||||
className="dropdown-list-portal"
|
||||
ref={dropdownRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
width: dropdownPosition.width,
|
||||
zIndex: 99999
|
||||
}}
|
||||
>
|
||||
<ul className="dropdown-list" role="listbox">
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'NONE' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('NONE')
|
||||
}}
|
||||
>
|
||||
{t('NONE')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_DAILY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('MONITORING_DAILY')
|
||||
}}
|
||||
>
|
||||
{t('MONITORING_DAILY')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_WEEKLY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('MONITORING_WEEKLY')
|
||||
}}
|
||||
>
|
||||
{t('MONITORING_WEEKLY')}
|
||||
</li>
|
||||
<li
|
||||
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_MONTHLY' ? 'current' : ''}`}
|
||||
onClick={() => {
|
||||
clickHandler('MONITORING_MONTHLY')
|
||||
}}
|
||||
>
|
||||
{t('MONITORING_MONTHLY')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function SelectedFilesAction({ filesId, callBack }: { filesId: Set<string>, callBack: (value: Set<string>) => void }) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
if (filesId.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function downloadHandler() {
|
||||
if (filesId.size) {
|
||||
const filesArray = Array.from(filesId);
|
||||
const filesString = filesArray.join(', ');
|
||||
toast.success(`${t('download')}: ${filesString}`)
|
||||
}
|
||||
}
|
||||
|
||||
function clearHandler() {
|
||||
callBack(new Set());
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="selected-files-wrapper"
|
||||
>
|
||||
<div
|
||||
className="selected-files-count"
|
||||
>
|
||||
{t('selected-files')}: {filesId.size}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="selected-files-action"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
downloadHandler();
|
||||
}}
|
||||
>
|
||||
{t('download-all')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
clearHandler();
|
||||
}}
|
||||
>
|
||||
{t('deselect')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,302 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getPaginationRowModel,
|
||||
getFilteredRowModel,
|
||||
ColumnDef,
|
||||
SortingState,
|
||||
ColumnFiltersState,
|
||||
FilterFn,
|
||||
} from '@tanstack/react-table';
|
||||
import { rankItem } from '@tanstack/match-sorter-utils';
|
||||
|
||||
// Типы данных
|
||||
type Person = {
|
||||
id: number;
|
||||
name: string;
|
||||
age: number;
|
||||
email: string;
|
||||
department: string;
|
||||
salary: number;
|
||||
};
|
||||
|
||||
// Кастомная функция фильтрации
|
||||
const fuzzyFilter: FilterFn<Person> = (row, columnId, value, addMeta) => {
|
||||
const itemRank = rankItem(row.getValue(columnId), value);
|
||||
addMeta({ itemRank });
|
||||
return itemRank.passed;
|
||||
};
|
||||
|
||||
// Определяем тип для фильтров
|
||||
declare module '@tanstack/react-table' {
|
||||
interface FilterFns {
|
||||
fuzzy: FilterFn<unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
export default function DataTable() {
|
||||
// Данные
|
||||
const [data] = useState<Person[]>([
|
||||
{ id: 1, name: 'Иван Петров', age: 28, email: 'ivan@example.com', department: 'IT', salary: 80000 },
|
||||
{ id: 2, name: 'Анна Сидорова', age: 32, email: 'anna@example.com', department: 'HR', salary: 65000 },
|
||||
{ id: 3, name: 'Сергей Иванов', age: 45, email: 'sergey@example.com', department: 'Финансы', salary: 95000 },
|
||||
{ id: 4, name: 'Мария Кузнецова', age: 26, email: 'maria@example.com', department: 'Маркетинг', salary: 60000 },
|
||||
{ id: 5, name: 'Алексей Смирнов', age: 38, email: 'alex@example.com', department: 'IT', salary: 85000 },
|
||||
{ id: 6, name: 'Ольга Васнецова', age: 29, email: 'olga@example.com', department: 'HR', salary: 62000 },
|
||||
{ id: 7, name: 'Дмитрий Орлов', age: 41, email: 'dmitry@example.com', department: 'Финансы', salary: 92000 },
|
||||
{ id: 8, name: 'Екатерина Новикова', age: 35, email: 'ekaterina@example.com', department: 'Маркетинг', salary: 70000 },
|
||||
]);
|
||||
|
||||
// Состояния
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
// Определение колонок
|
||||
const columns = useMemo<ColumnDef<Person>[]>(
|
||||
() => [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onChange={table.getToggleAllPageRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.getIsSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableColumnFilter: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
cell: (info) => info.getValue(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Имя',
|
||||
cell: (info) => info.getValue(),
|
||||
filterFn: fuzzyFilter,
|
||||
},
|
||||
{
|
||||
accessorKey: 'age',
|
||||
header: 'Возраст',
|
||||
cell: (info) => info.getValue(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
cell: (info) => info.getValue(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'department',
|
||||
header: 'Отдел',
|
||||
cell: (info) => info.getValue(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'salary',
|
||||
header: 'Зарплата',
|
||||
cell: (info) => `$${Number(info.getValue()).toLocaleString()}`,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Создание таблицы
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter,
|
||||
},
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
rowSelection,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
globalFilterFn: fuzzyFilter, // Используем саму функцию, а не строку
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-6">Таблица сотрудников</h1>
|
||||
|
||||
{/* Глобальный поиск */}
|
||||
<div className="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск по всем полям..."
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
className="px-4 py-2 border rounded-md w-full max-w-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Фильтры по колонкам */}
|
||||
<div className="flex gap-4 mb-4 flex-wrap">
|
||||
{table.getAllColumns()
|
||||
.filter(column => column.getCanFilter() && column.id !== 'select')
|
||||
.map(column => (
|
||||
<div key={column.id} className="flex flex-col">
|
||||
<label className="text-sm font-medium mb-1">
|
||||
{typeof column.columnDef.header === 'string'
|
||||
? column.columnDef.header
|
||||
: column.id}
|
||||
</label>
|
||||
{column.id === 'department' ? (
|
||||
<select
|
||||
value={(column.getFilterValue() as string) || ''}
|
||||
onChange={e => column.setFilterValue(e.target.value)}
|
||||
className="px-3 py-1 border rounded"
|
||||
>
|
||||
<option value="">Все отделы</option>
|
||||
<option value="IT">IT</option>
|
||||
<option value="HR">HR</option>
|
||||
<option value="Финансы">Финансы</option>
|
||||
<option value="Маркетинг">Маркетинг</option>
|
||||
</select>
|
||||
) : column.id === 'age' ? (
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Возраст от..."
|
||||
value={(column.getFilterValue() as number) || ''}
|
||||
onChange={e => column.setFilterValue(e.target.value ? parseInt(e.target.value) : undefined)}
|
||||
className="px-3 py-1 border rounded w-32"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
placeholder={`Фильтр ${column.id}`}
|
||||
value={(column.getFilterValue() as string) || ''}
|
||||
onChange={e => column.setFilterValue(e.target.value)}
|
||||
className="px-3 py-1 border rounded"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Таблица */}
|
||||
<div className="overflow-x-auto border rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map(header => (
|
||||
<th
|
||||
key={header.id}
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: typeof header.column.columnDef.header === 'string'
|
||||
? header.column.columnDef.header
|
||||
: header.column.id}
|
||||
{{
|
||||
asc: ' 🔼',
|
||||
desc: ' 🔽',
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{table.getRowModel().rows.map(row => (
|
||||
<tr key={row.id} className="hover:bg-gray-50">
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<td key={cell.id} className="px-6 py-4 whitespace-nowrap">
|
||||
{typeof cell.column.columnDef.cell === 'function'
|
||||
? cell.column.columnDef.cell(cell.getContext())
|
||||
: cell.getValue() as string}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Пагинация */}
|
||||
<div className="flex items-center justify-between mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
⟪
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
⟨
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
⟩
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
⟫
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span>
|
||||
Страница{' '}
|
||||
<strong>
|
||||
{table.getState().pagination.pageIndex + 1} из {table.getPageCount()}
|
||||
</strong>
|
||||
</span>
|
||||
<select
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={e => table.setPageSize(Number(e.target.value))}
|
||||
className="px-2 py-1 border rounded"
|
||||
>
|
||||
{[5, 10, 20, 30, 40, 50].map(pageSize => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Показать {pageSize}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
Всего строк: {data.length} | Отфильтровано: {table.getFilteredRowModel().rows.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useRef, ReactNode } from 'react';
|
||||
|
||||
interface PdfContextType {
|
||||
downloadPdf: () => Promise<void>;
|
||||
setPdfData: (data: ArrayBuffer | null, fileName: string) => void;
|
||||
}
|
||||
|
||||
const PdfContext = createContext<PdfContextType | null>(null);
|
||||
|
||||
export function PdfProvider({ children }: { children: ReactNode }) {
|
||||
const pdfDataRef = useRef<ArrayBuffer | null>(null);
|
||||
const fileNameRef = useRef<string>('');
|
||||
|
||||
const setPdfData = (data: ArrayBuffer | null, fileName: string) => {
|
||||
pdfDataRef.current = data;
|
||||
fileNameRef.current = fileName;
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
if (!pdfDataRef.current) {
|
||||
console.log('No PDF data available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = new Blob([pdfDataRef.current], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileNameRef.current || 'document.pdf';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.log('Error downloading PDF:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PdfContext.Provider value={{ downloadPdf, setPdfData }}>
|
||||
{children}
|
||||
</PdfContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePdf() {
|
||||
const context = useContext(PdfContext);
|
||||
if (!context) {
|
||||
throw new Error('usePdf must be used within PdfProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchActiveNotifications, NotificationBody } from '@/app/actions/notificationActions';
|
||||
|
||||
export interface UseActiveNotifications {
|
||||
notifications: NotificationBody[],
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
export const useActiveNotifications = () => {
|
||||
return useQuery({
|
||||
queryKey: ['activeNotifications'],
|
||||
queryFn: () => {
|
||||
return fetchActiveNotifications()
|
||||
},
|
||||
select: (data: UseActiveNotifications | null) => {
|
||||
if (!data) {
|
||||
return {
|
||||
notifications: [],
|
||||
unreadCount: 0
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
refetchInterval: 10000
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getAllFilesExtensions } from '@/app/lib/prefetch-queries';
|
||||
|
||||
|
||||
export interface allFilesExtensions {
|
||||
videos: string[];
|
||||
audios: string[];
|
||||
images: string[];
|
||||
documents: string[];
|
||||
maxFileSize: number;
|
||||
}
|
||||
|
||||
const EMPTY_EXTENSIONS: allFilesExtensions = {
|
||||
videos: [],
|
||||
audios: [],
|
||||
images: [],
|
||||
documents: [],
|
||||
maxFileSize: 0
|
||||
};
|
||||
|
||||
export const useAllFilesExtensions = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ['allFilesExtensions'],
|
||||
queryFn: getAllFilesExtensions,
|
||||
retry: false,
|
||||
initialData: EMPTY_EXTENSIONS
|
||||
});
|
||||
|
||||
return {
|
||||
...query,
|
||||
data: query.data ?? EMPTY_EXTENSIONS
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchAllNotifications, NotificationBody } from '@/app/actions/notificationActions';
|
||||
|
||||
export interface UseAllNotifications {
|
||||
notifications: NotificationBody[],
|
||||
currentPage: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useAllNotifications = (page: number, size: number = 10) => {
|
||||
return useQuery({
|
||||
queryKey: ['allNotifications', page, size],
|
||||
queryFn: () => {
|
||||
return fetchAllNotifications(page, size)
|
||||
},
|
||||
select: (data: UseAllNotifications | null) => {
|
||||
if (!data) {
|
||||
return {
|
||||
notifications: [],
|
||||
currentPage: 0,
|
||||
totalElements: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
refetchInterval: 10000
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getFileMatches } from '@/app/actions/violationActions';
|
||||
|
||||
export interface ViolationFileDetail {
|
||||
id: number;
|
||||
url: string;
|
||||
page_url: string;
|
||||
page_title: string;
|
||||
host: string;
|
||||
status: string;
|
||||
created_date: string;
|
||||
file_id: string;
|
||||
country: string;
|
||||
country_code: string;
|
||||
}
|
||||
|
||||
interface ViolationFile {
|
||||
violations: ViolationFileDetail[];
|
||||
total_elements: number;
|
||||
total_pages: number;
|
||||
current_page: number;
|
||||
page_size: number;
|
||||
has_next: boolean;
|
||||
has_previous: boolean;
|
||||
}
|
||||
|
||||
export const useFileViolations = (id: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: string,) => {
|
||||
return useQuery({
|
||||
queryKey: ['fileViolations', id, page, status, violationId, startDate, endDate],
|
||||
queryFn: () => {
|
||||
return getFileMatches(id, page, status, violationId, startDate, endDate)
|
||||
},
|
||||
select: (data: ViolationFile | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserPayments } from '@/app/actions/referralsActions';
|
||||
|
||||
export interface ReferralPayments {
|
||||
amount: number,
|
||||
completedAt: string | null,
|
||||
createdAt: string,
|
||||
id: number,
|
||||
processAt: string | null
|
||||
rejectionReason: string | null,
|
||||
status: string,
|
||||
updatedAt: string,
|
||||
}
|
||||
|
||||
export interface UserReferralPayments {
|
||||
payments: ReferralPayments[],
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useGetReferralPayments = () => {
|
||||
return useQuery({
|
||||
queryKey: ['referralPayments'],
|
||||
queryFn: getUserPayments,
|
||||
select: (data): UserReferralPayments => {
|
||||
if (!data) {
|
||||
return {
|
||||
payments: [],
|
||||
error: 'error'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
payments: data.payments,
|
||||
error: data.error
|
||||
}
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchHistoryView } from '@/app/actions/trackingActions';
|
||||
|
||||
export interface HistoryViewItem {
|
||||
city: string;
|
||||
country: string;
|
||||
viewer: string;
|
||||
document: string;
|
||||
view_date: string;
|
||||
}
|
||||
|
||||
export interface UseHistoryView {
|
||||
documentsCount: number;
|
||||
history: HistoryViewItem[];
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
uniqueCityCount: number;
|
||||
uniqueCountryCount: number;
|
||||
}
|
||||
|
||||
export const useHistoryView = (
|
||||
page: number,
|
||||
size: number,
|
||||
locale: string,
|
||||
sortDirection?: 'asc' | 'desc',
|
||||
sortBy?: string
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ['violationStatistic', page, size, locale, sortDirection, sortBy],
|
||||
queryFn: () => {
|
||||
return fetchHistoryView(page, size, locale, sortDirection, sortBy)
|
||||
},
|
||||
select: (data: UseHistoryView | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
placeholderData: (previousData) => previousData // для оптимистичных обновлений
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchLastClaims, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
|
||||
|
||||
export interface Case {
|
||||
amount: number;
|
||||
content: null | string;
|
||||
created_at: string;
|
||||
description: string;
|
||||
id: number;
|
||||
lawyer: null | string;
|
||||
name: string;
|
||||
page_number: number;
|
||||
page_size: number;
|
||||
priority: "HIGH" | "MEDIUM" | "LOW";
|
||||
total_elements: number;
|
||||
total_pages: number;
|
||||
type: "ACTIVE" | "INACTIVE" | string;
|
||||
updated_at: string;
|
||||
violation_id: number;
|
||||
file_id: string;
|
||||
}
|
||||
export interface useLastCase {
|
||||
content: Case[],
|
||||
page: number,
|
||||
size: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useLastCases = (params: ComplaintsCaseQueryParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['lastCases', params.page, params.size, params.sort_by, params.sort_direction],
|
||||
queryFn: () => {
|
||||
return fetchLastClaims(params)
|
||||
},
|
||||
select: (data: useLastCase | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchLastComplaint, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
|
||||
|
||||
export interface Complaint {
|
||||
complaint_text: string;
|
||||
created_at: string;
|
||||
email: string;
|
||||
id: number;
|
||||
not_moderated: boolean;
|
||||
status: "CREATED" | string;
|
||||
updated_at: string;
|
||||
violation_id: number;
|
||||
file_id: string;
|
||||
}
|
||||
export interface useLastComplaint {
|
||||
content: Complaint[],
|
||||
page: number,
|
||||
size: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useLastComplaints = (params: ComplaintsCaseQueryParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['lastComplaints', params.page, params.size, params.sort_by, params.sort_direction],
|
||||
queryFn: () => {
|
||||
return fetchLastComplaint(params)
|
||||
},
|
||||
select: (data: useLastComplaint | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchMonitoringInfo } from '@/app/actions/monitoringActions';
|
||||
|
||||
interface MonitoringCount {
|
||||
day: number;
|
||||
month: number;
|
||||
weekly: number;
|
||||
}
|
||||
|
||||
export const useMonitoringCount = () => {
|
||||
return useQuery({
|
||||
queryKey: ['monitoringCount'],
|
||||
queryFn: () => {
|
||||
return fetchMonitoringInfo()
|
||||
},
|
||||
select: (data: MonitoringCount | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchMonitoringStats } from '@/app/actions/monitoringActions';
|
||||
|
||||
interface MonitoringStats {
|
||||
total_searches: number;
|
||||
searches_by_status: {
|
||||
processing: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
};
|
||||
recent_searches: any[];
|
||||
}
|
||||
|
||||
export const useMonitoringStats = () => {
|
||||
return useQuery({
|
||||
queryKey: ['monitoringStats'],
|
||||
queryFn: () => {
|
||||
return fetchMonitoringStats()
|
||||
},
|
||||
select: (data: MonitoringStats | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPaymentsMethods } from '@/app/actions/paymentAction';
|
||||
|
||||
export interface PaymentsMethods {
|
||||
cardType: string,
|
||||
expiryMonth: string,
|
||||
expiryYear: string,
|
||||
lastFour: string,
|
||||
paymentMethodId: string
|
||||
}
|
||||
|
||||
export const usePaymentsMethods = () => {
|
||||
return useQuery({
|
||||
queryKey: ['paymentsMethods'],
|
||||
queryFn: () => {
|
||||
return getPaymentsMethods()
|
||||
},
|
||||
select: (data: PaymentsMethods[] | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchTrackingStats } from '@/app/actions/trackingActions';
|
||||
|
||||
export interface useTrackingStats {
|
||||
totalViews: number,
|
||||
uniqueIps: number,
|
||||
uniqueUsers: number,
|
||||
documentsCount: number,
|
||||
uniqueCountryCount: number,
|
||||
uniqueCityCount: number,
|
||||
firstViewDate: string,
|
||||
lastViewDate: string
|
||||
}
|
||||
|
||||
export const useTrackingStats = () => {
|
||||
return useQuery({
|
||||
queryKey: ['trackingStats'],
|
||||
queryFn: () => {
|
||||
return fetchTrackingStats()
|
||||
},
|
||||
select: (data: useTrackingStats | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserData } from '@/app/actions/action';
|
||||
|
||||
export interface Tariff {
|
||||
id: number;
|
||||
type: string;
|
||||
name: string;
|
||||
price: number;
|
||||
tokens: number;
|
||||
maxFilesCount: number;
|
||||
diskSize: number;
|
||||
maxUsers?: number;
|
||||
}
|
||||
|
||||
export interface TariffInfo {
|
||||
id: string;
|
||||
status: string;
|
||||
startTariff: string;
|
||||
endTariff: string;
|
||||
tariffId: number | null;
|
||||
tariffName: string | null;
|
||||
tokens: number | null;
|
||||
maxFileOnDisk: number | null;
|
||||
currentFileOnDisk: number;
|
||||
currentFileCounts: number;
|
||||
maxFileCounts: number;
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
fullName: string;
|
||||
company: string | null;
|
||||
email: string;
|
||||
active: boolean;
|
||||
phone: string;
|
||||
genderType: string | null;
|
||||
birthday: string | null;
|
||||
createdAt: string;
|
||||
subscriptionType: string;
|
||||
tariffs: Tariff[];
|
||||
tariffInfo: TariffInfo;
|
||||
permission: number;
|
||||
verifiedStatus?: 'unknow' | 'NOT_VERIFIED' | 'VERIFICATION_IN_PROGRESS' | 'VERIFIED' | 'VERIFICATION_FAILED';
|
||||
}
|
||||
|
||||
export const useUserProfile = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userData'],
|
||||
queryFn: getUserData,
|
||||
select: (data): UserData => {
|
||||
if (!data) {
|
||||
return {
|
||||
fullName: '',
|
||||
company: null,
|
||||
email: '',
|
||||
active: false,
|
||||
phone: '',
|
||||
genderType: '',
|
||||
birthday: null,
|
||||
createdAt: '',
|
||||
subscriptionType: '',
|
||||
tariffs: [],
|
||||
tariffInfo: {
|
||||
id: '',
|
||||
status: '',
|
||||
startTariff: '',
|
||||
endTariff: '',
|
||||
tariffId: 0,
|
||||
tariffName: null,
|
||||
tokens: 0,
|
||||
maxFileOnDisk: null,
|
||||
currentFileOnDisk: 0,
|
||||
currentFileCounts: 0,
|
||||
maxFileCounts: 0,
|
||||
},
|
||||
permission: 0,
|
||||
verifiedStatus: 'unknow'
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesInfo } from '@/app/actions/action';
|
||||
|
||||
export interface FilesInfo {
|
||||
total: {
|
||||
size: number;
|
||||
quantity: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
images: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
videos: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
audios: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
documents: {
|
||||
quantity: number;
|
||||
size: number;
|
||||
check: number;
|
||||
violation: number;
|
||||
protected: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const useUserFilesInfo = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userFilesInfo'],
|
||||
queryFn: getUserFilesInfo,
|
||||
select: (data): FilesInfo | null => {
|
||||
if (!data) {
|
||||
return {
|
||||
total: { size: 0, quantity: 0, check: 0, violation: 0, protected: 0 },
|
||||
images: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
videos: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
audios: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
documents: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
total: {
|
||||
size: data.all_files_size,
|
||||
quantity: data.all_files_quantity,
|
||||
check: data.all_files_check,
|
||||
violation: data.all_files_violation,
|
||||
protected: data.protected_files_count,
|
||||
},
|
||||
images: {
|
||||
quantity: data.images_quantity,
|
||||
size: data.images_size,
|
||||
check: data.images_check,
|
||||
violation: data.images_violations,
|
||||
protected: data.protected_image_files_count,
|
||||
},
|
||||
videos: {
|
||||
quantity: data.videos_quantity,
|
||||
size: data.videos_size,
|
||||
check: data.videos_check,
|
||||
violation: data.videos_violations,
|
||||
protected: data.protected_video_files_count,
|
||||
},
|
||||
audios: {
|
||||
quantity: data.audios_quantity,
|
||||
size: data.audios_size,
|
||||
check: data.audios_check,
|
||||
violation: data.audios_violations,
|
||||
protected: data.protected_audio_files_count,
|
||||
},
|
||||
documents: {
|
||||
quantity: data.document_quantity,
|
||||
size: data.document_size,
|
||||
check: data.document_check,
|
||||
violation: data.document_violations,
|
||||
protected: data.protected_document_files_count,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchPaymentsHistory } from '@/app/actions/yooKassa';
|
||||
|
||||
export interface PaymentsData {
|
||||
amount: number,
|
||||
cancellationReason: string | null,
|
||||
createdAt: string,
|
||||
id: number,
|
||||
operationType: string,
|
||||
paymentUuid: string,
|
||||
status: string,
|
||||
updatedAt: string,
|
||||
}
|
||||
|
||||
|
||||
export interface UserPaymentsData {
|
||||
payments: PaymentsData[],
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const useUserPaymentsData = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userPaymentsData'],
|
||||
queryFn: fetchPaymentsHistory,
|
||||
select: (data): UserPaymentsData => {
|
||||
if (!data) {
|
||||
return {
|
||||
payments: [],
|
||||
error: null
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
refetchInterval: 10000,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getSearchStats } from '@/app/actions/searchActions';
|
||||
|
||||
interface SearchType {
|
||||
count: number,
|
||||
fileType: string,
|
||||
}
|
||||
|
||||
export interface UserSearchData {
|
||||
allCountForPeriod: number;
|
||||
image: SearchType;
|
||||
video: SearchType;
|
||||
audio: SearchType;
|
||||
document: SearchType;
|
||||
}
|
||||
|
||||
export const useUserSearchData = () => {
|
||||
return useQuery({
|
||||
queryKey: ['userSearchData'],
|
||||
queryFn: getSearchStats,
|
||||
select: (data): UserSearchData | null => {
|
||||
if (!data) {
|
||||
return {
|
||||
allCountForPeriod: 0,
|
||||
image: {
|
||||
count: 0,
|
||||
fileType: 'image'
|
||||
},
|
||||
video: {
|
||||
count: 0,
|
||||
fileType: 'video'
|
||||
},
|
||||
audio: {
|
||||
count: 0,
|
||||
fileType: 'audio'
|
||||
},
|
||||
document: {
|
||||
count: 0,
|
||||
fileType: 'document'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchViolationAnalyticStatistic } from '@/app/actions/violationActions';
|
||||
|
||||
export interface UseViolationStatistic {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export const useViolationAnalyticStatistic = (group: 'domain' | 'tld') => {
|
||||
return useQuery({
|
||||
queryKey: ['violationAnalyticStatistic', group],
|
||||
queryFn: () => {
|
||||
return fetchViolationAnalyticStatistic(group)
|
||||
},
|
||||
select: (data: UseViolationStatistic | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchViolationFinalSummaryStatistic } from '@/app/actions/violationActions';
|
||||
|
||||
export interface UseViolationFinalSummaryStatistic {
|
||||
complaints_without_case: number;
|
||||
total_complaints: number;
|
||||
total_law_cases: number;
|
||||
total_violations: number;
|
||||
violations_with_complaint: number;
|
||||
violations_without_complaint: number;
|
||||
}
|
||||
|
||||
export const useViolationFinalSummaryStatistic = () => {
|
||||
return useQuery({
|
||||
queryKey: ['violationFinalSummaryStatistic'],
|
||||
queryFn: () => {
|
||||
return fetchViolationFinalSummaryStatistic()
|
||||
},
|
||||
select: (data: UseViolationFinalSummaryStatistic | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchViolationStatistic } from '@/app/actions/violationActions';
|
||||
|
||||
export interface UseViolationStatistic {
|
||||
total_violations: number,
|
||||
new_violations: number,
|
||||
in_progress_violations: number,
|
||||
resolved_violations: number,
|
||||
total_complaints: number,
|
||||
total_law_cases: number
|
||||
}
|
||||
|
||||
export const useViolationStatistic = (id?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['violationStatistic', id],
|
||||
queryFn: () => {
|
||||
return fetchViolationStatistic(id)
|
||||
},
|
||||
select: (data: UseViolationStatistic | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user