Compare commits

..
495 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 62b91f0ce9 add extension dropdown for image convert 2026-02-17 17:58:32 +07:00
smanylov fad2a525ea add image conver button 2026-02-17 14:43:26 +07:00
smanylov 0c20109254 remove sms notification, add for document-marking title and title-description translate 2026-02-17 12:41:38 +07:00
smanylov 84bf085453 add translate for INN input 2026-02-17 11:27:57 +07:00
smanylov 57c99bd981 add yoo-kassa payment(test setup) 2026-02-16 20:11:59 +07:00
smanylov bcbe64807b add sorting for comission levels 2026-02-16 12:28:24 +07:00
smanylov 198c8b452d change log 2026-02-13 19:23:34 +07:00
smanylov 1fec3b2584 add INN registration field 2026-02-13 19:17:10 +07:00
smanylov 7aa29bd398 add return null when fetch failed 2026-02-13 14:16:39 +07:00
smanylov 1f6301c531 refactor userData query 2026-02-13 13:44:03 +07:00
smanylov edc6e56c69 refactor backendBuildDate 2026-02-13 12:57:45 +07:00
smanylov d584e68e3a refactor userFilesInfo query 2026-02-13 12:50:58 +07:00
smanylov e36eb4d422 return next start -p 2026-02-12 21:55:31 +07:00
smanylov 3946e7a0e7 change npm start script for stand 2026-02-12 21:32:06 +07:00
smanylov a3da6b8530 modify search function for all files search 2026-02-12 18:02:25 +07:00
smanylov f3e8ef3f65 return download url 2026-02-12 16:45:29 +07:00
smanylov 03920524d3 change download url 2026-02-12 16:21:10 +07:00
smanylov 4958d3ca57 remove procent from MyContentStatsOverview and change icons 2026-02-12 14:01:35 +07:00
smanylov 5f12c58cc8 remove console.log 2026-02-12 13:12:52 +07:00
smanylov 6ccdfc0129 add reports-info 2026-02-12 13:11:14 +07:00
smanylov c089de52e7 hide size block for global seach items 2026-02-12 12:45:35 +07:00
smanylov 166de1bf15 adaptive styles for file uploads 2026-02-12 12:38:44 +07:00
smanylov 726c1dc16c refactor 2026-02-12 11:33:52 +07:00
smanylov a52be724c0 for getBuildData return null while error 2026-02-12 11:17:30 +07:00
smanylov be1f310595 add back build time 2026-02-12 11:16:58 +07:00
smanylov 953f2f22a9 add frontent build date 2026-02-11 20:47:40 +07:00
smanylov 74579fa53e Merge branch 'main' of https://code.3err0.ru/frontdev/no-copy-frontend 2026-02-11 20:11:42 +07:00
smanylov 930e811232 add upload multiple files 2026-02-11 20:11:39 +07:00
vladp 6ca112f3e3 add build front time 2026-02-11 19:43:53 +07:00
vladp 659d307a12 add build front time 2026-02-11 19:28:16 +07:00
smanylov 459035529c increment version 2026-02-11 13:57:32 +07:00
smanylov 0ad2b03f2b change filter icon for tanstak-table 2026-02-11 13:57:14 +07:00
smanylov 062c3e3539 fix date for users files 2026-02-10 21:00:00 +07:00
smanylov 78c606ba9e update copy function 2026-02-10 18:07:53 +07:00
smanylov ffeeff8be3 increment version 2026-02-10 18:00:05 +07:00
smanylov 9a9ec39ff6 update search pagination 2026-02-10 17:29:36 +07:00
smanylov 61ed604b3e add page for search 2026-02-10 16:24:56 +07:00
smanylov 40e6172c0b add pagination for test 2026-02-10 15:40:18 +07:00
smanylov 0898ea9aa0 change referral link error message 2026-02-10 00:49:07 +07:00
smanylov 63a241f3b5 change copy function 2026-02-09 18:37:56 +07:00
smanylov 54529fdbb5 remove console.error 2026-02-09 17:35:17 +07:00
smanylov 7d6882a006 add new endpoint for tariffs 2026-02-09 17:28:03 +07:00
smanylov 1b57d589fb merge global and local search 2026-02-09 16:44:34 +07:00
smanylov b6c5c59919 add ref link error, add refill button, update referral invites table 2026-02-09 16:07:49 +07:00
smanylov da99d7e2a8 edit error message 2026-02-09 13:33:42 +07:00
smanylov 82d795c1c5 hide inactive settings 2026-02-09 13:17:49 +07:00
smanylov e8b9b5e1b9 add referrals levels 2026-02-09 13:11:25 +07:00
smanylov e36d67be17 fix translate 2026-02-09 11:33:16 +07:00
smanylov 0ec7834cc5 edit referrals page, add referrals fetch functions 2026-02-07 14:31:04 +07:00
smanylov 7572c1ada2 add document marking page 2026-02-07 12:28:07 +07:00
smanylov 256ecbe31f add validation translate-key 2026-02-06 14:37:49 +07:00
smanylov 037640fc70 update ref 2026-02-06 13:40:30 +07:00
smanylov f6ca797802 add referal code for registration 2026-02-06 13:13:29 +07:00
smanylov c4de107fba add tanstak table for invite history 2026-02-06 12:37:22 +07:00
smanylov 6d9710e7cd add loading animation 2026-02-06 11:13:13 +07:00
smanylov 963b649aac update recent search component 2026-02-05 21:05:55 +07:00
smanylov 4f294e8b90 add search stats 2026-02-05 21:01:19 +07:00
smanylov a684e1dd94 update user registration 2026-02-05 19:50:38 +07:00
smanylov 777555c187 edit company-user registration 2026-02-05 19:31:06 +07:00
smanylov 8890cfb067 fix registration validation config 2026-02-05 18:10:16 +07:00
smanylov 6993dd3eb8 fix registration company-name error 2026-02-05 17:37:27 +07:00
smanylov 23317807f6 remove logs 2026-02-05 14:55:17 +07:00
smanylov c24a4febc9 add company user registration, update phone validation 2026-02-05 14:53:43 +07:00
smanylov c7a0131624 update tokens validation 2026-02-05 13:06:32 +07:00
smanylov 2f3c3696d6 add permission vision for users 2026-02-05 12:58:41 +07:00
smanylov de0a8d0c39 refactor 2026-02-05 12:52:34 +07:00
smanylov 8bf5111d7c add registration logs 2026-02-05 12:17:52 +07:00
smanylov 1037347e06 add logs 2026-02-05 11:55:02 +07:00
vladp a84cd39737 fix name 2026-02-04 18:10:48 +07:00
smanylov be130520d9 update dockerfile 2026-02-04 17:26:33 +07:00
smanylov 81614b028c add force for dockerfile 2026-02-04 17:24:57 +07:00
smanylov c0c89dba77 increment version 2026-02-04 16:15:24 +07:00
smanylov 97b846deec remove extra console.log 2026-02-04 16:14:17 +07:00
smanylov e778cf7e33 update error handler 2026-02-04 16:10:16 +07:00
smanylov 40fa737208 change dublicate-file-error text 2026-02-04 15:18:38 +07:00
smanylov 5df5e340e5 refactor 2026-02-04 15:13:17 +07:00
smanylov 7c757a3c59 add more user tarrif info 2026-02-04 14:41:40 +07:00
smanylov 2c26a8cd85 add search side panel layout 2026-02-04 14:10:38 +07:00
smanylov 584f0dda2f update errors handler 2026-02-04 13:21:32 +07:00
smanylov 362df2f609 version increment 2026-02-04 12:02:01 +07:00
smanylov f99e352693 add subscription info 2026-02-04 12:01:41 +07:00
frontdev 17a7672d4e Merge pull request 'add tarrifs' (#6) from tariffs into main
Reviewed-on: #6
2026-02-04 12:16:15 +08:00
smanylov 1aedd8bc29 Merge branch 'main' into tariffs 2026-02-03 17:42:34 +07:00
smanylov 3d601cc154 merge 2026-02-03 17:41:36 +07:00
smanylov e8c57729be revert changes 2026-02-03 17:36:09 +07:00
smanylov e0606c5486 add tarrifs 2026-02-03 17:31:27 +07:00
smanylov 61e3d08136 update tariffs and users info 2026-02-03 16:42:24 +07:00
smanylov 1e199de7b2 edit registration 2026-02-03 15:25:11 +07:00
smanylov 2adf66baa2 remove extra inputs from setting 2026-02-03 12:30:30 +07:00
smanylov 1ee55f503d change title block for my-content page 2026-02-03 11:49:40 +07:00
smanylov 8d10aa77c8 add errors handlers and translate 2026-02-03 11:44:02 +07:00
smanylov ab0b68a7b8 add error handlers 2026-02-02 15:09:29 +07:00
smanylov 7789f19055 add reset password 2026-02-02 15:00:56 +07:00
smanylov 19e807a7f6 fix link and text for navigation 2026-02-02 12:49:31 +07:00
smanylov b4249c7f38 add error handler 2026-01-30 23:02:18 +07:00
smanylov c5baec6d1d add size conver 2026-01-30 15:03:22 +07:00
smanylov 82c7bb3635 add search animation 2026-01-30 14:54:52 +07:00
smanylov c01499f4f1 add error message 2026-01-30 13:59:10 +07:00
smanylov f87be6d2c6 add add-user-block 2026-01-30 13:45:49 +07:00
smanylov 5c252cdcb1 add layout for violation page 2026-01-30 13:14:39 +07:00
smanylov 42100761c1 fix upload file bugs 2026-01-30 13:04:32 +07:00
smanylov 4f6648ead8 add message for empty file list 2026-01-30 12:35:53 +07:00
frontdev c65314e7cd Merge pull request 'NCFRONT-81' (#5) from NCFRONT-81 into main
Reviewed-on: #5
2026-01-29 22:34:35 +08:00
smanylov 8b04485533 complite b2b b2c registration 2026-01-29 21:34:02 +07:00
smanylov fc81d471ff update accountType 2026-01-29 17:01:00 +07:00
smanylov 55e0b0723e remove language switcher, change file search result 2026-01-29 16:53:16 +07:00
smanylov 63d380f0f7 edit DahboardLimitsSection 2026-01-29 16:40:38 +07:00
smanylov fc9a7afada update your files block 2026-01-29 16:28:05 +07:00
smanylov 27d8e75986 change buttons name 2026-01-29 14:48:54 +07:00
smanylov 8b68e37b89 btc, btb registration 2026-01-29 14:34:04 +07:00
smanylov 832c4b0e76 add info for file info modal window 2026-01-29 12:00:29 +07:00
smanylov c4ab78b89b add check modal window 2026-01-28 18:53:08 +07:00
smanylov a6a493fe97 add null result for search files 2026-01-28 16:30:17 +07:00
smanylov efae068bac transfer actionTracker 2026-01-28 15:32:19 +07:00
smanylov 2129fe1727 refactor 2026-01-28 12:12:23 +07:00
smanylov f7f53d5af3 image styles for image view modal window 2026-01-28 11:36:49 +07:00
smanylov 9a6491b76b add file search and some fixes and bugs 2026-01-27 19:52:01 +07:00
smanylov 1db333a21b add search page 2026-01-26 18:42:12 +07:00
smanylov e9e9f41f2a add support id 2026-01-26 16:38:24 +07:00
smanylov ab652117c8 increment version 2026-01-26 16:10:02 +07:00
smanylov 6ef23862e1 add protected info 2026-01-26 16:09:39 +07:00
smanylov 2be8f81a5b refactor 2026-01-26 13:26:21 +07:00
smanylov bd869798be edit small screen layout 2026-01-24 13:19:48 +07:00
220 changed files with 27865 additions and 4225 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ WORKDIR /no-copy-frontend
COPY . .
COPY --from=dependencies /no-copy-frontend/node_modules ./node_modules
RUN npm run build
RUN rm -rf .next && npm run build --force
FROM node:22.20.0 AS runner
WORKDIR /no-copy-frontend
+8
View File
@@ -34,3 +34,11 @@ docker network inspect no-copy_app-network
docker volume ls - посмотреть все базы данных
docker volume prune - удалить неиспользуемые базы данных
Юкаса
эндпоинт для бека.
{URL}/api/payments/webhook/yookassa
https://dev-workspace.not-copy.com/api/payments/webhook/yookassa
https://mutely-profound-sandfish.cloudpub.ru/api/payments/webhook/yookassa
+4
View File
@@ -1,5 +1,6 @@
import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
const { version } = require('./package.json');
const nextConfig: NextConfig = {
experimental: {
@@ -7,6 +8,9 @@ const nextConfig: NextConfig = {
bodySizeLimit: '10mb',
},
},
env: {
APP_VERSION: version,
},
};
const withNextIntl = createNextIntlPlugin();
+1097 -732
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "no-copy-frontend",
"version": "0.23.0",
"version": "0.122.0",
"private": true,
"scripts": {
"dev": "next dev -p 2999",
@@ -9,17 +9,20 @@
"lint": "eslint"
},
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
"@react-input/mask": "^2.0.4",
"@tailwindcss/postcss": "^4.1.17",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2",
"card-validator": "^10.0.4",
"clsx": "^2.1.1",
"jose": "^6.1.2",
"next": "^16.0.7",
"next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8",
"pdfjs-dist": "^5.7.284",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-slick": "^0.31.0",
@@ -27,6 +30,7 @@
"sonner": "^2.0.7",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.17",
"use-debounce": "^10.1.0",
"validator": "^13.15.26",
"zod": "^4.1.13",
"zustand": "^5.0.9"
@@ -38,6 +42,7 @@
"eslint": "^9",
"eslint-config-next": "16.0.3",
"sass": "^1.94.2",
"types-yoomoneycheckoutwidget": "^0.0.2",
"typescript": "^5"
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"buildTime": null
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -2,7 +2,6 @@ import { useTranslations } from 'next-intl'
import LogoIcon from '@/app/ui/logo-icon';
import styles from '@/app/styles/module/login.module.scss';
import Link from 'next/link';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import ForgotPasswordForm from '@/app/ui/forms/forgot-password-form';
export default function Page() {
@@ -10,9 +9,6 @@ export default function Page() {
return (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
+5 -5
View File
@@ -1,12 +1,13 @@
import Providers from '@/app/providers/getQueryServer'
import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import { ToastProvider } from '@/app/providers/ToastProvider';
import Providers from '@/app/providers/getQueryServer';
import { Suspense } from 'react';
import "../styles/globals.css";
import "../styles/global-styles.scss";
import ActivityTracker from '../components/ActivityTracker';
export function generateStaticParams() {
return routing.locales.map((locale: any) => ({ locale }));
@@ -30,12 +31,11 @@ export default async function RootLayout({
}
return (
<html lang={locale}>
<html>
<body>
<NextIntlClientProvider>
<NextIntlClientProvider locale={locale}>
<Providers>
{children}
<ActivityTracker/>
<ToastProvider />
</Providers>
</NextIntlClientProvider>
+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>
)
}
+18 -3
View File
@@ -7,6 +7,9 @@ import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import { useTranslations } from 'next-intl';
import VKLogin from '@/app/components/VKLogin';
import YandexAuthWithScript from '@/app/components/YandexLogin';
import { getHealt } from '@/app/actions/action';
import { Suspense } from 'react';
import {YandexLoginButton} from '@/app/components/YandexLoginButton';
export const metadata: Metadata = {
title: 'Login',
@@ -15,11 +18,22 @@ export const metadata: Metadata = {
export default function Page() {
const t = useTranslations('Login-register-form');
async function HealthData() {
const data = await getHealt();
if (!data) {
return <div>Сервер временно не работает</div>
} else {
return null
}
}
return (
<Suspense fallback={<div>Loading...</div>}>
<HealthData />
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
{/* <div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
</div> */}
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
@@ -38,7 +52,7 @@ export default function Page() {
<VKLogin />
</div>
<div>
<YandexAuthWithScript />
<YandexLoginButton />
</div>
<div className={`${styles['register-link']}`}>
<p>{t('no-account')}?
@@ -67,5 +81,6 @@ export default function Page() {
</div>
</div>
</div>
</Suspense>
);
}
+13 -5
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 DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check';
import ViolationsCarousel from '@/app/ui/dashboard/new/violations-carousel';
import DashboardUserViolations from '@/app/ui/dashboard/new/dashboard-user-violations';
export const metadata: Metadata = {
title: 'Dashboard',
@@ -24,18 +24,26 @@ export default function Page() {
<PageTitle title="statistics" />
<DashboardUserStats />
<div className="dashboard-main-grid">
{/* <DashboardUserFiles />
<DashboardUserViolations /> */}
<div className="left-column">
<DashboardUserFiles />
<DashboardPlatformsList />
</div>
<div className="right-column">
<ViolationsCarousel />
<DashboardUserViolations />
</div>
</div>
<DashboardFilesInfo />
<div className="dashboard-main-grid">
<DahboardLimitsSection />
<DahboardGeographySection />
{/* <DahboardGeographySection /> */}
</div>
<div
className="mb-4"
>
{/* <DahboardLimitsSection /> */}
</div>
<DashboardLastCheck />
</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 />
)
}
}
+2
View File
@@ -3,6 +3,7 @@ import HeaderPanel from '@/app/ui/header/headerPanel';
import { getQueryClient } from '@/app/providers/getQueryClient';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
import ActivityTracker from '@/app/components/ActivityTracker';
export default async function Layout({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
@@ -20,6 +21,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
</main>
</div>
</HydrationBoundary>
<ActivityTracker/>
</div>
);
}
@@ -1,7 +1,7 @@
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import FilesTable from '@/app/ui/dashboard/files-table';
import PageTitle from '@/app/ui/page-title';
import UploadSectionFile from '@/app/components/upload-section-file';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import { StackedBarChart } from '@/app/components/StackedBarChart';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
@@ -0,0 +1,26 @@
import FilesTable from '@/app/ui/dashboard/files-table';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
export default async function Page() {
const FILE_TYPE = "document";
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
return (
<div>
<PageTitleColorFrame
title="document-protection"
description="multi-layered-protection-for-your-document"
/>
<ProtectionStatistic fileType={FILE_TYPE} />
<UploadSectionFile
fileType={FILE_TYPE}
allowedExtensions={file_extension}
maxFileSize={max_file_size}
/>
<FilesTable fileType={FILE_TYPE} />
</div>
);
}
@@ -1,5 +1,5 @@
import FilesTable from '@/app/ui/dashboard/files-table';
import UploadSectionFile from '@/app/components/upload-section-file';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
@@ -1,7 +1,7 @@
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import FilesTable from '@/app/ui/dashboard/files-table';
import PageTitle from '@/app/ui/page-title';
import UploadSectionFile from '@/app/components/upload-section-file';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import { StackedBarChart } from '@/app/components/StackedBarChart';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
+2 -2
View File
@@ -1,13 +1,13 @@
import MyContentInfoBlock from '@/app/ui/my-content/my-content-info-block';
import FilesTable from '@/app/ui/dashboard/files-table';
import MyContentPieChart from '@/app/ui/my-content/my-content-pie-chart';
import MyContentPageTitleColorFrame from '@/app/ui/my-content/new/my-content-page-title';
import MyContentStatsOverview from '@/app/ui/my-content/new/my-content-stats-overview';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
export default function Page() {
return (
<>
<MyContentPageTitleColorFrame title="my-content" description="my-content-description" />
<PageTitleColorFrame title="my-content" description="my-content-description"/>
<MyContentStatsOverview />
{/* <div className="split-blocks">
<MyContentInfoBlock />
@@ -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 PaymentTabs from '@/app/ui/payment/payment-tabs';
import SecurePayments from '@/app/ui/payment/secure-payments';
import PaymentUserCards from '@/app/ui/payment/payment-user-cards';
export const metadata: Metadata = {
title: 'payment',
@@ -22,6 +23,7 @@ export default function Page() {
<UserUsage />
<TokensSpendStats />
<PaymentTabs />
<PaymentUserCards />
<SecurePayments />
</div>
</main>
+2 -3
View File
@@ -1,10 +1,9 @@
import { useTranslations } from 'next-intl';
import { Suspense } from 'react';
import PageTitle from '@/app/ui/page-title';
import ReferralStats from '@/app/ui/referral-page/referral-stats';
import ReferralLinkSection from '@/app/ui/referral-page/referral-link-section';
import CommissionLevels from '@/app/ui/referral-page/commission-levels';
import InvitationsTable from '@/app/ui/referral-page/invitations-table';
import InvitationsTable from '@/app/ui/referral-page/table/invitations-table';
import IncomeAndPayments from '@/app/ui/referral-page/Income-and-payments';
import SettingReferralProgram from '@/app/ui/referral-page/setting-referral-program';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
@@ -16,7 +15,7 @@ export default function Page() {
<Suspense fallback={<>...</>}>
<PageTitleColorFrame title="refferal-program" description="referral-program-description" />
<CommissionLevels />
<ReferralStats />
{/* <ReferralStats /> */}
<ReferralLinkSection />
<InvitationsTable />
<IncomeAndPayments />
+7 -5
View File
@@ -1,19 +1,21 @@
import StatsGrid from '@/app/ui/dashboard/stats-grid';
import ViolationsTable from '@/app/ui/dashboard/violations-table';
import ViolationsTable from '@/app/ui/violations/violations-table';
import FinalSummary from '@/app/ui/reports/final-summary';
import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month';
import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography';
import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
import ActivityByContentType from '@/app/ui/reports/activity-by-content-type';
import PageTitle from '@/app/ui/page-title';
import ReportsInfo from '@/app/ui/reports/reports-info';
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
export default function Page() {
return (
<div>
<PageTitle title="reports-and-analytics" />
<StatsGrid />
<ReportsInfo />
<ActivityByContentType />
<div className="analytics-grid">
<AnalyticsCardMonth />
{/* <AnalyticsCardMonth /> */}
<AnalyticsCardDistribution />
<AnalyticsCardGeography />
</div>
<ViolationsTable />
+21
View File
@@ -0,0 +1,21 @@
import SectionSearchFile from '@/app/ui/search/section-search-file';
import { SupportedFormats } from '@/app/ui/search/supported-formats';
import { SearchStats } from '@/app/ui/search/search-stats';
import SearchTitle from '@/app/ui/search/search-title';
export default async function Page() {
return (
<>
<SearchTitle />
<div className="search-grid">
<SectionSearchFile />
<div className="search-sidebar">
<SupportedFormats />
<SearchStats />
</div>
</div>
</>
)
}
+8 -3
View File
@@ -7,6 +7,8 @@ import LastActivitySettings from '@/app/ui/settings/last-activity-settings';
import DangerZone from '@/app/ui/settings/danger-zone';
import { useTranslations } from 'next-intl';
import PageTitle from '@/app/ui/page-title';
import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel';
import SettingUserVerification from '@/app/ui/settings/setting-user-verification';
export default function Page() {
const t = useTranslations('Global');
@@ -16,11 +18,14 @@ export default function Page() {
<PageTitle title="account-settings" />
<div className="settings-grid">
<PersonalDataSettings />
<NotificationsMonitoringSettings />
{/* <NotificationsMonitoringSettings /> */}
<SafetySetting />
<SubscriptionSettings />
<APISettings />
<LastActivitySettings />
{/* <APISettings /> */}
{/* LastActivitySettings возможно нужно будет вернуть */}
{/* <LastActivitySettings /> */}
<CompanyUsersSettingPanel />
<SettingUserVerification />
</div>
<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 DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
import DashboardPlatformsList from '@/app/ui/dashboard/new/dashboard-platforms-list';
import ViolationsMyCases from '@/app/ui/violations/violations-my-cases';
import ViolationsMyComplaint from '@/app/ui/violations/violations-my-complaint';
import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section';
import ViolationsTable from '@/app/ui/violations/violations-table';
import ViolationsStatistic from '@/app/ui/violations/violations-statistic';
export default function Page() {
const FILE_TYPE = "all";
import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
import ViolationsMyClaims from '@/app/ui/violations/violations-my-claims';
import ViolationsLastCombinedCases from '@/app/ui/violations/violations-last-combined-cases';
import { ViolationsMyComplaintsTable } from '@/app/ui/violations/tables/violations-my-complaints-table';
import { ViolationsMyClaimsTable } from '@/app/ui/violations/tables/violations-my-claims-table';
export default function Page() {
return (
<>
<PageTitleColorFrame
@@ -15,12 +18,18 @@ export default function Page() {
description="monitoring-copyright-infringements-description"
/>
<ViolationsStatistic />
<ViolationsMyCases />
<ViolationsCheckAllSection />
<ViolationsTable />
<div className="dashboard-main-grid">
<DashboardPlatformsList />
<DahboardGeographySection />
{/* <ViolationsLastCombinedCases /> */}
{/* <ViolationsMyComplaint />
<ViolationsMyClaims /> */}
<div className="analytics-grid">
<AnalyticsCardDistribution />
<AnalyticsCardGeography />
</div>
<div className="analytics-grid">
<ViolationsMyClaimsTable />
<ViolationsMyComplaintsTable />
</div>
</>
)
-4
View File
@@ -2,7 +2,6 @@ import LogoIcon from '@/app/ui/logo-icon';
import styles from '@/app/styles/module/login.module.scss'
import Link from 'next/link';
import RegisterForm from '@/app/ui/forms/register-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import {useTranslations} from 'next-intl';
export default function Page() {
@@ -10,9 +9,6 @@ export default function Page() {
return (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
+34
View File
@@ -0,0 +1,34 @@
import { useTranslations } from 'next-intl'
import LogoIcon from '@/app/ui/logo-icon';
import styles from '@/app/styles/module/login.module.scss';
import Link from 'next/link';
import ResetPasswordForm from '@/app/ui/forms/reset-password-form';
export default function Page() {
const t = useTranslations('Login-register-form');
return (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
<h1>NO COPY</h1>
<p>
{t('recover-password')}
</p>
</div>
<ResetPasswordForm />
<div className={`${styles['register-link']}`}>
<p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
</div>
<div className={`${styles['register-link']}`}>
<p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
</p>
</div>
</div>
</div>
)
}
+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;
}
}
+165 -12
View File
@@ -1,15 +1,14 @@
'use server'
import { getSessionData, deleteSession } from '@/app/actions/session';
import { testUserData, API_BASE_URL } from '@/app/actions/definitions';
import { tokenLifeExtension } from '@/app/actions/auth';
import { redirect } from 'next/navigation';
export async function getUserData() {
const userEmail = await getSessionData('email');
const token = await getSessionData('token');
/* для теста */
if (userEmail === "test" && token === "1111") {
if (userEmail === 'test' && token === '1111') {
return testUserData;
}
/* для теста */
@@ -18,17 +17,19 @@ export async function getUserData() {
const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
return await response.json();
} else {
return null
}
} catch (error) {
console.error('error');
return null
}
}
@@ -42,13 +43,13 @@ export async function getUserFilesInfo() {
version: 1,
msg_id: 20005,
message_body: {
action: "user_files_info",
token: token
action: 'user_files_info',
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
@@ -61,8 +62,160 @@ export async function getUserFilesInfo() {
} else {
throw parsed.message_code;
}
} else {
return null
}
} catch (error) {
console.error(`error: ${error}`);
return null
}
}
export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30001,
message_body: {
action: 'get',
tariff_term: tariffTerm
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json()
if (parsed?.message_code === 0) {
return parsed.message_body?.tariffs;
} else {
return []
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function fetchTokensBundle() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30001,
message_body: {
action: 'tariffs_by_type',
type: 'token'
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed?.message_code === 0) {
return parsed.message_body?.token_tariffs;
} else {
return null
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function getBuildData() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/check/api/build`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
return await response.json();
} else {
return null
}
} catch (error) {
return null;
}
}
export async function fetchINN(inn: string) {
try {
const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30004,
message_body: {
inn: inn,
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
const parsed = await response.json()
if (parsed?.message_code === 0) {
return parsed.message_body
} else {
return null
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function getHealt() {
try {
const response = await fetch(`${API_BASE_URL}/check/api/healt`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
return await response.json();
} else {
return null
}
} catch (error) {
return null
}
}
+328 -57
View File
@@ -3,6 +3,8 @@
import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
import { redirect } from 'next/navigation';
import { fetchINN } from '@/app/actions/action';
import { headers } from 'next/headers';
export async function logout() {
const token = await getSessionData('token');
@@ -11,9 +13,9 @@ export async function logout() {
await fetch(`${API_BASE_URL}/v1/api/auth/logout`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
} catch (error) {
@@ -36,8 +38,8 @@ export async function authorization(
const password = formData.get('password');
/* для теста */
if (email === "test" && password === "test") {
await createSession("1111", "test");
if (email === 'test' && password === 'test') {
await createSession('1111', 'test');
redirect('/pages/dashboard');
}
/* для теста */
@@ -70,7 +72,7 @@ export async function authorization(
try {
const { email, password } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST',
body: JSON.stringify({
version: 1,
@@ -81,13 +83,14 @@ export async function authorization(
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_desc === 'Operation successful') {
if (parsed.message_code === 0) {
await createSession(parsed.message_body.token, email);
} else {
throw (`${parsed.message_code}`);
@@ -130,14 +133,16 @@ export async function authorization(
export type FormState = {
previousState: {
fullName: string;
email: string;
password: string;
confirm_password: string;
phone: string;
companyName: string;
agree: string;
},
fullName?: string | undefined;
email?: string | undefined;
password?: string | undefined;
confirm_password?: string | undefined;
phone?: string | undefined;
companyName?: string | undefined;
agree?: string | undefined;
referralCode?: string | undefined;
inn?: string | undefined;
} | undefined,
mailConfirm?: boolean,
userId?: number,
error: Record<string, string> | null
@@ -147,6 +152,11 @@ export async function registration(
state: FormState | undefined,
formData: FormData,
): Promise<FormState> {
const headersList = await headers();
const accountType = formData.get('accountType') as 'b2b' | 'b2c';
/* вернуть в true когда проект выйдет из дева */
const mail_verified = false;
const fullName = formData.get('fullName') as string || '';
const email = formData.get('email') as string || '';
const password = formData.get('password') as string || '';
@@ -154,8 +164,16 @@ export async function registration(
const phone = formData.get('phone') as string || '';
const companyName = formData.get('companyName') as string || '';
const agree = formData.get('agree') as string || '';
const referralCode = formData.get('referralCode') as string || '';
const inn = formData.get('inn') as string || '';
const SignupFormSchema = await getSignupFormSchema();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const SignupFormSchema = await getSignupFormSchema(accountType);
const dataToValidate = {
fullName,
@@ -164,14 +182,27 @@ export async function registration(
password,
confirm_password,
agree,
companyName
...(accountType === 'b2b' && { companyName, inn })
};
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
const validatedFields = await SignupFormSchema.safeParse(dataToValidate);
if (!validatedFields.success) {
console.log(`try to register ${accountType}`);
let confirmedCompanyName: boolean = true;
if (accountType === 'b2b') {
try {
const response = await fetchINN(inn);
confirmedCompanyName = response?.companyName ? true : false;
} catch (error) {
confirmedCompanyName = false;
}
}
if (!validatedFields.success || !confirmedCompanyName) {
const errors: Record<string, string> = {}
JSON.parse(validatedFields.error.message).forEach((obj: {
if (validatedFields?.error?.message) {
JSON?.parse(validatedFields?.error?.message).forEach((obj: {
message: string,
path: string
}) => {
@@ -181,6 +212,11 @@ export async function registration(
errors[obj.path[0]] = obj.message;
}
});
}
if (!confirmedCompanyName) {
errors.inn = 'inn-invalid'
}
return {
previousState: {
@@ -189,8 +225,9 @@ export async function registration(
password,
confirm_password,
phone,
companyName,
agree
agree,
...(referralCode !== '' && { referralCode }),
...(accountType === 'b2b' && { companyName, inn })
},
error: errors
};
@@ -198,7 +235,7 @@ export async function registration(
try {
const { fullName, email, password, phone } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST',
body: JSON.stringify({
version: 1,
@@ -208,25 +245,52 @@ export async function registration(
email,
phone: phone,
password,
companyName: companyName,
accountType,
mail_verified: mail_verified,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode })
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
console.log({
fullName: fullName,
email,
phone: phone,
password,
accountType,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode })
})
if (response.ok) {
let parsed = await response.json() as {
message_desc: string,
message_body: { token: string, userId: number },
message_code: string
message_body: { token: string, userId: number, fieldErrors: string },
message_code: number
};
console.log(parsed);
if (parsed.message_desc === 'Operation successful') {
/* await createSession(parsed.message_body.token, email as string); */
if (parsed.message_code === 0) {
console.log(`registration - ${accountType}`);
console.log({
fullName: fullName,
email,
phone: phone,
password,
accountType,
...(referralCode !== '' && { referralLink: referralCode }),
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
});
if (mail_verified) {
return {
previousState: {
fullName,
@@ -234,13 +298,18 @@ export async function registration(
password,
confirm_password,
phone,
companyName,
agree
agree,
...(referralCode !== '' && { referralLink: referralCode }),
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
},
mailConfirm: true,
userId: parsed.message_body.userId,
error: null
};
} else {
await createSession(parsed.message_body.token, email as string);
}
} else {
throw parsed;
}
@@ -249,17 +318,22 @@ export async function registration(
}
} catch (error: unknown) {
const errors: Record<string, string> = {};
if (error) {
const typedError = error as any;
const errors: Record<string, string> = {};
switch (typedError.message_code) {
case 1:
if (typedError.message_desc === 'Refferal link is not exist') {
errors['server'] = 'referral-link-is-not-exist'
break;
} else {
errors['server'] = 'email-or-already-registered'
break;
}
case 2:
typedError.message_body.fieldErrors.forEach((obj: {
if (typedError.message_body?.fieldErrors) {
typedError.message_body?.fieldErrors?.forEach((obj: {
code: string,
field: string
}) => {
@@ -269,6 +343,12 @@ export async function registration(
errors[obj.field] = obj.code.replaceAll('.', '-');
}
});
} else if (typedError.message_desc === 'Company already exists') {
errors['inn'] = 'company-already-registered'
} else {
errors['server'] = 'unknown-error'
}
break;
default:
@@ -284,14 +364,27 @@ export async function registration(
confirm_password,
phone,
companyName,
agree
agree,
inn
},
mailConfirm: false,
error: errors
};
}
throw error;
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone,
companyName,
agree,
inn
},
error: errors
};
}
redirect('/pages/dashboard');
@@ -306,15 +399,13 @@ export async function tokenLifeExtension() {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
version: 1,
msg_id: 20008,
message_body: {
token: token
}
msg_id: 20008
}),
});
@@ -330,13 +421,8 @@ export async function tokenLifeExtension() {
}
}
export async function confirmEmail(
state: {error: string} | undefined,
formData: FormData
) {
export async function confirmEmail(userId: number, verifyToken: string) {
console.log('confirmEmail');
const emailConfirm = formData.get('emailConfirm') as string || '';
const userId = formData.get('userId') as string || '';
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -344,14 +430,14 @@ export async function confirmEmail(
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
'Authorization': `Bearer ${verifyToken}`,
},
body: JSON.stringify({
version: 1,
msg_id: 20009,
message_body: {
resend: 0,
user_id: userId,
verify_token: emailConfirm
user_id: userId
}
}),
});
@@ -362,12 +448,23 @@ export async function confirmEmail(
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 {
error: 'error'
}
success: false,
error: 'network-error'
};
}
redirect('/pages/dashboard');
@@ -376,7 +473,6 @@ export async function confirmEmail(
export async function resendConfirmEmail(userId: number) {
console.log('resendConfirmEmail');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
@@ -410,3 +506,178 @@ export async function resendConfirmEmail(userId: number) {
}
}
}
export type FormStateConfirmPassword = {
previousState: {
email: string;
password: string;
confirm_password: string;
verifyToken: string;
},
error: Record<string, string> | null
};
export async function resetPassword(
state: any | undefined,
formData: FormData,
) {
const email = formData.get('email');
try {
const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
version: 1,
msg_id: 20010,
message_body: {
email: email,
action: "resetPassword"
}
}),
});
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) {
} else {
throw 'error';
}
}
} catch (error) {
return {
error: error
};
}
console.log('redirect')
redirect(`/reset-password?email=${email}`);
}
export async function confirmPassword(
state: FormStateConfirmPassword | undefined,
formData: any,
): Promise<FormStateConfirmPassword | undefined> {
const email = formData.get('email');
const password = formData.get('password');
const confirm_password = formData.get('confirm_password');
const verifyToken = formData.get('verify-token');
console.log('confirmPassword');
const SignupFormSchema = await getSignupFormSchema('resetPassword');
const dataToValidate = {
password,
confirm_password
};
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
if (!validatedFields.success) {
const errors: Record<string, string> = {}
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: {
email,
password,
confirm_password,
verifyToken
},
error: errors
};
}
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: 20010,
message_body: {
email: email,
password: password,
action: "confirmVerification"
}
}),
});
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) {
await createSession(parsed.message_body.authToken, email as string);
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error: unknown) {
const errors: Record<string, string> = {};
if (error) {
const typedError = error as any;
switch (typedError.message_code) {
case 1:
errors['server'] = 'unknown-error'
break
case 2:
if (typedError.message_desc === 'Token not found or time expired') {
errors['server'] = 'token-not-found-or-time-expired'
} else {
errors['server'] = 'unknown-error'
}
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
return {
previousState: {
email,
password,
confirm_password,
verifyToken
},
error: errors
};
}
return {
previousState: {
email,
password,
confirm_password,
verifyToken
},
error: errors
};
}
redirect('/pages/dashboard');
}
+204
View File
@@ -0,0 +1,204 @@
'use server'
import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session';
import { headers } from 'next/headers';
export type CompanyUserRegistrationFormState = {
previousState: {
fullName: string;
email: string;
password: string;
confirm_password: string;
phone: string;
},
createdUser: string | null
error: Record<string, string> | null
} | undefined;
export async function companyUserRegistration(
state: CompanyUserRegistrationFormState | undefined,
formData: FormData,
): Promise<CompanyUserRegistrationFormState> {
const token = await getSessionData('token');
if (!token) {
return;
}
const headersList = await headers();
const fullName = formData.get('fullName') as string || '';
const email = formData.get('email') as string || '';
const password = formData.get('password') as string || '';
const confirm_password = formData.get('confirm_password') as string || '';
const phone = formData.get('phone') as string || '';
const SignupFormSchema = await getSignupFormSchema('b2c');
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const dataToValidate = {
fullName,
email,
phone: phone.replace(/[-\(\)\s]/g, ''),
password,
confirm_password,
agree: 'on'
};
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
console.log(`try to register company users ${email}`);
if (!validatedFields.success) {
const errors: Record<string, string> = {}
JSON.parse(validatedFields.error.message).forEach((obj: {
message: string,
path: string
}) => {
if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
} else {
errors[obj.path[0]] = obj.message;
}
});
console.log('registration error');
console.log(errors);
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone
},
createdUser: null,
error: errors
};
}
try {
const { fullName, email, password, phone } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20002,
message_body: {
fullName: fullName,
email,
phone: phone,
password,
accountType: 'b2b',
mail_verified: false,
ip: ip,
user_agent: userAgent,
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json() as {
message_desc: string,
message_body: { token: string },
message_code: number
};
if (parsed.message_code === 0) {
console.log(`registration - ${email}`);
console.log({
fullName: fullName,
email,
phone: phone,
password,
ip: ip,
user_agent: userAgent,
});
return {
previousState: {
fullName: '',
email: '',
password: '',
confirm_password: '',
phone: ''
},
createdUser: fullName,
error: null
}
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error: unknown) {
const errors: Record<string, string> = {};
if (error) {
const typedError = error as any;
switch (typedError.message_code) {
case 1:
errors['server'] = 'email-or-already-registered'
break;
case 2:
if (typedError.message_body?.fieldErrors) {
typedError.message_body?.fieldErrors?.forEach((obj: {
code: string,
field: string
}) => {
if (errors[obj.field]) {
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
} else {
errors[obj.field] = obj.code.replaceAll('.', '-');
}
});
} else {
errors['server'] = 'unknown-error'
}
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone
},
createdUser: null,
error: errors
};
}
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone
},
createdUser: null,
error: errors
};
}
}
+46 -12
View File
@@ -1,5 +1,5 @@
import * as z from 'zod'
import { createValidationSchema } from "@/app/lib/validation-config-parser";
import { createValidationSchema, createValidationSchemaForPassword } from '@/app/lib/validation-config-parser';
import validationConfig from '@/app/lib/validation-config.json';
export type FormState =
@@ -35,30 +35,31 @@ interface ValidationConfig {
const defaultConfig = validationConfig as ValidationConfig;
let cachedSignupSchema: any = null;
let schemaPromise: Promise<any> | null = null;
async function loadConfigFromBackend(): Promise<any> {
async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise<any> {
try {
const response = await fetch('/api/validation-config');
if (!response.ok) throw new Error('Failed to load config');
return await response.json();
} catch (error) {
console.error('Using default validation config:', error);
//сейчас выполняем это пока бека нету
/* console.error('Using default validation config:', error); */
return defaultConfig;
}
}
export async function getSignupFormSchema() {
console.log('get scheme');
if (cachedSignupSchema) {
return cachedSignupSchema;
}
export async function getSignupFormSchema(schemaType?: 'b2b' | 'b2c' | 'resetPassword') {
let schemaPromise: Promise<any> | null = null;
let cachedSignupSchema: any = null;
if (!schemaPromise) {
schemaPromise = (async () => {
const config = await loadConfigFromBackend();
cachedSignupSchema = createValidationSchema(config);
if (schemaType === 'resetPassword') {
cachedSignupSchema = createValidationSchemaForPassword(config);
} else {
cachedSignupSchema = createValidationSchema(config, schemaType);
}
return cachedSignupSchema;
})();
}
@@ -66,6 +67,7 @@ export async function getSignupFormSchema() {
return await schemaPromise;
}
// для валидации в реально времени на клиентской странице
export const SignupFormSchema = createValidationSchema(defaultConfig);
@@ -81,6 +83,30 @@ export const loginFormSchema = z
.trim(),
})
export const createComplaintSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'text-area-error-fill-complaint-text' })
.max(2000, {error: 'text-area-error-complaint-text-too-long'})
.trim(),
})
export const createClaimSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'fill-text-of-the-case' })
.max(2000, {error: 'claim-text-must-not-exceed-2000-characters'})
.trim(),
caseTitle: z
.string()
.min(1, { error: 'fill-title-of-the-case' })
.max(300, {error: 'claim-title-must-not-exceed-300-characters'})
.trim(),
})
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
export const API_BASE_URL = process.env.NODE_ENV === 'development'
@@ -92,3 +118,11 @@ export const testUserData = {
email: 'test@mail.com',
subscriptionType: 'base'
}
export const bankCardFormSchema = z
.object({
bankCard: z
.string()
.min(19, { error: 'error-bank-card' })
.trim()
})
+136 -12
View File
@@ -4,7 +4,23 @@ import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export async function getUserFilesData(page: number, pageSize: number) {
export async function getUserFilesData({
page,
pageSize,
sortBy,
sortOrder,
type,
date,
query = ''
}: {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc' | string;
type?: string;
date?: string;
query?: string;
}) {
const token = await getSessionData('token');
try {
@@ -14,35 +30,41 @@ export async function getUserFilesData(page: number, pageSize: number) {
version: 1,
msg_id: 20005,
message_body: {
action: 'user_files',
action: 'search_files',
page: page,
page_size: pageSize,
token: token
query: query,
sort_by: sortBy || '', // protectStatus, fileName, fileSize, createdAt, updatedAt, supportId
type: type || '', // image, video, document, audio
date: date || '', // today, week, month
sort_order: sortOrder
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'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;
throw parsed.message_text ? parsed.message_text : 'error';
}
} else {
throw (`${response.status}`);
throw `${response.status}`;
}
} catch (error) {
return error
return error;
}
}
export async function removeUserFile(fileId: string) {
export async function removeUserFile(fileId: string, fullDelete: number) {
const token = await getSessionData('token');
try {
@@ -54,12 +76,13 @@ export async function removeUserFile(fileId: string) {
message_body: {
action: 'delete_file',
file_id: fileId,
token: token
full_delete: fullDelete,
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
@@ -79,3 +102,104 @@ export async function removeUserFile(fileId: string) {
return error
}
}
export async function viewFileInfo(fileId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'file_info',
file_id: fileId
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
export async function downloadFile(fileId: string, fileName: string) {
const token = await getSessionData('token');
const response = await fetch(`${API_BASE_URL}/api/v1/files/download/${fileId}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (!response.ok) {
throw 'failed-to-download-file';
}
const arrayBuffer = await response.arrayBuffer()
const base64 = Buffer.from(arrayBuffer).toString('base64')
return {
data: base64,
contentType: response.headers.get('content-type') || 'application/octet-stream',
fileName: fileName
}
}
export async function FileAnAppeal(fileId: string, appealReason: string, additionalInfo: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'submit_appeal',
file_id: fileId,
appeal_reason: appealReason,
additional_info: additionalInfo
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
+119 -22
View File
@@ -9,17 +9,21 @@ interface initMessageBody {
extension: string,
file_size: number | string,
action?: string,
token?: string
token?: string,
convertTo?: string | undefined | null
}
export async function fileUpload(messageBody: initMessageBody) {
export async function fileUpload(messageBody: initMessageBody, convertTo?: string | null | undefined) {
const token = await getSessionData('token');
if (!token) {
return;
}
const message = messageBody;
message.action = "init";
message.token = token;
message.action = 'init';
if (convertTo && convertTo !== 'reject') {
message.convertTo = convertTo;
}
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -30,8 +34,9 @@ export async function fileUpload(messageBody: initMessageBody) {
message_body: message
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
@@ -61,6 +66,8 @@ export async function fileUpload(messageBody: initMessageBody) {
}
export async function cancelUpload(udloadId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
@@ -68,13 +75,14 @@ export async function cancelUpload(udloadId: string) {
version: 1,
msg_id: 20004,
message_body: {
action: "cancel",
action: 'cancel',
upload_id: udloadId,
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
@@ -101,31 +109,72 @@ export async function cancelUpload(udloadId: string) {
}
}
export async function chunkUpload(formData: FormData) {
export type ChunkResponse = {
errorMesage: string | null
fileId?: string
};
export async function chunkUpload(formData: FormData, itPrivate?: boolean): Promise<ChunkResponse> {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, {
const url = itPrivate
? `${API_BASE_URL}/api/v1/private/files/chunk`
: `${API_BASE_URL}/api/v1/files/chunk`;
/* if (token && itPrivate) {
formData.append('token', token);
} */
const response = await fetch(url, {
method: 'POST',
body: formData
body: formData,
headers: {
'Authorization': `Bearer ${token}`
}
});
/* ...(token && itPrivate && { 'Authorization': `Bearer ${token}` }) возможно нужно будет условно отправлять токен*/
if (response.ok) {
let parsed = await response.json();
if (parsed.message_desc === 'Upload cancelled successfully') {
return parsed.message_body;
if (parsed.message_code === 0) {
return {
errorMesage: null,
fileId: parsed.message_body.file_id
}
} else if (parsed.message_desc === 'Duplicate file upload') {
throw 'error-duplicate-file'
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: User not have tokens for protect') {
throw 'user-not-have-tokens-for-protect'
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: Failed to upload chunk: .wav file not have RIFF header') {
throw 'error-upload-chunk-wav-file-not-have-riff'
} else {
throw parsed;
throw 'error-uploading-file'
}
} else {
throw (`${response.status}`);
throw 'error-uploading-file'
}
} catch (error: unknown) {
if (error) {
const typedError = error as string;
return {
errorMesage: typedError
};
} else {
return {
errorMesage: 'error-uploading-file'
};
}
} catch (error) {
return error
}
}
export async function checkChunkStatus(upload_id: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
@@ -138,8 +187,9 @@ export async function checkChunkStatus(upload_id: string) {
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
@@ -161,6 +211,8 @@ export async function checkChunkStatus(upload_id: string) {
}
export async function getAllowedFilesExtensions(type: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
@@ -173,8 +225,9 @@ export async function getAllowedFilesExtensions(type: string) {
}
}),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
@@ -194,3 +247,47 @@ export async function getAllowedFilesExtensions(type: string) {
return error
}
}
export async function checkOperationPrice(action: string, filesCount: number, fileType: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30008,
message_body: {
action: action,
file_type: fileType,
count_files: filesCount
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
cost: 0,
success: false,
tokens_count: 0,
error: 'error'
}
}
}
+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
}
}
+437
View File
@@ -0,0 +1,437 @@
'use server'
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL, bankCardFormSchema } from '@/app/actions/definitions';
export async function fetchReferralsLevels() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30003,
message_body: {
action: 'levels',
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return {
referrals: parsed.message_body,
error: null
}
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
referrals: [],
error: error
}
}
}
export async function fetchReferralUserStats() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30003,
message_body: {
action: 'userStats',
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
error: error
}
}
}
export async function fetchReferralInvitees() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30003,
message_body: {
action: 'invitees',
pageSize: 100,
pageNumber: 0
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
error: error
}
}
}
export async function referralRefill() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30003,
message_body: {
action: 'refill',
amount: 2000,
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
error: error
}
}
}
export interface PayoutMethods {
cardNumber: string,
id: number,
type: string,
displayName: string,
maskedDetails: string,
default: boolean,
verified: boolean
}
export async function getPayoutMethods(): Promise<{
methods: PayoutMethods[],
error: string | null
}> {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/payouts/user/payout-methods`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
const parsed = await response.json();
return {
methods: parsed,
error: null
};
} else {
throw 'error';
}
} catch (error) {
return {
methods: [],
error: 'error'
};
}
}
export async function getUserPayments() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/payouts/requests`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
const parsed = await response.json();
return {
payments: parsed,
error: null
};
} else {
throw 'error';
}
} catch (error) {
return {
payments: [],
error: 'error'
};
}
}
export type PayoutMethodFormState = {
previousState: {
bankCard?: string | undefined;
paymentsType?: 'bank-card' | 'bank-transfer' | 'on-account-balance';
} | undefined,
success?: boolean,
error: Record<string, string | null> | null
};
export async function addPayoutMethod(
state: PayoutMethodFormState | undefined,
formData: FormData,
): Promise<PayoutMethodFormState> {
const token = await getSessionData('token');
console.log('addPayoutMethod');
const bankCard = formData.get('bankCard') as string || '';
const paymentsType = formData.get('paymentsType') as 'bank-card' | 'bank-transfer' | 'on-account-balance';
const validatedFields = bankCardFormSchema.safeParse({
bankCard
});
if (!validatedFields.success) {
const errors: Record<string, string> = {}
JSON.parse(validatedFields.error.message).forEach((obj: {
message: string,
path: string
}) => {
if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
} else {
errors[obj.path[0]] = obj.message;
}
});
return {
previousState: {
bankCard
},
success: false,
error: errors
};
}
try {
const { bankCard } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/payouts/payout-method/card`, {
method: 'POST',
body: JSON.stringify({
"cardNumber": bankCard.replaceAll(' ', '')
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
console.log(response);
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
return {
previousState: {
bankCard,
paymentsType
},
success: true,
error: null
};
} else {
throw 'error-save';
}
} catch (error: unknown) {
const errors: Record<string, string> = {};
errors['server'] = 'error-save'
if (error) {
const typedError = error as any;
}
return {
previousState: {
bankCard,
paymentsType
},
success: false,
error: errors
};
}
}
export type PayoutRequest = {
previousState: {
amount?: string | undefined;
} | undefined,
error: string | null
};
export async function createPayoutRequest(
state: PayoutRequest | undefined,
formData: FormData,
): Promise<PayoutRequest> {
const token = await getSessionData('token');
const amount = formData.get('amount') as string || '';
const payoutMethodId = formData.get('payoutMethodId') as string || '';
console.log('createPayoutRequest');
try {
const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, {
method: 'POST',
body: JSON.stringify({
'amount': amount,
'payoutMethodId': payoutMethodId
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.status === 400) {
const responseText = await response.text();
throw JSON.parse(responseText)?.message;
}
if (response.status === 409) {
const responseText = await response.text();
if (responseText.includes('Not have money for payout')) {
throw 'insufficient-funds-to-receive-payment'
}
if (responseText.includes('Not found payout method')) {
throw 'not-found-payout-method'
}
}
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
return {
previousState: {
amount
},
error: null
};
} else {
throw 'payment-error';
}
} catch (error: unknown) {
if (error) {
let typedError: string;
switch (error) {
case 'Sum of pay is required':
typedError = 'sum-of-pay-is-required';
break;
case 'Min sum for pay 1000':
typedError = 'min-sum-for-payout-1000';
break;
case 'insufficient-funds-to-receive-payment':
typedError = 'insufficient-funds-to-receive-payment';
break;
case 'not-found-payout-method':
typedError = 'not-found-payout-method';
break
default:
typedError = 'payment-error';
}
return {
previousState: {
amount
},
error: typedError
};
}
return {
previousState: {
amount
},
error: 'payment-error'
};
}
}
+125
View File
@@ -0,0 +1,125 @@
'use server'
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export async function searchUserFiles(fileId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed.message_desc;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
if (error === 'File for search unsupported') {
return {
errorMessage: 'file-for-search-unsupported'
}
} else {
return {
errorMessage: 'error'
}
}
}
}
export async function searchGlobalFiles(fileId: string, currentPage: number) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20007,
message_body: {
file_id: fileId,
page: currentPage,
check_similar_first: true,
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw 'error';
}
} catch (error) {
let errorMessage: string = 'error';
if (error) {
const typedError = error as any;
switch (typedError.message_desc) {
case 'No results found':
errorMessage = 'no-similar-files-title'
break;
default:
errorMessage = 'error'
break;
}
}
return {
images: [],
page: 0,
pageSize: 0,
totalPages: 0,
totalResults: 0,
error: errorMessage
}
}
}
export async function getSearchStats() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/check/file_stats`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
let parsed = await response.json();
return parsed;
} else {
return null;
}
} catch (error) {
return null;
}
}
+10 -5
View File
@@ -37,7 +37,7 @@ export async function createSession(token: string, email: string) {
});
const cookieStore = await cookies();
cookieStore.set('session', session, {
cookieStore.set('session_nc_db', session, {
httpOnly: true,
secure: false,
expires: expiresAt,
@@ -48,7 +48,7 @@ export async function createSession(token: string, email: string) {
export async function updateSession(newToken: string) {
const cookieStore = await cookies();
const session = cookieStore.get('session')?.value;
const session = cookieStore.get('session_nc_db')?.value;
if (!session) {
return null;
@@ -68,7 +68,7 @@ export async function updateSession(newToken: string) {
const updatedSession = await encrypt(updatedPayload);
cookieStore.set('session', updatedSession, {
cookieStore.set('session_nc_db', updatedSession, {
httpOnly: true,
secure: false,
expires: updatedPayload.expiresAt,
@@ -79,7 +79,7 @@ export async function updateSession(newToken: string) {
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session')?.value;
const sessionCookie = cookieStore.get('session_nc_db')?.value;
if (!sessionCookie) {
return null;
@@ -97,7 +97,12 @@ export async function getSessionData(type: 'token' | 'email'): Promise<string |
export async function deleteSession() {
console.log('delete session')
try {
const cookieStore = await cookies()
cookieStore.delete('session');
cookieStore.delete('session_nc_db');
} catch (error) {
} finally {
redirect('/login');
}
}
+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
}
}
+2 -2
View File
@@ -15,8 +15,8 @@ export async function vkAuthorization(data : any, codeVerifier: string) {
const response = await fetch(`${API_BASE_URL}/api/v1/vk/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
+2 -3
View File
@@ -4,7 +4,6 @@ import { API_BASE_URL } from '@/app/actions/definitions';
import { createSession } from '@/app/actions/session';
import { redirect } from 'next/navigation';
export async function YandexAuthorization(data: any, codeVerifier: string) {
console.log(data);
@@ -15,8 +14,8 @@ export async function YandexAuthorization(data: any, codeVerifier: string) {
const response = await fetch(`${API_BASE_URL}/api/v1/yandex/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
+122
View File
@@ -0,0 +1,122 @@
'use server'
import { YooCheckout, ICreatePayment } from '@a2seven/yoo-checkout';
import { getSessionData, deleteSession } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4' });
export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string, operationType: 'TARIFF' | 'TOKEN') {
const formData = new FormData();
const token = await getSessionData('token');
formData.append('email', email);
formData.append('tariffId', tariffId.toString());
formData.append('operationType', operationType);
formData.append('operationUuid', operationUuid);
try {
const response = await fetch(`${API_BASE_URL}/api/payments/create`, {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.paymentUuid) {
return true
} else {
throw 'error'
}
} else {
throw 'error'
}
} catch (error) {
return null
}
}
export async function yooKasaCreatePayment(amount: number, tariff: number, operationType: 'TARIFF' | 'TOKEN') {
const userEmail = await getSessionData('email');
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 10);
const idempotenceKey = `${userEmail}-${timestamp}-${amount}-${tariff}-${random}`;
if (!userEmail) {
return null;
}
const createPayload: ICreatePayment = {
amount: {
value: amount.toLocaleString().replace(/[,\s.]/g, ''),
currency: 'RUB'
},
confirmation: {
type: 'embedded',
},
metadata: {
userMail: userEmail,
tariff: tariff
},
merchant_customer_id: userEmail,
capture: true,
save_payment_method: operationType === 'TOKEN' ? false : true,
/* ...(operationType === 'TOKEN' && { save_payment_method: false }) */
};
try {
const payment = await checkout.createPayment(createPayload, idempotenceKey);
const response = await makePaymentOperation(userEmail, tariff, payment.id, operationType);
if (payment && response) {
return payment?.confirmation.confirmation_token;
} else {
return null;
}
} catch (error) {
return null;
}
}
export async function fetchPaymentsHistory() {
const email = await getSessionData('email');
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30005,
message_body: {
action: 'user_payments',
email: email
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return {
payments: parsed.message_body.payments,
error: null
};
} else {
throw parsed.message_code;
}
} else {
return null
}
} catch (error) {
return null
}
}
-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 }
);
}
}
@@ -48,6 +48,7 @@ export default function DropDownList({ children, value, callBack, translatedValu
setIsOpen(!isOpen)
}}
className="dropdown-button"
type="button"
>
<span className="flex items-center">
<span className="mr-2">
@@ -0,0 +1,115 @@
import React, { ReactNode, useState, useRef } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import { SelectedFile } from './UploadSectionFile';
import { useTranslations } from 'next-intl';
interface DropDownItem {
file: SelectedFile;
convertTo: string;
}
interface DropDownListProps {
children: ReactNode;
value: string | null | undefined;
callBack: (value: DropDownItem) => void;
translatedValue?: string | null | undefined;
selectedFiles: SelectedFile[];
}
interface LiElementProps extends React.LiHTMLAttributes<HTMLLIElement> {
'data-file'?: string;
'data-convert-to'?: string;
}
export default function DropDownListForImageConvert({
children,
value,
callBack,
translatedValue,
selectedFiles
}: DropDownListProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const t = useTranslations('Global')
useClickOutside(dropdownRef,
() => {
setIsOpen(false);
},
isOpen
);
const getDisplayValue = (): string => {
if (translatedValue) return translatedValue;
return value ? value : t('do-not-convert');
};
const findFileByName = (fileName: string): SelectedFile | undefined => {
return selectedFiles.find(file => file.name === fileName);
};
const enhancedChildren = React.Children.map(children, (child, index) => {
if (React.isValidElement<LiElementProps>(child)) {
const fileName = child.props['data-file'];
const convertTo = child.props['data-convert-to'];
const isCurrent = value === convertTo;
return React.cloneElement(child, {
onClick: () => {
if (fileName && convertTo) {
const file = findFileByName(fileName);
if (file) {
callBack({ file, convertTo });
} else {
console.error(`Файл с именем ${fileName} не найден`);
}
setIsOpen(false);
}
},
className: `dropdown-item ${isCurrent ? "current" : ""}`,
key: index,
});
}
return child;
});
return (
<div className="dropdown-wrapper" ref={dropdownRef}>
<button
onClick={() => {
setIsOpen(!isOpen);
}}
className="dropdown-button"
>
<span className="flex items-center">
<span className="mr-2">
{getDisplayValue()}
</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && (
<div className="dropdown-list">
<ul
className=""
role="listbox"
>
{enhancedChildren}
</ul>
</div>
)}
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { IconImageFile, IconVideoFile, IconAudioFile, IconDocument } from '@/app/ui/icons/icons';
export const FileTypeIcon = ({ type }: { type: string }) => {
switch (type) {
case 'image':
return <span className="color-image">
<IconImageFile />
</span>;
case 'video':
return <span className="color-video">
<IconVideoFile />
</span>;
case 'audio':
return <span className="color-audio">
<IconAudioFile />
</span>;
default:
return <span className="color-document">
<IconDocument />
</span>;
}
};
@@ -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>
);
}
+69
View File
@@ -0,0 +1,69 @@
'use client'
export function Pagination({ totalPages, currentPage, fileId, callBack }: any) {
if (totalPages === 0) {
return null;
}
const getVisiblePages = () => {
const pages = [];
const maxVisible = 7;
let start = Math.max(1, currentPage - 3);
let end = Math.min(totalPages, currentPage + 3);
if (end - start + 1 < maxVisible) {
if (start === 1) {
end = Math.min(totalPages, start + maxVisible - 1);
} else if (end === totalPages) {
start = Math.max(1, end - maxVisible + 1);
}
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
};
const visiblePages = getVisiblePages();
return (
<div className="pagination-wrapper">
{visiblePages[0] > 1 && (
<>
<button
className={`pagination-button`}
onClick={() => callBack(fileId, 1, true)}
>
1
</button>
{visiblePages[0] > 2 && <span>...</span>}
</>
)}
{visiblePages.map(page => (
<button
key={page}
onClick={() => callBack(fileId, page, true)}
className={`pagination-button ${page === currentPage ? 'current-page' : ''}`}
>
{page}
</button>
))}
{visiblePages[visiblePages.length - 1] < totalPages && (
<>
{visiblePages[visiblePages.length - 1] < totalPages - 1 && <span>...</span>}
<button
className={`pagination-button`}
onClick={() => callBack(fileId, totalPages, true)}
>
{totalPages}
</button>
</>
)}
</div>
);
}
+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>
);
}
+681
View File
@@ -0,0 +1,681 @@
'use client'
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
import { IconShieldAdd } from '@/app/ui/icons/icons';
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus, checkOperationPrice } from '@/app/actions/fileUpload';
import { useTranslations } from 'next-intl';
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
import { useQueryClient } from '@tanstack/react-query';
import DropDownListForImageConvert from '@/app/components/DropDownListForImageConvert';
export interface SelectedFile {
file: File;
preview: string | undefined;
name: string;
size: string;
dimensions?: {
width: number;
height: number;
};
uploadId?: string;
progress: number;
status: 'pending' | 'uploading' | 'completed' | 'error' | 'cancelled';
error?: string;
convertTo?: string | null;
}
interface FileUploadInitResponse {
upload_id: string;
file_name: string;
total_chunks: number;
chunk_size: number;
status: string;
}
interface UploadSectionFile {
fileType: string;
allowedExtensions: string[];
maxFileSize: number;
maxParallelUploads?: number;
}
export default function UploadSectionFile({
fileType,
allowedExtensions,
maxFileSize,
maxParallelUploads = 3
}: UploadSectionFile) {
const fileInputRef = useRef<HTMLInputElement>(null);
const [isDragging, setIsDragging] = useState<boolean>(false);
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);
const [error, setError] = useState<string | null>(null);
const [activeUploads, setActiveUploads] = useState<Set<string>>(new Set()); // ID активных загрузок
const queryClient = useQueryClient();
const isCancelledRef = useRef<Map<string, boolean>>(new Map()); // Для отслеживания отмен по файлам
const selectedFilesRef = useRef<SelectedFile[]>(selectedFiles);
const t = useTranslations('Global');
const acceptString = useMemo(() => {
if (!allowedExtensions || !Array.isArray(allowedExtensions)) {
return '';
}
return allowedExtensions.map(e => `.${e}`).join(', ');
}, [allowedExtensions]);
const validateFile = (file: File): { isValid: boolean; errorMessage?: string } => {
if (!allowedExtensions.includes(file.type as string)) {
const extension = file.name.split('.').pop()?.toLowerCase();
if (!extension || !allowedExtensions.includes(extension as string)) {
return {
isValid: false,
errorMessage: t('unsupported-file-format')
};
}
}
const MAX_SIZE = maxFileSize;
if (file.size > MAX_SIZE) {
return {
isValid: false,
errorMessage: t('the-file-is-too-large')
};
}
return { isValid: true };
};
const getImageDimensions = (file: File): Promise<{ width: number; height: number }> => {
return new Promise((resolve, reject) => {
const img = new Image();
const objectUrl = URL.createObjectURL(file);
img.onload = () => {
const dimensions = {
width: img.width,
height: img.height
};
URL.revokeObjectURL(objectUrl);
resolve(dimensions);
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error(t('error-uploading-file')));
};
img.src = objectUrl;
});
};
const handleFilesSelect = useCallback(async (files: File[]): Promise<void> => {
setError(null);
const newFiles: SelectedFile[] = [];
const selectedFileNames = new Set(selectedFiles.map(f => f.name));
const currentFileNames = new Set<string>();
const duplicateNames: string[] = [];
for (const file of files) {
if (selectedFileNames.has(file.name)) {
duplicateNames.push(file.name);
continue;
}
const validation = validateFile(file);
if (!validation.isValid) {
setError(prev => prev ? `${prev}, ${validation.errorMessage}` : validation.errorMessage || t('unknown-validation-error'));
continue;
}
let dimensions: {
width: number,
height: number,
} | undefined = undefined;
if (fileType === "image") {
try {
dimensions = await getImageDimensions(file) as { width: number, height: number };
if (dimensions?.width < 100 || dimensions?.height < 100 ||
dimensions?.width > 10000 || dimensions?.height > 10000) {
setError(prev => prev ? `${prev}, ${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.` :
`${t('image-size')}: ${dimensions?.width}x${dimensions?.height}. ${t('acceptable-range')}: 100x100 - 10000x10000 ${t('pixels')}.`);
continue;
}
} catch (err) {
setError(prev => prev ? `${prev}, ${t('error-reading-image')}` : t('error-reading-image'));
continue;
}
}
currentFileNames.add(file.name);
newFiles.push({
file,
name: file.name,
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
dimensions,
preview: file.size < 10 * 1024 * 1024 ? URL.createObjectURL(file) : undefined,
progress: 0,
status: 'pending'
});
}
if (duplicateNames.length > 0) {
const uniqueDuplicates = [...new Set(duplicateNames)];
setError(`${t('duplicate-files')}: ${uniqueDuplicates.join(', ')}`);
}
setSelectedFiles(prev => [...prev, ...newFiles]);
}, [fileType, selectedFiles]);
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
const files = Array.from(event.target.files || []);
handleFilesSelect(files);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleDragOver = (event: DragEvent<HTMLDivElement>): void => {
event.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (): void => {
setIsDragging(false);
};
const handleDrop = (event: DragEvent<HTMLDivElement>): void => {
event.preventDefault();
setIsDragging(false);
const files = Array.from(event.dataTransfer.files);
handleFilesSelect(files);
};
const handleClearFile = async (fileId?: string): Promise<void> => {
if (fileId) {
const fileToClear = selectedFiles.find(f => f.uploadId === fileId || f.name === fileId);
if (fileToClear?.preview) {
URL.revokeObjectURL(fileToClear.preview);
}
if (fileToClear?.uploadId) {
await cancelUpload(fileToClear.uploadId);
isCancelledRef.current.set(fileToClear.uploadId, true);
setActiveUploads(prev => {
const newSet = new Set(prev);
newSet.delete(fileToClear.uploadId!);
return newSet;
});
}
setSelectedFiles(prev => prev.filter(f =>
f.uploadId !== fileId && f.name !== fileId
));
} else {
console.log('clear all')
selectedFiles.forEach(file => {
if (file.preview) {
URL.revokeObjectURL(file.preview);
}
});
const cancelPromises = Array.from(activeUploads).map(uploadId =>
cancelUpload(uploadId)
);
await Promise.allSettled(cancelPromises);
isCancelledRef.current.clear();
setActiveUploads(new Set());
setSelectedFiles([]);
}
setError(null);
};
const uploadSingleFile = async (fileInfo: SelectedFile, uploadType: 'single' | 'multi'): Promise<void> => {
const fileId = `${fileInfo.name}-${Date.now()}`;
isCancelledRef.current.set(fileId, false);
if (uploadType === 'single') {
setError(null);
const priceCheck = await checkOperationPrice('checkTokensForProtect', 1, fileType);
if (!priceCheck.success) {
const errorMessage = `${t('there-are-not-enough-funds-to-protect-file', { count: priceCheck?.tokens_count })}`;
setError(errorMessage);
return;
}
}
try {
setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, status: 'uploading' } : f
));
const extension = fileInfo.file.name.split('.').pop() || '';
const initMessageBody = {
file_name: fileInfo.file.name,
file_type: fileType,
extension: extension,
file_size: fileInfo.file.size
};
const response = await fileUpload(initMessageBody, fileInfo.convertTo) as FileUploadInitResponse;
if (!response?.upload_id) {
throw new Error('Failed to get upload_id');
}
setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, uploadId: response.upload_id } : f
));
setActiveUploads(prev => new Set(prev).add(response.upload_id));
const CHUNK_SIZE = response.chunk_size;
const totalChunks = response.total_chunks;
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
if (isCancelledRef.current.get(fileId)) {
return;
}
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, fileInfo.file.size);
const chunk = fileInfo.file.slice(start, end);
const formData = new FormData();
formData.append('upload_id', response.upload_id);
formData.append('chunk_number', chunkIndex.toString());
formData.append('chunk', chunk);
const chunkResponse = await chunkUpload(formData);
if (chunkResponse.errorMesage !== null) {
throw chunkResponse.errorMesage;
}
const progress = Math.floor((chunkIndex + 1) / totalChunks * 100);
setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, progress } : f
));
}
const chunkStatus = await checkChunkStatus(response.upload_id);
if (chunkStatus.message_body.missing_chunks === 0) {
setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, status: 'completed', progress: 100 } : f
));
setActiveUploads(prev => {
const newSet = new Set(prev);
newSet.delete(response.upload_id);
return newSet;
});
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
await queryClient.invalidateQueries({ queryKey: ['userFilesInfo'] });
await queryClient.invalidateQueries({ queryKey: ['userData'] });
} else {
throw new Error('Not all chunks were uploaded');
}
} catch (error) {
if (!isCancelledRef.current.get(fileId)) {
const errorMessage = (typeof error === 'string') ? t(error) : t('error-uploading-file');
setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, status: 'error', error: errorMessage } : f
));
}
}
};
useEffect(() => {
selectedFilesRef.current = selectedFiles;
}, [selectedFiles]);
const handleStartAllUploads = useCallback(async (): Promise<void> => {
setError(null);
if (selectedFilesRef.current.length === 0) {
return;
}
const priceCheck = await checkOperationPrice('checkTokensForProtect', selectedFilesRef.current.length, fileType);
if (priceCheck.success) {
const uploadNextBatch = async () => {
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
const batch = currentPending.slice(0, maxParallelUploads);
if (batch.length === 0) return;
await Promise.all(batch.map(file => uploadSingleFile(file, 'multi')));
if (selectedFilesRef.current.some(f => f.status === 'pending')) {
await uploadNextBatch();
}
};
await uploadNextBatch();
} else {
const errorMessage = `${t('there-are-not-enough-funds-to-protect-files', { count: priceCheck?.tokens_count })}`;
setError(errorMessage);
}
}, [maxParallelUploads]);
const getTotalProgress = useMemo(() => {
if (selectedFiles.length === 0) return 0;
const total = selectedFiles.reduce((sum, file) => sum + file.progress, 0);
return Math.floor(total / selectedFiles.length);
}, [selectedFiles]);
const completedCount = useMemo(() =>
selectedFiles.filter(f => f.status === 'completed').length,
[selectedFiles]
);
const uploadingCount = useMemo(() =>
selectedFiles.filter(f => f.status === 'uploading').length,
[selectedFiles]
);
/* useEffect(() => {
// Обработка закрытия вкладки
// Тут можно попробовать изучить вопрос с navigator.sendBeacon('', blob)
// он нужен для того что бы послать запрос на бек при закрытии вкладки,
// но у него есть свои особенности и стандартные запросы не работают.
const handleUnload = () => {
if (activeUploads.size) {
//const data = JSON.stringify({ uploadId, reason: 'tab_closed' });
//const blob = new Blob([data], { type: 'application/json' });
//navigator.sendBeacon('/api/v1/data/cancel', blob);
console.log('sendBeacon');
}
};
if (activeUploads.size) {
window.addEventListener('unload', handleUnload);
}
return () => {
window.removeEventListener('unload', handleUnload);
};
}, [activeUploads.size]); */
useNavigationBlocker({
shouldBlock: !!activeUploads.size,
message: t('have-unsaved-changes'),
onConfirm: async () => {
console.log('User confirmed navigation');
if (activeUploads.size) {
await handleClearFile();
}
},
onCancel: () => {
console.log('User cancelled navigation');
}
});
function converButtonHandler(value: {
convertTo: string;
file: SelectedFile
}): void {
setSelectedFiles(selectedFiles.map(selectedFile => {
if (selectedFile.name === value.file.name) {
return {
...selectedFile,
convertTo: value.convertTo
};
}
return selectedFile;
}));
}
return (
<div className="upload-section" >
<h3>
{t('upload-for-protection', {
fileType: t(fileType.toLocaleLowerCase())
})}
</h3>
< div
className={`drag-drop-zone ${isDragging ? 'dragging' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<IconShieldAdd />
<h4>
{t('drag-the-file-here')}
</h4>
{
fileType === "image" && (
<p className="description" >
{t('image-resolution')}: 100x100 - 10000x10000 {t('pixels')}
</p>
)
}
<p className="description" >
{t('file-format')}: {acceptString}
</p>
< p className="description" >
{t('file-size')}: {t('to')} {(maxFileSize / 1024 / 1024)} {t('mb')}
</p>
< input
ref={fileInputRef}
type="file"
accept={acceptString}
style={{ display: 'none' }}
onChange={handleFileInputChange}
aria-label="Выбор файла для защиты"
multiple // Добавляем multiple для выбора нескольких файлов
/>
<button
className="btn btn-primary"
onClick={() => fileInputRef.current?.click()}
type="button"
disabled={uploadingCount > maxParallelUploads}
>
{t('select-files-to-protect')}
</button>
{
error && (
<div className="error-message">
<p className="error-message-text"> {error} </p>
</div>
)
}
{
selectedFiles.length > 0 && (
<div className="mt-6" >
<div className="selected-file-action-panel" >
<div>
<p className="selected-file-action-info" >
<span>{t('total-files')}: {selectedFiles.length}</span>
<span>{t('uploading')}: {uploadingCount}</span>
<span>{t('completed')}: {completedCount}</span>
</p>
<div className="w-full bg-gray-200 rounded-full h-2.5 mt-2">
<div
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
style={{ width: `${getTotalProgress}%` }
}
>
</div>
</div>
</div>
<div className="flex gap-2">
<button
className={`btn btn-primary`}
onClick={handleStartAllUploads}
type="button"
disabled={uploadingCount > 0 || selectedFiles.every(f => f.status !== 'pending')}
>
{t('upload-all')}
</button>
< button
className="btn btn-primary"
onClick={() => handleClearFile()}
type="button"
>
{t('clear-all')}
</button>
</div>
</div>
<div className="selected-file-wrapper">
{
selectedFiles.map((file, index) => (
<div
key={`${file.name}-${index}`}
className={`selected-file ${file.status === 'completed' ? 'done' : ''} ${file.status === 'error' ? 'error' : ''}`}
>
<div className="selected-file-file-info">
<div>
<p className="text-gray-700" >
<span className="font-medium" > {t('file')}: </span> {file.name}
</p>
< p className="text-gray-700" >
<span className="font-medium" > {t('size')}: </span> {file.size}
</p>
< p className="text-gray-700" >
<span className="font-medium" > {t('status')}: </span>
<span className={`ml-2 ${file.status === 'completed' ? 'text-green-600' :
file.status === 'uploading' ? 'text-blue-600' :
file.status === 'error' ? 'text-red-600' :
'text-gray-600'
}`}>
{
file.status === 'completed' ? t('completed') :
file.status === 'uploading' ? t('uploading') :
file.status === 'error' ? t('error') :
t('pending')
}
</span>
</p>
{
file.error && (
<p className="text-red-600 text-sm mt-1" > {file.error} </p>
)
}
</div>
<div className="selected-file-right-block">
{
(fileType === 'image' && file.status === 'pending') && (
<>
<DropDownListForImageConvert
value={file.convertTo}
translatedValue={file.convertTo === 'reject' ? t('do-not-convert') : file.convertTo}
callBack={converButtonHandler}
selectedFiles={selectedFiles}
>
<li data-file={file.name} data-convert-to="reject">
{t('do-not-convert')}
</li>
{
allowedExtensions && allowedExtensions.map(extension => {
if (extension === 'gif') {
return null
}
return (
<li key={extension} data-file={file.name} data-convert-to={extension}>
{t('convert-to')} {extension}
</li>
)
})
}
</DropDownListForImageConvert>
</>
)
}
<div
className="flex"
>
{file.status !== 'error' && (
<div className="upload-progress-circle">
<svg viewBox="0 0 36 36" className="circular-chart">
<path
className="circle-bg"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="#e5e7eb"
strokeWidth="3"
/>
<path
className="circle"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke={file.status === 'completed' ? '#10b981' : file.status === 'uploading' ? '#155dfc' : '#6b7280'}
strokeWidth="4"
strokeDasharray={`${file.progress}, 100`}
strokeLinecap="round"
/>
</svg>
<span className="circle-percentage">{file.progress}%</span>
</div>
)}
< button
className="btn btn-cancel"
onClick={() => handleClearFile(file.uploadId || file.name)}
type="button"
>
{t('remove')}
</button>
</div>
</div>
</div>
{
(fileType === 'image' && file.preview) && (
<div
className="image-preview-wrapper"
>
<img
src={file.preview}
alt="Предпросмотр загруженного изображения"
className="uploaded-image"
onError={() => setError('Не удалось загрузить превью изображения')
}
/>
</div>
)
}
{
file.status === 'pending' && (
<div className="mt-2 text-center" >
<button
className="btn btn-confirm"
onClick={() => uploadSingleFile(file, 'single')
}
type="button"
disabled={uploadingCount >= maxParallelUploads}
>
{t('start-upload')}
</button>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}
+11 -18
View File
@@ -14,10 +14,6 @@ export default function YandexAuthWithScript() {
const [isReady, setIsReady] = useState(false);
const [showButton, setShowButton] = useState(true);
const handleLogin = (token: string) => {
console.log('handleLogin');
};
useEffect(() => {
if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return;
@@ -25,31 +21,28 @@ export default function YandexAuthWithScript() {
{
client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
response_type: 'token',
redirect_uri: `http://localhost:2999/ru/login`,
redirect_uri: `https://fallibly-desired-moonfish.cloudpub.ru/en/login`,
},
'http://localhost:2999',
'https://fallibly-desired-moonfish.cloudpub.ru',
{
parentId: buttonRef.current.id,
view: 'button',
buttonView: 'main',
buttonTheme: 'light',
buttonSize: 'm',
buttonSize: 'l',
buttonBorderRadius: 10
}
)
.then((result: any) => {
console.log('then');
.then(function (result: any) {
console.log('you at least try?')
console.log(result);
if (result?.access_token) {
handleLogin(result.access_token);
} else {
setTimeout(() => {
setShowButton(false);
}, 1000)
}
return result.handler();
})
.catch((error: any) => {
console.error('Yandex auth error:', error);
.then(function (data: any) {
console.log('Сообщение с токеном: ', data);
})
.catch(function (error: any) {
console.log('Что-то пошло не так: ', error);
});
}, [isReady]);
+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
-876
View File
@@ -1,876 +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 { IconImageFile, IconVideoFile, IconAudioFile, IconEye, IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconShieldExclamation, IconDelete } 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 } 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';
type FileType = 'image' | 'video' | 'audio';
type FileItem = {
id: string;
fileName: string;
fileType: string;
violations?: number | undefined;
size?: number | undefined;
uploadDate?: number;
status?: string;
_original?: ApiFile;
};
type ApiFile = {
id: string;
originalFileName: string;
mimeType: string;
violations: number;
fileSize: number;
updatedAt: string;
status: string;
};
type ApiResponse = {
files?: ApiFile[];
};
// Иконки для типов файлов
const FileTypeIcon = ({ type }: { type: string }) => {
switch (type) {
case 'image':
return <span className="color-image">
<IconImageFile />
</span>;
case 'video':
return <span className="color-video">
<IconVideoFile />
</span>;
case 'audio':
return <span className="color-audio">
<IconAudioFile />
</span>;
default:
return <span>📄</span>;
}
};
// Форматирование даты из 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 [datePart, timePart] = item.updatedAt.split(' ');
const [day, month, year] = datePart.split('-').map(Number);
const [hours, minutes, seconds] = timePart.split(':').map(Number);
const newDate = new Date(year, month - 1, day, hours, minutes, seconds).getTime();
return {
id: item.id,
fileName: item.originalFileName,
fileType: item.mimeType.toLocaleLowerCase(),
violations: item.violations,
size: item.fileSize,
uploadDate: newDate,
status: item.status,
_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: '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?.status}</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">
{t('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={() => handleView(row.original)}
className="bg-blue-500 hover:bg-blue-600"
>
<IconEye />
</button>
<button
onClick={() => handleProtect(row.original)}
className="bg-violet-500 hover:bg-violet-600"
>
<IconShieldExclamation />
</button>
</div>
<div className="actions-group">
<button
onClick={() => handleDownload(row.original)}
disabled={isFileLoading}
className="table-action-download"
>
<IconFileDownload />
<span>
{t('download')}
</span>
</button>
<button
onClick={() => handleOpenWindowForRemove(row.original)}
className="table-action-delete"
>
<IconDelete />
<span>
{t('delete')}
</span>
</button>
</div>
</div>
),
enableSorting: false,
enableColumnFilter: false,
},
],
[]
);
// Обработчики действий
const handleView = (file: FileItem) => {
console.log(`Просмотр файла: ${file.fileName}`);
};
const handleDownload = async (file: FileItem) => {
setIsFileLoading(true);
try {
const response = await fetch(`/api/download/${file.id}`);
if (!response.ok) {
throw new Error(`error: ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.fileName || `file-${file.id}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - ${file.fileName}`);
} catch (error) {
toast.error(t('failed-to-download-file'))
} finally {
setIsFileLoading(false);
}
};
const handleProtect = (file: FileItem) => {
console.log(`Щиток: ${file.fileName}`);
};
const handleOpenWindowForRemove = async (file: FileItem) => {
setOpenWindowChildren(() => {
return (
<div>
<h3 className="text-2xl">
{t('you-sure-you-want-to-delete')}
</h3>
<div className="mt-2 mb-8 text-center">{file.fileName}</div>
<div className="flex justify-center gap-4">
<button className="btn-primary btn-modal"
onClick={() => {
deleteMutation.mutate(file.id)
}}
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: removeUserFile,
onMutate: async (fileId: string) => {
await queryClient.cancelQueries({ queryKey: ['userFilesData'] });
queryClient.setQueryData<ApiResponse>(['userFilesData'], (old) => {
if (!old?.files) return old;
return {
...old,
files: old.files.filter(file => file.id !== fileId)
};
});
},
onError: () => {
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
toast.error(t('error'));
},
onSuccess: (response, fileId) => {
if (response === fileId) {
queryClient.invalidateQueries({
queryKey: ['userFilesData'],
refetchType: 'active'
});
queryClient.invalidateQueries({
queryKey: ['userFilesInfo']
});
setOpenWindow(false);
setOpenWindowChildren(null);
toast.success(t('file-has-been-deleted'));
}
},
});
// Фильтрация по типу файла и дате
const filteredData = useMemo(() => {
let result = tableData;
if (!result) {
return [];
}
// Фильтр по типу файла
if (typeFilter !== 'all') {
result = result.filter(item => item.fileType === typeFilter);
}
// Фильтр по дате
if (dateFilter !== 'all') {
const now = Date.now();
const oneDay = 24 * 60 * 60 * 1000;
const sevenDays = 7 * oneDay;
const thirtyDays = 30 * oneDay;
switch (dateFilter) {
case 'today':
const todayStart = new Date().setHours(0, 0, 0, 0);
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate >= todayStart
} else {
return 0
}
});
break;
case 'week':
const weekAgo = now - sevenDays;
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate >= weekAgo;
}
});
break;
case 'month':
const monthAgo = now - thirtyDays;
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate >= monthAgo;
}
});
break;
case 'older':
const monthAgo2 = now - thirtyDays;
result = result.filter(item => {
if (item.uploadDate) {
return item.uploadDate < monthAgo2;
}
});
break;
}
}
return result;
}, [tableData, typeFilter, dateFilter]);
useEffect(() => {
const currentPageRows = table.getRowModel().rows;
const pageCount = table.getPageCount();
if (currentPageRows.length === 0 && pagination.pageIndex > 0 && pageCount > 0) {
table.setPageIndex(pagination.pageIndex - 1);
}
}, [filteredData, pagination.pageIndex]);
// Создание таблицы
const table = useReactTable({
data: filteredData,
columns,
state: {
sorting,
columnFilters,
pagination
},
autoResetPageIndex: false,
onPaginationChange: setPagination,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFilteredRowModel: getFilteredRowModel(),
initialState: {
pagination: {
pageSize: 10,
},
},
});
const pluralizeFiles = (number: number) => {
const translate = [t('file'), t('files-few'), t('files')];
return pluralize(number, translate[0], translate[1], translate[2], locale);
};
return (
<div className="tanstak-table-wrapper">
<h3
className="tanstak-table-title"
>
{(() => {
switch (fileType) {
case 'image':
return t('protected-image')
case 'video':
return t('protected-video')
case 'audio':
return t('protected-audio')
default:
return t('table-description')
}
})()}
</h3>
<ModalWindow children={openWindowChildren} state={openWindow} callBack={setOpenWindow} />
{/* Фильтры */}
<div className="tanstak-table-filtres" style={{ display: "none" }}>
<div className="table-filtres-wrapper">
<div className="table-filtres-item">
<div className="table-filtres-label">{t('date-filter')}:</div>
<DropDownList
value={dateFilter}
translatedValue={(() => {
switch (dateFilter) {
case 'all':
return t('all-dates')
case 'week':
return t('for-a-week')
case 'month':
return t('for-a-month')
case 'older':
return t('older-than-a-month')
default:
return t('today')
}
})()}
callBack={setDateFilter}
>
<li value="all">
{t('all-dates')}
</li>
<li value="today">
{t('today')}
</li>
<li value="week">
{t('for-a-week')}
</li>
<li value="month">
{t('for-a-month')}
</li>
<li value="older">
{t('older-than-a-month')}
</li>
</DropDownList>
</div>
{fileType === "all" && (
<div className="table-filtres-item">
<div className="table-filtres-label">{t('type-filter')}:</div>
<DropDownList
value={typeFilter}
translatedValue={
(() => {
switch (typeFilter) {
case 'image':
return t('images')
case 'video':
return t('videos')
case 'audio':
return t('audios')
default:
return t('all-types')
}
})()
}
callBack={setTypeFilter}
>
<li value="all">
{t('all-types')}
</li>
<li value="image">
{t('images-few')}
</li>
<li value="video">
{t('videos')}
</li>
<li value="audio">
{t('audios')}
</li>
</DropDownList>
</div>
)}
</div>
<div className="table-filtres-item">
<div className="table-filtres-label">{t('items-per-page')}:</div>
<DropDownList
value={table.getState().pagination.pageSize}
//@ts-ignore сделал так потому что для table.setPageSize нужна цифра
callBack={table.setPageSize}
>
{[5, 10, 20, 50, 100].map(pageSize => (
<li key={pageSize} value={pageSize}>
{t('show')} {pageSize}
</li>
))}
</DropDownList>
</div>
</div>
{/* Таблица */}
<div className="tanstak-table-block">
<table className="tanstak-table">
<thead className="tanstak-table-head">
{table.getHeaderGroups().map(headerGroup => (
<tr key={headerGroup.id}>
{headerGroup.headers.map(header => (
<th
key={header.id}
>
{header.isPlaceholder
? null
: typeof header.column.columnDef.header === 'function'
? header.column.columnDef.header(header.getContext())
: header.column.columnDef.header as string}
</th>
))}
</tr>
))}
</thead>
<tbody className="tanstak-table-body">
{table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>
{typeof cell.column.columnDef.cell === 'function'
? cell.column.columnDef.cell(cell.getContext())
: cell.getValue() as string}
</td>
))}
</tr>
))
) : (
<tr>
<td colSpan={columns.length} className="text-center">
{t('no-data-for-selected-filters')}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Пагинация */}
<div className="tanstak-table-pagination">
<div className="pagination-info">
<span className="pagination-info-pages">
{t('page')}{' '}
<strong>
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
</strong>
</span>
<span className="pagination-info-files">
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length} {pluralizeFiles(filteredData.length || 0)}
</span>
</div>
<div className="pagination-controls">
{table.getCanPreviousPage() && (
<button
className="arrow"
onClick={() => table.firstPage()}
disabled={!table.getCanPreviousPage()}
>
<IconDoubleArrowLeft />
</button>
)}
{table.getCanPreviousPage() && (
<button
className="arrow"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<IconArrowLeft />
</button>
)}
<div className="pagination-controls-pages">
{Array.from({ length: Math.min(5, table.getPageCount()) }, (_, i) => {
const pageIndex = Math.max(
0,
Math.min(
table.getPageCount() - 5,
table.getState().pagination.pageIndex - 2
)
) + i;
if (pageIndex < table.getPageCount()) {
return (
<button
key={pageIndex}
className={`${table.getState().pagination.pageIndex === pageIndex
? 'current'
: 'other'
}`}
onClick={() => table.setPageIndex(pageIndex)}
>
{pageIndex + 1}
</button>
);
}
return null;
})}
</div>
{table.getCanNextPage() && (
<button
className="arrow"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<IconArrowRight />
</button>
)}
{table.getCanNextPage() && (
<button
className="arrow"
onClick={() => table.lastPage()}
disabled={!table.getCanNextPage()}
>
<IconDoubleArrowRight />
</button>
)}
</div>
</div>
</div >
);
}
+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
});
};
@@ -0,0 +1,83 @@
import { useQuery } from '@tanstack/react-query';
import { getUserData } from '@/app/actions/action';
export interface Tariff {
id: number;
type: string;
name: string;
price: number;
tokens: number;
maxFilesCount: number;
diskSize: number;
maxUsers?: number;
}
export interface TariffInfo {
id: string;
status: string;
startTariff: string;
endTariff: string;
tariffId: number | null;
tariffName: string | null;
tokens: number | null;
maxFileOnDisk: number | null;
currentFileOnDisk: number;
currentFileCounts: number;
maxFileCounts: number;
}
export interface UserData {
fullName: string;
company: string | null;
email: string;
active: boolean;
phone: string;
genderType: string | null;
birthday: string | null;
createdAt: string;
subscriptionType: string;
tariffs: Tariff[];
tariffInfo: TariffInfo;
permission: number;
verifiedStatus?: 'unknow' | 'NOT_VERIFIED' | 'VERIFICATION_IN_PROGRESS' | 'VERIFIED' | 'VERIFICATION_FAILED';
}
export const useUserProfile = () => {
return useQuery({
queryKey: ['userData'],
queryFn: getUserData,
select: (data): UserData => {
if (!data) {
return {
fullName: '',
company: null,
email: '',
active: false,
phone: '',
genderType: '',
birthday: null,
createdAt: '',
subscriptionType: '',
tariffs: [],
tariffInfo: {
id: '',
status: '',
startTariff: '',
endTariff: '',
tariffId: 0,
tariffName: null,
tokens: 0,
maxFileOnDisk: null,
currentFileOnDisk: 0,
currentFileCounts: 0,
maxFileCounts: 0,
},
permission: 0,
verifiedStatus: 'unknow'
};
}
return data;
},
});
};
@@ -0,0 +1,96 @@
import { useQuery } from '@tanstack/react-query';
import { getUserFilesInfo } from '@/app/actions/action';
export interface FilesInfo {
total: {
size: number;
quantity: number;
check: number;
violation: number;
protected: number;
};
images: {
quantity: number;
size: number;
check: number;
violation: number;
protected: number;
};
videos: {
quantity: number;
size: number;
check: number;
violation: number;
protected: number;
};
audios: {
quantity: number;
size: number;
check: number;
violation: number;
protected: number;
};
documents: {
quantity: number;
size: number;
check: number;
violation: number;
protected: number;
};
}
export const useUserFilesInfo = () => {
return useQuery({
queryKey: ['userFilesInfo'],
queryFn: getUserFilesInfo,
select: (data): FilesInfo | null => {
if (!data) {
return {
total: { size: 0, quantity: 0, check: 0, violation: 0, protected: 0 },
images: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
videos: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
audios: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
documents: { quantity: 0, size: 0, check: 0, violation: 0, protected: 0 },
};
}
return {
total: {
size: data.all_files_size,
quantity: data.all_files_quantity,
check: data.all_files_check,
violation: data.all_files_violation,
protected: data.protected_files_count,
},
images: {
quantity: data.images_quantity,
size: data.images_size,
check: data.images_check,
violation: data.images_violations,
protected: data.protected_image_files_count,
},
videos: {
quantity: data.videos_quantity,
size: data.videos_size,
check: data.videos_check,
violation: data.videos_violations,
protected: data.protected_video_files_count,
},
audios: {
quantity: data.audios_quantity,
size: data.audios_size,
check: data.audios_check,
violation: data.audios_violations,
protected: data.protected_audio_files_count,
},
documents: {
quantity: data.document_quantity,
size: data.document_size,
check: data.document_check,
violation: data.document_violations,
protected: data.protected_document_files_count,
},
};
},
});
};
@@ -0,0 +1,38 @@
import { useQuery } from '@tanstack/react-query';
import { fetchPaymentsHistory } from '@/app/actions/yooKassa';
export interface PaymentsData {
amount: number,
cancellationReason: string | null,
createdAt: string,
id: number,
operationType: string,
paymentUuid: string,
status: string,
updatedAt: string,
}
export interface UserPaymentsData {
payments: PaymentsData[],
error: string | null
}
export const useUserPaymentsData = () => {
return useQuery({
queryKey: ['userPaymentsData'],
queryFn: fetchPaymentsHistory,
select: (data): UserPaymentsData => {
if (!data) {
return {
payments: [],
error: null
}
}
return data;
},
refetchInterval: 10000,
retry: false
});
};
@@ -0,0 +1,48 @@
import { useQuery } from '@tanstack/react-query';
import { getSearchStats } from '@/app/actions/searchActions';
interface SearchType {
count: number,
fileType: string,
}
export interface UserSearchData {
allCountForPeriod: number;
image: SearchType;
video: SearchType;
audio: SearchType;
document: SearchType;
}
export const useUserSearchData = () => {
return useQuery({
queryKey: ['userSearchData'],
queryFn: getSearchStats,
select: (data): UserSearchData | null => {
if (!data) {
return {
allCountForPeriod: 0,
image: {
count: 0,
fileType: 'image'
},
video: {
count: 0,
fileType: 'video'
},
audio: {
count: 0,
fileType: 'audio'
},
document: {
count: 0,
fileType: 'document'
}
};
}
return data;
},
retry: false
});
};
@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { fetchViolationAnalyticStatistic } from '@/app/actions/violationActions';
export interface UseViolationStatistic {
[key: string]: number
}
export const useViolationAnalyticStatistic = (group: 'domain' | 'tld') => {
return useQuery({
queryKey: ['violationAnalyticStatistic', group],
queryFn: () => {
return fetchViolationAnalyticStatistic(group)
},
select: (data: UseViolationStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchViolationFinalSummaryStatistic } from '@/app/actions/violationActions';
export interface UseViolationFinalSummaryStatistic {
complaints_without_case: number;
total_complaints: number;
total_law_cases: number;
total_violations: number;
violations_with_complaint: number;
violations_without_complaint: number;
}
export const useViolationFinalSummaryStatistic = () => {
return useQuery({
queryKey: ['violationFinalSummaryStatistic'],
queryFn: () => {
return fetchViolationFinalSummaryStatistic()
},
select: (data: UseViolationFinalSummaryStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchViolationStatistic } from '@/app/actions/violationActions';
export interface UseViolationStatistic {
total_violations: number,
new_violations: number,
in_progress_violations: number,
resolved_violations: number,
total_complaints: number,
total_law_cases: number
}
export const useViolationStatistic = (id?: string) => {
return useQuery({
queryKey: ['violationStatistic', id],
queryFn: () => {
return fetchViolationStatistic(id)
},
select: (data: UseViolationStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
+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;
}
+4
View File
@@ -1,4 +1,8 @@
export function convertBytes(bytes: number) {
if (!bytes) {
return 0;
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) return '0 B';
+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';
}
+18
View File
@@ -0,0 +1,18 @@
export function getProcents(
currentNumber: number | undefined,
totalNumber: number | undefined,
decimals: number = 0
): number {
if (!currentNumber && !totalNumber) {
return 0;
}
if (totalNumber === 0 || typeof currentNumber === 'undefined' || typeof totalNumber === 'undefined') {
return 0;
}
const percentage = (currentNumber / totalNumber) * 100;
const factor = Math.pow(10, decimals);
return Math.round(percentage * factor) / factor;
}
+59 -2
View File
@@ -1,6 +1,35 @@
import { QueryClient } from '@tanstack/react-query';
import { getUserData, getUserFilesInfo } from '@/app/actions/action';
import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action';
import { getUserFilesData } from '@/app/actions/fileEntity';
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
export interface allFilesExtensions {
videos: string[];
audios: string[];
images: string[];
documents: string[];
maxFileSize: number;
}
export async function getAllFilesExtensions(): Promise<allFilesExtensions> {
const [video, audio, image, document] = await Promise.all([
getAllowedFilesExtensions('video'),
getAllowedFilesExtensions('audio'),
getAllowedFilesExtensions('image'),
getAllowedFilesExtensions('document')
]);
const result: allFilesExtensions = {
videos: video?.file_extension ? video.file_extension : [],
audios: audio?.file_extension ? audio.file_extension : [],
images: image?.file_extension ? image.file_extension : [],
documents: document?.file_extension ? document.file_extension : [],
maxFileSize: video?.max_file_size ? video.max_file_size : 0
}
return result;
}
export async function prefetchLayoutQueries(queryClient: QueryClient) {
await Promise.all([
@@ -11,7 +40,15 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
queryClient.prefetchQuery({
queryKey: ['userFilesData'],
queryFn: () => {
return getUserFilesData(1, 10000);
return getUserFilesData({
page: 1,
pageSize: 10,
sortBy: '',
sortOrder: '',
type: '',
date: '',
query: ''
});
},
staleTime: 30 * 1000
}),
@@ -19,5 +56,25 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
queryKey: ['userFilesInfo'],
queryFn: () => getUserFilesInfo()
}),
queryClient.prefetchQuery({
queryKey: ['referralUserStats'],
queryFn: () => fetchReferralUserStats()
}),
queryClient.prefetchQuery({
queryKey: ['backendBuildDate'],
queryFn: async () => {
try {
const response = await getBuildData();
return response.buildTimeBack ?? null;
} catch (error) {
return null;
}
}
}),
queryClient.prefetchQuery({
queryKey: ['allFilesExtensions'],
queryFn: () => getAllFilesExtensions(),
staleTime: 5 * 60 * 1000
}),
]);
}
+87 -58
View File
@@ -19,7 +19,7 @@ interface ValidationConfig {
/* const EMAIL_REGEX = /^([a-z0-9.\-_]+|[а-яё0-9.\-_]+)@(([a-z0-9.\-]+)\.([a-z]{2,})|([a-z0-9.\-]+)\.([а-яё]{2,})|([а-яё0-9.\-]+)\.([a-z]{2,})|([а-яё0-9.\-]+)\.([а-яё]{2,}))$/i; */
export function createValidationSchema(config: ValidationConfig) {
export function createValidationSchema(config: ValidationConfig, accountType?: 'b2b' | 'b2c') {
const schema: Record<string, any> = {};
// fullName
@@ -38,6 +38,22 @@ export function createValidationSchema(config: ValidationConfig) {
.trim();
}
// fullName
if (config.companyName && accountType !== 'b2c') {
schema.companyName = z
.string()
.trim()
.min(config.companyName.minSize?.value || 1, {
error: config.companyName.minSize?.message || 'register-error-company-name-min'
})
.max(config.companyName.maxSize?.value || 100, {
error: config.companyName.maxSize?.message || 'register-error-company-name-max'
})
.regex(new RegExp(config.companyName.allowedSymbols?.value || '.*'), {
message: config.companyName.allowedSymbols?.message || 'register-error-company-name-not-allowed-symbols'
});
}
// email
if (config.email) {
let emailSchema = z
@@ -147,73 +163,33 @@ export function createValidationSchema(config: ValidationConfig) {
schema.password = passwordSchema;
}
// companyName (опциональное поле)
if (config.companyName) {
let companySchema = z.string();
// inn
if (config.inn && accountType !== 'b2c') {
let innSchema = z.string();
// Если поле опциональное, добавляем .optional().nullable()
if (config.companyName.optional?.value) {
companySchema = companySchema
.optional()
.nullable()
.transform((val) => (val === null || val === undefined ? '' : val))
.refine(
(value) => {
// Для пустого значения пропускаем проверки
if (!value || value.trim().length === 0) return true;
// Проверка минимальной длины
const minLength = config.companyName?.minSize?.value || 2;
return value.trim().length >= minLength;
},
{
message: config.companyName.minSize?.message || 'register-error-company-name-min'
}
) as unknown as z.ZodString;
} else {
// Если поле обязательное
if (config.companyName.minSize) {
companySchema = companySchema.min(config.companyName.minSize.value, {
error: config.companyName.minSize.message
// Минимальная длина
if (config.inn.minSize) {
innSchema = innSchema.min(config.inn.minSize.value, {
error: config.inn.minSize.message
});
}
}
// Максимальная длина (применяется всегда)
if (config.companyName.maxSize) {
companySchema = companySchema.refine(
(value) => {
if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) {
return true;
}
const maxLength = config.companyName?.maxSize?.value || 200;
return (value || '').trim().length <= maxLength;
},
{
message: config.companyName.maxSize.message
}
);
// Максимальная длина
if (config.inn.maxSize) {
innSchema = innSchema.max(config.inn.maxSize.value, {
error: config.inn.maxSize.message
});
}
// Допустимые символы
if (config.companyName.allowedSymbols?.value) {
companySchema = companySchema.refine(
(value) => {
if (config.companyName?.optional?.value && (!value || value.trim().length === 0)) {
return true;
}
const regex = new RegExp(config.companyName.allowedSymbols!.value);
return regex.test(value || '');
},
{
message: config.companyName.allowedSymbols.message
}
if (config.inn.allowedSymbols) {
innSchema = innSchema.regex(
new RegExp(config.inn.allowedSymbols.value),
{ message: config.inn.allowedSymbols.message }
);
}
companySchema = companySchema.transform((val) => val?.trim() || '') as unknown as z.ZodString;
schema.companyName = companySchema;
schema.inn = innSchema;
}
// Дополнительные поля для формы регистрации
@@ -227,3 +203,56 @@ export function createValidationSchema(config: ValidationConfig) {
path: ['confirm_password'],
});
}
export function createValidationSchemaForPassword(config: ValidationConfig) {
const schema: Record<string, any> = {};
// password
if (config.password) {
let passwordSchema = z
.string()
.min(config.password.minSize?.value || 8, {
error: config.password.minSize?.message || 'register-error-password-symbols'
})
.max(config.password.maxSize?.value || 124, {
error: config.password.maxSize?.message || 'register-error-max-password'
})
.trim();
// Проверка допустимых символов
if (config.password.allowedSymbols?.value) {
passwordSchema = passwordSchema.regex(
new RegExp(config.password.allowedSymbols.value),
{ message: config.password.allowedSymbols.message }
);
}
// Проверка наличия цифры
if (config.password.requiredDigit?.value) {
passwordSchema = passwordSchema.regex(
/[0-9]/,
{ message: config.password.requiredDigit.message }
);
}
// Проверка наличия буквы
if (config.password.requiredLetter?.value) {
passwordSchema = passwordSchema.regex(
/[a-zA-Zа-яёА-ЯЁ]/,
{ message: config.password.requiredLetter.message }
);
}
schema.password = passwordSchema;
}
// Дополнительные поля для формы регистрации
schema.confirm_password = z.string();
return z
.object(schema)
.refine((data) => data.password === data.confirm_password, {
message: 'repeat-password',
path: ['confirm_password'],
});
}
+16 -2
View File
@@ -70,7 +70,7 @@
"message": "register-error-company-name-min"
},
"allowedSymbols": {
"value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]+$",
"value": "^[a-zA-Zа-яА-ЯёЁ0-9\\s\\-&.,'\"()]*$",
"message": "register-error-company-name-not-allowed-symbols"
}
},
@@ -80,12 +80,26 @@
"message": "register-error-phone-max"
},
"minSize": {
"value": 2,
"value": 12,
"message": "register-error-phone-min"
},
"allowedSymbols": {
"value": "^[0-9\\+]+$",
"message": "register-error-phone-not-allowed-symbols"
}
},
"inn": {
"maxSize": {
"value": 10,
"message": "register-error-inn-max"
},
"minSize": {
"value": 10,
"message": "register-error-inn-min"
},
"allowedSymbols": {
"value": "^[0-9]+$",
"message": "register-error-inn-not-allowed-symbols"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
'use client';
import Providers from './getQueryServer';
export function WatchDocProviders({ children }: { children: React.ReactNode }) {
return (
<Providers>
{children}
</Providers>
);
}

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