Compare commits

..
375 Commits
Author SHA1 Message Date
smanylov 6696cc1027 set fixed width for document view table 2026-06-08 15:16:08 +07:00
smanylov 98cb5ab09b set location column width 2026-06-08 14:37:02 +07:00
smanylov c501c2bbbf hide template interface 2026-06-08 13:27:30 +07:00
smanylov a74ea966ad hide ViolationsLastCombinedCases 2026-06-08 13:23:13 +07:00
smanylov 0a7d29419f update watch dock info endpoint 2026-06-08 13:17:23 +07:00
smanylov a9599d8a1f add more appeal information for modal window 2026-06-04 14:56:03 +07:00
frontdev cbc1311e9d Merge pull request 'NCBACK-168' (#7) from NCBACK-168 into main
Reviewed-on: #7
2026-06-03 13:23:47 +08:00
smanylov 587d7ceead remove console.log 2026-06-02 12:50:15 +07:00
smanylov ac8ef8caa2 add barear for headers 2026-06-02 11:45:02 +07:00
smanylov 7d0fb265d4 add bearer tokens for functions 2026-06-02 11:39:54 +07:00
smanylov 743db57bb3 refactor tokens 2026-06-01 14:48:13 +07:00
smanylov d7d2e36573 increment version 2026-06-01 12:27:58 +07:00
smanylov 8b157b8e87 fix useViolationStatistic 2026-05-29 22:27:08 +07:00
smanylov 0935e8cbc7 fix send appeals 2026-05-29 13:05:49 +07:00
smanylov f04ce348ae add appeal modal window, add file status 2026-05-28 20:30:16 +07:00
smanylov 508e0d182c fix crush 2026-05-25 16:59:04 +07:00
smanylov dbd5d43d65 add translate for closed status 2026-05-25 15:01:42 +07:00
smanylov 7d8a725e34 cange location layout for history view table 2026-05-25 14:53:09 +07:00
smanylov 491adb4090 fix pagination info for all tanstak-tables 2026-05-25 14:22:41 +07:00
smanylov 1a3f5277d9 fix translate 2026-05-25 13:42:11 +07:00
smanylov ffcd5fc535 fix complaints/violations/matches statistick 2026-05-25 13:40:49 +07:00
smanylov 65e0c2ed62 fix expired tariff date 2026-05-22 11:58:42 +07:00
smanylov 36b4a3b481 add start-end-date range for match list and fix pagination 2026-05-21 18:20:50 +07:00
smanylov e0455b200a conect endpoints for FilePpageFileStatistic 2026-05-20 11:59:52 +07:00
smanylov 348a5398d0 change FinalSummary numbers 2026-05-20 11:47:34 +07:00
smanylov af87bfc7b6 fix crush 2026-05-20 11:21:24 +07:00
smanylov beefe86ffd change notification badge size 2026-05-20 11:17:36 +07:00
smanylov 3a7c609588 remove pagination-info-files 2026-05-20 11:09:15 +07:00
smanylov aeeee04166 fix pagination 2026-05-18 12:05:12 +07:00
smanylov 288d6782a4 remove console.log 2026-05-18 11:55:16 +07:00
smanylov 55915af9df rename function 2026-05-18 11:51:56 +07:00
smanylov bfc457470c increment version 2026-05-16 16:56:59 +07:00
smanylov 210145f5ca fix sorting for document view 2026-05-16 16:56:31 +07:00
smanylov d0793a281e change dropdown on link for dashboard user files 2026-05-14 15:22:35 +07:00
smanylov 6994edd216 add locale for document history view 2026-05-14 14:45:49 +07:00
smanylov 3a0ae93846 increment version 2026-05-14 14:30:53 +07:00
smanylov 13a5db6313 add notification context 2026-05-14 14:30:20 +07:00
smanylov 9c011b6416 rename opened document history 2026-05-13 12:15:11 +07:00
smanylov 294e16e1a6 hide select button for notification when notification list is null 2026-05-13 10:57:20 +07:00
smanylov 71f7120239 rename session 2026-05-12 11:50:35 +07:00
smanylov 0e28986dc7 rename functions 2026-05-11 14:10:42 +07:00
smanylov 51a3ed7d18 fix translate 2026-05-11 13:49:29 +07:00
smanylov c15867a41c fix complaint view bug 2026-05-11 13:37:06 +07:00
smanylov c4e4d775f5 fix some crashes 2026-05-08 15:48:26 +07:00
smanylov cd07502128 add translate 2026-05-08 15:37:25 +07:00
smanylov 1ef2cc227a remov olden that month filter for violation table 2026-05-08 15:31:41 +07:00
smanylov 7226285746 change swiper pagination block 2026-05-08 14:30:06 +07:00
smanylov dd93cd302c add space for translate 2026-05-08 14:13:02 +07:00
smanylov 37b7ccb52e fix loading 2026-05-08 13:57:55 +07:00
smanylov b8a76bd6a9 add file download for tracking page 2026-05-08 13:39:37 +07:00
smanylov 07e1237f05 add tracking statistic 2026-05-07 12:06:02 +07:00
smanylov 29546740c7 add styles for filtres 2026-05-06 19:17:47 +07:00
smanylov 42cc96a28a remove log 2026-05-06 19:13:48 +07:00
smanylov 84b07af246 fix pagination 2026-05-06 19:12:37 +07:00
smanylov 8a72dbaf68 add TrackingHistoryView 2026-05-06 19:03:29 +07:00
smanylov af6b997ceb increment version 2026-05-05 16:07:54 +07:00
smanylov e97dd392c0 fix download and download name for tracking document 2026-05-05 16:01:08 +07:00
smanylov ae0526a58a change notification count number rules 2026-05-05 15:30:26 +07:00
smanylov d419c1c2cb add permission change button 2026-05-05 13:07:52 +07:00
smanylov 48e03b4237 increment version 2026-05-04 20:31:31 +07:00
smanylov 2fad519b7e add file info fro document view 2026-05-04 20:31:15 +07:00
smanylov 84b73b912f increment version 2026-05-01 15:13:03 +07:00
smanylov a3cbaaa2e7 add document viewer 2026-05-01 15:12:00 +07:00
smanylov 881f3295c0 change bank card numer collor 2026-04-30 14:34:16 +07:00
smanylov 0cc9230672 add save text for claims/complaints form while validation error 2026-04-30 14:27:56 +07:00
smanylov 010a8f3a5a rename files 2026-04-30 13:39:22 +07:00
smanylov 4952e0daa1 fix tabs switch bag for matches table 2026-04-30 13:37:01 +07:00
smanylov 455b39bb1e fix complaint/claims validation 2026-04-30 12:06:31 +07:00
smanylov 34772bd3b4 add check_similar_first for global search 2026-04-30 11:59:06 +07:00
smanylov 2296044280 fix translate and change styles for cases carusel 2026-04-30 11:35:03 +07:00
smanylov 2d201bfcde add variable for styles 2026-04-29 17:23:59 +07:00
smanylov 17931191f5 fix matches table styles 2026-04-29 17:18:25 +07:00
smanylov 4e8289e976 add modal window for save compaint text template 2026-04-29 17:09:05 +07:00
smanylov 41c56e026f add max lenght validation for create complaint/claim 2026-04-29 15:40:34 +07:00
smanylov 1e8bf782ca remove date column add claims and complaint columns 2026-04-29 15:20:57 +07:00
smanylov e4eedf464b increment version 2026-04-29 15:15:26 +07:00
smanylov c274cb134d rework violation table and violation actions 2026-04-29 15:15:04 +07:00
smanylov c0fe6cce48 add tacking page 2026-04-28 16:32:23 +07:00
smanylov 9e89a37b4d fix violations complains/claims table syles 2026-04-28 13:32:09 +07:00
smanylov a704e410c5 increment version 2026-04-28 13:09:06 +07:00
smanylov d24884f7e6 add final summary block 2026-04-28 13:08:35 +07:00
smanylov 2cada9ba27 add document view layout 2026-04-27 17:29:16 +07:00
smanylov a7c203d7a4 fix file verification upload 2026-04-27 14:58:54 +07:00
smanylov abc07a5e2f hide AUTHORIZED_USE and NOT_OWNER_FILE button while match in work 2026-04-27 13:33:02 +07:00
smanylov 904c615665 change notification string 2026-04-27 12:48:09 +07:00
smanylov e9af59769d change collor for not my file button 2026-04-25 14:15:20 +07:00
smanylov e933d64790 add not my file button 2026-04-25 13:53:30 +07:00
smanylov c07fe860e5 increment version 2026-04-25 13:19:14 +07:00
smanylov 1b6dd3c40b fix document size infro remove violation infro frim info block 2026-04-25 13:17:47 +07:00
smanylov 7c82f940fc rework notifications list 2026-04-24 14:30:03 +07:00
smanylov a69636d7e3 remove console.log 2026-04-24 12:12:16 +07:00
smanylov 83f535c2e9 increment version 2026-04-24 12:09:31 +07:00
smanylov 47bf0b3e77 add preview for images in tanstak-table 2026-04-24 12:08:43 +07:00
smanylov e09cd3a77c increment version 2026-04-23 18:08:36 +07:00
smanylov 5d521358f7 add two tanstack tables for claims and complaints 2026-04-23 18:08:11 +07:00
smanylov cfb2f42e96 remove statistick block 2026-04-23 15:12:39 +07:00
smanylov cbbfb1c6ad add pagination for last cases lists 2026-04-23 12:55:21 +07:00
smanylov 86263bd235 change color, fix date for claims 2026-04-23 12:43:27 +07:00
smanylov fc9dc836e7 fix violation table styles 2026-04-23 11:42:54 +07:00
smanylov a508ede373 increment version 2026-04-22 14:10:39 +07:00
smanylov fd4da24b1d fix type bug 2026-04-22 14:09:38 +07:00
smanylov 2f0acfc07f add relocate from violations-last-combined-cases to match-list 2026-04-22 14:05:55 +07:00
smanylov f8cf3618f7 combine complaint and cases 2026-04-21 18:55:27 +07:00
smanylov 0ad1ce98ad change translates 2026-04-21 13:11:51 +07:00
smanylov 04c27a9b7f add cutFileName for match table, fix some styles 2026-04-21 12:58:24 +07:00
smanylov 12fb7104a1 fix loading bug 2026-04-20 18:46:29 +07:00
smanylov f5548f39da add user status translate + fix notification count-icon size while long numbers 2026-04-20 18:34:27 +07:00
smanylov b16d0949a4 remove file page note 2026-04-20 18:04:49 +07:00
smanylov 5dfc3573e5 fix translate add intends 2026-04-20 17:48:08 +07:00
smanylov 7800499c72 add more monitoring info 2026-04-20 17:36:33 +07:00
smanylov 045eef24c0 add real info for dashboard-last-check 2026-04-20 16:48:15 +07:00
smanylov 3a8a1f9c08 increment version 2026-04-20 16:08:50 +07:00
smanylov f55f4cedfe add complaints\claims blocks 2026-04-20 16:08:10 +07:00
smanylov 85b77aeb91 update matched table styles, add loading animation for some blocks 2026-04-18 14:25:20 +07:00
smanylov 1b4ad64ef6 change colors and styles for complaint/case form 2026-04-17 18:23:13 +07:00
smanylov c7939b8923 fix button disable for search page 2026-04-17 17:10:17 +07:00
smanylov a155a90fba increment version 2026-04-17 14:36:22 +07:00
smanylov 31d9307fe1 fix sorting and add opacity while sorting 2026-04-17 14:36:04 +07:00
smanylov 86f56b4735 disable a select file while global serch 2026-04-17 13:41:05 +07:00
smanylov 1f44211088 add verefid check for complain/case 2026-04-17 13:13:27 +07:00
smanylov fcc1f3bec9 add code blank for last case/complaint 2026-04-16 17:34:14 +07:00
smanylov cbf30604af add gap for notification icon 2026-04-16 17:08:35 +07:00
smanylov 839dd8cf94 change match table styles and notification styles 2026-04-16 17:07:14 +07:00
smanylov 365aeded43 remove utm marks 2026-04-16 12:40:15 +07:00
smanylov ad247fb269 add hover effect for tokens button 2026-04-15 19:48:24 +07:00
smanylov 1e0267c03d add br for source url 2026-04-15 19:44:22 +07:00
smanylov 47a16c0c2b more styles change for matched table 2026-04-15 19:43:29 +07:00
smanylov 6dcf3de29e change matched table styles 2026-04-15 19:36:48 +07:00
smanylov 8974d00078 fix complaint/lega render 2026-04-15 15:55:54 +07:00
smanylov d0263e7252 fix string for legal/complaint button 2026-04-15 15:48:34 +07:00
smanylov 0d2cb18745 change notification styles 2026-04-15 15:31:01 +07:00
smanylov 00084d769a version increment 2026-04-15 13:33:32 +07:00
smanylov c1271d9c34 change resolve button color 2026-04-15 13:31:49 +07:00
smanylov 4d957ea271 change matched table styles 2026-04-15 13:30:19 +07:00
smanylov 9b4fb796f9 change legal case title styles 2026-04-14 16:06:24 +07:00
smanylov c4aefdb8b9 change matched table layout 2026-04-14 16:01:49 +07:00
smanylov 35b7f82ad9 continue: work on match table 2026-04-14 12:50:52 +07:00
smanylov 25ad9b0350 rename 2026-04-14 10:14:47 +07:00
smanylov f22ea522b8 show userverifiation when verification status === null 2026-04-14 09:20:02 +07:00
smanylov 9f317d9878 add status for user verification 2026-04-14 09:15:05 +07:00
smanylov aaa46e7d3a add user verified status hint 2026-04-13 15:36:43 +07:00
smanylov 11692c8537 fix verification upload 2026-04-13 15:32:42 +07:00
smanylov d0901961be rework notification-table 2026-04-13 15:10:24 +07:00
smanylov d8d1eec3ed add token for private chankUpload 2026-04-13 14:17:19 +07:00
smanylov 0182616050 continue: change verification upload section for corret upload last file and mark last chank 2026-04-13 13:44:30 +07:00
smanylov 8da7056cd6 fix notification count icon 2026-04-13 11:48:22 +07:00
smanylov 50d2497288 fix string 2026-04-10 21:01:04 +07:00
smanylov 02b378612b unable to select a readed notification 2026-04-10 16:31:22 +07:00
smanylov 90903a67ac add stopPropagation for user menu dropdown 2026-04-10 16:16:20 +07:00
smanylov a551893322 remove valid color for bank card input when input empty 2026-04-10 15:15:14 +07:00
smanylov ea9d57cc65 change sortDirection for nitification 2026-04-10 13:10:22 +07:00
smanylov e141545035 remove loading procent while error, add circle progres bar 2026-04-10 12:17:50 +07:00
smanylov 62cd597994 add animations 2026-04-09 16:11:57 +07:00
smanylov 1eff2d2c0a change selected files translate and add count 2026-04-09 15:24:37 +07:00
smanylov 2af7c12cf6 fix payout error handler 2026-04-09 14:45:20 +07:00
smanylov 3a6034a460 add more accurate error handling 2026-04-09 14:34:48 +07:00
smanylov 9c9bf201c4 increase font size for payment type fro bank card list 2026-04-09 14:10:38 +07:00
smanylov cb8389bb4a version increment 2026-04-09 13:51:42 +07:00
smanylov 77ad126530 fix sorting for match table 2026-04-09 13:50:18 +07:00
smanylov e152013e2a fix image resize for matched list 2026-04-08 16:54:48 +07:00
smanylov 0c3ccb2806 matches: add animation, add suspense for table, add no-image image 2026-04-08 13:41:56 +07:00
smanylov eb2615c303 remove logs 2026-04-08 11:47:11 +07:00
smanylov 29b6120f80 increment version 2026-04-08 11:36:30 +07:00
smanylov 90315a112a add translate for notifications 2026-04-08 11:35:30 +07:00
smanylov 50f45a133b continue: create case 2026-04-07 19:05:44 +07:00
smanylov c57685065a add translate, add remove notification function 2026-04-07 12:45:09 +07:00
smanylov 8048c0ff0e fix build 2026-04-06 17:15:24 +07:00
smanylov 10c2ca5bc3 add comments 2026-04-06 16:25:26 +07:00
smanylov f688aa210f change styles 2026-04-06 13:37:56 +07:00
smanylov b377c11c41 finish notifications featur 2026-04-06 13:02:38 +07:00
smanylov 1fe49c24e8 continue: add pagination for notification page and some featur 2026-04-05 15:06:08 +07:00
smanylov 27ca7ddf8e finished header notifications 2026-04-04 16:14:56 +07:00
smanylov b6c5278282 change sorting for payment-tab-transaction-history 2026-04-03 15:05:46 +07:00
smanylov 6cf7bd766d continue: made a payments method list 2026-04-03 14:52:31 +07:00
smanylov e75fcd7bff version increment 2026-04-03 11:08:46 +07:00
smanylov eb14c5cb21 fix translate 2026-04-03 11:08:26 +07:00
smanylov 0097f768ef add payments method list 2026-04-02 17:59:47 +07:00
smanylov d6dee3361e continue: user verification 2026-04-02 16:46:50 +07:00
smanylov 9b83ba3176 add error handler for payment request 2026-04-02 11:29:15 +07:00
smanylov 2878f546ee set header max-width 2026-04-02 11:14:27 +07:00
smanylov e71b65d593 add VerificationUploadSection 2026-04-01 16:15:20 +07:00
smanylov 1d5925e178 tanstak-table: add select all, change listner area for select checkbox 2026-04-01 13:34:50 +07:00
smanylov 45c5137565 change value for cutFuleName function 2026-04-01 12:47:17 +07:00
smanylov 3544b2af81 change activity by content info type 2026-04-01 12:25:01 +07:00
smanylov ea003ea73c remove logs 2026-04-01 12:09:38 +07:00
smanylov dd483cb628 add monitoring stats fetch 2026-04-01 11:24:47 +07:00
smanylov b89e5b4c6b set fixed width for tariff card 2026-03-31 18:50:46 +07:00
smanylov a09ce94da1 change styles for tariffs table 2026-03-31 18:40:40 +07:00
smanylov 1256334805 fix blink bug for violations check block 2026-03-31 18:04:53 +07:00
smanylov 5ff450c3bc change total violation number 2026-03-31 17:58:21 +07:00
smanylov 8bb6097d90 continue: change styles for matched table, add new block for empty select 2026-03-31 17:26:58 +07:00
smanylov 89506c222b change pagination styles for file matches table 2026-03-31 15:35:16 +07:00
smanylov 0887ccde13 add text area validation 2026-03-31 14:47:39 +07:00
smanylov d68a35903c add mark as read button for notification table 2026-03-31 14:00:51 +07:00
smanylov b05661e120 increment version 2026-03-31 13:49:46 +07:00
smanylov d40e3d563b continue: add notification page styles 2026-03-31 13:47:27 +07:00
smanylov f31713a929 fix padding for notifications 2026-03-30 19:17:29 +07:00
smanylov f4b0a63e23 add notification list, add notification page 2026-03-30 19:11:48 +07:00
smanylov 884fb8dc50 change refetch time for userPaymentsData 2026-03-30 14:51:48 +07:00
smanylov 4abcc9689c fix traslate, fix filter bug for file matches tables 2026-03-30 12:53:46 +07:00
smanylov 154e9cc5c1 add multiselect for files tanstak-table 2026-03-30 12:48:31 +07:00
smanylov 534e18bbd4 add spentBalance 2026-03-27 22:31:34 +07:00
smanylov 0df34bdb1c add translate, remove avaible income number 2026-03-27 21:17:19 +07:00
smanylov 11d65410e6 add dropDown filter for matched list 2026-03-27 17:22:09 +07:00
smanylov f4a0222e0e update useFileViolations 2026-03-27 16:45:58 +07:00
smanylov 022943d2af change earning summ 2026-03-27 15:58:45 +07:00
smanylov 3a84394847 add translate for status from payment history 2026-03-27 15:40:02 +07:00
smanylov 70717f88fb chage marking page text 2026-03-27 15:27:22 +07:00
smanylov 95319e51ff remove comments 2026-03-27 15:25:30 +07:00
smanylov 6a1a3adce5 edit faq text 2026-03-27 15:23:45 +07:00
smanylov aff0b7a080 update work with marched files 2026-03-27 15:10:06 +07:00
smanylov 9128c0e298 add resolved button for matched file 2026-03-27 13:50:41 +07:00
smanylov 6453e8402e add translate and colors for matched status 2026-03-27 12:53:52 +07:00
smanylov 7ae0fecb61 add complain registration, add view complaint inforation, add status change for marches when user click on mat 2026-03-26 17:30:30 +07:00
smanylov c9f230a76f fix payout number 2026-03-26 12:09:37 +07:00
smanylov 17692d6497 rename filePageNotFound 2026-03-26 11:57:42 +07:00
smanylov a9861967ef refactor for file page 2026-03-26 11:56:36 +07:00
smanylov 4d5bae0812 increment version 2026-03-25 16:48:38 +07:00
smanylov 845a3b479d update tariff plan for annual 2026-03-25 16:41:32 +07:00
smanylov 010c39e869 remove payment method drop down from refferal settings 2026-03-25 12:31:38 +07:00
smanylov e9c2e2f0aa fix company user registration 2026-03-25 12:22:20 +07:00
smanylov cbb01de3a2 update registration 2026-03-25 12:17:33 +07:00
smanylov 6ed75bd08c change syles for secure-payment 2026-03-25 11:14:04 +07:00
smanylov 3c73dc40dc fix styles for payment tab 2026-03-25 10:53:15 +07:00
smanylov 4986455919 fix bank-card validation bug 2026-03-24 21:55:43 +07:00
smanylov f4b4f4bafb change list names for marking drop-down 2026-03-24 18:27:16 +07:00
smanylov 2982741558 remove save button from user settings 2026-03-24 18:22:28 +07:00
smanylov 18e700bb6e add search setting interface 2026-03-24 18:14:08 +07:00
smanylov 0955d66931 fix layout for small resolution 2026-03-23 17:00:00 +07:00
smanylov 39c3760161 increase gap for local search 2026-03-23 14:13:57 +07:00
smanylov b5ec4ccbcd continue: change local check button design 2026-03-23 14:07:44 +07:00
smanylov c5c4687063 remove icon from violation page title 2026-03-23 12:52:04 +07:00
smanylov b7865acbe3 take user agent and user ip for NCFRONT-166 2026-03-23 12:45:05 +07:00
smanylov 3a3a514825 update search logic 2026-03-20 21:37:51 +07:00
smanylov 7cdcb3a69a increment version 2026-03-20 21:03:25 +07:00
smanylov d7899d0dd5 remove RecentSearches and change styles for serachStat block 2026-03-20 21:02:22 +07:00
smanylov 88a383f85e add documents for all users 2026-03-20 20:42:52 +07:00
smanylov e5ed1e213d update prefetchQuery 2026-03-20 20:31:26 +07:00
smanylov 46ee8ac4a6 remove comment 2026-03-20 20:17:14 +07:00
smanylov 5ea3823946 add debounce for tanstak table 2026-03-20 20:07:50 +07:00
smanylov 9d90c81f64 update tanstack-table 2026-03-20 19:45:18 +07:00
smanylov 38bfc94125 increase opacity for disabled inputs 2026-03-20 14:13:13 +07:00
smanylov 261d1300e7 fix faq issues 2026-03-20 13:01:01 +07:00
smanylov e0fde479ad fix payments save method 2026-03-20 12:55:41 +07:00
smanylov 1af861aa94 fix issues for bank card input 2026-03-20 12:35:52 +07:00
smanylov ad76b06e8a fix issues for payment requst 2026-03-20 12:24:01 +07:00
smanylov 75bab70856 update yookasa payment save method 2026-03-19 18:11:33 +07:00
smanylov f65513b1cb update dashboard statisctic 2026-03-19 16:15:18 +07:00
smanylov ba0b47bec5 increment version 2026-03-19 12:51:57 +07:00
smanylov f2428d7fde update general statistic and analytic 2026-03-19 12:49:46 +07:00
smanylov 07eb00c705 rename file 2026-03-18 19:07:32 +07:00
smanylov 9907213dd8 add token for fetch violation list 2026-03-18 18:09:33 +07:00
smanylov 89e288c591 update report page 2026-03-18 15:21:49 +07:00
smanylov 75874ab46f change violatiuon note place 2026-03-17 18:59:04 +07:00
smanylov 333dd5d3c5 continue: add violation panel 2026-03-17 18:41:02 +07:00
smanylov 949038979e remove pagination blink 2026-03-17 12:17:26 +07:00
smanylov c61598010c remove buffer file delete whe user change route 2026-03-17 11:49:57 +07:00
smanylov f56e6ba6d1 remove logs 2026-03-16 17:34:41 +07:00
smanylov 13ac3a6543 continue: work on violation page and table 2026-03-16 17:12:52 +07:00
smanylov c3de5727fe return yookassa logo 2026-03-16 12:18:15 +07:00
smanylov 2774b7eab6 change payment method text, change resolution view for global search item 2026-03-14 14:01:39 +07:00
smanylov d54a8bfe60 add refetchInterval for user payments data 2026-03-14 11:56:08 +07:00
smanylov 8464616c37 change monitoring not supported message 2026-03-14 11:52:13 +07:00
smanylov be06d7b833 fix message string for header search 2026-03-14 10:52:04 +07:00
smanylov f7879485ef add validation message for back card input 2026-03-14 10:49:07 +07:00
smanylov 00c4f4062d add local file serach in to header menu 2026-03-13 17:29:52 +07:00
smanylov 8c52839c02 continue: work on violation page 2026-03-13 13:42:07 +07:00
smanylov 7a1b45895f add violation tanstak-table 2026-03-12 17:44:02 +07:00
smanylov d36b724d4f add bank card validation 2026-03-12 13:06:03 +07:00
smanylov 6e648a522a version increment 2026-03-11 18:28:06 +07:00
smanylov 8b56e3da21 rename operation type 2026-03-11 18:27:38 +07:00
smanylov 9fbb74ba60 add new yandex auth 2026-03-11 18:21:19 +07:00
smanylov e8f4f9939f redisign tokens paytment 2026-03-11 12:21:56 +07:00
smanylov fa60a79241 return marking-document pag for d2c 2026-03-11 10:53:09 +07:00
smanylov 413daf37d6 add favicon 2026-03-10 16:20:54 +07:00
smanylov e24b0b1dc8 version increment 2026-03-10 16:18:44 +07:00
smanylov df81c1e232 add faq contact 2026-03-10 16:17:55 +07:00
smanylov 5bc74d874c add frequently-asked-questions 2026-03-10 15:03:36 +07:00
smanylov 98f0571d1f remove logs, add readmy 2026-03-10 12:34:37 +07:00
smanylov 04ff892e86 fix file download for search list 2026-03-10 10:59:25 +07:00
smanylov 83f644cb03 start making FAQ page 2026-03-09 15:05:38 +07:00
smanylov 0c03bb9254 remove monitoring from files that are not image 2026-03-09 14:38:52 +07:00
smanylov 1280505297 version increment 2026-03-09 14:17:06 +07:00
smanylov 1efdb06d3b add input mask for phone in user settings page 2026-03-09 14:16:26 +07:00
smanylov 88c356bdf2 increment verison 2026-03-09 13:04:49 +07:00
smanylov 060b684e57 update download function 2026-03-09 13:04:28 +07:00
smanylov c739f2e7b1 monitoring status change refactor 2026-03-06 15:02:24 +07:00
smanylov 7f85d64167 increment version 2026-03-06 14:55:30 +07:00
smanylov 6873daa5de connect monitoring endpoints 2026-03-06 14:53:51 +07:00
smanylov 1a17683897 add logs 2026-03-06 14:46:42 +07:00
smanylov 5a6dcf33fa fix convert number to string for payments 2026-03-06 14:08:04 +07:00
smanylov 57ceddd9ee change file-info modal window styles 2026-03-05 17:37:53 +07:00
smanylov b79d8b8879 add viewport calculate for fileName cut 2026-03-05 16:44:33 +07:00
smanylov 6d6d0e102b fix resend button blink width 2026-03-05 12:10:14 +07:00
smanylov 33f5c15e45 return mail confirm 2026-03-05 10:43:57 +07:00
smanylov 640f167e4b add pice check for file upload 2026-03-04 19:21:44 +07:00
smanylov 0351c561b5 fix icon size for small montior resolutions 2026-03-04 17:47:51 +07:00
smanylov b8c439d2d7 add error message handler, add request animation 2026-03-04 17:16:30 +07:00
smanylov a11abdbb56 change modal-window for file-info style 2026-03-04 16:15:21 +07:00
smanylov 617984e587 add comment 2026-03-04 15:22:26 +07:00
smanylov b1f731fdde remove mail confirm from registartion temporary 2026-03-04 15:21:04 +07:00
smanylov 9899741c00 complite yooMoneyWidged and payments 2026-03-04 11:55:36 +07:00
smanylov a53c1b4b49 add YooMoneyWidget, change payments handler 2026-03-03 18:48:09 +07:00
smanylov 3e3a5df34d add error message for unsupported file 2026-03-03 13:25:26 +07:00
smanylov 2227a842ae add retry button for global search while error 2026-03-03 11:57:18 +07:00
smanylov 3b1509edf0 version increment 2026-03-03 11:28:16 +07:00
smanylov 72be27e3c1 edit mail confirm translate and style 2026-03-03 11:27:19 +07:00
smanylov 35f74791c9 reduce tanstak table uodate time 2026-03-03 11:17:41 +07:00
smanylov 913a199d95 change text 2026-03-02 19:01:08 +07:00
smanylov 90514e2327 add violation details page layout 2026-03-02 18:57:52 +07:00
smanylov 3e0f7f077a remove raw date value from tanstak-table 2026-03-02 14:41:39 +07:00
smanylov 8cea6ccfbb fix search for all files 2026-03-02 14:23:36 +07:00
smanylov c66ee30940 remove limits section block 2026-02-28 11:37:26 +07:00
smanylov 60427dbb4f change translate for resend mail code 2026-02-28 11:00:21 +07:00
smanylov a24ccd9cbe increment version 2026-02-27 14:51:29 +07:00
smanylov e547c561e3 remove last activity block 2026-02-27 14:51:07 +07:00
smanylov 1b878faf2b remove last activity block from setting page 2026-02-27 14:28:40 +07:00
smanylov 10c3a2326b fix mail confirm work 2026-02-27 14:18:14 +07:00
smanylov 55acfe7efe remove search count from global search 2026-02-27 12:25:08 +07:00
smanylov c4272bf1fe add translates 2026-02-27 12:03:46 +07:00
smanylov 2e2d8e897b add time for searched user files list in modal window 2026-02-27 11:38:13 +07:00
smanylov 18716cf97f version increment 2026-02-27 11:22:22 +07:00
smanylov 1a7bad29d1 add validation message while inn alredy registred 2026-02-27 11:22:00 +07:00
smanylov 3b3e18f22d remove date, block global search for audio, video and documents 2026-02-26 19:32:57 +07:00
smanylov d2161fe221 add translate 2026-02-26 13:29:50 +07:00
smanylov 4895dcbcc4 add style for succeeded status and translate for referral status 2026-02-26 13:07:32 +07:00
smanylov 6990f78f69 add type interface, fix date formate, add translation for tokens error 2026-02-26 12:53:27 +07:00
smanylov 64d606a791 decrease font size for build version/date 2026-02-26 11:58:30 +07:00
smanylov 3b72aaa7c2 add toast for payments method save 2026-02-26 11:49:32 +07:00
smanylov de7199a3df add convertTo for fileUpload 2026-02-25 20:04:34 +07:00
smanylov c8f2233e24 add invalidateQueries for payoutMethods 2026-02-25 18:32:42 +07:00
smanylov d18071e533 edit referrals actions 2026-02-25 18:17:08 +07:00
smanylov bc1d5d5337 edit payments history 2026-02-25 12:53:18 +07:00
smanylov 9da0a095cb add tanstak table for paymenrs history(mock-data) 2026-02-24 18:01:19 +07:00
smanylov 92bd3fdcdb add availableIncome and totalIncome info 2026-02-24 17:47:50 +07:00
smanylov 021ca69e6b refactor 2026-02-24 17:41:50 +07:00
smanylov 454ec6f865 add PayoutMethod, getReferralPayments,
add translates
2026-02-24 17:22:01 +07:00
smanylov 97b24cfadf change refill amount 2026-02-24 11:05:00 +07:00
smanylov 332788a77a remove preview for all file except images 2026-02-23 14:44:22 +07:00
smanylov a33ccf4180 change styles and remove research button 2026-02-23 13:53:36 +07:00
smanylov 1b9b843f1a add research button and translate 2026-02-23 13:41:07 +07:00
smanylov 82e27a0ce4 add formate date for file info 2026-02-23 12:53:06 +07:00
smanylov a3ac3f6535 add refetch for tanstak table files-list 2026-02-23 12:36:54 +07:00
smanylov c1d5f9eebf add frontend version to nav-bar 2026-02-23 12:20:25 +07:00
smanylov 70d6dea264 increment version 2026-02-23 12:09:25 +07:00
smanylov 891c3325c9 remove double space while registration 2026-02-23 12:07:42 +07:00
smanylov 9709b938f2 add new error message for upload .wav file 2026-02-21 22:11:43 +07:00
smanylov 79e3a5b902 remove mail verified from company user registration 2026-02-21 20:21:39 +07:00
smanylov 6248909003 add translates 2026-02-21 15:13:26 +07:00
smanylov c88646b64c add payments history tanstak-table 2026-02-21 15:07:37 +07:00
smanylov 4f7b464bed add tanstakQuery clear for login form 2026-02-20 17:09:26 +07:00
smanylov 9d03968bc1 add queryClient.clear() for logout 2026-02-20 16:55:44 +07:00
smanylov 9ab78a55c3 remove console.log 2026-02-20 15:40:45 +07:00
smanylov 9f276657c3 edit filter styles 2026-02-20 15:13:52 +07:00
smanylov aeb61936ce add dropdown monitoring 2026-02-20 14:55:23 +07:00
smanylov a3ea25c1e6 add new folder for tanstak-table 2026-02-20 12:52:18 +07:00
smanylov 13d68f93d9 update package-lock 2026-02-19 19:05:50 +07:00
smanylov 81c31547ac add document for tanstak-table filter 2026-02-19 18:49:41 +07:00
smanylov b5d53e0831 do refCode save after register form validation 2026-02-19 18:37:49 +07:00
smanylov 22bc8fdcbe change ReportsInfo styles 2026-02-19 18:14:39 +07:00
smanylov c7c78e303c add tarif operation 2026-02-19 17:45:40 +07:00
smanylov 048e1269ad add tokens bundle payments 2026-02-19 17:23:52 +07:00
smanylov 13128d282b remove spaces from payment value 2026-02-19 11:20:47 +07:00
smanylov 498424cf27 add mail_verified true for registration 2026-02-19 10:47:03 +07:00
smanylov 67bf12c11f add color for active monitoring button 2026-02-18 18:29:21 +07:00
smanylov c658721333 add monitoring action button for tanstak-table 2026-02-18 18:22:11 +07:00
smanylov 906b3a554c add health check 2026-02-18 14:21:57 +07:00
smanylov f4a13e2a8e return functio after merge 2026-02-17 19:20:04 +07:00
smanylov 1f61ba7edd remove dev button 2026-02-17 18:44:24 +07:00
frontdev 38b262a3f0 Merge pull request 'NCFRONT-72' (#4) from NCFRONT-72 into main
Reviewed-on: #4
2026-02-17 19:33:05 +08:00
smanylov 868ed7ca7e set error message work correct 2026-02-17 18:32:45 +07:00
smanylov 7e6760e769 Merge branch 'main' into NCFRONT-72 2026-02-17 18:20:50 +07:00
smanylov bf9926f004 update confirm email code window 2026-01-26 13:04:13 +07:00
smanylov 4166cb1d80 refactor confirm window 2026-01-23 18:06:08 +07:00
smanylov 396d3888db add confirm mail modal window 2026-01-23 17:33:48 +07:00
186 changed files with 21626 additions and 3854 deletions
+8
View File
@@ -34,3 +34,11 @@ docker network inspect no-copy_app-network
docker volume ls - посмотреть все базы данных docker volume ls - посмотреть все базы данных
docker volume prune - удалить неиспользуемые базы данных docker volume prune - удалить неиспользуемые базы данных
Юкаса
эндпоинт для бека.
{URL}/api/payments/webhook/yookassa
https://dev-workspace.not-copy.com/api/payments/webhook/yookassa
https://mutely-profound-sandfish.cloudpub.ru/api/payments/webhook/yookassa
+4
View File
@@ -1,5 +1,6 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin'; import createNextIntlPlugin from 'next-intl/plugin';
const { version } = require('./package.json');
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
experimental: { experimental: {
@@ -7,6 +8,9 @@ const nextConfig: NextConfig = {
bodySizeLimit: '10mb', bodySizeLimit: '10mb',
}, },
}, },
env: {
APP_VERSION: version,
},
}; };
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
+1021 -732
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.42.0", "version": "0.122.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
@@ -16,11 +16,13 @@
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2", "@vkid/sdk": "^2.6.2",
"card-validator": "^10.0.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8", "next-intl": "^4.5.8",
"pdfjs-dist": "^5.7.284",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-slick": "^0.31.0", "react-slick": "^0.31.0",
@@ -40,6 +42,7 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.3", "eslint-config-next": "16.0.3",
"sass": "^1.94.2", "sass": "^1.94.2",
"types-yoomoneycheckoutwidget": "^0.0.2",
"typescript": "^5" "typescript": "^5"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+5 -3
View File
@@ -1,9 +1,11 @@
import Providers from '@/app/providers/getQueryServer'
import type { Metadata } from "next"; import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl'; import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing'; import { routing } from '@/i18n/routing';
import { ToastProvider } from '@/app/providers/ToastProvider'; import { ToastProvider } from '@/app/providers/ToastProvider';
import Providers from '@/app/providers/getQueryServer';
import { Suspense } from 'react';
import "../styles/globals.css"; import "../styles/globals.css";
import "../styles/global-styles.scss"; import "../styles/global-styles.scss";
@@ -29,9 +31,9 @@ export default async function RootLayout({
} }
return ( return (
<html lang={locale}> <html>
<body> <body>
<NextIntlClientProvider> <NextIntlClientProvider locale={locale}>
<Providers> <Providers>
{children} {children}
<ToastProvider /> <ToastProvider />
+53
View File
@@ -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>
)
}
+17 -2
View File
@@ -7,6 +7,9 @@ import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import VKLogin from '@/app/components/VKLogin'; import VKLogin from '@/app/components/VKLogin';
import YandexAuthWithScript from '@/app/components/YandexLogin'; 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 = { export const metadata: Metadata = {
title: 'Login', title: 'Login',
@@ -15,9 +18,20 @@ export const metadata: Metadata = {
export default function Page() { export default function Page() {
const t = useTranslations('Login-register-form'); const t = useTranslations('Login-register-form');
async function HealthData() {
const data = await getHealt();
if (!data) {
return <div>Сервер временно не работает</div>
} else {
return null
}
}
return ( return (
<Suspense fallback={<div>Loading...</div>}>
<HealthData />
<div className={`${styles['login-container-wrapper']}`}> <div className={`${styles['login-container-wrapper']}`}>
{/* <div className={`${styles['login-container-language']}`}> {/* <div className={`${styles['login-container-language']}`}>
<LanguageSwitcher /> <LanguageSwitcher />
</div> */} </div> */}
<div className={`${styles['login-container']}`}> <div className={`${styles['login-container']}`}>
@@ -38,7 +52,7 @@ export default function Page() {
<VKLogin /> <VKLogin />
</div> </div>
<div> <div>
<YandexAuthWithScript /> <YandexLoginButton />
</div> </div>
<div className={`${styles['register-link']}`}> <div className={`${styles['register-link']}`}>
<p>{t('no-account')}? <p>{t('no-account')}?
@@ -67,5 +81,6 @@ export default function Page() {
</div> </div>
</div> </div>
</div> </div>
</Suspense>
); );
} }
+7 -4
View File
@@ -10,7 +10,7 @@ import DashboardFilesInfo from '@/app/ui/dashboard/new/dashboard-files-info';
import DahboardLimitsSection from '@/app/ui/dashboard/new/dashborad-limits-section'; import DahboardLimitsSection from '@/app/ui/dashboard/new/dashborad-limits-section';
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section'; import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check'; import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check';
import ViolationsCarousel from '@/app/ui/dashboard/new/violations-carousel'; import DashboardUserViolations from '@/app/ui/dashboard/new/dashboard-user-violations';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Dashboard', title: 'Dashboard',
@@ -24,13 +24,16 @@ export default function Page() {
<PageTitle title="statistics" /> <PageTitle title="statistics" />
<DashboardUserStats /> <DashboardUserStats />
<div className="dashboard-main-grid"> <div className="dashboard-main-grid">
{/* <DashboardUserFiles />
<DashboardUserViolations /> */}
<div className="left-column"> <div className="left-column">
<DashboardUserFiles /> <DashboardUserFiles />
{/* <DashboardPlatformsList /> */}
</div> </div>
<div className="right-column"> <div className="right-column">
<ViolationsCarousel /> <DashboardUserViolations />
</div> </div>
</div> </div>
<DashboardFilesInfo /> <DashboardFilesInfo />
<div className="dashboard-main-grid"> <div className="dashboard-main-grid">
@@ -40,7 +43,7 @@ export default function Page() {
<div <div
className="mb-4" className="mb-4"
> >
<DahboardLimitsSection /> {/* <DahboardLimitsSection /> */}
</div> </div>
<DashboardLastCheck /> <DashboardLastCheck />
</Suspense> </Suspense>
+15
View File
@@ -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>
)
}
+98
View File
@@ -0,0 +1,98 @@
// app/files/[id]/page.tsx
'use server'
import FilePageTitle from '@/app/ui/file-page/file-page-title';
import FilePageViolationInfo from '@/app/ui/file-page/file-page-violation-info';
import ViolationPpageFileStatistic from '@/app/ui/file-page/file-page-file-statistic';
import FilePageFileInfo from '@/app/ui/file-page/file-page-file-info';
import FilePageActions from '@/app/ui/file-page/file-page-actions';
import FilePageViolationsList from '@/app/ui/file-page/violation-table/file-page-violations-list';
import FilePageNote from '@/app/ui/file-page/file-page-note';
import { viewFileInfo } from '@/app/actions/fileEntity';
import { useTranslations } from 'next-intl';
import FilePageNotFound from '@/app/ui/file-page/file-page-file-not-found';
type MonitoringStatus = 'NONE' | 'MONITORING_DAILY' | 'MONITORING_WEEKLY' | 'MONITORING_MONTHLY';
type MimeType = 'image' | 'video' | 'audio' | 'document';
export interface FileDetails {
id: string;
userId: number;
originalFileName: string;
storedFileName: string;
filePath: string;
protectedFilePath: string;
fileSize: number;
mimeType: MimeType;
fileExtension: string;
checksum: string;
uploadSessionId: string;
status: string;
createdAt: string;
updatedAt: string;
formattedSize: string;
downloadUrl: string;
existsOnDisk: boolean;
supportId: number | null;
protectStatus: string;
ownerName: string;
ownerEmail: string;
ownerCompany: string | null;
fileName: string;
fileFormat: string | null;
checksCount: number;
fileUploadDate: string;
monitoring: MonitoringStatus;
thumbnailFileUrl: string;
}
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ page?: string, status?: string, violationId?: string, caseType?: 'complaint' | 'claim', startDate: string, endDate: string }>;
}
export default async function Page({
params,
searchParams
}: PageProps) {
const { id } = await params;
const { page, status, violationId, caseType, startDate, endDate } = await searchParams;
const currentPage = Number(page) || 1;
try {
const fileInfo: FileDetails = await viewFileInfo(id);
return (
<div className="violation-details">
<FilePageTitle fileInfo={fileInfo} />
<div>
<div
className="grid grid-cols-[2fr_1fr] gap-3"
>
<FilePageFileInfo fileInfo={fileInfo} />
{/* <FilePageActions /> */}
<ViolationPpageFileStatistic id={id} />
</div>
<div>
<FilePageViolationsList
currentPage={currentPage}
status={status}
fileId={id}
violationId={violationId}
caseType={caseType}
startDate={startDate}
endDate={endDate}
/>
{/* <FilePageNote /> */}
{/* <FilePageViolationInfo /> */}
</div>
</div>
</div>
)
} catch (error) {
return (
<FilePageNotFound />
)
}
}
@@ -3,26 +3,11 @@ import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload'; import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic'; import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import { getQueryClient } from '@/app/providers/getQueryClient';
export default async function Page() { export default async function Page() {
const FILE_TYPE = "document"; const FILE_TYPE = "document";
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE); const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
const queryClient = getQueryClient();
try {
const data = await queryClient.fetchQuery({
queryKey: ['referralUserStats'],
queryFn: fetchReferralUserStats,
});
if (data?.referralLink) {
return null;
}
} catch (error) {
}
return ( return (
<div> <div>
<PageTitleColorFrame <PageTitleColorFrame
@@ -0,0 +1,12 @@
import PageTitle from '@/app/ui/page-title';
import { NotificationsList } from '@/app/ui/notifications/notifications-list';
export default function Page() {
return (
<div>
<PageTitle title='notifications' />
<NotificationsList />
</div>
)
}
+2
View File
@@ -3,6 +3,7 @@ import UserUsage from '@/app/ui/payment/user-usage';
import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats'; import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats';
import PaymentTabs from '@/app/ui/payment/payment-tabs'; import PaymentTabs from '@/app/ui/payment/payment-tabs';
import SecurePayments from '@/app/ui/payment/secure-payments'; import SecurePayments from '@/app/ui/payment/secure-payments';
import PaymentUserCards from '@/app/ui/payment/payment-user-cards';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'payment', title: 'payment',
@@ -22,6 +23,7 @@ export default function Page() {
<UserUsage /> <UserUsage />
<TokensSpendStats /> <TokensSpendStats />
<PaymentTabs /> <PaymentTabs />
<PaymentUserCards />
<SecurePayments /> <SecurePayments />
</div> </div>
</main> </main>
+1 -2
View File
@@ -1,10 +1,9 @@
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Suspense } from 'react'; import { Suspense } from 'react';
import PageTitle from '@/app/ui/page-title';
import ReferralStats from '@/app/ui/referral-page/referral-stats'; import ReferralStats from '@/app/ui/referral-page/referral-stats';
import ReferralLinkSection from '@/app/ui/referral-page/referral-link-section'; import ReferralLinkSection from '@/app/ui/referral-page/referral-link-section';
import CommissionLevels from '@/app/ui/referral-page/commission-levels'; import CommissionLevels from '@/app/ui/referral-page/commission-levels';
import InvitationsTable from '@/app/ui/referral-page/invitations-table'; import InvitationsTable from '@/app/ui/referral-page/table/invitations-table';
import IncomeAndPayments from '@/app/ui/referral-page/Income-and-payments'; import IncomeAndPayments from '@/app/ui/referral-page/Income-and-payments';
import SettingReferralProgram from '@/app/ui/referral-page/setting-referral-program'; import SettingReferralProgram from '@/app/ui/referral-page/setting-referral-program';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
+5 -5
View File
@@ -1,21 +1,21 @@
import StatsGrid from '@/app/ui/dashboard/stats-grid'; import ViolationsTable from '@/app/ui/violations/violations-table';
import ViolationsTable from '@/app/ui/dashboard/violations-table';
import FinalSummary from '@/app/ui/reports/final-summary'; import FinalSummary from '@/app/ui/reports/final-summary';
import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month'; 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 ActivityByContentType from '@/app/ui/reports/activity-by-content-type';
import PageTitle from '@/app/ui/page-title'; import PageTitle from '@/app/ui/page-title';
import ReportsInfo from '@/app/ui/reports/reports-info'; import ReportsInfo from '@/app/ui/reports/reports-info';
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
export default function Page() { export default function Page() {
return ( return (
<div> <div>
<PageTitle title="reports-and-analytics" /> <PageTitle title="reports-and-analytics" />
{/* <StatsGrid /> */}
<ReportsInfo /> <ReportsInfo />
<ActivityByContentType /> <ActivityByContentType />
<div className="analytics-grid"> <div className="analytics-grid">
<AnalyticsCardMonth /> {/* <AnalyticsCardMonth /> */}
<AnalyticsCardDistribution />
<AnalyticsCardGeography /> <AnalyticsCardGeography />
</div> </div>
<ViolationsTable /> <ViolationsTable />
+4 -31
View File
@@ -1,45 +1,18 @@
import SectionSearchFile from '@/app/ui/search/section-search-file'; import SectionSearchFile from '@/app/ui/search/section-search-file';
import { SupportedFormats } from '@/app/ui/search/supported-formats'; import { SupportedFormats } from '@/app/ui/search/supported-formats';
import { SearchStats } from '@/app/ui/search/search-stats'; import { SearchStats } from '@/app/ui/search/search-stats';
import { RecentSearches } from '@/app/ui/search/recent-searches'; import SearchTitle from '@/app/ui/search/search-title';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
export default async function Page() { export default async function Page() {
const { file_extension: extensionVideo, max_file_size } = await getAllowedFilesExtensions('video');
const { file_extension: extensionAudio } = await getAllowedFilesExtensions('audio');
const { file_extension: extensionImage } = await getAllowedFilesExtensions('image');
const { file_extension: extensionDocument } = await getAllowedFilesExtensions('document');
return ( return (
<> <>
<div className="block-wrapper"> <SearchTitle />
<h1 className="page-title">
Поиск защищенного контента
</h1>
<p className="page-description">
Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете
</p>
</div>
<div className="search-grid"> <div className="search-grid">
<SectionSearchFile <SectionSearchFile />
maxFileSize={max_file_size}
/* allowedExtensions={allExtensions} */
allowedExtensions={{
images: extensionImage,
videos: extensionVideo,
audios: extensionAudio,
documents: extensionDocument
}}
/>
<div className="search-sidebar"> <div className="search-sidebar">
<SupportedFormats <SupportedFormats />
extensionVideo={extensionVideo}
extensionAudio={extensionAudio}
extensionImage={extensionImage}
extensionDocument={extensionDocument}
/>
<SearchStats /> <SearchStats />
<RecentSearches />
</div> </div>
</div> </div>
</> </>
+4 -1
View File
@@ -8,6 +8,7 @@ import DangerZone from '@/app/ui/settings/danger-zone';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import PageTitle from '@/app/ui/page-title'; import PageTitle from '@/app/ui/page-title';
import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel'; import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel';
import SettingUserVerification from '@/app/ui/settings/setting-user-verification';
export default function Page() { export default function Page() {
const t = useTranslations('Global'); const t = useTranslations('Global');
@@ -21,8 +22,10 @@ export default function Page() {
<SafetySetting /> <SafetySetting />
<SubscriptionSettings /> <SubscriptionSettings />
{/* <APISettings /> */} {/* <APISettings /> */}
<LastActivitySettings /> {/* LastActivitySettings возможно нужно будет вернуть */}
{/* <LastActivitySettings /> */}
<CompanyUsersSettingPanel /> <CompanyUsersSettingPanel />
<SettingUserVerification />
</div> </div>
<DangerZone /> <DangerZone />
</> </>
+19
View File
@@ -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>
)
}
+18 -9
View File
@@ -1,13 +1,16 @@
import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section'; import ViolationsMyComplaint from '@/app/ui/violations/violations-my-complaint';
import DashboardPlatformsList from '@/app/ui/dashboard/new/dashboard-platforms-list';
import ViolationsMyCases from '@/app/ui/violations/violations-my-cases';
import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section'; import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section';
import ViolationsTable from '@/app/ui/violations/violations-table'; import ViolationsTable from '@/app/ui/violations/violations-table';
import ViolationsStatistic from '@/app/ui/violations/violations-statistic'; import ViolationsStatistic from '@/app/ui/violations/violations-statistic';
export default function Page() { import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
const FILE_TYPE = "all"; 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 ( return (
<> <>
<PageTitleColorFrame <PageTitleColorFrame
@@ -15,12 +18,18 @@ export default function Page() {
description="monitoring-copyright-infringements-description" description="monitoring-copyright-infringements-description"
/> />
<ViolationsStatistic /> <ViolationsStatistic />
<ViolationsMyCases />
<ViolationsCheckAllSection /> <ViolationsCheckAllSection />
<ViolationsTable /> <ViolationsTable />
<div className="dashboard-main-grid"> {/* <ViolationsLastCombinedCases /> */}
<DashboardPlatformsList /> {/* <ViolationsMyComplaint />
<DahboardGeographySection /> <ViolationsMyClaims /> */}
<div className="analytics-grid">
<AnalyticsCardDistribution />
<AnalyticsCardGeography />
</div>
<div className="analytics-grid">
<ViolationsMyClaimsTable />
<ViolationsMyComplaintsTable />
</div> </div>
</> </>
) )
+14
View File
@@ -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>
);
}
+151
View File
@@ -0,0 +1,151 @@
'use server';
import Link from 'next/link';
import PdfViewer from '@/app/components/PdfViewer';
import { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
import { fetchDocumentInfo } from '@/app/actions/trackingActions';
import { LoginUserBage } from '@/app/components/document-viewer/LoginUserBage';
interface PageProps {
searchParams: Promise<{ docid: string; title?: string }>;
}
interface DocumentInfo {
checksCount?: number;
checksum?: string;
createdAt?: string;
downloadUrl?: string;
existsOnDisk?: boolean;
fileExtension?: string;
fileFormat?: string | null;
fileName?: string;
filePath?: string;
fileSize?: number;
fileUploadDate?: string;
formattedSize?: string;
id: string;
mimeType?: string;
monitoring?: string;
originalFileName?: string;
ownerCompany?: string | null;
ownerEmail?: string;
ownerName?: string;
permissions?: {
DOWNLOAD?: boolean;
};
protectStatus?: string;
protectedFilePath?: string;
status?: string;
storedFileName?: string;
supportId?: number;
thumbnailFileUrl?: string;
updatedAt?: string;
uploadSessionId?: string;
userId?: number;
appealInfos?: any[]; //Не знаю что тут будет
}
export default async function Page({ searchParams }: PageProps) {
const { docid, title } = await searchParams;
let response: DocumentInfo | null = null;
let error: string | null = null;
try {
response = await fetchDocumentInfo(docid);
} catch (err) {
error = 'Не удалось загрузить информацию о документе';
}
const documentTitle = response?.fileName || 'FileName';
if (error || !response) {
return (
<div className="watch-doc-wrapper">
<div className="watch-doc-page">
<header className="watch-doc-header">
<Link href="#" className="header-logo">NoCopy</Link>
<LoginUserBage />
</header>
<div className="error-container">
<h2>Ошибка загрузки документа</h2>
<p>{error || 'Документ не найден'}</p>
</div>
</div>
</div>
);
}
return (
<div className="watch-doc-wrapper">
<div className="watch-doc-page">
<header className="watch-doc-header">
<Link href="#" className="header-logo">NoCopy</Link>
<LoginUserBage />
</header>
<div className="watch-doc-status">
<div className="st-title">Защита подтверждена</div>
<div className="st-sub">
Документ <strong>{response?.fileName ? response?.fileName : '#'}</strong> зарегистрирован и защищён платформой NoCopy.
</div>
</div>
<div className="watch-doc-grid">
<div
className="flex flex-col justify-between"
>
<div className="watch-doc-card">
<div className="card-head">Правообладатель</div>
<div className="card-body">
<div className="owner-row">
<div className="owner-av">
{response?.fileName ? response?.fileName[0] : ''}
</div>
<div>
{response?.ownerName && (
<div className="owner-name">{response?.ownerName}</div>
)}
{response?.ownerCompany && (
<div className="owner-name">{response?.ownerCompany}</div>
)}
{response?.ownerName && (
<div className="owner-meta">Зарегистрирован в NoCopy</div>
)}
</div>
</div>
</div>
</div>
<div className="watch-doc-actions">
{response?.permissions?.DOWNLOAD && (
<DownloadButton fileId={docid} fileName={response?.fileName}/>
)}
<CopyLinkButton />
</div>
</div>
<div>
<div className="watch-doc-card">
<div className="card-head">Предпросмотр</div>
<div className="card-body">
<PdfViewer
fileId={docid}
fileName={documentTitle}
showDownload={response.permissions?.DOWNLOAD}
/>
</div>
</div>
</div>
</div>
<div className="watch-doc-footer">
<Link href="#">NoCopy</Link>
<Link href="/terms-of-use">Условия</Link>
<Link href="/privacy-policy">Конфиденциальность</Link>
</div>
</div>
</div>
);
}
+63
View File
@@ -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;
}
}
+66 -6
View File
@@ -44,12 +44,12 @@ export async function getUserFilesInfo() {
msg_id: 20005, msg_id: 20005,
message_body: { message_body: {
action: 'user_files_info', action: 'user_files_info',
token: token
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -70,7 +70,7 @@ export async function getUserFilesInfo() {
} }
} }
export async function fetchTariffs() { export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
const token = await getSessionData('token'); const token = await getSessionData('token');
try { try {
@@ -81,12 +81,13 @@ export async function fetchTariffs() {
msg_id: 30001, msg_id: 30001,
message_body: { message_body: {
action: 'get', action: 'get',
user_token: token tariff_term: tariffTerm
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -106,7 +107,45 @@ export async function fetchTariffs() {
} }
} }
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() { export async function getBuildData() {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/check/api/build`, { const response = await fetch(`${API_BASE_URL}/check/api/build`, {
@@ -114,6 +153,7 @@ export async function getBuildData() {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -129,7 +169,7 @@ export async function getBuildData() {
export async function fetchINN(inn: string) { export async function fetchINN(inn: string) {
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
@@ -159,3 +199,23 @@ export async function fetchINN(inn: string) {
return null 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
}
}
+171 -41
View File
@@ -4,6 +4,7 @@ import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/action
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session'; import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { fetchINN } from '@/app/actions/action'; import { fetchINN } from '@/app/actions/action';
import { headers } from 'next/headers';
export async function logout() { export async function logout() {
const token = await getSessionData('token'); const token = await getSessionData('token');
@@ -71,7 +72,7 @@ export async function authorization(
try { try {
const { email, password } = validatedFields.data; 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', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
@@ -88,6 +89,7 @@ export async function authorization(
}); });
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
await createSession(parsed.message_body.token, email); await createSession(parsed.message_body.token, email);
} else { } else {
@@ -142,6 +144,7 @@ export type FormState = {
inn?: string | undefined; inn?: string | undefined;
} | undefined, } | undefined,
mailConfirm?: boolean, mailConfirm?: boolean,
userId?: number,
error: Record<string, string> | null error: Record<string, string> | null
}; };
@@ -149,8 +152,11 @@ export async function registration(
state: FormState | undefined, state: FormState | undefined,
formData: FormData, formData: FormData,
): Promise<FormState> { ): Promise<FormState> {
const headersList = await headers();
const accountType = formData.get('accountType') as 'b2b' | 'b2c'; const accountType = formData.get('accountType') as 'b2b' | 'b2c';
/* вернуть в true когда проект выйдет из дева */
const mail_verified = false;
const fullName = formData.get('fullName') as string || ''; const fullName = formData.get('fullName') as string || '';
const email = formData.get('email') as string || ''; const email = formData.get('email') as string || '';
const password = formData.get('password') as string || ''; const password = formData.get('password') as string || '';
@@ -158,9 +164,15 @@ export async function registration(
const phone = formData.get('phone') as string || ''; const phone = formData.get('phone') as string || '';
const companyName = formData.get('companyName') as string || ''; const companyName = formData.get('companyName') as string || '';
const agree = formData.get('agree') as string || ''; const agree = formData.get('agree') as string || '';
const referralCode = formData.get('referralCode'); const referralCode = formData.get('referralCode') as string || '';
const inn = formData.get('inn') as string || ''; const inn = formData.get('inn') as string || '';
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const SignupFormSchema = await getSignupFormSchema(accountType); const SignupFormSchema = await getSignupFormSchema(accountType);
const dataToValidate = { const dataToValidate = {
@@ -214,6 +226,7 @@ export async function registration(
confirm_password, confirm_password,
phone, phone,
agree, agree,
...(referralCode !== '' && { referralCode }),
...(accountType === 'b2b' && { companyName, inn }) ...(accountType === 'b2b' && { companyName, inn })
}, },
error: errors error: errors
@@ -222,7 +235,7 @@ export async function registration(
try { try {
const { fullName, email, password, phone } = validatedFields.data; const { fullName, email, password, phone } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
@@ -233,6 +246,9 @@ export async function registration(
phone: phone, phone: phone,
password, password,
accountType, accountType,
mail_verified: mail_verified,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }), ...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode }) ...(referralCode !== '' && { referralLink: referralCode })
} }
@@ -249,6 +265,8 @@ export async function registration(
phone: phone, phone: phone,
password, password,
accountType, accountType,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }), ...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode }) ...(referralCode !== '' && { referralLink: referralCode })
}) })
@@ -256,7 +274,7 @@ export async function registration(
if (response.ok) { if (response.ok) {
let parsed = await response.json() as { let parsed = await response.json() as {
message_desc: string, message_desc: string,
message_body: { token: string }, message_body: { token: string, userId: number, fieldErrors: string },
message_code: number message_code: number
}; };
@@ -271,28 +289,34 @@ export async function registration(
...(referralCode !== '' && { referralLink: referralCode }), ...(referralCode !== '' && { referralLink: referralCode }),
...(accountType === 'b2b' && { companyName: companyName, inn: inn }) ...(accountType === 'b2b' && { companyName: companyName, inn: inn })
}); });
await createSession(parsed.message_body.token, email as string);
} else {
console.log(parsed);
throw parsed;
}
} else {
throw (`${response.status}`);
}
/* return { if (mail_verified) {
return {
previousState: { previousState: {
fullName, fullName,
email, email,
password, password,
confirm_password, confirm_password,
phone, phone,
companyName, agree,
agree ...(referralCode !== '' && { referralLink: referralCode }),
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
}, },
mailConfirm: true, mailConfirm: true,
userId: parsed.message_body.userId,
error: null error: null
}; */ };
} else {
await createSession(parsed.message_body.token, email as string);
}
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error: unknown) { } catch (error: unknown) {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
if (error) { if (error) {
@@ -320,6 +344,8 @@ export async function registration(
} }
}); });
} else if (typedError.message_desc === 'Company already exists') {
errors['inn'] = 'company-already-registered'
} else { } else {
errors['server'] = 'unknown-error' errors['server'] = 'unknown-error'
} }
@@ -341,6 +367,7 @@ export async function registration(
agree, agree,
inn inn
}, },
mailConfirm: false,
error: errors error: errors
}; };
} }
@@ -374,13 +401,11 @@ export async function tokenLifeExtension() {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
msg_id: 20008, msg_id: 20008
message_body: {
token: token
}
}), }),
}); });
@@ -396,24 +421,110 @@ export async function tokenLifeExtension() {
} }
} }
export async function confirmEmail( export async function confirmEmail(userId: number, verifyToken: string) {
state: any | undefined,
formData: any,
) {
console.log('confirmEmail'); console.log('confirmEmail');
const emailConfirm = formData.get('emailConfirm') as string || '';
console.log(emailConfirm); try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
'Authorization': `Bearer ${verifyToken}`,
},
body: JSON.stringify({
version: 1,
msg_id: 20009,
message_body: {
resend: 0,
user_id: userId
}
}),
});
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) {
await createSession(parsed.message_body.token, parsed.message_body.email as string);
} else {
return {
success: false,
error: 'entered-code-is-incorrect'
};
}
} else {
return {
success: false,
error: 'server-error'
};
}
} catch (error) {
return {
success: false,
error: 'network-error'
};
}
redirect('/pages/dashboard');
} }
export async function resendConfirmEmail(userId: number) {
console.log('resendConfirmEmail');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({
version: 1,
msg_id: 20009,
message_body: {
resend: 1,
user_id: userId,
}
}),
});
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) {
return {
fine: true
}
}
}
} catch (error) {
return {
fine: false
}
}
}
export type FormStateConfirmPassword = {
previousState: {
email: string;
password: string;
confirm_password: string;
verifyToken: string;
},
error: Record<string, string> | null
};
export async function resetPassword( export async function resetPassword(
state: any | undefined, state: any | undefined,
formData: FormData, formData: FormData,
) { ) {
const email = formData.get('email'); const email = formData.get('email');
console.log('resetPassword');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -447,15 +558,6 @@ export async function resetPassword(
redirect(`/reset-password?email=${email}`); redirect(`/reset-password?email=${email}`);
} }
export type FormStateConfirmPassword = {
previousState: {
email: string;
password: string;
confirm_password: string;
verifyToken: string;
},
error: Record<string, string> | null
};
export async function confirmPassword( export async function confirmPassword(
state: FormStateConfirmPassword | undefined, state: FormStateConfirmPassword | undefined,
@@ -506,6 +608,7 @@ export async function confirmPassword(
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${verifyToken}`
}, },
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
@@ -513,7 +616,6 @@ export async function confirmPassword(
message_body: { message_body: {
email: email, email: email,
password: password, password: password,
verifyToken: verifyToken,
action: "confirmVerification" action: "confirmVerification"
} }
}), }),
@@ -526,16 +628,35 @@ export async function confirmPassword(
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
await createSession(parsed.message_body.authToken, email as string); await createSession(parsed.message_body.authToken, email as string);
} else { } else {
throw parsed.message_body; throw parsed;
} }
} else { } else {
throw (`${response.status}`); throw (`${response.status}`);
} }
} catch (error: unknown) { } catch (error: unknown) {
const typedError = error as any;
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
if (error) { if (error) {
const typedError = error as any;
switch (typedError.message_code) {
case 1:
errors['server'] = 'unknown-error'
break
case 2:
if (typedError.message_desc === 'Token not found or time expired') {
errors['server'] = 'token-not-found-or-time-expired'
} else {
errors['server'] = 'unknown-error'
}
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
return { return {
previousState: { previousState: {
email, email,
@@ -546,7 +667,16 @@ export async function confirmPassword(
error: errors error: errors
}; };
} }
throw error;
return {
previousState: {
email,
password,
confirm_password,
verifyToken
},
error: errors
};
} }
redirect('/pages/dashboard'); redirect('/pages/dashboard');
+16 -11
View File
@@ -2,6 +2,7 @@
import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions'; import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session'; import { getSessionData } from '@/app/actions/session';
import { headers } from 'next/headers';
export type CompanyUserRegistrationFormState = { export type CompanyUserRegistrationFormState = {
previousState: { previousState: {
@@ -24,6 +25,8 @@ export async function companyUserRegistration(
return; return;
} }
const headersList = await headers();
const fullName = formData.get('fullName') as string || ''; const fullName = formData.get('fullName') as string || '';
const email = formData.get('email') as string || ''; const email = formData.get('email') as string || '';
const password = formData.get('password') as string || ''; const password = formData.get('password') as string || '';
@@ -32,6 +35,11 @@ export async function companyUserRegistration(
const SignupFormSchema = await getSignupFormSchema('b2c'); 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 = { const dataToValidate = {
fullName, fullName,
email, email,
@@ -87,12 +95,15 @@ export async function companyUserRegistration(
phone: phone, phone: phone,
password, password,
accountType: 'b2b', accountType: 'b2b',
authToken: token mail_verified: false,
ip: ip,
user_agent: userAgent,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -102,14 +113,6 @@ export async function companyUserRegistration(
message_body: { token: string }, message_body: { token: string },
message_code: number message_code: number
}; };
console.log({
fullName: fullName,
email,
phone: phone,
password,
accountType: 'b2b',
authToken: token
});
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
console.log(`registration - ${email}`); console.log(`registration - ${email}`);
@@ -117,7 +120,9 @@ export async function companyUserRegistration(
fullName: fullName, fullName: fullName,
email, email,
phone: phone, phone: phone,
password password,
ip: ip,
user_agent: userAgent,
}); });
return { return {
+33 -1
View File
@@ -1,5 +1,5 @@
import * as z from 'zod' import * as z from 'zod'
import {createValidationSchema, createValidationSchemaForPassword} from '@/app/lib/validation-config-parser'; import { createValidationSchema, createValidationSchemaForPassword } from '@/app/lib/validation-config-parser';
import validationConfig from '@/app/lib/validation-config.json'; import validationConfig from '@/app/lib/validation-config.json';
export type FormState = export type FormState =
@@ -83,6 +83,30 @@ export const loginFormSchema = z
.trim(), .trim(),
}) })
export const createComplaintSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'text-area-error-fill-complaint-text' })
.max(2000, {error: 'text-area-error-complaint-text-too-long'})
.trim(),
})
export const createClaimSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'fill-text-of-the-case' })
.max(2000, {error: 'claim-text-must-not-exceed-2000-characters'})
.trim(),
caseTitle: z
.string()
.min(1, { error: 'fill-title-of-the-case' })
.max(300, {error: 'claim-title-must-not-exceed-300-characters'})
.trim(),
})
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost'; export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
export const API_BASE_URL = process.env.NODE_ENV === 'development' export const API_BASE_URL = process.env.NODE_ENV === 'development'
@@ -94,3 +118,11 @@ export const testUserData = {
email: 'test@mail.com', email: 'test@mail.com',
subscriptionType: 'base' subscriptionType: 'base'
} }
export const bankCardFormSchema = z
.object({
bankCard: z
.string()
.min(19, { error: 'error-bank-card' })
.trim()
})
+97 -12
View File
@@ -4,7 +4,23 @@ import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions'; import { API_BASE_URL } from '@/app/actions/definitions';
export async function getUserFilesData(page: number, pageSize: number) { export async function getUserFilesData({
page,
pageSize,
sortBy,
sortOrder,
type,
date,
query = ''
}: {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc' | string;
type?: string;
date?: string;
query?: string;
}) {
const token = await getSessionData('token'); const token = await getSessionData('token');
try { try {
@@ -14,31 +30,37 @@ export async function getUserFilesData(page: number, pageSize: number) {
version: 1, version: 1,
msg_id: 20005, msg_id: 20005,
message_body: { message_body: {
action: 'user_files', action: 'search_files',
page: page, page: page,
page_size: pageSize, page_size: pageSize,
token: token query: query,
sort_by: sortBy || '', // protectStatus, fileName, fileSize, createdAt, updatedAt, supportId
type: type || '', // image, video, document, audio
date: date || '', // today, week, month
sort_order: sortOrder
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
} else { } else {
throw parsed; throw parsed.message_text ? parsed.message_text : 'error';
} }
} else { } else {
throw (`${response.status}`); throw `${response.status}`;
} }
} catch (error) { } catch (error) {
return error return error;
} }
} }
@@ -55,12 +77,12 @@ export async function removeUserFile(fileId: string, fullDelete: number) {
action: 'delete_file', action: 'delete_file',
file_id: fileId, file_id: fileId,
full_delete: fullDelete, full_delete: fullDelete,
token: token
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -92,13 +114,13 @@ export async function viewFileInfo(fileId: string) {
msg_id: 20005, msg_id: 20005,
message_body: { message_body: {
action: 'file_info', action: 'file_info',
file_id: fileId, file_id: fileId
token: token
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -118,3 +140,66 @@ export async function viewFileInfo(fileId: string) {
return error 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
}
}
+89 -19
View File
@@ -2,8 +2,6 @@
import { API_BASE_URL } from '@/app/actions/definitions'; import { API_BASE_URL } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session'; import { getSessionData } from '@/app/actions/session';
import { FormState } from '@/app/actions/auth';
import { unknown } from 'zod';
interface initMessageBody { interface initMessageBody {
file_name: string, file_name: string,
@@ -11,20 +9,21 @@ interface initMessageBody {
extension: string, extension: string,
file_size: number | string, file_size: number | string,
action?: string, action?: string,
token?: string token?: string,
convertTo?: string | undefined | null
} }
export async function fileUpload(messageBody: initMessageBody, convertTo: string | null | undefined) { export async function fileUpload(messageBody: initMessageBody, convertTo?: string | null | undefined) {
const token = await getSessionData('token'); const token = await getSessionData('token');
if (!token) { if (!token) {
return; return;
} }
const message = messageBody; const message = messageBody;
message.action = 'init'; message.action = 'init';
message.token = token;
// нужно будет добавить свойство convertTo в тело запроса он может прийти со значением reject тогда конвертировать не нужно. if (convertTo && convertTo !== 'reject') {
// console.log(convertTo); message.convertTo = convertTo;
}
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -36,7 +35,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo: string
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -66,6 +66,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo: string
} }
export async function cancelUpload(udloadId: string) { export async function cancelUpload(udloadId: string) {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -79,7 +81,8 @@ export async function cancelUpload(udloadId: string) {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -111,12 +114,27 @@ export type ChunkResponse = {
fileId?: string fileId?: string
}; };
export async function chunkUpload(formData: FormData): Promise<ChunkResponse> { export async function chunkUpload(formData: FormData, itPrivate?: boolean): Promise<ChunkResponse> {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, { const url = itPrivate
? `${API_BASE_URL}/api/v1/private/files/chunk`
: `${API_BASE_URL}/api/v1/files/chunk`;
/* if (token && itPrivate) {
formData.append('token', token);
} */
const response = await fetch(url, {
method: 'POST', method: 'POST',
body: formData body: formData,
headers: {
'Authorization': `Bearer ${token}`
}
}); });
/* ...(token && itPrivate && { 'Authorization': `Bearer ${token}` }) возможно нужно будет условно отправлять токен*/
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
@@ -127,25 +145,27 @@ export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
fileId: parsed.message_body.file_id fileId: parsed.message_body.file_id
} }
} else if (parsed.message_desc === 'Duplicate file upload') { } else if (parsed.message_desc === 'Duplicate file upload') {
throw 'duplicate-file' throw 'error-duplicate-file'
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: User not have tokens for protect') { } 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' 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 { } else {
throw 'error' throw 'error-uploading-file'
} }
} else { } else {
throw 'error' throw 'error-uploading-file'
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error) { if (error) {
const typedError = error as any; const typedError = error as string;
return { return {
errorMesage: typedError errorMesage: typedError
}; };
} else { } else {
return { return {
errorMesage: 'error' errorMesage: 'error-uploading-file'
}; };
} }
@@ -153,6 +173,8 @@ export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
} }
export async function checkChunkStatus(upload_id: string) { export async function checkChunkStatus(upload_id: string) {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -166,7 +188,8 @@ export async function checkChunkStatus(upload_id: string) {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -188,6 +211,8 @@ export async function checkChunkStatus(upload_id: string) {
} }
export async function getAllowedFilesExtensions(type: string) { export async function getAllowedFilesExtensions(type: string) {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -201,7 +226,8 @@ export async function getAllowedFilesExtensions(type: string) {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -221,3 +247,47 @@ export async function getAllowedFilesExtensions(type: string) {
return 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'
}
}
}
+151
View File
@@ -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
}
}
+173
View File
@@ -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
}
}
}
+59
View File
@@ -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
}
}
+280 -24
View File
@@ -1,7 +1,7 @@
'use server' 'use server'
import { getSessionData } from '@/app/actions/session'; import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions'; import { API_BASE_URL, bankCardFormSchema } from '@/app/actions/definitions';
export async function fetchReferralsLevels() { export async function fetchReferralsLevels() {
const token = await getSessionData('token'); const token = await getSessionData('token');
@@ -14,12 +14,12 @@ export async function fetchReferralsLevels() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'levels', action: 'levels',
token: token,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -57,12 +57,12 @@ export async function fetchReferralUserStats() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'userStats', action: 'userStats',
token: token,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -96,26 +96,19 @@ export async function fetchReferralInvitees() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'invitees', action: 'invitees',
token: token,
pageSize: 100, pageSize: 100,
pageNumber: 0 pageNumber: 0
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
console.log({
action: 'invitees',
token: token,
pageSize: 10,
pageNumber: 0
})
console.log(parsed);
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
@@ -144,25 +137,18 @@ export async function referralRefill() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'refill', action: 'refill',
token: token, amount: 2000,
amount: 1,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
if (response.ok) { if (response.ok) {
console.log('referralRefill');
console.log({
action: 'refill',
token: token,
amount: 1,
})
let parsed = await response.json(); let parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
@@ -179,3 +165,273 @@ export async function referralRefill() {
} }
} }
} }
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'
};
}
}
+42 -43
View File
@@ -7,8 +7,11 @@ export async function searchUserFiles(fileId: string) {
const token = await getSessionData('token'); const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY&auth_token=${token}`, { const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY`, {
method: 'GET' method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
}); });
if (response.ok) { if (response.ok) {
@@ -17,55 +20,28 @@ export async function searchUserFiles(fileId: string) {
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
} else { } else {
throw parsed; throw parsed.message_desc;
} }
} else { } else {
throw (`${response.status}`); throw (`${response.status}`);
} }
} catch (error) { } catch (error) {
return error
if (error === 'File for search unsupported') {
return {
errorMessage: 'file-for-search-unsupported'
}
} else {
return {
errorMessage: 'error'
}
}
} }
} }
export async function searchGlobalFiles(fileId: string, currentPage: number) { export async function searchGlobalFiles(fileId: string, currentPage: number) {
const token = await getSessionData('token'); const token = await getSessionData('token');
console.log('searchGlobalFiles');
console.log(currentPage);
//удалить когда поиск будет нормально работать
/* return {
images: [
{
url: 'string1',
pageTitle: `string1 from page ${currentPage}`,
height: 100,
width: 100,
host: 'string1',
pageUrl: 'string1'
},
{
url: 'string1',
pageTitle: `string2 from page ${currentPage}`,
height: 100,
width: 100,
host: 'string1',
pageUrl: 'string1'
},
{
url: 'string1',
pageTitle: `string3 from page ${currentPage}`,
height: 100,
width: 100,
host: 'string1',
pageUrl: 'string1'
}
],
page: 0,
pageSize: 0,
totalPages: 11,
totalResults: 100,
} */
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -76,11 +52,13 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
message_body: { message_body: {
file_id: fileId, file_id: fileId,
page: currentPage, page: currentPage,
check_similar_first: true,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -93,11 +71,31 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
throw parsed; throw parsed;
} }
} else { } else {
throw (`${response.status}`); throw 'error';
} }
} catch (error) { } catch (error) {
return error let errorMessage: string = 'error';
if (error) {
const typedError = error as any;
switch (typedError.message_desc) {
case 'No results found':
errorMessage = 'no-similar-files-title'
break;
default:
errorMessage = 'error'
break;
}
}
return {
images: [],
page: 0,
pageSize: 0,
totalPages: 0,
totalResults: 0,
error: errorMessage
}
} }
} }
@@ -116,6 +114,7 @@ export async function getSearchStats() {
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
return parsed; return parsed;
} else { } else {
return null; return null;
+5 -5
View File
@@ -37,7 +37,7 @@ export async function createSession(token: string, email: string) {
}); });
const cookieStore = await cookies(); const cookieStore = await cookies();
cookieStore.set('session', session, { cookieStore.set('session_nc_db', session, {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
expires: expiresAt, expires: expiresAt,
@@ -48,7 +48,7 @@ export async function createSession(token: string, email: string) {
export async function updateSession(newToken: string) { export async function updateSession(newToken: string) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const session = cookieStore.get('session')?.value; const session = cookieStore.get('session_nc_db')?.value;
if (!session) { if (!session) {
return null; return null;
@@ -68,7 +68,7 @@ export async function updateSession(newToken: string) {
const updatedSession = await encrypt(updatedPayload); const updatedSession = await encrypt(updatedPayload);
cookieStore.set('session', updatedSession, { cookieStore.set('session_nc_db', updatedSession, {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
expires: updatedPayload.expiresAt, expires: updatedPayload.expiresAt,
@@ -79,7 +79,7 @@ export async function updateSession(newToken: string) {
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> { export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
const cookieStore = await cookies(); const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session')?.value; const sessionCookie = cookieStore.get('session_nc_db')?.value;
if (!sessionCookie) { if (!sessionCookie) {
return null; return null;
@@ -99,7 +99,7 @@ export async function deleteSession() {
console.log('delete session') console.log('delete session')
try { try {
const cookieStore = await cookies() const cookieStore = await cookies()
cookieStore.delete('session'); cookieStore.delete('session_nc_db');
} catch (error) { } catch (error) {
} finally { } finally {
+265
View File
@@ -0,0 +1,265 @@
'use server';
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
import { headers } from 'next/headers';
export async function requestFileWithTracking(fileId: string) {
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const data = await response.json();
return data.message_body;
} else {
const blob = await response.blob();
return URL.createObjectURL(blob);
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> {
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const data = await response.json();
const fileUrl = data.message_body;
const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) {
throw new Error(`Failed to fetch file: ${fileResponse.status}`);
}
return await fileResponse.arrayBuffer();
}
else {
return await response.arrayBuffer();
}
} catch (error) {
return null;
}
}
export async function fetchDocumentInfo(fileId: string) {
const token = await getSessionData('token');
console.log('fetchDocumentInfo');
try {
const response = await fetch(`${API_BASE_URL}/api/files/public/file-info/${fileId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
return parsed;
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function filePermisionChange(fileId: string, permissions: boolean) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
file_id: fileId,
action: 'change_permission',
permissions: {
DOWNLOAD: permissions
}
}
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return true;
} else {
return false;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function fetchHistoryView(
page: number,
size: number,
locale: string,
sortDirection?: 'asc' | 'desc',
sortBy?: string
) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30028,
message_body: {
action: 'history_view',
page: page,
size: size,
locale: locale,
sort_direction: sortDirection,
sort_by: sortBy
}
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function fetchTrackingStats() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30028,
message_body: {
action: 'view_stats'
}
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function downloadTrackingFile(fileId: string, fileName?: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/public-download/${fileId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw 'failed-to-download-file';
}
const arrayBuffer = await response.arrayBuffer()
const base64 = Buffer.from(arrayBuffer).toString('base64')
return {
data: base64,
contentType: response.headers.get('content-type') || 'application/octet-stream',
fileName: fileName ? fileName : 'fileName'
}
} catch (error) {
return null;
}
}
+719
View File
@@ -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
}
}
-1
View File
@@ -4,7 +4,6 @@ import { API_BASE_URL } from '@/app/actions/definitions';
import { createSession } from '@/app/actions/session'; import { createSession } from '@/app/actions/session';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
export async function YandexAuthorization(data: any, codeVerifier: string) { export async function YandexAuthorization(data: any, codeVerifier: string) {
console.log(data); console.log(data);
+98 -19
View File
@@ -1,43 +1,122 @@
'use server' 'use server'
import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout'; import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout';
import { getSessionData } from '@/app/actions/session'; 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' }); const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4' });
const idempotenceKey = '02347fc4-a1f0-49db-807e-f0d67c2ed5a5'; export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string, operationType: 'TARIFF' | 'TOKEN') {
const formData = new FormData();
const token = await getSessionData('token');
export async function yooKasaCreatePayment(amount: number, tariff: string) { formData.append('email', email);
console.log(amount); formData.append('tariffId', tariffId.toString());
console.log(tariff); formData.append('operationType', operationType);
/* return true; */ 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 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 = { const createPayload: ICreatePayment = {
amount: { amount: {
value: amount.toLocaleString(), value: amount.toLocaleString().replace(/[,\s.]/g, ''),
currency: 'RUB' currency: 'RUB'
}, },
payment_method_data: {
type: 'bank_card'
},
confirmation: { confirmation: {
type: 'redirect', type: 'embedded',
return_url: 'test'
}, },
metadata: { metadata: {
userId: userEmail, userMail: userEmail,
tariff: tariff tariff: tariff
} },
merchant_customer_id: userEmail,
capture: true,
save_payment_method: operationType === 'TOKEN' ? false : true,
/* ...(operationType === 'TOKEN' && { save_payment_method: false }) */
}; };
try { try {
const payment = await checkout.createPayment(createPayload, Date.now().toLocaleString()); const payment = await checkout.createPayment(createPayload, idempotenceKey);
console.log(payment); const response = await makePaymentOperation(userEmail, tariff, payment.id, operationType);
return payment?.confirmation.confirmation_url;
/* return payment; */ if (payment && response) {
return payment?.confirmation.confirmation_token;
} else {
return null;
}
} catch (error) { } catch (error) {
console.error(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
} }
} }
-51
View File
@@ -1,51 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const token = await getSessionData('token');
if (!token) {
return NextResponse.json(
{ error: 'need authorization' },
{ status: 401 }
);
}
const { id } = await params;
const response = await fetch(`${API_BASE_URL}/api/v1/files/download/${id}`, {
method: 'GET',
headers: {
"Authorization": token
}
});
if (!response.ok) {
throw new Error(`backend error: ${response.status}`);
}
const fileBlob = await response.blob();
const headers = new Headers(response.headers);
return new NextResponse(fileBlob, {
status: 200,
headers: {
'Content-Type': headers.get('content-type') || 'application/octet-stream',
'Content-Disposition': headers.get('content-disposition') || `attachment; filename="file-${id}"`,
'Content-Length': headers.get('content-length') || String(fileBlob.size),
},
});
} catch (error) {
console.error('download error:', error);
return NextResponse.json(
{ error: 'download file error' },
{ status: 500 }
);
}
}
-24
View File
@@ -3,36 +3,12 @@
import { useEffect, useRef, useCallback } from 'react'; import { useEffect, useRef, useCallback } from 'react';
import { tokenLifeExtension } from '@/app/actions/auth'; import { tokenLifeExtension } from '@/app/actions/auth';
import { usePathname, useSearchParams } from 'next/navigation';
import { removeUserFile } from '@/app/actions/fileEntity';
export default function ActivityTracker() { export default function ActivityTracker() {
const lastRefreshTime = useRef(Date.now()); const lastRefreshTime = useRef(Date.now());
const activityTimeoutRef = useRef<NodeJS.Timeout>(null); const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
const isRefreshing = useRef(false); const isRefreshing = useRef(false);
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const selectedId = document.cookie
.split('; ')
.find(row => row.startsWith('searchedFileId='))
?.split('=')[1];
const handleCookie = async () => {
if (selectedId) {
await removeUserFile(selectedId, 1);
document.cookie = 'searchedFileId=; path=/; max-age=0';
}
};
if (selectedId) {
handleCookie();
}
}, [pathname, searchParams]);
// Debounce функция // Debounce функция
const debounce = useCallback((func: () => void, delay: number) => { const debounce = useCallback((func: () => void, delay: number) => {
if (activityTimeoutRef.current) { if (activityTimeoutRef.current) {
+1
View File
@@ -48,6 +48,7 @@ export default function DropDownList({ children, value, callBack, translatedValu
setIsOpen(!isOpen) setIsOpen(!isOpen)
}} }}
className="dropdown-button" className="dropdown-button"
type="button"
> >
<span className="flex items-center"> <span className="flex items-center">
<span className="mr-2"> <span className="mr-2">
@@ -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>
);
}
+251
View File
@@ -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>
);
}
-920
View File
@@ -1,920 +0,0 @@
'use client';
import { useState, useMemo, useEffect, ReactNode } from 'react';
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel,
getFilteredRowModel,
ColumnDef,
SortingState,
ColumnFiltersState,
} from '@tanstack/react-table';
import { IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation } from '@/app/ui/icons/icons';
import { useTranslations, useLocale } from 'next-intl';
import DropDownList from '@/app/components/DropDownList';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getUserFilesData, removeUserFile, viewFileInfo } from '@/app/actions/fileEntity';
import ModalWindow from '@/app/components/ModalWindow';
import { toast } from 'sonner';
import { pluralize } from '@/app/lib/pluralize';
import { convertBytes } from '@/app/lib/convertBytes';
import { FileInfoModalWindow } from '@/app/ui/modal-windows/file-info-modal-window';
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
type FileType = 'image' | 'video' | 'audio';
type FileItem = {
id: string;
fileName: string;
fileType: string;
violations?: number | undefined;
size?: number | undefined;
uploadDate?: number;
status?: string;
protectStatus: string;
_original?: ApiFile;
supportId: number;
};
type ApiFile = {
id: string;
originalFileName: string;
mimeType: string;
violations: number;
fileSize: number;
updatedAt: string;
status: string;
protectStatus: string;
supportId: number;
fileName: string;
};
type ApiResponse = {
files?: ApiFile[];
};
// Форматирование даты из timestamp
const formatDate = (timestamp: number) => {
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
const formatDateTime = (timestamp: number) => {
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
const cutFileName = (fileName: string) => {
const MAX_FILE_NAME_LENGTH = 26;
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex <= 0) {
return fileName.length > MAX_FILE_NAME_LENGTH ? fileName.substring(0, MAX_FILE_NAME_LENGTH - 3) + '...' : fileName;
}
const nameWithoutExtension = fileName.substring(0, lastDotIndex);
const extension = fileName.substring(lastDotIndex);
if (fileName.length <= MAX_FILE_NAME_LENGTH) {
return fileName;
}
const maxNameLength = MAX_FILE_NAME_LENGTH - extension.length - 3;
if (maxNameLength <= 0) {
return '...' /* + extension */;
}
return nameWithoutExtension.substring(0, maxNameLength) + '...' /* + extension */;
}
const cutFileExtension = (fileName: string) => {
const lastDotIndex = fileName.lastIndexOf('.');
const extension = fileName.substring(lastDotIndex + 1);
return extension;
}
export default function TanstakFilesTable({ fileType }: { fileType: string }) {
const {
data: tableData,
isLoading,
isError,
error,
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({
queryKey: ['userFilesData', 1, 10000],
queryFn: () => getUserFilesData(1, 10000),
select: (data: ApiResponse): FileItem[] => {
if (!data?.files) return [];
return data.files.map((item: ApiFile) => {
const newDate = new Date(item.updatedAt).getTime();
return {
id: item.id,
fileName: item.originalFileName,
fileType: item.mimeType.toLocaleLowerCase(),
violations: item.violations,
size: item.fileSize,
uploadDate: newDate,
status: item.status,
protectStatus: item.protectStatus,
supportId: item.supportId,
_original: item
};
});
},
});
const queryClient = useQueryClient();
// Состояния
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [dateFilter, setDateFilter] = useState<string>('all');
const [typeFilter, setTypeFilter] = useState<string>(fileType);
const [pagination, setPagination] = useState({
pageIndex: 0,
pageSize: 10,
});
const [openWindow, setOpenWindow] = useState<boolean>(false);
const [openWindowChildren, setOpenWindowChildren] = useState<ReactNode>(null);
const [isFileLoading, setIsFileLoading] = useState(false);
const t = useTranslations("Global");
const locale = useLocale();
// Определение колонок
const columns = useMemo<ColumnDef<FileItem>[]>(
() => [
{
accessorKey: 'id',
header: ({ column }) => (
<div className="column">
<span>
ID
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className="text-center table-item table-item-id">
{row.original.supportId ? row.original.supportId : '-'}
</div>
)
},
},
{
accessorKey: 'fileName',
header: ({ column }) => (
<div className="column start">
<span>
{t('file')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="flex items-center space-x-2 table-item">
<FileTypeIcon type={row.original.fileType} />
<div>
<div className="font-medium w-full truncate" title={row.original.fileName}>
<span className="font-semibold">
{cutFileName(row.original.fileName)}
</span>
<br />
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <span className="table-item-protected">
{row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')}
</span>
</div>
</div>
</div>
),
enableColumnFilter: false,
},
/* {
accessorKey: 'violations',
header: ({ column }) => (
<div className="column">
<span>
{t('violations')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
let classCollor = () => {
let result = ''
if (row.original.violations !== undefined) {
result = row.original.violations > 0 ? 'text-red-600' : 'text-green-600'
}
return result;
}
return (
<div className={`text-center font-semibold ${classCollor()}`}>
{row.original.violations !== undefined ? row.original.violations : '-'}
</div>
)
},
}, */
{
accessorKey: 'size',
header: ({ column }) => (
<div className="column">
<span>
{t('size')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => (
<div className="text-center table-item">
{row.original.size !== undefined ? convertBytes(row.original.size) : '-'}
</div>
),
},
{
accessorKey: 'uploadDate',
header: ({ column }) => (
<div className="column">
<span>
{t('upload-date')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className="text-center table-item">
{row.original.uploadDate ? (
<>
{formatDate(row.original.uploadDate)}
<br />
{formatDateTime(row.original.uploadDate)}
</>
) : (
<div>-</div>
)}
</div>
)
},
enableColumnFilter: false,
},
{
accessorKey: 'status',
header: ({ column }) => (
<div className="column">
<span>
{t('status')}
</span>
<button
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="sort-button"
>
{
column.getIsSorted() === 'asc' ?
<span>
<IconArrowUp />
</span>
: column.getIsSorted() === 'desc' ?
<span>
<IconArrowDown />
</span>
: <span>
<IconFilter />
</span>
}
</button>
</div>
),
cell: ({ row }) => {
return (
<div className="text-center font-semibold table-item">
<span className="table-item-status">
{row.original.status}
</span>
</div>
)
},
},
{
id: 'actions',
header: () => {
return (
<div className="column">
{t('actions')}
</div>
)
},
cell: ({ row }) => (
<div className="actions">
<div className="actions-group" style={{ display: "none" }}>
<button
onClick={() => handleProtect(row.original)}
className="bg-violet-500 hover:bg-violet-600"
>
<IconShieldExclamation />
</button>
{/* <button
onClick={() => handleOpenWindowForRemove(row.original)}
className="table-action-delete"
>
<IconDelete />
<span>
{t('delete')}
</span>
</button> */}
</div>
<div className="actions-group">
{row.original.protectStatus === 'PROTECTED' && (
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="table-action-download"
>
<IconFileDownload />
<span>
{t('download')}
</span>
</button>
)}
<button
onClick={() => {
handleView(row.original)
}}
className="table-action-view"
>
<IconEye />
<span>
{t('make-check')}
</span>
</button>
</div>
</div>
),
enableSorting: false,
enableColumnFilter: false,
},
],
[]
);
const handleView = async (file: FileItem) => {
console.log(`Просмотр файла: ${file.fileName}`);
console.log(file);
const fileInfo = await viewFileInfo(file.id);
console.log(fileInfo);
setOpenWindowChildren(() => {
return (
<FileInfoModalWindow fileInfo={fileInfo} setWindowClose={setOpenWindow} setWindowChildren={setOpenWindowChildren} />
)
})
setOpenWindow(true);
};
const handleDownload = async (file: FileItem) => {
setIsFileLoading(true);
try {
const response = await fetch(`/api/download/${file.id}`);
if (!response.ok) {
throw new Error(`error: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file._original?.fileName || file.fileName || `file-${file.id}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - ${file.fileName}`);
} catch (error) {
toast.error(t('failed-to-download-file'))
} finally {
setIsFileLoading(false);
}
};
const handleProtect = (file: FileItem) => {
console.log(`Щиток: ${file.fileName}`);
};
const handleOpenWindowForRemove = async (file: FileItem) => {
setOpenWindowChildren(() => {
return (
<div>
<h3 className="text-2xl">
{t('you-sure-you-want-to-delete')}
</h3>
<div className="mt-2 mb-8 text-center">{file.fileName}</div>
<div className="flex justify-center gap-4">
<button className="btn-primary btn-modal"
onClick={() => {
deleteMutation.mutate({
fileId: file.id,
removeParam: 1,
})
}}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? '...' : t('yes')}
</button>
<button className="btn-primary btn-modal"
onClick={() => {
setOpenWindow(false);
setOpenWindowChildren(null);
}}
>
{t('no')}
</button>
</div>
</div>
)
})
setOpenWindow(true);
};
const deleteMutation = useMutation({
mutationFn: ({ fileId, removeParam }: { fileId: string; removeParam: number }) =>
removeUserFile(fileId, removeParam),
onMutate: async ({ fileId, removeParam }: { fileId: string; removeParam: number }) => {
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
if (!old?.files) return old;
return {
...old,
files: old.files.filter(file => file.id !== fileId)
};
});
},
onError: () => {
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
toast.error(t('error'));
},
onSuccess: (response, { fileId }) => {
if (response === fileId) {
queryClient.invalidateQueries({
queryKey: ['userFilesData'],
refetchType: 'active'
});
queryClient.invalidateQueries({
queryKey: ['userFilesInfo']
});
setOpenWindow(false);
setOpenWindowChildren(null);
toast.success(t('file-has-been-deleted'));
}
},
});
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
let result = tableData;
if (!result) {
return [];
}
// Фильтр по типу файла
if (typeFilter !== 'all') {
result = result.filter(item => item.fileType === typeFilter);
}
// Фильтр по дате
if (dateFilter !== 'all') {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
switch (dateFilter) {
case 'today':
const todayStart = new Date().setHours(0, 0, 0, 0);
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate >= todayStart
} else {
return 0
}
});
break;
case 'week':
const weekAgo = now - sevenDays;
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate >= weekAgo;
}
});
break;
case 'month':
const monthAgo = now - thirtyDays;
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate >= monthAgo;
}
});
break;
case 'older':
const monthAgo2 = now - thirtyDays;
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate < monthAgo2;
}
});
break;
}
}
return result;
}, [tableData, typeFilter, dateFilter]);
useEffect(() => {
const currentPageRows = table.getRowModel().rows;
const pageCount = table.getPageCount();
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
table.setPageIndex(pagination.pageIndex - 1);
}
}, [filteredData, pagination.pageIndex]);
// Создание таблицы
const table = useReactTable({
data: filteredData,
columns,
state: {
sorting,
columnFilters,
pagination
},
autoResetPageIndex: false,
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
});
const pluralizeFiles = (number: number) => {
const translate = [t('file'), t('files-few'), t('files')];
return pluralize(number, translate[0], translate[1], translate[2], locale);
};
return (
<div className="tanstak-table-wrapper">
<h3
className="tanstak-table-title"
>
{(() => {
switch (fileType) {
case 'image':
return t('protected-image')
case 'video':
return t('protected-video')
case 'audio':
return t('protected-audio')
default:
return t('table-description')
}
})()}
</h3>
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
{/* Фильтры */}
<div className="tanstak-table-filtres">
<div className="table-filtres-wrapper">
<div className="table-filtres-item">
<div className="table-filtres-label">{t('date-filter')}:</div>
<DropDownList
value={dateFilter}
translatedValue={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates')
case 'week':
return t('for-a-week')
case 'month':
return t('for-a-month')
case 'older':
return t('older-than-a-month')
default:
return t('today')
}
})()}
callBack={setDateFilter}
>
<li value="all">
{t('all-dates')}
</li>
<li value="today">
{t('today')}
</li>
<li value="week">
{t('for-a-week')}
</li>
<li value="month">
{t('for-a-month')}
</li>
<li value="older">
{t('older-than-a-month')}
</li>
</DropDownList>
</div>
{fileType === "all" && (
<div className="table-filtres-item">
<div className="table-filtres-label">{t('type-filter')}:</div>
<DropDownList
value={typeFilter}
translatedValue={
(() => {
switch (typeFilter) {
case 'image':
return t('images')
case 'video':
return t('videos')
case 'audio':
return t('audios')
default:
return t('all-types')
}
})()
}
callBack={setTypeFilter}
>
<li value="all">
{t('all-types')}
</li>
<li value="image">
{t('images-few')}
</li>
<li value="video">
{t('videos')}
</li>
<li value="audio">
{t('audios')}
</li>
</DropDownList>
</div>
)}
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
callBack={table.setPageSize}
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
{t('show')} {pageSize}
</li>
))}
</DropDownList>
</div>
</div>
{/* Таблица */}
<div className="tanstak-table-block">
<table className="tanstak-table">
<thead className="tanstak-table-head">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
>
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header as string}
</th>
))}
</tr>
))}
</thead>
<tbody className="tanstak-table-body">
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Пагинация */}
<div className="tanstak-table-pagination">
<div className="pagination-info">
<span className="pagination-info-pages">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
</strong>
</span>
<span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
</span>
</div>
<div className="pagination-controls">
{table.getCanPreviousPage() && (
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
)}
{table.getCanPreviousPage() && (
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
)}
<div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
const pageIndex = Math.max(
0,
Math.min(
table.getPageCount() - 5,
table.getState().pagination.pageIndex - 2
)
) + i;
if (pageIndex < table.getPageCount()) {
return (
<button
key={pageIndex}
className={`${table.getState().pagination.pageIndex === pageIndex
? 'current'
: 'other'
}`}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
return null;
})}
</div>
{table.getCanNextPage() && (
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
)}
{table.getCanNextPage() && (
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
)}
</div>
</div>
</div >
);
}
+72 -28
View File
@@ -2,7 +2,7 @@
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react'; import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
import { IconShieldAdd } from '@/app/ui/icons/icons'; import { IconShieldAdd } from '@/app/ui/icons/icons';
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload'; import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus, checkOperationPrice } from '@/app/actions/fileUpload';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker'; import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
@@ -113,8 +113,16 @@ export default function UploadSectionFile({
setError(null); setError(null);
const newFiles: SelectedFile[] = []; 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) { for (const file of files) {
if (selectedFileNames.has(file.name)) {
duplicateNames.push(file.name);
continue;
}
const validation = validateFile(file); const validation = validateFile(file);
if (!validation.isValid) { if (!validation.isValid) {
@@ -141,6 +149,8 @@ export default function UploadSectionFile({
} }
} }
currentFileNames.add(file.name);
newFiles.push({ newFiles.push({
file, file,
name: file.name, name: file.name,
@@ -152,8 +162,13 @@ export default function UploadSectionFile({
}); });
} }
if (duplicateNames.length > 0) {
const uniqueDuplicates = [...new Set(duplicateNames)];
setError(`${t('duplicate-files')}: ${uniqueDuplicates.join(', ')}`);
}
setSelectedFiles(prev => [...prev, ...newFiles]); setSelectedFiles(prev => [...prev, ...newFiles]);
}, [fileType, t]); }, [fileType, selectedFiles]);
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => { const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
const files = Array.from(event.target.files || []); const files = Array.from(event.target.files || []);
@@ -204,6 +219,7 @@ export default function UploadSectionFile({
f.uploadId !== fileId && f.name !== fileId f.uploadId !== fileId && f.name !== fileId
)); ));
} else { } else {
console.log('clear all')
selectedFiles.forEach(file => { selectedFiles.forEach(file => {
if (file.preview) { if (file.preview) {
URL.revokeObjectURL(file.preview); URL.revokeObjectURL(file.preview);
@@ -223,10 +239,21 @@ export default function UploadSectionFile({
setError(null); setError(null);
}; };
const uploadSingleFile = async (fileInfo: SelectedFile): Promise<void> => { const uploadSingleFile = async (fileInfo: SelectedFile, uploadType: 'single' | 'multi'): Promise<void> => {
const fileId = `${fileInfo.name}-${Date.now()}`; const fileId = `${fileInfo.name}-${Date.now()}`;
isCancelledRef.current.set(fileId, false); 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 { try {
setSelectedFiles(prev => prev.map(f => setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, status: 'uploading' } : f f.name === fileInfo.name ? { ...f, status: 'uploading' } : f
@@ -302,17 +329,7 @@ export default function UploadSectionFile({
} catch (error) { } catch (error) {
if (!isCancelledRef.current.get(fileId)) { if (!isCancelledRef.current.get(fileId)) {
const errorMessage = (() => { const errorMessage = (typeof error === 'string') ? t(error) : t('error-uploading-file');
switch (error) {
case 'duplicate-file':
return t('error-duplicate-file');
case 'user-not-have-tokens-for-protect':
return t('error-user-not-have-tokens-for-protect');
default:
return t('error-uploading-file');
}
})();
setSelectedFiles(prev => prev.map(f => setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, status: 'error', error: errorMessage } : f f.name === fileInfo.name ? { ...f, status: 'error', error: errorMessage } : f
)); ));
@@ -325,13 +342,20 @@ export default function UploadSectionFile({
}, [selectedFiles]); }, [selectedFiles]);
const handleStartAllUploads = useCallback(async (): Promise<void> => { 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 uploadNextBatch = async () => {
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending'); const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
const batch = currentPending.slice(0, maxParallelUploads); const batch = currentPending.slice(0, maxParallelUploads);
if (batch.length === 0) return; if (batch.length === 0) return;
await Promise.all(batch.map(file => uploadSingleFile(file))); await Promise.all(batch.map(file => uploadSingleFile(file, 'multi')));
if (selectedFilesRef.current.some(f => f.status === 'pending')) { if (selectedFilesRef.current.some(f => f.status === 'pending')) {
await uploadNextBatch(); await uploadNextBatch();
@@ -339,6 +363,10 @@ export default function UploadSectionFile({
}; };
await uploadNextBatch(); await uploadNextBatch();
} else {
const errorMessage = `${t('there-are-not-enough-funds-to-protect-files', { count: priceCheck?.tokens_count })}`;
setError(errorMessage);
}
}, [maxParallelUploads]); }, [maxParallelUploads]);
const getTotalProgress = useMemo(() => { const getTotalProgress = useMemo(() => {
@@ -409,7 +437,6 @@ export default function UploadSectionFile({
})); }));
} }
return ( return (
<div className="upload-section" > <div className="upload-section" >
<h3> <h3>
@@ -464,8 +491,8 @@ export default function UploadSectionFile({
{ {
error && ( error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg" > <div className="error-message">
<p className="text-red-600 font-medium" > {error} </p> <p className="error-message-text"> {error} </p>
</div> </div>
) )
} }
@@ -575,15 +602,32 @@ export default function UploadSectionFile({
</> </>
) )
} }
<div> <div
<span className={ className="flex"
`upload-progress ${file.status === 'completed' ? 'text-green-600' : >
file.status === 'uploading' ? 'text-blue-600' : {file.status !== 'error' && (
'text-gray-600' <div className="upload-progress-circle">
}` <svg viewBox="0 0 36 36" className="circular-chart">
}> <path
{file.progress} % className="circle-bg"
</span> 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 < button
className="btn btn-cancel" className="btn btn-cancel"
onClick={() => handleClearFile(file.uploadId || file.name)} onClick={() => handleClearFile(file.uploadId || file.name)}
@@ -617,7 +661,7 @@ export default function UploadSectionFile({
<div className="mt-2 text-center" > <div className="mt-2 text-center" >
<button <button
className="btn btn-confirm" className="btn btn-confirm"
onClick={() => uploadSingleFile(file) onClick={() => uploadSingleFile(file, 'single')
} }
type="button" type="button"
disabled={uploadingCount >= maxParallelUploads} disabled={uploadingCount >= maxParallelUploads}
+11 -18
View File
@@ -14,10 +14,6 @@ export default function YandexAuthWithScript() {
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const [showButton, setShowButton] = useState(true); const [showButton, setShowButton] = useState(true);
const handleLogin = (token: string) => {
console.log('handleLogin');
};
useEffect(() => { useEffect(() => {
if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return; if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return;
@@ -25,31 +21,28 @@ export default function YandexAuthWithScript() {
{ {
client_id: 'b356a7ceac0f4c50a3b434220f86bce0', client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
response_type: 'token', response_type: 'token',
redirect_uri: `http://localhost:2999/ru/login`, redirect_uri: `https://fallibly-desired-moonfish.cloudpub.ru/en/login`,
}, },
'http://localhost:2999', 'https://fallibly-desired-moonfish.cloudpub.ru',
{ {
parentId: buttonRef.current.id, parentId: buttonRef.current.id,
view: 'button', view: 'button',
buttonView: 'main', buttonView: 'main',
buttonTheme: 'light', buttonTheme: 'light',
buttonSize: 'm', buttonSize: 'l',
buttonBorderRadius: 10 buttonBorderRadius: 10
} }
) )
.then((result: any) => { .then(function (result: any) {
console.log('then'); console.log('you at least try?')
console.log(result); console.log(result);
if (result?.access_token) { return result.handler();
handleLogin(result.access_token);
} else {
setTimeout(() => {
setShowButton(false);
}, 1000)
}
}) })
.catch((error: any) => { .then(function (data: any) {
console.error('Yandex auth error:', error); console.log('Сообщение с токеном: ', data);
})
.catch(function (error: any) {
console.log('Что-то пошло не так: ', error);
}); });
}, [isReady]); }, [isReady]);
+15
View File
@@ -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>
);
}
+99
View File
@@ -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
+55
View File
@@ -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 // для оптимистичных обновлений
});
};
+45
View File
@@ -0,0 +1,45 @@
import { useQuery } from '@tanstack/react-query';
import { fetchLastClaims, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
export interface Case {
amount: number;
content: null | string;
created_at: string;
description: string;
id: number;
lawyer: null | string;
name: string;
page_number: number;
page_size: number;
priority: "HIGH" | "MEDIUM" | "LOW";
total_elements: number;
total_pages: number;
type: "ACTIVE" | "INACTIVE" | string;
updated_at: string;
violation_id: number;
file_id: string;
}
export interface useLastCase {
content: Case[],
page: number,
size: number,
totalElements: number,
totalPages: number,
}
export const useLastCases = (params: ComplaintsCaseQueryParams) => {
return useQuery({
queryKey: ['lastCases', params.page, params.size, params.sort_by, params.sort_direction],
queryFn: () => {
return fetchLastClaims(params)
},
select: (data: useLastCase | null) => {
if (!data) {
return null
}
return data;
},
placeholderData: (previousData) => previousData,
retry: false
});
};
@@ -0,0 +1,38 @@
import { useQuery } from '@tanstack/react-query';
import { fetchLastComplaint, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
export interface Complaint {
complaint_text: string;
created_at: string;
email: string;
id: number;
not_moderated: boolean;
status: "CREATED" | string;
updated_at: string;
violation_id: number;
file_id: string;
}
export interface useLastComplaint {
content: Complaint[],
page: number,
size: number,
totalElements: number,
totalPages: number,
}
export const useLastComplaints = (params: ComplaintsCaseQueryParams) => {
return useQuery({
queryKey: ['lastComplaints', params.page, params.size, params.sort_by, params.sort_direction],
queryFn: () => {
return fetchLastComplaint(params)
},
select: (data: useLastComplaint | null) => {
if (!data) {
return null
}
return data;
},
placeholderData: (previousData) => previousData,
retry: false
});
};
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMonitoringInfo } from '@/app/actions/monitoringActions';
interface MonitoringCount {
day: number;
month: number;
weekly: number;
}
export const useMonitoringCount = () => {
return useQuery({
queryKey: ['monitoringCount'],
queryFn: () => {
return fetchMonitoringInfo()
},
select: (data: MonitoringCount | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMonitoringStats } from '@/app/actions/monitoringActions';
interface MonitoringStats {
total_searches: number;
searches_by_status: {
processing: number;
completed: number;
failed: number;
total: number;
};
recent_searches: any[];
}
export const useMonitoringStats = () => {
return useQuery({
queryKey: ['monitoringStats'],
queryFn: () => {
return fetchMonitoringStats()
},
select: (data: MonitoringStats | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { getPaymentsMethods } from '@/app/actions/paymentAction';
export interface PaymentsMethods {
cardType: string,
expiryMonth: string,
expiryYear: string,
lastFour: string,
paymentMethodId: string
}
export const usePaymentsMethods = () => {
return useQuery({
queryKey: ['paymentsMethods'],
queryFn: () => {
return getPaymentsMethods()
},
select: (data: PaymentsMethods[] | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchTrackingStats } from '@/app/actions/trackingActions';
export interface useTrackingStats {
totalViews: number,
uniqueIps: number,
uniqueUsers: number,
documentsCount: number,
uniqueCountryCount: number,
uniqueCityCount: number,
firstViewDate: string,
lastViewDate: string
}
export const useTrackingStats = () => {
return useQuery({
queryKey: ['trackingStats'],
queryFn: () => {
return fetchTrackingStats()
},
select: (data: useTrackingStats | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -39,6 +39,7 @@ export interface UserData {
tariffs: Tariff[]; tariffs: Tariff[];
tariffInfo: TariffInfo; tariffInfo: TariffInfo;
permission: number; permission: number;
verifiedStatus?: 'unknow' | 'NOT_VERIFIED' | 'VERIFICATION_IN_PROGRESS' | 'VERIFIED' | 'VERIFICATION_FAILED';
} }
export const useUserProfile = () => { export const useUserProfile = () => {
@@ -72,6 +73,7 @@ export const useUserProfile = () => {
maxFileCounts: 0, maxFileCounts: 0,
}, },
permission: 0, permission: 0,
verifiedStatus: 'unknow'
}; };
} }
@@ -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,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
});
};
+36
View File
@@ -0,0 +1,36 @@
'use client'
import { useState, useEffect } from 'react'
export const MOBILE = 'MOBILE'
export const TABLET = 'TABLET'
export const DESKTOP = 'DESKTOP'
const getDevice = (width: number) => {
if (width < 768) return MOBILE
else if (width < 992) return TABLET
else return DESKTOP
}
export function useViewport() {
const [viewport, setViewport] = useState({
width: 0,
device: 'DESKTOP'
});
useEffect(() => {
const handleResize = () => {
setViewport({
width: window.innerWidth,
device: getDevice(window.innerWidth)
});
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return viewport;
}
+25
View File
@@ -0,0 +1,25 @@
// Форматирование даты из timestamp
export const formatDate = (timestamp: number | string) => {
if (!timestamp) {
return '---'
}
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
export const formatDateTime = (timestamp: number | string) => {
if (!timestamp) {
return '---'
}
const date = new Date(timestamp);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${hours}:${minutes}`;
};
+28
View File
@@ -0,0 +1,28 @@
export const getFileType = (fileName: string, allowedExtensions: {
images: string[],
videos: string[],
audios: string[],
documents: string[]
}): 'image' | 'video' | 'audio' | 'document' | 'unknown' => {
const lastDotIndex = fileName.lastIndexOf('.');
const extension = fileName.substring(lastDotIndex + 1).toLowerCase();
if (allowedExtensions.images.includes(extension)) {
return 'image';
}
if (allowedExtensions.videos.includes(extension)) {
return 'video';
}
if (allowedExtensions.audios.includes(extension)) {
return 'audio';
}
if (allowedExtensions.documents.includes(extension)) {
return 'document';
}
return 'unknown';
}
+42 -1
View File
@@ -2,6 +2,34 @@ import { QueryClient } from '@tanstack/react-query';
import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action'; import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action';
import { getUserFilesData } from '@/app/actions/fileEntity'; import { getUserFilesData } from '@/app/actions/fileEntity';
import { fetchReferralUserStats } from '@/app/actions/referralsActions'; import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
export interface allFilesExtensions {
videos: string[];
audios: string[];
images: string[];
documents: string[];
maxFileSize: number;
}
export async function getAllFilesExtensions(): Promise<allFilesExtensions> {
const [video, audio, image, document] = await Promise.all([
getAllowedFilesExtensions('video'),
getAllowedFilesExtensions('audio'),
getAllowedFilesExtensions('image'),
getAllowedFilesExtensions('document')
]);
const result: allFilesExtensions = {
videos: video?.file_extension ? video.file_extension : [],
audios: audio?.file_extension ? audio.file_extension : [],
images: image?.file_extension ? image.file_extension : [],
documents: document?.file_extension ? document.file_extension : [],
maxFileSize: video?.max_file_size ? video.max_file_size : 0
}
return result;
}
export async function prefetchLayoutQueries(queryClient: QueryClient) { export async function prefetchLayoutQueries(queryClient: QueryClient) {
await Promise.all([ await Promise.all([
@@ -12,7 +40,15 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ['userFilesData'], queryKey: ['userFilesData'],
queryFn: () => { queryFn: () => {
return getUserFilesData(1, 10000); return getUserFilesData({
page: 1,
pageSize: 10,
sortBy: '',
sortOrder: '',
type: '',
date: '',
query: ''
});
}, },
staleTime: 30 * 1000 staleTime: 30 * 1000
}), }),
@@ -35,5 +71,10 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
} }
} }
}), }),
queryClient.prefetchQuery({
queryKey: ['allFilesExtensions'],
queryFn: () => getAllFilesExtensions(),
staleTime: 5 * 60 * 1000
}),
]); ]);
} }
+11
View File
@@ -0,0 +1,11 @@
'use client';
import Providers from './getQueryServer';
export function WatchDocProviders({ children }: { children: React.ReactNode }) {
return (
<Providers>
{children}
</Providers>
);
}
-23
View File
@@ -1,34 +1,11 @@
'use client' 'use client'
import { import {
isServer,
QueryClient,
QueryClientProvider, QueryClientProvider,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import { getQueryClient } from '@/app/providers/getQueryClient'; import { getQueryClient } from '@/app/providers/getQueryClient';
/* export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
return makeQueryClient()
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
} */
export default function Providers({ children }: { children: React.ReactNode }) { export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient(); const queryClient = getQueryClient();
+70
View File
@@ -0,0 +1,70 @@
@use './variable.scss' as v;
.loading-animation {
display: block;
text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.global-spinner {
display: block;
width: 24px;
height: 24px;
margin: 0 auto;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 4px solid #e5e7eb;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
&.large {
width: 80px;
height: 80px;
&::before {
border: 10px solid #e5e7eb;
border-top: 10px solid #667eea;
}
}
}
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
/* min-width: 80px;
min-height: 80px; */
&.start {
justify-content: start;
}
.loading-animation {
display: block;
text-align: center;
position: relative;
left: auto;
top: auto;
transform: none;
}
.global-spinner {
display: inline-block;
width: 24px;
height: 24px;
position: relative;
}
}
+211 -2
View File
@@ -6,6 +6,10 @@
@use './reports.scss'; @use './reports.scss';
@use './privacy-and-terms-pages.scss'; @use './privacy-and-terms-pages.scss';
@use './VKLogin.scss'; @use './VKLogin.scss';
@use './animation.scss';
@use './violation-details.scss';
@use './watch-doc.scss';
@use './tracking-statistic.scss';
.page-title { .page-title {
font-size: 30px; font-size: 30px;
@@ -151,10 +155,50 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:disabled {
opacity: 0.5;
}
}
.btn,
.btn-s,
.btn-l {
border: none;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
&:disabled {
opacity: 0.5;
}
}
.btn {
padding: 12px 24px;
font-size: 14px;
border-radius: 10px;
}
.btn-s {
padding: 6px 12px;
font-size: 12px;
border-radius: 6px;
}
.btn-l {
padding: 16px 32px;
font-size: 16px;
border-radius: 12px;
} }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, v.$p-color, v.$s-color); /* background: linear-gradient(135deg, v.$p-color, v.$s-color); */
background: #6366f1;
color: v.$white; color: v.$white;
border: 2px solid transparent; border: 2px solid transparent;
@@ -179,6 +223,16 @@
} }
} }
.btn-cancel {
background: v.$bg-hover;
color: v.$text-p;
border: 2px solid v.$border-color-1;
&:hover {
transform: translateY(-2px);
}
}
.btn-outline { .btn-outline {
background: transparent; background: transparent;
border: 2px solid v.$p-color; border: 2px solid v.$p-color;
@@ -190,6 +244,14 @@
} }
} }
.btn-warning {
background: v.$color-warning;
&:hover {
background: v.$color-warning-hover;
}
}
.divider { .divider {
text-align: center; text-align: center;
margin: 20px 0; margin: 20px 0;
@@ -312,6 +374,7 @@
bottom: 10px; bottom: 10px;
left: 10px; left: 10px;
color: #fffc; color: #fffc;
font-size: 12px;
} }
} }
@@ -347,7 +410,7 @@
svg { svg {
margin-right: 15px; margin-right: 15px;
width: 20px; width: 20px;
height: 20px; min-width: 20px;
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
@@ -552,10 +615,72 @@
} }
} }
.dropdown-list-portal {
.dropdown-list {
width: 100%;
margin-top: 4px;
background-color: #ffffff;
border: 2px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.dropdown-item {
display: flex;
align-items: center;
width: 100%;
padding: 8px 16px;
font-size: 12px;
text-align: left;
cursor: pointer;
user-select: none;
&:hover {
background-color: #dbeafe;
}
&.current {
font-weight: 700;
}
}
}
.dropdown-wrapper { .dropdown-wrapper {
position: relative; position: relative;
width: 100%; width: 100%;
&.monitoring-dropdown {
max-width: 184px;
margin: 0 auto;
.dropdown-button {
font-size: 12px;
}
.dropdown-list {
position: absolute;
left: 0;
right: auto;
z-index: 9999;
width: 100%;
margin-top: 4px;
transform-origin: top left;
background-color: #ffffff;
border: 2px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
&:focus {
outline: none;
}
}
}
.dropdown-button { .dropdown-button {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -635,3 +760,87 @@
.yaPreloadingSuggestBlockContainer { .yaPreloadingSuggestBlockContainer {
height: 52px !important; height: 52px !important;
} }
.error-message {
margin-top: 1rem;
padding: 0.75rem;
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 0.5rem;
.error-message-text {
color: #dc2626;
font-weight: 500;
}
}
.yandex-auth-button {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 100%;
box-sizing: border-box;
background: v.$black;
color: #FFFFFF;
&:hover {
background: #1a1a1a;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
/* transform: translateY(-2px); */
}
}
.search-settings {
&-wrapper {
margin-bottom: 20px;
background: #f0f9ff;
border: 1px solid #bfdbfe;
border-radius: 12px;
margin-bottom: 20px;
padding: 16px;
color: #1e40af;
}
&-header {
display: flex;
gap: 10px;
font-weight: 600;
&.loading {
opacity: 0.5;
}
}
&-item {
cursor: pointer;
span {
margin-left: 4px;
color: v.$red;
&.active {
color: v.$green;
}
}
&:hover {
text-decoration: underline;
}
}
/* &-action-panel {
display: flex;
gap: 10px;
} */
}
+3
View File
@@ -53,3 +53,6 @@ path:focus {
} }
@import "tailwindcss"; @import "tailwindcss";
@import 'swiper/css';
@import 'swiper/css/navigation';
@import 'swiper/css/pagination';
+111 -27
View File
@@ -4,11 +4,12 @@
display: flex; display: flex;
justify-content: end; justify-content: end;
align-items: center; align-items: center;
margin-bottom: 30px;
background: v.$white; background: v.$white;
padding: 20px 30px; padding: 20px 30px;
border-radius: 20px; border-radius: 20px;
box-shadow: 0 4px 20px v.$shadow-1; box-shadow: 0 4px 20px v.$shadow-1;
max-width: 1920px;
margin: 0 auto 30px;
@media (max-width: 490px) { @media (max-width: 490px) {
padding: 20px 20px; padding: 20px 20px;
@@ -49,6 +50,10 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
/* &.active {
background: v.$color-warning;
} */
} }
} }
@@ -85,6 +90,11 @@
@media (max-width: 440px) { @media (max-width: 440px) {
padding: 8px; padding: 8px;
} }
&:hover {
background: v.$p-color;
color: v.$white;
}
} }
.tokens-count { .tokens-count {
@@ -110,7 +120,7 @@
background: v.$white; background: v.$white;
border-radius: 12px; border-radius: 12px;
box-shadow: 10px 10px 20px 10px v.$shadow-1; box-shadow: 10px 10px 20px 10px v.$shadow-1;
min-width: 320px; min-width: 350px;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-10px); transform: translateY(-10px);
@@ -124,8 +134,10 @@
} }
} }
.notification-dropdown { .notification {
.notification-header { /* &-dropdown {} */
&-header {
padding: 15px 20px; padding: 15px 20px;
border-bottom: 1px solid v.$b-color-1; border-bottom: 1px solid v.$b-color-1;
display: flex; display: flex;
@@ -138,64 +150,136 @@
font-weight: 600; font-weight: 600;
color: v.$text-p; color: v.$text-p;
} }
}
.notification-count { &-count {
font-size: 12px; font-size: 10px;
color: v.$p-color; background: v.$p-color;
padding: 5px;
color: v.$white;
font-weight: 500; font-weight: 500;
} border-radius: 5px;
margin-left: 5px;
user-select: none;
} }
.notification-list { &-list {
max-height: 400px; max-height: 600px;
overflow-y: auto; overflow-y: auto;
} }
.notification-item { &-item {
display: flex; display: flex;
gap: 12px; gap: 8px;
padding: 15px 20px; padding: 10px 14px;
border-bottom: 1px solid v.$b-color-1; border-bottom: 1px solid v.$b-color-1;
transition: background-color 0.2s; transition: background-color 0.2s;
cursor: pointer; align-items: center;
&.loading {
opacity: 0.5;
}
} }
.notification-title { &-title {
display: block; display: block;
font-size: 15px; font-size: 13px;
font-weight: 700; font-weight: 700;
color: v.$text-p; color: v.$text-p;
margin-bottom: 6px; margin-bottom: 6px;
} }
.notification-icon { &-content {
font-size: 24px; width: 100%;
flex-shrink: 0; position: relative;
padding: 5px;
} }
.notification-text { &-item-header {
display: flex;
justify-content: space-between;
}
&-text {
font-size: 13px; font-size: 13px;
color: v.$text-s; color: v.$text-s;
margin-bottom: 4px;
font-weight: 500; font-weight: 500;
} }
.notification-time { &-data {
font-size: 12px; font-size: 10px;
color: v.$text-m; color: v.$text-m;
} }
.notification-footer { &-icon {
padding: 12px 20px; font-size: 12px;
cursor: pointer;
height: max-content;
color: v.$p-color;
align-self: start;
padding-top: 5px;
svg {
fill: v.$p-color;
&:hover {
fill: v.$p-color-hover;
}
}
&:hover {
color: v.$p-color-hover;
}
}
&-mark-all-as-read {
font-size: 10px;
color: v.$p-color;
text-align: end;
cursor: pointer;
&:hover {
color: v.$p-color-hover;
}
}
&-footer {
padding: 8px 20px;
border-top: 1px solid v.$b-color-1; border-top: 1px solid v.$b-color-1;
text-align: center; text-align: center;
position: relative;
.notification-link { .notification-link {
color: v.$p-color; color: v.$p-color;
font-size: 14px; font-size: 16px;
font-weight: 500; font-weight: 600;
text-decoration: none; text-decoration: none;
cursor: pointer;
&:disabled {
opacity: 0.5;
} }
&:hover {
color: v.$p-color-hover;
}
}
}
&-unread-count {
position: absolute;
right: -5px;
bottom: -5px;
/* width: 22px;
height: 22px; */
border-radius: 15px;
background: v.$red;
color: v.$white;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
padding: 2px 5px;
} }
} }
+44 -4
View File
@@ -112,6 +112,15 @@
&.password { &.password {
padding-right: 32px; padding-right: 32px;
} }
&:disabled {
opacity: 0.5;
}
&.valid-card {
border: 2px solid #2f9e44;
background: #ecf8ee;
}
} }
.form-options { .form-options {
@@ -172,16 +181,47 @@
margin-bottom: 18px; margin-bottom: 18px;
margin: 20px 0 0 0; margin: 20px 0 0 0;
&:disabled {
opacity: 0.5;
}
&:hover:not(:disabled) { &:hover:not(:disabled) {
transform: translateY(-2px); transform: translateY(-2px);
} }
&:disabled {
opacity: 0.5;
background: linear-gradient(135deg, #414291, #4b3975);
&:hover {
transform: none;
}
}
&.refresh {
width: 100%;
display: flex;
justify-content: center;
gap: 3px;
white-space: nowrap;
}
} }
.form-group-confrim-window { .form-group-confrim-window {
min-width: 500px;
&-button-group {
justify-content: center;
gap: 10px;
display: grid;
grid-template-columns: 50% 50%;
}
@media (max-width: 550px) {
min-width: auto;
&-button-group {
flex-direction: column;
gap: 10px;
}
}
.btn { .btn {
margin: 0; margin: 0;
} }
File diff suppressed because it is too large Load Diff
+130 -3
View File
@@ -201,10 +201,18 @@
} }
.subscription-grid { .subscription-grid {
display: grid; /* display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-columns: repeat(4, auto);
gap: 20px; gap: 20px;
margin-bottom: 25px; margin-bottom: 25px;
justify-content: center; */
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 278px));
gap: 18px;
margin-bottom: 25px;
justify-content: center;
} }
.plan-card { .plan-card {
@@ -216,7 +224,7 @@
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer; cursor: pointer;
overflow: visible; overflow: visible;
max-width: 350px; max-width: 278px;
.plan-header { .plan-header {
text-align: center; text-align: center;
@@ -249,6 +257,7 @@
.plan-period { .plan-period {
color: #64748b; color: #64748b;
font-size: 0.85rem; font-size: 0.85rem;
text-transform: lowercase;
} }
.plan-tokens { .plan-tokens {
@@ -260,6 +269,11 @@
font-weight: 600; font-weight: 600;
margin: 12px auto; margin: 12px auto;
display: inline-block; display: inline-block;
&.header {
font-size: 2rem;
font-weight: 800;
}
} }
.plan-features { .plan-features {
@@ -293,6 +307,97 @@
} }
} }
} }
.token-package {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 16px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-4px) scale(1.02);
border-color: #6366f1;
box-shadow: 0 12px 30px rgba(99, 102, 241, 0.2);
&::before {
left: 100%;
}
}
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1), transparent);
transition: left 0.5s ease;
}
&.selected {
border-color: #10b981;
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
transform: scale(1.05);
}
.token-package-amount {
font-size: 1.8rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 6px;
}
.token-package-text {
font-size: 0.85rem;
color: #64748b;
margin-bottom: 8px;
}
.token-package-price {
font-size: 1.3rem;
font-weight: 600;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
}
.token-package-detail {
color: #64748b;
font-size: 0.8rem;
margin-top: 6px;
}
}
.plan-button {
width: 100%;
max-width: 280px;
padding: 12px 18px;
border: none;
border-radius: 12px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
display: block;
margin: 0 auto;
}
.selected-token-info {
margin-top: 8px;
color: #64748b;
font-size: 0.85rem;
text-align: center;
}
} }
.secure-payments-wrapper { .secure-payments-wrapper {
@@ -319,16 +424,38 @@
.secure-payments-card { .secure-payments-card {
padding: 12px; padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-around;
/* &:has(.yookassa) {
align-self: flex-end;
} */
.secure-payments-card-header { .secure-payments-card-header {
font-size: 1.8rem; font-size: 1.8rem;
margin-bottom: 8px; margin-bottom: 8px;
height: 44px;
&.yookassa {
max-width: 88px;
margin: 0 auto 8px;
display: flex;
align-items: center;
}
} }
h4 { h4 {
color: #1e293b; color: #1e293b;
margin-bottom: 6px; margin-bottom: 6px;
font-size: 0.95rem; font-size: 0.95rem;
height: 32px;
display: flex;
justify-content: center;
svg {
max-width: 88px;
margin: 0 auto 8px;
}
} }
p { p {
+38 -15
View File
@@ -7,6 +7,7 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin-bottom: 30px; margin-bottom: 30px;
max-width: calc(100vw - var(--side-bar-width) - 30px);
.enhanced-chart-title { .enhanced-chart-title {
font-size: 22px; font-size: 22px;
@@ -14,12 +15,7 @@
color: white; color: white;
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 20px;
&::before {
content: '🔍';
margin-right: 12px;
font-size: 28px;
}
} }
.search-stats-grid { .search-stats-grid {
@@ -49,7 +45,13 @@
.search-stat-icon { .search-stat-icon {
font-size: 32px; font-size: 32px;
margin-bottom: 15px; margin-bottom: 15px;
display: block; display: flex;
justify-content: center;
.icon {
width: 50px;
height: 50px;
}
} }
.search-stat-value { .search-stat-value {
@@ -68,11 +70,19 @@
} }
.analytics-grid { .analytics-grid {
display: flex;
flex-wrap: wrap;
gap: 30px; gap: 30px;
margin-bottom: 40px; margin-bottom: 10px;
justify-content: space-between; display: grid;
grid-template-columns: 1fr 1fr;
.tanstak-table-wrapper .tanstak-table {
width: 100%;
}
@media (max-width: 1370px) {
flex-direction: column;
grid-template-columns: 1fr;
}
.analytics-card { .analytics-card {
background: white; background: white;
@@ -80,10 +90,12 @@
padding: 30px; padding: 30px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease; transition: all 0.3s ease;
flex: 1;
min-width: 350px;
max-width: calc(50% - 15px);
margin-bottom: 20px; margin-bottom: 20px;
width: 100%;
@media (max-width: 970px) {
max-width: calc(100vw - var(--side-bar-width) - 30px);
}
} }
.analytics-header { .analytics-header {
@@ -152,6 +164,7 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 20px 40px rgba(30, 58, 138, 0.4); box-shadow: 0 20px 40px rgba(30, 58, 138, 0.4);
max-width: calc(100vw - var(--side-bar-width) - 30px);
&::before { &::before {
content: ''; content: '';
@@ -189,10 +202,14 @@
.summary-stats { .summary-stats {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: 1fr 2fr 1fr;
gap: 30px; gap: 30px;
position: relative; position: relative;
z-index: 2; z-index: 2;
@media (max-width: 940px) {
grid-template-columns: 1fr;
}
} }
.summary-stat { .summary-stat {
@@ -204,6 +221,12 @@
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.4s ease; transition: all 0.4s ease;
&.center {
display: flex;
justify-content: space-around;
gap: 10px;
}
&:hover { &:hover {
transform: translateY(-8px) scale(1.02); transform: translateY(-8px) scale(1.02);
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
+58
View File
@@ -219,6 +219,64 @@
padding: 20px; padding: 20px;
} }
} }
&.verefication-section {
height: 100%;
display: flex;
flex-direction: column;
.verefication-block {
flex-grow: 1;
display: flex;
flex-direction: column;
h3 {
margin-bottom: 10px;
}
.drag-drop-zone {
border: 2px dashed #6366f1;
border-radius: .75rem;
padding: 10px;
}
.selected-file-action-panel {
display: flex;
justify-content: center;
gap: 10px;
}
.selected-file-actions {
display: flex;
justify-content: center;
gap: 10px;
}
.description {
font-size: 12px;
color: v.$text-s;
}
.btn {
padding: 5px 12px;
}
.btn-confirm {
background: v.$p-color;
color: v.$white;
background: linear-gradient(135deg, v.$p-color, v.$s-color);
color: v.$white;
border: 2px solid transparent;
padding: 2px 10px;
}
.btn-cancel {
border: 2px solid v.$p-color;
padding: 2px 10px;
}
}
}
} }
.danger-zone { .danger-zone {
+87
View File
@@ -0,0 +1,87 @@
@use './variable.scss' as v;
:root {
--p: #6366f1;
--pd: #4f46e5;
--pl: #e0e7ff;
--g: #10b981;
--a: #f59e0b;
--r: #f43f5e;
--s: #0ea5e9;
--bg: #f1f5f9;
--sur: #fff;
--brd: #e2e8f0;
--tx: #1e293b;
--mu: #64748b;
--rad: 14px;
--sh: 0 2px 10px rgba(0, 0, 0, .07);
}
.tracking-page {
.general-statistic {
background: linear-gradient(135deg, v.$p-color 55%, v.$s-color);
border-radius: var(--rad);
padding: 32px;
color: #fff;
margin-bottom: 24px;
position: relative;
overflow: hidden;
&-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 24px;
}
&-title {
font-size: 24px;
font-weight: 800;
}
&-sub {
font-size: 13px;
opacity: .75;
margin-top: 4px;
}
&-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 14px;
}
&-item {
background: rgba(255, 255, 255, .13);
backdrop-filter: blur(8px);
border-radius: 12px;
padding: 16px 18px;
border: 1px solid rgba(255, 255, 255, .15);
&-ico {
font-size: 20px;
margin-bottom: 6px;
svg {
fill: v.$white;
color: v.$white;
}
}
&-lbl {
font-size: 11px;
opacity: .8;
text-transform: uppercase;
letter-spacing: .5px;
margin-top: 4px;
}
&-val {
font-size: 30px;
font-weight: 800;
line-height: 1;
}
}
}
}
+16
View File
@@ -1,4 +1,5 @@
$p-color: #6366f1; $p-color: #6366f1;
$p-color-hover: #1e0ff0;
$s-color: #8b5cf6; $s-color: #8b5cf6;
$text-p: #1f2937; $text-p: #1f2937;
$text-s: #4b5563; $text-s: #4b5563;
@@ -10,16 +11,31 @@ $bg-hover: #e9ecef;
$bg-light: #f8f9ff; $bg-light: #f8f9ff;
$bg-darkening: #0000007a; $bg-darkening: #0000007a;
$shadow-1: #0000001a; $shadow-1: #0000001a;
$shadow-2: #8686861a;
$white: #fff; $white: #fff;
$black: #000;
$red: #dc2626; $red: #dc2626;
$green: #00a63e;
$green-2: #10b981;
$grey: #5a6e8a;
$p-color-disabled: #6365f18e; $p-color-disabled: #6365f18e;
$s-color-disabled: #8a5cf663; $s-color-disabled: #8a5cf663;
$border-color-1: #e2e8f0;
$border-color-1-hover: #b1b7be;
$color-warning: #fab005;
$color-warning-hover: #fa9805;
$color-warning-2: #f08c00;
$color-image: #f08c00; $color-image: #f08c00;
$color-video: #2f9e44; $color-video: #2f9e44;
$color-audio: #1971c2; $color-audio: #1971c2;
$color-document: #a561e6; $color-document: #a561e6;
$status-new: #fab005;
$status-showed: #5a6e8a;
$status-in-work: #3b82f6;
$authorized-use: #2ecc71;
:root { :root {
--side-bar-width: 280px; --side-bar-width: 280px;
File diff suppressed because it is too large Load Diff
+319
View File
@@ -0,0 +1,319 @@
@use './variable.scss' as v;
:root {
--bg: #f5f6fa;
--card: #fff;
--border: #e2e5ef;
--border2: #d0d4e4;
--text: #0f1221;
--text2: #4b5270;
--muted: #8890ad;
--accent: #5b47e0;
--accent2: #7c5cf6;
--green: #059669;
--green-bg: #ecfdf5;
--green-bd: #a7f3d0;
--red: #dc2626;
--red-bg: #fef2f2;
--amber: #b45309;
--shadow: 0 2px 12px rgba(15, 18, 33, .07);
}
.watch-doc {
&-wrapper {
background: var(--bg);
color: var(--text);
font-size: 15px;
line-height: 1.6;
min-height: 100vh;
.btn-acc {
background: var(--accent);
color: #fff;
box-shadow: 0 3px 12px rgba(91, 71, 224, .28);
margin: 0 auto;
display: block;
&:hover {
background: var(--accent2);
text-decoration: none;
}
}
.btn-out {
background: var(--card);
color: var(--text2);
border: 1px solid var(--border2);
width: 100%;
&:hover {
border-color: var(--accent);
color: var(--accent);
text-decoration: none;
}
}
.document-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
gap: 10px;
&__nav {
display: flex;
align-items: center;
gap: 8px;
}
&__next,
&__prev {
font-size: 0.75rem;
color: var(--accent);
background: none;
border: 1px solid var(--border2);
border-radius: 6px;
padding: 3px 10px;
cursor: pointer;
font-family: inherit;
display: inline;
&:hover {
background: var(--accent);
color: #fff;
}
}
&__current-page {
font-size: 0.75rem;
color: var(--muted);
display: inline;
}
&__fullscreen {
font-size: .75rem;
color: var(--accent);
background: none;
border: 1px solid var(--border2);
border-radius: 6px;
padding: 3px 10px;
cursor: pointer;
font-family: inherit;
&:hover {
background: var(--accent);
color: #fff;
}
}
}
.prev-wrap {
min-height: 400px;
min-width: 300px;
position: relative;
display: flex;
justify-content: center;
}
.pdf-canvas {
display: block;
max-width: 100%;
height: auto;
}
.pdf-container {
width: 100%;
overflow-x: auto;
}
.pdf-container:fullscreen {
background: #f5f5f5;
padding: 20px;
gap: 20px;
.prev-wrap {
flex: 1;
min-height: auto;
display: flex;
justify-content: center;
align-items: center;
background: #f5f5f5;
}
.pdf-canvas {
max-width: 100%;
max-height: calc(100vh - 120px);
width: auto;
height: auto;
object-fit: contain;
}
.document-controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-radius: 12px;
padding: 0.75rem 1.5rem;
z-index: 1000;
min-width: 300px;
justify-content: center;
}
}
.error-container {
text-align: center;
}
}
&-page {
max-width: 880px;
margin: 0 auto;
padding: 0 16px 60px;
}
&-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 0 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 28px;
.header-logo {
font-size: 1.1rem;
font-weight: 700;
color: v.$p-color;
display: flex;
align-items: center;
gap: 7px;
}
.header-user {
font-size: .83rem;
color: var(--text2);
background: #fff;
border: 1px solid var(--border);
border-radius: 8px;
padding: 7px 14px;
}
}
&-status {
background: var(--green-bg);
border: 1.5px solid var(--green-bd);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
margin-bottom: 22px;
padding: 20px 22px;
border-radius: 14px;
.st-title {
color: var(--green);
font-size: 1.05rem;
font-weight: 700;
margin-bottom: 3px;
}
.st-sub {
font-size: .83rem;
color: var(--text2);
line-height: 1.5;
}
}
&-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
&-footer {
text-align: center;
padding: 28px 0 0;
color: var(--muted);
font-size: .76rem;
border-top: 1px solid var(--border);
margin-top: 14px;
display: flex;
justify-content: center;
gap: 10px;
}
&-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow);
height: 100%;
.card-head {
padding: 14px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 9px;
font-size: .78rem;
font-weight: 700;
color: var(--text);
text-transform: uppercase;
letter-spacing: .6px;
}
.card-body {
padding: 14px 20px;
.owner-row {
display: flex;
align-items: center;
gap: 12px;
}
.owner-av {
width: 42px;
height: 42px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.1rem;
font-weight: 700;
flex-shrink: 0;
}
.owner-name {
font-weight: 700;
margin-bottom: 5px;
}
.owner-meta {
color: var(--muted);
}
}
.download-description {
font-size: .82rem;
color: var(--text2);
margin-bottom: 14px;
text-align: center;
}
}
&-actions {
display: flex;
gap: 9px;
flex-wrap: wrap;
margin-top: 16px;
}
}
+5 -4
View File
@@ -1,13 +1,14 @@
import TanstakFilesTable from '@/app/components/TanstakTable'; import TanstakFilesTable from '@/app/components/tanstak-table/TanstakTable';
type FilesTable = { type FilesTable = {
fileType: string fileType: string;
showFileLink?: boolean;
} }
export default function FilesTable({ fileType }: FilesTable) { export default function FilesTable({ fileType, showFileLink }: FilesTable) {
return ( return (
<div className="block-wrapper"> <div className="block-wrapper">
<TanstakFilesTable fileType={fileType} /> <TanstakFilesTable fileType={fileType} showFileLink={showFileLink}/>
</div> </div>
) )
} }
@@ -25,10 +25,6 @@ export default function DashboardFilesInfo() {
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.images.violation ? filesInfo?.images.violation : 0}</span>
</div>
</div> </div>
</div> </div>
@@ -47,10 +43,6 @@ export default function DashboardFilesInfo() {
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.videos.violation ? filesInfo?.videos.violation : 0}</span>
</div>
</div> </div>
</div> </div>
@@ -69,10 +61,6 @@ export default function DashboardFilesInfo() {
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.audios.violation ? filesInfo?.audios.violation : 0}</span>
</div>
</div> </div>
</div> </div>
@@ -85,16 +73,12 @@ export default function DashboardFilesInfo() {
<div className="type-card-stats"> <div className="type-card-stats">
<div className="type-card-stat-row"> <div className="type-card-stat-row">
<span className="type-card-stat-label">{t('size')}</span> <span className="type-card-stat-label">{t('size')}</span>
<span className="type-card-stat-value">{filesInfo?.documents.check? convertBytes(filesInfo?.documents.check) : 0}</span> <span className="type-card-stat-value">{filesInfo?.documents.size? convertBytes(filesInfo?.documents.size) : 0}</span>
</div> </div>
<div className="type-card-stat-row"> <div className="type-card-stat-row">
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.documents.violation ? filesInfo?.documents.violation : 0}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,26 +1,63 @@
'use client'
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link'; import Link from 'next/link';
import { fetchLastMonitoringCheck } from '@/app/actions/monitoringActions';
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
import { useTranslations } from 'next-intl';
export default function DashboardLastCheck() { export default function DashboardLastCheck() {
const { data, isLoading } = useQuery({
queryKey: ['lastMonitoringCheck'],
queryFn: () => {
return fetchLastMonitoringCheck()
},
select: (data) => {
if (!data) {
return null
}
return data;
},
retry: false
});
const t = useTranslations('Global');
return ( return (
<div className="dashboard-last-check"> <div className="dashboard-last-check">
<div className="last-check-info"> <div className="last-check-info">
<div className="last-check-title">Последняя проверка контента</div> <div className="last-check-title">
{t('last-content-check')}
</div>
<div
className="relative"
>
{isLoading ? (
<div
className="loading-animation"
>
<div className="global-spinner"></div>
</div>
) : (
<div className="last-check-date"> <div className="last-check-date">
17.10.2025 в 10:23</div> {data?.recent_searches[0]?.created_at ? `${formatDate(data.recent_searches[0].created_at)} ${t('in')} ${formatDateTime(data.recent_searches[0].created_at)}` : '---'}
</div>
)}
</div>
</div> </div>
<div className="last-check-actions"> <div className="last-check-actions">
<Link <Link
className="btn-report" className="btn-report"
href={'reports'} href={'/pages/reports'}
> >
📄 Отчёт {t('reports')}
</Link> </Link>
<Link {/* <Link
className="btn-new-search" className="btn-new-search"
href={'violations'} href={'violations'}
> >
🔍 Мониторинг нарушений Мониторинг нарушений
</Link> </Link> */}
</div> </div>
</div> </div>
) )
@@ -2,11 +2,9 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getUserFilesData } from '@/app/actions/fileEntity'; import { getUserFilesData } from '@/app/actions/fileEntity';
import { useEffect, useState, useRef } from 'react';
import { FileTypeIcon } from '@/app/components/FileTypeIcon'; import { FileTypeIcon } from '@/app/components/FileTypeIcon';
import { convertBytes } from '@/app/lib/convertBytes'; import { convertBytes } from '@/app/lib/convertBytes';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import Link from 'next/link'; import Link from 'next/link';
type FileItem = { type FileItem = {
@@ -41,7 +39,15 @@ export default function DashboardUserFiles() {
error, error,
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({ } = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({
queryKey: ['userFilesData', 1, 5], queryKey: ['userFilesData', 1, 5],
queryFn: () => getUserFilesData(1, 5), queryFn: () => getUserFilesData({
page: 1,
pageSize: 5,
sortBy: 'updatedAt',
sortOrder: 'desc',
type: '',
date: '',
query: ''
}),
select: (data: ApiResponse): FileItem[] => { select: (data: ApiResponse): FileItem[] => {
if (!data?.files) return []; if (!data?.files) return [];
@@ -63,15 +69,6 @@ export default function DashboardUserFiles() {
}); });
const t = useTranslations('Global'); const t = useTranslations('Global');
const [openDropDownList, setOpenDropDownList] = useState(false);
const dropDownList = useRef(null);
useClickOutside(
dropDownList,
() => {
setOpenDropDownList(false)
},
openDropDownList
);
const formatDate = (timestamp: number) => { const formatDate = (timestamp: number) => {
const date = new Date(timestamp); const date = new Date(timestamp);
@@ -85,36 +82,29 @@ export default function DashboardUserFiles() {
return ( return (
<div className="content-section"> <div className="content-section">
<div className="section-header"> <div className="section-header">
<h3 className="section-title">Ваши файлы</h3> <h3 className="section-title">
{t('your-files')}
</h3>
<div <div
className="section-add-file btn view-all-link" className="btn view-all-link"
onClick={() => {
setOpenDropDownList(!openDropDownList);
}}
ref={dropDownList}
> >
{t('add')} <Link href='/pages/my-content'>
<div {t('all')}
className={`section-drop-down-list ${openDropDownList ? 'opened' : ''}`}
>
<div
className="flex flex-col text-center"
>
<Link href='/pages/marking-images'>
{t('photo-marking')}
</Link> </Link>
<Link href='/pages/marking-video'>
{t('video-marking')}
</Link>
<Link href='/pages/marking-audio'>
{t('audio-marking')}
</Link>
</div>
</div>
</div> </div>
</div> </div>
<div className="user-files-list"> <div className="user-files-list">
{isLoading && (
<div
className="loading-animation"
>
<div
className="global-spinner"
>
</div>
</div>
)}
{tableData?.length ? ( {tableData?.length ? (
tableData?.map((file) => { tableData?.map((file) => {
@@ -141,9 +131,11 @@ export default function DashboardUserFiles() {
) )
}) })
) : ( ) : (
!isLoading && (
<div> <div>
{t('there-are-no-files-yet')} {t('there-are-no-files-yet')}
</div> </div>
)
)} )}
</div> </div>
</div> </div>
+125 -23
View File
@@ -1,52 +1,154 @@
'use client' 'use client'
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import {convertBytes} from '@/app/lib/convertBytes'; import { convertBytes } from '@/app/lib/convertBytes';
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo'; import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
import { useEffect } from 'react';
import { useViolationStatistic } from '@/app/hooks/react-query/useViolationStatistic';
import { IconDocument, IconSearch, IconWarning, IconGraph, IconDiscet, IconShield } from '@/app/ui/icons/icons';
export default function DashboardUserStats() { export default function DashboardUserStats() {
const t = useTranslations("Global"); const t = useTranslations("Global");
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
const { data: violationStatistic, isLoading: violationStatisticLoading } = useViolationStatistic();
return ( return (
<>
<div className="dashboard-user-stats"> <div className="dashboard-user-stats">
<div className="stat-card projects"> <div className="stat-card projects">
<div className="stat-type">ЗАЩИЩЁННЫЕ ФАЙЛЫ</div> <div className="stat-header">
<div className="stat-value">{filesInfo?.total.protected ? filesInfo?.total.protected : 0}</div> <div className="stat-icon">
<div className="stat-label">Файлов под защитой</div> <IconShield />
<div className="stat-icon">🛡</div> </div>
</div>
<div className="stat-value">
<div
className="loading-placeholder start"
>
{isLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
filesInfo?.total.protected ? filesInfo?.total.protected : 0
)}
</div>
</div>
<div className="stat-label">
{t('files-under-protection')}
</div>
</div> </div>
<div className="stat-card matches"> <div className="stat-card matches">
<div className="stat-type">СОВПАДЕНИЯ</div> <div className="stat-header">
<div className="stat-value">0</div> <div className="stat-icon">
<div className="stat-label">Найдено совпадений</div> <IconSearch />
<div className="stat-icon">🔍</div> </div>
</div>
{/* <div className="stat-value">
<div
className="loading-placeholder start"
>
{isLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
filesInfo?.total.check ? filesInfo?.total.check : 0
)}
</div>
</div> */}
<div className="stat-value">
<div
className="loading-placeholder start"
>
{violationStatisticLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
violationStatistic?.total_violations ?? 0
)}
</div>
</div>
<div className="stat-label">
{t('matches-found')}
</div>
</div> </div>
<div className="stat-card violations"> <div className="stat-card violations hidden">
<div className="stat-type">НАРУШЕНИЯ</div> <div className="stat-header">
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div> <div className="stat-icon">
<div className="stat-label">Требуют внимания</div> <IconWarning />
<div className="stat-icon"></div> </div>
</div>
<div className="stat-value">
<div
className="loading-placeholder start"
>
{violationStatisticLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
violationStatistic?.total_violations ?? 0
)}
</div>
</div>
<div className="stat-label">
{t('matches')}
</div>
</div> </div>
<div className="stat-card storage"> <div className="stat-card storage">
<div className="stat-type">ХРАНИЛИЩЕ</div> <div className="stat-header">
<div className="stat-icon">
<IconDiscet />
</div>
</div>
<div className="stat-value"> <div className="stat-value">
{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0} <div
className="loading-placeholder start"
>
{isLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0
)}
</div>
</div>
<div className="stat-label">
{t('disk-space-used')}
</div> </div>
<div className="stat-label">Использовано места</div>
<div className="stat-icon">💾</div>
</div> </div>
<div className="stat-card legal"> <div className="stat-card legal">
<div className="stat-type">ЖАЛОБЫ</div> <div className="stat-header">
<div className="stat-value">0</div> <div className="stat-icon">
<div className="stat-label">Отправлено юристам</div> <IconDocument />
<div className="stat-icon"></div>
</div> </div>
</div> </div>
<div className="stat-value">
<div
className="loading-placeholder start"
>
{violationStatisticLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
violationStatistic?.in_progress_violations ?? 0
)}
</div>
</div>
<div className="stat-label">
{t('violations-in-progress')}
</div>
</div>
</div>
</>
) )
} }
@@ -0,0 +1,122 @@
'use client'
import { useQuery } from '@tanstack/react-query';
import { getUserFilesData } from '@/app/actions/fileEntity';
import { useEffect, useState, useRef } from 'react';
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
import { convertBytes } from '@/app/lib/convertBytes';
import { useTranslations } from 'next-intl';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import Link from 'next/link';
import { getViolationFilesArray } from '@/app/actions/violationActions';
import { IconEye } from '@/app/ui/icons/icons';
interface FileViolation {
createdAt: string;
fileId: string;
fileName: string;
fileSize: number;
latestViolationDate: string;
mimeType: string;
status: string;
supportId: number;
violationCount: number;
}
interface ViolationData {
content: FileViolation[];
}
export default function DashboardUserViolations() {
const {
data: violationData,
isLoading,
isError,
error,
} = useQuery<ViolationData>({
queryKey: ['violationData', { page: 0, size: 5 }],
queryFn: () => getViolationFilesArray({
page: 0,
size: 5,
}),
select: (data) => {
return data;
}
});
const t = useTranslations('Global');
const formatDate = (timestamp: number | string) => {
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
return (
<div className="content-section">
<div className="section-header">
<h3 className="section-title">
{t('violation')}
</h3>
<div
className="btn view-all-link"
>
<Link href='/pages/violations'>
{t('all')}
</Link>
</div>
</div>
<div className="user-files-list">
{isLoading && (
<div
className="loading-animation"
>
<div
className="global-spinner"
>
</div>
</div>
)}
{violationData?.content?.length ? (
violationData?.content.map((file) => {
return (
<div
key={file.fileId}
className="file-item"
>
<div className="file-icon violent">
<Link
href={`/pages/file/${file.fileId}`}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
>
<IconEye />
</Link>
</div>
<div className="file-info">
<div className="file-name" title={file.fileName}>
{file.fileName}
</div>
<div className="file-meta">
{file.createdAt ? formatDate(file.createdAt) : 0} {t('violations-found')}: {file.violationCount ? file.violationCount : 0}
</div>
</div>
</div>
)
})
) : (
!isLoading && (
<div>
{t('there-are-no-files-yet')}
</div>
)
)}
</div>
</div>
)
}
@@ -1,274 +0,0 @@
'use client'
import { Swiper, SwiperSlide } from 'swiper/react';
import { Pagination, Autoplay } from 'swiper/modules';
import Image from 'next/image';
import 'swiper/css';
import 'swiper/css/pagination';
import testImage from '@/app/src/image-preview.png'
import { useRef } from 'react';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export default function ViolationsCarousel() {
const paginationRef = useRef(null);
const t = useTranslations('Global');
return (
<div className="content-section">
<div className="section-header">
<h3 className="section-title">Нарушения</h3>
<Link
href="violations"
className="view-all-link"
>
Смотреть все
</Link>
</div>
<div>
{t('are-no-violations')}
</div>
{/* <div className="violations-carousel">
<Swiper
spaceBetween={50}
slidesPerView={1}
modules={[Pagination, Autoplay]}
pagination={{
clickable: true,
el: '.custon-swiper-pagination'
}}
loop={true}
autoplay={{
delay: 3000,
disableOnInteraction: false,
}}
>
<SwiperSlide>
<div className="violation-slide">
<div className="violation-image-container">
<Image src={testImage} alt="Preview" className="violation-image" />
<div className="violation-badge">НАРУШЕНИЕ</div>
<div className="violation-status-badge">ОБРАБОТАНО</div>
</div>
<div className="violation-content">
<div className="violation-title-row">
<div className="violation-filename">
🖼banner111.png</div>
<div className="violation-type-badge photo">
Фото
</div>
</div>
<div className="violation-info-grid">
<div className="violation-info-item">
<div className="violation-info-label">
🌍 Регион
</div>
<div className="violation-info-value">
<span className="region-flag">🇺🇸</span>
<span>США</span>
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📅 Обнаружено</div>
<div className="violation-info-value">
17.10.2025</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">🌐 Домен</div>
<div className="violation-info-value">
m.apkpure.com </div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📊 Категория</div>
<div className="violation-info-value">
Единичное
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📁 Тип</div>
<div className="violation-info-value">
Изображение</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label"> Статус</div>
<div className="violation-info-value">
Обработано</div>
</div>
</div>
<div className="violation-actions-row">
<button className="btn-visit-site">
🔗 Перейти на сайт
</button>
<button className="violation-action">
🚨 Открыть дело
</button>
</div>
</div>
</div>
</SwiperSlide>
<SwiperSlide>
<div className="violation-slide">
<div className="violation-image-container">
<Image src={testImage} alt="Preview" className="violation-image" />
<div className="violation-badge">НАРУШЕНИЕ</div>
<div className="violation-status-badge">ОБРАБОТАНО</div>
</div>
<div className="violation-content">
<div className="violation-title-row">
<div className="violation-filename">
🖼banner111.png</div>
<div className="violation-type-badge photo">
Фото
</div>
</div>
<div className="violation-info-grid">
<div className="violation-info-item">
<div className="violation-info-label">
🌍 Регион
</div>
<div className="violation-info-value">
<span className="region-flag">🇺🇸</span>
<span>США</span>
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📅 Обнаружено</div>
<div className="violation-info-value">
17.10.2025</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">🌐 Домен</div>
<div className="violation-info-value">
m.apkpure.com </div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📊 Категория</div>
<div className="violation-info-value">
Единичное
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📁 Тип</div>
<div className="violation-info-value">
Изображение</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label"> Статус</div>
<div className="violation-info-value">
Обработано</div>
</div>
</div>
<div className="violation-actions-row">
<button className="btn-visit-site">
🔗 Перейти на сайт
</button>
<button className="violation-action">
🚨 Открыть дело
</button>
</div>
</div>
</div>
</SwiperSlide>
<SwiperSlide>
<div className="violation-slide">
<div className="violation-image-container">
<Image src={testImage} alt="Preview" className="violation-image" />
<div className="violation-badge">НАРУШЕНИЕ</div>
<div className="violation-status-badge">ОБРАБОТАНО</div>
</div>
<div className="violation-content">
<div className="violation-title-row">
<div className="violation-filename">
🖼banner111.png</div>
<div className="violation-type-badge photo">
Фото
</div>
</div>
<div className="violation-info-grid">
<div className="violation-info-item">
<div className="violation-info-label">
🌍 Регион
</div>
<div className="violation-info-value">
<span className="region-flag">🇺🇸</span>
<span>США</span>
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📅 Обнаружено</div>
<div className="violation-info-value">
17.10.2025</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">🌐 Домен</div>
<div className="violation-info-value">
m.apkpure.com </div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📊 Категория</div>
<div className="violation-info-value">
Единичное
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📁 Тип</div>
<div className="violation-info-value">
Изображение</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label"> Статус</div>
<div className="violation-info-value">
Обработано</div>
</div>
</div>
<div className="violation-actions-row">
<button className="btn-visit-site">
🔗 Перейти на сайт
</button>
<button className="violation-action">
🚨 Открыть дело
</button>
</div>
</div>
</div>
</SwiperSlide>
</Swiper>
<div
className="flex justify-center mt-6"
>
<div
ref={paginationRef}
className="flex justify-center gap-2 custon-swiper-pagination"
>
</div>
</div>
</div> */}
</div>
)
}

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