Compare commits

...
646 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 bf9926f004 update confirm email code window 2026-01-26 13:04:13 +07:00
smanylov bd869798be edit small screen layout 2026-01-24 13:19:48 +07:00
smanylov 4166cb1d80 refactor confirm window 2026-01-23 18:06:08 +07:00
smanylov 396d3888db add confirm mail modal window 2026-01-23 17:33:48 +07:00
smanylov f8ab107724 edit layout for small resolution screens 2026-01-23 15:43:05 +07:00
smanylov 38ce69047d 1 2026-01-23 12:19:41 +07:00
smanylov 8f8302f76f change violations statisctic greed 2026-01-23 12:19:37 +07:00
smanylov 9366f531d1 add translate for ProtectionStatistic 2026-01-22 11:31:39 +07:00
smanylov f5302f2b4b increment version 2026-01-21 15:12:20 +07:00
smanylov fc7efbfa07 edit styles 2026-01-21 15:05:02 +07:00
smanylov fb661df8a1 edit tanstak-table 2026-01-20 20:09:46 +07:00
smanylov 3589487e39 add more adaptive styles 2026-01-20 18:52:21 +07:00
smanylov 7d7213b457 update token life extension 2026-01-20 16:53:11 +07:00
smanylov 2b7ba7255a remove admin panel 2026-01-19 14:37:09 +07:00
smanylov 7a03b0bc2e edit styles 2026-01-18 16:27:09 +07:00
smanylov a9c85570b7 add hover 2026-01-15 14:20:37 +07:00
smanylov 4697eb6fda add violations layout 2026-01-15 14:18:08 +07:00
smanylov b763e438b8 edit styles 2026-01-15 13:11:53 +07:00
smanylov 7213e40b96 add my content layout 2026-01-15 12:27:39 +07:00
smanylov 719a3e3582 update allowed input symbols 2026-01-14 19:24:01 +07:00
smanylov 049f404b20 edit register 2026-01-14 18:17:02 +07:00
smanylov 18a48236b5 add confirm email modal window 2026-01-14 18:15:52 +07:00
smanylov 3d38729004 increase version 2026-01-14 15:01:48 +07:00
smanylov ede6ad179c add new marking-content layout 2026-01-14 15:01:30 +07:00
smanylov f790a95e71 edit yandex button behavior 2026-01-14 13:26:18 +07:00
smanylov e53a85caa2 reafactor 2026-01-14 12:48:33 +07:00
smanylov e4d6df71a1 add endpoins for dashboard-files-info 2026-01-14 12:23:07 +07:00
smanylov afb6bf1760 change dash-board layout 2026-01-13 15:29:34 +07:00
smanylov 2e928fb91a fix pluralize 2026-01-12 17:31:42 +07:00
smanylov d0918b8c0b add refferal page layout 2026-01-12 17:12:50 +07:00
smanylov 77ca4775b1 update token refresh function 2026-01-12 15:41:16 +07:00
smanylov 9ac254aa40 add update session 2026-01-12 14:16:34 +07:00
smanylov 5cc822a535 remove extra token update 2026-01-12 14:08:10 +07:00
smanylov 75b505464c add ActivityTracker 2026-01-12 14:07:06 +07:00
smanylov 4c239f12f9 add referrals page 2026-01-12 13:53:12 +07:00
smanylov cb5a005041 add function tokenLifeExtension 2026-01-12 13:39:35 +07:00
smanylov 6b9279f802 add new validation message 2026-01-10 15:54:27 +07:00
smanylov 13ab4bfcfe updtate allowedSymbolds for password 2026-01-10 15:51:07 +07:00
smanylov 09e9d2df40 update version 2026-01-10 15:45:17 +07:00
smanylov 73a349e01d update validation 2026-01-10 15:44:37 +07:00
smanylov 31af7036a3 update validation 2026-01-10 15:12:51 +07:00
smanylov b5754b75a8 edit validation rules 2026-01-10 13:59:04 +07:00
smanylov e2174df41b add validation rules 2026-01-10 13:54:53 +07:00
smanylov b674be212b edit validation keys 2026-01-10 13:53:59 +07:00
smanylov 54cb433278 fix build 2026-01-10 12:25:42 +07:00
smanylov 24e96d2f8a add string pixels 2026-01-09 22:50:32 +07:00
smanylov 2350ac8a5d edit validation, add new validation message 2026-01-09 19:46:59 +07:00
smanylov 9f880f71d1 fix styles 2026-01-09 16:12:55 +07:00
smanylov d3a2bd1497 add yandex button 2026-01-09 15:46:14 +07:00
smanylov a8ae39751e update tanstak-table fetch settings 2026-01-09 12:35:48 +07:00
smanylov 5541d76574 add dev inputs 2026-01-09 11:41:26 +07:00
smanylov da47a7a974 update text layout 2026-01-09 11:08:31 +07:00
smanylov 46f11e081c turn off vk button 2026-01-08 17:56:29 +07:00
smanylov d81ed71e1d add declension of words 2026-01-08 17:21:09 +07:00
smanylov 3c0fea3eb0 fix language switch for http protocol 2026-01-08 15:53:12 +07:00
smanylov 0e35c5e601 update download icon 2026-01-08 11:51:07 +07:00
smanylov b2952c3594 fix diagram 2026-01-08 11:45:30 +07:00
smanylov 9547ead7aa status translate, fix text hide 2026-01-08 11:16:59 +07:00
smanylov 46bd4f7855 update padding 2026-01-07 20:29:27 +07:00
smanylov 71cc51b3cf add docker info 2026-01-07 19:57:23 +07:00
smanylov a590514e05 update ports 2026-01-07 19:47:15 +07:00
smanylov f6b04d6e33 update styles 2026-01-07 19:38:21 +07:00
smanylov d3bbc593a6 update vk auth 2026-01-07 19:14:37 +07:00
smanylov fe5fd79d85 change codeVerifier on codeChallenge 2026-01-07 18:13:53 +07:00
smanylov be98bcba3f fix translate 2026-01-07 14:44:31 +07:00
smanylov e19b559a19 update version 2026-01-07 13:06:14 +07:00
smanylov aa0f9398eb update user files info 2026-01-07 13:03:50 +07:00
smanylov 5597fe42a2 update readme 2026-01-06 19:17:24 +07:00
smanylov bde030d5b5 update config 2026-01-06 19:12:12 +07:00
smanylov 052c66fd7c update vkAuth endpoint 2026-01-06 19:05:07 +07:00
smanylov 0044d3c703 vk auth 2026-01-06 18:52:26 +07:00
smanylov 9406004ee4 admin-panel layout 2026-01-06 16:00:07 +07:00
smanylov 94261cde7a add amin-panel layout 2026-01-05 16:53:30 +07:00
smanylov 9d4604ac4e update styles 2026-01-05 16:15:07 +07:00
smanylov c438ae3fb9 redesign 2026-01-05 15:07:10 +07:00
smanylov 9f41386b65 fix const name 2026-01-04 17:44:42 +07:00
smanylov 630a108968 add fileNameCut 2026-01-04 17:43:58 +07:00
smanylov 3e9c87ec46 my-content-page and fix adaptive styles 2026-01-04 17:08:28 +07:00
smanylov d0de025d96 split error messages on new lines 2026-01-04 14:42:59 +07:00
smanylov eff7fbc171 fix showPasswordButton for login form 2026-01-04 13:34:01 +07:00
smanylov 6c46a1a655 tanstak table corrected adaptive layout 2026-01-04 13:27:59 +07:00
smanylov edcac25b02 add translate 2026-01-02 13:00:37 +07:00
smanylov 3537bd1f3a add file download 2026-01-02 12:52:01 +07:00
smanylov a085685d33 refactor styles 2025-12-30 14:33:39 +07:00
smanylov f16e527a32 rewrite table styles from tailwind into scss 2025-12-30 13:35:20 +07:00
smanylov 1032c5420b add react-input-mask 2025-12-29 18:47:50 +07:00
smanylov bd0169bd61 update files date between modules 2025-12-29 16:08:38 +07:00
smanylov 198a01d026 update files-state, add toast, add zustand 2025-12-29 14:00:02 +07:00
smanylov fffd38a812 fix build 2025-12-29 11:21:54 +07:00
smanylov 0c1aad1932 add nunito sans font 2025-12-28 13:09:28 +07:00
smanylov 6d9313cbb8 transfer stacked bart chart from overview block 2025-12-28 11:34:22 +07:00
smanylov bd2ec273a9 refactor 2025-12-27 13:47:51 +07:00
smanylov f8bbf56f47 refactor 2025-12-27 12:10:13 +07:00
smanylov aaa95091d2 refactor 2025-12-27 11:54:54 +07:00
smanylov ab6e385984 update cancel upload function 2025-12-26 22:16:22 +07:00
smanylov a3bcccdf28 update state 2025-12-26 20:17:41 +07:00
smanylov aa67d722c3 update limit 2025-12-26 19:21:39 +07:00
smanylov 2c54d3e375 add logs 2025-12-26 18:51:05 +07:00
smanylov 2209bfa48c try fix upload for server 2025-12-26 17:56:57 +07:00
smanylov 164e2568b9 tanstake-table add remove, update function 2025-12-26 16:10:42 +07:00
smanylov d52dfb7ab8 add life data for tanstak table 2025-12-26 14:03:57 +07:00
smanylov eeef5ac9f9 rename file-upload 2025-12-26 11:02:35 +07:00
smanylov 49088e5e5f edit errors message 2025-12-25 17:24:59 +07:00
smanylov e82d96a44c edit get files extensions query 2025-12-25 15:31:24 +07:00
smanylov e0420fa6d3 add navagation handler while file upload 2025-12-25 13:19:32 +07:00
smanylov 87b8d68f41 file upload update 2025-12-24 21:36:59 +07:00
smanylov fe77b4ad36 remove console.logs 2025-12-24 15:42:29 +07:00
smanylov 8058be42c1 file upload develop 2025-12-24 15:41:06 +07:00
smanylov c38b7ee558 add fileUpload functions 2025-12-23 18:56:48 +07:00
smanylov 3376fee99a remove emoji check 2025-12-23 12:25:08 +07:00
smanylov 79ca9ac540 update remove function for phone input 2025-12-22 17:19:59 +07:00
smanylov 2bf6fcac2f edit register form layout 2025-12-22 14:30:25 +07:00
smanylov e26a8c4742 add sync form validate 2025-12-22 13:02:39 +07:00
smanylov 02ebf8aa07 make label part of checkbox 2025-12-19 16:20:57 +07:00
smanylov a7467d486d update validation messages 2025-12-19 16:11:58 +07:00
smanylov c26c2ebc7f add fixes 2025-12-19 13:05:50 +07:00
smanylov 35659a2878 fix padding for tanstak-table 2025-12-18 23:22:31 +07:00
smanylov 7540dd57de develop 2025-12-18 17:21:18 +07:00
smanylov a538fe1684 fix translates and visual bugs 2025-12-18 13:36:56 +07:00
smanylov 9b8cb3b583 add translate and make refactor 2025-12-17 16:41:07 +07:00
smanylov fd59fb3e5e undate full-name validation for register 2025-12-17 15:43:13 +07:00
smanylov 61cd288359 update endpoints 2025-12-17 14:30:54 +07:00
frontdev 58192b2959 Merge pull request 'NCFRONT-17 change endpoints' (#3) from NCFRONT-17 into main
Reviewed-on: #3
2025-12-17 14:29:11 +08:00
smanylov 6ad888d399 Merge branch 'main' into NCFRONT-17 2025-12-17 13:03:30 +07:00
smanylov 8748a4b3ea remove TestSection from layout 2025-12-17 12:52:55 +07:00
smanylov 70ebf06663 fix stacked bar shar diagram 2025-12-17 12:50:01 +07:00
smanylov c42d5eae27 fix page highlight 2025-12-17 09:18:18 +07:00
smanylov 6e4dceda0c set start number 7 for phone mask 2025-12-16 21:04:55 +07:00
smanylov 3b32b38586 border color for stacked bar 2025-12-16 20:33:06 +07:00
smanylov 169d0d33a0 update translate and styles 2025-12-16 20:26:17 +07:00
smanylov c9784bf18f change marking page layout 2025-12-16 15:03:32 +07:00
smanylov e94b0bf041 change tanstak table text color 2025-12-16 11:34:04 +07:00
smanylov 668eed9c64 cyrillic email regex 2025-12-16 11:24:05 +07:00
smanylov 3d57486030 add margin for diagram 2025-12-16 10:17:26 +07:00
smanylov ee1575cac7 reafactor + show password button for confirm password 2025-12-16 10:11:39 +07:00
smanylov 79df200ccb edit styles, add vk-button and initial setup for vk auth 2025-12-15 17:32:30 +07:00
smanylov b0d1b954dd add show-password-button for login form 2025-12-15 12:42:50 +07:00
smanylov 7fb6f7418f add show password button 2025-12-15 12:22:42 +07:00
smanylov 72bd21c648 change stats-table style 2025-12-15 11:24:07 +07:00
smanylov 3334059df7 remove border radius from navigation 2025-12-15 10:34:20 +07:00
smanylov 7aacea387e add tanstak table 2025-12-12 20:28:59 +07:00
smanylov 8541916e4c NCFRONT-17 change endpoints 2025-12-12 14:29:00 +07:00
frontdev 868293ad51 Merge pull request 'dev-1' (#2) from dev-1 into main
Reviewed-on: #2
2025-12-12 15:23:52 +08:00
smanylov 54c63a6a40 ad tanstak test-table 2025-12-12 14:22:00 +07:00
smanylov fcf9114319 add version page 2025-12-12 13:31:17 +07:00
smanylov d17337d817 add privacy page 2025-12-12 13:09:54 +07:00
smanylov 0ce49ca364 return old endpoints 2025-12-12 12:06:59 +07:00
smanylov 6cbf5d421a add server logs 2025-12-12 11:52:57 +07:00
smanylov 140fa78062 add privacy page 2025-12-12 11:43:28 +07:00
smanylov b5b66695df add translate 2025-12-12 11:00:23 +07:00
smanylov b63828befb adit endpoints 2025-12-11 20:38:06 +07:00
smanylov 67c6d07e4d change dashboard layout 2025-12-11 18:28:35 +07:00
271 changed files with 37971 additions and 3645 deletions
+2
View File
@@ -0,0 +1,2 @@
SESSION_SECRET=odW+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo=
DEV_URL=http://localhost
+1 -1
View File
@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* # .env*
# vercel # vercel
.vercel .vercel
+1 -1
View File
@@ -8,7 +8,7 @@ WORKDIR /no-copy-frontend
COPY . . COPY . .
COPY --from=dependencies /no-copy-frontend/node_modules ./node_modules 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 FROM node:22.20.0 AS runner
WORKDIR /no-copy-frontend WORKDIR /no-copy-frontend
+16 -1
View File
@@ -6,6 +6,10 @@ npm run dev
Open [http://localhost:2999](http://localhost:2999) Open [http://localhost:2999](http://localhost:2999)
## Смена портов
в .env меняешь "DEV_URL" это урл до бека
в package.json "-p 2999" - это порт на котором стартанет приложение.
## Docker ## Docker
docker-compose down -v --rmi all docker-compose down -v --rmi all
@@ -26,4 +30,15 @@ docker network ls
docker network connect no-copy_app-network no-copy-frontend docker network connect no-copy_app-network no-copy-frontend
docker network inspect no-copy_app-network docker network inspect no-copy_app-network
docker volume ls - посмотреть все базы данных
docker volume prune - удалить неиспользуемые базы данных
Юкаса
эндпоинт для бека.
{URL}/api/payments/webhook/yookassa
https://dev-workspace.not-copy.com/api/payments/webhook/yookassa
https://mutely-profound-sandfish.cloudpub.ru/api/payments/webhook/yookassa
+9 -1
View File
@@ -1,8 +1,16 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin'; import createNextIntlPlugin from 'next-intl/plugin';
const { version } = require('./package.json');
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* output: 'export' */ experimental: {
serverActions: {
bodySizeLimit: '10mb',
},
},
env: {
APP_VERSION: version,
},
}; };
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
+1689 -696
View File
File diff suppressed because it is too large Load Diff
+17 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.1.0", "version": "0.122.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
@@ -9,17 +9,31 @@
"lint": "eslint" "lint": "eslint"
}, },
"dependencies": { "dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
"@react-input/mask": "^2.0.4",
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-query": "^5.90.11", "@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", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8", "next-intl": "^4.5.8",
"pdfjs-dist": "^5.7.284",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-slick": "^0.31.0",
"recharts": "^3.5.1",
"sonner": "^2.0.7",
"swiper": "^12.0.3",
"tailwindcss": "^4.1.17", "tailwindcss": "^4.1.17",
"zod": "^4.1.13" "use-debounce": "^10.1.0",
"validator": "^13.15.26",
"zod": "^4.1.13",
"zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
@@ -28,6 +42,7 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.3", "eslint-config-next": "16.0.3",
"sass": "^1.94.2", "sass": "^1.94.2",
"types-yoomoneycheckoutwidget": "^0.0.2",
"typescript": "^5" "typescript": "^5"
} }
} }
+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

+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 ForgotPasswordForm from '@/app/ui/forms/forgot-password-form';
export default function Page() {
const t = useTranslations('Login-register-form');
return (
<div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}>
<LogoIcon />
<h1>NO COPY</h1>
<p>
{t('recover-password')}
</p>
</div>
<ForgotPasswordForm/>
<div className={`${styles['register-link']}`}>
<p>{t('already-have-an-account')}? <Link href="login">{t('login')}</Link></p>
</div>
<div className={`${styles['register-link']}`}>
<p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
</p>
</div>
</div>
</div>
)
}
+7 -3
View File
@@ -1,8 +1,11 @@
import Providers from '@/app/providers/getQueryServer'
import type { Metadata } from "next"; import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl'; import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing'; import { routing } from '@/i18n/routing';
import { ToastProvider } from '@/app/providers/ToastProvider';
import Providers from '@/app/providers/getQueryServer';
import { Suspense } from 'react';
import "../styles/globals.css"; import "../styles/globals.css";
import "../styles/global-styles.scss"; import "../styles/global-styles.scss";
@@ -28,11 +31,12 @@ export default async function RootLayout({
} }
return ( return (
<html lang={locale}> <html>
<body> <body>
<NextIntlClientProvider> <NextIntlClientProvider locale={locale}>
<Providers> <Providers>
{children} {children}
<ToastProvider />
</Providers> </Providers>
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
+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>
)
}
+64 -41
View File
@@ -1,10 +1,15 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import styles from '@/app/styles/login.module.scss' import styles from '@/app/styles/module/login.module.scss'
import LogoIcon from '@/app/ui/logo-icon'; import LogoIcon from '@/app/ui/logo-icon';
import Link from 'next/link'; import Link from 'next/link';
import LoginForm from '@/app/ui/login-form'; import LoginForm from '@/app/ui/forms/login-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher'; import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import { useTranslations } from 'next-intl'; 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 = { export const metadata: Metadata = {
title: 'Login', title: 'Login',
@@ -13,51 +18,69 @@ export const metadata: Metadata = {
export default function Page() { export default function Page() {
const t = useTranslations('Login-register-form'); const t = useTranslations('Login-register-form');
async function HealthData() {
const data = await getHealt();
if (!data) {
return <div>Сервер временно не работает</div>
} else {
return null
}
}
return ( return (
<div className={`${styles['login-container-wrapper']}`}> <Suspense fallback={<div>Loading...</div>}>
<div className={`${styles['login-container-language']}`}> <HealthData />
<div className={`${styles['login-container-wrapper']}`}>
{/* <div className={`${styles['login-container-language']}`}>
<LanguageSwitcher /> <LanguageSwitcher />
</div> </div> */}
<div className={`${styles['login-container']}`}> <div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}> <div className={`${styles['logo']}`}>
<LogoIcon /> <LogoIcon />
<h1>NO COPY</h1> <h1>NO COPY</h1>
<p> <p>
{t('copyright-protection')} {t('copyright-protection')}
</p> </p>
</div>
<LoginForm />
<div className="divider">
<span>
{t('or-log-in-via')}
</span>
</div>
<div className={`${styles['register-link']}`}>
<p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
</p>
</div>
<div className={`${styles['features']}`}>
<div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{t('protecting-watermark')}
</div> </div>
<div className={`${styles['feature']}`}> <LoginForm />
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor"> <div className="divider">
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path> <span>
</svg> {t('or-log-in-via')}
{t('monitoring-violations')} </span>
</div> </div>
<div className={`${styles['feature']}`}> <div className="mb-4">
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor"> <VKLogin />
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path> </div>
</svg> <div>
{t('safe-storage')} <YandexLoginButton />
</div>
<div className={`${styles['register-link']}`}>
<p>{t('no-account')}?
<Link href="register"> {t('register')}</Link>
</p>
</div>
<div className={`${styles['features']}`}>
<div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{t('protecting-watermark')}
</div>
<div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
{t('monitoring-violations')}
</div>
<div className={`${styles['feature']}`}>
<svg className={`${styles['feature-icon']}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1L3 5v6c0 5.55 3.84 9.74 9 11 5.16-1.26 9-5.45 9-11V5l-9-4z"></path>
</svg>
{t('safe-storage')}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </Suspense>
); );
} }
+2 -7
View File
@@ -1,4 +1,3 @@
import styles from "@/app/styles/page.module.scss";
import { Metadata } from 'next'; import { Metadata } from 'next';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
@@ -11,12 +10,8 @@ export default function Home() {
const t = useTranslations('HomePage'); const t = useTranslations('HomePage');
return ( return (
<div className={styles.page}> <div>
<main className={styles.main}> <main>
<div className={styles.intro}>
Home page
{t('title')}
</div>
<Link key={'dashboard'} <Link key={'dashboard'}
href={'/pages/dashboard'} href={'/pages/dashboard'}
>dash board</Link> >dash board</Link>
+37 -19
View File
@@ -1,12 +1,16 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { Suspense } from 'react'; import { Suspense } from 'react';
import ProtectionOverview from '@/app/ui/dashboard/protection-overview';
import StatsGrid from '@/app/ui/dashboard/stats-grid';
import StorageBreakdown from '@/app/ui/dashboard/storage-breakdown';
import ChartContainer from '@/app/ui/dashboard/chart-container';
import ViolationsTable from '@/app/ui/dashboard/violations-table';
import ViolationsTypeTable from '@/app/ui/dashboard/violations-type-table';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import FilesTable from '@/app/ui/dashboard/files-table';
import PageTitle from '@/app/ui/page-title';
import DashboardUserStats from '@/app/ui/dashboard/new/dashboard-user-stats';
import DashboardUserFiles from '@/app/ui/dashboard/new/dashboard-user-files';
import DashboardPlatformsList from '@/app/ui/dashboard/new/dashboard-platforms-list';
import DashboardFilesInfo from '@/app/ui/dashboard/new/dashboard-files-info';
import DahboardLimitsSection from '@/app/ui/dashboard/new/dashborad-limits-section';
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check';
import DashboardUserViolations from '@/app/ui/dashboard/new/dashboard-user-violations';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Dashboard', title: 'Dashboard',
@@ -16,18 +20,32 @@ export default function Page() {
const t = useTranslations('Global'); const t = useTranslations('Global');
return ( return (
<div> <Suspense fallback={<>...</>}>
<Suspense fallback={<>...</>}> <PageTitle title="statistics" />
<h1 className="page-title"> <DashboardUserStats />
{t("statistics")} <div className="dashboard-main-grid">
</h1> {/* <DashboardUserFiles />
<ProtectionOverview /> <DashboardUserViolations /> */}
<StatsGrid />
<StorageBreakdown /> <div className="left-column">
<ChartContainer /> <DashboardUserFiles />
<ViolationsTable /> </div>
<ViolationsTypeTable /> <div className="right-column">
</Suspense> <DashboardUserViolations />
</div> </div>
</div>
<DashboardFilesInfo />
<div className="dashboard-main-grid">
{/* <DahboardGeographySection /> */}
</div>
<div
className="mb-4"
>
{/* <DahboardLimitsSection /> */}
</div>
<DashboardLastCheck />
</Suspense>
); );
} }
@@ -0,0 +1,28 @@
import { Metadata } from 'next';
import { Suspense } from 'react';
import ProtectionOverview from '@/app/ui/dashboard/protection-overview';
import StatsGrid from '@/app/ui/dashboard/stats-grid';
import { useTranslations } from 'next-intl';
import FilesTable from '@/app/ui/dashboard/files-table';
import PageTitle from '@/app/ui/page-title';
export const metadata: Metadata = {
title: 'Dashboard',
};
export default function Page() {
const t = useTranslations('Global');
return (
<>
<Suspense fallback={<>...</>}>
<PageTitle title="statistics" />
<div className="split-blocks">
<ProtectionOverview />
<StatsGrid />
</div>
<FilesTable fileType={'all'} />
</Suspense>
</>
);
}
+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 />
)
}
}
+7 -11
View File
@@ -1,31 +1,27 @@
import NavLinks from '@/app/ui/nav-links' import NavLinks from '@/app/ui/navigation/nav-links'
import styles from '@/app/styles/page.module.scss'
import HeaderPanel from '@/app/ui/header/headerPanel'; import HeaderPanel from '@/app/ui/header/headerPanel';
import { getQueryClient } from '@/app/providers/getQueryClient'; import { getQueryClient } from '@/app/providers/getQueryClient';
import { HydrationBoundary, dehydrate } from '@tanstack/react-query'; import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { getUserData } from '@/app/actions/action'; import { prefetchLayoutQueries } from '@/app/lib/prefetch-queries';
import ActivityTracker from '@/app/components/ActivityTracker';
export default async function Layout({ children }: { children: React.ReactNode }) { export default async function Layout({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient(); const queryClient = getQueryClient();
await Promise.all([ await prefetchLayoutQueries(queryClient);
queryClient.prefetchQuery({
queryKey: ['userData'],
queryFn: () => getUserData()
})
])
return ( return (
<div className="flex"> <div className="flex">
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>
<NavLinks /> <NavLinks />
<div className={`${styles['main-containter']}`}> <div className="main-containter-wrapper">
<HeaderPanel /> <HeaderPanel />
<main> <main className="main-containter">
{children} {children}
</main> </main>
</div> </div>
</HydrationBoundary> </HydrationBoundary>
<ActivityTracker/>
</div> </div>
); );
} }
+57 -10
View File
@@ -1,17 +1,64 @@
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
import ProtectionSummary from '@/app/ui/marking-page/protection-summary'; import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import TestSection from '@/app/ui/marking-page/test-section'; import FilesTable from '@/app/ui/dashboard/files-table';
import { IconAudioFile } from '@/app/ui/icons/icons'; import PageTitle from '@/app/ui/page-title';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import { StackedBarChart } from '@/app/components/StackedBarChart';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
const data = [
{
name: 'File A',
checks: 4,
violations: 2
},
{
name: 'File B',
checks: 8,
violations: 4
},
{
name: 'File C',
checks: 2,
violations: 1
},
{
name: 'File D',
checks: 7,
violations: 1
},
{
name: 'File E',
checks: 5,
violations: 4
},
{
name: 'File F',
checks: 5,
violations: 3
}
];
export default async function Page() {
const FILE_TYPE = "audio";
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
export default function Page() {
return ( return (
<div> <div>
<div className="page-title"> <PageTitleColorFrame
<h1 >Защита аудио</h1> title="audio-protection"
</div> description="multi-layered-protection-for-your-audios"
<ProtectionSummary /> />
<TestSection /> <ProtectionStatistic fileType={FILE_TYPE} />
<ProtectedFilesTable /> {/* <div className="split-blocks">
<ProtectionSummary fileType={FILE_TYPE} />
<div className="protection-overview">
<StackedBarChart data={data} />
</div>
</div> */}
<UploadSectionFile fileType={FILE_TYPE} allowedExtensions={file_extension} maxFileSize={max_file_size} />
<FilesTable fileType={FILE_TYPE} />
</div> </div>
) )
} }
@@ -0,0 +1,26 @@
import FilesTable from '@/app/ui/dashboard/files-table';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
export default async function Page() {
const FILE_TYPE = "document";
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
return (
<div>
<PageTitleColorFrame
title="document-protection"
description="multi-layered-protection-for-your-document"
/>
<ProtectionStatistic fileType={FILE_TYPE} />
<UploadSectionFile
fileType={FILE_TYPE}
allowedExtensions={file_extension}
maxFileSize={max_file_size}
/>
<FilesTable fileType={FILE_TYPE} />
</div>
);
}
@@ -0,0 +1,57 @@
import FilesTable from '@/app/ui/dashboard/files-table';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
const data = [
{
name: 'File A',
checks: 4,
violations: 2
},
{
name: 'File B',
checks: 8,
violations: 4
},
{
name: 'File C',
checks: 2,
violations: 1
},
{
name: 'File D',
checks: 7,
violations: 1
},
{
name: 'File E',
checks: 5,
violations: 4
},
];
export default async function Page() {
const FILE_TYPE = "image";
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
return (
<div>
<PageTitleColorFrame
title="image-protection"
description="multi-layered-protection-for-your-images"
/>
<ProtectionStatistic fileType={FILE_TYPE}/>
{/* <div className="split-blocks">
<ProtectionSummary fileType={FILE_TYPE} />
<div className="protection-overview">
<StackedBarChart data={data} />
</div>
</div> */}
<UploadSectionFile fileType={FILE_TYPE} allowedExtensions={file_extension} maxFileSize={max_file_size} />
<FilesTable fileType={FILE_TYPE} />
</div>
)
}
@@ -1,19 +0,0 @@
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import TestSection from '@/app/ui/marking-page/test-section';
import UploadSectionImage from '@/app/ui/marking-page/upload-section-image';
import { IconImageFile } from '@/app/ui/icons/icons';
export default function Page() {
return (
<div>
<div className="page-title">
<h1 >Защита изображений</h1>
</div>
<ProtectionSummary />
<UploadSectionImage />
<TestSection />
<ProtectedFilesTable />
</div>
)
}
+56 -11
View File
@@ -1,19 +1,64 @@
import ProtectedFilesTable from '@/app/ui/marking-page/protected-files-table';
import ProtectionSummary from '@/app/ui/marking-page/protection-summary'; import ProtectionSummary from '@/app/ui/marking-page/protection-summary';
import TestSection from '@/app/ui/marking-page/test-section'; import FilesTable from '@/app/ui/dashboard/files-table';
import UploadSectionVideo from '@/app/ui/marking-page/upload-section-video'; import PageTitle from '@/app/ui/page-title';
import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import { StackedBarChart } from '@/app/components/StackedBarChart';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
const data = [
{
name: 'File A',
checks: 4,
violations: 2
},
{
name: 'File B',
checks: 8,
violations: 4
},
{
name: 'File C',
checks: 2,
violations: 1
},
{
name: 'File D',
checks: 7,
violations: 1
},
{
name: 'File E',
checks: 5,
violations: 4
},
{
name: 'File F',
checks: 5,
violations: 3
}
];
export default async function Page() {
const FILE_TYPE = "video";
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
export default function Page() {
return ( return (
<div> <div>
<div className="page-title"> <PageTitleColorFrame
<h1 >Защита видео</h1> title="video-protection"
</div> description="multi-layered-protection-for-your-videos"
<ProtectionSummary /> />
<UploadSectionVideo /> <ProtectionStatistic fileType={FILE_TYPE} />
<TestSection /> {/* <div className="split-blocks">
<ProtectedFilesTable /> <ProtectionSummary fileType={FILE_TYPE} />
<div className="protection-overview">
<StackedBarChart data={data} />
</div>
</div> */}
<UploadSectionFile fileType={FILE_TYPE} allowedExtensions={file_extension} maxFileSize={max_file_size} />
<FilesTable fileType={FILE_TYPE} />
</div> </div>
) )
} }
@@ -0,0 +1,19 @@
import MyContentInfoBlock from '@/app/ui/my-content/my-content-info-block';
import FilesTable from '@/app/ui/dashboard/files-table';
import MyContentPieChart from '@/app/ui/my-content/my-content-pie-chart';
import MyContentStatsOverview from '@/app/ui/my-content/new/my-content-stats-overview';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
export default function Page() {
return (
<>
<PageTitleColorFrame title="my-content" description="my-content-description"/>
<MyContentStatsOverview />
{/* <div className="split-blocks">
<MyContentInfoBlock />
<MyContentPieChart />
</div> */}
<FilesTable fileType={'all'} />
</>
)
}
@@ -0,0 +1,12 @@
import PageTitle from '@/app/ui/page-title';
import { NotificationsList } from '@/app/ui/notifications/notifications-list';
export default function Page() {
return (
<div>
<PageTitle title='notifications' />
<NotificationsList />
</div>
)
}
+2
View File
@@ -3,6 +3,7 @@ import UserUsage from '@/app/ui/payment/user-usage';
import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats'; import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats';
import PaymentTabs from '@/app/ui/payment/payment-tabs'; import PaymentTabs from '@/app/ui/payment/payment-tabs';
import SecurePayments from '@/app/ui/payment/secure-payments'; import SecurePayments from '@/app/ui/payment/secure-payments';
import PaymentUserCards from '@/app/ui/payment/payment-user-cards';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'payment', title: 'payment',
@@ -22,6 +23,7 @@ export default function Page() {
<UserUsage /> <UserUsage />
<TokensSpendStats /> <TokensSpendStats />
<PaymentTabs /> <PaymentTabs />
<PaymentUserCards />
<SecurePayments /> <SecurePayments />
</div> </div>
</main> </main>
+25
View File
@@ -0,0 +1,25 @@
import { useTranslations } from 'next-intl';
import { Suspense } from 'react';
import ReferralStats from '@/app/ui/referral-page/referral-stats';
import ReferralLinkSection from '@/app/ui/referral-page/referral-link-section';
import CommissionLevels from '@/app/ui/referral-page/commission-levels';
import InvitationsTable from '@/app/ui/referral-page/table/invitations-table';
import IncomeAndPayments from '@/app/ui/referral-page/Income-and-payments';
import SettingReferralProgram from '@/app/ui/referral-page/setting-referral-program';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
export default function Page() {
const t = useTranslations('Global');
return (
<Suspense fallback={<>...</>}>
<PageTitleColorFrame title="refferal-program" description="referral-program-description" />
<CommissionLevels />
{/* <ReferralStats /> */}
<ReferralLinkSection />
<InvitationsTable />
<IncomeAndPayments />
<SettingReferralProgram />
</Suspense>
)
}
+9 -6
View File
@@ -1,18 +1,21 @@
import StatsGrid from '@/app/ui/dashboard/stats-grid'; import ViolationsTable from '@/app/ui/violations/violations-table';
import ViolationsTable from '@/app/ui/dashboard/violations-table';
import FinalSummary from '@/app/ui/reports/final-summary'; import FinalSummary from '@/app/ui/reports/final-summary';
import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month'; import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month';
import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography'; import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
import ActivityByContentType from '@/app/ui/reports/activity-by-content-type'; import ActivityByContentType from '@/app/ui/reports/activity-by-content-type';
import PageTitle from '@/app/ui/page-title';
import ReportsInfo from '@/app/ui/reports/reports-info';
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
export default function Page() { export default function Page() {
return ( return (
<div> <div>
<h1 className="page-title">Отчёты и аналитика</h1> <PageTitle title="reports-and-analytics" />
<StatsGrid /> <ReportsInfo />
<ActivityByContentType /> <ActivityByContentType />
<div className="analytics-grid"> <div className="analytics-grid">
<AnalyticsCardMonth /> {/* <AnalyticsCardMonth /> */}
<AnalyticsCardDistribution />
<AnalyticsCardGeography /> <AnalyticsCardGeography />
</div> </div>
<ViolationsTable /> <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>
</>
)
}
+16 -7
View File
@@ -5,20 +5,29 @@ import SubscriptionSettings from '@/app/ui/settings/subscription-settings';
import APISettings from '@/app/ui/settings/api-settings'; import APISettings from '@/app/ui/settings/api-settings';
import LastActivitySettings from '@/app/ui/settings/last-activity-settings'; import LastActivitySettings from '@/app/ui/settings/last-activity-settings';
import DangerZone from '@/app/ui/settings/danger-zone'; 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() { export default function Page() {
const t = useTranslations('Global');
return ( return (
<> <>
<h1 className="page-title"> Настройки аккаунта</h1> <PageTitle title="account-settings" />
<div className="settings-grid"> <div className="settings-grid">
<PersonalDataSettings /> <PersonalDataSettings />
<NotificationsMonitoringSettings/> {/* <NotificationsMonitoringSettings /> */}
<SafetySetting/> <SafetySetting />
<SubscriptionSettings/> <SubscriptionSettings />
<APISettings/> {/* <APISettings /> */}
<LastActivitySettings/> {/* LastActivitySettings возможно нужно будет вернуть */}
{/* <LastActivitySettings /> */}
<CompanyUsersSettingPanel />
<SettingUserVerification />
</div> </div>
<DangerZone/> <DangerZone />
</> </>
) )
} }
+19
View File
@@ -0,0 +1,19 @@
'use server'
import { TrackingStatistic } from '@/app/ui/tracking-page/tracking-statistic';
import FilesTable from '@/app/ui/dashboard/files-table';
import { TrackingHistoryView } from '@/app/ui/tracking-page/tracking-history-view';
export default async function Page() {
const FILE_TYPE = "document";
return (
<div
className="tracking-page"
>
<TrackingStatistic />
<FilesTable fileType={FILE_TYPE} showFileLink={true} />
<TrackingHistoryView />
</div>
)
}
@@ -0,0 +1,36 @@
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ViolationsMyComplaint from '@/app/ui/violations/violations-my-complaint';
import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section';
import ViolationsTable from '@/app/ui/violations/violations-table';
import ViolationsStatistic from '@/app/ui/violations/violations-statistic';
import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
import ViolationsMyClaims from '@/app/ui/violations/violations-my-claims';
import ViolationsLastCombinedCases from '@/app/ui/violations/violations-last-combined-cases';
import { ViolationsMyComplaintsTable } from '@/app/ui/violations/tables/violations-my-complaints-table';
import { ViolationsMyClaimsTable } from '@/app/ui/violations/tables/violations-my-claims-table';
export default function Page() {
return (
<>
<PageTitleColorFrame
title="monitoring-copyright-infringements"
description="monitoring-copyright-infringements-description"
/>
<ViolationsStatistic />
<ViolationsCheckAllSection />
<ViolationsTable />
{/* <ViolationsLastCombinedCases /> */}
{/* <ViolationsMyComplaint />
<ViolationsMyClaims /> */}
<div className="analytics-grid">
<AnalyticsCardDistribution />
<AnalyticsCardGeography />
</div>
<div className="analytics-grid">
<ViolationsMyClaimsTable />
<ViolationsMyComplaintsTable />
</div>
</>
)
}
+189
View File
@@ -0,0 +1,189 @@
export default function Page() {
return (
<main className="main-content">
<div className="container">
<div className="hero-container">
<h1 className="hero-title">Пользовательское соглашение</h1>
<p className="hero-subtitle">
Условия использования сервиса No Copy. Настоящее соглашение определяет правила и условия использования нашей платформы защиты интеллектуальной собственности.
</p>
</div>
<div className="document-content-container">
<div className="document-content">
<div className="document-section">
<h2>1. Общие положения</h2>
<p>Настоящее Пользовательское соглашение (далее «Соглашение») регулирует отношения между ООО «НО КОПИ» (далее «Компания», «Мы») и пользователем сервиса No Copy (далее «Пользователь», «Вы») при использовании веб-платформы, расположенной по адресу no-copy.ru и связанных с ней сервисов.</p>
<p>Используя наш сервис, Вы подтверждаете, что:</p>
<ul>
<li>Полностью прочитали и поняли условия настоящего Соглашения</li>
<li>Согласны соблюдать все условия и ограничения</li>
<li>Имеете право заключать данное соглашение согласно применимому законодательству</li>
<li>Достигли возраста 18 лет или действуете с согласия законных представителей</li>
</ul>
<div className="important-note">
<p>Если Вы не согласны с условиями данного Соглашения, пожалуйста, не используйте наш сервис.</p>
</div>
</div>
<div className="document-section">
<h2>2. Описание сервиса</h2>
<p>No Copy это инновационная платформа для защиты интеллектуальной собственности, предоставляющая следующие услуги:</p>
<h3>2.1 Основные функции:</h3>
<ul>
<li>Обфускация и защита исходного кода программного обеспечения</li>
<li>Внедрение цифровых водяных знаков в различные типы контента</li>
<li>Мониторинг и обнаружение несанкционированного использования</li>
<li>Защита PDF-документов от копирования и несанкционированного доступа</li>
<li>Защита аудио и видео контента</li>
<li>Юридическая поддержка при нарушении авторских прав</li>
</ul>
<h3>2.2 Дополнительные услуги:</h3>
<ul>
<li>Консультации по вопросам интеллектуальной собственности</li>
<li>Анализ уязвимостей и рисков</li>
<li>Индивидуальные решения для корпоративных клиентов</li>
<li>Техническая поддержка 24/7</li>
</ul>
</div>
<div className="document-section">
<h2>3. Права и обязанности пользователя</h2>
<h3>3.1 Права пользователя:</h3>
<ul>
<li>Использовать сервис в соответствии с выбранным тарифным планом</li>
<li>Получать техническую поддержку в рамках подписки</li>
<li>Требовать защиты персональных данных</li>
<li>Расторгнуть соглашение в любое время с соблюдением условий</li>
</ul>
<h3>3.2 Обязанности пользователя:</h3>
<ul>
<li>Предоставлять достоверную информацию при регистрации</li>
<li>Не нарушать права третьих лиц при использовании сервиса</li>
<li>Соблюдать требования безопасности и конфиденциальности</li>
<li>Своевременно оплачивать выбранные услуги</li>
<li>Не передавать доступ к аккаунту третьим лицам</li>
<li>Уведомлять о любых нарушениях безопасности</li>
</ul>
<div className="important-note">
<p>Пользователь несет полную ответственность за законность защищаемого контента и соблюдение авторских прав.</p>
</div>
</div>
<div className="document-section">
<h2>4. Права и обязанности Компании</h2>
<h3>4.1 Права Компании:</h3>
<ul>
<li>Изменять функционал сервиса для его улучшения</li>
<li>Приостанавливать обслуживание при нарушении условий</li>
<li>Обновлять настоящее Соглашение с предварительным уведомлением</li>
<li>Устанавливать технические ограничения использования</li>
</ul>
<h3>4.2 Обязанности Компании:</h3>
<ul>
<li>Обеспечивать работоспособность сервиса в соответствии с SLA</li>
<li>Защищать персональные данные пользователей</li>
<li>Предоставлять техническую поддержку</li>
<li>Уведомлять об изменениях в работе сервиса</li>
</ul>
</div>
<div className="document-section">
<h2>5. Платежи и возврат средств</h2>
<h3>5.1 Условия оплаты:</h3>
<ul>
<li>Оплата производится согласно выбранному тарифному плану</li>
<li>Цены указаны в российских рублях включая НДС</li>
<li>Оплата может производиться банковской картой или по счету</li>
<li>Подписка автоматически продлевается до отмены</li>
</ul>
<h3>5.2 Возврат средств:</h3>
<ul>
<li>Возврат возможен в течение 14 дней с момента оплаты</li>
<li>Возврат не производится за использованные ресурсы</li>
<li>При нарушении условий соглашения возврат не предусмотрен</li>
<li>Возврат осуществляется тем же способом, что и оплата</li>
</ul>
</div>
<div className="document-section">
<h2>6. Ответственность сторон</h2>
<p>Компания не несет ответственности за:</p>
<ul>
<li>Убытки, возникшие в результате неправомерных действий пользователя</li>
<li>Временные технические сбои, не зависящие от Компании</li>
<li>Действия третьих лиц, включая хакерские атаки</li>
<li>Нарушение авторских прав третьих лиц пользователем</li>
</ul>
<div className="important-note">
<p>Максимальная ответственность Компании ограничена суммой, уплаченной пользователем за услуги в течение последних 3 месяцев.</p>
</div>
</div>
<div className="document-section">
<h2>7. Интеллектуальная собственность</h2>
<p>Все права на сервис No Copy, включая программное обеспечение, дизайн, логотипы, торговые марки и другие элементы принадлежат Компании или ее партнерам.</p>
<p>Пользователь сохраняет все права на загружаемый контент, но предоставляет Компании ограниченную лицензию на его обработку в целях предоставления услуг защиты.</p>
</div>
<div className="document-section">
<h2>8. Конфиденциальность</h2>
<p>Компания обязуется:</p>
<ul>
<li>Не разглашать персональные данные пользователей</li>
<li>Использовать современные методы защиты информации</li>
<li>Соблюдать требования ФЗ «О персональных данных»</li>
<li>Получать согласие на обработку данных</li>
</ul>
<p>Подробная информация содержится в Политике конфиденциальности.</p>
</div>
<div className="document-section">
<h2>9. Изменение соглашения</h2>
<p>Компания имеет право изменять условия настоящего Соглашения. Об изменениях пользователи уведомляются не менее чем за 10 дней до их вступления в силу.</p>
<p>Продолжение использования сервиса после изменений означает согласие с новыми условиями.</p>
</div>
<div className="document-section">
<h2>10. Разрешение споров</h2>
<p>Все споры решаются путем переговоров. При невозможности мирного урегулирования споры рассматриваются в суде по месту нахождения Компании в соответствии с законодательством Российской Федерации.</p>
</div>
<div className="contact-info">
<h3>Контактная информация</h3>
<p><strong>ООО «НО КОПИ»</strong></p>
<p>📧 Email: legal@no-copy.ru</p>
<p>📞 Телефон: +7 (800) 100-43-98</p>
<p>🏢 Адрес: г. Москва, ул. Проспект Мира, д. 39</p>
<p>🌐 Веб-сайт: https://no-copy.ru</p>
</div>
<div className="effective-date">
<p>Соглашение вступает в силу с 01 января 2025 года</p>
<p>Последнее обновление: 25 сентября 2025 года</p>
</div>
</div>
</div>
</div>
</main>
)
}
+2 -6
View File
@@ -1,8 +1,7 @@
import LogoIcon from '@/app/ui/logo-icon'; import LogoIcon from '@/app/ui/logo-icon';
import styles from '@/app/styles/login.module.scss' import styles from '@/app/styles/module/login.module.scss'
import Link from 'next/link'; import Link from 'next/link';
import RegisterForm from '@/app/ui/register-form'; import RegisterForm from '@/app/ui/forms/register-form';
import LanguageSwitcher from '@/app/components/LanguageSwitcher';
import {useTranslations} from 'next-intl'; import {useTranslations} from 'next-intl';
export default function Page() { export default function Page() {
@@ -10,9 +9,6 @@ export default function Page() {
return ( return (
<div className={`${styles['login-container-wrapper']}`}> <div className={`${styles['login-container-wrapper']}`}>
<div className={`${styles['login-container-language']}`}>
<LanguageSwitcher />
</div>
<div className={`${styles['login-container']}`}> <div className={`${styles['login-container']}`}>
<div className={`${styles['logo']}`}> <div className={`${styles['logo']}`}>
<LogoIcon /> <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>
)
}
@@ -1,7 +0,0 @@
export default function Page() {
return (
<div>
terms-and-privacy-policy
</div>
)
}
+414
View File
@@ -0,0 +1,414 @@
export default function Page() {
return (
<main className="main-content">
<div className="container">
<div className="hero-container">
<h1 className="hero-title">Политика конфиденциальности</h1>
<p className="hero-subtitle">
Настоящая политика определяет, как мы собираем, используем и защищаем ваши персональные данные. Мы серьезно относимся к вашей конфиденциальности и обеспечиваем надежную защиту информации.
</p>
</div>
<div className="document-content-container">
<div className="document-content">
<div className="document-section">
<h2>1. Общие положения</h2>
<p>Настоящая Политика конфиденциальности (далее «Политика») определяет порядок обработки персональных данных пользователей сервиса No Copy, принадлежащего ООО «НО КОПИ» (далее «Компания», «Оператор»).</p>
<p>Политика разработана в соответствии с:</p>
<ul>
<li>Федеральным законом «О персональных данных» 152-ФЗ</li>
<li>Федеральным законом «Об информации, информационных технологиях и о защите информации» 149-ФЗ</li>
<li>Общим регламентом по защите данных (GDPR) Европейского Союза</li>
<li>Иными применимыми нормативными правовыми актами</li>
</ul>
<div className="important-note">
<p>Используя наш сервис, вы соглашаетесь с условиями настоящей Политики конфиденциальности.</p>
</div>
</div>
<div className="document-section">
<h2>2. Основные понятия</h2>
<h3>2.1 Персональные данные:</h3>
<p>Любая информация, относящаяся к прямо или косвенно определенному или определяемому физическому лицу (субъекту персональных данных).</p>
<h3>2.2 Обработка персональных данных:</h3>
<p>Любое действие (операция) или совокупность действий (операций), совершаемых с использованием средств автоматизации или без использования таких средств с персональными данными.</p>
<h3>2.3 Автоматизированная обработка:</h3>
<p>Обработка персональных данных с помощью средств вычислительной техники.</p>
<h3>2.4 Блокирование персональных данных:</h3>
<p>Временное прекращение обработки персональных данных (за исключением случаев, если обработка необходима для уточнения персональных данных).</p>
</div>
<div className="document-section">
<h2>3. Какие данные мы собираем</h2>
<p>Мы можем собирать следующие категории персональных данных:</p>
<table className="data-table">
<thead>
<tr>
<th>Категория данных</th>
<th>Примеры</th>
<th>Источник получения</th>
</tr>
</thead>
<tbody>
<tr>
<td>Идентификационные данные</td>
<td>ФИО, email, телефон</td>
<td>Регистрация, контактные формы</td>
</tr>
<tr>
<td>Данные об использовании</td>
<td>IP-адрес, время посещения, страницы</td>
<td>Автоматический сбор</td>
</tr>
<tr>
<td>Технические данные</td>
<td>Браузер, операционная система</td>
<td>Автоматический сбор</td>
</tr>
<tr>
<td>Платежные данные</td>
<td>Данные карты, чеки</td>
<td>Платежные системы</td>
</tr>
<tr>
<td>Контент пользователя</td>
<td>Загружаемые файлы, сообщения</td>
<td>Пользовательский ввод</td>
</tr>
</tbody>
</table>
<h3>3.1 Автоматически собираемые данные:</h3>
<ul>
<li>IP-адрес и данные о местоположении</li>
<li>Информация о браузере и операционной системе</li>
<li>Данные о посещенных страницах и времени на сайте</li>
<li>Реферер (источник перехода на сайт)</li>
<li>Файлы cookie и локальное хранилище</li>
</ul>
</div>
<div className="document-section">
<h2>4. Цели обработки персональных данных</h2>
<p>Мы обрабатываем ваши персональные данные для следующих целей:</p>
<h3>4.1 Основные цели:</h3>
<ul>
<li>Предоставление услуг защиты интеллектуальной собственности</li>
<li>Регистрация и ведение аккаунтов пользователей</li>
<li>Обработка платежей и выставление счетов</li>
<li>Техническая поддержка и обслуживание клиентов</li>
<li>Уведомления о статусе услуг и важных изменениях</li>
</ul>
<h3>4.2 Дополнительные цели:</h3>
<ul>
<li>Аналитика использования сервиса для его улучшения</li>
<li>Предотвращение мошенничества и обеспечение безопасности</li>
<li>Маркетинговые коммуникации (с вашего согласия)</li>
<li>Соблюдение правовых обязательств</li>
<li>Разрешение споров и правоприменение</li>
</ul>
<div className="important-note">
<p>Мы не обрабатываем персональные данные в целях, несовместимых с указанными выше, без получения дополнительного согласия.</p>
</div>
</div>
<div className="document-section">
<h2>5. Правовые основания обработки</h2>
<p>Обработка персональных данных осуществляется на основании:</p>
<h3>5.1 Согласие субъекта персональных данных:</h3>
<ul>
<li>Маркетинговые рассылки</li>
<li>Использование cookies для аналитики</li>
<li>Обработка специальных категорий данных</li>
</ul>
<h3>5.2 Исполнение договора:</h3>
<ul>
<li>Предоставление заказанных услуг</li>
<li>Обработка платежей</li>
<li>Техническая поддержка</li>
</ul>
<h3>5.3 Правовые обязательства:</h3>
<ul>
<li>Ведение налогового и бухгалтерского учета</li>
<li>Предоставление данных государственным органам</li>
<li>Соблюдение валютного законодательства</li>
</ul>
<h3>5.4 Законные интересы:</h3>
<ul>
<li>Обеспечение безопасности системы</li>
<li>Предотвращение мошенничества</li>
<li>Улучшение качества услуг</li>
</ul>
</div>
<div className="document-section">
<h2>6. Передача данных третьим лицам</h2>
<p>Мы можем передавать ваши персональные данные следующим категориям получателей:</p>
<h3>6.1 Поставщики услуг:</h3>
<ul>
<li>Платежные системы (для обработки платежей)</li>
<li>Хостинг-провайдеры (для размещения данных)</li>
<li>Системы аналитики (Google Analytics, Яндекс.Метрика)</li>
<li>Службы доставки уведомлений</li>
</ul>
<h3>6.2 Государственные органы:</h3>
<ul>
<li>По запросам правоохранительных органов</li>
<li>В рамках налогового контроля</li>
<li>При исполнении судебных решений</li>
</ul>
<h3>6.3 Партнеры:</h3>
<ul>
<li>Юридические фирмы (для защиты прав)</li>
<li>Технические партнеры (интеграции)</li>
</ul>
<div className="important-note">
<p>Все третьи лица обязуются обеспечивать адекватный уровень защиты персональных данных и использовать их только в согласованных целях.</p>
</div>
</div>
<div className="document-section">
<h2>7. Трансграничная передача данных</h2>
<p>Некоторые из наших поставщиков услуг могут обрабатывать данные за пределами Российской Федерации. В таких случаях мы обеспечиваем:</p>
<ul>
<li>Передачу данных только в страны с адекватным уровнем защиты</li>
<li>Заключение соответствующих договоров с гарантиями защиты</li>
<li>Применение стандартных договорных положений ЕС</li>
<li>Получение необходимых согласий субъектов данных</li>
</ul>
<p>Актуальный список стран, в которые передаются данные, доступен по запросу.</p>
</div>
<div className="document-section">
<h2>8. Сроки хранения данных</h2>
<p>Мы храним персональные данные не дольше, чем это необходимо для достижения целей обработки:</p>
<table className="data-table">
<thead>
<tr>
<th>Тип данных</th>
<th>Срок хранения</th>
<th>Основание</th>
</tr>
</thead>
<tbody>
<tr>
<td>Данные аккаунта</td>
<td>До удаления аккаунта + 1 год</td>
<td>Исполнение договора</td>
</tr>
<tr>
<td>Платежные данные</td>
<td>5 лет</td>
<td>Налоговое законодательство</td>
</tr>
<tr>
<td>Логи доступа</td>
<td>1 год</td>
<td>Требования безопасности</td>
</tr>
<tr>
<td>Маркетинговые данные</td>
<td>До отзыва согласия</td>
<td>Согласие</td>
</tr>
<tr>
<td>Данные поддержки</td>
<td>3 года</td>
<td>Обслуживание клиентов</td>
</tr>
</tbody>
</table>
<p>По истечении указанных сроков данные удаляются или обезличиваются.</p>
</div>
<div className="document-section">
<h2>9. Ваши права</h2>
<p>В соответствии с применимым законодательством вы имеете следующие права:</p>
<h3>9.1 Право на доступ:</h3>
<ul>
<li>Получить подтверждение факта обработки ваших данных</li>
<li>Получить копию обрабатываемых данных</li>
<li>Узнать цели и правовые основания обработки</li>
</ul>
<h3>9.2 Право на исправление:</h3>
<ul>
<li>Исправить неточные данные</li>
<li>Дополнить неполные данные</li>
</ul>
<h3>9.3 Право на удаление («право быть забытым»):</h3>
<ul>
<li>Удалить данные при отзыве согласия</li>
<li>Удалить данные при незаконной обработке</li>
<li>Удалить данные, когда они больше не нужны</li>
</ul>
<h3>9.4 Право на ограничение обработки:</h3>
<ul>
<li>Приостановить обработку на время проверки точности</li>
<li>Ограничить обработку при незаконности</li>
</ul>
<h3>9.5 Право на портируемость данных:</h3>
<ul>
<li>Получить данные в структурированном формате</li>
<li>Передать данные другому оператору</li>
</ul>
<h3>9.6 Право на возражение:</h3>
<ul>
<li>Возразить против обработки для прямого маркетинга</li>
<li>Возразить против обработки на основании законных интересов</li>
</ul>
<div className="important-note">
<p>Для реализации своих прав обращайтесь к нам по адресу privacy@no-copy.ru. Мы рассмотрим ваш запрос в течение 30 дней.</p>
</div>
</div>
<div className="document-section">
<h2>10. Меры безопасности</h2>
<p>Мы применяем следующие технические и организационные меры для защиты ваших данных:</p>
<h3>10.1 Технические меры:</h3>
<ul>
<li>Шифрование данных при передаче (SSL/TLS)</li>
<li>Шифрование данных при хранении (AES-256)</li>
<li>Многофакторная аутентификация</li>
<li>Регулярные обновления безопасности</li>
<li>Мониторинг безопасности 24/7</li>
<li>Резервное копирование данных</li>
</ul>
<h3>10.2 Организационные меры:</h3>
<ul>
<li>Ограничение доступа к данным по принципу необходимости</li>
<li>Обучение сотрудников основам защиты данных</li>
<li>Регулярные аудиты безопасности</li>
<li>Процедуры реагирования на инциденты</li>
<li>Соглашения о конфиденциальности с сотрудниками</li>
</ul>
</div>
<div className="document-section">
<h2>11. Cookies и похожие технологии</h2>
<p>Мы используем файлы cookie и похожие технологии для:</p>
<h3>11.1 Необходимые cookies:</h3>
<ul>
<li>Аутентификация пользователей</li>
<li>Настройки безопасности</li>
<li>Функционирование сервиса</li>
</ul>
<h3>11.2 Аналитические cookies:</h3>
<ul>
<li>Google Analytics</li>
<li>Яндекс.Метрика</li>
<li>Внутренняя аналитика</li>
</ul>
<h3>11.3 Маркетинговые cookies:</h3>
<ul>
<li>Ретаргетинг</li>
<li>Персонализация рекламы</li>
<li>Измерение эффективности</li>
</ul>
<p>Вы можете управлять cookies через настройки браузера или наш центр управления конфиденциальностью.</p>
</div>
<div className="document-section">
<h2>12. Дети</h2>
<p>Наш сервис не предназначен для лиц младше 18 лет. Мы намеренно не собираем персональные данные детей младше 18 лет.</p>
<p>Если вы являетесь родителем или опекуном и знаете, что ваш ребенок предоставил нам персональные данные, пожалуйста, свяжитесь с нами. Мы удалим такие данные из наших систем.</p>
</div>
<div className="document-section">
<h2>13. Изменения в политике</h2>
<p>Мы можем периодически обновлять настоящую Политику конфиденциальности. При внесении существенных изменений мы уведомим вас:</p>
<ul>
<li>Размещением уведомления на главной странице сайта</li>
<li>Отправкой уведомления на ваш email</li>
<li>Через уведомления в личном кабинете</li>
</ul>
<p>Рекомендуем периодически просматривать данную страницу для ознакомления с актуальной версией Политики.</p>
</div>
<div className="document-section">
<h2>14. Жалобы и контроль</h2>
<p>Если вы считаете, что ваши права на защиту персональных данных были нарушены, вы имеете право:</p>
<ul>
<li>Обратиться к нам с жалобой</li>
<li>Подать жалобу в Роскомнадзор</li>
<li>Обратиться в суд</li>
<li>При обработке данных в ЕС обратиться к соответствующему регулятору</li>
</ul>
<p>Контактные данные для подачи жалоб указаны в разделе «Контактная информация».</p>
</div>
<div className="contact-info">
<h3>Контактная информация</h3>
<p><strong>Контролер персональных данных:</strong></p>
<p>ООО «НО КОПИ»</p>
<p>📧 Email: privacy@no-copy.ru</p>
<p>📧 Вопросы по защите данных: info@no-copy.ru</p>
<p>📞 Телефон: +7 (800) 100-43-98</p>
<p>🏢 Адрес: г. Москва, ул. Проспект Мира, д. 39</p>
<p>🌐 Веб-сайт: https://no-copy.ru</p>
<p><strong>Надзорный орган:</strong></p>
<p>Федеральная служба по надзору в сфере связи, информационных технологий и массовых коммуникаций (Роскомнадзор)</p>
<p>🌐 https://rkn.gov.ru</p>
</div>
<div className="effective-date">
<p>Политика конфиденциальности вступает в силу с 01 января 2025 года</p>
<p>Последнее обновление: 25 сентября 2025 года</p>
</div>
</div>
</div>
</div>
</main>
)
}
+9
View File
@@ -0,0 +1,9 @@
import packageJson from '../../../../package.json';
export default function Page() {
return (
<div>
{packageJson.version}
</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;
}
}
+201 -34
View File
@@ -1,54 +1,221 @@
'use server' 'use server'
import { getSessionData } from '@/app/actions/session';
import { localDevelopmentUrl } from '@/app/actions/definitions';
const API_BASE_URL = process.env.NODE_ENV === 'development' import { getSessionData, deleteSession } from '@/app/actions/session';
? localDevelopmentUrl import { testUserData, API_BASE_URL } from '@/app/actions/definitions';
: 'http://app:8080';
const FRUITS_URL = 'https://www.fruityvice.com/api/fruit/';
const ALL = 'all';
export async function fetchFruits() {
try {
const response = await fetch(FRUITS_URL + ALL, { 'method': 'GET' });
let parsed = await response.json();
return parsed;
} catch (error) {
console.error('Error:', error);
throw new Error('Failed to fetch fruits data.');
}
}
export async function fetchFruit(id: number) {
try {
const response = await fetch(FRUITS_URL + id, { 'method': 'GET' });
let parsed = await response.json();
return parsed;
} catch (error) {
console.error('Error:', error);
throw new Error('Failed to fetch fruits data.');
}
}
export async function getUserData() { export async function getUserData() {
const userEmail = await getSessionData('email'); const userEmail = await getSessionData('email');
const token = await getSessionData('token'); const token = await getSessionData('token');
/* для теста */
if (userEmail === 'test' && token === '1111') {
return testUserData;
}
/* для теста */
try { try {
const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, { const response = await fetch(`${API_BASE_URL}/v1/api/user?email=${userEmail}`, {
method: 'GET', method: 'GET',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"Accept": "application/json", 'Accept': 'application/json',
"Authorization": `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
} }
}); });
if (response.ok) { if (response.ok) {
return await response.json(); return await response.json();
} else {
return null
} }
} catch (error) { } catch (error) {
console.error('error'); return null
}
}
export async function getUserFilesInfo() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'user_files_info',
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else if (parsed.message_code === 2) {
await deleteSession();
} else {
throw parsed.message_code;
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30001,
message_body: {
action: 'get',
tariff_term: tariffTerm
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json()
if (parsed?.message_code === 0) {
return parsed.message_body?.tariffs;
} else {
return []
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function fetchTokensBundle() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30001,
message_body: {
action: 'tariffs_by_type',
type: 'token'
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed?.message_code === 0) {
return parsed.message_body?.token_tariffs;
} else {
return null
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function getBuildData() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/check/api/build`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
return await response.json();
} else {
return null
}
} catch (error) {
return null;
}
}
export async function fetchINN(inn: string) {
try {
const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30004,
message_body: {
inn: inn,
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
const parsed = await response.json()
if (parsed?.message_code === 0) {
return parsed.message_body
} else {
return null
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function getHealt() {
try {
const response = await fetch(`${API_BASE_URL}/check/api/healt`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
return await response.json();
} else {
return null
}
} catch (error) {
return null
} }
} }
+529 -54
View File
@@ -1,12 +1,10 @@
'use server' 'use server'
import { SignupFormSchema, loginFormSchema, localDevelopmentUrl } from '@/app/actions/definitions'; import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
import { createSession, deleteSession, getSessionData } from '@/app/actions/session'; import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { fetchINN } from '@/app/actions/action';
const API_BASE_URL = process.env.NODE_ENV === 'development' import { headers } from 'next/headers';
? localDevelopmentUrl
: 'http://app:8080';
export async function logout() { export async function logout() {
const token = await getSessionData('token'); const token = await getSessionData('token');
@@ -15,17 +13,16 @@ export async function logout() {
await fetch(`${API_BASE_URL}/v1/api/auth/logout`, { await fetch(`${API_BASE_URL}/v1/api/auth/logout`, {
method: 'POST', method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"Accept": "application/json", 'Accept': 'application/json',
"Authorization": `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
} }
}); });
} catch (error) { } catch (error) {
throw error; throw error;
} finally {
await deleteSession();
} }
await deleteSession();
redirect('/login');
} }
export async function authorization( export async function authorization(
@@ -40,6 +37,13 @@ export async function authorization(
const email = formData.get('email') as string || ''; const email = formData.get('email') as string || '';
const password = formData.get('password'); const password = formData.get('password');
/* для теста */
if (email === 'test' && password === 'test') {
await createSession('1111', 'test');
redirect('/pages/dashboard');
}
/* для теста */
const validatedFields = loginFormSchema.safeParse({ const validatedFields = loginFormSchema.safeParse({
email, email,
password password
@@ -68,27 +72,49 @@ export async function authorization(
try { try {
const { email, password } = validatedFields.data; const { email, password } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/v1/api/auth/login`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
email: email, version: 1,
password: password msg_id: 20001,
message_body: {
email: email,
password: password
}
}), }),
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"Accept": "application/json" 'Accept': 'application/json'
} }
}); });
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
await createSession(parsed.token, parsed.email);
if (parsed.message_code === 0) {
await createSession(parsed.message_body.token, email);
} else {
throw (`${parsed.message_code}`);
}
} else { } else {
throw ('request-ended-with-an-error'); throw ('request-ended-with-an-error');
} }
} catch (error) { } catch (error) {
const errors: Record<string, string> = {} const errors: Record<string, string> = {}
errors['server'] = `${error}`;
switch (error) {
case '2':
errors['server'] = 'password-does-not-match'
break;
case '4':
errors['server'] = 'email-not-found'
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
if (error) { if (error) {
return { return {
@@ -105,39 +131,452 @@ export async function authorization(
redirect('/pages/dashboard'); redirect('/pages/dashboard');
} }
type FormState = { export type FormState = {
previousState: { previousState: {
full_name: string; fullName?: string | undefined;
email: string; email?: string | undefined;
password: string; password?: string | undefined;
confirm_password: string; confirm_password?: string | undefined;
phone: string; phone?: string | undefined;
company: string; companyName?: string | undefined;
agree: string; agree?: string | undefined;
} referralCode?: string | undefined;
error: Record<string, string> inn?: string | undefined;
} | undefined,
mailConfirm?: boolean,
userId?: number,
error: Record<string, string> | null
}; };
export async function registration( export async function registration(
state: FormState | undefined, state: FormState | undefined,
formData: FormData, formData: FormData,
): Promise<FormState> { ): Promise<FormState> {
const full_name = formData.get('full_name') as string || ''; const headersList = await headers();
const accountType = formData.get('accountType') as 'b2b' | 'b2c';
/* вернуть в true когда проект выйдет из дева */
const mail_verified = false;
const fullName = formData.get('fullName') as string || '';
const email = formData.get('email') as string || ''; const email = formData.get('email') as string || '';
const password = formData.get('password') as string || ''; const password = formData.get('password') as string || '';
const confirm_password = formData.get('confirm_password') as string || ''; const confirm_password = formData.get('confirm_password') as string || '';
const phone = formData.get('phone') as string || ''; const phone = formData.get('phone') as string || '';
const company = formData.get('company') as string || ''; const companyName = formData.get('companyName') as string || '';
const agree = formData.get('agree') as string || ''; const agree = formData.get('agree') as string || '';
const referralCode = formData.get('referralCode') as string || '';
const inn = formData.get('inn') as string || '';
const validatedFields = SignupFormSchema.safeParse({
full_name, const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const SignupFormSchema = await getSignupFormSchema(accountType);
const dataToValidate = {
fullName,
email, email,
phone: phone.replace(/[-\(\)\s]/g, ''), phone: phone.replace(/[-\(\)\s]/g, ''),
password, password,
confirm_password, confirm_password,
agree, agree,
}); ...(accountType === 'b2b' && { companyName, inn })
};
const validatedFields = await SignupFormSchema.safeParse(dataToValidate);
console.log(`try to register ${accountType}`);
let confirmedCompanyName: boolean = true;
if (accountType === 'b2b') {
try {
const response = await fetchINN(inn);
confirmedCompanyName = response?.companyName ? true : false;
} catch (error) {
confirmedCompanyName = false;
}
}
if (!validatedFields.success || !confirmedCompanyName) {
const errors: Record<string, string> = {}
if (validatedFields?.error?.message) {
JSON?.parse(validatedFields?.error?.message).forEach((obj: {
message: string,
path: string
}) => {
if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`;
} else {
errors[obj.path[0]] = obj.message;
}
});
}
if (!confirmedCompanyName) {
errors.inn = 'inn-invalid'
}
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone,
agree,
...(referralCode !== '' && { referralCode }),
...(accountType === 'b2b' && { companyName, inn })
},
error: errors
};
}
try {
const { fullName, email, password, phone } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20002,
message_body: {
fullName: fullName,
email,
phone: phone,
password,
accountType,
mail_verified: mail_verified,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode })
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
console.log({
fullName: fullName,
email,
phone: phone,
password,
accountType,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode })
})
if (response.ok) {
let parsed = await response.json() as {
message_desc: string,
message_body: { token: string, userId: number, fieldErrors: string },
message_code: number
};
if (parsed.message_code === 0) {
console.log(`registration - ${accountType}`);
console.log({
fullName: fullName,
email,
phone: phone,
password,
accountType,
...(referralCode !== '' && { referralLink: referralCode }),
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
});
if (mail_verified) {
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone,
agree,
...(referralCode !== '' && { referralLink: referralCode }),
...(accountType === 'b2b' && { companyName: companyName, inn: inn })
},
mailConfirm: true,
userId: parsed.message_body.userId,
error: null
};
} else {
await createSession(parsed.message_body.token, email as string);
}
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error: unknown) {
const errors: Record<string, string> = {};
if (error) {
const typedError = error as any;
switch (typedError.message_code) {
case 1:
if (typedError.message_desc === 'Refferal link is not exist') {
errors['server'] = 'referral-link-is-not-exist'
break;
} else {
errors['server'] = 'email-or-already-registered'
break;
}
case 2:
if (typedError.message_body?.fieldErrors) {
typedError.message_body?.fieldErrors?.forEach((obj: {
code: string,
field: string
}) => {
if (errors[obj.field]) {
errors[obj.field] = `${errors[obj.field]}&${obj.code.replaceAll('.', '-')}`;
} else {
errors[obj.field] = obj.code.replaceAll('.', '-');
}
});
} else if (typedError.message_desc === 'Company already exists') {
errors['inn'] = 'company-already-registered'
} else {
errors['server'] = 'unknown-error'
}
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone,
companyName,
agree,
inn
},
mailConfirm: false,
error: errors
};
}
return {
previousState: {
fullName,
email,
password,
confirm_password,
phone,
companyName,
agree,
inn
},
error: errors
};
}
redirect('/pages/dashboard');
/* console.log('/v2');
redirect('/v2'); */
}
export async function tokenLifeExtension() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
version: 1,
msg_id: 20008
}),
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
await updateSession(parsed.message_body.token);
}
}
} catch (error) {
await deleteSession();
throw error;
}
}
export async function confirmEmail(userId: number, verifyToken: string) {
console.log('confirmEmail');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
'Authorization': `Bearer ${verifyToken}`,
},
body: JSON.stringify({
version: 1,
msg_id: 20009,
message_body: {
resend: 0,
user_id: userId
}
}),
});
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) {
await createSession(parsed.message_body.token, parsed.message_body.email as string);
} else {
return {
success: false,
error: 'entered-code-is-incorrect'
};
}
} else {
return {
success: false,
error: 'server-error'
};
}
} catch (error) {
return {
success: false,
error: 'network-error'
};
}
redirect('/pages/dashboard');
}
export async function resendConfirmEmail(userId: number) {
console.log('resendConfirmEmail');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({
version: 1,
msg_id: 20009,
message_body: {
resend: 1,
user_id: userId,
}
}),
});
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) {
return {
fine: true
}
}
}
} catch (error) {
return {
fine: false
}
}
}
export type FormStateConfirmPassword = {
previousState: {
email: string;
password: string;
confirm_password: string;
verifyToken: string;
},
error: Record<string, string> | null
};
export async function resetPassword(
state: any | undefined,
formData: FormData,
) {
const email = formData.get('email');
try {
const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
version: 1,
msg_id: 20010,
message_body: {
email: email,
action: "resetPassword"
}
}),
});
if (response.ok) {
const parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) {
} else {
throw 'error';
}
}
} catch (error) {
return {
error: error
};
}
console.log('redirect')
redirect(`/reset-password?email=${email}`);
}
export async function confirmPassword(
state: FormStateConfirmPassword | undefined,
formData: any,
): Promise<FormStateConfirmPassword | undefined> {
const email = formData.get('email');
const password = formData.get('password');
const confirm_password = formData.get('confirm_password');
const verifyToken = formData.get('verify-token');
console.log('confirmPassword');
const SignupFormSchema = await getSignupFormSchema('resetPassword');
const dataToValidate = {
password,
confirm_password
};
const validatedFields = SignupFormSchema.safeParse(dataToValidate);
if (!validatedFields.success) { if (!validatedFields.success) {
const errors: Record<string, string> = {} const errors: Record<string, string> = {}
@@ -154,54 +593,90 @@ export async function registration(
return { return {
previousState: { previousState: {
full_name,
email, email,
password, password,
confirm_password, confirm_password,
phone, verifyToken
company,
agree
}, },
error: errors error: errors
}; };
} }
try { try {
const { full_name, email, password, phone } = validatedFields.data; const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
const response = await fetch(`${API_BASE_URL}/v1/api/auth/register1`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ fullName: full_name, email, password, companyName: company, phone: phone }),
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
"Accept": "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) { if (response.ok) {
let parsed = await response.json();
await createSession(parsed.token, email); 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 { } else {
throw (`${response.status}`); throw (`${response.status}`);
} }
} catch (error) { } catch (error: unknown) {
const errors: Record<string, string> = {};
if (error) { if (error) {
const errors: Record<string, string> = {}; const typedError = error as any;
errors['server'] = 'request-ended-with-an-error'
switch (typedError.message_code) {
case 1:
errors['server'] = 'unknown-error'
break
case 2:
if (typedError.message_desc === 'Token not found or time expired') {
errors['server'] = 'token-not-found-or-time-expired'
} else {
errors['server'] = 'unknown-error'
}
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
return { return {
previousState: { previousState: {
full_name,
email, email,
password, password,
confirm_password, confirm_password,
phone, verifyToken
company,
agree
}, },
error: errors error: errors
}; };
} }
throw error;
return {
previousState: {
email,
password,
confirm_password,
verifyToken
},
error: errors
};
} }
redirect('/pages/dashboard'); 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
};
}
}
+97 -27
View File
@@ -1,4 +1,6 @@
import * as z from 'zod' import * as z from 'zod'
import { createValidationSchema, createValidationSchemaForPassword } from '@/app/lib/validation-config-parser';
import validationConfig from '@/app/lib/validation-config.json';
export type FormState = export type FormState =
| { | {
@@ -16,32 +18,58 @@ export type FormAction = (
formData: FormData formData: FormData
) => Promise<FormState> ) => Promise<FormState>
export const SignupFormSchema = z interface FieldConfig {
.object({ maxSize?: { value: number; message: string };
full_name: z minSize?: { value: number; message: string };
.string() allowedSymbols?: { value: string; message: string };
.min(3, { error: 'register-error-name' }) optional?: { value: boolean };
.trim(), requiredDigit?: { value: boolean; message: string };
email: z.email({ error: 'register-error-valid-email' }).trim(), requiredLetter?: { value: boolean; message: string };
phone: z caseType?: { value: 'LOWER' | 'UPPER' | 'ANY'; message: string };
.string() emailValidation?: { value: boolean; message: string };
.min(12, { error: 'register-error-phone' }), }
password: z
.string() interface ValidationConfig {
.min(8, { error: 'register-error-password-symbols' }) [key: string]: FieldConfig;
.regex(/[a-zA-Zа-яёА-ЯЁ]/, { error: 'register-error-password-one-letter'}) }
.regex(/[0-9]/, { error: 'register-error-password-one-digit' })
.trim(), const defaultConfig = validationConfig as ValidationConfig;
confirm_password: z
.string(), async function loadConfigFromBackend(accountType?: 'b2b' | 'b2c'): Promise<any> {
agree: z try {
.string() const response = await fetch('/api/validation-config');
.min(2, { error: 'register-error-agree' }) if (!response.ok) throw new Error('Failed to load config');
}) return await response.json();
.refine((data) => data.password === data.confirm_password, { } catch (error) {
message: 'repeat-password', //сейчас выполняем это пока бека нету
path: ['confirm_password'], /* console.error('Using default validation config:', error); */
}); return defaultConfig;
}
}
export async function getSignupFormSchema(schemaType?: 'b2b' | 'b2c' | 'resetPassword') {
let schemaPromise: Promise<any> | null = null;
let cachedSignupSchema: any = null;
if (!schemaPromise) {
schemaPromise = (async () => {
const config = await loadConfigFromBackend();
if (schemaType === 'resetPassword') {
cachedSignupSchema = createValidationSchemaForPassword(config);
} else {
cachedSignupSchema = createValidationSchema(config, schemaType);
}
return cachedSignupSchema;
})();
}
return await schemaPromise;
}
// для валидации в реально времени на клиентской странице
export const SignupFormSchema = createValidationSchema(defaultConfig);
export const loginFormSchema = z export const loginFormSchema = z
.object({ .object({
@@ -55,4 +83,46 @@ export const loginFormSchema = z
.trim(), .trim(),
}) })
export const localDevelopmentUrl = 'http://localhost:80';
export const createComplaintSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'text-area-error-fill-complaint-text' })
.max(2000, {error: 'text-area-error-complaint-text-too-long'})
.trim(),
})
export const createClaimSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'fill-text-of-the-case' })
.max(2000, {error: 'claim-text-must-not-exceed-2000-characters'})
.trim(),
caseTitle: z
.string()
.min(1, { error: 'fill-title-of-the-case' })
.max(300, {error: 'claim-title-must-not-exceed-300-characters'})
.trim(),
})
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
export const API_BASE_URL = process.env.NODE_ENV === 'development'
? localDevelopmentUrl
: 'http://app:8080';
export const testUserData = {
fullName: 'test',
email: 'test@mail.com',
subscriptionType: 'base'
}
export const bankCardFormSchema = z
.object({
bankCard: z
.string()
.min(19, { error: 'error-bank-card' })
.trim()
})
+205
View File
@@ -0,0 +1,205 @@
'use server'
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export async function getUserFilesData({
page,
pageSize,
sortBy,
sortOrder,
type,
date,
query = ''
}: {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc' | string;
type?: string;
date?: string;
query?: string;
}) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'search_files',
page: page,
page_size: pageSize,
query: query,
sort_by: sortBy || '', // protectStatus, fileName, fileSize, createdAt, updatedAt, supportId
type: type || '', // image, video, document, audio
date: date || '', // today, week, month
sort_order: sortOrder
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed.message_text ? parsed.message_text : 'error';
}
} else {
throw `${response.status}`;
}
} catch (error) {
return error;
}
}
export async function removeUserFile(fileId: string, fullDelete: number) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'delete_file',
file_id: fileId,
full_delete: fullDelete,
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body.file_id;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
export async function viewFileInfo(fileId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'file_info',
file_id: fileId
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
export async function downloadFile(fileId: string, fileName: string) {
const token = await getSessionData('token');
const response = await fetch(`${API_BASE_URL}/api/v1/files/download/${fileId}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (!response.ok) {
throw 'failed-to-download-file';
}
const arrayBuffer = await response.arrayBuffer()
const base64 = Buffer.from(arrayBuffer).toString('base64')
return {
data: base64,
contentType: response.headers.get('content-type') || 'application/octet-stream',
fileName: fileName
}
}
export async function FileAnAppeal(fileId: string, appealReason: string, additionalInfo: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'submit_appeal',
file_id: fileId,
appeal_reason: appealReason,
additional_info: additionalInfo
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
+293
View File
@@ -0,0 +1,293 @@
'use server'
import { API_BASE_URL } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session';
interface initMessageBody {
file_name: string,
file_type: string,
extension: string,
file_size: number | string,
action?: string,
token?: string,
convertTo?: string | undefined | null
}
export async function fileUpload(messageBody: initMessageBody, convertTo?: string | null | undefined) {
const token = await getSessionData('token');
if (!token) {
return;
}
const message = messageBody;
message.action = 'init';
if (convertTo && convertTo !== 'reject') {
message.convertTo = convertTo;
}
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20004,
message_body: message
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json() as {
message_desc: string,
message_body: {
upload_id: string,
total_chunks: number,
chunk_size: number,
status: string
},
message_code: string
};
if (parsed.message_desc === 'Operation successful') {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error;
}
}
export async function cancelUpload(udloadId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20004,
message_body: {
action: 'cancel',
upload_id: udloadId,
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json() as {
message_code: string,
message_desc: string,
message_body: {
upload_id: string,
message: string
}
};
if (parsed.message_desc === 'Upload cancelled successfully') {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error;
}
}
export type ChunkResponse = {
errorMesage: string | null
fileId?: string
};
export async function chunkUpload(formData: FormData, itPrivate?: boolean): Promise<ChunkResponse> {
const token = await getSessionData('token');
try {
const url = itPrivate
? `${API_BASE_URL}/api/v1/private/files/chunk`
: `${API_BASE_URL}/api/v1/files/chunk`;
/* if (token && itPrivate) {
formData.append('token', token);
} */
const response = await fetch(url, {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${token}`
}
});
/* ...(token && itPrivate && { 'Authorization': `Bearer ${token}` }) возможно нужно будет условно отправлять токен*/
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return {
errorMesage: null,
fileId: parsed.message_body.file_id
}
} else if (parsed.message_desc === 'Duplicate file upload') {
throw 'error-duplicate-file'
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: User not have tokens for protect') {
throw 'user-not-have-tokens-for-protect'
} else if (parsed.message_desc === 'Failed to upload chunk: Failed to upload chunk: Failed to upload chunk: .wav file not have RIFF header') {
throw 'error-upload-chunk-wav-file-not-have-riff'
} else {
throw 'error-uploading-file'
}
} else {
throw 'error-uploading-file'
}
} catch (error: unknown) {
if (error) {
const typedError = error as string;
return {
errorMesage: typedError
};
} else {
return {
errorMesage: 'error-uploading-file'
};
}
}
}
export async function checkChunkStatus(upload_id: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20004,
message_body: {
action: 'chunks',
upload_id
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_desc === 'Upload cancelled successfully') {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
export async function getAllowedFilesExtensions(type: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20004,
message_body: {
action: 'file_extension',
file_type: type
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_desc === 'Operation successful') {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
export async function checkOperationPrice(action: string, filesCount: number, fileType: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30008,
message_body: {
action: action,
file_type: fileType,
count_files: filesCount
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
cost: 0,
success: false,
tokens_count: 0,
error: 'error'
}
}
}
+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;
}
}
+47 -7
View File
@@ -1,7 +1,9 @@
import 'server-only' 'use server';
/* import 'server-only'; */
import { cookies } from 'next/headers' import { cookies } from 'next/headers'
import { SignJWT, jwtVerify } from 'jose' import { SignJWT, jwtVerify } from 'jose'
/* import { SessionPayload } from '@/app/lib/definitions' */ import { redirect } from 'next/navigation';
/* const secretKey = process.env.SESSION_SECRET */ /* const secretKey = process.env.SESSION_SECRET */
const secretKey = 'AAA+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo=' const secretKey = 'AAA+sq1yKte/gMgUUu/B/OyXqr45/LMYplPVOlWc+uo='
@@ -27,7 +29,7 @@ export async function decrypt(session: string | undefined = '') {
} }
export async function createSession(token: string, email: string) { export async function createSession(token: string, email: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const expiresAt = new Date(Date.now() + 50 * 60 * 1000);
const session = await encrypt({ const session = await encrypt({
token, token,
email, email,
@@ -35,7 +37,7 @@ export async function createSession(token: string, email: string) {
}); });
const cookieStore = await cookies(); const cookieStore = await cookies();
cookieStore.set('session', session, { cookieStore.set('session_nc_db', session, {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
expires: expiresAt, expires: expiresAt,
@@ -44,9 +46,40 @@ export async function createSession(token: string, email: string) {
}); });
} }
export async function updateSession(newToken: string) {
const cookieStore = await cookies();
const session = cookieStore.get('session_nc_db')?.value;
if (!session) {
return null;
}
const payload = await decrypt(session);
if (!payload) {
return null;
}
const updatedPayload = {
...payload,
token: newToken,
expiresAt: new Date(Date.now() + 50 * 60 * 1000)
};
const updatedSession = await encrypt(updatedPayload);
cookieStore.set('session_nc_db', updatedSession, {
httpOnly: true,
secure: false,
expires: updatedPayload.expiresAt,
sameSite: 'lax',
path: '/',
});
}
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> { export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
const cookieStore = await cookies(); const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session')?.value; const sessionCookie = cookieStore.get('session_nc_db')?.value;
if (!sessionCookie) { if (!sessionCookie) {
return null; return null;
@@ -63,6 +96,13 @@ export async function getSessionData(type: 'token' | 'email'): Promise<string |
export async function deleteSession() { export async function deleteSession() {
const cookieStore = await cookies() console.log('delete session')
cookieStore.delete('session'); try {
const cookieStore = await cookies()
cookieStore.delete('session_nc_db');
} catch (error) {
} finally {
redirect('/login');
}
} }
-23
View File
@@ -1,23 +0,0 @@
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/actions/session'
export async function updateSession() {
const session = (await cookies()).get('session')?.value
const payload = await decrypt(session)
if (!session || !payload) {
return null
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
+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
}
}
+58
View File
@@ -0,0 +1,58 @@
'use server'
import { API_BASE_URL } from '@/app/actions/definitions';
import { createSession } from '@/app/actions/session';
import { redirect } from 'next/navigation';
export async function vkAuthorization(data : any, codeVerifier: string) {
console.log(data);
console.log(codeVerifier);
const { code, state, device_id } = data
try {
const response = await fetch(`${API_BASE_URL}/api/v1/vk/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
let parsed = await response.json();
console.log(parsed);
if (parsed.message_desc === 'Operation successful') {
/* await createSession(parsed.message_body.token, email); */
} else {
throw (`${parsed.message_code}`);
}
} else {
throw ('request-ended-with-an-error');
}
} catch (error) {
const errors: Record<string, string> = {}
switch (error) {
case '2':
errors['server'] = 'password-does-not-match'
break;
case '4':
errors['server'] = 'email-not-found'
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
if (error) {
return error;
}
throw error;
}
redirect('/pages/dashboard');
}
+57
View File
@@ -0,0 +1,57 @@
'use server'
import { API_BASE_URL } from '@/app/actions/definitions';
import { createSession } from '@/app/actions/session';
import { redirect } from 'next/navigation';
export async function YandexAuthorization(data: any, codeVerifier: string) {
console.log(data);
console.log(codeVerifier);
const { code, state, device_id } = data
try {
const response = await fetch(`${API_BASE_URL}/api/v1/yandex/authorization/${code}/${state}/${codeVerifier}/${device_id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (response.ok) {
let parsed = await response.json();
console.log(parsed);
if (parsed.message_desc === 'Operation successful') {
/* await createSession(parsed.message_body.token, email); */
} else {
throw (`${parsed.message_code}`);
}
} else {
throw ('request-ended-with-an-error');
}
} catch (error) {
const errors: Record<string, string> = {}
switch (error) {
case '2':
errors['server'] = 'password-does-not-match'
break;
case '4':
errors['server'] = 'email-not-found'
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
if (error) {
return error;
}
throw error;
}
redirect('/pages/dashboard');
}
+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
}
}
+107
View File
@@ -0,0 +1,107 @@
// components/ActivityTracker.tsx
'use client';
import { useEffect, useRef, useCallback } from 'react';
import { tokenLifeExtension } from '@/app/actions/auth';
export default function ActivityTracker() {
const lastRefreshTime = useRef(Date.now());
const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
const isRefreshing = useRef(false);
// Debounce функция
const debounce = useCallback((func: () => void, delay: number) => {
if (activityTimeoutRef.current) {
clearTimeout(activityTimeoutRef.current);
}
activityTimeoutRef.current = setTimeout(func, delay);
}, []);
// Ваша функция обновления токена (адаптированная для клиента)
const refreshTokenSilently = useCallback(async () => {
if (isRefreshing.current) return false;
isRefreshing.current = true;
try {
let response = tokenLifeExtension();
//@ts-ignore
return response.ok;
} catch (error) {
console.error('Silent token refresh failed:', error);
return false;
} finally {
isRefreshing.current = false;
}
}, []);
// Проверка необходимости обновления
const checkAndRefresh = useCallback(async () => {
const now = Date.now();
const timeSinceLastRefresh = now - lastRefreshTime.current;
// Обновляем не чаще чем раз в 10 минут
// и только если прошло больше 5 минут с последнего обновления
if (timeSinceLastRefresh > 5 * 60 * 1000 && timeSinceLastRefresh > 30000) {
const refreshed = await refreshTokenSilently();
if (refreshed) {
lastRefreshTime.current = now;
console.log('Token silently refreshed at', new Date().toISOString());
}
}
}, [refreshTokenSilently]);
// Обработчик активности
const handleActivity = useCallback(() => {
// Используем debounce чтобы не спамить запросами
// Ждем 30 секунд после последнего действия пользователя
debounce(checkAndRefresh, 30000);
}, [debounce, checkAndRefresh]);
useEffect(() => {
const events = [
'mousedown', 'mousemove', 'wheel', 'keydown',
'touchstart', 'touchend', 'scroll', 'click',
'focus', 'blur', 'input', 'change'
];
// Добавляем обработчики с passive: true для производительности
events.forEach(event => {
document.addEventListener(event, handleActivity, { passive: true });
});
// Также обновляем при возвращении на вкладку
const handleVisibilityChange = () => {
if (!document.hidden) {
handleActivity();
// Немедленно проверяем при возвращении на вкладку
setTimeout(checkAndRefresh, 1000);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
// Периодическая проверка (каждые 2 минуты)
const intervalId = setInterval(checkAndRefresh, 2 * 60 * 1000);
// Инициализация - проверяем сразу если нужно
const initialCheck = setTimeout(checkAndRefresh, 10000);
return () => {
// Очистка
events.forEach(event => {
document.removeEventListener(event, handleActivity);
});
document.removeEventListener('visibilitychange', handleVisibilityChange);
clearInterval(intervalId);
clearTimeout(initialCheck);
if (activityTimeoutRef.current) {
clearTimeout(activityTimeoutRef.current);
}
};
}, [handleActivity, checkAndRefresh]);
return null; // Не рендерит ничего
}
+84
View File
@@ -0,0 +1,84 @@
import React, { ReactNode, useState, useRef } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside';
interface DropDownListProps {
children: ReactNode;
value: string | number;
callBack: (value: string) => void;
translatedValue?: string;
}
interface ChildProps {
value?: string;
onClick?: () => void;
className?: string;
children?: React.ReactNode;
}
export default function DropDownList({ children, value, callBack, translatedValue }: DropDownListProps) {
const [isOpen, setIsOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
useClickOutside(dropdownRef,
() => {
setIsOpen(false);
},
isOpen
);
const enhancedChildren = React.Children.map(children, (child, index) => {
if (React.isValidElement<ChildProps>(child)) {
const childValue = child.props.value || undefined;
return React.cloneElement<ChildProps>(child, {
onClick: () => {
callBack(childValue || "");
setIsOpen(false);
},
className: `dropdown-item ${value === childValue ? "current" : ""}`,
key: index,
});
}
return child;
});
return (
<div className="dropdown-wrapper" ref={dropdownRef}>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
className="dropdown-button"
type="button"
>
<span className="flex items-center">
<span className="mr-2">
{translatedValue ? translatedValue : value}
</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && (
<div className="dropdown-list">
<ul
className="py-1"
role="listbox"
aria-labelledby="language-selector"
>
{enhancedChildren}
</ul>
</div>
)}
</div>
)
}
@@ -0,0 +1,115 @@
import React, { ReactNode, useState, useRef } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import { SelectedFile } from './UploadSectionFile';
import { useTranslations } from 'next-intl';
interface DropDownItem {
file: SelectedFile;
convertTo: string;
}
interface DropDownListProps {
children: ReactNode;
value: string | null | undefined;
callBack: (value: DropDownItem) => void;
translatedValue?: string | null | undefined;
selectedFiles: SelectedFile[];
}
interface LiElementProps extends React.LiHTMLAttributes<HTMLLIElement> {
'data-file'?: string;
'data-convert-to'?: string;
}
export default function DropDownListForImageConvert({
children,
value,
callBack,
translatedValue,
selectedFiles
}: DropDownListProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const t = useTranslations('Global')
useClickOutside(dropdownRef,
() => {
setIsOpen(false);
},
isOpen
);
const getDisplayValue = (): string => {
if (translatedValue) return translatedValue;
return value ? value : t('do-not-convert');
};
const findFileByName = (fileName: string): SelectedFile | undefined => {
return selectedFiles.find(file => file.name === fileName);
};
const enhancedChildren = React.Children.map(children, (child, index) => {
if (React.isValidElement<LiElementProps>(child)) {
const fileName = child.props['data-file'];
const convertTo = child.props['data-convert-to'];
const isCurrent = value === convertTo;
return React.cloneElement(child, {
onClick: () => {
if (fileName && convertTo) {
const file = findFileByName(fileName);
if (file) {
callBack({ file, convertTo });
} else {
console.error(`Файл с именем ${fileName} не найден`);
}
setIsOpen(false);
}
},
className: `dropdown-item ${isCurrent ? "current" : ""}`,
key: index,
});
}
return child;
});
return (
<div className="dropdown-wrapper" ref={dropdownRef}>
<button
onClick={() => {
setIsOpen(!isOpen);
}}
className="dropdown-button"
>
<span className="flex items-center">
<span className="mr-2">
{getDisplayValue()}
</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && (
<div className="dropdown-list">
<ul
className=""
role="listbox"
>
{enhancedChildren}
</ul>
</div>
)}
</div>
);
}
+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>;
}
};
+43 -15
View File
@@ -7,20 +7,50 @@ import { useState, useRef, useEffect } from 'react'
import { useClickOutside } from '@/app/hooks/useClickOutside'; import { useClickOutside } from '@/app/hooks/useClickOutside';
export default function LanguageSwitcher() { export default function LanguageSwitcher() {
const locale = useLocale() const locale = useLocale();
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null) const dropdownRef = useRef<HTMLDivElement>(null);
const switchLanguage = (newLocale: string) => { const switchLanguage = (newLocale: string) => {
const currentLang = document.documentElement.getAttribute('lang') || 'ru';
router.replace(pathname, { locale: newLocale });
setIsOpen(false);
const observer = new MutationObserver((mutations) => {
const newLang = document.documentElement.getAttribute('lang')
if (newLang && newLang !== currentLang) {
observer.disconnect();
if (window.location.protocol === 'http:' && process.env.NODE_ENV !== 'development') {
setTimeout(() => window.location.reload(), 100)
}
}
})
if (window.location.protocol === 'http:' && process.env.NODE_ENV !== 'development') {
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['lang', 'dir']
})
}
router.replace(pathname, { locale: newLocale }) router.replace(pathname, { locale: newLocale })
setIsOpen(false)
setTimeout(() => {
observer.disconnect();
}, 3000)
} }
useClickOutside(dropdownRef, () => { useClickOutside(
setIsOpen(false); dropdownRef,
}); () => {
setIsOpen(false);
},
isOpen
);
useEffect(() => { useEffect(() => {
const handleEscape = (event: KeyboardEvent) => { const handleEscape = (event: KeyboardEvent) => {
@@ -32,12 +62,10 @@ export default function LanguageSwitcher() {
}, []); }, []);
return ( return (
<div className="relative" ref={dropdownRef}> <div className="language-switcher" ref={dropdownRef}>
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between w-full px-4 py-2 text-sm font-medium text-[#6366f1] bg-white border-2 border-[#6366f1] rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" className="switcher-btn"
aria-haspopup="listbox"
aria-expanded={isOpen}
> >
<span className="flex items-center"> <span className="flex items-center">
<span className="mr-2">{locale === 'ru' ? 'РУ' : locale.toUpperCase()}</span> <span className="mr-2">{locale === 'ru' ? 'РУ' : locale.toUpperCase()}</span>
@@ -57,7 +85,7 @@ export default function LanguageSwitcher() {
</button> </button>
{isOpen && ( {isOpen && (
<div className="absolute right-0 z-10 w-full mt-2 origin-top-right bg-white rounded-md shadow-lg border-2 border-[#6366f1] focus:outline-none"> <div className="switcher-list">
<ul <ul
className="py-1" className="py-1"
role="listbox" role="listbox"
@@ -67,7 +95,7 @@ export default function LanguageSwitcher() {
<li key={lang} role="option"> <li key={lang} role="option">
<button <button
onClick={() => switchLanguage(lang)} onClick={() => switchLanguage(lang)}
className={`flex items-center w-full px-4 py-2 text-sm text-left hover:bg-blue-100 ${locale === lang ? 'bg-blue-50 text-blue-600 font-medium' : 'text-[#6366f1]' className={`language-btn ${locale === lang ? 'active' : ''
}`} }`}
role="menuitem" role="menuitem"
> >
+29
View File
@@ -0,0 +1,29 @@
import React, { ReactNode } from 'react';
interface ModalWindowtProps {
children: ReactNode | null;
state: boolean;
callBack: (value: boolean) => void;
}
export default function ModalWindow({ children, state, callBack }: ModalWindowtProps) {
return (
<>
{state && (
<div
className="modal-wrapper"
onClick={() => {
callBack(false);
}}
>
<div
className="modal-window"
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
)}
</>
)
}
@@ -0,0 +1,103 @@
'use client'
import { useTranslations } from 'next-intl'
interface NotificationTextHandlerProps {
notificationType: string
notificationMessage: string
notificationContext: Record<string, string>
}
export function NotificationTextHandler({
notificationType,
notificationMessage,
notificationContext
}: NotificationTextHandlerProps) {
const t = useTranslations('Notifications');
const getNotificationText = () => {
if (!notificationType) {
return t.has(notificationMessage) ? t(notificationMessage) : notificationMessage
}
switch (notificationType) {
case 'USER_VERIFIED':
return t('user-verified', {
status: notificationContext?.status || t('not-specified'),
updateDate: notificationContext?.update_date || t('not-specified-date')
});
case 'MONITORING_RESULT':
return t('monitoring-result', {
message: notificationContext?.message || t('message-missing')
});
case 'COMPLAINT_STATUS_CHANGED':
return t('complaint-status-changed', {
newStatus: notificationContext?.new_status || t('not-specified'),
oldStatus: notificationContext?.old_status || t('not-specified'),
violation: notificationContext?.violation || t('not-specified'),
file: notificationContext?.file || t('not-specified')
});
case 'FILE_ADDED_TO_SYSTEM':
return t('file-added-to-system', {
status: notificationContext?.status || t('not-specified'),
fileName: notificationContext?.file_name || t('not-specified-file-name')
});
case 'FILE_MODERATION_EVENT':
return t('file-moderation-event', {
fileName: notificationContext?.file_name || t('not-specified-file-name'),
oldStatus: notificationContext?.old_status || t('not-specified'),
newStatus: notificationContext?.new_status || t('not-specified'),
comment: notificationContext?.comment || t('no-comment')
});
case 'PAYMENT_RESULT':
return t('payment-result', {
status: notificationContext?.status || t('not-specified'),
amount: notificationContext?.amount || '0'
});
case 'REFERRAL_REGISTERED':
return t('referral-registered', {
referralUser: notificationContext?.referral_user || t('unknown-user')
});
case 'SEARCH_RESULT':
return t('search-result', {
startDate: notificationContext?.start_date || t('not-specified-date'),
endDate: notificationContext?.end_date || t('not-specified-date'),
searchType: notificationContext?.search_type || t('not-specified'),
count: notificationContext?.count || '0'
});
case 'SEARCH_OPERATION_FAILED':
return t('search-operation-failed', {
status: notificationContext?.status || t('not-specified'),
errorMessage: notificationContext?.error_message || t('error-not-described'),
message: notificationContext?.message || t('message-missing'),
reason: notificationContext?.reason || t('reason-not-specified'),
nextRun: notificationContext?.next_run || t('not-scheduled')
});
case 'TOKEN_NOT_FOUND':
return t('token-not-found', {
currentTokens: notificationContext?.current_tokens || '0',
needTokens: notificationContext?.need_tokens || t('not-specified'),
message: notificationContext?.message || t('message-missing'),
nextRun: notificationContext?.next_run || t('not-scheduled')
});
default:
return notificationMessage || t('notification-without-text');
}
};
return (
<div>
{getNotificationText()}
</div>
);
}
+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>
);
}
+57
View File
@@ -0,0 +1,57 @@
'use client'
import { useTranslations } from 'next-intl';
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
export const PieChartComponent = ({ data, show }: {
data: {
name: string,
value: number,
color: string
}[],
show: number | string
}) => {
const t = useTranslations('Global');
return (
<div
className="flex flex-col items-center"
>
<ResponsiveContainer width={200} height={200}>
<PieChart>
<text x={100} y={75} className="pie-char-text">
{show}
</text>
<text x={100} y={100} className="pie-char-text">
{t('out-of')}
</text>
<text x={100} y={125} className="pie-char-text">
0
</text>
<Pie
data={data}
cx="50%"
cy="50%"
innerRadius={65}
outerRadius={100}
paddingAngle={0}
dataKey="value"
stroke="none"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} stroke="none" />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
<div className="flex gap-6 mt-4">
{data.map((entry, index) => (
<div key={`cell-${index}`} className="">
<span className="w-2 h-2 rounded-full font-bold inline-block mr-1.5" style={{ backgroundColor: entry.color }}></span>
<span>{t(entry.name)}</span>
</div>
))}
</div>
</div>
)
};
+230
View File
@@ -0,0 +1,230 @@
'use client'
import { useTranslations, useLocale } from 'next-intl';
import { useMemo } from 'react';
import { BarChart, Bar, Cell, XAxis, Tooltip } from 'recharts';
import { pluralize } from '@/app/lib/pluralize';
import { useStoreWithDevtools } from '@/app/stores/useStoreWithDevtools';
type Files = {
data: {
name: string,
checks: number,
violations: number
}[]
}
interface BarShapeProps {
x: number;
y: number;
width: number;
height: number;
fill: string;
radius?: number[];
}
export const StackedBarChart = ({ data }: Files) => {
const locale = useLocale();
const t = useTranslations('Global');
const CHECKS_COLOR = '#6366f1';
const VIOLATIONS_COLOR = '#f08c00'
const BORDER_RADIUS = 5;
const STROKE_COLOR = "#dfdede24";
const STROKE_WIDTH = 1;
/* const { totalChecks, totalViolations } = useMemo(() => ({
totalChecks: data.reduce((sum, item) => sum + (item.checks || 0), 0),
totalViolations: data.reduce((sum, item) => sum + (item.violations || 0), 0)
}), [data]); */
const totalChecks = useStoreWithDevtools(s => s.value1);
const totalViolations = useStoreWithDevtools(s => s.value2);
const renderBarWithPartialBorder = (props: BarShapeProps) => {
const { x, y, width, height, fill } = props;
return (
<>
{/* Основной прямоугольник с закругленными углами */}
<rect
x={x}
y={y}
width={width}
height={height}
fill={fill}
rx={BORDER_RADIUS}
ry={BORDER_RADIUS}
/>
{/* Левая граница (центральная часть, без закруглений) */}
<line
x1={x}
y1={y + BORDER_RADIUS}
x2={x}
y2={y + height - BORDER_RADIUS}
/>
{/* Правая граница (центральная часть, без закруглений) */}
<line
x1={x + width}
y1={y + BORDER_RADIUS}
x2={x + width}
y2={y + height - BORDER_RADIUS}
/>
{/* Нижняя граница (прямая часть между закруглениями) */}
<line
x1={x + BORDER_RADIUS}
y1={y + height}
x2={x + width - BORDER_RADIUS}
y2={y + height}
/>
{/* Верхняя граница (прямая часть между закруглениями) */}
<line
x1={x + BORDER_RADIUS}
y1={y}
x2={x + width - BORDER_RADIUS}
y2={y}
/>
</>
);
};
const renderBarWithTopBorder = (props: BarShapeProps) => {
const { x, y, width, height, fill } = props;
if (height <= 0 || width <= 0) return null;
return (
<>
{/* Основной прямоугольник с закругленными углами */}
<rect
x={x}
y={y}
width={width}
height={height}
fill={fill}
rx={BORDER_RADIUS}
ry={BORDER_RADIUS}
/>
{/* Левая граница (центральная часть, без закруглений) */}
<line
x1={x}
y1={y + BORDER_RADIUS}
x2={x}
y2={y + height - BORDER_RADIUS}
stroke={STROKE_COLOR}
strokeWidth={STROKE_WIDTH}
/>
{/* Правая граница (центральная часть, без закруглений) */}
<line
x1={x + width}
y1={y + BORDER_RADIUS}
x2={x + width}
y2={y + height - BORDER_RADIUS}
stroke={STROKE_COLOR}
strokeWidth={STROKE_WIDTH}
/>
{/* Нижняя граница (прямая часть между закруглениями) */}
<line
x1={x + BORDER_RADIUS}
y1={y + height}
x2={x + width - BORDER_RADIUS}
y2={y + height}
stroke={STROKE_COLOR}
strokeWidth={STROKE_WIDTH}
/>
{/* Верхняя граница (прямая часть между закруглениями) */}
<line
x1={x + BORDER_RADIUS}
y1={y}
x2={x + width - BORDER_RADIUS}
y2={y}
stroke={STROKE_COLOR}
strokeWidth={STROKE_WIDTH}
/>
</>
);
};
const pluralizeCheks = (number: number) => {
const translate = [t('check'), t('checks-few'), t('checks')];
return pluralize(number, translate[0], translate[1], translate[2], locale);
};
const pluralizeViolations = (number: number) => {
const translate = [t('violation'), t('violations-few'), t('violations')];
return pluralize(number, translate[0], translate[1], translate[2], locale);
};
return (
<>
<div className="stacked-bar-chart-wrapper">
<div className="stacked-bar-chart">
<BarChart
style={{ width: '100%', maxWidth: '600px', maxHeight: '210px', aspectRatio: 1.618 }}
responsive
data={data}
barCategoryGap={15}
margin={{
top: 20,
right: 0,
left: 0,
bottom: 5,
}}
>
{/* <Tooltip /> */}
<XAxis
dataKey="name"
stroke={CHECKS_COLOR}
fontSize={12}
axisLine={false}
/>
<Bar
dataKey="checks"
stackId="a"
label={{ position: 'top', fill: CHECKS_COLOR, fontSize: 12 }}
radius={5}
shape={renderBarWithPartialBorder as any}
>
{data.map((_entry, index) => (
<Cell key={`cell-${index}`} fill={CHECKS_COLOR} />
))}
</Bar>
<Bar
dataKey="violations"
stackId="b"
label={{ position: 'top', fill: CHECKS_COLOR, fontSize: 12 }}
radius={5}
shape={renderBarWithTopBorder as any}
>
{data.map((_entry, index) => (
<Cell key={`cell-${index}`} fill={VIOLATIONS_COLOR} />
))}
</Bar>
</BarChart>
</div>
<div className="protection-stat-total-info">
<div
className={`font-bold`}
style={{ color: CHECKS_COLOR }}
>
{totalChecks} {pluralizeCheks(totalChecks)}
</div>
<div
className={`font-bold`}
style={{ color: VIOLATIONS_COLOR }}
>
{totalViolations} {pluralizeViolations(totalViolations)}
</div>
</div>
</div>
</>
);
};
+38
View File
@@ -0,0 +1,38 @@
'use client'
import { useStoreWithDevtools } from '@/app/stores/useStoreWithDevtools';
import { useTranslations } from 'next-intl';
export function TestComponent() {
const t = useTranslations('Global');
const setText1 = useStoreWithDevtools(s => s.setValue1);
const setText2 = useStoreWithDevtools(s => s.setValue2);
const text1 = useStoreWithDevtools(s => s.value1);
const text2 = useStoreWithDevtools(s => s.value2);
return (
<div className="mb-4">
<div className='mb-2'>
<input
className='border rounded-md mr-1'
type="text"
onChange={(e) => {
setText1(Number(e.target.value))
}}
/>
<span>{t('check')} {text1}</span>
</div>
<div>
<input
className='border rounded-md mr-1'
type="text"
onChange={(e) => {
setText2(Number(e.target.value))
}}
/>
<span>{t('violation')} {text2}</span>
</div>
</div>
)
}
+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>
);
}
+148
View File
@@ -0,0 +1,148 @@
'use client'
import { useEffect, useState } from 'react';
import * as VKID from '@vkid/sdk';
import { vkAuthorization } from '@/app/actions/vkAuth';
export default function VKLogin() {
const [codeVerifier, setCodeVerifier] = useState<string>('');
const [codeChallenge, setCodeChallenge] = useState<string>('');
const [state, setState] = useState<string>('');
async function generateAuthString(minLength: number = 43, maxLength: number = 128) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;
let result = '';
const charsLength = chars.length;
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
const randomValues = new Uint32Array(length);
crypto.getRandomValues(randomValues);
for (let i = 0; i < length; i++) {
result += chars.charAt(randomValues[i] % charsLength);
}
} else {
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * charsLength));
}
}
return result;
}
async function generateCodeChallenge(codeVerifier: string) {
try {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const base64 = btoa(String.fromCharCode(...new Uint8Array(digest)));
const base64url = base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return base64url;
} catch (error) {
console.error('Error generating code challenge:', error);
throw error;
}
}
async function clickHandler() {
console.log('clickHandler');
console.log('codeChallenge:', codeChallenge);
alert("Кнопка отключена");
return;
try {
if (!codeVerifier) {
const newCodeVerifier = await generateAuthString(43, 128);
const newCodeChallenge = await generateCodeChallenge(newCodeVerifier);
setCodeVerifier(newCodeVerifier);
setCodeChallenge(newCodeChallenge);
VKID.Config.update({
codeChallenge: newCodeChallenge
});
}
const data = await VKID.Auth.login();
vkidOnSuccess(data);
} catch (error) {
console.error('Authorization error:', error);
}
}
async function vkidOnSuccess(data: any) {
console.log('vkidOnSuccess');
console.log('vk data:', data);
console.log('codeVerifier :', codeVerifier);
vkAuthorization(data, codeVerifier);
const newCodeVerifier = await generateAuthString(43, 128);
const newCodeChallenge = await generateCodeChallenge(newCodeVerifier);
setCodeVerifier(newCodeVerifier);
setCodeChallenge(newCodeChallenge);
VKID.Config.update({
codeChallenge: newCodeChallenge
});
console.log('new codeChallenge:', newCodeChallenge);
console.log('state: ', state)
}
useEffect(() => {
return; //выключил кнопку пока нету бизне аккаунта, что бы не было ошибко на странице.
async function initVKID() {
try {
const newCodeVerifier = await generateAuthString(43, 128);
const newCodeChallenge = await generateCodeChallenge(newCodeVerifier);
const newState = await generateAuthString(32, 128);
setState(newState);
setCodeVerifier(newCodeVerifier);
setCodeChallenge(newCodeChallenge);
VKID.Config.init({
app: 54389559,
redirectUrl: 'http://localhost',
responseMode: VKID.ConfigResponseMode.Callback,
source: VKID.ConfigSource.LOWCODE,
scope: 'email phone vkid.personal_info',
codeChallenge: newCodeChallenge,
state: newState
});
console.log('Generated codeVerifier:', newCodeVerifier);
console.log('Generated codeChallenge:', newCodeChallenge);
} catch (error) {
console.error('Error initializing VKID:', error);
}
}
initVKID();
}, []);
return (
<button id="VKIDSDKAuthButton" className="VkIdWebSdk__button VkIdWebSdk__button_reset">
<div className="VkIdWebSdk__button_container">
<div className="VkIdWebSdk__button_icon">
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M4.54648 4.54648C3 6.09295 3 8.58197 3 13.56V14.44C3 19.418 3 21.907 4.54648 23.4535C6.09295 25 8.58197 25 13.56 25H14.44C19.418 25 21.907 25 23.4535 23.4535C25 21.907 25
19.418 25 14.44V13.56C25 8.58197 25 6.09295 23.4535 4.54648C21.907 3 19.418 3 14.44 3H13.56C8.58197 3 6.09295 3 4.54648 4.54648ZM6.79999 10.15C6.91798 15.8728 9.92951 19.31 14.8932 19.31H15.1812V16.05C16.989 16.2332 18.3371
17.5682 18.8875 19.31H21.4939C20.7869 16.7044 18.9535 15.2604 17.8141 14.71C18.9526 14.0293 20.5641 12.3893 20.9436 10.15H18.5722C18.0747 11.971 16.5945 13.6233 15.1803 13.78V10.15H12.7711V16.5C11.305 16.1337 9.39237 14.3538 9.314 10.15H6.79999Z" fill="white" />
</svg>
</div>
<div className="VkIdWebSdk__button_text" onClick={clickHandler}>
Войти с VK ID
</div>
</div>
</button>
)
}
+69
View File
@@ -0,0 +1,69 @@
"use client";
import Script from 'next/script';
import { useEffect, useRef, useState } from 'react';
declare global {
interface Window {
YaAuthSuggest: any;
}
}
export default function YandexAuthWithScript() {
const buttonRef = useRef<HTMLDivElement>(null);
const [isReady, setIsReady] = useState(false);
const [showButton, setShowButton] = useState(true);
useEffect(() => {
if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return;
window.YaAuthSuggest.init(
{
client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
response_type: 'token',
redirect_uri: `https://fallibly-desired-moonfish.cloudpub.ru/en/login`,
},
'https://fallibly-desired-moonfish.cloudpub.ru',
{
parentId: buttonRef.current.id,
view: 'button',
buttonView: 'main',
buttonTheme: 'light',
buttonSize: 'l',
buttonBorderRadius: 10
}
)
.then(function (result: any) {
console.log('you at least try?')
console.log(result);
return result.handler();
})
.then(function (data: any) {
console.log('Сообщение с токеном: ', data);
})
.catch(function (error: any) {
console.log('Что-то пошло не так: ', error);
});
}, [isReady]);
return (
<>
<Script
src="https://yastatic.net/s3/passport-sdk/autofill/v1/sdk-suggest-with-polyfills-latest.js"
strategy="lazyOnload"
onLoad={() => setIsReady(true)}
onError={() => console.error('Failed to load Yandex script')}
/>
{showButton && (
<div
ref={buttonRef}
id="yandex-auth-container"
className="flex justify-center"
>
{!isReady && <div>Загрузка Яндекс авторизации...</div>}
</div>
)}
</>
);
}
+15
View File
@@ -0,0 +1,15 @@
export function YandexLoginButton() {
const clientId = 'b356a7ceac0f4c50a3b434220f86bce0';
const redirectUri = encodeURIComponent('https://fallibly-desired-moonfish.cloudpub.ru/login/callback');
const authUrl = `https://oauth.yandex.ru/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&force_confirm=yes`;
return (
<a
href={authUrl}
className="yandex-auth-button"
>
Войти через Яндекс
</a>
);
}
+99
View File
@@ -0,0 +1,99 @@
import { useCallback, useEffect } from 'react';
import {
YooMoneyCheckoutWidget,
YooMoneyCheckoutWidgetConfig,
YooMoneyErrorCallbackResult
} from "types-yoomoneycheckoutwidget";
let isLoading = false;
let isInited = false;
export default function YooWidget({ config, onComplete, onSuccess, onFail, onModalClose }: {
/**
* Описание https://yookassa.ru/developers/payment-acceptance/integration-scenarios/widget/reference#initialization-parameters
*/
config: YooMoneyCheckoutWidgetConfig,
/** после успешной оплаты*/
onComplete?: () => void,
/** после успешной оплаты, только если не подписались на onComplete*/
onSuccess?: () => void,
/** только если вы отключили обработку неуспешных попыток в виджете: пользователь выбрал любой способ оплаты, но платеж не прошел; закончился срок действия токена.*/
onFail?: () => void,
/** после закрытия модального окна платежной формы если customization.modal==true */
onModalClose?: () => void,
}) {
const isModal = config?.customization?.modal;
let checkout: YooMoneyCheckoutWidget;
const destroy = () => {
console.log('checkout.destroy');
checkout.destroy();
isInited = false;
}
const initializeWidget = useCallback(
() => {
isInited = true;
isLoading = false;
if (!isModal) {
const userErrorCallBack = config.error_callback;
config.error_callback = (error: YooMoneyErrorCallbackResult) => {
userErrorCallBack(error);
isInited = false;
};
}
checkout = new window.YooMoneyCheckoutWidget(config);
checkout.on('complete', (result) => {
console.log('on complete', result);
onComplete && onComplete();
destroy();
});
onSuccess && checkout.on('success', (result) => {
console.log('on success', result);
onSuccess();
destroy();
});
onFail && checkout.on('fail', (result) => {
console.warn('on fail', result);
onFail();
destroy();
});
if (config.customization?.modal) {
checkout.on('modal_close', (result) => {
onModalClose && onModalClose();
destroy();
});
}
checkout.render(config.customization?.modal ? undefined : 'payment-form');
}, [config, onSuccess]);
useEffect(() => {
if (!isLoading && !isInited) {
if (!window.YooMoneyCheckoutWidget) {
isLoading = true;
const script = document.createElement('script');
script.src = 'https://yookassa.ru/checkout-widget/v1/checkout-widget.js';
script.async = true;
script.onload = initializeWidget;
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
} else {
initializeWidget();
}
}
return () => {
if (!isInited && !isModal) {
destroy();
}
}
});
return (
<>
{isModal ? '' : <div id="payment-form" />}
</>
);
}
@@ -0,0 +1,30 @@
'use client';
import { toast } from 'sonner';
interface CopyLinkButtonProps {
className?: string;
children?: React.ReactNode;
}
export function CopyLinkButton({ className = 'btn btn-out', children = 'Скопировать ссылку' }: CopyLinkButtonProps) {
const copyCurrentPageLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
toast.success('Ссылка скопирована');
} catch (err) {
toast.error('Не удалось скопировать ссылку');
}
};
return (
<button
className={className}
onClick={copyCurrentPageLink}
type="button"
>
{children}
</button>
);
}
@@ -0,0 +1,50 @@
'use client';
import { downloadTrackingFile } from '@/app/actions/trackingActions';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
interface DownloadButtonProps {
fileId: string;
fileName?: string;
}
export function DownloadButton({ fileId, fileName }: DownloadButtonProps) {
const t = useTranslations('Global');
const handleDownload = async () => {
try {
const result = await downloadTrackingFile(fileId, fileName);
if (!result) {
throw 'failed-to-download-file'
}
const byteCharacters = atob(result.data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: result.contentType });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - `);
} catch (error) {
toast.error(t('failed-to-download-file'));
}
};
return (
<button className="btn btn-out" onClick={handleDownload}>
Скачать документ
</button>
);
}
@@ -0,0 +1,18 @@
'use client';
import Link from 'next/link';
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
export function LoginUserBage() {
const { data: userData, isLoading, isError, error } = useUserProfile();
if (!userData?.fullName) {
return null;
}
return (
<Link href="#" className="header-user">
{userData?.fullName}
</Link>
);
}
@@ -0,0 +1,160 @@
'use client'
import { FileItem } from '@/app/components/tanstak-table/TanstakTable';
import { useEffect, useState, useRef } from 'react';
import { useTranslations } from 'next-intl';
import { useQueryClient } from '@tanstack/react-query';
import { createPortal } from 'react-dom';
import { setMonitoring } from '@/app/actions/monitoringActions';
import { toast } from 'sonner';
export function MonitoringDropDown({ file }: {
file: FileItem
}) {
const [isOpen, setIsOpen] = useState(false);
const [currentMonitoringStatus, setCurrentMonitoringStatus] = useState(file.monitoring);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const t = useTranslations('Monitoring-status');
const queryClient = useQueryClient();
useEffect(() => {
if (!isOpen) return;
const handleClick = (event: MouseEvent) => {
if (!dropdownRef?.current) {
return;
}
if (!buttonRef?.current) {
return;
}
if (buttonRef?.current.contains(event.target as Node)) {
return;
}
if (!dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [dropdownRef, setIsOpen, isOpen]);
useEffect(() => {
if (file.monitoring) {
setCurrentMonitoringStatus(file.monitoring ? file.monitoring : 'NONE');
}
}, [file.monitoring]);
useEffect(() => {
if (isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setDropdownPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
width: rect.width
});
}
}, [isOpen]);
async function clickHandler(monitoring: string) {
const response = await setMonitoring(file.id, monitoring);
if (response.success) {
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
setIsOpen(false);
toast.success(t('the-file-is monitored', { fileName: file.fileName }));
setCurrentMonitoringStatus(monitoring);
} else {
toast.warning(t('failed-to-establish-monitoring'));
}
}
return (
<div className="dropdown-wrapper monitoring-dropdown">
<button
ref={buttonRef}
onClick={() => {
setIsOpen(!isOpen);
}}
className="dropdown-button"
>
<span className="flex items-center">
<span className="mr-2">
{t(currentMonitoringStatus)}
</span>
</span>
<svg
className={`w-5 h-5 ml-2 transition-transform ${isOpen ? 'rotate-180' : ''}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{isOpen && createPortal(
<div
className="dropdown-list-portal"
ref={dropdownRef}
style={{
position: 'absolute',
top: dropdownPosition.top,
left: dropdownPosition.left,
width: dropdownPosition.width,
zIndex: 99999
}}
>
<ul className="dropdown-list" role="listbox">
<li
className={`dropdown-item ${currentMonitoringStatus === 'NONE' ? 'current' : ''}`}
onClick={() => {
clickHandler('NONE')
}}
>
{t('NONE')}
</li>
<li
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_DAILY' ? 'current' : ''}`}
onClick={() => {
clickHandler('MONITORING_DAILY')
}}
>
{t('MONITORING_DAILY')}
</li>
<li
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_WEEKLY' ? 'current' : ''}`}
onClick={() => {
clickHandler('MONITORING_WEEKLY')
}}
>
{t('MONITORING_WEEKLY')}
</li>
<li
className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_MONTHLY' ? 'current' : ''}`}
onClick={() => {
clickHandler('MONITORING_MONTHLY')
}}
>
{t('MONITORING_MONTHLY')}
</li>
</ul>
</div>,
document.body
)}
</div>
)
}
@@ -0,0 +1,57 @@
'use client'
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export function SelectedFilesAction({ filesId, callBack }: { filesId: Set<string>, callBack: (value: Set<string>) => void }) {
const t = useTranslations('Global');
if (filesId.size === 0) {
return null;
}
function downloadHandler() {
if (filesId.size) {
const filesArray = Array.from(filesId);
const filesString = filesArray.join(', ');
toast.success(`${t('download')}: ${filesString}`)
}
}
function clearHandler() {
callBack(new Set());
}
return (
<div
className="selected-files-wrapper"
>
<div
className="selected-files-count"
>
{t('selected-files')}: {filesId.size}
</div>
<div
className="selected-files-action"
>
<button
className="btn btn-primary"
onClick={() => {
downloadHandler();
}}
>
{t('download-all')}
</button>
<button
className="btn btn-primary"
onClick={() => {
clearHandler();
}}
>
{t('deselect')}
</button>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
'use client';
import { createContext, useContext, useRef, ReactNode } from 'react';
interface PdfContextType {
downloadPdf: () => Promise<void>;
setPdfData: (data: ArrayBuffer | null, fileName: string) => void;
}
const PdfContext = createContext<PdfContextType | null>(null);
export function PdfProvider({ children }: { children: ReactNode }) {
const pdfDataRef = useRef<ArrayBuffer | null>(null);
const fileNameRef = useRef<string>('');
const setPdfData = (data: ArrayBuffer | null, fileName: string) => {
pdfDataRef.current = data;
fileNameRef.current = fileName;
};
const downloadPdf = async () => {
if (!pdfDataRef.current) {
console.log('No PDF data available');
return;
}
try {
const blob = new Blob([pdfDataRef.current], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileNameRef.current || 'document.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
} catch (err) {
console.log('Error downloading PDF:', err);
}
};
return (
<PdfContext.Provider value={{ downloadPdf, setPdfData }}>
{children}
</PdfContext.Provider>
);
}
export function usePdf() {
const context = useContext(PdfContext);
if (!context) {
throw new Error('usePdf must be used within PdfProvider');
}
return context;
}
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchActiveNotifications, NotificationBody } from '@/app/actions/notificationActions';
export interface UseActiveNotifications {
notifications: NotificationBody[],
unreadCount: number
}
export const useActiveNotifications = () => {
return useQuery({
queryKey: ['activeNotifications'],
queryFn: () => {
return fetchActiveNotifications()
},
select: (data: UseActiveNotifications | null) => {
if (!data) {
return {
notifications: [],
unreadCount: 0
}
}
return data;
},
retry: false,
refetchInterval: 10000
});
};
@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import { getAllFilesExtensions } from '@/app/lib/prefetch-queries';
export interface allFilesExtensions {
videos: string[];
audios: string[];
images: string[];
documents: string[];
maxFileSize: number;
}
const EMPTY_EXTENSIONS: allFilesExtensions = {
videos: [],
audios: [],
images: [],
documents: [],
maxFileSize: 0
};
export const useAllFilesExtensions = () => {
const query = useQuery({
queryKey: ['allFilesExtensions'],
queryFn: getAllFilesExtensions,
retry: false,
initialData: EMPTY_EXTENSIONS
});
return {
...query,
data: query.data ?? EMPTY_EXTENSIONS
};
};
@@ -0,0 +1,31 @@
import { useQuery } from '@tanstack/react-query';
import { fetchAllNotifications, NotificationBody } from '@/app/actions/notificationActions';
export interface UseAllNotifications {
notifications: NotificationBody[],
currentPage: number,
totalElements: number,
totalPages: number,
}
export const useAllNotifications = (page: number, size: number = 10) => {
return useQuery({
queryKey: ['allNotifications', page, size],
queryFn: () => {
return fetchAllNotifications(page, size)
},
select: (data: UseAllNotifications | null) => {
if (!data) {
return {
notifications: [],
currentPage: 0,
totalElements: 0,
totalPages: 0,
}
}
return data;
},
retry: false,
refetchInterval: 10000
});
};
@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
import { getFileMatches } from '@/app/actions/violationActions';
export interface ViolationFileDetail {
id: number;
url: string;
page_url: string;
page_title: string;
host: string;
status: string;
created_date: string;
file_id: string;
country: string;
country_code: string;
}
interface ViolationFile {
violations: ViolationFileDetail[];
total_elements: number;
total_pages: number;
current_page: number;
page_size: number;
has_next: boolean;
has_previous: boolean;
}
export const useFileViolations = (id: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: string,) => {
return useQuery({
queryKey: ['fileViolations', id, page, status, violationId, startDate, endDate],
queryFn: () => {
return getFileMatches(id, page, status, violationId, startDate, endDate)
},
select: (data: ViolationFile | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,39 @@
import { useQuery } from '@tanstack/react-query';
import { getUserPayments } from '@/app/actions/referralsActions';
export interface ReferralPayments {
amount: number,
completedAt: string | null,
createdAt: string,
id: number,
processAt: string | null
rejectionReason: string | null,
status: string,
updatedAt: string,
}
export interface UserReferralPayments {
payments: ReferralPayments[],
error: string | null
}
export const useGetReferralPayments = () => {
return useQuery({
queryKey: ['referralPayments'],
queryFn: getUserPayments,
select: (data): UserReferralPayments => {
if (!data) {
return {
payments: [],
error: 'error'
}
}
return {
payments: data.payments,
error: data.error
}
},
retry: false
});
};
@@ -0,0 +1,44 @@
import { useQuery } from '@tanstack/react-query';
import { fetchHistoryView } from '@/app/actions/trackingActions';
export interface HistoryViewItem {
city: string;
country: string;
viewer: string;
document: string;
view_date: string;
}
export interface UseHistoryView {
documentsCount: number;
history: HistoryViewItem[];
pageNumber: number;
pageSize: number;
totalElements: number;
totalPages: number;
uniqueCityCount: number;
uniqueCountryCount: number;
}
export const useHistoryView = (
page: number,
size: number,
locale: string,
sortDirection?: 'asc' | 'desc',
sortBy?: string
) => {
return useQuery({
queryKey: ['violationStatistic', page, size, locale, sortDirection, sortBy],
queryFn: () => {
return fetchHistoryView(page, size, locale, sortDirection, sortBy)
},
select: (data: UseHistoryView | null) => {
if (!data) {
return null
}
return data;
},
retry: false,
placeholderData: (previousData) => previousData // для оптимистичных обновлений
});
};
+45
View File
@@ -0,0 +1,45 @@
import { useQuery } from '@tanstack/react-query';
import { fetchLastClaims, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
export interface Case {
amount: number;
content: null | string;
created_at: string;
description: string;
id: number;
lawyer: null | string;
name: string;
page_number: number;
page_size: number;
priority: "HIGH" | "MEDIUM" | "LOW";
total_elements: number;
total_pages: number;
type: "ACTIVE" | "INACTIVE" | string;
updated_at: string;
violation_id: number;
file_id: string;
}
export interface useLastCase {
content: Case[],
page: number,
size: number,
totalElements: number,
totalPages: number,
}
export const useLastCases = (params: ComplaintsCaseQueryParams) => {
return useQuery({
queryKey: ['lastCases', params.page, params.size, params.sort_by, params.sort_direction],
queryFn: () => {
return fetchLastClaims(params)
},
select: (data: useLastCase | null) => {
if (!data) {
return null
}
return data;
},
placeholderData: (previousData) => previousData,
retry: false
});
};
@@ -0,0 +1,38 @@
import { useQuery } from '@tanstack/react-query';
import { fetchLastComplaint, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
export interface Complaint {
complaint_text: string;
created_at: string;
email: string;
id: number;
not_moderated: boolean;
status: "CREATED" | string;
updated_at: string;
violation_id: number;
file_id: string;
}
export interface useLastComplaint {
content: Complaint[],
page: number,
size: number,
totalElements: number,
totalPages: number,
}
export const useLastComplaints = (params: ComplaintsCaseQueryParams) => {
return useQuery({
queryKey: ['lastComplaints', params.page, params.size, params.sort_by, params.sort_direction],
queryFn: () => {
return fetchLastComplaint(params)
},
select: (data: useLastComplaint | null) => {
if (!data) {
return null
}
return data;
},
placeholderData: (previousData) => previousData,
retry: false
});
};
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMonitoringInfo } from '@/app/actions/monitoringActions';
interface MonitoringCount {
day: number;
month: number;
weekly: number;
}
export const useMonitoringCount = () => {
return useQuery({
queryKey: ['monitoringCount'],
queryFn: () => {
return fetchMonitoringInfo()
},
select: (data: MonitoringCount | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMonitoringStats } from '@/app/actions/monitoringActions';
interface MonitoringStats {
total_searches: number;
searches_by_status: {
processing: number;
completed: number;
failed: number;
total: number;
};
recent_searches: any[];
}
export const useMonitoringStats = () => {
return useQuery({
queryKey: ['monitoringStats'],
queryFn: () => {
return fetchMonitoringStats()
},
select: (data: MonitoringStats | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { getPaymentsMethods } from '@/app/actions/paymentAction';
export interface PaymentsMethods {
cardType: string,
expiryMonth: string,
expiryYear: string,
lastFour: string,
paymentMethodId: string
}
export const usePaymentsMethods = () => {
return useQuery({
queryKey: ['paymentsMethods'],
queryFn: () => {
return getPaymentsMethods()
},
select: (data: PaymentsMethods[] | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchTrackingStats } from '@/app/actions/trackingActions';
export interface useTrackingStats {
totalViews: number,
uniqueIps: number,
uniqueUsers: number,
documentsCount: number,
uniqueCountryCount: number,
uniqueCityCount: number,
firstViewDate: string,
lastViewDate: string
}
export const useTrackingStats = () => {
return useQuery({
queryKey: ['trackingStats'],
queryFn: () => {
return fetchTrackingStats()
},
select: (data: useTrackingStats | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -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
});
};

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