Compare commits

...
299 Commits
Author SHA1 Message Date
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
178 changed files with 16924 additions and 2853 deletions
+8
View File
@@ -34,3 +34,11 @@ docker network inspect no-copy_app-network
docker volume ls - посмотреть все базы данных docker volume ls - посмотреть все базы данных
docker volume prune - удалить неиспользуемые базы данных docker volume prune - удалить неиспользуемые базы данных
Юкаса
эндпоинт для бека.
{URL}/api/payments/webhook/yookassa
https://dev-workspace.not-copy.com/api/payments/webhook/yookassa
https://mutely-profound-sandfish.cloudpub.ru/api/payments/webhook/yookassa
+289 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.44.0", "version": "0.104.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.44.0", "version": "0.104.0",
"dependencies": { "dependencies": {
"@a2seven/yoo-checkout": "^1.1.4", "@a2seven/yoo-checkout": "^1.1.4",
"@react-input/mask": "^2.0.4", "@react-input/mask": "^2.0.4",
@@ -15,11 +15,13 @@
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2", "@vkid/sdk": "^2.6.2",
"card-validator": "^10.0.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8", "next-intl": "^4.5.8",
"pdfjs-dist": "^5.7.284",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-slick": "^0.31.0", "react-slick": "^0.31.0",
@@ -39,6 +41,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"
} }
}, },
@@ -1127,6 +1130,256 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@napi-rs/canvas": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
"integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.100",
"@napi-rs/canvas-darwin-arm64": "0.1.100",
"@napi-rs/canvas-darwin-x64": "0.1.100",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
"@napi-rs/canvas-linux-arm64-musl": "0.1.100",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
"@napi-rs/canvas-linux-x64-gnu": "0.1.100",
"@napi-rs/canvas-linux-x64-musl": "0.1.100",
"@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
"@napi-rs/canvas-win32-x64-msvc": "0.1.100"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
"integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
"integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
"integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
"integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
"integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
"integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
"integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
"integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
"integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
"integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.100",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
"integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12", "version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -3415,6 +3668,15 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/card-validator": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.4.tgz",
"integrity": "sha512-1Q/tDCSvM5iL3psPo6j8KF/VoI63WA7Y4Zthy1E7l/H4Iul2e2X+1S0vp33n3OMRjLgGU2+gLaK2NsGtwArJkQ==",
"license": "MIT",
"dependencies": {
"credit-card-type": "^10.1.0"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3503,6 +3765,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/credit-card-type": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.1.2.tgz",
"integrity": "sha512-fP9vlyeYSHfKbIdE6IOR63YivTXMy0ZqyEkBhHxuAbvpltepe+zJmQ34A2Va7zJ8J7ZaB5IMAdD+1YJOmsajJA==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -6556,6 +6824,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/pdfjs-dist": {
"version": "5.7.284",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz",
"integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==",
"license": "Apache-2.0",
"engines": {
"node": ">=22.13.0 || >=24"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.100"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -7732,6 +8012,13 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/types-yoomoneycheckoutwidget": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/types-yoomoneycheckoutwidget/-/types-yoomoneycheckoutwidget-0.0.2.tgz",
"integrity": "sha512-Vab30GrWEE5qq65CbBcJW5deFjJ9jRVlmh+ZneaUsqnPKPlWHGwhA1ukx+o70LjqEdgK+HgMkxNlFsqchLX7ww==",
"dev": true,
"license": "MIT"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+4 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.48.0", "version": "0.120.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
@@ -16,11 +16,13 @@
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2", "@vkid/sdk": "^2.6.2",
"card-validator": "^10.0.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"next-intl": "^4.5.8", "next-intl": "^4.5.8",
"pdfjs-dist": "^5.7.284",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-slick": "^0.31.0", "react-slick": "^0.31.0",
@@ -40,6 +42,7 @@
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.0.3", "eslint-config-next": "16.0.3",
"sass": "^1.94.2", "sass": "^1.94.2",
"types-yoomoneycheckoutwidget": "^0.0.2",
"typescript": "^5" "typescript": "^5"
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+5 -3
View File
@@ -1,9 +1,11 @@
import Providers from '@/app/providers/getQueryServer'
import type { Metadata } from "next"; import type { Metadata } from "next";
import { NextIntlClientProvider, hasLocale } from 'next-intl'; import { NextIntlClientProvider, hasLocale } from 'next-intl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing'; import { routing } from '@/i18n/routing';
import { ToastProvider } from '@/app/providers/ToastProvider'; import { ToastProvider } from '@/app/providers/ToastProvider';
import Providers from '@/app/providers/getQueryServer';
import { Suspense } from 'react';
import "../styles/globals.css"; import "../styles/globals.css";
import "../styles/global-styles.scss"; import "../styles/global-styles.scss";
@@ -29,9 +31,9 @@ export default async function RootLayout({
} }
return ( return (
<html lang={locale}> <html>
<body> <body>
<NextIntlClientProvider> <NextIntlClientProvider locale={locale}>
<Providers> <Providers>
{children} {children}
<ToastProvider /> <ToastProvider />
+53
View File
@@ -0,0 +1,53 @@
'use client'
import { useEffect } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import styles from '@/app/styles/module/login.module.scss'
export default function YandexCallback() {
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
const code = searchParams.get('code');
const error = searchParams.get('error');
if (error) {
console.error('Ошибка авторизации:', error);
router.push('/login?error=yandex_denied');
return;
}
if (code) {
console.log('получаю код');
console.log(code);
fetch('/api/auth/yandex/callback', {
method: 'POST',
body: JSON.stringify({ code })
})
.then(res => res.json())
.then(data => {
if (data.success) {
console.log(data);
console.log('all ok');
/* router.push('/dashboard'); */
}
});
}
}, [searchParams, router]);
return (
<div
className={`${styles['login-container-wrapper']}`}
>
<div
className="loading-animation"
>
<div
className="global-spinner large"
>
</div>
</div>
</div>
)
}
+2 -1
View File
@@ -9,6 +9,7 @@ import VKLogin from '@/app/components/VKLogin';
import YandexAuthWithScript from '@/app/components/YandexLogin'; import YandexAuthWithScript from '@/app/components/YandexLogin';
import { getHealt } from '@/app/actions/action'; import { getHealt } from '@/app/actions/action';
import { Suspense } from 'react'; import { Suspense } from 'react';
import {YandexLoginButton} from '@/app/components/YandexLoginButton';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Login', title: 'Login',
@@ -51,7 +52,7 @@ export default function Page() {
<VKLogin /> <VKLogin />
</div> </div>
<div> <div>
<YandexAuthWithScript /> <YandexLoginButton />
</div> </div>
<div className={`${styles['register-link']}`}> <div className={`${styles['register-link']}`}>
<p>{t('no-account')}? <p>{t('no-account')}?
+6 -3
View File
@@ -10,7 +10,7 @@ import DashboardFilesInfo from '@/app/ui/dashboard/new/dashboard-files-info';
import DahboardLimitsSection from '@/app/ui/dashboard/new/dashborad-limits-section'; import DahboardLimitsSection from '@/app/ui/dashboard/new/dashborad-limits-section';
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section'; import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check'; import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check';
import ViolationsCarousel from '@/app/ui/dashboard/new/violations-carousel'; import DashboardUserViolations from '@/app/ui/dashboard/new/dashboard-user-violations';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Dashboard', title: 'Dashboard',
@@ -24,13 +24,16 @@ export default function Page() {
<PageTitle title="statistics" /> <PageTitle title="statistics" />
<DashboardUserStats /> <DashboardUserStats />
<div className="dashboard-main-grid"> <div className="dashboard-main-grid">
{/* <DashboardUserFiles />
<DashboardUserViolations /> */}
<div className="left-column"> <div className="left-column">
<DashboardUserFiles /> <DashboardUserFiles />
{/* <DashboardPlatformsList /> */}
</div> </div>
<div className="right-column"> <div className="right-column">
<ViolationsCarousel /> <DashboardUserViolations />
</div> </div>
</div> </div>
<DashboardFilesInfo /> <DashboardFilesInfo />
<div className="dashboard-main-grid"> <div className="dashboard-main-grid">
+15
View File
@@ -0,0 +1,15 @@
import PageTitle from '@/app/ui/page-title';
import PopularAskedQuestions from '@/app/ui/faq/popular-asked-questions';
import FrequentlyAskedQuestions from '@/app/ui/faq/frequently-asked-questions';
import FaqContact from '@/app/ui/faq/faq-contact';
export default function Page() {
return (
<div>
<PageTitle title="frequently-asked-questions" />
<PopularAskedQuestions />
<FrequentlyAskedQuestions />
<FaqContact />
</div>
)
}
+98
View File
@@ -0,0 +1,98 @@
// app/files/[id]/page.tsx
'use server'
import FilePageTitle from '@/app/ui/file-page/file-page-title';
import FilePageViolationInfo from '@/app/ui/file-page/file-page-violation-info';
import ViolationPpageFileStatistic from '@/app/ui/file-page/file-page-file-statistic';
import FilePageFileInfo from '@/app/ui/file-page/file-page-file-info';
import FilePageActions from '@/app/ui/file-page/file-page-actions';
import FilePageViolationsList from '@/app/ui/file-page/violation-table/file-page-violations-list';
import FilePageNote from '@/app/ui/file-page/file-page-note';
import { viewFileInfo } from '@/app/actions/fileEntity';
import { useTranslations } from 'next-intl';
import FilePageNotFound from '@/app/ui/file-page/file-page-file-not-found';
type MonitoringStatus = 'NONE' | 'MONITORING_DAILY' | 'MONITORING_WEEKLY' | 'MONITORING_MONTHLY';
type MimeType = 'image' | 'video' | 'audio' | 'document';
export interface FileDetails {
id: string;
userId: number;
originalFileName: string;
storedFileName: string;
filePath: string;
protectedFilePath: string;
fileSize: number;
mimeType: MimeType;
fileExtension: string;
checksum: string;
uploadSessionId: string;
status: string;
createdAt: string;
updatedAt: string;
formattedSize: string;
downloadUrl: string;
existsOnDisk: boolean;
supportId: number | null;
protectStatus: string;
ownerName: string;
ownerEmail: string;
ownerCompany: string | null;
fileName: string;
fileFormat: string | null;
checksCount: number;
fileUploadDate: string;
monitoring: MonitoringStatus;
thumbnailFileUrl: string;
}
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ page?: string, status?: string, violationId?: string, caseType?: 'complaint' | 'claim', startDate: string, endDate: string }>;
}
export default async function Page({
params,
searchParams
}: PageProps) {
const { id } = await params;
const { page, status, violationId, caseType, startDate, endDate } = await searchParams;
const currentPage = Number(page) || 1;
try {
const fileInfo: FileDetails = await viewFileInfo(id);
return (
<div className="violation-details">
<FilePageTitle fileInfo={fileInfo} />
<div>
<div
className="grid grid-cols-[2fr_1fr] gap-3"
>
<FilePageFileInfo fileInfo={fileInfo} />
{/* <FilePageActions /> */}
<ViolationPpageFileStatistic id={id} />
</div>
<div>
<FilePageViolationsList
currentPage={currentPage}
status={status}
fileId={id}
violationId={violationId}
caseType={caseType}
startDate={startDate}
endDate={endDate}
/>
{/* <FilePageNote /> */}
{/* <FilePageViolationInfo /> */}
</div>
</div>
</div>
)
} catch (error) {
return (
<FilePageNotFound />
)
}
}
@@ -3,26 +3,11 @@ import UploadSectionFile from '@/app/components/UploadSectionFile';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload'; import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic'; import ProtectionStatistic from '@/app/ui/marking-page/new/protection-statistic';
import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import { getQueryClient } from '@/app/providers/getQueryClient';
export default async function Page() { export default async function Page() {
const FILE_TYPE = "document"; const FILE_TYPE = "document";
const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE); const { file_extension, max_file_size } = await getAllowedFilesExtensions(FILE_TYPE);
const queryClient = getQueryClient();
try {
const data = await queryClient.fetchQuery({
queryKey: ['referralUserStats'],
queryFn: fetchReferralUserStats,
});
if (data?.referralLink) {
return null;
}
} catch (error) {
}
return ( return (
<div> <div>
<PageTitleColorFrame <PageTitleColorFrame
@@ -0,0 +1,12 @@
import PageTitle from '@/app/ui/page-title';
import { NotificationsList } from '@/app/ui/notifications/notifications-list';
export default function Page() {
return (
<div>
<PageTitle title='notifications' />
<NotificationsList />
</div>
)
}
+2
View File
@@ -3,6 +3,7 @@ import UserUsage from '@/app/ui/payment/user-usage';
import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats'; import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats';
import PaymentTabs from '@/app/ui/payment/payment-tabs'; import PaymentTabs from '@/app/ui/payment/payment-tabs';
import SecurePayments from '@/app/ui/payment/secure-payments'; import SecurePayments from '@/app/ui/payment/secure-payments';
import PaymentUserCards from '@/app/ui/payment/payment-user-cards';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'payment', title: 'payment',
@@ -22,6 +23,7 @@ export default function Page() {
<UserUsage /> <UserUsage />
<TokensSpendStats /> <TokensSpendStats />
<PaymentTabs /> <PaymentTabs />
<PaymentUserCards />
<SecurePayments /> <SecurePayments />
</div> </div>
</main> </main>
+5 -5
View File
@@ -1,21 +1,21 @@
import StatsGrid from '@/app/ui/dashboard/stats-grid'; import ViolationsTable from '@/app/ui/violations/violations-table';
import ViolationsTable from '@/app/ui/dashboard/violations-table';
import FinalSummary from '@/app/ui/reports/final-summary'; import FinalSummary from '@/app/ui/reports/final-summary';
import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month'; import AnalyticsCardMonth from '@/app/ui/reports/analytics-card-month';
import AnalyticsCardGeography from '@/app/ui/reports/analytics-carв-geography'; import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
import ActivityByContentType from '@/app/ui/reports/activity-by-content-type'; import ActivityByContentType from '@/app/ui/reports/activity-by-content-type';
import PageTitle from '@/app/ui/page-title'; import PageTitle from '@/app/ui/page-title';
import ReportsInfo from '@/app/ui/reports/reports-info'; import ReportsInfo from '@/app/ui/reports/reports-info';
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
export default function Page() { export default function Page() {
return ( return (
<div> <div>
<PageTitle title="reports-and-analytics" /> <PageTitle title="reports-and-analytics" />
{/* <StatsGrid /> */}
<ReportsInfo /> <ReportsInfo />
<ActivityByContentType /> <ActivityByContentType />
<div className="analytics-grid"> <div className="analytics-grid">
<AnalyticsCardMonth /> {/* <AnalyticsCardMonth /> */}
<AnalyticsCardDistribution />
<AnalyticsCardGeography /> <AnalyticsCardGeography />
</div> </div>
<ViolationsTable /> <ViolationsTable />
+4 -31
View File
@@ -1,45 +1,18 @@
import SectionSearchFile from '@/app/ui/search/section-search-file'; import SectionSearchFile from '@/app/ui/search/section-search-file';
import { SupportedFormats } from '@/app/ui/search/supported-formats'; import { SupportedFormats } from '@/app/ui/search/supported-formats';
import { SearchStats } from '@/app/ui/search/search-stats'; import { SearchStats } from '@/app/ui/search/search-stats';
import { RecentSearches } from '@/app/ui/search/recent-searches'; import SearchTitle from '@/app/ui/search/search-title';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
export default async function Page() { export default async function Page() {
const { file_extension: extensionVideo, max_file_size } = await getAllowedFilesExtensions('video');
const { file_extension: extensionAudio } = await getAllowedFilesExtensions('audio');
const { file_extension: extensionImage } = await getAllowedFilesExtensions('image');
const { file_extension: extensionDocument } = await getAllowedFilesExtensions('document');
return ( return (
<> <>
<div className="block-wrapper"> <SearchTitle />
<h1 className="page-title">
Поиск защищенного контента
</h1>
<p className="page-description">
Загрузите файл для поиска похожего защищенного контента в вашей библиотеке и в интернете
</p>
</div>
<div className="search-grid"> <div className="search-grid">
<SectionSearchFile <SectionSearchFile />
maxFileSize={max_file_size}
/* allowedExtensions={allExtensions} */
allowedExtensions={{
images: extensionImage,
videos: extensionVideo,
audios: extensionAudio,
documents: extensionDocument
}}
/>
<div className="search-sidebar"> <div className="search-sidebar">
<SupportedFormats <SupportedFormats />
extensionVideo={extensionVideo}
extensionAudio={extensionAudio}
extensionImage={extensionImage}
extensionDocument={extensionDocument}
/>
<SearchStats /> <SearchStats />
<RecentSearches />
</div> </div>
</div> </div>
</> </>
+2
View File
@@ -8,6 +8,7 @@ import DangerZone from '@/app/ui/settings/danger-zone';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import PageTitle from '@/app/ui/page-title'; import PageTitle from '@/app/ui/page-title';
import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel'; import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel';
import SettingUserVerification from '@/app/ui/settings/setting-user-verification';
export default function Page() { export default function Page() {
const t = useTranslations('Global'); const t = useTranslations('Global');
@@ -24,6 +25,7 @@ export default function Page() {
{/* LastActivitySettings возможно нужно будет вернуть */} {/* LastActivitySettings возможно нужно будет вернуть */}
{/* <LastActivitySettings /> */} {/* <LastActivitySettings /> */}
<CompanyUsersSettingPanel /> <CompanyUsersSettingPanel />
<SettingUserVerification />
</div> </div>
<DangerZone /> <DangerZone />
</> </>
+19
View File
@@ -0,0 +1,19 @@
'use server'
import { TrackingStatistic } from '@/app/ui/tracking-page/tracking-statistic';
import FilesTable from '@/app/ui/dashboard/files-table';
import { TrackingHistoryView } from '@/app/ui/tracking-page/tracking-history-view';
export default async function Page() {
const FILE_TYPE = "document";
return (
<div
className="tracking-page"
>
<TrackingStatistic />
<FilesTable fileType={FILE_TYPE} showFileLink={true} />
<TrackingHistoryView />
</div>
)
}
@@ -1,39 +0,0 @@
'use client'
import { useParams } from 'next/navigation'
import ViolationPageTitle from '@/app/ui/violations/file-page/violation-page-title';
import ViolationPageViolationInfo from '@/app/ui/violations/file-page/violation-page-violation-info';
import ViolationPpageFileStatistic from '@/app/ui/violations/file-page/violation-page-file-statistic';
import ViolationPageFileInfo from '@/app/ui/violations/file-page/violation-page-file-info';
import ViolationPageActions from '@/app/ui/violations/file-page/violation-page-actions';
import ViolationPageViolationsList from '@/app/ui/violations/file-page/violation-page-violations-list';
import ViolationPageNote from '@/app/ui/violations/file-page/violation-page-note';
/* violation-details */
export default function Page() {
const params = useParams()
const id = params.id
return (
<div
className="violation-details"
>
<ViolationPageTitle />
<div
className="grid grid-cols-[2fr_1fr] gap-3"
>
<div>
<ViolationPageViolationInfo />
<ViolationPageViolationsList />
<ViolationPageNote />
</div>
<div>
<ViolationPpageFileStatistic />
<ViolationPageFileInfo />
<ViolationPageActions />
</div>
</div>
</div>
)
}
+18 -9
View File
@@ -1,13 +1,16 @@
import PageTitleColorFrame from '@/app/ui/page-title-color-frame'; import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section'; import ViolationsMyComplaint from '@/app/ui/violations/violations-my-complaint';
import DashboardPlatformsList from '@/app/ui/dashboard/new/dashboard-platforms-list';
import ViolationsMyCases from '@/app/ui/violations/violations-my-cases';
import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section'; import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section';
import ViolationsTable from '@/app/ui/violations/violations-table'; import ViolationsTable from '@/app/ui/violations/violations-table';
import ViolationsStatistic from '@/app/ui/violations/violations-statistic'; import ViolationsStatistic from '@/app/ui/violations/violations-statistic';
export default function Page() { import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
const FILE_TYPE = "all"; import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
import ViolationsMyClaims from '@/app/ui/violations/violations-my-claims';
import ViolationsLastCombinedCases from '@/app/ui/violations/violations-last-combined-cases';
import { ViolationsMyComplaintsTable } from '@/app/ui/violations/tables/violations-my-complaints-table';
import { ViolationsMyClaimsTable } from '@/app/ui/violations/tables/violations-my-claims-table';
export default function Page() {
return ( return (
<> <>
<PageTitleColorFrame <PageTitleColorFrame
@@ -15,12 +18,18 @@ export default function Page() {
description="monitoring-copyright-infringements-description" description="monitoring-copyright-infringements-description"
/> />
<ViolationsStatistic /> <ViolationsStatistic />
<ViolationsMyCases />
<ViolationsCheckAllSection /> <ViolationsCheckAllSection />
<ViolationsTable /> <ViolationsTable />
<div className="dashboard-main-grid"> <ViolationsLastCombinedCases />
<DashboardPlatformsList /> {/* <ViolationsMyComplaint />
<DahboardGeographySection /> <ViolationsMyClaims /> */}
<div className="analytics-grid">
<AnalyticsCardDistribution />
<AnalyticsCardGeography />
</div>
<div className="analytics-grid">
<ViolationsMyClaimsTable />
<ViolationsMyComplaintsTable />
</div> </div>
</> </>
) )
+14
View File
@@ -0,0 +1,14 @@
'use client'
import { PdfProvider } from '@/app/contexts/PdfContext';
import { WatchDocProviders } from '@/app/providers/WatchDocProviders';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<WatchDocProviders>
<PdfProvider>
{children}
</PdfProvider>
</WatchDocProviders>
);
}
+150
View File
@@ -0,0 +1,150 @@
'use server';
import Link from 'next/link';
import PdfViewer from '@/app/components/PdfViewer';
import { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
import { fetchDocumentInfo } from '@/app/actions/trackingActions';
import { LoginUserBage } from '@/app/components/document-viewer/LoginUserBage';
interface PageProps {
searchParams: Promise<{ docid: string; title?: string }>;
}
interface DocumentInfo {
checksCount?: number;
checksum?: string;
createdAt?: string;
downloadUrl?: string;
existsOnDisk?: boolean;
fileExtension?: string;
fileFormat?: string | null;
fileName?: string;
filePath?: string;
fileSize?: number;
fileUploadDate?: string;
formattedSize?: string;
id: string;
mimeType?: string;
monitoring?: string;
originalFileName?: string;
ownerCompany?: string | null;
ownerEmail?: string;
ownerName?: string;
permissions?: {
DOWNLOAD?: boolean;
};
protectStatus?: string;
protectedFilePath?: string;
status?: string;
storedFileName?: string;
supportId?: number;
thumbnailFileUrl?: string;
updatedAt?: string;
uploadSessionId?: string;
userId?: number;
}
export default async function Page({ searchParams }: PageProps) {
const { docid, title } = await searchParams;
let response: DocumentInfo | null = null;
let error: string | null = null;
try {
response = await fetchDocumentInfo(docid);
} catch (err) {
error = 'Не удалось загрузить информацию о документе';
}
const documentTitle = response?.fileName || 'FileName';
if (error || !response) {
return (
<div className="watch-doc-wrapper">
<div className="watch-doc-page">
<header className="watch-doc-header">
<Link href="#" className="header-logo">NoCopy</Link>
<LoginUserBage />
</header>
<div className="error-container">
<h2>Ошибка загрузки документа</h2>
<p>{error || 'Документ не найден'}</p>
</div>
</div>
</div>
);
}
return (
<div className="watch-doc-wrapper">
<div className="watch-doc-page">
<header className="watch-doc-header">
<Link href="#" className="header-logo">NoCopy</Link>
<LoginUserBage />
</header>
<div className="watch-doc-status">
<div className="st-title">Защита подтверждена</div>
<div className="st-sub">
Документ <strong>{response?.fileName ? response?.fileName : '#'}</strong> зарегистрирован и защищён платформой NoCopy.
</div>
</div>
<div className="watch-doc-grid">
<div
className="flex flex-col justify-between"
>
<div className="watch-doc-card">
<div className="card-head">Правообладатель</div>
<div className="card-body">
<div className="owner-row">
<div className="owner-av">
{response?.fileName ? response?.fileName[0] : ''}
</div>
<div>
{response?.ownerName && (
<div className="owner-name">{response?.ownerName}</div>
)}
{response?.ownerCompany && (
<div className="owner-name">{response?.ownerCompany}</div>
)}
{response?.ownerName && (
<div className="owner-meta">Зарегистрирован в NoCopy</div>
)}
</div>
</div>
</div>
</div>
<div className="watch-doc-actions">
{response?.permissions?.DOWNLOAD && (
<DownloadButton fileId={docid} fileName={response?.fileName}/>
)}
<CopyLinkButton />
</div>
</div>
<div>
<div className="watch-doc-card">
<div className="card-head">Предпросмотр</div>
<div className="card-body">
<PdfViewer
fileId={docid}
fileName={documentTitle}
showDownload={response.permissions?.DOWNLOAD}
/>
</div>
</div>
</div>
</div>
<div className="watch-doc-footer">
<Link href="#">NoCopy</Link>
<Link href="/terms-of-use">Условия</Link>
<Link href="/privacy-policy">Конфиденциальность</Link>
</div>
</div>
</div>
);
}
+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;
}
}
+12 -7
View File
@@ -44,12 +44,12 @@ export async function getUserFilesInfo() {
msg_id: 20005, msg_id: 20005,
message_body: { message_body: {
action: 'user_files_info', action: 'user_files_info',
token: token
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -70,7 +70,7 @@ export async function getUserFilesInfo() {
} }
} }
export async function fetchTariffs() { export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
const token = await getSessionData('token'); const token = await getSessionData('token');
try { try {
@@ -81,12 +81,13 @@ export async function fetchTariffs() {
msg_id: 30001, msg_id: 30001,
message_body: { message_body: {
action: 'get', action: 'get',
user_token: token tariff_term: tariffTerm
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -107,6 +108,7 @@ export async function fetchTariffs() {
} }
export async function fetchTokensBundle() { export async function fetchTokensBundle() {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -121,7 +123,8 @@ export async function fetchTokensBundle() {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -142,6 +145,7 @@ export async function fetchTokensBundle() {
} }
export async function getBuildData() { export async function getBuildData() {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/check/api/build`, { const response = await fetch(`${API_BASE_URL}/check/api/build`, {
@@ -149,6 +153,7 @@ export async function getBuildData() {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -164,7 +169,7 @@ export async function getBuildData() {
export async function fetchINN(inn: string) { export async function fetchINN(inn: string) {
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
+55 -17
View File
@@ -4,6 +4,7 @@ import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/action
import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session'; import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { fetchINN } from '@/app/actions/action'; import { fetchINN } from '@/app/actions/action';
import { headers } from 'next/headers';
export async function logout() { export async function logout() {
const token = await getSessionData('token'); const token = await getSessionData('token');
@@ -71,7 +72,7 @@ export async function authorization(
try { try {
const { email, password } = validatedFields.data; const { email, password } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
@@ -88,6 +89,7 @@ export async function authorization(
}); });
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
await createSession(parsed.message_body.token, email); await createSession(parsed.message_body.token, email);
} else { } else {
@@ -150,9 +152,11 @@ export async function registration(
state: FormState | undefined, state: FormState | undefined,
formData: FormData, formData: FormData,
): Promise<FormState> { ): Promise<FormState> {
const headersList = await headers();
const accountType = formData.get('accountType') as 'b2b' | 'b2c'; const accountType = formData.get('accountType') as 'b2b' | 'b2c';
const mail_verified = true; /* вернуть в true когда проект выйдет из дева */
const mail_verified = false;
const fullName = formData.get('fullName') as string || ''; const fullName = formData.get('fullName') as string || '';
const email = formData.get('email') as string || ''; const email = formData.get('email') as string || '';
const password = formData.get('password') as string || ''; const password = formData.get('password') as string || '';
@@ -163,6 +167,12 @@ export async function registration(
const referralCode = formData.get('referralCode') as string || ''; const referralCode = formData.get('referralCode') as string || '';
const inn = formData.get('inn') as string || ''; const inn = formData.get('inn') as string || '';
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const SignupFormSchema = await getSignupFormSchema(accountType); const SignupFormSchema = await getSignupFormSchema(accountType);
const dataToValidate = { const dataToValidate = {
@@ -225,7 +235,7 @@ export async function registration(
try { try {
const { fullName, email, password, phone } = validatedFields.data; const { fullName, email, password, phone } = validatedFields.data;
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
@@ -237,6 +247,8 @@ export async function registration(
password, password,
accountType, accountType,
mail_verified: mail_verified, mail_verified: mail_verified,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }), ...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode }) ...(referralCode !== '' && { referralLink: referralCode })
} }
@@ -253,6 +265,8 @@ export async function registration(
phone: phone, phone: phone,
password, password,
accountType, accountType,
ip: ip,
user_agent: userAgent,
...(accountType === 'b2b' && { companyName: companyName, inn: inn }), ...(accountType === 'b2b' && { companyName: companyName, inn: inn }),
...(referralCode !== '' && { referralLink: referralCode }) ...(referralCode !== '' && { referralLink: referralCode })
}) })
@@ -260,7 +274,7 @@ export async function registration(
if (response.ok) { if (response.ok) {
let parsed = await response.json() as { let parsed = await response.json() as {
message_desc: string, message_desc: string,
message_body: { token: string, userId: number }, message_body: { token: string, userId: number, fieldErrors: string },
message_code: number message_code: number
}; };
@@ -387,13 +401,11 @@ export async function tokenLifeExtension() {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
msg_id: 20008, msg_id: 20008
message_body: {
token: token
}
}), }),
}); });
@@ -418,14 +430,14 @@ export async function confirmEmail(userId: number, verifyToken: string) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
'Authorization': `Bearer ${verifyToken}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
msg_id: 20009, msg_id: 20009,
message_body: { message_body: {
resend: 0, resend: 0,
user_id: userId, user_id: userId
verify_token: verifyToken
} }
}), }),
}); });
@@ -461,7 +473,6 @@ export async function confirmEmail(userId: number, verifyToken: string) {
export async function resendConfirmEmail(userId: number) { export async function resendConfirmEmail(userId: number) {
console.log('resendConfirmEmail'); console.log('resendConfirmEmail');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -511,10 +522,9 @@ export async function resetPassword(
formData: FormData, formData: FormData,
) { ) {
const email = formData.get('email'); const email = formData.get('email');
console.log('resetPassword');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/auth`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -598,6 +608,7 @@ export async function confirmPassword(
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'Authorization': `Bearer ${verifyToken}`
}, },
body: JSON.stringify({ body: JSON.stringify({
version: 1, version: 1,
@@ -605,7 +616,6 @@ export async function confirmPassword(
message_body: { message_body: {
email: email, email: email,
password: password, password: password,
verifyToken: verifyToken,
action: "confirmVerification" action: "confirmVerification"
} }
}), }),
@@ -618,16 +628,35 @@ export async function confirmPassword(
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
await createSession(parsed.message_body.authToken, email as string); await createSession(parsed.message_body.authToken, email as string);
} else { } else {
throw parsed.message_body; throw parsed;
} }
} else { } else {
throw (`${response.status}`); throw (`${response.status}`);
} }
} catch (error: unknown) { } catch (error: unknown) {
const typedError = error as any;
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
if (error) { if (error) {
const typedError = error as any;
switch (typedError.message_code) {
case 1:
errors['server'] = 'unknown-error'
break
case 2:
if (typedError.message_desc === 'Token not found or time expired') {
errors['server'] = 'token-not-found-or-time-expired'
} else {
errors['server'] = 'unknown-error'
}
break;
default:
errors['server'] = 'request-ended-with-an-error'
break;
}
return { return {
previousState: { previousState: {
email, email,
@@ -638,7 +667,16 @@ export async function confirmPassword(
error: errors error: errors
}; };
} }
throw error;
return {
previousState: {
email,
password,
confirm_password,
verifyToken
},
error: errors
};
} }
redirect('/pages/dashboard'); redirect('/pages/dashboard');
+16 -12
View File
@@ -2,6 +2,7 @@
import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions'; import { API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session'; import { getSessionData } from '@/app/actions/session';
import { headers } from 'next/headers';
export type CompanyUserRegistrationFormState = { export type CompanyUserRegistrationFormState = {
previousState: { previousState: {
@@ -24,6 +25,8 @@ export async function companyUserRegistration(
return; return;
} }
const headersList = await headers();
const fullName = formData.get('fullName') as string || ''; const fullName = formData.get('fullName') as string || '';
const email = formData.get('email') as string || ''; const email = formData.get('email') as string || '';
const password = formData.get('password') as string || ''; const password = formData.get('password') as string || '';
@@ -32,6 +35,11 @@ export async function companyUserRegistration(
const SignupFormSchema = await getSignupFormSchema('b2c'); const SignupFormSchema = await getSignupFormSchema('b2c');
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
const dataToValidate = { const dataToValidate = {
fullName, fullName,
email, email,
@@ -87,13 +95,15 @@ export async function companyUserRegistration(
phone: phone, phone: phone,
password, password,
accountType: 'b2b', accountType: 'b2b',
authToken: token, mail_verified: false,
mail_verified: false ip: ip,
user_agent: userAgent,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -103,14 +113,6 @@ export async function companyUserRegistration(
message_body: { token: string }, message_body: { token: string },
message_code: number message_code: number
}; };
console.log({
fullName: fullName,
email,
phone: phone,
password,
accountType: 'b2b',
authToken: token
});
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
console.log(`registration - ${email}`); console.log(`registration - ${email}`);
@@ -118,7 +120,9 @@ export async function companyUserRegistration(
fullName: fullName, fullName: fullName,
email, email,
phone: phone, phone: phone,
password password,
ip: ip,
user_agent: userAgent,
}); });
return { return {
+24
View File
@@ -83,6 +83,30 @@ export const loginFormSchema = z
.trim(), .trim(),
}) })
export const createComplaintSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'text-area-error-fill-complaint-text' })
.max(2000, {error: 'text-area-error-complaint-text-too-long'})
.trim(),
})
export const createClaimSchema = z
.object({
textArea: z
.string()
.min(1, { error: 'fill-text-of-the-case' })
.max(2000, {error: 'claim-text-must-not-exceed-2000-characters'})
.trim(),
caseTitle: z
.string()
.min(1, { error: 'fill-title-of-the-case' })
.max(300, {error: 'claim-title-must-not-exceed-300-characters'})
.trim(),
})
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost'; export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
export const API_BASE_URL = process.env.NODE_ENV === 'development' export const API_BASE_URL = process.env.NODE_ENV === 'development'
+97 -12
View File
@@ -4,7 +4,23 @@ import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions'; import { API_BASE_URL } from '@/app/actions/definitions';
export async function getUserFilesData(page: number, pageSize: number) { export async function getUserFilesData({
page,
pageSize,
sortBy,
sortOrder,
type,
date,
query = ''
}: {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc' | string;
type?: string;
date?: string;
query?: string;
}) {
const token = await getSessionData('token'); const token = await getSessionData('token');
try { try {
@@ -14,31 +30,37 @@ export async function getUserFilesData(page: number, pageSize: number) {
version: 1, version: 1,
msg_id: 20005, msg_id: 20005,
message_body: { message_body: {
action: 'user_files', action: 'search_files',
page: page, page: page,
page_size: pageSize, page_size: pageSize,
token: token query: query,
sort_by: sortBy || '', // protectStatus, fileName, fileSize, createdAt, updatedAt, supportId
type: type || '', // image, video, document, audio
date: date || '', // today, week, month
sort_order: sortOrder
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
} else { } else {
throw parsed; throw parsed.message_text ? parsed.message_text : 'error';
} }
} else { } else {
throw (`${response.status}`); throw `${response.status}`;
} }
} catch (error) { } catch (error) {
return error return error;
} }
} }
@@ -55,12 +77,12 @@ export async function removeUserFile(fileId: string, fullDelete: number) {
action: 'delete_file', action: 'delete_file',
file_id: fileId, file_id: fileId,
full_delete: fullDelete, full_delete: fullDelete,
token: token
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -92,13 +114,13 @@ export async function viewFileInfo(fileId: string) {
msg_id: 20005, msg_id: 20005,
message_body: { message_body: {
action: 'file_info', action: 'file_info',
file_id: fileId, file_id: fileId
token: token
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -118,3 +140,66 @@ export async function viewFileInfo(fileId: string) {
return error return error
} }
} }
export async function downloadFile(fileId: string, fileName: string) {
const token = await getSessionData('token');
const response = await fetch(`${API_BASE_URL}/api/v1/files/download/${fileId}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (!response.ok) {
throw 'failed-to-download-file';
}
const arrayBuffer = await response.arrayBuffer()
const base64 = Buffer.from(arrayBuffer).toString('base64')
return {
data: base64,
contentType: response.headers.get('content-type') || 'application/octet-stream',
fileName: fileName
}
}
export async function FileAnAppeal(fileId: string, appealReason: string, additionalInfo: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
action: 'submit_appeal',
file_id: fileId,
appeal_reason: appealReason,
additional_info: additionalInfo
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return error
}
}
+77 -15
View File
@@ -2,8 +2,6 @@
import { API_BASE_URL } from '@/app/actions/definitions'; import { API_BASE_URL } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session'; import { getSessionData } from '@/app/actions/session';
import { FormState } from '@/app/actions/auth';
import { unknown } from 'zod';
interface initMessageBody { interface initMessageBody {
file_name: string, file_name: string,
@@ -12,7 +10,7 @@ interface initMessageBody {
file_size: number | string, file_size: number | string,
action?: string, action?: string,
token?: string, token?: string,
convertTo? : string | undefined | null convertTo?: string | undefined | null
} }
export async function fileUpload(messageBody: initMessageBody, convertTo?: string | null | undefined) { export async function fileUpload(messageBody: initMessageBody, convertTo?: string | null | undefined) {
@@ -22,15 +20,11 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
} }
const message = messageBody; const message = messageBody;
message.action = 'init'; message.action = 'init';
message.token = token;
if (convertTo && convertTo !== 'reject') { if (convertTo && convertTo !== 'reject') {
message.convertTo = convertTo; message.convertTo = convertTo;
} }
// нужно будет добавить свойство convertTo в тело запроса он может прийти со значением reject тогда конвертировать не нужно.
// console.log(convertTo);
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -41,7 +35,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -71,6 +66,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
} }
export async function cancelUpload(udloadId: string) { export async function cancelUpload(udloadId: string) {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -84,7 +81,8 @@ export async function cancelUpload(udloadId: string) {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -116,16 +114,30 @@ export type ChunkResponse = {
fileId?: string fileId?: string
}; };
export async function chunkUpload(formData: FormData): Promise<ChunkResponse> { export async function chunkUpload(formData: FormData, itPrivate?: boolean): Promise<ChunkResponse> {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, { const url = itPrivate
? `${API_BASE_URL}/api/v1/private/files/chunk`
: `${API_BASE_URL}/api/v1/files/chunk`;
/* if (token && itPrivate) {
formData.append('token', token);
} */
const response = await fetch(url, {
method: 'POST', method: 'POST',
body: formData body: formData,
headers: {
'Authorization': `Bearer ${token}`
}
}); });
/* ...(token && itPrivate && { 'Authorization': `Bearer ${token}` }) возможно нужно будет условно отправлять токен*/
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return { return {
@@ -161,6 +173,8 @@ export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
} }
export async function checkChunkStatus(upload_id: string) { export async function checkChunkStatus(upload_id: string) {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -174,7 +188,8 @@ export async function checkChunkStatus(upload_id: string) {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -196,6 +211,8 @@ export async function checkChunkStatus(upload_id: string) {
} }
export async function getAllowedFilesExtensions(type: string) { export async function getAllowedFilesExtensions(type: string) {
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST', method: 'POST',
@@ -209,7 +226,8 @@ export async function getAllowedFilesExtensions(type: string) {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -229,3 +247,47 @@ export async function getAllowedFilesExtensions(type: string) {
return error return error
} }
} }
export async function checkOperationPrice(action: string, filesCount: number, fileType: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30008,
message_body: {
action: action,
file_type: fileType,
count_files: filesCount
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
cost: 0,
success: false,
tokens_count: 0,
error: 'error'
}
}
}
+151
View File
@@ -0,0 +1,151 @@
'use server'
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export async function setMonitoring(fileId: string, monitoringType: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30007,
message_body: {
monitoring_type: monitoringType,
file_id: fileId
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return {
success: true,
errorMessage: null
};
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
success: false,
errorMessage: 'error'
};
}
}
export interface MonitoringStats {
total_searches: number;
searches_by_status: {
processing: number;
completed: number;
failed: number;
total: number;
};
recent_searches: any[];
}
export async function fetchMonitoringStats(): Promise<MonitoringStats | null> {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30011
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed?.message_body?.searches_by_status) {
return parsed.message_body;
}
}
return null;
} catch (error) {
return null
}
}
export async function fetchLastMonitoringCheck() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30011,
message_body: {
limit: 1
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed?.message_body?.searches_by_status) {
return parsed.message_body;
} else {
return null
}
}
} catch (error) {
return null
}
}
export async function fetchMonitoringInfo() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30027
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed?.message_body) {
return parsed.message_body;
}
}
} catch (error) {
return null
}
}
+173
View File
@@ -0,0 +1,173 @@
'use server'
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export interface NotificationBody {
createdAt: string | null,
id: number,
message: string,
status: string | null,
type: string,
context: Record<string, string>
}
export async function fetchActiveNotifications() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30015,
message_body: {
action: 'active'
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_body) {
return parsed?.message_body
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return false
}
}
export async function fetchAllNotifications(page: number = 0, size: number = 10) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30015,
message_body: {
action: 'list',
page: page,
size: size,
sortBy: "createdAt",
sortDirection: "DESC", //DESC, ASC
types: [], //"SEARCH_RESULT", "MONITORING_RESULT"
statuses: ["NEW", "READIED"] //"NEW"
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return parsed?.message_body
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return false
}
}
export async function notificationsMarkAsRead(ids: number[]) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30015,
message_body: {
action: 'markRead',
notificationIds: ids
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return parsed?.message_body
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
success: false
}
}
}
export async function notificationsDelete(ids: number[]) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30015,
message_body: {
action: 'delete',
notificationIds: ids
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
console.log('parsed');
console.log(parsed);
if (parsed?.message_code === 0) {
return parsed?.message_body
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
success: false
}
}
}
+59
View File
@@ -0,0 +1,59 @@
'use server'
import { API_BASE_URL } from '@/app/actions/definitions';
import { getSessionData } from '@/app/actions/session';
export async function getPaymentsMethods() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/payments/methods`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
const parsed = await response.json();
return parsed;
} else {
return null
}
} catch (error) {
return null
}
}
export async function deletePaymentsMethods(cardId: string): Promise<boolean | undefined> {
if (!cardId) {
return
}
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/payments/methods/${cardId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message === 'Payment method deleted') {
return true;
}
} else {
return false
}
} catch (error) {
return false
}
}
+42 -23
View File
@@ -14,12 +14,12 @@ export async function fetchReferralsLevels() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'levels', action: 'levels',
token: token,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -57,12 +57,12 @@ export async function fetchReferralUserStats() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'userStats', action: 'userStats',
token: token,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -96,14 +96,14 @@ export async function fetchReferralInvitees() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'invitees', action: 'invitees',
token: token,
pageSize: 100, pageSize: 100,
pageNumber: 0 pageNumber: 0
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -137,25 +137,18 @@ export async function referralRefill() {
msg_id: 30003, msg_id: 30003,
message_body: { message_body: {
action: 'refill', action: 'refill',
token: token,
amount: 2000, amount: 2000,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
if (response.ok) { if (response.ok) {
console.log('referralRefill');
console.log({
action: 'refill',
token: token,
amount: 2000,
})
let parsed = await response.json(); let parsed = await response.json();
console.log(parsed);
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
@@ -359,15 +352,15 @@ export async function createPayoutRequest(
const token = await getSessionData('token'); const token = await getSessionData('token');
const amount = formData.get('amount') as string || ''; const amount = formData.get('amount') as string || '';
const payoutMethodId = formData.get('payoutMethodId') as string || ''; const payoutMethodId = formData.get('payoutMethodId') as string || '';
console.log('createPayoutRequest'); console.log('createPayoutRequest');
console.log(token);
try { try {
const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, { const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
"amount": amount, 'amount': amount,
"payoutMethodId": payoutMethodId 'payoutMethodId': payoutMethodId
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -375,13 +368,22 @@ export async function createPayoutRequest(
} }
}); });
console.log(response);
if (response.status === 400) { if (response.status === 400) {
const responseText = await response.text(); const responseText = await response.text();
throw JSON.parse(responseText)?.message; 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) { if (response.ok) {
const parsed = await response.json(); const parsed = await response.json();
console.log(parsed); console.log(parsed);
@@ -393,12 +395,29 @@ export async function createPayoutRequest(
error: null error: null
}; };
} else { } else {
throw 'error-request'; throw 'payment-error';
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error) { if (error) {
const typedError = error as string; let typedError: string;
switch (error) {
case 'Sum of pay is required':
typedError = 'sum-of-pay-is-required';
break;
case 'Min sum for pay 1000':
typedError = 'min-sum-for-payout-1000';
break;
case 'insufficient-funds-to-receive-payment':
typedError = 'insufficient-funds-to-receive-payment';
break;
case 'not-found-payout-method':
typedError = 'not-found-payout-method';
break
default:
typedError = 'payment-error';
}
return { return {
previousState: { previousState: {
@@ -412,7 +431,7 @@ export async function createPayoutRequest(
previousState: { previousState: {
amount amount
}, },
error: 'error-request' error: 'payment-error'
}; };
} }
} }
+34 -50
View File
@@ -7,8 +7,11 @@ export async function searchUserFiles(fileId: string) {
const token = await getSessionData('token'); const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY&auth_token=${token}`, { const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY`, {
method: 'GET' method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
}); });
if (response.ok) { if (response.ok) {
@@ -17,63 +20,28 @@ export async function searchUserFiles(fileId: string) {
if (parsed.message_code === 0) { if (parsed.message_code === 0) {
return parsed.message_body; return parsed.message_body;
} else { } else {
throw parsed; throw parsed.message_desc;
} }
} else { } else {
throw (`${response.status}`); throw (`${response.status}`);
} }
} catch (error) { } catch (error) {
return error
if (error === 'File for search unsupported') {
return {
errorMessage: 'file-for-search-unsupported'
}
} else {
return {
errorMessage: 'error'
}
}
} }
} }
export async function searchGlobalFiles(fileId: string, currentPage: number) { export async function searchGlobalFiles(fileId: string, currentPage: number) {
const token = await getSessionData('token'); const token = await getSessionData('token');
console.log('searchGlobalFiles');
console.log(currentPage);
//удалить когда поиск будет нормально работать
/* return {
images: [
{
url: 'string1',
pageTitle: `string1 from page ${currentPage}`,
height: 100,
width: 100,
host: 'string1',
pageUrl: 'string1'
},
{
url: 'string1',
pageTitle: `string2 from page ${currentPage}`,
height: 100,
width: 100,
host: 'string1',
pageUrl: 'string1'
},
{
url: 'string1',
pageTitle: `string3 from page ${currentPage}`,
height: 100,
width: 100,
host: 'string1',
pageUrl: 'string1'
}
],
page: 0,
pageSize: 0,
totalPages: 11,
totalResults: 100,
} */
/* return {
images: [],
page: 0,
pageSize: 0,
totalPages: 0,
totalResults: 0
} */
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -84,11 +52,13 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
message_body: { message_body: {
file_id: fileId, file_id: fileId,
page: currentPage, page: currentPage,
check_similar_first: true,
} }
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
@@ -105,13 +75,26 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
} }
} catch (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 { return {
images: [], images: [],
page: 0, page: 0,
pageSize: 0, pageSize: 0,
totalPages: 0, totalPages: 0,
totalResults: 0, totalResults: 0,
error: 'error' error: errorMessage
} }
} }
} }
@@ -131,6 +114,7 @@ export async function getSearchStats() {
if (response.ok) { if (response.ok) {
let parsed = await response.json(); let parsed = await response.json();
return parsed; return parsed;
} else { } else {
return null; return null;
+5 -5
View File
@@ -37,7 +37,7 @@ export async function createSession(token: string, email: string) {
}); });
const cookieStore = await cookies(); const cookieStore = await cookies();
cookieStore.set('session', session, { cookieStore.set('session_nc_db', session, {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
expires: expiresAt, expires: expiresAt,
@@ -48,7 +48,7 @@ export async function createSession(token: string, email: string) {
export async function updateSession(newToken: string) { export async function updateSession(newToken: string) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const session = cookieStore.get('session')?.value; const session = cookieStore.get('session_nc_db')?.value;
if (!session) { if (!session) {
return null; return null;
@@ -68,7 +68,7 @@ export async function updateSession(newToken: string) {
const updatedSession = await encrypt(updatedPayload); const updatedSession = await encrypt(updatedPayload);
cookieStore.set('session', updatedSession, { cookieStore.set('session_nc_db', updatedSession, {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
expires: updatedPayload.expiresAt, expires: updatedPayload.expiresAt,
@@ -79,7 +79,7 @@ export async function updateSession(newToken: string) {
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> { export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
const cookieStore = await cookies(); const cookieStore = await cookies();
const sessionCookie = cookieStore.get('session')?.value; const sessionCookie = cookieStore.get('session_nc_db')?.value;
if (!sessionCookie) { if (!sessionCookie) {
return null; return null;
@@ -99,7 +99,7 @@ export async function deleteSession() {
console.log('delete session') console.log('delete session')
try { try {
const cookieStore = await cookies() const cookieStore = await cookies()
cookieStore.delete('session'); cookieStore.delete('session_nc_db');
} catch (error) { } catch (error) {
} finally { } finally {
+278
View File
@@ -0,0 +1,278 @@
'use server';
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
import { headers } from 'next/headers';
export async function requestFileWithTracking(fileId: string) {
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const data = await response.json();
return data.message_body;
} else {
const blob = await response.blob();
return URL.createObjectURL(blob);
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> {
const token = await getSessionData('token');
const headersList = await headers();
const forwardedFor = headersList.get('x-forwarded-for');
const realIp = headersList.get('x-real-ip');
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
const userAgent = headersList.get('user-agent') || 'unknown';
try {
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
method: 'POST',
body: JSON.stringify({
file_id: fileId,
ip: ip,
user_agent: userAgent
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`${response.status}`);
}
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const data = await response.json();
const fileUrl = data.message_body;
const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) {
throw new Error(`Failed to fetch file: ${fileResponse.status}`);
}
return await fileResponse.arrayBuffer();
}
else {
return await response.arrayBuffer();
}
} catch (error) {
return null;
}
}
export async function fetchDocumentInfo(fileId: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
file_id: fileId,
action: 'file_info'
}
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function filePermisionChange(fileId: string, permissions: boolean) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 20005,
message_body: {
file_id: fileId,
action: 'change_permission',
permissions: {
DOWNLOAD: permissions
}
}
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return true;
} else {
return false;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function fetchHistoryView(
page: number,
size: number,
locale: string,
sortDirection?: 'asc' | 'desc',
sortBy?: string
) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30028,
message_body: {
action: 'history_view',
page: page,
size: size,
locale: locale,
sort_direction: sortDirection,
sort_by: sortBy
}
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function fetchTrackingStats() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30028,
message_body: {
action: 'view_stats'
}
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
return null;
}
} else {
throw new Error(`${response.status}`);
}
} catch (error) {
return null;
}
}
export async function downloadTrackingFile(fileId: string, fileName?: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/files/public-download/${fileId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw 'failed-to-download-file';
}
const arrayBuffer = await response.arrayBuffer()
const base64 = Buffer.from(arrayBuffer).toString('base64')
return {
data: base64,
contentType: response.headers.get('content-type') || 'application/octet-stream',
fileName: fileName ? fileName : 'fileName'
}
} catch (error) {
return null;
}
}
+719
View File
@@ -0,0 +1,719 @@
'use server'
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL, createComplaintSchema, createClaimSchema } from '@/app/actions/definitions';
export async function getViolationSearchStatus() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/actual-task`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
const text = await response.text();
if (text) {
try {
const parsed = JSON.parse(text);
return parsed;
} catch (e) {
return {
status: text === 'Task not found' ? 'task-not-found' : text
};
}
} else {
return null;
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function startGlobalMonitoring(monitoringType: 'monitoring' | 'all_files') {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/start`, {
method: 'POST',
body: JSON.stringify({
searchType: monitoringType,
...(monitoringType === 'all_files' && { fileIds: [] })
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
}
});
if (response.ok) {
const parsed: {
taskId: string,
status: string,
totalFiles: number
} = await response.json();
if (parsed?.status === 'ACCEPTED') {
return parsed
} else {
return null
}
} else {
return null
}
} catch (error) {
return null
}
}
export async function getViolationFilesArray(props: {
page?: number,
size?: number,
start_date?: string,
end_date?: string,
file_name?: string
}) {
const token = await getSessionData('token');
const { page, size, start_date, end_date, file_name } = props;
try {
const body: Record<string, any> = {};
if (page !== undefined) body.page = page;
if (size !== undefined) body.size = size;
if (start_date !== undefined) body.start_date = start_date;
if (end_date !== undefined) body.end_date = end_date;
if (file_name !== undefined) body.file_name = file_name;
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/violation-summary-with-files`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body),
});
if (response.ok) {
let parsed = await response.json();
if (parsed.content.length) {
return parsed;
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return []
}
}
export async function getFileMatches(fileId: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30009,
message_body: {
file_id: fileId,
page: (page > 0 ? page - 1 : 0),
size: 5,
sort_direction: 'desc',
status: status,
...(violationId && { action: 'getByViolationId' }),
...(violationId && { violation_id: violationId }),
...((startDate && endDate) && { start_date: startDate, end_date: endDate })
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_body) {
return parsed.message_body;
} else {
throw null;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return null
}
}
export async function fetchViolationAnalyticStatistic(group: 'domain' | 'tld') {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30009,
message_body: {
group_by: group,
action: "group",
sort_direction: 'desc'
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_body) {
return parsed?.message_body;
} else {
throw null;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return null
}
}
export async function fetchViolationStatistic(id?: string) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30010,
message_body: {
...(id && { file_id: id })
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_body) {
return parsed?.message_body;
} else {
throw null;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return null
}
}
export async function fetchViolationFinalSummaryStatistic() {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30023
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return parsed?.message_body;
} else {
throw null;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return null
}
}
interface complainBody {
previousText: string,
violationID: string | null,
success: boolean,
errorMessage?: Record<string, string> | null
}
export async function createComplaint(
state: complainBody | undefined,
formData: FormData
): Promise<complainBody> {
const email = await getSessionData('email');
const token = await getSessionData('token');
const violationId = formData.get('violationId') as string || '';
const textArea = formData.get('note') as string || '';
const validatedFields = createComplaintSchema.safeParse({
textArea
});
if (!validatedFields.success) {
const errors: Record<string, string> = {}
JSON.parse(validatedFields.error.message).forEach((obj: {
message: string,
path: string
}) => {
if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
} else {
errors[obj.path[0]] = obj.message;
}
});
return {
violationID: violationId,
previousText: textArea,
success: false,
errorMessage: errors
}
}
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30013,
message_body: {
action: 'create',
violation_id: violationId,
text: textArea,
email: email
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return {
violationID: violationId,
previousText: textArea,
success: true
};
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
const errors: Record<string, string> = {}
if (error && typeof error === 'object' && 'message_code' in error) {
const err = error as { message_code: number };
if (err.message_code === 2) {
errors['server'] = 'the-complaint-has-not-been-registered'
return {
violationID: violationId,
previousText: textArea,
success: false,
errorMessage: errors
}
}
}
errors['server'] = 'error-save';
return {
violationID: violationId,
previousText: textArea,
success: false,
errorMessage: errors
}
}
}
export async function fetchComplainInfo(id: number) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30013,
message_body: {
action: 'get_by_violation',
violation_id: id
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return {
body: parsed?.message_body
}
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
errorMessage: 'error'
}
}
}
export type MatchStatus = 'NEW' | 'SHOWED' | 'LEGAL_IN_WORK' | 'COMPLAINT_IN_WORK' | 'COMPLAINT_AND_LEGAL_IN_WORK' | 'AUTHORIZED_USE' | 'NOT_OWNER_FILE';
export async function updateMatchStatus(id: number, status: MatchStatus) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30009,
message_body: {
action: 'updateStatus',
violation_id: id,
status: status
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_body.status) {
return true
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return false
}
}
export async function fetchClaimInfo(id: number) {
const token = await getSessionData('token');
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30017,
message_body: {
action: 'get_all',
pageSize: 1,
pageNumber: 0,
sortBy: "createdAt",
sortDir: "desc",
violation_id: id
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return {
body: parsed?.message_body
}
} else if (parsed?.message_code !== 0 && parsed?.message_desc === 'Law case not found') {
return {
body: null
}
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return {
errorMessage: 'error'
}
}
}
interface caseBody {
previousText: string,
previousCaseTitle: string,
previousAmount: string,
violationID: string | null,
success: boolean,
errorMessage?: Record<string, string> | null
}
export async function createClaim(
state: caseBody | undefined,
formData: FormData
): Promise<caseBody> {
const email = await getSessionData('email');
const token = await getSessionData('token');
const violationId = formData.get('violationId') as string || '';
const textArea = formData.get('note') as string || '';
const caseTitle = formData.get('caseTitle') as string || '';
const amount = formData.get('amount') as string || '';
const validatedFields = createClaimSchema.safeParse({
textArea,
caseTitle
});
if (!validatedFields.success) {
const errors: Record<string, string> = {}
JSON.parse(validatedFields.error.message).forEach((obj: {
message: string,
path: string
}) => {
if (errors[obj.path[0]]) {
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
} else {
errors[obj.path[0]] = obj.message;
}
});
return {
violationID: violationId,
previousText: textArea,
previousCaseTitle: caseTitle,
previousAmount: amount,
success: false,
errorMessage: errors
}
}
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30017,
message_body: {
action: 'create',
violation_id: violationId,
description: textArea,
email: email,
name: caseTitle,
priority: 'HIGH',
amount: Number(amount)
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed?.message_code === 0) {
return {
violationID: violationId,
previousCaseTitle: caseTitle,
previousText: textArea,
previousAmount: amount,
success: true
};
} else {
throw parsed;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
const errors: Record<string, string> = {}
if (error && typeof error === 'object' && 'message_code' in error) {
const err = error as { message_code: number };
if (err.message_code === 2) {
errors['server'] = 'the-complaint-has-not-been-registered'
return {
violationID: violationId,
previousText: textArea,
previousCaseTitle: caseTitle,
previousAmount: amount,
success: false,
errorMessage: errors
}
}
}
errors['server'] = 'error-save';
return {
violationID: violationId,
previousText: textArea,
previousCaseTitle: caseTitle,
previousAmount: amount,
success: false,
errorMessage: errors
}
}
}
export interface ComplaintsCaseQueryParams {
page?: number,
size?: number,
sort_by?: string,
sort_direction?: 'desc' | 'asc'
}
export async function fetchLastClaims(params: ComplaintsCaseQueryParams) {
const token = await getSessionData('token');
const { page, size, sort_by, sort_direction } = params;
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30017,
message_body: {
action: "get_all",
pageNumber: page,
size,
sort_by,
sort_direction
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw null;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return null
}
}
export async function fetchLastComplaint(params: ComplaintsCaseQueryParams) {
const token = await getSessionData('token');
const { page, size, sort_by, sort_direction } = params;
try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
method: 'POST',
body: JSON.stringify({
version: 1,
msg_id: 30013,
message_body: {
action: "get_all",
page,
size,
sort_by,
sort_direction
}
}),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let parsed = await response.json();
if (parsed.message_code === 0) {
return parsed.message_body;
} else {
throw null;
}
} else {
throw (`${response.status}`);
}
} catch (error) {
return null
}
}
-1
View File
@@ -4,7 +4,6 @@ import { API_BASE_URL } from '@/app/actions/definitions';
import { createSession } from '@/app/actions/session'; import { createSession } from '@/app/actions/session';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
export async function YandexAuthorization(data: any, codeVerifier: string) { export async function YandexAuthorization(data: any, codeVerifier: string) {
console.log(data); console.log(data);
+19 -14
View File
@@ -8,6 +8,7 @@ const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV
export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string, operationType: 'TARIFF' | 'TOKEN') { export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string, operationType: 'TARIFF' | 'TOKEN') {
const formData = new FormData(); const formData = new FormData();
const token = await getSessionData('token');
formData.append('email', email); formData.append('email', email);
formData.append('tariffId', tariffId.toString()); formData.append('tariffId', tariffId.toString());
@@ -17,7 +18,10 @@ export async function makePaymentOperation(email: string, tariffId: number, oper
try { try {
const response = await fetch(`${API_BASE_URL}/api/payments/create`, { const response = await fetch(`${API_BASE_URL}/api/payments/create`, {
method: 'POST', method: 'POST',
body: formData body: formData,
headers: {
'Authorization': `Bearer ${token}`
}
}); });
if (response.ok) { if (response.ok) {
@@ -36,7 +40,6 @@ export async function makePaymentOperation(email: string, tariffId: number, oper
} }
export async function yooKasaCreatePayment(amount: number, tariff: number, operationType: 'TARIFF' | 'TOKEN') { export async function yooKasaCreatePayment(amount: number, tariff: number, operationType: 'TARIFF' | 'TOKEN') {
const userEmail = await getSessionData('email'); const userEmail = await getSessionData('email');
const timestamp = Date.now(); const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 10); const random = Math.random().toString(36).substring(2, 10);
@@ -48,38 +51,39 @@ export async function yooKasaCreatePayment(amount: number, tariff: number, opera
const createPayload: ICreatePayment = { const createPayload: ICreatePayment = {
amount: { amount: {
value: amount.toLocaleString().replace(/\s/g, ''), value: amount.toLocaleString().replace(/[,\s.]/g, ''),
currency: 'RUB' currency: 'RUB'
}, },
payment_method_data: {
type: 'bank_card'
},
confirmation: { confirmation: {
type: 'redirect', type: 'embedded',
return_url: 'test'
}, },
metadata: { metadata: {
userMail: userEmail, userMail: userEmail,
tariff: tariff tariff: tariff
}, },
capture: true merchant_customer_id: userEmail,
capture: true,
save_payment_method: operationType === 'TOKEN' ? false : true,
/* ...(operationType === 'TOKEN' && { save_payment_method: false }) */
}; };
try { try {
const payment = await checkout.createPayment(createPayload, idempotenceKey); const payment = await checkout.createPayment(createPayload, idempotenceKey);
const response = await makePaymentOperation(userEmail, tariff, payment.id, operationType); const response = await makePaymentOperation(userEmail, tariff, payment.id, operationType);
if (response && payment) {
return payment?.confirmation.confirmation_url; if (payment && response) {
return payment?.confirmation.confirmation_token;
} else { } else {
return null return null;
} }
} catch (error) { } catch (error) {
return null return null;
} }
} }
export async function fetchPaymentsHistory() { export async function fetchPaymentsHistory() {
const email = await getSessionData('email'); const email = await getSessionData('email');
const token = await getSessionData('token');
try { try {
const response = await fetch(`${API_BASE_URL}/api/v1/data`, { const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
@@ -94,7 +98,8 @@ export async function fetchPaymentsHistory() {
}), }),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json',
'Authorization': `Bearer ${token}`
} }
}); });
-51
View File
@@ -1,51 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSessionData } from '@/app/actions/session';
import { API_BASE_URL } from '@/app/actions/definitions';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const token = await getSessionData('token');
if (!token) {
return NextResponse.json(
{ error: 'need authorization' },
{ status: 401 }
);
}
const { id } = await params;
const response = await fetch(`${API_BASE_URL}/api/v1/files/download/${id}`, {
method: 'GET',
headers: {
"Authorization": token
}
});
if (!response.ok) {
throw new Error(`backend error: ${response.status}`);
}
const fileBlob = await response.blob();
const headers = new Headers(response.headers);
return new NextResponse(fileBlob, {
status: 200,
headers: {
'Content-Type': headers.get('content-type') || 'application/octet-stream',
'Content-Disposition': headers.get('content-disposition') || `attachment; filename="file-${id}"`,
'Content-Length': headers.get('content-length') || String(fileBlob.size),
},
});
} catch (error) {
console.error('download error:', error);
return NextResponse.json(
{ error: 'download file error' },
{ status: 500 }
);
}
}
-24
View File
@@ -3,36 +3,12 @@
import { useEffect, useRef, useCallback } from 'react'; import { useEffect, useRef, useCallback } from 'react';
import { tokenLifeExtension } from '@/app/actions/auth'; import { tokenLifeExtension } from '@/app/actions/auth';
import { usePathname, useSearchParams } from 'next/navigation';
import { removeUserFile } from '@/app/actions/fileEntity';
export default function ActivityTracker() { export default function ActivityTracker() {
const lastRefreshTime = useRef(Date.now()); const lastRefreshTime = useRef(Date.now());
const activityTimeoutRef = useRef<NodeJS.Timeout>(null); const activityTimeoutRef = useRef<NodeJS.Timeout>(null);
const isRefreshing = useRef(false); const isRefreshing = useRef(false);
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const selectedId = document.cookie
.split('; ')
.find(row => row.startsWith('searchedFileId='))
?.split('=')[1];
const handleCookie = async () => {
if (selectedId) {
await removeUserFile(selectedId, 1);
document.cookie = 'searchedFileId=; path=/; max-age=0';
}
};
if (selectedId) {
handleCookie();
}
}, [pathname, searchParams]);
// Debounce функция // Debounce функция
const debounce = useCallback((func: () => void, delay: number) => { const debounce = useCallback((func: () => void, delay: number) => {
if (activityTimeoutRef.current) { if (activityTimeoutRef.current) {
@@ -0,0 +1,103 @@
'use client'
import { useTranslations } from 'next-intl'
interface NotificationTextHandlerProps {
notificationType: string
notificationMessage: string
notificationContext: Record<string, string>
}
export function NotificationTextHandler({
notificationType,
notificationMessage,
notificationContext
}: NotificationTextHandlerProps) {
const t = useTranslations('Notifications');
const getNotificationText = () => {
if (!notificationType) {
return t.has(notificationMessage) ? t(notificationMessage) : notificationMessage
}
switch (notificationType) {
case 'USER_VERIFIED':
return t('user-verified', {
status: notificationContext?.status || t('not-specified'),
updateDate: notificationContext?.update_date || t('not-specified-date')
});
case 'MONITORING_RESULT':
return t('monitoring-result', {
message: notificationContext?.message || t('message-missing')
});
case 'COMPLAINT_STATUS_CHANGED':
return t('complaint-status-changed', {
newStatus: notificationContext?.new_status || t('not-specified'),
oldStatus: notificationContext?.old_status || t('not-specified'),
violation: notificationContext?.violation || t('not-specified'),
file: notificationContext?.file || t('not-specified')
});
case 'FILE_ADDED_TO_SYSTEM':
return t('file-added-to-system', {
status: notificationContext?.status || t('not-specified'),
fileName: notificationContext?.file_name || t('not-specified-file-name')
});
case 'FILE_MODERATION_EVENT':
return t('file-moderation-event', {
fileName: notificationContext?.file_name || t('not-specified-file-name'),
oldStatus: notificationContext?.old_status || t('not-specified'),
newStatus: notificationContext?.new_status || t('not-specified'),
comment: notificationContext?.comment || t('no-comment')
});
case 'PAYMENT_RESULT':
return t('payment-result', {
status: notificationContext?.status || t('not-specified'),
amount: notificationContext?.amount || '0'
});
case 'REFERRAL_REGISTERED':
return t('referral-registered', {
referralUser: notificationContext?.referral_user || t('unknown-user')
});
case 'SEARCH_RESULT':
return t('search-result', {
startDate: notificationContext?.start_date || t('not-specified-date'),
endDate: notificationContext?.end_date || t('not-specified-date'),
searchType: notificationContext?.search_type || t('not-specified'),
count: notificationContext?.count || '0'
});
case 'SEARCH_OPERATION_FAILED':
return t('search-operation-failed', {
status: notificationContext?.status || t('not-specified'),
errorMessage: notificationContext?.error_message || t('error-not-described'),
message: notificationContext?.message || t('message-missing'),
reason: notificationContext?.reason || t('reason-not-specified'),
nextRun: notificationContext?.next_run || t('not-scheduled')
});
case 'TOKEN_NOT_FOUND':
return t('token-not-found', {
currentTokens: notificationContext?.current_tokens || '0',
needTokens: notificationContext?.need_tokens || t('not-specified'),
message: notificationContext?.message || t('message-missing'),
nextRun: notificationContext?.next_run || t('not-scheduled')
});
default:
return notificationMessage || t('notification-without-text');
}
};
return (
<div>
{getNotificationText()}
</div>
);
}
+251
View File
@@ -0,0 +1,251 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { usePdf } from '@/app/contexts/PdfContext';
import { requestFileAsBuffer } from '@/app/actions/trackingActions';
interface PdfViewerProps {
fileId: string;
fileName?: string;
showDownload?: boolean;
}
export default function PdfViewer({ fileId, fileName = 'document.pdf', showDownload }: PdfViewerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [fullscreen, setFullscreen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const pdfDocumentRef = useRef<any>(null);
const { setPdfData } = usePdf();
useEffect(() => {
let isMounted = true;
let objectUrl: string | null = null;
const loadAndDisplayPdf = async () => {
try {
setLoading(true);
const pdfData = await requestFileAsBuffer(fileId);
if (!pdfData) {
throw new Error('Failed to load document');
}
//это нужно для скачивания так-как иначе pdfData теряется для скачивания
if (showDownload) {
const pdfDataForDownload = pdfData.slice(0);
setPdfData(pdfDataForDownload, fileName);
}
if (!isMounted) return;
const pdfjs = await import('pdfjs-dist');
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString();
const loadingTask = pdfjs.getDocument({
data: pdfData,
useSystemFonts: true,
disableFontFace: false,
});
const pdfDocument = await loadingTask.promise;
if (!isMounted) return;
pdfDocumentRef.current = pdfDocument;
setTotalPages(pdfDocument.numPages);
setCurrentPage(1);
await renderPage(pdfDocument, 1);
} catch (err) {
console.error('Error loading PDF:', err);
if (isMounted) {
setError('Не удалось загрузить документ');
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
loadAndDisplayPdf();
return () => {
isMounted = false;
if (pdfDocumentRef.current) {
pdfDocumentRef.current.destroy();
}
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
};
}, [fileId, setPdfData]);
const renderPage = async (pdfDocument: any, pageNum: number) => {
if (!canvasRef.current) return;
try {
const page = await pdfDocument.getPage(pageNum);
const container = canvasRef.current.parentElement;
if (!container) return;
const containerWidth = container.clientWidth || 800;
// Если ширина контейнера 0, откладываем рендер
if (containerWidth === 0) {
console.log('Container width is 0, waiting...');
setTimeout(() => renderPage(pdfDocument, pageNum), 100);
return;
}
const originalViewport = page.getViewport({ scale: 1 });
const scale = containerWidth / originalViewport.width;
const viewport = page.getViewport({ scale: Math.min(scale, 1.5) });
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context!,
viewport: viewport,
};
await page.render(renderContext).promise;
} catch (err) {
console.log('Error rendering page:', err);
}
};
useEffect(() => {
if (!pdfDocumentRef.current || loading) return;
const checkContainer = () => {
const container = canvasRef.current?.parentElement;
if (container && container.clientWidth > 0) {
renderPage(pdfDocumentRef.current, currentPage);
} else {
setTimeout(checkContainer, 100);
}
};
checkContainer();
}, [pdfDocumentRef.current, loading]);
const goToPrevPage = async () => {
if (pdfDocumentRef.current && currentPage > 1) {
const newPage = currentPage - 1;
setCurrentPage(newPage);
await renderPage(pdfDocumentRef.current, newPage);
}
};
const goToNextPage = async () => {
if (pdfDocumentRef.current && currentPage < totalPages) {
const newPage = currentPage + 1;
setCurrentPage(newPage);
await renderPage(pdfDocumentRef.current, newPage);
}
};
const toggleFullscreen = () => {
if (!containerRef.current) return;
if (!document.fullscreenElement) {
containerRef.current.requestFullscreen();
setFullscreen(true);
} else {
document.exitFullscreen();
setFullscreen(false);
}
};
useEffect(() => {
const handleFullscreenChange = () => {
setFullscreen(!!document.fullscreenElement);
if (pdfDocumentRef.current && !document.fullscreenElement) {
setTimeout(() => renderPage(pdfDocumentRef.current, currentPage), 100);
}
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, [currentPage]);
useEffect(() => {
const handleResize = () => {
if (pdfDocumentRef.current && !loading) {
renderPage(pdfDocumentRef.current, currentPage);
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [currentPage, loading]);
if (loading) {
return (
<div className="prev-wrap">
<div className="pdf-loading">Загрузка документа...</div>
</div>
);
}
if (error) {
return (
<div className="prev-wrap">
<div className="pdf-error">{error}</div>
</div>
);
}
return (
<div ref={containerRef} className="pdf-container">
<div className="prev-wrap">
<canvas ref={canvasRef} className="pdf-canvas" />
</div>
{totalPages > 0 && (
<div className="document-controls">
<div className="document-controls__nav">
<button
className="document-controls__prev"
onClick={goToPrevPage}
disabled={currentPage === 1}
>
Пред.
</button>
<span className="document-controls__current-page">
{currentPage} / {totalPages}
</span>
<button
className="document-controls__next"
onClick={goToNextPage}
disabled={currentPage === totalPages}
>
След.
</button>
</div>
<button
className="document-controls__fullscreen"
onClick={toggleFullscreen}
>
{fullscreen ? 'Выйти' : 'На весь экран'}
</button>
</div>
)}
</div>
);
}
+78 -24
View File
@@ -2,7 +2,7 @@
import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react'; import { useRef, useState, useCallback, ChangeEvent, DragEvent, useEffect, useMemo } from 'react';
import { IconShieldAdd } from '@/app/ui/icons/icons'; import { IconShieldAdd } from '@/app/ui/icons/icons';
import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus } from '@/app/actions/fileUpload'; import { fileUpload, cancelUpload, chunkUpload, checkChunkStatus, checkOperationPrice } from '@/app/actions/fileUpload';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker'; import { useNavigationBlocker } from '@/app/hooks/useNavigationBlocker';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
@@ -113,8 +113,16 @@ export default function UploadSectionFile({
setError(null); setError(null);
const newFiles: SelectedFile[] = []; const newFiles: SelectedFile[] = [];
const selectedFileNames = new Set(selectedFiles.map(f => f.name));
const currentFileNames = new Set<string>();
const duplicateNames: string[] = [];
for (const file of files) { for (const file of files) {
if (selectedFileNames.has(file.name)) {
duplicateNames.push(file.name);
continue;
}
const validation = validateFile(file); const validation = validateFile(file);
if (!validation.isValid) { if (!validation.isValid) {
@@ -141,6 +149,8 @@ export default function UploadSectionFile({
} }
} }
currentFileNames.add(file.name);
newFiles.push({ newFiles.push({
file, file,
name: file.name, name: file.name,
@@ -152,8 +162,13 @@ export default function UploadSectionFile({
}); });
} }
if (duplicateNames.length > 0) {
const uniqueDuplicates = [...new Set(duplicateNames)];
setError(`${t('duplicate-files')}: ${uniqueDuplicates.join(', ')}`);
}
setSelectedFiles(prev => [...prev, ...newFiles]); setSelectedFiles(prev => [...prev, ...newFiles]);
}, [fileType, t]); }, [fileType, selectedFiles]);
const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => { const handleFileInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
const files = Array.from(event.target.files || []); const files = Array.from(event.target.files || []);
@@ -204,6 +219,7 @@ export default function UploadSectionFile({
f.uploadId !== fileId && f.name !== fileId f.uploadId !== fileId && f.name !== fileId
)); ));
} else { } else {
console.log('clear all')
selectedFiles.forEach(file => { selectedFiles.forEach(file => {
if (file.preview) { if (file.preview) {
URL.revokeObjectURL(file.preview); URL.revokeObjectURL(file.preview);
@@ -223,10 +239,21 @@ export default function UploadSectionFile({
setError(null); setError(null);
}; };
const uploadSingleFile = async (fileInfo: SelectedFile): Promise<void> => { const uploadSingleFile = async (fileInfo: SelectedFile, uploadType: 'single' | 'multi'): Promise<void> => {
const fileId = `${fileInfo.name}-${Date.now()}`; const fileId = `${fileInfo.name}-${Date.now()}`;
isCancelledRef.current.set(fileId, false); isCancelledRef.current.set(fileId, false);
if (uploadType === 'single') {
setError(null);
const priceCheck = await checkOperationPrice('checkTokensForProtect', 1, fileType);
if (!priceCheck.success) {
const errorMessage = `${t('there-are-not-enough-funds-to-protect-file', { count: priceCheck?.tokens_count })}`;
setError(errorMessage);
return;
}
}
try { try {
setSelectedFiles(prev => prev.map(f => setSelectedFiles(prev => prev.map(f =>
f.name === fileInfo.name ? { ...f, status: 'uploading' } : f f.name === fileInfo.name ? { ...f, status: 'uploading' } : f
@@ -315,20 +342,31 @@ export default function UploadSectionFile({
}, [selectedFiles]); }, [selectedFiles]);
const handleStartAllUploads = useCallback(async (): Promise<void> => { const handleStartAllUploads = useCallback(async (): Promise<void> => {
const uploadNextBatch = async () => { setError(null);
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending'); if (selectedFilesRef.current.length === 0) {
const batch = currentPending.slice(0, maxParallelUploads); return;
}
const priceCheck = await checkOperationPrice('checkTokensForProtect', selectedFilesRef.current.length, fileType);
if (batch.length === 0) return; if (priceCheck.success) {
const uploadNextBatch = async () => {
const currentPending = selectedFilesRef.current.filter(f => f.status === 'pending');
const batch = currentPending.slice(0, maxParallelUploads);
await Promise.all(batch.map(file => uploadSingleFile(file))); if (batch.length === 0) return;
if (selectedFilesRef.current.some(f => f.status === 'pending')) { await Promise.all(batch.map(file => uploadSingleFile(file, 'multi')));
await uploadNextBatch();
}
};
await uploadNextBatch(); 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]); }, [maxParallelUploads]);
const getTotalProgress = useMemo(() => { const getTotalProgress = useMemo(() => {
@@ -399,7 +437,6 @@ export default function UploadSectionFile({
})); }));
} }
return ( return (
<div className="upload-section" > <div className="upload-section" >
<h3> <h3>
@@ -565,15 +602,32 @@ export default function UploadSectionFile({
</> </>
) )
} }
<div> <div
<span className={ className="flex"
`upload-progress ${file.status === 'completed' ? 'text-green-600' : >
file.status === 'uploading' ? 'text-blue-600' : {file.status !== 'error' && (
'text-gray-600' <div className="upload-progress-circle">
}` <svg viewBox="0 0 36 36" className="circular-chart">
}> <path
{file.progress} % className="circle-bg"
</span> d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke="#e5e7eb"
strokeWidth="3"
/>
<path
className="circle"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
fill="none"
stroke={file.status === 'completed' ? '#10b981' : file.status === 'uploading' ? '#155dfc' : '#6b7280'}
strokeWidth="4"
strokeDasharray={`${file.progress}, 100`}
strokeLinecap="round"
/>
</svg>
<span className="circle-percentage">{file.progress}%</span>
</div>
)}
< button < button
className="btn btn-cancel" className="btn btn-cancel"
onClick={() => handleClearFile(file.uploadId || file.name)} onClick={() => handleClearFile(file.uploadId || file.name)}
@@ -607,7 +661,7 @@ export default function UploadSectionFile({
<div className="mt-2 text-center" > <div className="mt-2 text-center" >
<button <button
className="btn btn-confirm" className="btn btn-confirm"
onClick={() => uploadSingleFile(file) onClick={() => uploadSingleFile(file, 'single')
} }
type="button" type="button"
disabled={uploadingCount >= maxParallelUploads} disabled={uploadingCount >= maxParallelUploads}
+11 -18
View File
@@ -14,10 +14,6 @@ export default function YandexAuthWithScript() {
const [isReady, setIsReady] = useState(false); const [isReady, setIsReady] = useState(false);
const [showButton, setShowButton] = useState(true); const [showButton, setShowButton] = useState(true);
const handleLogin = (token: string) => {
console.log('handleLogin');
};
useEffect(() => { useEffect(() => {
if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return; if (!isReady || !buttonRef.current || !window.YaAuthSuggest) return;
@@ -25,31 +21,28 @@ export default function YandexAuthWithScript() {
{ {
client_id: 'b356a7ceac0f4c50a3b434220f86bce0', client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
response_type: 'token', response_type: 'token',
redirect_uri: `http://localhost:2999/ru/login`, redirect_uri: `https://fallibly-desired-moonfish.cloudpub.ru/en/login`,
}, },
'http://localhost:2999', 'https://fallibly-desired-moonfish.cloudpub.ru',
{ {
parentId: buttonRef.current.id, parentId: buttonRef.current.id,
view: 'button', view: 'button',
buttonView: 'main', buttonView: 'main',
buttonTheme: 'light', buttonTheme: 'light',
buttonSize: 'm', buttonSize: 'l',
buttonBorderRadius: 10 buttonBorderRadius: 10
} }
) )
.then((result: any) => { .then(function (result: any) {
console.log('then'); console.log('you at least try?')
console.log(result); console.log(result);
if (result?.access_token) { return result.handler();
handleLogin(result.access_token);
} else {
setTimeout(() => {
setShowButton(false);
}, 1000)
}
}) })
.catch((error: any) => { .then(function (data: any) {
console.error('Yandex auth error:', error); console.log('Сообщение с токеном: ', data);
})
.catch(function (error: any) {
console.log('Что-то пошло не так: ', error);
}); });
}, [isReady]); }, [isReady]);
+15
View File
@@ -0,0 +1,15 @@
export function YandexLoginButton() {
const clientId = 'b356a7ceac0f4c50a3b434220f86bce0';
const redirectUri = encodeURIComponent('https://fallibly-desired-moonfish.cloudpub.ru/login/callback');
const authUrl = `https://oauth.yandex.ru/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&force_confirm=yes`;
return (
<a
href={authUrl}
className="yandex-auth-button"
>
Войти через Яндекс
</a>
);
}
+99
View File
@@ -0,0 +1,99 @@
import { useCallback, useEffect } from 'react';
import {
YooMoneyCheckoutWidget,
YooMoneyCheckoutWidgetConfig,
YooMoneyErrorCallbackResult
} from "types-yoomoneycheckoutwidget";
let isLoading = false;
let isInited = false;
export default function YooWidget({ config, onComplete, onSuccess, onFail, onModalClose }: {
/**
* Описание https://yookassa.ru/developers/payment-acceptance/integration-scenarios/widget/reference#initialization-parameters
*/
config: YooMoneyCheckoutWidgetConfig,
/** после успешной оплаты*/
onComplete?: () => void,
/** после успешной оплаты, только если не подписались на onComplete*/
onSuccess?: () => void,
/** только если вы отключили обработку неуспешных попыток в виджете: пользователь выбрал любой способ оплаты, но платеж не прошел; закончился срок действия токена.*/
onFail?: () => void,
/** после закрытия модального окна платежной формы если customization.modal==true */
onModalClose?: () => void,
}) {
const isModal = config?.customization?.modal;
let checkout: YooMoneyCheckoutWidget;
const destroy = () => {
console.log('checkout.destroy');
checkout.destroy();
isInited = false;
}
const initializeWidget = useCallback(
() => {
isInited = true;
isLoading = false;
if (!isModal) {
const userErrorCallBack = config.error_callback;
config.error_callback = (error: YooMoneyErrorCallbackResult) => {
userErrorCallBack(error);
isInited = false;
};
}
checkout = new window.YooMoneyCheckoutWidget(config);
checkout.on('complete', (result) => {
console.log('on complete', result);
onComplete && onComplete();
destroy();
});
onSuccess && checkout.on('success', (result) => {
console.log('on success', result);
onSuccess();
destroy();
});
onFail && checkout.on('fail', (result) => {
console.warn('on fail', result);
onFail();
destroy();
});
if (config.customization?.modal) {
checkout.on('modal_close', (result) => {
onModalClose && onModalClose();
destroy();
});
}
checkout.render(config.customization?.modal ? undefined : 'payment-form');
}, [config, onSuccess]);
useEffect(() => {
if (!isLoading && !isInited) {
if (!window.YooMoneyCheckoutWidget) {
isLoading = true;
const script = document.createElement('script');
script.src = 'https://yookassa.ru/checkout-widget/v1/checkout-widget.js';
script.async = true;
script.onload = initializeWidget;
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
} else {
initializeWidget();
}
}
return () => {
if (!isInited && !isModal) {
destroy();
}
}
});
return (
<>
{isModal ? '' : <div id="payment-form" />}
</>
);
}
@@ -0,0 +1,30 @@
'use client';
import { toast } from 'sonner';
interface CopyLinkButtonProps {
className?: string;
children?: React.ReactNode;
}
export function CopyLinkButton({ className = 'btn btn-out', children = 'Скопировать ссылку' }: CopyLinkButtonProps) {
const copyCurrentPageLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
toast.success('Ссылка скопирована');
} catch (err) {
toast.error('Не удалось скопировать ссылку');
}
};
return (
<button
className={className}
onClick={copyCurrentPageLink}
type="button"
>
{children}
</button>
);
}
@@ -0,0 +1,50 @@
'use client';
import { downloadTrackingFile } from '@/app/actions/trackingActions';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
interface DownloadButtonProps {
fileId: string;
fileName?: string;
}
export function DownloadButton({ fileId, fileName }: DownloadButtonProps) {
const t = useTranslations('Global');
const handleDownload = async () => {
try {
const result = await downloadTrackingFile(fileId, fileName);
if (!result) {
throw 'failed-to-download-file'
}
const byteCharacters = atob(result.data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: result.contentType });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.fileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success(`${t('file-is-downloading')} - `);
} catch (error) {
toast.error(t('failed-to-download-file'));
}
};
return (
<button className="btn btn-out" onClick={handleDownload}>
Скачать документ
</button>
);
}
@@ -0,0 +1,18 @@
'use client';
import Link from 'next/link';
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
export function LoginUserBage() {
const { data: userData, isLoading, isError, error } = useUserProfile();
if (!userData?.fullName) {
return null;
}
return (
<Link href="#" className="header-user">
{userData?.fullName}
</Link>
);
}
@@ -2,29 +2,29 @@
import { FileItem } from '@/app/components/tanstak-table/TanstakTable'; import { FileItem } from '@/app/components/tanstak-table/TanstakTable';
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { setMonitoring } from '@/app/actions/monitoringActions';
import { toast } from 'sonner';
export function MonitoringDropDown({ file }: { export function MonitoringDropDown({ file }: {
file: FileItem file: FileItem
}) { }) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [currentMonitoringStatus, setCurrentMonitoringStatus] = useState('without-monitoring'); const [currentMonitoringStatus, setCurrentMonitoringStatus] = useState(file.monitoring);
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const t = useTranslations('Global') const t = useTranslations('Monitoring-status');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
console.log('click');
if (!dropdownRef?.current) { if (!dropdownRef?.current) {
return; return;
} }
@@ -49,10 +49,10 @@ export function MonitoringDropDown({ file }: {
}, [dropdownRef, setIsOpen, isOpen]); }, [dropdownRef, setIsOpen, isOpen]);
useEffect(() => { useEffect(() => {
if (file.status) { if (file.monitoring) {
setCurrentMonitoringStatus(file.status === 'ACTIVE' ? 'without-monitoring' : 'without-monitoring'); setCurrentMonitoringStatus(file.monitoring ? file.monitoring : 'NONE');
} }
}, [file]); }, [file.monitoring]);
useEffect(() => { useEffect(() => {
if (isOpen && buttonRef.current) { if (isOpen && buttonRef.current) {
@@ -66,10 +66,17 @@ export function MonitoringDropDown({ file }: {
}, [isOpen]); }, [isOpen]);
async function clickHandler(monitoring: string) { async function clickHandler(monitoring: string) {
console.log('click', file.id);
setCurrentMonitoringStatus(monitoring); const response = await setMonitoring(file.id, monitoring);
await queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
setIsOpen(false); 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 ( return (
@@ -113,36 +120,36 @@ export function MonitoringDropDown({ file }: {
> >
<ul className="dropdown-list" role="listbox"> <ul className="dropdown-list" role="listbox">
<li <li
className={`dropdown-item ${currentMonitoringStatus === 'without-monitoring' ? 'current' : ''}`} className={`dropdown-item ${currentMonitoringStatus === 'NONE' ? 'current' : ''}`}
onClick={() => { onClick={() => {
clickHandler('without-monitoring') clickHandler('NONE')
}} }}
> >
{t('without-monitoring')} {t('NONE')}
</li> </li>
<li <li
className={`dropdown-item ${currentMonitoringStatus === 'every-day' ? 'current' : ''}`} className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_DAILY' ? 'current' : ''}`}
onClick={() => { onClick={() => {
clickHandler('every-day') clickHandler('MONITORING_DAILY')
}} }}
> >
{t('every-day')} {t('MONITORING_DAILY')}
</li> </li>
<li <li
className={`dropdown-item ${currentMonitoringStatus === 'every-week' ? 'current' : ''}`} className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_WEEKLY' ? 'current' : ''}`}
onClick={() => { onClick={() => {
clickHandler('every-week') clickHandler('MONITORING_WEEKLY')
}} }}
> >
{t('every-week')} {t('MONITORING_WEEKLY')}
</li> </li>
<li <li
className={`dropdown-item ${currentMonitoringStatus === 'every-month' ? 'current' : ''}`} className={`dropdown-item ${currentMonitoringStatus === 'MONITORING_MONTHLY' ? 'current' : ''}`}
onClick={() => { onClick={() => {
clickHandler('every-month') clickHandler('MONITORING_MONTHLY')
}} }}
> >
{t('every-month')} {t('MONITORING_MONTHLY')}
</li> </li>
</ul> </ul>
</div>, </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,44 @@
import { useQuery } from '@tanstack/react-query';
import { fetchHistoryView } from '@/app/actions/trackingActions';
export interface HistoryViewItem {
city: string;
country: string;
viewer: string;
document: string;
view_date: string;
}
export interface UseHistoryView {
documentsCount: number;
history: HistoryViewItem[];
pageNumber: number;
pageSize: number;
totalElements: number;
totalPages: number;
uniqueCityCount: number;
uniqueCountryCount: number;
}
export const useHistoryView = (
page: number,
size: number,
locale: string,
sortDirection?: 'asc' | 'desc',
sortBy?: string
) => {
return useQuery({
queryKey: ['violationStatistic', page, size, locale, sortDirection, sortBy],
queryFn: () => {
return fetchHistoryView(page, size, locale, sortDirection, sortBy)
},
select: (data: UseHistoryView | null) => {
if (!data) {
return null
}
return data;
},
retry: false,
placeholderData: (previousData) => previousData // для оптимистичных обновлений
});
};
+45
View File
@@ -0,0 +1,45 @@
import { useQuery } from '@tanstack/react-query';
import { fetchLastClaims, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
export interface Case {
amount: number;
content: null | string;
created_at: string;
description: string;
id: number;
lawyer: null | string;
name: string;
page_number: number;
page_size: number;
priority: "HIGH" | "MEDIUM" | "LOW";
total_elements: number;
total_pages: number;
type: "ACTIVE" | "INACTIVE" | string;
updated_at: string;
violation_id: number;
file_id: string;
}
export interface useLastCase {
content: Case[],
page: number,
size: number,
totalElements: number,
totalPages: number,
}
export const useLastCases = (params: ComplaintsCaseQueryParams) => {
return useQuery({
queryKey: ['lastCases', params.page, params.size, params.sort_by, params.sort_direction],
queryFn: () => {
return fetchLastClaims(params)
},
select: (data: useLastCase | null) => {
if (!data) {
return null
}
return data;
},
placeholderData: (previousData) => previousData,
retry: false
});
};
@@ -0,0 +1,38 @@
import { useQuery } from '@tanstack/react-query';
import { fetchLastComplaint, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
export interface Complaint {
complaint_text: string;
created_at: string;
email: string;
id: number;
not_moderated: boolean;
status: "CREATED" | string;
updated_at: string;
violation_id: number;
file_id: string;
}
export interface useLastComplaint {
content: Complaint[],
page: number,
size: number,
totalElements: number,
totalPages: number,
}
export const useLastComplaints = (params: ComplaintsCaseQueryParams) => {
return useQuery({
queryKey: ['lastComplaints', params.page, params.size, params.sort_by, params.sort_direction],
queryFn: () => {
return fetchLastComplaint(params)
},
select: (data: useLastComplaint | null) => {
if (!data) {
return null
}
return data;
},
placeholderData: (previousData) => previousData,
retry: false
});
};
@@ -0,0 +1,24 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMonitoringInfo } from '@/app/actions/monitoringActions';
interface MonitoringCount {
day: number;
month: number;
weekly: number;
}
export const useMonitoringCount = () => {
return useQuery({
queryKey: ['monitoringCount'],
queryFn: () => {
return fetchMonitoringInfo()
},
select: (data: MonitoringCount | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchMonitoringStats } from '@/app/actions/monitoringActions';
interface MonitoringStats {
total_searches: number;
searches_by_status: {
processing: number;
completed: number;
failed: number;
total: number;
};
recent_searches: any[];
}
export const useMonitoringStats = () => {
return useQuery({
queryKey: ['monitoringStats'],
queryFn: () => {
return fetchMonitoringStats()
},
select: (data: MonitoringStats | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { getPaymentsMethods } from '@/app/actions/paymentAction';
export interface PaymentsMethods {
cardType: string,
expiryMonth: string,
expiryYear: string,
lastFour: string,
paymentMethodId: string
}
export const usePaymentsMethods = () => {
return useQuery({
queryKey: ['paymentsMethods'],
queryFn: () => {
return getPaymentsMethods()
},
select: (data: PaymentsMethods[] | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import { fetchTrackingStats } from '@/app/actions/trackingActions';
export interface useTrackingStats {
totalViews: number,
uniqueIps: number,
uniqueUsers: number,
documentsCount: number,
uniqueCountryCount: number,
uniqueCityCount: number,
firstViewDate: string,
lastViewDate: string
}
export const useTrackingStats = () => {
return useQuery({
queryKey: ['trackingStats'],
queryFn: () => {
return fetchTrackingStats()
},
select: (data: useTrackingStats | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -39,6 +39,7 @@ export interface UserData {
tariffs: Tariff[]; tariffs: Tariff[];
tariffInfo: TariffInfo; tariffInfo: TariffInfo;
permission: number; permission: number;
verifiedStatus?: 'unknow' | 'NOT_VERIFIED' | 'VERIFICATION_IN_PROGRESS' | 'VERIFIED' | 'VERIFICATION_FAILED';
} }
export const useUserProfile = () => { export const useUserProfile = () => {
@@ -72,6 +73,7 @@ export const useUserProfile = () => {
maxFileCounts: 0, maxFileCounts: 0,
}, },
permission: 0, permission: 0,
verifiedStatus: 'unknow'
}; };
} }
@@ -32,6 +32,7 @@ export const useUserPaymentsData = () => {
return data; return data;
}, },
refetchInterval: 10000,
retry: false retry: false
}); });
}; };
@@ -0,0 +1,22 @@
import { useQuery } from '@tanstack/react-query';
import { fetchViolationAnalyticStatistic } from '@/app/actions/violationActions';
export interface UseViolationStatistic {
[key: string]: number
}
export const useViolationAnalyticStatistic = (group: 'domain' | 'tld') => {
return useQuery({
queryKey: ['violationAnalyticStatistic', group],
queryFn: () => {
return fetchViolationAnalyticStatistic(group)
},
select: (data: UseViolationStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchViolationFinalSummaryStatistic } from '@/app/actions/violationActions';
export interface UseViolationFinalSummaryStatistic {
complaints_without_case: number;
total_complaints: number;
total_law_cases: number;
total_violations: number;
violations_with_complaint: number;
violations_without_complaint: number;
}
export const useViolationFinalSummaryStatistic = () => {
return useQuery({
queryKey: ['violationFinalSummaryStatistic'],
queryFn: () => {
return fetchViolationFinalSummaryStatistic()
},
select: (data: UseViolationFinalSummaryStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { fetchViolationStatistic } from '@/app/actions/violationActions';
export interface UseViolationStatistic {
total_violations: number,
new_violations: number,
in_progress_violations: number,
resolved_violations: number,
total_complaints: number,
total_law_cases: number
}
export const useViolationStatistic = (id?: string) => {
return useQuery({
queryKey: ['violationStatistic', id],
queryFn: () => {
return fetchViolationStatistic(id)
},
select: (data: UseViolationStatistic | null) => {
if (!data) {
return null
}
return data;
},
retry: false
});
};
+36
View File
@@ -0,0 +1,36 @@
'use client'
import { useState, useEffect } from 'react'
export const MOBILE = 'MOBILE'
export const TABLET = 'TABLET'
export const DESKTOP = 'DESKTOP'
const getDevice = (width: number) => {
if (width < 768) return MOBILE
else if (width < 992) return TABLET
else return DESKTOP
}
export function useViewport() {
const [viewport, setViewport] = useState({
width: 0,
device: 'DESKTOP'
});
useEffect(() => {
const handleResize = () => {
setViewport({
width: window.innerWidth,
device: getDevice(window.innerWidth)
});
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return viewport;
}
+42 -1
View File
@@ -2,6 +2,34 @@ import { QueryClient } from '@tanstack/react-query';
import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action'; import { getUserData, getUserFilesInfo, getBuildData } from '@/app/actions/action';
import { getUserFilesData } from '@/app/actions/fileEntity'; import { getUserFilesData } from '@/app/actions/fileEntity';
import { fetchReferralUserStats } from '@/app/actions/referralsActions'; import { fetchReferralUserStats } from '@/app/actions/referralsActions';
import { getAllowedFilesExtensions } from '@/app/actions/fileUpload';
export interface allFilesExtensions {
videos: string[];
audios: string[];
images: string[];
documents: string[];
maxFileSize: number;
}
export async function getAllFilesExtensions(): Promise<allFilesExtensions> {
const [video, audio, image, document] = await Promise.all([
getAllowedFilesExtensions('video'),
getAllowedFilesExtensions('audio'),
getAllowedFilesExtensions('image'),
getAllowedFilesExtensions('document')
]);
const result: allFilesExtensions = {
videos: video?.file_extension ? video.file_extension : [],
audios: audio?.file_extension ? audio.file_extension : [],
images: image?.file_extension ? image.file_extension : [],
documents: document?.file_extension ? document.file_extension : [],
maxFileSize: video?.max_file_size ? video.max_file_size : 0
}
return result;
}
export async function prefetchLayoutQueries(queryClient: QueryClient) { export async function prefetchLayoutQueries(queryClient: QueryClient) {
await Promise.all([ await Promise.all([
@@ -12,7 +40,15 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
queryClient.prefetchQuery({ queryClient.prefetchQuery({
queryKey: ['userFilesData'], queryKey: ['userFilesData'],
queryFn: () => { queryFn: () => {
return getUserFilesData(1, 10000); return getUserFilesData({
page: 1,
pageSize: 10,
sortBy: '',
sortOrder: '',
type: '',
date: '',
query: ''
});
}, },
staleTime: 30 * 1000 staleTime: 30 * 1000
}), }),
@@ -35,5 +71,10 @@ export async function prefetchLayoutQueries(queryClient: QueryClient) {
} }
} }
}), }),
queryClient.prefetchQuery({
queryKey: ['allFilesExtensions'],
queryFn: () => getAllFilesExtensions(),
staleTime: 5 * 60 * 1000
}),
]); ]);
} }
+11
View File
@@ -0,0 +1,11 @@
'use client';
import Providers from './getQueryServer';
export function WatchDocProviders({ children }: { children: React.ReactNode }) {
return (
<Providers>
{children}
</Providers>
);
}
-23
View File
@@ -1,34 +1,11 @@
'use client' 'use client'
import { import {
isServer,
QueryClient,
QueryClientProvider, QueryClientProvider,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import { getQueryClient } from '@/app/providers/getQueryClient'; import { getQueryClient } from '@/app/providers/getQueryClient';
/* export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
return makeQueryClient()
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
} */
export default function Providers({ children }: { children: React.ReactNode }) { export default function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient(); const queryClient = getQueryClient();
+70
View File
@@ -0,0 +1,70 @@
@use './variable.scss' as v;
.loading-animation {
display: block;
text-align: center;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
.global-spinner {
display: block;
width: 24px;
height: 24px;
margin: 0 auto;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 4px solid #e5e7eb;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
&.large {
width: 80px;
height: 80px;
&::before {
border: 10px solid #e5e7eb;
border-top: 10px solid #667eea;
}
}
}
}
.loading-placeholder {
display: flex;
align-items: center;
justify-content: center;
/* min-width: 80px;
min-height: 80px; */
&.start {
justify-content: start;
}
.loading-animation {
display: block;
text-align: center;
position: relative;
left: auto;
top: auto;
transform: none;
}
.global-spinner {
display: inline-block;
width: 24px;
height: 24px;
position: relative;
}
}
+135 -2
View File
@@ -6,6 +6,10 @@
@use './reports.scss'; @use './reports.scss';
@use './privacy-and-terms-pages.scss'; @use './privacy-and-terms-pages.scss';
@use './VKLogin.scss'; @use './VKLogin.scss';
@use './animation.scss';
@use './violation-details.scss';
@use './watch-doc.scss';
@use './tracking-statistic.scss';
.page-title { .page-title {
font-size: 30px; font-size: 30px;
@@ -151,10 +155,50 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:disabled {
opacity: 0.5;
}
}
.btn,
.btn-s,
.btn-l {
border: none;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
&:disabled {
opacity: 0.5;
}
}
.btn {
padding: 12px 24px;
font-size: 14px;
border-radius: 10px;
}
.btn-s {
padding: 6px 12px;
font-size: 12px;
border-radius: 6px;
}
.btn-l {
padding: 16px 32px;
font-size: 16px;
border-radius: 12px;
} }
.btn-primary { .btn-primary {
background: linear-gradient(135deg, v.$p-color, v.$s-color); /* background: linear-gradient(135deg, v.$p-color, v.$s-color); */
background: #6366f1;
color: v.$white; color: v.$white;
border: 2px solid transparent; border: 2px solid transparent;
@@ -179,6 +223,16 @@
} }
} }
.btn-cancel {
background: v.$bg-hover;
color: v.$text-p;
border: 2px solid v.$border-color-1;
&:hover {
transform: translateY(-2px);
}
}
.btn-outline { .btn-outline {
background: transparent; background: transparent;
border: 2px solid v.$p-color; border: 2px solid v.$p-color;
@@ -190,6 +244,14 @@
} }
} }
.btn-warning {
background: v.$color-warning;
&:hover {
background: v.$color-warning-hover;
}
}
.divider { .divider {
text-align: center; text-align: center;
margin: 20px 0; margin: 20px 0;
@@ -348,7 +410,7 @@
svg { svg {
margin-right: 15px; margin-right: 15px;
width: 20px; width: 20px;
height: 20px; min-width: 20px;
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
@@ -711,3 +773,74 @@
font-weight: 500; font-weight: 500;
} }
} }
.yandex-auth-button {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
width: 100%;
box-sizing: border-box;
background: v.$black;
color: #FFFFFF;
&:hover {
background: #1a1a1a;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
/* transform: translateY(-2px); */
}
}
.search-settings {
&-wrapper {
margin-bottom: 20px;
background: #f0f9ff;
border: 1px solid #bfdbfe;
border-radius: 12px;
margin-bottom: 20px;
padding: 16px;
color: #1e40af;
}
&-header {
display: flex;
gap: 10px;
font-weight: 600;
&.loading {
opacity: 0.5;
}
}
&-item {
cursor: pointer;
span {
margin-left: 4px;
color: v.$red;
&.active {
color: v.$green;
}
}
&:hover {
text-decoration: underline;
}
}
/* &-action-panel {
display: flex;
gap: 10px;
} */
}
+3
View File
@@ -53,3 +53,6 @@ path:focus {
} }
@import "tailwindcss"; @import "tailwindcss";
@import 'swiper/css';
@import 'swiper/css/navigation';
@import 'swiper/css/pagination';
+113 -29
View File
@@ -4,11 +4,12 @@
display: flex; display: flex;
justify-content: end; justify-content: end;
align-items: center; align-items: center;
margin-bottom: 30px;
background: v.$white; background: v.$white;
padding: 20px 30px; padding: 20px 30px;
border-radius: 20px; border-radius: 20px;
box-shadow: 0 4px 20px v.$shadow-1; box-shadow: 0 4px 20px v.$shadow-1;
max-width: 1920px;
margin: 0 auto 30px;
@media (max-width: 490px) { @media (max-width: 490px) {
padding: 20px 20px; padding: 20px 20px;
@@ -49,6 +50,10 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
/* &.active {
background: v.$color-warning;
} */
} }
} }
@@ -85,6 +90,11 @@
@media (max-width: 440px) { @media (max-width: 440px) {
padding: 8px; padding: 8px;
} }
&:hover {
background: v.$p-color;
color: v.$white;
}
} }
.tokens-count { .tokens-count {
@@ -110,7 +120,7 @@
background: v.$white; background: v.$white;
border-radius: 12px; border-radius: 12px;
box-shadow: 10px 10px 20px 10px v.$shadow-1; box-shadow: 10px 10px 20px 10px v.$shadow-1;
min-width: 320px; min-width: 350px;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transform: translateY(-10px); transform: translateY(-10px);
@@ -124,8 +134,10 @@
} }
} }
.notification-dropdown { .notification {
.notification-header { /* &-dropdown {} */
&-header {
padding: 15px 20px; padding: 15px 20px;
border-bottom: 1px solid v.$b-color-1; border-bottom: 1px solid v.$b-color-1;
display: flex; display: flex;
@@ -138,65 +150,137 @@
font-weight: 600; font-weight: 600;
color: v.$text-p; color: v.$text-p;
} }
.notification-count {
font-size: 12px;
color: v.$p-color;
font-weight: 500;
}
} }
.notification-list { &-count {
max-height: 400px; font-size: 10px;
background: v.$p-color;
padding: 5px;
color: v.$white;
font-weight: 500;
border-radius: 5px;
margin-left: 5px;
user-select: none;
}
&-list {
max-height: 600px;
overflow-y: auto; overflow-y: auto;
} }
.notification-item { &-item {
display: flex; display: flex;
gap: 12px; gap: 8px;
padding: 15px 20px; padding: 10px 14px;
border-bottom: 1px solid v.$b-color-1; border-bottom: 1px solid v.$b-color-1;
transition: background-color 0.2s; transition: background-color 0.2s;
cursor: pointer; align-items: center;
&.loading {
opacity: 0.5;
}
} }
.notification-title { &-title {
display: block; display: block;
font-size: 15px; font-size: 13px;
font-weight: 700; font-weight: 700;
color: v.$text-p; color: v.$text-p;
margin-bottom: 6px; margin-bottom: 6px;
} }
.notification-icon { &-content {
font-size: 24px; width: 100%;
flex-shrink: 0; position: relative;
padding: 5px;
} }
.notification-text { &-item-header {
display: flex;
justify-content: space-between;
}
&-text {
font-size: 13px; font-size: 13px;
color: v.$text-s; color: v.$text-s;
margin-bottom: 4px;
font-weight: 500; font-weight: 500;
} }
.notification-time { &-data {
font-size: 12px; font-size: 10px;
color: v.$text-m; color: v.$text-m;
} }
.notification-footer { &-icon {
padding: 12px 20px; font-size: 12px;
cursor: pointer;
height: max-content;
color: v.$p-color;
align-self: start;
padding-top: 5px;
svg {
fill: v.$p-color;
&:hover {
fill: v.$p-color-hover;
}
}
&:hover {
color: v.$p-color-hover;
}
}
&-mark-all-as-read {
font-size: 10px;
color: v.$p-color;
text-align: end;
cursor: pointer;
&:hover {
color: v.$p-color-hover;
}
}
&-footer {
padding: 8px 20px;
border-top: 1px solid v.$b-color-1; border-top: 1px solid v.$b-color-1;
text-align: center; text-align: center;
position: relative;
.notification-link { .notification-link {
color: v.$p-color; color: v.$p-color;
font-size: 14px; font-size: 16px;
font-weight: 500; font-weight: 600;
text-decoration: none; text-decoration: none;
cursor: pointer;
&:disabled {
opacity: 0.5;
}
&:hover {
color: v.$p-color-hover;
}
} }
} }
&-unread-count {
position: absolute;
right: -5px;
bottom: -5px;
/* width: 22px;
height: 22px; */
border-radius: 15px;
background: v.$red;
color: v.$white;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
padding: 2px 5px;
}
} }
.user-menu { .user-menu {
+11 -1
View File
@@ -112,6 +112,15 @@
&.password { &.password {
padding-right: 32px; padding-right: 32px;
} }
&:disabled {
opacity: 0.5;
}
&.valid-card {
border: 2px solid #2f9e44;
background: #ecf8ee;
}
} }
.form-options { .form-options {
@@ -198,9 +207,10 @@
min-width: 500px; min-width: 500px;
&-button-group { &-button-group {
display: flex;
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
display: grid;
grid-template-columns: 50% 50%;
} }
@media (max-width: 550px) { @media (max-width: 550px) {
File diff suppressed because it is too large Load Diff
+125 -3
View File
@@ -201,10 +201,18 @@
} }
.subscription-grid { .subscription-grid {
display: grid; /* display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-columns: repeat(4, auto);
gap: 20px; gap: 20px;
margin-bottom: 25px; margin-bottom: 25px;
justify-content: center; */
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 278px));
gap: 18px;
margin-bottom: 25px;
justify-content: center;
} }
.plan-card { .plan-card {
@@ -216,7 +224,7 @@
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer; cursor: pointer;
overflow: visible; overflow: visible;
max-width: 350px; max-width: 278px;
.plan-header { .plan-header {
text-align: center; text-align: center;
@@ -249,6 +257,7 @@
.plan-period { .plan-period {
color: #64748b; color: #64748b;
font-size: 0.85rem; font-size: 0.85rem;
text-transform: lowercase;
} }
.plan-tokens { .plan-tokens {
@@ -298,6 +307,97 @@
} }
} }
} }
.token-package {
background: #f8fafc;
border: 2px solid #e2e8f0;
border-radius: 16px;
padding: 20px;
text-align: center;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-4px) scale(1.02);
border-color: #6366f1;
box-shadow: 0 12px 30px rgba(99, 102, 241, 0.2);
&::before {
left: 100%;
}
}
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(99, 102, 241, 0.1), transparent);
transition: left 0.5s ease;
}
&.selected {
border-color: #10b981;
background: linear-gradient(135deg, #f0fdf4, #dcfce7);
transform: scale(1.05);
}
.token-package-amount {
font-size: 1.8rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 6px;
}
.token-package-text {
font-size: 0.85rem;
color: #64748b;
margin-bottom: 8px;
}
.token-package-price {
font-size: 1.3rem;
font-weight: 600;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
}
.token-package-detail {
color: #64748b;
font-size: 0.8rem;
margin-top: 6px;
}
}
.plan-button {
width: 100%;
max-width: 280px;
padding: 12px 18px;
border: none;
border-radius: 12px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
display: block;
margin: 0 auto;
}
.selected-token-info {
margin-top: 8px;
color: #64748b;
font-size: 0.85rem;
text-align: center;
}
} }
.secure-payments-wrapper { .secure-payments-wrapper {
@@ -324,16 +424,38 @@
.secure-payments-card { .secure-payments-card {
padding: 12px; padding: 12px;
display: flex;
flex-direction: column;
justify-content: space-around;
/* &:has(.yookassa) {
align-self: flex-end;
} */
.secure-payments-card-header { .secure-payments-card-header {
font-size: 1.8rem; font-size: 1.8rem;
margin-bottom: 8px; margin-bottom: 8px;
height: 44px;
&.yookassa {
max-width: 88px;
margin: 0 auto 8px;
display: flex;
align-items: center;
}
} }
h4 { h4 {
color: #1e293b; color: #1e293b;
margin-bottom: 6px; margin-bottom: 6px;
font-size: 0.95rem; font-size: 0.95rem;
height: 32px;
display: flex;
justify-content: center;
svg {
max-width: 88px;
margin: 0 auto 8px;
}
} }
p { p {
+38 -15
View File
@@ -7,6 +7,7 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
margin-bottom: 30px; margin-bottom: 30px;
max-width: calc(100vw - var(--side-bar-width) - 30px);
.enhanced-chart-title { .enhanced-chart-title {
font-size: 22px; font-size: 22px;
@@ -14,12 +15,7 @@
color: white; color: white;
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 20px;
&::before {
content: '🔍';
margin-right: 12px;
font-size: 28px;
}
} }
.search-stats-grid { .search-stats-grid {
@@ -49,7 +45,13 @@
.search-stat-icon { .search-stat-icon {
font-size: 32px; font-size: 32px;
margin-bottom: 15px; margin-bottom: 15px;
display: block; display: flex;
justify-content: center;
.icon {
width: 50px;
height: 50px;
}
} }
.search-stat-value { .search-stat-value {
@@ -68,11 +70,19 @@
} }
.analytics-grid { .analytics-grid {
display: flex;
flex-wrap: wrap;
gap: 30px; gap: 30px;
margin-bottom: 40px; margin-bottom: 10px;
justify-content: space-between; display: grid;
grid-template-columns: 1fr 1fr;
.tanstak-table-wrapper .tanstak-table {
width: 100%;
}
@media (max-width: 1370px) {
flex-direction: column;
grid-template-columns: 1fr;
}
.analytics-card { .analytics-card {
background: white; background: white;
@@ -80,10 +90,12 @@
padding: 30px; padding: 30px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease; transition: all 0.3s ease;
flex: 1;
min-width: 350px;
max-width: calc(50% - 15px);
margin-bottom: 20px; margin-bottom: 20px;
width: 100%;
@media (max-width: 970px) {
max-width: calc(100vw - var(--side-bar-width) - 30px);
}
} }
.analytics-header { .analytics-header {
@@ -152,6 +164,7 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 20px 40px rgba(30, 58, 138, 0.4); box-shadow: 0 20px 40px rgba(30, 58, 138, 0.4);
max-width: calc(100vw - var(--side-bar-width) - 30px);
&::before { &::before {
content: ''; content: '';
@@ -189,10 +202,14 @@
.summary-stats { .summary-stats {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: 1fr 2fr 1fr;
gap: 30px; gap: 30px;
position: relative; position: relative;
z-index: 2; z-index: 2;
@media (max-width: 940px) {
grid-template-columns: 1fr;
}
} }
.summary-stat { .summary-stat {
@@ -204,6 +221,12 @@
border: 2px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.2);
transition: all 0.4s ease; transition: all 0.4s ease;
&.center {
display: flex;
justify-content: space-around;
gap: 10px;
}
&:hover { &:hover {
transform: translateY(-8px) scale(1.02); transform: translateY(-8px) scale(1.02);
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
+58
View File
@@ -219,6 +219,64 @@
padding: 20px; padding: 20px;
} }
} }
&.verefication-section {
height: 100%;
display: flex;
flex-direction: column;
.verefication-block {
flex-grow: 1;
display: flex;
flex-direction: column;
h3 {
margin-bottom: 10px;
}
.drag-drop-zone {
border: 2px dashed #6366f1;
border-radius: .75rem;
padding: 10px;
}
.selected-file-action-panel {
display: flex;
justify-content: center;
gap: 10px;
}
.selected-file-actions {
display: flex;
justify-content: center;
gap: 10px;
}
.description {
font-size: 12px;
color: v.$text-s;
}
.btn {
padding: 5px 12px;
}
.btn-confirm {
background: v.$p-color;
color: v.$white;
background: linear-gradient(135deg, v.$p-color, v.$s-color);
color: v.$white;
border: 2px solid transparent;
padding: 2px 10px;
}
.btn-cancel {
border: 2px solid v.$p-color;
padding: 2px 10px;
}
}
}
} }
.danger-zone { .danger-zone {
+87
View File
@@ -0,0 +1,87 @@
@use './variable.scss' as v;
:root {
--p: #6366f1;
--pd: #4f46e5;
--pl: #e0e7ff;
--g: #10b981;
--a: #f59e0b;
--r: #f43f5e;
--s: #0ea5e9;
--bg: #f1f5f9;
--sur: #fff;
--brd: #e2e8f0;
--tx: #1e293b;
--mu: #64748b;
--rad: 14px;
--sh: 0 2px 10px rgba(0, 0, 0, .07);
}
.tracking-page {
.general-statistic {
background: linear-gradient(135deg, v.$p-color 55%, v.$s-color);
border-radius: var(--rad);
padding: 32px;
color: #fff;
margin-bottom: 24px;
position: relative;
overflow: hidden;
&-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 24px;
}
&-title {
font-size: 24px;
font-weight: 800;
}
&-sub {
font-size: 13px;
opacity: .75;
margin-top: 4px;
}
&-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 14px;
}
&-item {
background: rgba(255, 255, 255, .13);
backdrop-filter: blur(8px);
border-radius: 12px;
padding: 16px 18px;
border: 1px solid rgba(255, 255, 255, .15);
&-ico {
font-size: 20px;
margin-bottom: 6px;
svg {
fill: v.$white;
color: v.$white;
}
}
&-lbl {
font-size: 11px;
opacity: .8;
text-transform: uppercase;
letter-spacing: .5px;
margin-top: 4px;
}
&-val {
font-size: 30px;
font-weight: 800;
line-height: 1;
}
}
}
}
+15
View File
@@ -1,4 +1,5 @@
$p-color: #6366f1; $p-color: #6366f1;
$p-color-hover: #1e0ff0;
$s-color: #8b5cf6; $s-color: #8b5cf6;
$text-p: #1f2937; $text-p: #1f2937;
$text-s: #4b5563; $text-s: #4b5563;
@@ -10,17 +11,31 @@ $bg-hover: #e9ecef;
$bg-light: #f8f9ff; $bg-light: #f8f9ff;
$bg-darkening: #0000007a; $bg-darkening: #0000007a;
$shadow-1: #0000001a; $shadow-1: #0000001a;
$shadow-2: #8686861a;
$white: #fff; $white: #fff;
$black: #000;
$red: #dc2626; $red: #dc2626;
$green: #00a63e; $green: #00a63e;
$green-2: #10b981;
$grey: #5a6e8a;
$p-color-disabled: #6365f18e; $p-color-disabled: #6365f18e;
$s-color-disabled: #8a5cf663; $s-color-disabled: #8a5cf663;
$border-color-1: #e2e8f0;
$border-color-1-hover: #b1b7be;
$color-warning: #fab005;
$color-warning-hover: #fa9805;
$color-warning-2: #f08c00;
$color-image: #f08c00; $color-image: #f08c00;
$color-video: #2f9e44; $color-video: #2f9e44;
$color-audio: #1971c2; $color-audio: #1971c2;
$color-document: #a561e6; $color-document: #a561e6;
$status-new: #fab005;
$status-showed: #5a6e8a;
$status-in-work: #3b82f6;
$authorized-use: #2ecc71;
:root { :root {
--side-bar-width: 280px; --side-bar-width: 280px;
File diff suppressed because it is too large Load Diff
+319
View File
@@ -0,0 +1,319 @@
@use './variable.scss' as v;
:root {
--bg: #f5f6fa;
--card: #fff;
--border: #e2e5ef;
--border2: #d0d4e4;
--text: #0f1221;
--text2: #4b5270;
--muted: #8890ad;
--accent: #5b47e0;
--accent2: #7c5cf6;
--green: #059669;
--green-bg: #ecfdf5;
--green-bd: #a7f3d0;
--red: #dc2626;
--red-bg: #fef2f2;
--amber: #b45309;
--shadow: 0 2px 12px rgba(15, 18, 33, .07);
}
.watch-doc {
&-wrapper {
background: var(--bg);
color: var(--text);
font-size: 15px;
line-height: 1.6;
min-height: 100vh;
.btn-acc {
background: var(--accent);
color: #fff;
box-shadow: 0 3px 12px rgba(91, 71, 224, .28);
margin: 0 auto;
display: block;
&:hover {
background: var(--accent2);
text-decoration: none;
}
}
.btn-out {
background: var(--card);
color: var(--text2);
border: 1px solid var(--border2);
width: 100%;
&:hover {
border-color: var(--accent);
color: var(--accent);
text-decoration: none;
}
}
.document-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
gap: 10px;
&__nav {
display: flex;
align-items: center;
gap: 8px;
}
&__next,
&__prev {
font-size: 0.75rem;
color: var(--accent);
background: none;
border: 1px solid var(--border2);
border-radius: 6px;
padding: 3px 10px;
cursor: pointer;
font-family: inherit;
display: inline;
&:hover {
background: var(--accent);
color: #fff;
}
}
&__current-page {
font-size: 0.75rem;
color: var(--muted);
display: inline;
}
&__fullscreen {
font-size: .75rem;
color: var(--accent);
background: none;
border: 1px solid var(--border2);
border-radius: 6px;
padding: 3px 10px;
cursor: pointer;
font-family: inherit;
&:hover {
background: var(--accent);
color: #fff;
}
}
}
.prev-wrap {
min-height: 400px;
min-width: 300px;
position: relative;
display: flex;
justify-content: center;
}
.pdf-canvas {
display: block;
max-width: 100%;
height: auto;
}
.pdf-container {
width: 100%;
overflow-x: auto;
}
.pdf-container:fullscreen {
background: #f5f5f5;
padding: 20px;
gap: 20px;
.prev-wrap {
flex: 1;
min-height: auto;
display: flex;
justify-content: center;
align-items: center;
background: #f5f5f5;
}
.pdf-canvas {
max-width: 100%;
max-height: calc(100vh - 120px);
width: auto;
height: auto;
object-fit: contain;
}
.document-controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-radius: 12px;
padding: 0.75rem 1.5rem;
z-index: 1000;
min-width: 300px;
justify-content: center;
}
}
.error-container {
text-align: center;
}
}
&-page {
max-width: 880px;
margin: 0 auto;
padding: 0 16px 60px;
}
&-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 0 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 28px;
.header-logo {
font-size: 1.1rem;
font-weight: 700;
color: v.$p-color;
display: flex;
align-items: center;
gap: 7px;
}
.header-user {
font-size: .83rem;
color: var(--text2);
background: #fff;
border: 1px solid var(--border);
border-radius: 8px;
padding: 7px 14px;
}
}
&-status {
background: var(--green-bg);
border: 1.5px solid var(--green-bd);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
margin-bottom: 22px;
padding: 20px 22px;
border-radius: 14px;
.st-title {
color: var(--green);
font-size: 1.05rem;
font-weight: 700;
margin-bottom: 3px;
}
.st-sub {
font-size: .83rem;
color: var(--text2);
line-height: 1.5;
}
}
&-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
&-footer {
text-align: center;
padding: 28px 0 0;
color: var(--muted);
font-size: .76rem;
border-top: 1px solid var(--border);
margin-top: 14px;
display: flex;
justify-content: center;
gap: 10px;
}
&-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
box-shadow: var(--shadow);
height: 100%;
.card-head {
padding: 14px 20px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 9px;
font-size: .78rem;
font-weight: 700;
color: var(--text);
text-transform: uppercase;
letter-spacing: .6px;
}
.card-body {
padding: 14px 20px;
.owner-row {
display: flex;
align-items: center;
gap: 12px;
}
.owner-av {
width: 42px;
height: 42px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.1rem;
font-weight: 700;
flex-shrink: 0;
}
.owner-name {
font-weight: 700;
margin-bottom: 5px;
}
.owner-meta {
color: var(--muted);
}
}
.download-description {
font-size: .82rem;
color: var(--text2);
margin-bottom: 14px;
text-align: center;
}
}
&-actions {
display: flex;
gap: 9px;
flex-wrap: wrap;
margin-top: 16px;
}
}
+4 -3
View File
@@ -1,13 +1,14 @@
import TanstakFilesTable from '@/app/components/tanstak-table/TanstakTable'; import TanstakFilesTable from '@/app/components/tanstak-table/TanstakTable';
type FilesTable = { type FilesTable = {
fileType: string fileType: string;
showFileLink?: boolean;
} }
export default function FilesTable({ fileType }: FilesTable) { export default function FilesTable({ fileType, showFileLink }: FilesTable) {
return ( return (
<div className="block-wrapper"> <div className="block-wrapper">
<TanstakFilesTable fileType={fileType} /> <TanstakFilesTable fileType={fileType} showFileLink={showFileLink}/>
</div> </div>
) )
} }
@@ -25,10 +25,6 @@ export default function DashboardFilesInfo() {
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.images.violation ? filesInfo?.images.violation : 0}</span>
</div>
</div> </div>
</div> </div>
@@ -47,10 +43,6 @@ export default function DashboardFilesInfo() {
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.videos.violation ? filesInfo?.videos.violation : 0}</span>
</div>
</div> </div>
</div> </div>
@@ -69,10 +61,6 @@ export default function DashboardFilesInfo() {
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.audios.violation ? filesInfo?.audios.violation : 0}</span>
</div>
</div> </div>
</div> </div>
@@ -85,16 +73,12 @@ export default function DashboardFilesInfo() {
<div className="type-card-stats"> <div className="type-card-stats">
<div className="type-card-stat-row"> <div className="type-card-stat-row">
<span className="type-card-stat-label">{t('size')}</span> <span className="type-card-stat-label">{t('size')}</span>
<span className="type-card-stat-value">{filesInfo?.documents.check? convertBytes(filesInfo?.documents.check) : 0}</span> <span className="type-card-stat-value">{filesInfo?.documents.size? convertBytes(filesInfo?.documents.size) : 0}</span>
</div> </div>
<div className="type-card-stat-row"> <div className="type-card-stat-row">
<span className="type-card-stat-label">{t('PROTECTED')}</span> <span className="type-card-stat-label">{t('PROTECTED')}</span>
<span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span> <span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span>
</div> </div>
<div className="type-card-stat-row">
<span className="type-card-stat-label">{t('violations')}</span>
<span className="type-card-stat-value">{filesInfo?.documents.violation ? filesInfo?.documents.violation : 0}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,26 +1,63 @@
'use client'
import { useQuery } from '@tanstack/react-query';
import Link from 'next/link'; import Link from 'next/link';
import { fetchLastMonitoringCheck } from '@/app/actions/monitoringActions';
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
import { useTranslations } from 'next-intl';
export default function DashboardLastCheck() { export default function DashboardLastCheck() {
const { data, isLoading } = useQuery({
queryKey: ['lastMonitoringCheck'],
queryFn: () => {
return fetchLastMonitoringCheck()
},
select: (data) => {
if (!data) {
return null
}
return data;
},
retry: false
});
const t = useTranslations('Global');
return ( return (
<div className="dashboard-last-check"> <div className="dashboard-last-check">
<div className="last-check-info"> <div className="last-check-info">
<div className="last-check-title">Последняя проверка контента</div> <div className="last-check-title">
<div className="last-check-date"> {t('last-content-check')}
17.10.2025 в 10:23</div> </div>
<div
className="relative"
>
{isLoading ? (
<div
className="loading-animation"
>
<div className="global-spinner"></div>
</div>
) : (
<div className="last-check-date">
{data?.recent_searches[0]?.created_at ? `${formatDate(data.recent_searches[0].created_at)} ${t('in')} ${formatDateTime(data.recent_searches[0].created_at)}` : '---'}
</div>
)}
</div>
</div> </div>
<div className="last-check-actions"> <div className="last-check-actions">
<Link <Link
className="btn-report" className="btn-report"
href={'reports'} href={'/pages/reports'}
> >
📄 Отчёт {t('reports')}
</Link> </Link>
<Link {/* <Link
className="btn-new-search" className="btn-new-search"
href={'violations'} href={'violations'}
> >
🔍 Мониторинг нарушений Мониторинг нарушений
</Link> </Link> */}
</div> </div>
</div> </div>
) )
@@ -2,13 +2,10 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getUserFilesData } from '@/app/actions/fileEntity'; import { getUserFilesData } from '@/app/actions/fileEntity';
import { useEffect, useState, useRef } from 'react';
import { FileTypeIcon } from '@/app/components/FileTypeIcon'; import { FileTypeIcon } from '@/app/components/FileTypeIcon';
import { convertBytes } from '@/app/lib/convertBytes'; import { convertBytes } from '@/app/lib/convertBytes';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import Link from 'next/link'; import Link from 'next/link';
import { getSessionData } from '@/app/actions/session';
type FileItem = { type FileItem = {
id: string; id: string;
@@ -42,7 +39,15 @@ export default function DashboardUserFiles() {
error, error,
} = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({ } = useQuery<ApiResponse, Error, FileItem[], ['userFilesData', number, number]>({
queryKey: ['userFilesData', 1, 5], queryKey: ['userFilesData', 1, 5],
queryFn: () => getUserFilesData(1, 5), queryFn: () => getUserFilesData({
page: 1,
pageSize: 5,
sortBy: 'updatedAt',
sortOrder: 'desc',
type: '',
date: '',
query: ''
}),
select: (data: ApiResponse): FileItem[] => { select: (data: ApiResponse): FileItem[] => {
if (!data?.files) return []; if (!data?.files) return [];
@@ -64,15 +69,6 @@ export default function DashboardUserFiles() {
}); });
const t = useTranslations('Global'); const t = useTranslations('Global');
const [openDropDownList, setOpenDropDownList] = useState(false);
const dropDownList = useRef(null);
useClickOutside(
dropDownList,
() => {
setOpenDropDownList(false)
},
openDropDownList
);
const formatDate = (timestamp: number) => { const formatDate = (timestamp: number) => {
const date = new Date(timestamp); const date = new Date(timestamp);
@@ -86,36 +82,29 @@ export default function DashboardUserFiles() {
return ( return (
<div className="content-section"> <div className="content-section">
<div className="section-header"> <div className="section-header">
<h3 className="section-title">Ваши файлы</h3> <h3 className="section-title">
{t('your-files')}
</h3>
<div <div
className="section-add-file btn view-all-link" className="btn view-all-link"
onClick={() => {
setOpenDropDownList(!openDropDownList);
}}
ref={dropDownList}
> >
{t('add')} <Link href='/pages/my-content'>
<div {t('all')}
className={`section-drop-down-list ${openDropDownList ? 'opened' : ''}`} </Link>
>
<div
className="flex flex-col text-center"
>
<Link href='/pages/marking-images'>
{t('photo-marking')}
</Link>
<Link href='/pages/marking-video'>
{t('video-marking')}
</Link>
<Link href='/pages/marking-audio'>
{t('audio-marking')}
</Link>
</div>
</div>
</div> </div>
</div> </div>
<div className="user-files-list"> <div className="user-files-list">
{isLoading && (
<div
className="loading-animation"
>
<div
className="global-spinner"
>
</div>
</div>
)}
{tableData?.length ? ( {tableData?.length ? (
tableData?.map((file) => { tableData?.map((file) => {
@@ -142,9 +131,11 @@ export default function DashboardUserFiles() {
) )
}) })
) : ( ) : (
<div> !isLoading && (
{t('there-are-no-files-yet')} <div>
</div> {t('there-are-no-files-yet')}
</div>
)
)} )}
</div> </div>
</div> </div>
+139 -37
View File
@@ -1,52 +1,154 @@
'use client' 'use client'
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import {convertBytes} from '@/app/lib/convertBytes'; import { convertBytes } from '@/app/lib/convertBytes';
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo'; import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
import { useEffect } from 'react';
import { useViolationStatistic } from '@/app/hooks/react-query/useViolationStatistic';
import { IconDocument, IconSearch, IconWarning, IconGraph, IconDiscet, IconShield } from '@/app/ui/icons/icons';
export default function DashboardUserStats() { export default function DashboardUserStats() {
const t = useTranslations("Global"); const t = useTranslations("Global");
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo(); const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
const { data: violationStatistic, isLoading: violationStatisticLoading } = useViolationStatistic();
return ( return (
<div className="dashboard-user-stats"> <>
<div className="dashboard-user-stats">
<div className="stat-card projects"> <div className="stat-card projects">
<div className="stat-type">ЗАЩИЩЁННЫЕ ФАЙЛЫ</div> <div className="stat-header">
<div className="stat-value">{filesInfo?.total.protected ? filesInfo?.total.protected : 0}</div> <div className="stat-icon">
<div className="stat-label">Файлов под защитой</div> <IconShield />
<div className="stat-icon">🛡</div> </div>
</div> </div>
<div className="stat-value">
<div className="stat-card matches"> <div
<div className="stat-type">СОВПАДЕНИЯ</div> className="loading-placeholder start"
<div className="stat-value">0</div> >
<div className="stat-label">Найдено совпадений</div> {isLoading ? (
<div className="stat-icon">🔍</div> <div className="loading-animation">
</div> <div className="global-spinner" />
</div>
<div className="stat-card violations"> ) : (
<div className="stat-type">НАРУШЕНИЯ</div> filesInfo?.total.protected ? filesInfo?.total.protected : 0
<div className="stat-value">{filesInfo?.total.violation ? filesInfo?.total.violation : 0}</div> )}
<div className="stat-label">Требуют внимания</div> </div>
<div className="stat-icon"></div> </div>
</div> <div className="stat-label">
{t('files-under-protection')}
<div className="stat-card storage"> </div>
<div className="stat-type">ХРАНИЛИЩЕ</div>
<div className="stat-value">
{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0}
</div> </div>
<div className="stat-label">Использовано места</div>
<div className="stat-icon">💾</div>
</div>
<div className="stat-card legal"> <div className="stat-card matches">
<div className="stat-type">ЖАЛОБЫ</div> <div className="stat-header">
<div className="stat-value">0</div> <div className="stat-icon">
<div className="stat-label">Отправлено юристам</div> <IconSearch />
<div className="stat-icon"></div> </div>
</div>
{/* <div className="stat-value">
<div
className="loading-placeholder start"
>
{isLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
filesInfo?.total.check ? filesInfo?.total.check : 0
)}
</div>
</div> */}
<div className="stat-value">
<div
className="loading-placeholder start"
>
{violationStatisticLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
violationStatistic?.total_violations ?? 0
)}
</div>
</div>
<div className="stat-label">
{t('matches-found')}
</div>
</div>
<div className="stat-card violations hidden">
<div className="stat-header">
<div className="stat-icon">
<IconWarning />
</div>
</div>
<div className="stat-value">
<div
className="loading-placeholder start"
>
{violationStatisticLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
violationStatistic?.total_violations ?? 0
)}
</div>
</div>
<div className="stat-label">
{t('matches')}
</div>
</div>
<div className="stat-card storage">
<div className="stat-header">
<div className="stat-icon">
<IconDiscet />
</div>
</div>
<div className="stat-value">
<div
className="loading-placeholder start"
>
{isLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0
)}
</div>
</div>
<div className="stat-label">
{t('disk-space-used')}
</div>
</div>
<div className="stat-card legal">
<div className="stat-header">
<div className="stat-icon">
<IconDocument />
</div>
</div>
<div className="stat-value">
<div
className="loading-placeholder start"
>
{violationStatisticLoading ? (
<div className="loading-animation">
<div className="global-spinner" />
</div>
) : (
violationStatistic?.in_progress_violations ?? 0
)}
</div>
</div>
<div className="stat-label">
{t('violations-in-progress')}
</div>
</div>
</div> </div>
</div> </>
) )
} }
@@ -0,0 +1,122 @@
'use client'
import { useQuery } from '@tanstack/react-query';
import { getUserFilesData } from '@/app/actions/fileEntity';
import { useEffect, useState, useRef } from 'react';
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
import { convertBytes } from '@/app/lib/convertBytes';
import { useTranslations } from 'next-intl';
import { useClickOutside } from '@/app/hooks/useClickOutside';
import Link from 'next/link';
import { getViolationFilesArray } from '@/app/actions/violationActions';
import { IconEye } from '@/app/ui/icons/icons';
interface FileViolation {
createdAt: string;
fileId: string;
fileName: string;
fileSize: number;
latestViolationDate: string;
mimeType: string;
status: string;
supportId: number;
violationCount: number;
}
interface ViolationData {
content: FileViolation[];
}
export default function DashboardUserViolations() {
const {
data: violationData,
isLoading,
isError,
error,
} = useQuery<ViolationData>({
queryKey: ['violationData', { page: 0, size: 5 }],
queryFn: () => getViolationFilesArray({
page: 0,
size: 5,
}),
select: (data) => {
return data;
}
});
const t = useTranslations('Global');
const formatDate = (timestamp: number | string) => {
const date = new Date(timestamp);
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
return (
<div className="content-section">
<div className="section-header">
<h3 className="section-title">
{t('violation')}
</h3>
<div
className="btn view-all-link"
>
<Link href='/pages/violations'>
{t('all')}
</Link>
</div>
</div>
<div className="user-files-list">
{isLoading && (
<div
className="loading-animation"
>
<div
className="global-spinner"
>
</div>
</div>
)}
{violationData?.content?.length ? (
violationData?.content.map((file) => {
return (
<div
key={file.fileId}
className="file-item"
>
<div className="file-icon violent">
<Link
href={`/pages/file/${file.fileId}`}
className="bg-violet-500 hover:bg-violet-600"
title={t('view')}
>
<IconEye />
</Link>
</div>
<div className="file-info">
<div className="file-name" title={file.fileName}>
{file.fileName}
</div>
<div className="file-meta">
{file.createdAt ? formatDate(file.createdAt) : 0} {t('violations-found')}: {file.violationCount ? file.violationCount : 0}
</div>
</div>
</div>
)
})
) : (
!isLoading && (
<div>
{t('there-are-no-files-yet')}
</div>
)
)}
</div>
</div>
)
}
@@ -1,274 +0,0 @@
'use client'
import { Swiper, SwiperSlide } from 'swiper/react';
import { Pagination, Autoplay } from 'swiper/modules';
import Image from 'next/image';
import 'swiper/css';
import 'swiper/css/pagination';
import testImage from '@/app/src/image-preview.png'
import { useRef } from 'react';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
export default function ViolationsCarousel() {
const paginationRef = useRef(null);
const t = useTranslations('Global');
return (
<div className="content-section">
<div className="section-header">
<h3 className="section-title">Нарушения</h3>
<Link
href="violations"
className="view-all-link"
>
Смотреть все
</Link>
</div>
<div>
{t('are-no-violations')}
</div>
{/* <div className="violations-carousel">
<Swiper
spaceBetween={50}
slidesPerView={1}
modules={[Pagination, Autoplay]}
pagination={{
clickable: true,
el: '.custon-swiper-pagination'
}}
loop={true}
autoplay={{
delay: 3000,
disableOnInteraction: false,
}}
>
<SwiperSlide>
<div className="violation-slide">
<div className="violation-image-container">
<Image src={testImage} alt="Preview" className="violation-image" />
<div className="violation-badge">НАРУШЕНИЕ</div>
<div className="violation-status-badge">ОБРАБОТАНО</div>
</div>
<div className="violation-content">
<div className="violation-title-row">
<div className="violation-filename">
🖼banner111.png</div>
<div className="violation-type-badge photo">
Фото
</div>
</div>
<div className="violation-info-grid">
<div className="violation-info-item">
<div className="violation-info-label">
🌍 Регион
</div>
<div className="violation-info-value">
<span className="region-flag">🇺🇸</span>
<span>США</span>
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📅 Обнаружено</div>
<div className="violation-info-value">
17.10.2025</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">🌐 Домен</div>
<div className="violation-info-value">
m.apkpure.com </div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📊 Категория</div>
<div className="violation-info-value">
Единичное
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📁 Тип</div>
<div className="violation-info-value">
Изображение</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label"> Статус</div>
<div className="violation-info-value">
Обработано</div>
</div>
</div>
<div className="violation-actions-row">
<button className="btn-visit-site">
🔗 Перейти на сайт
</button>
<button className="violation-action">
🚨 Открыть дело
</button>
</div>
</div>
</div>
</SwiperSlide>
<SwiperSlide>
<div className="violation-slide">
<div className="violation-image-container">
<Image src={testImage} alt="Preview" className="violation-image" />
<div className="violation-badge">НАРУШЕНИЕ</div>
<div className="violation-status-badge">ОБРАБОТАНО</div>
</div>
<div className="violation-content">
<div className="violation-title-row">
<div className="violation-filename">
🖼banner111.png</div>
<div className="violation-type-badge photo">
Фото
</div>
</div>
<div className="violation-info-grid">
<div className="violation-info-item">
<div className="violation-info-label">
🌍 Регион
</div>
<div className="violation-info-value">
<span className="region-flag">🇺🇸</span>
<span>США</span>
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📅 Обнаружено</div>
<div className="violation-info-value">
17.10.2025</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">🌐 Домен</div>
<div className="violation-info-value">
m.apkpure.com </div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📊 Категория</div>
<div className="violation-info-value">
Единичное
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📁 Тип</div>
<div className="violation-info-value">
Изображение</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label"> Статус</div>
<div className="violation-info-value">
Обработано</div>
</div>
</div>
<div className="violation-actions-row">
<button className="btn-visit-site">
🔗 Перейти на сайт
</button>
<button className="violation-action">
🚨 Открыть дело
</button>
</div>
</div>
</div>
</SwiperSlide>
<SwiperSlide>
<div className="violation-slide">
<div className="violation-image-container">
<Image src={testImage} alt="Preview" className="violation-image" />
<div className="violation-badge">НАРУШЕНИЕ</div>
<div className="violation-status-badge">ОБРАБОТАНО</div>
</div>
<div className="violation-content">
<div className="violation-title-row">
<div className="violation-filename">
🖼banner111.png</div>
<div className="violation-type-badge photo">
Фото
</div>
</div>
<div className="violation-info-grid">
<div className="violation-info-item">
<div className="violation-info-label">
🌍 Регион
</div>
<div className="violation-info-value">
<span className="region-flag">🇺🇸</span>
<span>США</span>
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📅 Обнаружено</div>
<div className="violation-info-value">
17.10.2025</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">🌐 Домен</div>
<div className="violation-info-value">
m.apkpure.com </div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📊 Категория</div>
<div className="violation-info-value">
Единичное
</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label">📁 Тип</div>
<div className="violation-info-value">
Изображение</div>
</div>
<div className="violation-info-item">
<div className="violation-info-label"> Статус</div>
<div className="violation-info-value">
Обработано</div>
</div>
</div>
<div className="violation-actions-row">
<button className="btn-visit-site">
🔗 Перейти на сайт
</button>
<button className="violation-action">
🚨 Открыть дело
</button>
</div>
</div>
</div>
</SwiperSlide>
</Swiper>
<div
className="flex justify-center mt-6"
>
<div
ref={paginationRef}
className="flex justify-center gap-2 custon-swiper-pagination"
>
</div>
</div>
</div> */}
</div>
)
}
+51
View File
@@ -0,0 +1,51 @@
'use client'
import { ReactNode, useEffect, useRef, useState } from 'react';
interface FaqItemProps {
question: string;
children?: ReactNode | null;
}
export default function FaqItem({ question, children }: FaqItemProps) {
const [isOpen, setIsOpen] = useState(false);
const [height, setHeight] = useState(0);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (contentRef.current) {
const scrollHeight = contentRef.current.scrollHeight;
if (isOpen) {
setHeight(scrollHeight);
} else {
setHeight(0);
}
}
}, [isOpen]);
function toggleHandler() {
setIsOpen(!isOpen);
}
return (
<div className="faq-item">
<div className="faq-question" onClick={toggleHandler}>
<span>{question}</span>
<div className="faq-toggle">
<svg className={`dropdown-arrow ${isOpen? 'expanded' : ''}`} viewBox="0 0 24 24" fill="currentColor">
<path d="M7 10l5 -5 5 5z"></path>
</svg>
</div>
</div>
<div
className="faq-answer"
style={{ height: `${height}px` }}
>
<div ref={contentRef}>
{children}
</div>
</div>
</div>
);
}
+54
View File
@@ -0,0 +1,54 @@
export default function FaqContact() {
return (
<>
<div className="support-block" >
<h3 >Не нашли ответ на свой вопрос?</h3>
<p >
Наша команда поддержки работает 24/7 и готова помочь вам
<br />
с любыми вопросами по использованию платформы NO COPY.
</p>
<div className="support-buttons" >
<button className="btn btn-primary">
Написать в поддержку
</button>
<a href="tel:+78003021090" className="btn btn-outline">
📞 Позвонить: 8 (800) 302-10-90
</a>
</div>
<div className="support-schedule">
<h4 >Время работы поддержки:</h4>
<div className="support-schedule-grid">
<div>📧 E-mail: круглосуточно</div>
<div>📞 Телефон: 9:30 - 18:00 МСК</div>
<div>💬 Чат: 9:30 - 18:00 МСК</div>
</div>
</div>
</div>
<div className="stats-mini-wrapper">
<div className="stats-mini-content">
<div className="stats-mini-item">
<div className="stats-mini-value" style={{ 'color': '#6366f1' }}>
18
</div>
<div className="stats-mini-label">Вопросов в базе</div>
</div>
<div className="stats-mini-item">
<div className="stats-mini-value" style={{ 'color': '#10b981' }}>
99%
</div>
<div className="stats-mini-label">Находят ответ</div>
</div>
<div className="stats-mini-item">
<div className="stats-mini-value" style={{ 'color': '#f59e0b' }}>
24/7
</div>
<div className="stats-mini-label">Поддержка</div>
</div>
</div>
</div>
</>
)
}
@@ -0,0 +1,178 @@
import FaqItem from '@/app/ui/faq/component/faq-item';
export default function FrequentlyAskedQuestions() {
return (
<div
className="faq-wrapper"
>
<section className="faq-section">
<h2>
<span></span>
Защита контента
<span>(2 вопросов)</span>
</h2>
<FaqItem
question="Как скрытый водяной знак защищает контент?"
>
Скрытый водяной знак встраивается незаметно в ваш контент и позволяет отслеживать его использование в интернете. Технология работает на основе скрытого водяного знака No Copy и не влияет на качество изображений, видео или аудио.
</FaqItem >
<FaqItem
question="Насколько надежна защита?"
>
Наша многослойная защита обеспечивает 99.8% точность определения авторства даже после обработки файлов.
</FaqItem >
</section>
<section className="faq-section">
<h2>
<span></span>
Общие вопросы
<span>(2 вопросов)</span>
</h2>
<FaqItem
question="Что такое NO COPY?"
>
NO COPY - это платформа для защиты авторских прав с помощью невидимых водяных знаков для изображений, видео, аудио и документов.
</FaqItem >
<FaqItem
question="Как работает защита?"
>
Мы встраиваем невидимые цифровые метки непосредственно в структуру файлов. Эти метки сохраняются даже при сжатии и конвертации.
</FaqItem >
</section>
<section className="faq-section">
<h2>
<span></span>
Лимиты и ограничения
<span>(3 вопросов)</span>
</h2>
<FaqItem
question="Сколько фотографий я могу промаркировать?"
>
Вы можете промаркировать столько фотографий, сколько прописано в выбранном Вами тарифном плане. Минимально в месяц можно маркировать от 100 фотографий на платных тарифных планах No Copy.
</FaqItem >
<FaqItem
question="Сколько файлов можно загрузить?"
>
Количество файлов зависит от вашего тарифного плана. Для примера, на тарифе СТАРТ: ~100 фото / ~20 видео / ~35 аудио / ~60 pdf
</FaqItem >
<FaqItem
question="Что происходит при превышении лимитов?"
>
При превышении лимитов подписки вы можете использовать токены для продолжения работы или обновить тариф. Без токенов новые файлы нельзя будет защитить до начала следующего месяца или смены тарифа.
</FaqItem >
</section>
<section className="faq-section">
<h2>
<span></span>
Хранение данных
<span>(1 вопросов)</span>
</h2>
<FaqItem
question="Где хранится весь промаркированный контент?"
>
Весь промаркированный контент хранится на защищенных серверах No Copy с многоуровневой системой резервного копирования согласно закону ФЗ-152. Доступ к вашим файлам имеете только вы.
</FaqItem >
</section>
<section className="faq-section">
<h2>
<span></span>
Правовые вопросы
<span>(1 вопросов)</span>
</h2>
<FaqItem
question="Могу ли я маркировать чужой контент?"
>
Нет, маркировать можно только контент, авторские права на который принадлежат вам. Это важное условие использования платформы No Copy.
</FaqItem >
</section>
<section className="faq-section">
<h2>
<span></span>
Поддержка
<span>(1 вопросов)</span>
</h2>
<FaqItem
question="Я сам нашёл нарушение контента, куда заявить?"
>
Если вы обнаружили нарушение самостоятельно, свяжитесь с нашей службой поддержки через раздел "Поддержка" или отправьте письмо на info@no-copy.ru.
</FaqItem >
</section>
<section className="faq-section">
<h2>
<span></span>
Технические вопросы
<span>(1 вопросов)</span>
</h2>
<FaqItem
question="Какие форматы поддерживаются?"
>
Изображения: JPG, JPEG, PNG, BMP. Видео: MP4, AVI, MOV, WMV. Аудио: WAV. Документы: PDF.
</FaqItem >
</section>
<section className="faq-section">
<h2>
<span></span>
Оплата и подписка
<span>(7 вопросов)</span>
</h2>
<FaqItem
question="Какие способы оплаты поддерживаются?"
>
Основной вид оплаты - это оплата российскими(выпущенные в РФ) банковскими картами (Visa, MasterCard, МИР).
По запросу можно выбрать другой способ оплаты: электронными кошельками (Yoo.Money), через систему быстрых платежей (СБП) и другими способами.
</FaqItem >
<FaqItem
question="Как работает автопродление подписки?"
>
Подписка автоматически продлевается каждый месяц с вашей карты. Вы можете отменить автопродление в любой момент в настройках аккаунта. Уведомление о списании приходит за 3 дня до продления.
</FaqItem >
<FaqItem
question="Что такое токены и зачем они нужны?"
>
Токены - это внутренняя валюта платформы для оплаты защиты файлов и других действий на платформе. Например круглосуточный мониторинг совпадений контента, подача жалоб DMCA, фиксация нарушений(протокол) или подключение юристов на платформе и создание дела.
</FaqItem >
<FaqItem
question="Можно ли получить возврат средств?"
>
Возврат возможен только в день оформления подписки при не использовании токенов, защиты файлов, поиска и других функций платформы. Для подачи уведомления обратитесь в техническую поддержку через форму обратной связи или E-mail: support@no-copy.ru.
</FaqItem >
<FaqItem
question="Как изменить или отменить подписку?"
>
Вы можете повысить тариф в любой момент - доплата рассчитывается пропорционально. Понижение тарифа вступает в силу со следующего периода оплаты. Отменить подписку можно в настройках аккаунта.
</FaqItem >
<FaqItem
question="Безопасна ли оплата через ваш сайт?"
>
Да, все платежи обрабатываются через ЮKassa с использованием защищенного SSL-соединения. Мы не храним данные ваших банковских карт. Платформа соответствует стандартам безопасности PCI DSS.
</FaqItem >
<FaqItem
question="Как получить чек об оплате?"
>
Электронный чек автоматически отправляется на email, указанный при оплате.
</FaqItem >
</section>
</div>
)
}
@@ -0,0 +1,23 @@
export default function PopularAskedQuestions() {
return (
<div className="popular-questions">
<h2>
Популярные вопросы
</h2>
<div className="popular-grid">
<div>
<h4>Как работает защита?</h4>
<p>Мы встраиваем невидимые водяные знаки в ваш контент</p>
</div>
<div>
<h4>Сколько файлов можно загрузить?</h4>
<p>Зависит от вашего тарифного плана</p>
</div>
<div>
<h4>Безопасность данных</h4>
<p>Все файлы хранятся на защищенных серверах</p>
</div>
</div>
</div>
)
}
@@ -1,4 +1,4 @@
export default function ViolationPageActions() { export default function FilePageActions() {
return ( return (
<div <div
className="block-wrapper" className="block-wrapper"
@@ -0,0 +1,64 @@
'use client'
import { FileDetails } from '@/app/[locale]/pages/file/[id]/page';
import { convertBytes } from '@/app/lib/convertBytes';
import { formatDate } from '@/app/lib/formatDate';
import { useTranslations } from 'next-intl';
import { useEffect } from 'react';
export default function FilenPageFileInfo({ fileInfo }: { fileInfo: FileDetails }) {
const t = useTranslations('Global');
return (
<div
className="block-wrapper"
>
<div className="content-card">
<div className="card-header">
<h3 className="card-title">
{t('file-information')}
</h3>
</div>
<div className="info-row">
<span className="info-label">
ID
</span>
<span className="info-value">
{fileInfo ? fileInfo.supportId : '#'}
</span>
</div>
<div className="info-row">
<span className="info-label">
{t('file-type')}
</span>
<span className="info-value">
{fileInfo ? fileInfo.mimeType : '#'}
</span>
</div>
<div className="info-row">
<span className="info-label">
{t('size')}
</span>
<span className="info-value">{fileInfo ? convertBytes(fileInfo.fileSize) : '#'}</span>
</div>
<div className="info-row">
<span className="info-label">
{t('loaded')}
</span>
<span className="info-value">
{fileInfo ? formatDate(fileInfo.createdAt) : '#'}
</span>
</div>
<div className="info-row">
<span className="info-label">
{t('monitoring')}
</span>
<span className="info-value">
{fileInfo ? fileInfo.monitoring : '#'}
</span>
</div>
</div>
</div>
)
}

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