Compare commits

..

74 Commits

Author SHA1 Message Date
semantic-release-bot 01a3aa02ec 🔖 chore(release): v1.0.0-beta.24 [skip ci]
# [1.0.0-beta.24](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2025-02-27)

### ♻ Code Refactoring

* **ui**: Optimize document display ([2ca2992](https://github.com/perfect-panel/ppanel-web/commit/2ca2992))
* Reduce code complexity and improve readability ([e11f18c](https://github.com/perfect-panel/ppanel-web/commit/e11f18c))

###  Features

* **loading**: Add loading components and integrate them in Providers ([d5847fa](https://github.com/perfect-panel/ppanel-web/commit/d5847fa))

### 🎫 Chores

* **merge**: Add advertising module and device settings ([0130e02](https://github.com/perfect-panel/ppanel-web/commit/0130e02))

### 🐛 Bug Fixes

* **locales**: Order recharge related fields ([35210fe](https://github.com/perfect-panel/ppanel-web/commit/35210fe))
2025-02-27 08:02:53 +00:00
web@ppanel 35210fe8cf 🐛 fix(locales): Order recharge related fields 2025-02-27 14:53:19 +07:00
web@ppanel d5847faeda feat(loading): Add loading components and integrate them in Providers 2025-02-27 14:44:30 +07:00
web@ppanel 0130e02ffd 🔧 chore(merge): Add advertising module and device settings 2025-02-27 13:46:51 +07:00
web@ppanel c2b858e3fe Merge pull request #11 from turbolnk-com/develop
♻️ refactor(ui): Optimize document display
2025-02-25 18:35:05 +07:00
turbolnk e11f18c034 ♻️ refactor: Reduce code complexity and improve readability 2025-02-25 19:31:32 +08:00
turbolnk 2ca299224d ♻️ refactor(ui): Optimize document display 2025-02-25 18:38:25 +08:00
semantic-release-bot cf1d66d07e 🔖 chore(release): v1.0.0-beta.23 [skip ci]
# [1.0.0-beta.23](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2025-02-24)

### 🐛 Bug Fixes

* **auth**: Update email verification logic to use domain suffix check ([62662bb](https://github.com/perfect-panel/ppanel-web/commit/62662bb))
2025-02-24 05:10:30 +00:00
web@ppanel 62662bb79c 🐛 fix(auth): Update email verification logic to use domain suffix check 2025-02-24 12:07:41 +07:00
semantic-release-bot c0fb34fa50 🔖 chore(release): v1.0.0-beta.22 [skip ci]
# [1.0.0-beta.22](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2025-02-23)

### 🐛 Bug Fixes

* **locales**: Removed language file import to clean up unnecessary language support ([68f6ab2](https://github.com/perfect-panel/ppanel-web/commit/68f6ab2))
2025-02-23 15:06:05 +00:00
web@ppanel 68f6ab2e6c 🐛 fix(locales): Removed language file import to clean up unnecessary language support 2025-02-23 22:03:15 +07:00
semantic-release-bot ca642c2001 🔖 chore(release): v1.0.0-beta.21 [skip ci]
# [1.0.0-beta.21](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2025-02-23)

###  Features

* **privacy-policy**: Add privacy policy related text and links ([baa68f0](https://github.com/perfect-panel/ppanel-web/commit/baa68f0))

### 🐛 Bug Fixes

* **locales**: Removed multilingual files to clean up unnecessary language support ([5b151cd](https://github.com/perfect-panel/ppanel-web/commit/5b151cd))
* **locales**: Update custom HTML description in language file, ([87381da](https://github.com/perfect-panel/ppanel-web/commit/87381da))
* **table**: Update privacy policy tab translation key and remove unnecessary requestType from OAuth callback ([14b3af5](https://github.com/perfect-panel/ppanel-web/commit/14b3af5))
2025-02-23 14:17:29 +00:00
web@ppanel 5b151cdbfc 🐛 fix(locales): Removed multilingual files to clean up unnecessary language support 2025-02-23 21:14:17 +07:00
web@ppanel 14b3af5f96 🐛 fix(table): Update privacy policy tab translation key and remove unnecessary requestType from OAuth callback 2025-02-23 20:59:48 +07:00
web@ppanel baa68f03e3 feat(privacy-policy): Add privacy policy related text and links 2025-02-23 20:54:56 +07:00
web@ppanel 87381da1e5 🐛 fix(locales): Update custom HTML description in language file, 2025-02-23 17:25:31 +07:00
semantic-release-bot bbd44f0107 🔖 chore(release): v1.0.0-beta.20 [skip ci]
# [1.0.0-beta.20](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2025-02-23)

### 🐛 Bug Fixes

* **auth**: Add error handling to form submission and reset Turnstile on failure ([715d011](https://github.com/perfect-panel/ppanel-web/commit/715d011))
* **subscribe**: Update subscription domain placeholder to include examples; improve site name retrieval in global store ([c65a44c](https://github.com/perfect-panel/ppanel-web/commit/c65a44c))
2025-02-23 06:31:01 +00:00
web@ppanel c65a44c2c7 🐛 fix(subscribe): Update subscription domain placeholder to include examples; improve site name retrieval in global store 2025-02-23 13:25:01 +07:00
web@ppanel 715d0113ca 🐛 fix(auth): Add error handling to form submission and reset Turnstile on failure 2025-02-23 13:12:25 +07:00
semantic-release-bot 0f15fb82cb 🔖 chore(release): v1.0.0-beta.19 [skip ci]
# [1.0.0-beta.19](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2025-02-23)

###  Features

* **form**: Make version field optional and set default value; update site domain placeholder for clarity ([42ba9e8](https://github.com/perfect-panel/ppanel-web/commit/42ba9e8))

### 🐛 Bug Fixes

* **auth**: Refactor forms to use Turnstile ref for reset functionality ([320a7dc](https://github.com/perfect-panel/ppanel-web/commit/320a7dc))
2025-02-23 05:58:05 +00:00
web@ppanel 320a7dc342 🐛 fix(auth): Refactor forms to use Turnstile ref for reset functionality 2025-02-23 12:55:21 +07:00
web@ppanel 42ba9e88ad feat(form): Make version field optional and set default value; update site domain placeholder for clarity 2025-02-23 12:46:29 +07:00
semantic-release-bot 4a002333d4 🔖 chore(release): v1.0.0-beta.18 [skip ci]
# [1.0.0-beta.18](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2025-02-22)

###  Features

* **platform**: Update platform naming and add keywords and custom HTML fields ([6384237](https://github.com/perfect-panel/ppanel-web/commit/6384237))
* **site**: Added localization support for custom HTML and keyword fields ([f9d7736](https://github.com/perfect-panel/ppanel-web/commit/f9d7736))

### 🐛 Bug Fixes

* **locales**: Update custom HTML description for clarity across multiple languages ([557c5cd](https://github.com/perfect-panel/ppanel-web/commit/557c5cd))
2025-02-22 16:38:46 +00:00
web@ppanel 6384237782 feat(platform): Update platform naming and add keywords and custom HTML fields 2025-02-22 23:35:30 +07:00
web@ppanel 557c5cdc29 🐛 fix(locales): Update custom HTML description for clarity across multiple languages 2025-02-22 12:51:57 +07:00
web@ppanel f9d7736e48 feat(site): Added localization support for custom HTML and keyword fields 2025-02-22 12:33:09 +07:00
semantic-release-bot 5b64389544 🔖 chore(release): v1.0.0-beta.17 [skip ci]
# [1.0.0-beta.17](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2025-02-21)

### 🐛 Bug Fixes

* **auth**: Refactor reset password form to simplify code input and update placeholder text ([23833b4](https://github.com/perfect-panel/ppanel-web/commit/23833b4))
2025-02-21 13:26:11 +00:00
web@ppanel 23833b47d1 🐛 fix(auth): Refactor reset password form to simplify code input and update placeholder text 2025-02-21 20:23:30 +07:00
semantic-release-bot c3eff0a0e7 🔖 chore(release): v1.0.0-beta.16 [skip ci]
# [1.0.0-beta.16](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2025-02-21)

### 🐛 Bug Fixes

* **auth**: Simplify email verification code input rendering ([6f7bc37](https://github.com/perfect-panel/ppanel-web/commit/6f7bc37))
2025-02-21 13:18:47 +00:00
web@ppanel 6f7bc37233 🐛 fix(auth): Simplify email verification code input rendering 2025-02-21 20:16:02 +07:00
semantic-release-bot b2e8fadaf3 🔖 chore(release): v1.0.0-beta.15 [skip ci]
# [1.0.0-beta.15](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2025-02-21)

### 🐛 Bug Fixes

* **profile**: Restore filter to ensure only valid OAuth accounts are shown ([315c8f9](https://github.com/perfect-panel/ppanel-web/commit/315c8f9))
2025-02-21 12:59:02 +00:00
web@ppanel 315c8f9afe 🐛 fix(profile): Restore filter to ensure only valid OAuth accounts are shown 2025-02-21 19:55:24 +07:00
semantic-release-bot 0fb0d8bc11 🔖 chore(release): v1.0.0-beta.14 [skip ci]
# [1.0.0-beta.14](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2025-02-21)

### 🐛 Bug Fixes

* **locales**: Fixed description in multilingual files, updated text related to email registration functionality ([c356bc2](https://github.com/perfect-panel/ppanel-web/commit/c356bc2))
2025-02-21 07:07:36 +00:00
web@ppanel c356bc26ff 🐛 fix(locales): Fixed description in multilingual files, updated text related to email registration functionality 2025-02-21 14:04:15 +07:00
semantic-release-bot 23c974a94d 🔖 chore(release): v1.0.0-beta.13 [skip ci]
# [1.0.0-beta.13](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2025-02-21)

### 🐛 Bug Fixes

* **api**: Fix type error in API request and add return URL parameter ([ee286dd](https://github.com/perfect-panel/ppanel-web/commit/ee286dd))
2025-02-21 06:03:00 +00:00
web@ppanel ee286dd629 🐛 fix(api): Fix type error in API request and add return URL parameter 2025-02-21 12:59:23 +07:00
semantic-release-bot 4418c47f4f 🔖 chore(release): v1.0.0-beta.12 [skip ci]
# [1.0.0-beta.12](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2025-02-18)

### 🐛 Bug Fixes

* More bugs ([2d88a3a](https://github.com/perfect-panel/ppanel-web/commit/2d88a3a))
2025-02-18 14:57:24 +00:00
web@ppanel 2d88a3a81d 🐛 fix: More bugs 2025-02-18 21:53:24 +07:00
semantic-release-bot 1f29506179 🔖 chore(release): v1.0.0-beta.11 [skip ci]
# [1.0.0-beta.11](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2025-02-16)

### 🐛 Bug Fixes

* **email**: Update platform configuration handling to use current ref for consistency ([c90175b](https://github.com/perfect-panel/ppanel-web/commit/c90175b))
2025-02-16 12:09:00 +00:00
web@ppanel c90175b6eb 🐛 fix(email): Update platform configuration handling to use current ref for consistency 2025-02-16 19:06:17 +07:00
semantic-release-bot 5cf573ab03 🔖 chore(release): v1.0.0-beta.10 [skip ci]
# [1.0.0-beta.10](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2025-02-16)

### 🐛 Bug Fixes

* **subscribe**: Update forms to include refetch functionality and improve toast messages ([fc55e95](https://github.com/perfect-panel/ppanel-web/commit/fc55e95))
2025-02-16 10:14:25 +00:00
web@ppanel fc55e9544d 🐛 fix(subscribe): Update forms to include refetch functionality and improve toast messages 2025-02-16 17:08:34 +07:00
semantic-release-bot 855d1b096f 🔖 chore(release): v1.0.0-beta.9 [skip ci]
# [1.0.0-beta.9](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2025-02-15)

### 🐛 Bug Fixes

* **register**: Adjust user email verification logic to handle domain suffix checks correctly ([686aa2d](https://github.com/perfect-panel/ppanel-web/commit/686aa2d))
2025-02-15 15:02:16 +00:00
web@ppanel 686aa2d0ba 🐛 fix(register): Adjust user email verification logic to handle domain suffix checks correctly 2025-02-15 21:58:25 +07:00
semantic-release-bot a593eac119 🔖 chore(release): v1.0.0-beta.8 [skip ci]
# [1.0.0-beta.8](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2025-02-15)

###  Features

* **accounts**: Update third-party account binding and unbinding ([1841552](https://github.com/perfect-panel/ppanel-web/commit/1841552))
* **auth-control**: Adding phone number labels to mobile verification configurations in multiple languages ([046740f](https://github.com/perfect-panel/ppanel-web/commit/046740f))
* **auth-control**: Update general ([3883646](https://github.com/perfect-panel/ppanel-web/commit/3883646))
* **auth**: Add type parameter to SendCode and update related API typings ([4198871](https://github.com/perfect-panel/ppanel-web/commit/4198871))
* **auth**: Refactor mobile authentication config to support whitelist functionality ([c761ec7](https://github.com/perfect-panel/ppanel-web/commit/c761ec7))
* **device**: Modify IMEI to device identifier support ([e3f9ef6](https://github.com/perfect-panel/ppanel-web/commit/e3f9ef6))
* **imei**: Add IMEI related internationalization support and menu items ([13c3337](https://github.com/perfect-panel/ppanel-web/commit/13c3337))
* **locales**: Add kick offline confirmation and success messages in multiple languages ([5db5343](https://github.com/perfect-panel/ppanel-web/commit/5db5343))
* **locales**: Update 'sms' to 'mobile' in authentication methods across multiple languages ([fea2171](https://github.com/perfect-panel/ppanel-web/commit/fea2171))
* **log**: Add message log retrieval functionality and update related typings ([1c0ecae](https://github.com/perfect-panel/ppanel-web/commit/1c0ecae))
* **profile**:  Update localization strings and enhance third-party account binding ([2d1effb](https://github.com/perfect-panel/ppanel-web/commit/2d1effb))
* **release**: Extend supported platforms for Docker images, closes [#9](https://github.com/perfect-panel/ppanel-web/issues/9) ([e3a31eb](https://github.com/perfect-panel/ppanel-web/commit/e3a31eb))
* **subscription**: Add delete user subscription functionality ([1fc3a10](https://github.com/perfect-panel/ppanel-web/commit/1fc3a10))
* **ui**: Update input components and enhance card minimum width for better layout ([8a02310](https://github.com/perfect-panel/ppanel-web/commit/8a02310))
* **user**: Add user Detail ([3a3d223](https://github.com/perfect-panel/ppanel-web/commit/3a3d223))
* **user**: Add User Detail ([fdaf11b](https://github.com/perfect-panel/ppanel-web/commit/fdaf11b))

### 🐛 Bug Fixes

* **auth-control**: Fix citation error for platform values ([c940f3c](https://github.com/perfect-panel/ppanel-web/commit/c940f3c))
* **auth-control**: Fix citation error for platform values ([28813d2](https://github.com/perfect-panel/ppanel-web/commit/28813d2))
* **auth-control**: Rename phone_variable to phone_number in mobile verification configuration ([e5455aa](https://github.com/perfect-panel/ppanel-web/commit/e5455aa))
* **auth**: Update authentication configuration and localization strings ([47f2c58](https://github.com/perfect-panel/ppanel-web/commit/47f2c58))
* **locales**: Update expiration time description from minutes to seconds in multiple languages ([5bac933](https://github.com/perfect-panel/ppanel-web/commit/5bac933))
* **notify**: Ensure user info is updated after notification settings submission ([9bc3a94](https://github.com/perfect-panel/ppanel-web/commit/9bc3a94))
* **notify**: Set default values for notification settings to false ([3652819](https://github.com/perfect-panel/ppanel-web/commit/3652819))
* **third-party-accounts**: Remove mobile display logic from third-party accounts component ([b4946f7](https://github.com/perfect-panel/ppanel-web/commit/b4946f7))
* **third-party-accounts**: Update redirect property name in binding response handling ([012e83a](https://github.com/perfect-panel/ppanel-web/commit/012e83a))
* **user**: Refactor user form validation and reset password fields ([6733fc2](https://github.com/perfect-panel/ppanel-web/commit/6733fc2))
* **user**: Update locales ([4e7d249](https://github.com/perfect-panel/ppanel-web/commit/4e7d249))
* **user**: Update notification and verify code settings ([574b043](https://github.com/perfect-panel/ppanel-web/commit/574b043))

### 💄 Styles

* **dashboard**: Enhance card components with full height and improved empty state handling ([7e1d551](https://github.com/perfect-panel/ppanel-web/commit/7e1d551))
2025-02-15 11:08:52 +00:00
web@ppanel 7e1d551d8f 💄 style(dashboard): Enhance card components with full height and improved empty state handling 2025-02-15 18:06:07 +07:00
web@ppanel 1fc3a10c67 feat(subscription): Add delete user subscription functionality 2025-02-14 23:58:10 +07:00
web@ppanel 9bc3a940ba 🐛 fix(notify): Ensure user info is updated after notification settings submission 2025-02-14 23:42:54 +07:00
web@ppanel 3652819ac9 🐛 fix(notify): Set default values for notification settings to false 2025-02-14 23:01:16 +07:00
web@ppanel c761ec7538 feat(auth): Refactor mobile authentication config to support whitelist functionality 2025-02-14 22:13:50 +07:00
web@ppanel 5bac933fbc 🐛 fix(locales): Update expiration time description from minutes to seconds in multiple languages 2025-02-14 15:53:26 +07:00
web@ppanel 8a02310752 feat(ui): Update input components and enhance card minimum width for better layout 2025-02-14 15:49:34 +07:00
web@ppanel 574b043be6 🐛 fix(user): Update notification and verify code settings 2025-02-14 15:39:25 +07:00
web@ppanel e3f9ef6ebe feat(device): Modify IMEI to device identifier support 2025-02-13 22:28:45 +07:00
web@ppanel 4e7d249902 🐛 fix(user): Update locales 2025-02-13 21:42:20 +07:00
web@ppanel 5db5343e4b feat(locales): Add kick offline confirmation and success messages in multiple languages 2025-02-13 21:37:05 +07:00
web@ppanel 1c0ecaecbd feat(log): Add message log retrieval functionality and update related typings 2025-02-13 21:25:21 +07:00
web@ppanel 13c33378aa feat(imei): Add IMEI related internationalization support and menu items 2025-02-12 16:34:23 +07:00
web@ppanel b4946f7a06 🐛 fix(third-party-accounts): Remove mobile display logic from third-party accounts component 2025-02-10 23:12:43 +07:00
web@ppanel 012e83a25c 🐛 fix(third-party-accounts): Update redirect property name in binding response handling 2025-02-10 22:51:31 +07:00
web@ppanel 1841552b1b feat(accounts): Update third-party account binding and unbinding 2025-02-10 21:39:28 +07:00
web@ppanel 4198871eef feat(auth): Add type parameter to SendCode and update related API typings 2025-02-10 14:39:31 +07:00
web@ppanel e5455aa1dc 🐛 fix(auth-control): Rename phone_variable to phone_number in mobile verification configuration 2025-02-10 13:05:10 +07:00
web@ppanel 046740f037 feat(auth-control): Adding phone number labels to mobile verification configurations in multiple languages 2025-02-10 13:03:48 +07:00
web@ppanel fea2171d54 feat(locales): Update 'sms' to 'mobile' in authentication methods across multiple languages 2025-02-09 22:24:00 +07:00
web@ppanel c940f3c209 🐛 fix(auth-control): Fix citation error for platform values 2025-02-09 22:06:29 +07:00
web@ppanel 28813d2d3a 🐛 fix(auth-control): Fix citation error for platform values 2025-02-09 21:12:26 +07:00
web@ppanel 2d1effb7ab feat(profile): Update localization strings and enhance third-party account binding 2025-02-09 21:05:56 +07:00
web@ppanel 3883646566 feat(auth-control): Update general 2025-02-09 20:14:27 +07:00
web@ppanel e3a31eb709 feat(release): Extend supported platforms for Docker images (#9) 2025-02-09 16:34:43 +07:00
web@ppanel 47f2c5853c 🐛 fix(auth): Update authentication configuration and localization strings 2025-02-08 23:58:03 +07:00
web@ppanel 3a3d2236e2 feat(user): Add user Detail 2025-02-08 14:47:23 +07:00
web@ppanel fdaf11b654 feat(user): Add User Detail 2025-02-02 14:40:29 +07:00
web@ppanel 6733fc211c 🐛 fix(user): Refactor user form validation and reset password fields 2025-01-29 17:16:25 +07:00
650 changed files with 13500 additions and 6384 deletions
+242 -57
View File
@@ -1,79 +1,264 @@
<a name="readme-top"></a> <a name="readme-top"></a>
# Changelog # Changelog
# [1.0.0-beta.7](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2025-01-28) # [1.0.0-beta.24](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2025-02-27)
### ♻ Code Refactoring ### ♻ Code Refactoring
* **sbscribe**: Rename and reorganize components for better structure and clarity ([5e5e4ed](https://github.com/perfect-panel/ppanel-web/commit/5e5e4ed)) * **ui**: Optimize document display ([2ca2992](https://github.com/perfect-panel/ppanel-web/commit/2ca2992))
* Reduce code complexity and improve readability ([e11f18c](https://github.com/perfect-panel/ppanel-web/commit/e11f18c))
### ✨ Features ### ✨ Features
* **auth**: Add email and SMS code sending functionality with localization updates ([57eaa55](https://github.com/perfect-panel/ppanel-web/commit/57eaa55)) * **loading**: Add loading components and integrate them in Providers ([d5847fa](https://github.com/perfect-panel/ppanel-web/commit/d5847fa))
* **auth**: Add Oauth configuration for Telegram, Facebook, Google, Github, and Apple ([18ee600](https://github.com/perfect-panel/ppanel-web/commit/18ee600))
* **auth**: Add SMS and email configuration options to global store and update localization ([4acf7b1](https://github.com/perfect-panel/ppanel-web/commit/4acf7b1))
* **auth**: Enhance user registration with invite handling and logo display ([207bc24](https://github.com/perfect-panel/ppanel-web/commit/207bc24)) ### 🎫 Chores
* **auth**: Redirect user after OAuth login and add logos icon collection ([aa6dda8](https://github.com/perfect-panel/ppanel-web/commit/aa6dda8))
* **config**: Add application selection and encryption settings to configuration form ([88b3504](https://github.com/perfect-panel/ppanel-web/commit/88b3504)) * **merge**: Add advertising module and device settings ([0130e02](https://github.com/perfect-panel/ppanel-web/commit/0130e02))
* **config**: Update encryption fields in configuration form and refactor OAuth callback parameters ([652e032](https://github.com/perfect-panel/ppanel-web/commit/652e032))
* **global**: Add SMS configuration options to global store ([39a9ce6](https://github.com/perfect-panel/ppanel-web/commit/39a9ce6))
* **locales**: Add area code and telephone fields to user forms in multiple languages ([9b8258c](https://github.com/perfect-panel/ppanel-web/commit/9b8258c))
* **locales**: Add description information of communication keys and encryption methods to enhance client configuration capabilities ([d1f5a9b](https://github.com/perfect-panel/ppanel-web/commit/d1f5a9b))
* **node**: Add tags ([f408fdf](https://github.com/perfect-panel/ppanel-web/commit/f408fdf))
* **node**: Move the node configuration to the server module ([7f0f5ce](https://github.com/perfect-panel/ppanel-web/commit/7f0f5ce))
* **oauth**: Add certification component for handling OAuth login callbacks and improve user authentication flow ([5ed04c0](https://github.com/perfect-panel/ppanel-web/commit/5ed04c0))
* **oauth**: Implement OAuth token retrieval and refactor login callback handling ([40a6f7c](https://github.com/perfect-panel/ppanel-web/commit/40a6f7c))
* **oauth**: Refactor platform parameter handling and improve logout redirection logic ([8346c85](https://github.com/perfect-panel/ppanel-web/commit/8346c85))
* **oauth**: Update OAuth login handling to use callback parameter and improve URL parameter retrieval ([9227411](https://github.com/perfect-panel/ppanel-web/commit/9227411))
* **sms**: Update locales ([938363b](https://github.com/perfect-panel/ppanel-web/commit/938363b))
* **subscribe**: Add 'sold' column to SubscribeTable and update inventory terminology ([19619fd](https://github.com/perfect-panel/ppanel-web/commit/19619fd))
* **subscribe**: Move subscription configuration and application to subscription module ([f90d4d2](https://github.com/perfect-panel/ppanel-web/commit/f90d4d2))
* **subscribe**: Update SubscribeTable component to use API.SubscribeItem type and ensure proper type casting ([f26f1c2](https://github.com/perfect-panel/ppanel-web/commit/f26f1c2))
* **tutorial**: Fetch the latest tutorial version from GitHub API for dynamic URL generation ([28f8c78](https://github.com/perfect-panel/ppanel-web/commit/28f8c78))
* **user**: Add telephone input with area code selection and update localization ([585b99c](https://github.com/perfect-panel/ppanel-web/commit/585b99c))
* Update Auth Control ([c59742a](https://github.com/perfect-panel/ppanel-web/commit/c59742a))
### 🐛 Bug Fixes ### 🐛 Bug Fixes
* **api**: Remove redundant requestType parameter in appleLoginCallback ([0aa5d5b](https://github.com/perfect-panel/ppanel-web/commit/0aa5d5b)) * **locales**: Order recharge related fields ([35210fe](https://github.com/perfect-panel/ppanel-web/commit/35210fe))
* **api**: Rename app-related functions and types to application for consistency ([9d8b814](https://github.com/perfect-panel/ppanel-web/commit/9d8b814))
* **api**: Update subscription_protocol to subscribe_type for consistency across services ([b6da51b](https://github.com/perfect-panel/ppanel-web/commit/b6da51b))
* **auth**: Change Textarea value to defaultValue for client_secret in Apple auth page ([69fc670](https://github.com/perfect-panel/ppanel-web/commit/69fc670))
* **auth**: Remove unused telephone code login function and update typings for telephone login requests ([7239685](https://github.com/perfect-panel/ppanel-web/commit/7239685))
* **auth**: Require minimum length for invite string when forced invite is enabled ([a604f28](https://github.com/perfect-panel/ppanel-web/commit/a604f28))
* **auth**: Update user authentication flow to include email and phone code verification ([5d078fd](https://github.com/perfect-panel/ppanel-web/commit/5d078fd))
* **dashboard**: Improve URL encoding for subscription links and enhance success message handling ([4983c33](https://github.com/perfect-panel/ppanel-web/commit/4983c33))
* **dashboard**: Update icon imports for platform consistency and adjust icon size ([3e8912e](https://github.com/perfect-panel/ppanel-web/commit/3e8912e))
* **dashboard**: Update platform detection logic and improve layout responsiveness ([b0aa364](https://github.com/perfect-panel/ppanel-web/commit/b0aa364))
* **deps**: Remove outdated @iconify/react dependency and add iconify-json packages ([d6fbc38](https://github.com/perfect-panel/ppanel-web/commit/d6fbc38))
* **locales**: Add error message for incorrect user information ([52c1d1f](https://github.com/perfect-panel/ppanel-web/commit/52c1d1f))
* **locales**: Add error message for incorrect user information ([3d92902](https://github.com/perfect-panel/ppanel-web/commit/3d92902))
* **locales**: Add logout message to authentication localization files ([1d0d911](https://github.com/perfect-panel/ppanel-web/commit/1d0d911))
* **nav**: Comment out unused social login options to simplify navigation configuration ([cefcb31](https://github.com/perfect-panel/ppanel-web/commit/cefcb31))
* **node-config**: Add null checks for time slots and ensure proper handling of undefined values ([1cdb7e7](https://github.com/perfect-panel/ppanel-web/commit/1cdb7e7))
* **node**: Add country and city fields to the form schema and localization files ([8775fb6](https://github.com/perfect-panel/ppanel-web/commit/8775fb6))
* **oauth**: Refactor OAuth configuration types and update related API methods ([6227ba9](https://github.com/perfect-panel/ppanel-web/commit/6227ba9))
* **oauth**: Remove redundant checks when updating configuration to simplify logic ([9140b8a](https://github.com/perfect-panel/ppanel-web/commit/9140b8a))
* **payment**: Replace window.open with window.location.href for checkout links ([1d8c765](https://github.com/perfect-panel/ppanel-web/commit/1d8c765))
* **phone**: Update SMS expiration time field to use 'sms_expire_time' with default value of 300 ([18b07c7](https://github.com/perfect-panel/ppanel-web/commit/18b07c7))
* **redirect**: Simplify redirect URL logic by removing unnecessary condition for sessionStorage ([c53ac61](https://github.com/perfect-panel/ppanel-web/commit/c53ac61))
* **redirect**: Update redirect URL logic to ensure proper handling of OAuth and auth paths ([7954762](https://github.com/perfect-panel/ppanel-web/commit/7954762))
* **site**: Add image upload functionality for site logo configuration ([4ea6e4a](https://github.com/perfect-panel/ppanel-web/commit/4ea6e4a))
* **sort**: Refactor sorting logic in NodeTable and SubscribeTable components for improved clarity and performance ([331bbea](https://github.com/perfect-panel/ppanel-web/commit/331bbea))
* **subscription**: Add reset functionality for user subscription token ([39e89bf](https://github.com/perfect-panel/ppanel-web/commit/39e89bf))
* **type**: Fix ts type check error ([3cb0629](https://github.com/perfect-panel/ppanel-web/commit/3cb0629))
* **user-nav**: Update user avatar and label to display telephone if email is not available ([7b6bb7b](https://github.com/perfect-panel/ppanel-web/commit/7b6bb7b))
* **user**: Update user identifier field and localizations ([1b6befa](https://github.com/perfect-panel/ppanel-web/commit/1b6befa))
<a name="readme-top"></a>
# Changelog
# [1.0.0-beta.23](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2025-02-24)
### 🐛 Bug Fixes
- **auth**: Update email verification logic to use domain suffix check ([62662bb](https://github.com/perfect-panel/ppanel-web/commit/62662bb))
# [1.0.0-beta.22](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2025-02-23)
### 🐛 Bug Fixes
- **locales**: Removed language file import to clean up unnecessary language support ([68f6ab2](https://github.com/perfect-panel/ppanel-web/commit/68f6ab2))
# [1.0.0-beta.21](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2025-02-23)
### ✨ Features
- **privacy-policy**: Add privacy policy related text and links ([baa68f0](https://github.com/perfect-panel/ppanel-web/commit/baa68f0))
### 🐛 Bug Fixes
- **locales**: Removed multilingual files to clean up unnecessary language support ([5b151cd](https://github.com/perfect-panel/ppanel-web/commit/5b151cd))
- **locales**: Update custom HTML description in language file, ([87381da](https://github.com/perfect-panel/ppanel-web/commit/87381da))
- **table**: Update privacy policy tab translation key and remove unnecessary requestType from OAuth callback ([14b3af5](https://github.com/perfect-panel/ppanel-web/commit/14b3af5))
<a name="readme-top"></a>
# Changelog
# [1.0.0-beta.20](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2025-02-23)
### 🐛 Bug Fixes
- **auth**: Add error handling to form submission and reset Turnstile on failure ([715d011](https://github.com/perfect-panel/ppanel-web/commit/715d011))
- **subscribe**: Update subscription domain placeholder to include examples; improve site name retrieval in global store ([c65a44c](https://github.com/perfect-panel/ppanel-web/commit/c65a44c))
# [1.0.0-beta.19](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2025-02-23)
### ✨ Features
- **form**: Make version field optional and set default value; update site domain placeholder for clarity ([42ba9e8](https://github.com/perfect-panel/ppanel-web/commit/42ba9e8))
### 🐛 Bug Fixes
- **auth**: Refactor forms to use Turnstile ref for reset functionality ([320a7dc](https://github.com/perfect-panel/ppanel-web/commit/320a7dc))
# [1.0.0-beta.18](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2025-02-22)
### ✨ Features
- **platform**: Update platform naming and add keywords and custom HTML fields ([6384237](https://github.com/perfect-panel/ppanel-web/commit/6384237))
- **site**: Added localization support for custom HTML and keyword fields ([f9d7736](https://github.com/perfect-panel/ppanel-web/commit/f9d7736))
### 🐛 Bug Fixes
- **locales**: Update custom HTML description for clarity across multiple languages ([557c5cd](https://github.com/perfect-panel/ppanel-web/commit/557c5cd))
<a name="readme-top"></a>
# Changelog
# [1.0.0-beta.17](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2025-02-21)
### 🐛 Bug Fixes
- **auth**: Refactor reset password form to simplify code input and update placeholder text ([23833b4](https://github.com/perfect-panel/ppanel-web/commit/23833b4))
# [1.0.0-beta.16](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2025-02-21)
### 🐛 Bug Fixes
- **auth**: Simplify email verification code input rendering ([6f7bc37](https://github.com/perfect-panel/ppanel-web/commit/6f7bc37))
# [1.0.0-beta.15](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2025-02-21)
### 🐛 Bug Fixes
- **profile**: Restore filter to ensure only valid OAuth accounts are shown ([315c8f9](https://github.com/perfect-panel/ppanel-web/commit/315c8f9))
# [1.0.0-beta.14](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2025-02-21)
### 🐛 Bug Fixes
- **locales**: Fixed description in multilingual files, updated text related to email registration functionality ([c356bc2](https://github.com/perfect-panel/ppanel-web/commit/c356bc2))
# [1.0.0-beta.13](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2025-02-21)
### 🐛 Bug Fixes
- **api**: Fix type error in API request and add return URL parameter ([ee286dd](https://github.com/perfect-panel/ppanel-web/commit/ee286dd))
<a name="readme-top"></a>
# Changelog
# [1.0.0-beta.12](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2025-02-18)
### 🐛 Bug Fixes
- More bugs ([2d88a3a](https://github.com/perfect-panel/ppanel-web/commit/2d88a3a))
# [1.0.0-beta.11](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2025-02-16)
### 🐛 Bug Fixes
- **email**: Update platform configuration handling to use current ref for consistency ([c90175b](https://github.com/perfect-panel/ppanel-web/commit/c90175b))
# [1.0.0-beta.10](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2025-02-16)
### 🐛 Bug Fixes
- **subscribe**: Update forms to include refetch functionality and improve toast messages ([fc55e95](https://github.com/perfect-panel/ppanel-web/commit/fc55e95))
# [1.0.0-beta.9](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2025-02-15)
### 🐛 Bug Fixes
- **register**: Adjust user email verification logic to handle domain suffix checks correctly ([686aa2d](https://github.com/perfect-panel/ppanel-web/commit/686aa2d))
# [1.0.0-beta.8](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2025-02-15)
### ✨ Features
- **accounts**: Update third-party account binding and unbinding ([1841552](https://github.com/perfect-panel/ppanel-web/commit/1841552))
- **auth-control**: Adding phone number labels to mobile verification configurations in multiple languages ([046740f](https://github.com/perfect-panel/ppanel-web/commit/046740f))
- **auth-control**: Update general ([3883646](https://github.com/perfect-panel/ppanel-web/commit/3883646))
- **auth**: Add type parameter to SendCode and update related API typings ([4198871](https://github.com/perfect-panel/ppanel-web/commit/4198871))
- **auth**: Refactor mobile authentication config to support whitelist functionality ([c761ec7](https://github.com/perfect-panel/ppanel-web/commit/c761ec7))
- **device**: Modify IMEI to device identifier support ([e3f9ef6](https://github.com/perfect-panel/ppanel-web/commit/e3f9ef6))
- **imei**: Add IMEI related internationalization support and menu items ([13c3337](https://github.com/perfect-panel/ppanel-web/commit/13c3337))
- **locales**: Add kick offline confirmation and success messages in multiple languages ([5db5343](https://github.com/perfect-panel/ppanel-web/commit/5db5343))
- **locales**: Update 'sms' to 'mobile' in authentication methods across multiple languages ([fea2171](https://github.com/perfect-panel/ppanel-web/commit/fea2171))
- **log**: Add message log retrieval functionality and update related typings ([1c0ecae](https://github.com/perfect-panel/ppanel-web/commit/1c0ecae))
- **profile**: Update localization strings and enhance third-party account binding ([2d1effb](https://github.com/perfect-panel/ppanel-web/commit/2d1effb))
- **release**: Extend supported platforms for Docker images, closes [#9](https://github.com/perfect-panel/ppanel-web/issues/9) ([e3a31eb](https://github.com/perfect-panel/ppanel-web/commit/e3a31eb))
- **subscription**: Add delete user subscription functionality ([1fc3a10](https://github.com/perfect-panel/ppanel-web/commit/1fc3a10))
- **ui**: Update input components and enhance card minimum width for better layout ([8a02310](https://github.com/perfect-panel/ppanel-web/commit/8a02310))
- **user**: Add user Detail ([3a3d223](https://github.com/perfect-panel/ppanel-web/commit/3a3d223))
- **user**: Add User Detail ([fdaf11b](https://github.com/perfect-panel/ppanel-web/commit/fdaf11b))
### 🐛 Bug Fixes
- **auth-control**: Fix citation error for platform values ([c940f3c](https://github.com/perfect-panel/ppanel-web/commit/c940f3c))
- **auth-control**: Fix citation error for platform values ([28813d2](https://github.com/perfect-panel/ppanel-web/commit/28813d2))
- **auth-control**: Rename phone_variable to phone_number in mobile verification configuration ([e5455aa](https://github.com/perfect-panel/ppanel-web/commit/e5455aa))
- **auth**: Update authentication configuration and localization strings ([47f2c58](https://github.com/perfect-panel/ppanel-web/commit/47f2c58))
- **locales**: Update expiration time description from minutes to seconds in multiple languages ([5bac933](https://github.com/perfect-panel/ppanel-web/commit/5bac933))
- **notify**: Ensure user info is updated after notification settings submission ([9bc3a94](https://github.com/perfect-panel/ppanel-web/commit/9bc3a94))
- **notify**: Set default values for notification settings to false ([3652819](https://github.com/perfect-panel/ppanel-web/commit/3652819))
- **third-party-accounts**: Remove mobile display logic from third-party accounts component ([b4946f7](https://github.com/perfect-panel/ppanel-web/commit/b4946f7))
- **third-party-accounts**: Update redirect property name in binding response handling ([012e83a](https://github.com/perfect-panel/ppanel-web/commit/012e83a))
- **user**: Refactor user form validation and reset password fields ([6733fc2](https://github.com/perfect-panel/ppanel-web/commit/6733fc2))
- **user**: Update locales ([4e7d249](https://github.com/perfect-panel/ppanel-web/commit/4e7d249))
- **user**: Update notification and verify code settings ([574b043](https://github.com/perfect-panel/ppanel-web/commit/574b043))
### 💄 Styles ### 💄 Styles
* **dashboard**: Adjust grid layout and update image dimensions in application display ([f3204b7](https://github.com/perfect-panel/ppanel-web/commit/f3204b7)) - **dashboard**: Enhance card components with full height and improved empty state handling ([7e1d551](https://github.com/perfect-panel/ppanel-web/commit/7e1d551))
* **globals**: Refactor delete confirmation button and update badge styles in node and subscribe tables ([30ae781](https://github.com/perfect-panel/ppanel-web/commit/30ae781))
* Update node secret UI and add telephone code field to authentication form ([770932e](https://github.com/perfect-panel/ppanel-web/commit/770932e)) <a name="readme-top"></a>
# Changelog
# [1.0.0-beta.7](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2025-01-28)
### ♻ Code Refactoring
- **sbscribe**: Rename and reorganize components for better structure and clarity ([5e5e4ed](https://github.com/perfect-panel/ppanel-web/commit/5e5e4ed))
### ✨ Features
- **auth**: Add email and SMS code sending functionality with localization updates ([57eaa55](https://github.com/perfect-panel/ppanel-web/commit/57eaa55))
- **auth**: Add Oauth configuration for Telegram, Facebook, Google, Github, and Apple ([18ee600](https://github.com/perfect-panel/ppanel-web/commit/18ee600))
- **auth**: Add SMS and email configuration options to global store and update localization ([4acf7b1](https://github.com/perfect-panel/ppanel-web/commit/4acf7b1))
- **auth**: Enhance user registration with invite handling and logo display ([207bc24](https://github.com/perfect-panel/ppanel-web/commit/207bc24))
- **auth**: Redirect user after OAuth login and add logos icon collection ([aa6dda8](https://github.com/perfect-panel/ppanel-web/commit/aa6dda8))
- **config**: Add application selection and encryption settings to configuration form ([88b3504](https://github.com/perfect-panel/ppanel-web/commit/88b3504))
- **config**: Update encryption fields in configuration form and refactor OAuth callback parameters ([652e032](https://github.com/perfect-panel/ppanel-web/commit/652e032))
- **global**: Add SMS configuration options to global store ([39a9ce6](https://github.com/perfect-panel/ppanel-web/commit/39a9ce6))
- **locales**: Add area code and telephone fields to user forms in multiple languages ([9b8258c](https://github.com/perfect-panel/ppanel-web/commit/9b8258c))
- **locales**: Add description information of communication keys and encryption methods to enhance client configuration capabilities ([d1f5a9b](https://github.com/perfect-panel/ppanel-web/commit/d1f5a9b))
- **node**: Add tags ([f408fdf](https://github.com/perfect-panel/ppanel-web/commit/f408fdf))
- **node**: Move the node configuration to the server module ([7f0f5ce](https://github.com/perfect-panel/ppanel-web/commit/7f0f5ce))
- **oauth**: Add certification component for handling OAuth login callbacks and improve user authentication flow ([5ed04c0](https://github.com/perfect-panel/ppanel-web/commit/5ed04c0))
- **oauth**: Implement OAuth token retrieval and refactor login callback handling ([40a6f7c](https://github.com/perfect-panel/ppanel-web/commit/40a6f7c))
- **oauth**: Refactor platform parameter handling and improve logout redirection logic ([8346c85](https://github.com/perfect-panel/ppanel-web/commit/8346c85))
- **oauth**: Update OAuth login handling to use callback parameter and improve URL parameter retrieval ([9227411](https://github.com/perfect-panel/ppanel-web/commit/9227411))
- **sms**: Update locales ([938363b](https://github.com/perfect-panel/ppanel-web/commit/938363b))
- **subscribe**: Add 'sold' column to SubscribeTable and update inventory terminology ([19619fd](https://github.com/perfect-panel/ppanel-web/commit/19619fd))
- **subscribe**: Move subscription configuration and application to subscription module ([f90d4d2](https://github.com/perfect-panel/ppanel-web/commit/f90d4d2))
- **subscribe**: Update SubscribeTable component to use API.SubscribeItem type and ensure proper type casting ([f26f1c2](https://github.com/perfect-panel/ppanel-web/commit/f26f1c2))
- **tutorial**: Fetch the latest tutorial version from GitHub API for dynamic URL generation ([28f8c78](https://github.com/perfect-panel/ppanel-web/commit/28f8c78))
- **user**: Add telephone input with area code selection and update localization ([585b99c](https://github.com/perfect-panel/ppanel-web/commit/585b99c))
- Update Auth Control ([c59742a](https://github.com/perfect-panel/ppanel-web/commit/c59742a))
### 🐛 Bug Fixes
- **api**: Remove redundant requestType parameter in appleLoginCallback ([0aa5d5b](https://github.com/perfect-panel/ppanel-web/commit/0aa5d5b))
- **api**: Rename app-related functions and types to application for consistency ([9d8b814](https://github.com/perfect-panel/ppanel-web/commit/9d8b814))
- **api**: Update subscription_protocol to subscribe_type for consistency across services ([b6da51b](https://github.com/perfect-panel/ppanel-web/commit/b6da51b))
- **auth**: Change Textarea value to defaultValue for client_secret in Apple auth page ([69fc670](https://github.com/perfect-panel/ppanel-web/commit/69fc670))
- **auth**: Remove unused telephone code login function and update typings for telephone login requests ([7239685](https://github.com/perfect-panel/ppanel-web/commit/7239685))
- **auth**: Require minimum length for invite string when forced invite is enabled ([a604f28](https://github.com/perfect-panel/ppanel-web/commit/a604f28))
- **auth**: Update user authentication flow to include email and phone code verification ([5d078fd](https://github.com/perfect-panel/ppanel-web/commit/5d078fd))
- **dashboard**: Improve URL encoding for subscription links and enhance success message handling ([4983c33](https://github.com/perfect-panel/ppanel-web/commit/4983c33))
- **dashboard**: Update icon imports for platform consistency and adjust icon size ([3e8912e](https://github.com/perfect-panel/ppanel-web/commit/3e8912e))
- **dashboard**: Update platform detection logic and improve layout responsiveness ([b0aa364](https://github.com/perfect-panel/ppanel-web/commit/b0aa364))
- **deps**: Remove outdated @iconify/react dependency and add iconify-json packages ([d6fbc38](https://github.com/perfect-panel/ppanel-web/commit/d6fbc38))
- **locales**: Add error message for incorrect user information ([52c1d1f](https://github.com/perfect-panel/ppanel-web/commit/52c1d1f))
- **locales**: Add error message for incorrect user information ([3d92902](https://github.com/perfect-panel/ppanel-web/commit/3d92902))
- **locales**: Add logout message to authentication localization files ([1d0d911](https://github.com/perfect-panel/ppanel-web/commit/1d0d911))
- **nav**: Comment out unused social login options to simplify navigation configuration ([cefcb31](https://github.com/perfect-panel/ppanel-web/commit/cefcb31))
- **node-config**: Add null checks for time slots and ensure proper handling of undefined values ([1cdb7e7](https://github.com/perfect-panel/ppanel-web/commit/1cdb7e7))
- **node**: Add country and city fields to the form schema and localization files ([8775fb6](https://github.com/perfect-panel/ppanel-web/commit/8775fb6))
- **oauth**: Refactor OAuth configuration types and update related API methods ([6227ba9](https://github.com/perfect-panel/ppanel-web/commit/6227ba9))
- **oauth**: Remove redundant checks when updating configuration to simplify logic ([9140b8a](https://github.com/perfect-panel/ppanel-web/commit/9140b8a))
- **payment**: Replace window.open with window.location.href for checkout links ([1d8c765](https://github.com/perfect-panel/ppanel-web/commit/1d8c765))
- **phone**: Update SMS expiration time field to use 'sms_expire_time' with default value of 300 ([18b07c7](https://github.com/perfect-panel/ppanel-web/commit/18b07c7))
- **redirect**: Simplify redirect URL logic by removing unnecessary condition for sessionStorage ([c53ac61](https://github.com/perfect-panel/ppanel-web/commit/c53ac61))
- **redirect**: Update redirect URL logic to ensure proper handling of OAuth and auth paths ([7954762](https://github.com/perfect-panel/ppanel-web/commit/7954762))
- **site**: Add image upload functionality for site logo configuration ([4ea6e4a](https://github.com/perfect-panel/ppanel-web/commit/4ea6e4a))
- **sort**: Refactor sorting logic in NodeTable and SubscribeTable components for improved clarity and performance ([331bbea](https://github.com/perfect-panel/ppanel-web/commit/331bbea))
- **subscription**: Add reset functionality for user subscription token ([39e89bf](https://github.com/perfect-panel/ppanel-web/commit/39e89bf))
- **type**: Fix ts type check error ([3cb0629](https://github.com/perfect-panel/ppanel-web/commit/3cb0629))
- **user-nav**: Update user avatar and label to display telephone if email is not available ([7b6bb7b](https://github.com/perfect-panel/ppanel-web/commit/7b6bb7b))
- **user**: Update user identifier field and localizations ([1b6befa](https://github.com/perfect-panel/ppanel-web/commit/1b6befa))
### 💄 Styles
- **dashboard**: Adjust grid layout and update image dimensions in application display ([f3204b7](https://github.com/perfect-panel/ppanel-web/commit/f3204b7))
- **globals**: Refactor delete confirmation button and update badge styles in node and subscribe tables ([30ae781](https://github.com/perfect-panel/ppanel-web/commit/30ae781))
- Update node secret UI and add telephone code field to authentication form ([770932e](https://github.com/perfect-panel/ppanel-web/commit/770932e))
<a name="readme-top"></a> <a name="readme-top"></a>
+13 -4
View File
@@ -5,10 +5,10 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/
import { Input } from '@workspace/ui/components/input'; import { Input } from '@workspace/ui/components/input';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction, useRef } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import CloudFlareTurnstile from '../turnstile'; import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
export default function LoginForm({ export default function LoginForm({
loading, loading,
@@ -38,10 +38,19 @@ export default function LoginForm({
defaultValues: initialValues, defaultValues: initialValues,
}); });
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (error) {
turnstile.current?.reset();
}
});
return ( return (
<> <>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'> <form onSubmit={handleSubmit} className='grid gap-6'>
<FormField <FormField
control={form.control} control={form.control}
name='email' name='email'
@@ -73,7 +82,7 @@ export default function LoginForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<CloudFlareTurnstile id='login' {...field} /> <CloudFlareTurnstile id='login' {...field} ref={turnstile} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
+26 -12
View File
@@ -6,11 +6,11 @@ import { Input } from '@workspace/ui/components/input';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { Markdown } from '@workspace/ui/custom-components/markdown'; import { Markdown } from '@workspace/ui/custom-components/markdown';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction, useRef } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import SendCode from '../send-code'; import SendCode from '../send-code';
import CloudFlareTurnstile from '../turnstile'; import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
export default function RegisterForm({ export default function RegisterForm({
loading, loading,
@@ -31,11 +31,10 @@ export default function RegisterForm({
const handleCheckUser = async (email: string) => { const handleCheckUser = async (email: string) => {
try { try {
if (!auth.email.enable_domain_suffix) return true;
const domain = email.split('@')[1]; const domain = email.split('@')[1];
const isValid = const isValid = auth.email?.domain_suffix_list.split('\n').includes(domain || '');
!auth.email.email_enable_verify || return isValid;
auth.email?.email_domain_suffix_list.split('\n').includes(domain || '');
return !isValid;
} catch (error) { } catch (error) {
console.log('Error checking user:', error); console.log('Error checking user:', error);
return false; return false;
@@ -52,8 +51,8 @@ export default function RegisterForm({
}), }),
password: z.string(), password: z.string(),
repeat_password: z.string(), repeat_password: z.string(),
code: auth.email.email_enable_verify ? z.string() : z.string().nullish(), code: auth.email.enable_verify ? z.string() : z.string().nullish(),
invite: invite.forced_invite ? z.string() : z.string().nullish(), invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
cf_token: cf_token:
verify.enable_register_verify && verify.turnstile_site_key verify.enable_register_verify && verify.turnstile_site_key
? z.string() ? z.string()
@@ -77,13 +76,22 @@ export default function RegisterForm({
}, },
}); });
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (error) {
turnstile.current?.reset();
}
});
return ( return (
<> <>
{auth.register.stop_register ? ( {auth.register.stop_register ? (
<Markdown>{t('message')}</Markdown> <Markdown>{t('message')}</Markdown>
) : ( ) : (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'> <form onSubmit={handleSubmit} className='grid gap-6'>
<FormField <FormField
control={form.control} control={form.control}
name='email' name='email'
@@ -114,13 +122,18 @@ export default function RegisterForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input placeholder='Enter password again...' type='password' {...field} /> <Input
disabled={loading}
placeholder='Enter password again...'
type='password'
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{auth.email.email_enable_verify && ( {auth.email.enable_verify && (
<FormField <FormField
control={form.control} control={form.control}
name='code' name='code'
@@ -129,6 +142,7 @@ export default function RegisterForm({
<FormControl> <FormControl>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Input <Input
disabled={loading}
placeholder='Enter code...' placeholder='Enter code...'
type='text' type='text'
{...field} {...field}
@@ -172,7 +186,7 @@ export default function RegisterForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<CloudFlareTurnstile id='register' {...field} /> <CloudFlareTurnstile id='register' {...field} ref={turnstile} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
+43 -36
View File
@@ -5,11 +5,11 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/
import { Input } from '@workspace/ui/components/input'; import { Input } from '@workspace/ui/components/input';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction, useRef } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { z } from 'zod'; import { z } from 'zod';
import SendCode from '../send-code'; import SendCode from '../send-code';
import CloudFlareTurnstile from '../turnstile'; import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
export default function ResetForm({ export default function ResetForm({
loading, loading,
@@ -32,7 +32,7 @@ export default function ResetForm({
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email(t('email')), email: z.string().email(t('email')),
password: z.string(), password: z.string(),
code: auth?.email?.email_enable_verify ? z.string() : z.string().nullish(), code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
cf_token: cf_token:
verify.enable_register_verify && verify.turnstile_site_key verify.enable_register_verify && verify.turnstile_site_key
? z.string() ? z.string()
@@ -43,10 +43,19 @@ export default function ResetForm({
defaultValues: initialValues, defaultValues: initialValues,
}); });
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (error) {
turnstile.current?.reset();
}
});
return ( return (
<> <>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'> <form onSubmit={handleSubmit} className='grid gap-6'>
<FormField <FormField
control={form.control} control={form.control}
name='email' name='email'
@@ -61,45 +70,43 @@ export default function ResetForm({
/> />
<FormField <FormField
control={form.control} control={form.control}
name='password' name='code'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<Input placeholder='Enter your password...' type='password' {...field} /> <div className='flex items-center gap-2'>
<Input
disabled={loading}
placeholder='Enter code...'
type='text'
{...field}
value={field.value as string}
/>
<SendCode
type='email'
params={{
...form.getValues(),
type: 2,
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem>
<FormControl>
<Input placeholder='Enter your new password...' type='password' {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{auth?.email?.email_enable_verify && (
<FormField
control={form.control}
name='code'
render={({ field }) => (
<FormItem>
<FormControl>
<div className='flex items-center gap-2'>
<Input
placeholder='Enter code...'
type='text'
{...field}
value={field.value as string}
/>
<SendCode
type='email'
params={{
...form.getValues(),
type: 2,
}}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{verify.enable_reset_password_verify && ( {verify.enable_reset_password_verify && (
<FormField <FormField
control={form.control} control={form.control}
@@ -107,7 +114,7 @@ export default function ResetForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<CloudFlareTurnstile id='reset' {...field} /> <CloudFlareTurnstile id='reset' {...field} ref={turnstile} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
+2 -1
View File
@@ -37,10 +37,11 @@ export default function SendCode({ type, params }: SendCodeProps) {
}; };
const getPhoneCode = async () => { const getPhoneCode = async () => {
if (params.telephone && params.telephone_area_code) { if (params.telephone && params.telephone_area_code && params.type) {
await sendSmsCode({ await sendSmsCode({
telephone: params.telephone, telephone: params.telephone,
telephone_area_code: params.telephone_area_code, telephone_area_code: params.telephone_area_code,
type: params.type,
}); });
setTargetDate(Date.now() + 60000); setTargetDate(Date.now() + 60000);
} }
+26 -12
View File
@@ -1,26 +1,38 @@
'use client'; 'use client';
import useGlobalStore from '@/config/use-global';
import { useLocale } from 'next-intl'; import { useLocale } from 'next-intl';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useEffect } from 'react'; import { forwardRef, useEffect, useImperativeHandle } from 'react';
import Turnstile, { useTurnstile } from 'react-turnstile'; import Turnstile, { useTurnstile } from 'react-turnstile';
export default function CloudFlareTurnstile({ import useGlobalStore from '@/config/use-global';
id,
value, export type TurnstileRef = {
onChange, reset: () => void;
}: { };
id?: string;
value?: null | string; const CloudFlareTurnstile = forwardRef<
onChange: (value?: string) => void; TurnstileRef,
}) { {
id?: string;
value?: null | string;
onChange: (value?: string) => void;
}
>(function CloudFlareTurnstile({ id, value, onChange }, ref) {
const { common } = useGlobalStore(); const { common } = useGlobalStore();
const { verify } = common; const { verify } = common;
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const locale = useLocale(); const locale = useLocale();
const turnstile = useTurnstile(); const turnstile = useTurnstile();
useImperativeHandle(
ref,
() => ({
reset: () => turnstile.reset(),
}),
[turnstile],
);
useEffect(() => { useEffect(() => {
if (value === '') { if (value === '') {
turnstile.reset(); turnstile.reset();
@@ -51,4 +63,6 @@ export default function CloudFlareTurnstile({
/> />
) )
); );
} });
export default CloudFlareTurnstile;
+299
View File
@@ -0,0 +1,299 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@workspace/ui/components/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@workspace/ui/components/form';
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
const formSchema = z.object({
title: z.string(),
type: z.enum(['image', 'video']),
content: z.string(),
description: z.string(),
target_url: z.string().url(),
start_time: z.number(),
end_time: z.number(),
});
interface AdsFormProps<T> {
onSubmit: (data: T) => Promise<boolean> | boolean;
initialValues?: T;
loading?: boolean;
trigger: string;
title: string;
}
export default function AdsForm<T extends Record<string, any>>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: AdsFormProps<T>) {
const t = useTranslations('ads');
const [open, setOpen] = useState(false);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
...initialValues,
} as any,
});
useEffect(() => {
form?.reset(initialValues);
}, [form, initialValues]);
const type = form.watch('type');
const startTime = form.watch('start_time');
const renderContentField = () => {
return (
<FormField
control={form.control}
name='content'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.content')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={
type === 'image'
? 'https://example.com/image.jpg'
: 'https://example.com/video.mp4'
}
value={field.value}
onValueChange={(value) => {
form.setValue('content', value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
async function handleSubmit(data: { [x: string]: any }) {
const bool = await onSubmit(data as T);
if (bool) setOpen(false);
}
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
onClick={() => {
form.reset();
setOpen(true);
}}
>
{trigger}
</Button>
</SheetTrigger>
<SheetContent className='w-[500px] max-w-full md:max-w-screen-md'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='-mx-6 h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))]'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4 px-6 pt-4'>
<FormField
control={form.control}
name='title'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.title')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.enterTitle')}
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='type'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.type')}</FormLabel>
<FormControl>
<RadioGroup
defaultValue={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
className='flex gap-4'
>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='image' />
</FormControl>
<FormLabel className='font-normal'>{t('form.typeImage')}</FormLabel>
</FormItem>
<FormItem className='flex items-center space-x-3 space-y-0'>
<FormControl>
<RadioGroupItem value='video' />
</FormControl>
<FormLabel className='font-normal'>{t('form.typeVideo')}</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{renderContentField()}
<FormField
control={form.control}
name='description'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.description')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.enterDescription')}
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='target_url'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.targetUrl')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('form.enterTargetUrl')}
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='start_time'
render={({ field }) => (
<FormItem>
<FormLabel>{t('form.startTime')}</FormLabel>
<FormControl>
<EnhancedInput
type='datetime-local'
placeholder={t('form.enterStartTime')}
value={field.value ? new Date(field.value).toISOString().slice(0, 16) : ''}
min={Number(new Date().toISOString().slice(0, 16))}
onValueChange={(value) => {
const timestamp = value ? new Date(value).getTime() : 0;
form.setValue(field.name, timestamp);
const endTime = form.getValues('end_time');
if (endTime && timestamp > endTime) {
form.setValue('end_time', '');
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='end_time'
render={({ field }) => {
return (
<FormItem>
<FormLabel>{t('form.endTime')}</FormLabel>
<FormControl>
<EnhancedInput
type='datetime-local'
placeholder={t('form.enterEndTime')}
value={
field.value ? new Date(field.value).toISOString().slice(0, 16) : ''
}
min={Number(
startTime
? new Date(startTime).toISOString().slice(0, 16)
: new Date().toISOString().slice(0, 16),
)}
disabled={!startTime}
onValueChange={(value) => {
const timestamp = value ? new Date(value).getTime() : 0;
if (!startTime || timestamp < startTime) return;
form.setValue(field.name, timestamp);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button
variant='outline'
disabled={loading}
onClick={() => {
setOpen(false);
}}
>
{t('form.cancel')}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('form.confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
+162
View File
@@ -0,0 +1,162 @@
'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import { createAds, deleteAds, getAdsList, updateAds } from '@/services/admin/ads';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button';
import { Switch } from '@workspace/ui/components/switch';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
import AdsForm from './ads-form';
export default function Page() {
const t = useTranslations('ads');
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>(null);
return (
<ProTable<API.Ads, Record<string, unknown>>
action={ref}
header={{
toolbar: (
<AdsForm<API.CreateAdsRequest>
trigger={t('create')}
title={t('createAds')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createAds({
...values,
status: 0,
});
toast.success(t('createSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>
),
}}
params={[
{
key: 'search',
},
{
key: 'status',
placeholder: t('status'),
options: [
{ label: t('enabled'), value: '1' },
{ label: t('disabled'), value: '0' },
],
},
]}
request={async (pagination, filters) => {
const { data } = await getAdsList({
...pagination,
...filters,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
columns={[
{
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => {
return (
<Switch
defaultChecked={row.getValue('status') === 1}
onCheckedChange={async (checked) => {
await updateAds({
...row.original,
status: checked ? 1 : 0,
});
ref.current?.refresh();
}}
/>
);
},
},
{
accessorKey: 'title',
header: t('title'),
},
{
accessorKey: 'type',
header: t('type'),
cell: ({ row }) => {
const type = row.original.type;
return <Badge>{type}</Badge>;
},
},
{
accessorKey: 'target_url',
header: t('targetUrl'),
},
{
accessorKey: 'description',
header: t('form.description'),
},
{
accessorKey: 'period',
header: t('validityPeriod'),
cell: ({ row }) => {
const { start_time, end_time } = row.original;
return (
<>
{formatDate(start_time)} - {formatDate(end_time)}
</>
);
},
},
]}
actions={{
render: (row) => [
<AdsForm<API.UpdateAdsRequest>
key='edit'
trigger={t('edit')}
title={t('editAds')}
loading={loading}
initialValues={row}
onSubmit={async (values) => {
setLoading(true);
try {
await updateAds({ ...row, ...values });
toast.success(t('updateSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteWarning')}
onConfirm={async () => {
await deleteAds({ id: row.id });
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
],
}}
/>
);
}
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system'; import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
@@ -14,21 +14,21 @@ export default function Page() {
const t = useTranslations('apple'); const t = useTranslations('apple');
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'apple'], queryKey: ['getAuthMethodConfig', 'apple'],
queryFn: async () => { queryFn: async () => {
const { data } = await getOAuthByPlatform({ const { data } = await getAuthMethodConfig({
platform: 'apple', method: 'apple',
}); });
return data.data; return data.data;
}, },
}); });
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) { async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try { try {
await updateOAuthConfig({ await updateAuthMethodConfig({
...data, ...data,
[key]: value, [key]: value,
} as API.UpdateOAuthConfig); } as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess')); toast.success(t('saveSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
@@ -131,9 +131,14 @@ export default function Page() {
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
<EnhancedInput <EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/apple' placeholder='https://your-domain.com'
value={data?.redirect} value={data?.config.redirect_url}
onValueBlur={(value) => updateConfig('redirect', value)} onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
redirect_url: value,
})
}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -0,0 +1,141 @@
'use client';
import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { DicesIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { uid } from 'radash';
import { toast } from 'sonner';
export default function Page() {
const t = useTranslations('device');
const { data, refetch } = useQuery({
queryKey: ['getAuthMethodConfig', 'device'],
queryFn: async () => {
const { data } = await getAuthMethodConfig({
method: 'device',
});
return data.data;
},
});
async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try {
await updateAuthMethodConfig({
...data,
[key]: value,
} as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
toast.error(t('saveFailed'));
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enable')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('enabled', checked)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('showAds')}</Label>
<p className='text-muted-foreground text-xs'>{t('showAdsDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.show_ads}
onCheckedChange={(checked) => {
updateConfig('config', {
...data?.config,
show_ads: checked,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('blockVirtualMachine')}</Label>
<p className='text-muted-foreground text-xs'>{t('blockVirtualMachineDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.only_real_device}
onCheckedChange={(checked) => {
updateConfig('config', {
...data?.config,
only_real_device: checked,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('enableSecurity')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableSecurityDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.enable_security}
onCheckedChange={(checked) => {
updateConfig('config', {
...data?.config,
enable_security: checked,
});
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('communicationKey')}</Label>
<p className='text-muted-foreground text-xs'>{t('communicationKeyDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.security_secret}
onValueBlur={(value) => {
updateConfig('config', {
...data?.config,
security_secret: value,
});
}}
suffix={
<div className='bg-muted flex h-9 items-center text-nowrap px-3'>
<DicesIcon
onClick={() => {
const id = uid(32).toLowerCase();
const formatted = `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
updateConfig('config', {
...data?.config,
security_secret: formatted,
});
}}
className='cursor-pointer'
/>
</div>
}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -1,6 +1,10 @@
'use client'; 'use client';
import { getEmailSmtpConfig, testEmailSmtp, updateEmailSmtpConfig } from '@/services/admin/system'; import {
getAuthMethodConfig,
testEmailSend,
updateAuthMethodConfig,
} from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { import {
@@ -20,17 +24,20 @@ import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { LogsTable } from '../log';
export default function Page() { export default function Page() {
const t = useTranslations('email'); const t = useTranslations('email');
const ref = useRef<Partial<API.EmailSmtpConfig>>({}); const ref = useRef<Partial<API.AuthMethodConfig>>({});
const [email, setEmail] = useState<string>(); const [email, setEmail] = useState<string>();
const { data, refetch, isFetching } = useQuery({ const { data, refetch, isFetching } = useQuery({
queryKey: ['getEmailSmtpConfig'], queryKey: ['getAuthMethodConfig', 'email'],
queryFn: async () => { queryFn: async () => {
const { data } = await getEmailSmtpConfig(); const { data } = await getAuthMethodConfig({
ref.current = data.data as API.EmailSmtpConfig; method: 'email',
});
ref.current = data.data as API.AuthMethodConfig;
return data.data; return data.data;
}, },
}); });
@@ -38,10 +45,10 @@ export default function Page() {
async function updateConfig(key: string, value: unknown) { async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return; if (data?.[key] === value) return;
try { try {
await updateEmailSmtpConfig({ await updateAuthMethodConfig({
...ref.current, ...ref.current,
[key]: value, [key]: value,
} as API.EmailSmtpConfig); } as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess')); toast.success(t('saveSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
@@ -50,103 +57,202 @@ export default function Page() {
} }
return ( return (
<Tabs defaultValue='basic'> <Tabs defaultValue='settings'>
<TabsList> <TabsList className='h-full flex-wrap'>
<TabsTrigger value='basic'>{t('emailBasicConfig')}</TabsTrigger> <TabsTrigger value='settings'>{t('settings')}</TabsTrigger>
<TabsTrigger value='template'>{t('emailTemplate')}</TabsTrigger> <TabsTrigger value='template'>{t('template')}</TabsTrigger>
<TabsTrigger value='logs'>{t('emailLogs')}</TabsTrigger> <TabsTrigger value='logs'>{t('logs')}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='basic'> <TabsContent value='settings'>
<Table> <Table>
<TableBody> <TableBody>
{[ <TableRow>
{ <TableCell>
key: 'email_enabled', <Label>{t('enable')}</Label>
label: t('enable'), <p className='text-muted-foreground text-xs'>{t('enableDescription')}</p>
description: t('enableDescription'), </TableCell>
component: 'switch', <TableCell className='text-right'>
}, <Switch
{ checked={data?.enabled}
key: 'enable_email_verify', onCheckedChange={(checked) => updateConfig('enabled', checked)}
label: t('emailVerification'), />
description: t('emailVerificationDescription'), </TableCell>
component: 'switch', </TableRow>
}, <TableRow>
{ <TableCell>
key: 'enable_email_domain_suffix', <Label>{t('emailVerification')}</Label>
label: t('emailSuffixWhitelist'), <p className='text-muted-foreground text-xs'>{t('emailVerificationDescription')}</p>
description: t('emailSuffixWhitelistDescription'), </TableCell>
component: 'switch', <TableCell className='text-right'>
}, <Switch
{ checked={data?.config?.enable_verify}
key: 'email_domain_suffix_list', onCheckedChange={(checked) =>
label: t('whitelistSuffixes'), updateConfig('config', { ...data?.config, enable_verify: checked })
description: t('whitelistSuffixesDescription'), }
component: 'textarea', />
}, </TableCell>
{ </TableRow>
key: 'email_smtp_host', <TableRow>
label: t('smtpServerAddress'), <TableCell>
description: t('smtpServerAddressDescription'), <Label>{t('emailSuffixWhitelist')}</Label>
}, <p className='text-muted-foreground text-xs'>
{ {t('emailSuffixWhitelistDescription')}
key: 'email_smtp_port', </p>
label: t('smtpServerPort'), </TableCell>
description: t('smtpServerPortDescription'), <TableCell className='text-right'>
type: 'number', <Switch
}, checked={data?.config?.enable_domain_suffix}
{ onCheckedChange={(checked) =>
key: 'email_smtp_ssl', updateConfig('config', { ...data?.config, enable_domain_suffix: checked })
label: t('smtpEncryptionMethod'), }
description: t('smtpEncryptionMethodDescription'), />
component: 'switch', </TableCell>
}, </TableRow>
{ <TableRow>
key: 'email_smtp_user', <TableCell className='align-top'>
label: t('smtpAccount'), <Label>{t('whitelistSuffixes')}</Label>
description: t('smtpAccountDescription'), <p className='text-muted-foreground text-xs'>{t('whitelistSuffixesDescription')}</p>
}, </TableCell>
{ <TableCell className='text-right'>
key: 'email_smtp_pass', <Textarea
label: t('smtpPassword'), className='h-32'
description: t('smtpPasswordDescription'), placeholder={t('whitelistSuffixesPlaceholder')}
type: 'password', defaultValue={data?.config?.domain_suffix_list}
}, onBlur={(e) =>
{ updateConfig('config', { ...data?.config, domain_suffix_list: e.target.value })
key: 'email_smtp_from', }
label: t('senderAddress'), />
description: t('senderAddressDescription'), </TableCell>
}, </TableRow>
].map(({ key, label, description, type = 'text', component = 'input' }) => ( <TableRow>
<TableRow key={key}> <TableCell>
<TableCell className={component === 'textarea' ? 'align-top' : undefined}> <Label>{t('smtpServerAddress')}</Label>
<Label>{label}</Label> <p className='text-muted-foreground text-xs'>{t('smtpServerAddressDescription')}</p>
<p className='text-muted-foreground text-xs'>{description}</p> </TableCell>
</TableCell> <TableCell className='text-right'>
<TableCell className='text-right'> <EnhancedInput
{component === 'input' ? ( placeholder={t('inputPlaceholder')}
<EnhancedInput value={data?.config?.platform_config?.host}
placeholder={t('inputPlaceholder')} onValueBlur={(value) =>
value={data?.[key]} updateConfig('config', {
type={type} ...data?.config,
onValueBlur={(value) => updateConfig(key, value)} platform_config: {
/> ...data?.platform_config,
) : component === 'switch' ? ( host: value,
<Switch },
checked={data?.[key]} })
onCheckedChange={(checked) => updateConfig(key, checked)} }
/> />
) : ( </TableCell>
<Textarea </TableRow>
className='h-32' <TableRow>
placeholder={t('whitelistSuffixesPlaceholder')} <TableCell>
defaultValue={data?.[key]} <Label>{t('smtpServerPort')}</Label>
onBlur={(e) => updateConfig(key, e.target.value)} <p className='text-muted-foreground text-xs'>{t('smtpServerPortDescription')}</p>
/> </TableCell>
)} <TableCell className='text-right'>
</TableCell> <EnhancedInput
</TableRow> placeholder={t('inputPlaceholder')}
))} value={data?.config?.platform_config?.port}
type='number'
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
port: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpEncryptionMethod')}</Label>
<p className='text-muted-foreground text-xs'>
{t('smtpEncryptionMethodDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.config?.platform_config?.ssl}
onCheckedChange={(checked) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
ssl: checked,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpAccount')}</Label>
<p className='text-muted-foreground text-xs'>{t('smtpAccountDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.user}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
user: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('smtpPassword')}</Label>
<p className='text-muted-foreground text-xs'>{t('smtpPasswordDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.pass}
type='password'
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
pass: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('senderAddress')}</Label>
<p className='text-muted-foreground text-xs'>{t('senderAddressDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.config?.platform_config?.from}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...ref.current?.config?.platform_config,
from: value,
},
})
}
/>
</TableCell>
</TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('sendTestEmail')}</Label> <Label>{t('sendTestEmail')}</Label>
@@ -164,7 +270,7 @@ export default function Page() {
onClick={async () => { onClick={async () => {
if (!email) return; if (!email) return;
try { try {
await testEmailSmtp({ email }); await testEmailSend({ email });
toast.success(t('sendSuccess')); toast.success(t('sendSuccess'));
} catch { } catch {
toast.error(t('sendFailure')); toast.error(t('sendFailure'));
@@ -193,8 +299,13 @@ export default function Page() {
<CardContent> <CardContent>
<HTMLEditor <HTMLEditor
placeholder={t('inputPlaceholder')} placeholder={t('inputPlaceholder')}
value={data?.[templateKey as keyof API.EmailSmtpConfig] as string} value={data?.config?.[templateKey] as string}
onBlur={(value) => updateConfig(templateKey, value)} onBlur={(value) =>
updateConfig('config', {
...data?.config,
[templateKey]: value,
})
}
/> />
</CardContent> </CardContent>
</Card> </Card>
@@ -203,7 +314,9 @@ export default function Page() {
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value='logs'>Logs</TabsContent> <TabsContent value='logs'>
<LogsTable type='email' />
</TabsContent>
</Tabs> </Tabs>
); );
} }
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system'; import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
@@ -13,21 +13,21 @@ export default function Page() {
const t = useTranslations('facebook'); const t = useTranslations('facebook');
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'facebook'], queryKey: ['getAuthMethodConfig', 'facebook'],
queryFn: async () => { queryFn: async () => {
const { data } = await getOAuthByPlatform({ const { data } = await getAuthMethodConfig({
platform: 'facebook', method: 'facebook',
}); });
return data.data; return data.data;
}, },
}); });
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) { async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try { try {
await updateOAuthConfig({ await updateAuthMethodConfig({
...data, ...data,
[key]: value, [key]: value,
} as API.UpdateOAuthConfig); } as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess')); toast.success(t('saveSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
@@ -86,19 +86,6 @@ export default function Page() {
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/facebook'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody> </TableBody>
</Table> </Table>
); );
@@ -0,0 +1,103 @@
'use client';
import { getInviteConfig, updateInviteConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export function Invite() {
const t = useTranslations('auth-control.invite');
const { data, refetch } = useQuery({
queryKey: ['getInviteConfig'],
queryFn: async () => {
const { data } = await getInviteConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateInviteConfig({
...data,
[key]: value,
} as API.InviteConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Card>
<CardHeader>
<CardTitle>{t('inviteSettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enableForcedInvite')}</Label>
<p className='text-muted-foreground text-xs'>
{t('enableForcedInviteDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.forced_invite}
onCheckedChange={(checked) => {
updateConfig('forced_invite', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('inviteCommissionPercentage')}</Label>
<p className='text-muted-foreground text-xs'>
{t('inviteCommissionPercentageDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.referral_percentage}
type='number'
min={0}
max={100}
suffix='%'
onValueBlur={(value) => updateConfig('referral_percentage', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('commissionFirstTimeOnly')}</Label>
<p className='text-muted-foreground text-xs'>
{t('commissionFirstTimeOnlyDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.only_first_purchase}
onCheckedChange={(checked) => {
updateConfig('only_first_purchase', checked);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}
@@ -1,118 +1,17 @@
'use client'; 'use client';
import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system'; import { Invite } from './invite';
import { useQuery } from '@tanstack/react-query'; import { Register } from './register';
import { Label } from '@workspace/ui/components/label'; import { Verify } from './verify';
import { Switch } from '@workspace/ui/components/switch'; import { VerifyCode } from './verify-code';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Page() { export default function Page() {
const t = useTranslations('auth-control');
const { data, refetch } = useQuery({
queryKey: ['getRegisterConfig'],
queryFn: async () => {
const { data } = await getRegisterConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateRegisterConfig({
...data,
[key]: value,
} as API.RegisterConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return ( return (
<Table> <div className='space-y-3'>
<TableBody> <Invite />
<TableRow> <Register />
<TableCell> <VerifyCode />
<Label>{t('stopNewUserRegistration')}</Label> <Verify />
<p className='text-muted-foreground text-xs'> </div>
{t('stopNewUserRegistrationDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.stop_register}
onCheckedChange={(checked) => {
updateConfig('stop_register', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('ipRegistrationLimit')}</Label>
<p className='text-muted-foreground text-xs'>{t('ipRegistrationLimitDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_ip_register_limit}
onCheckedChange={(checked) => {
updateConfig('enable_ip_register_limit', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('registrationLimitCount')}</Label>
<p className='text-muted-foreground text-xs'>
{t('registrationLimitCountDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
min={0}
value={data?.ip_register_limit}
onValueBlur={(value) => updateConfig('ip_register_limit', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('penaltyTime')}</Label>
<p className='text-muted-foreground text-xs'>{t('penaltyTimeDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
min={0}
value={data?.ip_register_limit_duration}
onValueBlur={(value) => updateConfig('ip_register_limit_duration', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('trialRegistration')}</Label>
<p className='text-muted-foreground text-xs'>{t('trialRegistrationDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_trial}
onCheckedChange={(checked) => {
updateConfig('enable_trial', checked);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
); );
} }
@@ -0,0 +1,201 @@
'use client';
import { getSubscribeList } from '@/services/admin/subscribe';
import { getRegisterConfig, updateRegisterConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@workspace/ui/components/select';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { Combobox } from '@workspace/ui/custom-components/combobox';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export function Register() {
const t = useTranslations('auth-control.register');
const { data, refetch } = useQuery({
queryKey: ['getRegisterConfig'],
queryFn: async () => {
const { data } = await getRegisterConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
await updateRegisterConfig({
...data,
[key]: value,
} as API.RegisterConfig);
toast.success(t('saveSuccess'));
refetch();
}
const { data: subscribe } = useQuery({
queryKey: ['getSubscribeList', 'all'],
queryFn: async () => {
const { data } = await getSubscribeList({
page: 1,
size: 9999,
});
return data.data?.list as API.Subscribe[];
},
});
return (
<Card>
<CardHeader>
<CardTitle>{t('registerSettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('stopNewUserRegistration')}</Label>
<p className='text-muted-foreground text-xs'>
{t('stopNewUserRegistrationDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.stop_register}
onCheckedChange={(checked) => {
updateConfig('stop_register', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('ipRegistrationLimit')}</Label>
<p className='text-muted-foreground text-xs'>
{t('ipRegistrationLimitDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_ip_register_limit}
onCheckedChange={(checked) => {
updateConfig('enable_ip_register_limit', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('registrationLimitCount')}</Label>
<p className='text-muted-foreground text-xs'>
{t('registrationLimitCountDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
min={0}
value={data?.ip_register_limit}
onValueBlur={(value) => updateConfig('ip_register_limit', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('penaltyTime')}</Label>
<p className='text-muted-foreground text-xs'>{t('penaltyTimeDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
min={0}
value={data?.ip_register_limit_duration}
onValueBlur={(value) => updateConfig('ip_register_limit_duration', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('trialRegistration')}</Label>
<p className='text-muted-foreground text-xs'>{t('trialRegistrationDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_trial}
onCheckedChange={(checked) => {
updateConfig('enable_trial', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('trialSubscribePlan')}</Label>
<p className='text-muted-foreground text-xs'>
{t('trialSubscribePlanDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('trialDuration')}
type='number'
min={0}
value={data?.trial_time}
onValueBlur={(value) => updateConfig('trial_time', value)}
prefix={
<Select
value={String(data?.trial_subscribe)}
onValueChange={(value) => updateConfig('trial_subscribe', Number(value))}
>
<SelectTrigger className='bg-secondary rounded-r-none'>
{data?.trial_subscribe ? (
<SelectValue placeholder='Select Subscribe' />
) : (
'Select Subscribe'
)}
</SelectTrigger>
<SelectContent>
{subscribe?.map((item) => (
<SelectItem key={item.id} value={String(item.id)}>
{item.name}
</SelectItem>
))}
</SelectContent>
</Select>
}
suffix={
<Combobox
className='bg-secondary rounded-l-none'
value={data?.trial_time_unit}
onChange={(value) => {
if (value) {
updateConfig('trial_time_unit', value);
}
}}
options={[
{ label: t('noLimit'), value: 'NoLimit' },
{ label: t('year'), value: 'Year' },
{ label: t('month'), value: 'Month' },
{ label: t('day'), value: 'Day' },
{ label: t('hour'), value: 'Hour' },
{ label: t('minute'), value: 'Minute' },
]}
/>
}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}
@@ -0,0 +1,95 @@
'use client';
import { getVerifyCodeConfig, updateVerifyCodeConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export function VerifyCode() {
const t = useTranslations('auth-control.verify-code');
const { data, refetch } = useQuery({
queryKey: ['getVerifyCodeConfig'],
queryFn: async () => {
const { data } = await getVerifyCodeConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateVerifyCodeConfig({
...data,
[key]: value,
} as API.VerifyCodeConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Card className='mb-6'>
<CardHeader>
<CardTitle>{t('verifyCodeSettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('expireTime')}</Label>
<p className='text-muted-foreground text-xs'>{t('expireTimeDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
placeholder='300'
value={data?.verify_code_expire_time}
onValueBlur={(value) => updateConfig('verify_code_expire_time', Number(value))}
suffix={t('second')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('interval')}</Label>
<p className='text-muted-foreground text-xs'>{t('intervalDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
placeholder='60'
value={data?.verify_code_interval}
onValueBlur={(value) => updateConfig('verify_code_interval', Number(value))}
suffix={t('second')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('dailyLimit')}</Label>
<p className='text-muted-foreground text-xs'>{t('dailyLimitDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
placeholder='15'
value={data?.verify_code_limit}
onValueBlur={(value) => updateConfig('verify_code_limit', Number(value))}
suffix={t('times')}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}
@@ -0,0 +1,125 @@
'use client';
import { getVerifyConfig, updateVerifyConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export function Verify() {
const t = useTranslations('auth-control.verify');
const { data, refetch } = useQuery({
queryKey: ['getVerifyConfig'],
queryFn: async () => {
const { data } = await getVerifyConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateVerifyConfig({
...data,
[key]: value,
} as API.VerifyConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Card className='mb-6'>
<CardHeader>
<CardTitle>{t('verifySettings')}</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>Turnstile Site Key</Label>
<p className='text-muted-foreground text-xs'>{t('turnstileSiteKeyDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.turnstile_site_key}
onValueBlur={(value) => updateConfig('turnstile_site_key', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>Turnstile Site Secret</Label>
<p className='text-muted-foreground text-xs'>{t('turnstileSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.turnstile_secret}
onValueBlur={(value) => updateConfig('turnstile_secret', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('registrationVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('registrationVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_register_verify}
onCheckedChange={(checked) => {
updateConfig('enable_register_verify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('loginVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('loginVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_login_verify}
onCheckedChange={(checked) => {
updateConfig('enable_login_verify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('resetPasswordVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('resetPasswordVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_reset_password_verify}
onCheckedChange={(checked) => {
updateConfig('enable_reset_password_verify', checked);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
);
}
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system'; import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
@@ -13,21 +13,21 @@ export default function Page() {
const t = useTranslations('github'); const t = useTranslations('github');
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'github'], queryKey: ['getAuthMethodConfig', 'github'],
queryFn: async () => { queryFn: async () => {
const { data } = await getOAuthByPlatform({ const { data } = await getAuthMethodConfig({
platform: 'github', method: 'github',
}); });
return data.data; return data.data;
}, },
}); });
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) { async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try { try {
await updateOAuthConfig({ await updateAuthMethodConfig({
...data, ...data,
[key]: value, [key]: value,
} as API.UpdateOAuthConfig); } as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess')); toast.success(t('saveSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
@@ -85,19 +85,6 @@ export default function Page() {
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/github'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody> </TableBody>
</Table> </Table>
); );
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system'; import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
@@ -13,21 +13,21 @@ export default function Page() {
const t = useTranslations('google'); const t = useTranslations('google');
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'google'], queryKey: ['getAuthMethodConfig', 'google'],
queryFn: async () => { queryFn: async () => {
const { data } = await getOAuthByPlatform({ const { data } = await getAuthMethodConfig({
platform: 'google', method: 'google',
}); });
return data.data; return data.data;
}, },
}); });
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) { async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try { try {
await updateOAuthConfig({ await updateAuthMethodConfig({
...data, ...data,
[key]: value, [key]: value,
} as API.UpdateOAuthConfig); } as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess')); toast.success(t('saveSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
@@ -86,19 +86,6 @@ export default function Page() {
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/google'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody> </TableBody>
</Table> </Table>
); );
@@ -0,0 +1,108 @@
import { ProTable, ProTableActions } from '@/components/pro-table';
import { getMessageLogList } from '@/services/admin/log';
import { Badge } from '@workspace/ui/components/badge';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useRef } from 'react';
export function LogsTable({ type }: { type: 'email' | 'mobile' }) {
const t = useTranslations('auth-control.log');
const ref = useRef<ProTableActions>(null);
return (
<ProTable<
API.MessageLog,
{
platform?: string;
to?: string;
subject?: string;
content?: string;
status?: number;
}
>
action={ref}
header={{
title: t(`${type}Log`),
}}
columns={[
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'platform',
header: t('platform'),
},
{
accessorKey: 'to',
header: t('to'),
},
{
accessorKey: 'subject',
header: t('subject'),
},
{
accessorKey: 'content',
header: t('content'),
},
{
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => {
const status = row.getValue('status');
const text = status === 1 ? t('sendSuccess') : t('sendFailed');
return <Badge variant={status === 1 ? 'default' : 'destructive'}>{text}</Badge>;
},
},
{
accessorKey: 'created_at',
header: t('createdAt'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
{
accessorKey: 'updated_at',
header: t('updatedAt'),
cell: ({ row }) => formatDate(row.getValue('updated_at')),
},
]}
params={[
// {
// key: 'platform',
// placeholder: t('platform'),
// },
{
key: 'to',
placeholder: t('to'),
},
{
key: 'subject',
placeholder: t('subject'),
},
{
key: 'content',
placeholder: t('content'),
},
{
key: 'status',
placeholder: t('status'),
options: [
{ label: t('sendSuccess'), value: '1' },
{ label: t('sendFailed'), value: '0' },
],
},
]}
request={async (pagination, filter) => {
const { data } = await getMessageLogList({
...pagination,
...filter,
status: filter.status === undefined ? undefined : Number(filter.status),
type: type,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
);
}
@@ -1,15 +1,12 @@
'use client'; 'use client';
import { ProTable, ProTableActions } from '@/components/pro-table';
import { getSmsList } from '@/services/admin/sms';
import { import {
getSmsConfig, getAuthMethodConfig,
getSmsPlatform, getSmsPlatform,
testSmsSend, testSmsSend,
updateSmsConfig, updateAuthMethodConfig,
} from '@/services/admin/system'; } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { import {
@@ -25,18 +22,21 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/componen
import { Textarea } from '@workspace/ui/components/textarea'; import { Textarea } from '@workspace/ui/components/textarea';
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select'; import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { formatDate } from '@workspace/ui/utils'; import TagInput from '@workspace/ui/custom-components/tag-input';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link'; import Link from 'next/link';
import { useRef, useState } from 'react'; import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { LogsTable } from '../log';
export default function Page() { export default function Page() {
const t = useTranslations('phone'); const t = useTranslations('phone');
const { data, refetch, isFetching } = useQuery({ const { data, refetch, isFetching } = useQuery({
queryKey: ['getSmsConfig'], queryKey: ['getAuthMethodConfig', 'mobile'],
queryFn: async () => { queryFn: async () => {
const { data } = await getSmsConfig(); const { data } = await getAuthMethodConfig({
method: 'mobile',
});
return data.data; return data.data;
}, },
}); });
@@ -49,25 +49,27 @@ export default function Page() {
}, },
}); });
const selectedPlatform = platforms?.find((platform) => platform.platform === data?.sms_platform); const selectedPlatform = platforms?.find(
(platform) => platform.platform === data?.config?.platform,
);
const { platform_url, platform_field_description: platformConfig } = selectedPlatform ?? {}; const { platform_url, platform_field_description: platformConfig } = selectedPlatform ?? {};
async function updateConfig(key: string, value: unknown) { async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return; if (data?.[key] === value) return;
try { try {
await updateSmsConfig({ await updateAuthMethodConfig({
...data, ...data,
[key]: value, [key]: value,
} as API.SmsConfig); } as API.UpdateAuthMethodConfigRequest);
toast.success(t('updateSuccess')); toast.success(t('updateSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
/* empty */ /* empty */
} }
} }
const [params, setParams] = useState<API.SendSmsRequest>({ const [params, setParams] = useState<API.TestSmsSendRequest>({
telephone: '', telephone: '',
content: t('testSmsContent'),
area_code: '1', area_code: '1',
}); });
@@ -88,59 +90,38 @@ export default function Page() {
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
<Switch <Switch
checked={data?.sms_enabled} checked={data?.enabled}
onCheckedChange={(checked) => updateConfig('sms_enabled', checked)} onCheckedChange={(checked) => updateConfig('enabled', checked)}
disabled={isFetching} disabled={isFetching}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('expireTime')}</Label> <Label>{t('whitelistValidation')}</Label>
<p className='text-muted-foreground text-xs'>{t('expireTimeTip')}</p> <p className='text-muted-foreground text-xs'>{t('whitelistValidationTip')}</p>
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
<EnhancedInput <Switch
type='number' defaultValue={data?.config?.enable_whitelist}
min={0} onCheckedChange={(checked) =>
value={data?.sms_expire_time ?? 300} updateConfig('config', { ...data?.config, enable_whitelist: checked })
onValueBlur={(value) => updateConfig('sms_expire_time', value)} }
suffix='S'
disabled={isFetching}
placeholder={t('placeholders.expireTime')}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('interval')}</Label>
<p className='text-muted-foreground text-xs'>{t('intervalTip')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
type='number'
value={data?.sms_interval ?? 60}
min={0}
onValueBlur={(value) => updateConfig('sms_interval', value)}
suffix='S'
disabled={isFetching}
placeholder={t('placeholders.interval')}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('limit')}</Label> <Label>{t('whitelistAreaCode')}</Label>
<p className='text-muted-foreground text-xs'>{t('limitTip')}</p> <p className='text-muted-foreground text-xs'>{t('whitelistAreaCodeTip')}</p>
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='w-1/2 text-right'>
<EnhancedInput <TagInput
type='number' placeholder='1, 852, 886, 888'
value={data?.sms_limit ?? 20} value={data?.config?.whitelist || []}
min={0} onChange={(value) =>
onValueBlur={(value) => updateConfig('sms_limit', value)} updateConfig('config', { ...data?.config, whitelist: value })
disabled={isFetching} }
placeholder={t('placeholders.limit')}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
@@ -151,8 +132,13 @@ export default function Page() {
</TableCell> </TableCell>
<TableCell className='flex items-center gap-1 text-right'> <TableCell className='flex items-center gap-1 text-right'>
<Select <Select
value={data?.sms_platform} value={data?.config?.platform}
onValueChange={(value) => updateConfig('sms_platform', value)} onValueChange={(value) =>
updateConfig('config', {
...data?.config,
platform: value,
})
}
disabled={isFetching} disabled={isFetching}
> >
<SelectTrigger> <SelectTrigger>
@@ -177,91 +163,189 @@ export default function Page() {
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>Key</Label> <Label>{t('accessLabel')}</Label>
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_key })} {t('platformConfigTip', { key: platformConfig?.access })}
</p> </p>
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
<EnhancedInput <EnhancedInput
value={data?.sms_key ?? ''} value={data?.config?.platform_config.access ?? ''}
onValueBlur={(value) => updateConfig('sms_key', value)} onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
access: value,
},
})
}
disabled={isFetching} disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.sms_key })} placeholder={t('platformConfigTip', { key: platformConfig?.access })}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow> {platformConfig?.endpoint && (
<TableCell>
<Label>Secret</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_secret })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.sms_secret ?? ''}
onValueBlur={(value) => updateConfig('sms_secret', value)}
disabled={isFetching}
type='password'
placeholder={t('platformConfigTip', { key: platformConfig?.sms_secret })}
/>
</TableCell>
</TableRow>
{platformConfig?.sms_template_code && (
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('templateCode')}</Label> <Label>{t('endpointLabel')}</Label>
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_template_code })} {t('platformConfigTip', { key: platformConfig?.endpoint })}
</p> </p>
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
<EnhancedInput <EnhancedInput
value={data?.sms_template_code ?? ''} value={data?.config?.platform_config.endpoint ?? ''}
onValueBlur={(value) => updateConfig('sms_template_code', value)} onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
endpoint: value,
},
})
}
disabled={isFetching} disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.sms_template_code })} placeholder={t('platformConfigTip', { key: platformConfig?.endpoint })}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
{platformConfig?.sms_template_param && ( <TableRow>
<TableCell>
<Label>{t('secretLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.secret })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config?.secret ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
secret: value,
},
})
}
disabled={isFetching}
type='password'
placeholder={t('platformConfigTip', { key: platformConfig?.secret })}
/>
</TableCell>
</TableRow>
{platformConfig?.template_code && (
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('templateParam')}</Label> <Label>{t('templateCodeLabel')}</Label>
<p className='text-muted-foreground text-xs'> <p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sms_template_param })} {t('platformConfigTip', { key: platformConfig?.template_code })}
</p> </p>
</TableCell> </TableCell>
<TableCell className='text-right'> <TableCell className='text-right'>
<EnhancedInput <EnhancedInput
value={data?.sms_template_param ?? 'code'} value={data?.config?.platform_config?.template_code ?? 'code'}
onValueBlur={(value) => updateConfig('sms_template_param', value)} onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
template_code: value,
},
})
}
disabled={isFetching}
placeholder={t('platformConfigTip', { key: platformConfig?.template_code })}
/>
</TableCell>
</TableRow>
)}
{platformConfig?.sign_name && (
<TableRow>
<TableCell>
<Label>{t('signNameLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.sign_name })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config?.sign_name ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
sign_name: value,
},
})
}
disabled={isFetching} disabled={isFetching}
placeholder={t('platformConfigTip', { placeholder={t('platformConfigTip', {
key: platformConfig?.sms_template_param, key: platformConfig?.sign_name,
})}
/>
</TableCell>
</TableRow>
)}
{platformConfig?.phone_number && (
<TableRow>
<TableCell>
<Label>{t('phoneNumberLabel')}</Label>
<p className='text-muted-foreground text-xs'>
{t('platformConfigTip', { key: platformConfig?.phone_number })}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
value={data?.config?.platform_config?.phone_number ?? ''}
onValueBlur={(value) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
phone_number: value,
},
})
}
disabled={isFetching}
placeholder={t('platformConfigTip', {
key: platformConfig?.phone_number,
})}
/>
</TableCell>
</TableRow>
)}
{platformConfig?.code_variable && (
<TableRow>
<TableCell>
<Label>{t('template')}</Label>
<p className='text-muted-foreground text-xs'>
{t('templateTip', { code: platformConfig?.code_variable })}
</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
defaultValue={data?.config?.platform_config?.template ?? ''}
onBlur={(e) =>
updateConfig('config', {
...data?.config,
platform_config: {
...data?.config?.platform_config,
template: e.target.value,
},
})
}
disabled={isFetching}
placeholder={t('placeholders.template', {
code: platformConfig?.code_variable,
})} })}
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
<TableRow>
<TableCell>
<Label>{t('template')}</Label>
<p className='text-muted-foreground text-xs'>
{t('templateTip', { code: platformConfig?.sms_template })}
</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
defaultValue={data?.sms_template ?? ''}
onBlur={(e) => updateConfig('sms_template', e.target.value)}
disabled={isFetching}
placeholder={t('placeholders.template', { code: platformConfig?.sms_template })}
/>
</TableCell>
</TableRow>
<TableRow> <TableRow>
<TableCell> <TableCell>
<Label>{t('testSms')}</Label> <Label>{t('testSms')}</Label>
@@ -304,67 +388,8 @@ export default function Page() {
</TabsContent> </TabsContent>
<TabsContent value='logs'> <TabsContent value='logs'>
<LogsTable /> <LogsTable type='mobile' />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
); );
} }
function LogsTable() {
const t = useTranslations('phone');
const ref = useRef<ProTableActions>(null);
return (
<ProTable<API.Sms, { telephone: string }>
action={ref}
header={{
title: t('SmsList'),
}}
columns={[
{
accessorKey: 'platform',
header: t('platform'),
},
{
accessorKey: 'areaCode',
header: t('areaCode'),
},
{
accessorKey: 'telephone',
header: t('telephone'),
},
{
accessorKey: 'content',
header: t('content'),
},
{
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => {
const status = row.getValue('status');
const text = status === 1 ? t('sendSuccess') : t('sendFailed');
return <Badge variant={status === 1 ? 'default' : 'destructive'}>{text}</Badge>;
},
},
{
accessorKey: 'created_at',
header: t('createdAt'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
params={[
{
key: 'telephone',
placeholder: t('search'),
},
]}
request={async (pagination, filter) => {
const { data } = await getSmsList({ ...pagination, ...filter });
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
);
}
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { getOAuthByPlatform, updateOAuthConfig } from '@/services/admin/system'; import { getAuthMethodConfig, updateAuthMethodConfig } from '@/services/admin/authMethod';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label'; import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
@@ -13,21 +13,21 @@ export default function Page() {
const t = useTranslations('telegram'); const t = useTranslations('telegram');
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ['getOAuthByPlatform', 'telegram'], queryKey: ['getAuthMethodConfig', 'telegram'],
queryFn: async () => { queryFn: async () => {
const { data } = await getOAuthByPlatform({ const { data } = await getAuthMethodConfig({
platform: 'telegram', method: 'telegram',
}); });
return data.data; return data.data;
}, },
}); });
async function updateConfig(key: keyof API.UpdateOAuthConfig, value: unknown) { async function updateConfig(key: keyof API.UpdateAuthMethodConfigRequest, value: unknown) {
try { try {
await updateOAuthConfig({ await updateAuthMethodConfig({
...data, ...data,
[key]: value, [key]: value,
} as API.UpdateOAuthConfig); } as API.UpdateAuthMethodConfigRequest);
toast.success(t('saveSuccess')); toast.success(t('saveSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
@@ -86,19 +86,6 @@ export default function Page() {
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<TableCell>
<Label>{t('redirectUri')}</Label>
<p className='text-muted-foreground text-xs'>{t('redirectUriDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder='https://your-domain.com/v1/auth/oauth/callback/telegram'
value={data?.redirect}
onValueBlur={(value) => updateConfig('redirect', value)}
/>
</TableCell>
</TableRow>
</TableBody> </TableBody>
</Table> </Table>
); );
+14 -5
View File
@@ -17,7 +17,7 @@ import { cn } from '@workspace/ui/lib/utils';
import { formatDate } from '@workspace/ui/utils'; import { formatDate } from '@workspace/ui/utils';
import { UserDetail } from '../user/user-detail'; import { UserDetail } from '../user/user-detail';
export default function Page() { export default function Page(props: any) {
const t = useTranslations('order'); const t = useTranslations('order');
const statusOptions = [ const statusOptions = [
@@ -115,7 +115,7 @@ export default function Page() {
<Separator className='my-4' /> <Separator className='my-4' />
<ul className='grid gap-3'> <ul className='grid gap-3'>
<li className='flex items-center justify-between'> <li className='flex items-center justify-between'>
<span className='text-muted-foreground'></span> <span className='text-muted-foreground'>{t('method')}</span>
<span>{t(`methods.${row.original.method}`)}</span> <span>{t(`methods.${row.original.method}`)}</span>
</li> </li>
</ul> </ul>
@@ -177,10 +177,19 @@ export default function Page() {
value: String(item.id), value: String(item.id),
})), })),
}, },
{ key: 'user_id', placeholder: `${t('user')}ID` }, ].concat(
]} props.userId
? []
: [
{
key: 'user_id',
placeholder: `${t('user')} ID`,
options: undefined,
},
],
)}
request={async (pagination, filter) => { request={async (pagination, filter) => {
const { data } = await getOrderList({ ...pagination, ...filter }); const { data } = await getOrderList({ ...pagination, ...filter, user_id: props.userId });
return { return {
list: data.data?.list || [], list: data.data?.list || [],
total: data.data?.total || 0, total: data.data?.total || 0,
@@ -35,7 +35,7 @@ function getTimeRangeData(slots: API.TimePeriod[]) {
.filter((slot) => slot.start_time && slot.end_time) .filter((slot) => slot.start_time && slot.end_time)
.flatMap((slot) => { .flatMap((slot) => {
const [startH = 0, startM = 0] = slot.start_time.split(':').map(Number); const [startH = 0, startM = 0] = slot.start_time.split(':').map(Number);
const [endH = 0, endM = 0] = slot?.end_time?.split(':').map(Number); const [endH = 0, endM = 0] = slot?.end_time.split(':').map(Number);
const start = startH * 60 + startM; const start = startH * 60 + startM;
let end = endH * 60 + endM; let end = endH * 60 + endM;
if (end < start) end += MINUTES_IN_DAY; if (end < start) end += MINUTES_IN_DAY;
@@ -117,7 +117,7 @@ export default function NodeConfig() {
}, [timeSlots]); }, [timeSlots]);
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
return chartTimeSlots.reduce( return chartTimeSlots?.reduce(
(acc, item, index) => { (acc, item, index) => {
acc[item.name] = { acc[item.name] = {
label: item.name, label: item.name,
@@ -50,7 +50,7 @@ const defaultValues = {
const versionSchema = z.object({ const versionSchema = z.object({
url: z.string(), url: z.string(),
version: z.string(), version: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
is_default: z.boolean().optional(), is_default: z.boolean().optional(),
}); });
@@ -221,6 +221,7 @@ export default function SubscribeAppForm<
name: 'version', name: 'version',
type: 'text', type: 'text',
placeholder: t('version'), placeholder: t('version'),
defaultValue: '1.0.0',
}, },
{ {
name: 'description', name: 'description',
@@ -251,7 +252,10 @@ export default function SubscribeAppForm<
]} ]}
value={field.value} value={field.value}
onChange={(value) => { onChange={(value) => {
form.setValue(field.name, value); form.setValue(
field.name,
value.filter((item) => item.url),
);
}} }}
/> />
</FormControl> </FormControl>
@@ -30,7 +30,7 @@ export default function SubscribeConfig() {
...data, ...data,
[key]: value, [key]: value,
} as API.SubscribeConfig); } as API.SubscribeConfig);
toast.success(t('saveSuccess')); toast.success(t('updateSuccess'));
refetch(); refetch();
} catch (error) { } catch (error) {
/* empty */ /* empty */
@@ -92,7 +92,7 @@ export default function SubscribeConfig() {
<TableCell className='text-right'> <TableCell className='text-right'>
<Textarea <Textarea
className='h-52' className='h-52'
placeholder={t('subscriptionDomainPlaceholder')} placeholder={`${t('subscriptionDomainPlaceholder')}\nexample.com\nwww.example.com`}
defaultValue={data?.subscribe_domain} defaultValue={data?.subscribe_domain}
onBlur={(e) => { onBlur={(e) => {
updateConfig('subscribe_domain', e.target.value); updateConfig('subscribe_domain', e.target.value);
@@ -105,7 +105,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
server_group: z.array(z.number()).optional().default([]), server_group: z.array(z.number()).optional().default([]),
server: z.array(z.number()).optional().default([]), server: z.array(z.number()).optional().default([]),
deduction_ratio: z.number().optional().default(0), deduction_ratio: z.number().optional().default(0),
purchase_with_discount: z.boolean().optional().default(false), allow_deduction: z.boolean().optional().default(false),
reset_cycle: z.number().optional().default(0), reset_cycle: z.number().optional().default(0),
renewal_reset: z.boolean().optional().default(false), renewal_reset: z.boolean().optional().default(false),
}); });
@@ -639,7 +639,7 @@ export default function SubscribeForm<T extends Record<string, any>>({
/> />
<FormField <FormField
control={form.control} control={form.control}
name='purchase_with_discount' name='allow_deduction'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
@@ -1,93 +0,0 @@
'use client';
import { getInviteConfig, updateInviteConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Invite() {
const t = useTranslations('system.invite');
const { data, refetch } = useQuery({
queryKey: ['getInviteConfig'],
queryFn: async () => {
const { data } = await getInviteConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateInviteConfig({
...data,
[key]: value,
} as API.InviteConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('enableForcedInvite')}</Label>
<p className='text-muted-foreground text-xs'>{t('enableForcedInviteDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.forced_invite}
onCheckedChange={(checked) => {
updateConfig('forced_invite', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('inviteCommissionPercentage')}</Label>
<p className='text-muted-foreground text-xs'>
{t('inviteCommissionPercentageDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
value={data?.referral_percentage}
type='number'
min={0}
max={100}
suffix='%'
onValueBlur={(value) => updateConfig('referral_percentage', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('commissionFirstTimeOnly')}</Label>
<p className='text-muted-foreground text-xs'>
{t('commissionFirstTimeOnlyDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.only_first_purchase}
onCheckedChange={(checked) => {
updateConfig('only_first_purchase', checked);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
+5 -16
View File
@@ -1,12 +1,9 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import Currency from './currency'; import Currency from './currency';
import Invite from './invite'; import PrivacyPolicy from './privacy-policy';
import Site from './site'; import Site from './site';
import Telegram from './telegram';
import Tos from './tos'; import Tos from './tos';
import Verify from './verify';
export default async function Page() { export default async function Page() {
const t = await getTranslations('system'); const t = await getTranslations('system');
@@ -16,10 +13,8 @@ export default async function Page() {
<TabsList className='h-full flex-wrap'> <TabsList className='h-full flex-wrap'>
<TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger> <TabsTrigger value='site'>{t('tabs.site')}</TabsTrigger>
<TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger> <TabsTrigger value='currency'>{t('tabs.currency')}</TabsTrigger>
<TabsTrigger value='verify'>{t('tabs.verify')}</TabsTrigger>
<TabsTrigger value='invite'>{t('tabs.invite')}</TabsTrigger>
<TabsTrigger value='telegram'>{t('tabs.telegram')}</TabsTrigger>
<TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger> <TabsTrigger value='tos'>{t('tabs.tos')}</TabsTrigger>
<TabsTrigger value='privacy-policy'>{t('privacy-policy.title')}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value='site'> <TabsContent value='site'>
<Site /> <Site />
@@ -27,18 +22,12 @@ export default async function Page() {
<TabsContent value='currency'> <TabsContent value='currency'>
<Currency /> <Currency />
</TabsContent> </TabsContent>
<TabsContent value='verify'>
<Verify />
</TabsContent>
<TabsContent value='invite'>
<Invite />
</TabsContent>
<TabsContent value='telegram'>
<Telegram />
</TabsContent>
<TabsContent value='tos'> <TabsContent value='tos'>
<Tos /> <Tos />
</TabsContent> </TabsContent>
<TabsContent value='privacy-policy'>
<PrivacyPolicy />
</TabsContent>
</Tabs> </Tabs>
); );
} }
@@ -0,0 +1,48 @@
'use client';
import { getPrivacyPolicyConfig, updatePrivacyPolicyConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { MarkdownEditor } from '@workspace/ui/custom-components/editor';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function PrivacyPolicy() {
const t = useTranslations('system.privacy-policy');
const { data, refetch, isFetched } = useQuery({
queryKey: ['getPrivacyPolicyConfig'],
queryFn: async () => {
const { data } = await getPrivacyPolicyConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updatePrivacyPolicyConfig({
...data,
[key]: value,
} as API.PrivacyPolicyConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
isFetched && (
<div className='h-[calc(100dvh-132px-env(safe-area-inset-top))] overflow-hidden'>
<MarkdownEditor
title={t('title')}
value={data?.privacy_policy}
onBlur={(value) => {
if (data?.privacy_policy !== value) {
updateConfig('privacy_policy', value);
}
}}
/>
</div>
)
);
}
+30 -1
View File
@@ -85,6 +85,35 @@ export default function Site() {
/> />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow>
<TableCell>
<Label>{t('keywords')}</Label>
<p className='text-muted-foreground text-xs'>{t('keywordsDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('keywordsDescription')}
value={data?.keywords}
onValueBlur={(value) => updateConfig('keywords', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='align-top'>
<Label>{t('customHtml')}</Label>
<p className='text-muted-foreground text-xs'>{t('customHtmlDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Textarea
className='h-52'
placeholder={t('customHtmlDescription')}
defaultValue={data?.custom_html}
onBlur={(e) => {
updateConfig('custom_html', e.target.value);
}}
/>
</TableCell>
</TableRow>
<TableRow> <TableRow>
<TableCell className='align-top'> <TableCell className='align-top'>
<Label>{t('siteDomain')}</Label> <Label>{t('siteDomain')}</Label>
@@ -93,7 +122,7 @@ export default function Site() {
<TableCell className='text-right'> <TableCell className='text-right'>
<Textarea <Textarea
className='h-52' className='h-52'
placeholder={t('siteDomainPlaceholder')} placeholder={`${t('siteDomainPlaceholder')}\nexample.com\nwww.example.com`}
defaultValue={data?.host} defaultValue={data?.host}
onBlur={(e) => { onBlur={(e) => {
updateConfig('host', e.target.value); updateConfig('host', e.target.value);
@@ -1,99 +0,0 @@
'use client';
import { getTelegramConfig, updateTelegramConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Telegram() {
const t = useTranslations('system.telegram');
const { data, refetch } = useQuery({
queryKey: ['getTelegramConfig'],
queryFn: async () => {
const { data } = await getTelegramConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateTelegramConfig({
...data,
[key]: value,
} as API.TelegramConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('webhookDomain')}</Label>
<p className='text-muted-foreground text-xs'>{t('webhookDomainDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholderWebhookDomain')}
value={data?.telegram_web_hook_domain}
onValueBlur={(value) => updateConfig('telegram_web_hook_domain', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('botToken')}</Label>
<p className='text-muted-foreground text-xs'>{t('botTokenDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholderBotToken')}
value={data?.telegram_bot_token}
onValueBlur={(value) => updateConfig('telegram_bot_token', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('enableBotNotifications')}</Label>
<p className='text-muted-foreground text-xs'>
{t('enableBotNotificationsDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.telegram_notify}
onCheckedChange={(checked) => {
updateConfig('telegram_notify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('groupURL')}</Label>
<p className='text-muted-foreground text-xs'>{t('groupURLDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholderGroupURL')}
value={data?.telegram_group_url}
onValueBlur={(value) => updateConfig('telegram_group_url', value)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
-115
View File
@@ -1,115 +0,0 @@
'use client';
import { getVerifyConfig, updateVerifyConfig } from '@/services/admin/system';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { Switch } from '@workspace/ui/components/switch';
import { Table, TableBody, TableCell, TableRow } from '@workspace/ui/components/table';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { toast } from 'sonner';
export default function Verify() {
const t = useTranslations('system.verify');
const { data, refetch } = useQuery({
queryKey: ['getVerifyConfig'],
queryFn: async () => {
const { data } = await getVerifyConfig();
return data.data;
},
});
async function updateConfig(key: string, value: unknown) {
if (data?.[key] === value) return;
try {
await updateVerifyConfig({
...data,
[key]: value,
} as API.VerifyConfig);
toast.success(t('saveSuccess'));
refetch();
} catch (error) {
/* empty */
}
}
return (
<Table>
<TableBody>
<TableRow>
<TableCell>
<Label>{t('turnstileSiteKey')}</Label>
<p className='text-muted-foreground text-xs'>{t('turnstileSiteKeyDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
defaultValue={data?.turnstile_site_key}
onValueBlur={(value) => updateConfig('turnstile_site_key', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('turnstileSecret')}</Label>
<p className='text-muted-foreground text-xs'>{t('turnstileSecretDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<EnhancedInput
placeholder={t('inputPlaceholder')}
defaultValue={data?.turnstile_secret}
onValueBlur={(value) => updateConfig('turnstile_secret', value)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('registrationVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('registrationVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_register_verify}
onCheckedChange={(checked) => {
updateConfig('enable_register_verify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('loginVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>{t('loginVerificationCodeDescription')}</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_login_verify}
onCheckedChange={(checked) => {
updateConfig('enable_login_verify', checked);
}}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>
<Label>{t('resetPasswordVerificationCode')}</Label>
<p className='text-muted-foreground text-xs'>
{t('resetPasswordVerificationCodeDescription')}
</p>
</TableCell>
<TableCell className='text-right'>
<Switch
checked={data?.enable_reset_password_verify}
onCheckedChange={(checked) => {
updateConfig('enable_reset_password_verify', checked);
}}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
);
}
@@ -0,0 +1,33 @@
import UserOrderList from '@/app/dashboard/order/page';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { getTranslations } from 'next-intl/server';
import UserLoginHistory from './user-login-history';
import { UserProfileForm } from './user-profile';
import UserSubscription from './user-subscription';
export default async function Page({ params }: { params: Promise<{ id: number }> }) {
const t = await getTranslations('user');
const { id } = await params;
return (
<Tabs defaultValue='profile'>
<TabsList>
<TabsTrigger value='profile'>{t('userProfile')}</TabsTrigger>
<TabsTrigger value='subscriptions'>{t('userSubscriptions')}</TabsTrigger>
<TabsTrigger value='orders'>{t('userOrders')}</TabsTrigger>
<TabsTrigger value='logs'>{t('userLogs')}</TabsTrigger>
</TabsList>
<TabsContent value='profile'>
<UserProfileForm />
</TabsContent>
<TabsContent value='subscriptions'>
<UserSubscription userId={id} />
</TabsContent>
<TabsContent value='orders'>
<UserOrderList userId={id} />
</TabsContent>
<TabsContent value='logs'>
<UserLoginHistory />
</TabsContent>
</Tabs>
);
}
@@ -0,0 +1,53 @@
'use client';
import { ProTable } from '@/components/pro-table';
import { getUserLoginLogs } from '@/services/admin/user';
import { Badge } from '@workspace/ui/components/badge';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
export default function UserLoginHistory() {
const t = useTranslations('user');
const { id } = useParams<{ id: string }>();
return (
<ProTable<API.UserLoginLog, Record<string, unknown>>
columns={[
{
accessorKey: 'success',
header: t('loginStatus'),
cell: ({ row }) => (
<Badge variant={row.getValue('success') ? 'default' : 'destructive'}>
{row.getValue('success') ? t('success') : t('failed')}
</Badge>
),
},
{
accessorKey: 'login_ip',
header: t('loginIp'),
},
{
accessorKey: 'user_agent',
header: t('userAgent'),
},
{
accessorKey: 'created_at',
header: t('loginTime'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
request={async (pagination, filter) => {
const { data } = await getUserLoginLogs({
user_id: Number(id),
...pagination,
...filter,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
);
}
@@ -0,0 +1,140 @@
'use client';
import {
createUserAuthMethod,
deleteUserAuthMethod,
updateUserAuthMethod,
} from '@/services/admin/user';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { toast } from 'sonner';
export function AuthMethodsForm({ user, refetch }: { user: API.User; refetch: () => void }) {
const t = useTranslations('user');
const [emailChanges, setEmailChanges] = useState<Record<string, string>>({});
const handleRemoveAuth = async (authType: string) => {
await deleteUserAuthMethod({
user_id: user.id,
auth_type: authType,
});
toast.success(t('deleteSuccess'));
};
const handleUpdateEmail = async (email: string) => {
await updateUserAuthMethod({
user_id: user.id,
auth_type: 'email',
auth_identifier: email,
});
toast.success(t('updateSuccess'));
refetch();
};
const handleCreateEmail = async (email: string) => {
await createUserAuthMethod({
user_id: user.id,
auth_type: 'email',
auth_identifier: email,
});
toast.success(t('createSuccess'));
refetch();
};
const handleEmailChange = (authType: string, value: string) => {
setEmailChanges((prev) => ({
...prev,
[authType]: value,
}));
};
const emailMethod = user.auth_methods.find((method) => method.auth_type === 'email');
const otherMethods = user.auth_methods.filter((method) => method.auth_type !== 'email');
const defaultEmailMethod = {
auth_type: 'email',
auth_identifier: '',
verified: false,
...emailMethod,
};
const isEmailExists = !!emailMethod;
const handleEmailAction = () => {
const email = emailChanges['email'];
if (isEmailExists) {
handleUpdateEmail(email as string);
} else {
handleCreateEmail(email as string);
}
};
return (
<Card>
<CardHeader>
<CardTitle>{t('authMethodsTitle')}</CardTitle>
</CardHeader>
<CardContent className='space-y-6'>
<div className='space-y-6'>
<Card className='border-none shadow-none'>
<CardContent className='space-y-3 p-0'>
<div className='flex items-center justify-between'>
<div className='font-medium uppercase'>email</div>
<Badge variant={defaultEmailMethod.verified ? 'default' : 'destructive'}>
{defaultEmailMethod.verified ? t('verified') : t('unverified')}
</Badge>
</div>
<div className='flex items-center gap-2'>
<div className='flex-1'>
<EnhancedInput
value={defaultEmailMethod.auth_identifier}
placeholder={t('pleaseEnterEmail')}
onValueChange={(value) => handleEmailChange('email', value as string)}
/>
</div>
<Button
onClick={handleEmailAction}
disabled={
!emailChanges['email'] ||
(isEmailExists && emailChanges['email'] === defaultEmailMethod.auth_identifier)
}
>
{isEmailExists ? t('update') : t('add')}
</Button>
</div>
</CardContent>
</Card>
{otherMethods.map((method) => (
<Card key={method.auth_type} className='border-none shadow-none'>
<CardContent className='space-y-3 p-0'>
<div className='flex items-center justify-between'>
<div className='font-medium uppercase'>{method.auth_type}</div>
<Badge variant={method.verified ? 'default' : 'destructive'}>
{method.verified ? t('verified') : t('unverified')}
</Badge>
</div>
<div className='flex items-center gap-4'>
<div className='flex-1'>
<div className='text-muted-foreground text-sm'>{method.auth_identifier}</div>
</div>
<Button
variant='destructive'
size='sm'
onClick={() => handleRemoveAuth(method.auth_type)}
>
{t('remove')}
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,259 @@
'use client';
import useGlobalStore from '@/config/use-global';
import { updateUserBasicInfo } from '@/services/admin/user';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@workspace/ui/components/form';
import { Input } from '@workspace/ui/components/input';
import { Switch } from '@workspace/ui/components/switch';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { UploadImage } from '@workspace/ui/custom-components/upload-image';
import { unitConversion } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import * as z from 'zod';
const basicInfoSchema = z.object({
avatar: z.string().optional(),
balance: z.number().optional(),
commission: z.number().optional(),
gift_amount: z.number().optional(),
refer_code: z.string().optional(),
referer_id: z.number().optional(),
is_admin: z.boolean().optional(),
password: z.string().optional(),
enable: z.boolean(),
});
type BasicInfoValues = z.infer<typeof basicInfoSchema>;
export function BasicInfoForm({ user, refetch }: { user: API.User; refetch: () => void }) {
const t = useTranslations('user');
const { common } = useGlobalStore();
const { currency } = common;
const form = useForm<BasicInfoValues>({
resolver: zodResolver(basicInfoSchema),
defaultValues: {
avatar: user.avatar,
balance: user.balance,
commission: user.commission,
gift_amount: user.gift_amount,
refer_code: user.refer_code,
referer_id: user.referer_id,
is_admin: user.is_admin,
enable: user.enable,
},
});
async function onSubmit(data: BasicInfoValues) {
await updateUserBasicInfo({
user_id: user.id,
telegram: user.telegram,
...data,
} as API.UpdateUserBasiceInfoRequest);
toast.success(t('updateSuccess'));
refetch();
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle>{t('basicInfoTitle')}</CardTitle>
<Button type='submit' size='sm'>
{t('save')}
</Button>
</CardHeader>
<CardContent className='space-y-4'>
<FormField
control={form.control}
name='enable'
render={({ field }) => (
<FormItem className='flex items-center justify-between space-x-2'>
<FormLabel>{t('accountEnable')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='is_admin'
render={({ field }) => (
<FormItem className='flex items-center justify-between space-x-2'>
<FormLabel>{t('administrator')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<div className='grid grid-cols-3 gap-4'>
<FormField
control={form.control}
name='balance'
render={({ field }) => (
<FormItem>
<FormLabel>{t('balance')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
value={field.value}
prefix={currency?.currency_symbol ?? '$'}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value as number);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='commission'
render={({ field }) => (
<FormItem>
<FormLabel>{t('commission')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
value={field.value}
prefix={currency?.currency_symbol ?? '$'}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value as number);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='gift_amount'
render={({ field }) => (
<FormItem>
<FormLabel>{t('giftAmount')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
value={field.value}
prefix={currency?.currency_symbol ?? '$'}
formatInput={(value) => unitConversion('centsToDollars', value)}
formatOutput={(value) => unitConversion('dollarsToCents', value)}
onValueChange={(value) => {
form.setValue(field.name, value as number);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className='grid grid-cols-2 gap-4'>
<FormField
control={form.control}
name='refer_code'
render={({ field }) => (
<FormItem>
<FormLabel>{t('referralCode')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value as string);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='referer_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('referrerUserId')}</FormLabel>
<FormControl>
<EnhancedInput
type='number'
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value as number);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name='avatar'
render={({ field }) => (
<FormItem>
<FormLabel>{t('avatar')}</FormLabel>
<FormControl>
<EnhancedInput
value={field.value}
onValueChange={(value) => {
form.setValue(field.name, value as string);
}}
suffix={
<UploadImage
className='bg-muted h-9 rounded-none border-none px-2'
returnType='base64'
onChange={(value) => form.setValue('avatar', value as string)}
/>
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='password'
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input type='password' placeholder={t('passwordPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
</form>
</Form>
);
}
@@ -0,0 +1,38 @@
'use client';
import { getUserDetail } from '@/services/admin/user';
import { useQuery } from '@tanstack/react-query';
import { useParams } from 'next/navigation';
import { AuthMethodsForm } from './auth-methods-form';
import { BasicInfoForm } from './basic-info-form';
import { NotifySettingsForm } from './notify-settings-form';
export function UserProfileForm() {
const { id } = useParams<{ id: string }>();
const { data: user, refetch } = useQuery({
queryKey: ['user', id],
queryFn: async () => {
const { data } = await getUserDetail({
id: Number(id),
});
return data.data;
},
});
if (!user) return null;
return (
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-3'>
<div className='md:col-span-2 xl:col-span-1'>
<BasicInfoForm user={user} refetch={refetch} />
</div>
<div>
<NotifySettingsForm user={user} refetch={refetch} />
</div>
<div>
<AuthMethodsForm user={user} refetch={refetch} />
</div>
</div>
);
}
@@ -0,0 +1,114 @@
'use client';
import { updateUserNotifySetting } from '@/services/admin/user';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Form, FormControl, FormField, FormItem, FormLabel } from '@workspace/ui/components/form';
import { Switch } from '@workspace/ui/components/switch';
import { useTranslations } from 'next-intl';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import * as z from 'zod';
const notifySettingsSchema = z.object({
enable_balance_notify: z.boolean(),
enable_login_notify: z.boolean(),
enable_subscribe_notify: z.boolean(),
enable_trade_notify: z.boolean(),
});
type NotifySettingsValues = z.infer<typeof notifySettingsSchema>;
export function NotifySettingsForm({ user, refetch }: { user: API.User; refetch: () => void }) {
const t = useTranslations('user');
const form = useForm<NotifySettingsValues>({
resolver: zodResolver(notifySettingsSchema),
defaultValues: {
enable_balance_notify: user.enable_balance_notify,
enable_login_notify: user.enable_login_notify,
enable_subscribe_notify: user.enable_subscribe_notify,
enable_trade_notify: user.enable_trade_notify,
},
});
async function onSubmit(data: NotifySettingsValues) {
await updateUserNotifySetting({
...data,
user_id: user.id,
});
toast.success(t('updateSuccess'));
refetch();
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardHeader className='flex flex-row items-center justify-between'>
<CardTitle>{t('notifySettingsTitle')}</CardTitle>
<Button type='submit' size='sm'>
{t('save')}
</Button>
</CardHeader>
<CardContent className='space-y-4'>
<div className='grid grid-cols-1 gap-4'>
<FormField
control={form.control}
name='enable_balance_notify'
render={({ field }) => (
<FormItem className='flex items-center justify-between space-x-2'>
<FormLabel>{t('balanceNotifications')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='enable_login_notify'
render={({ field }) => (
<FormItem className='flex items-center justify-between space-x-2'>
<FormLabel>{t('loginNotifications')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='enable_subscribe_notify'
render={({ field }) => (
<FormItem className='flex items-center justify-between space-x-2'>
<FormLabel>{t('subscriptionNotifications')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name='enable_trade_notify'
render={({ field }) => (
<FormItem className='flex items-center justify-between space-x-2'>
<FormLabel>{t('tradeNotifications')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
</form>
</Form>
);
}
@@ -0,0 +1,166 @@
'use client';
import { Display } from '@/components/display';
import { ProTable, ProTableActions } from '@/components/pro-table';
import {
createUserSubscribe,
deleteUserSubscribe,
getUserSubscribe,
updateUserSubscribe,
} from '@/services/admin/user';
import { Button } from '@workspace/ui/components/button';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import { toast } from 'sonner';
import { SubscriptionDetail } from './subscription-detail';
import { SubscriptionForm } from './subscription-form';
export default function UserSubscription({ userId }: { userId: number }) {
const t = useTranslations('user');
const [loading, setLoading] = useState(false);
const ref = useRef<ProTableActions>(null);
return (
<ProTable<API.UserSubscribe, Record<string, unknown>>
action={ref}
header={{
title: t('subscriptionList'),
toolbar: (
<SubscriptionForm
key='create'
trigger={t('add')}
title={t('createSubscription')}
loading={loading}
onSubmit={async (values) => {
setLoading(true);
await createUserSubscribe({
user_id: Number(userId),
...values,
});
toast.success(t('createSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
}}
/>
),
}}
columns={[
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'name',
header: t('subscriptionName'),
cell: ({ row }) => row.original.subscribe.name,
},
{
accessorKey: 'upload',
header: t('upload'),
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
},
{
accessorKey: 'download',
header: t('download'),
cell: ({ row }) => <Display type='traffic' value={row.getValue('download')} />,
},
{
accessorKey: 'traffic',
header: t('totalTraffic'),
cell: ({ row }) => <Display type='traffic' value={row.getValue('traffic')} unlimited />,
},
{
accessorKey: 'speed_limit',
header: t('speedLimit'),
cell: ({ row }) => {
const speed = row.original?.subscribe?.speed_limit;
return <Display type='trafficSpeed' value={speed} />;
},
},
{
accessorKey: 'device_limit',
header: t('deviceLimit'),
cell: ({ row }) => {
const limit = row.original?.subscribe?.device_limit;
return <Display type='number' value={limit} unlimited />;
},
},
{
accessorKey: 'reset_time',
header: t('resetTime'),
cell: ({ row }) => {
return <Display type='number' value={row.getValue('reset_time')} unlimited />;
},
},
{
accessorKey: 'expire_time',
header: t('expireTime'),
cell: ({ row }) =>
row.getValue('expire_time') ? formatDate(row.getValue('expire_time')) : t('permanent'),
},
{
accessorKey: 'created_at',
header: t('createdAt'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
request={async (pagination) => {
const { data } = await getUserSubscribe({
user_id: userId,
...pagination,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
actions={{
render: (row) => {
return [
<SubscriptionForm
key='edit'
trigger={t('edit')}
title={t('editSubscription')}
loading={loading}
initialData={row}
onSubmit={async (values) => {
setLoading(true);
await updateUserSubscribe({
user_id: Number(userId),
user_subscribe_id: row.id,
...values,
});
toast.success(t('updateSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
}}
/>,
<SubscriptionDetail
key='detail'
trigger={<Button variant='secondary'>{t('detail')}</Button>}
userId={userId}
subscriptionId={row.id}
/>,
<ConfirmButton
key='delete'
trigger={<Button variant='destructive'>{t('delete')}</Button>}
title={t('confirmDelete')}
description={t('deleteSubscriptionDescription')}
onConfirm={async () => {
await deleteUserSubscribe({ user_subscribe_id: row.id });
toast.success(t('deleteSuccess'));
ref.current?.refresh();
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
];
},
}}
/>
);
}
@@ -0,0 +1,209 @@
'use client';
import { Display } from '@/components/display';
import { ProTable } from '@/components/pro-table';
import {
getUserSubscribeDevices,
getUserSubscribeLogs,
getUserSubscribeTrafficLogs,
kickOfflineByUserDevice,
} from '@/services/admin/user';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@workspace/ui/components/dialog';
import { Switch } from '@workspace/ui/components/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { ReactNode, useState } from 'react';
import { toast } from 'sonner';
export function SubscriptionDetail({
trigger,
userId,
subscriptionId,
}: {
trigger: ReactNode;
userId: number;
subscriptionId: number;
}) {
const t = useTranslations('user');
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className='max-w-5xl'>
<DialogHeader>
<DialogTitle>{t('subscriptionDetails')}</DialogTitle>
</DialogHeader>
<div className='mt-4'>
<Tabs defaultValue='logs'>
<TabsList className='w-full'>
<TabsTrigger value='logs' className='flex-1'>
{t('subscriptionLogs')}
</TabsTrigger>
<TabsTrigger value='traffic' className='flex-1'>
{t('trafficLogs')}
</TabsTrigger>
<TabsTrigger value='devices' className='flex-1'>
{t('onlineDevices')}
</TabsTrigger>
</TabsList>
<TabsContent value='logs'>
<ProTable<API.UserSubscribeLog, Record<string, unknown>>
columns={[
{
accessorKey: 'ip',
header: 'IP',
},
{
accessorKey: 'user_agent',
header: t('userAgent'),
},
{
accessorKey: 'token',
header: t('token'),
},
{
accessorKey: 'created_at',
header: t('time'),
cell: ({ row }) => formatDate(row.getValue('created_at')),
},
]}
request={async (pagination) => {
const { data } = await getUserSubscribeLogs({
user_id: userId,
subscribe_id: subscriptionId,
...pagination,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
</TabsContent>
<TabsContent value='traffic'>
<ProTable<API.TrafficLog, Record<string, unknown>>
columns={[
{
accessorKey: 'download',
header: t('download'),
cell: ({ row }) => <Display type='traffic' value={row.getValue('download')} />,
},
{
accessorKey: 'upload',
header: t('upload'),
cell: ({ row }) => <Display type='traffic' value={row.getValue('upload')} />,
},
{
accessorKey: 'timestamp',
header: t('time'),
cell: ({ row }) => formatDate(row.getValue('timestamp')),
},
]}
request={async (pagination) => {
const { data } = await getUserSubscribeTrafficLogs({
user_id: userId,
subscribe_id: subscriptionId,
...pagination,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
</TabsContent>
<TabsContent value='devices'>
<ProTable<API.UserDevice, Record<string, unknown>>
columns={[
{
accessorKey: 'enabled',
header: t('enable'),
cell: ({ row }) => (
<Switch
checked={row.getValue('enabled')}
onChange={(checked) => {
console.log('Switch:', checked);
}}
/>
),
},
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'identifier',
header: 'IMEI',
},
{
accessorKey: 'user_agent',
header: t('userAgent'),
},
{
accessorKey: 'ip',
header: 'IP',
},
{
accessorKey: 'online',
header: t('loginStatus'),
cell: ({ row }) => (
<Badge variant={row.getValue('online') ? 'default' : 'destructive'}>
{row.getValue('online') ? t('online') : t('offline')}
</Badge>
),
},
{
accessorKey: 'updated_at',
header: t('lastSeen'),
cell: ({ row }) => formatDate(row.getValue('updated_at')),
},
]}
request={async (pagination) => {
const { data } = await getUserSubscribeDevices({
user_id: userId,
subscribe_id: subscriptionId,
...pagination,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
actions={{
render: (row) => {
if (!row.identifier) return [];
return [
<ConfirmButton
key='offline'
trigger={<Button variant='destructive'>{t('confirmOffline')}</Button>}
title={t('confirmOffline')}
description={t('kickOfflineConfirm', { ip: row.ip })}
onConfirm={async () => {
await kickOfflineByUserDevice({ id: row.id });
toast.success(t('kickOfflineSuccess'));
}}
cancelText={t('cancel')}
confirmText={t('confirm')}
/>,
];
},
}}
/>
</TabsContent>
</Tabs>
</div>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,240 @@
'use client';
import { getSubscribeList } from '@/services/admin/subscribe';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@workspace/ui/components/form';
import { ScrollArea } from '@workspace/ui/components/scroll-area';
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@workspace/ui/components/sheet';
import { Combobox } from '@workspace/ui/custom-components/combobox';
import { DatePicker } from '@workspace/ui/custom-components/date-picker';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { unitConversion } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { ReactNode, useState } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
interface Props {
trigger: ReactNode;
title: string;
loading?: boolean;
initialData?: API.UserSubscribe;
onSubmit: (values: any) => Promise<boolean>;
}
const formSchema = z.object({
subscribe_id: z.number().optional(),
traffic: z.number().optional(),
speed_limit: z.number().optional(),
device_limit: z.number().optional(),
expired_at: z.number().nullish().optional(),
});
export function SubscriptionForm({ trigger, title, loading, initialData, onSubmit }: Props) {
const t = useTranslations('user');
const [open, setOpen] = useState(false);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
subscribe_id: initialData?.subscribe_id || 0,
traffic: initialData?.traffic || 0,
upload: initialData?.upload || 0,
download: initialData?.download || 0,
expired_at: initialData?.expire_time || 0,
...(initialData && { id: initialData.id }),
},
});
const handleSubmit = async (values: any) => {
const success = await onSubmit(values);
if (success) {
setOpen(false);
form.reset();
}
};
const { data: subscribe } = useQuery({
queryKey: ['getSubscribeList', 'all'],
queryFn: async () => {
const { data } = await getSubscribeList({
page: 1,
size: 9999,
});
return data.data?.list as API.Subscribe[];
},
});
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button
onClick={() => {
form.reset();
setOpen(true);
}}
>
{trigger}
</Button>
</SheetTrigger>
<SheetContent side='right'>
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className='h-[calc(100dvh-48px-36px-36px-env(safe-area-inset-top))]'>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)} className='mt-4 space-y-4'>
<FormField
control={form.control}
name='subscribe_id'
render={({ field }) => (
<FormItem>
<FormLabel>{t('subscription')}</FormLabel>
<FormControl>
<Combobox<number, false>
placeholder='Select Subscription'
value={field.value}
onChange={(value) => {
form.setValue(field.name, value);
}}
options={subscribe?.map((item: API.Subscribe) => ({
value: item.id,
label: item.name,
}))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='traffic'
render={({ field }) => (
<FormItem>
<FormLabel>{t('trafficLimit')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t('unlimited')}
type='number'
{...field}
formatInput={(value) => unitConversion('bytesToGb', value)}
formatOutput={(value) => unitConversion('gbToBytes', value)}
suffix='GB'
onValueChange={(value) => {
form.setValue(field.name, value as number);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='upload'
render={({ field }) => (
<FormItem>
<FormLabel>{t('uploadTraffic')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='0'
type='number'
{...field}
formatInput={(value) => unitConversion('bytesToGb', value)}
formatOutput={(value) => unitConversion('gbToBytes', value)}
suffix='GB'
onValueChange={(value) => {
form.setValue(field.name, value as number);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='download'
render={({ field }) => (
<FormItem>
<FormLabel>{t('downloadTraffic')}</FormLabel>
<FormControl>
<EnhancedInput
placeholder='0'
type='number'
{...field}
formatInput={(value) => unitConversion('bytesToGb', value)}
formatOutput={(value) => unitConversion('gbToBytes', value)}
suffix='GB'
onValueChange={(value) => {
form.setValue(field.name, value as number);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='expired_at'
render={({ field }) => (
<FormItem>
<FormLabel>{t('expiredAt')}</FormLabel>
<FormControl>
<DatePicker
placeholder={t('permanent')}
value={field.value}
onChange={(value) => {
if (value === field.value) {
form.setValue(field.name, 0);
} else {
form.setValue(field.name, value!);
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button
variant='outline'
disabled={loading}
onClick={() => {
setOpen(false);
}}
>
{t('cancel')}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}
{t('confirm')}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
+19 -29
View File
@@ -2,12 +2,14 @@
import { Display } from '@/components/display'; import { Display } from '@/components/display';
import { ProTable, ProTableActions } from '@/components/pro-table'; import { ProTable, ProTableActions } from '@/components/pro-table';
import { createUser, deleteUser, getUserList, updateUser } from '@/services/admin/user'; import { createUser, deleteUser, getUserList, updateUserBasicInfo } from '@/services/admin/user';
import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Switch } from '@workspace/ui/components/switch'; import { Switch } from '@workspace/ui/components/switch';
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button'; import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
import { formatDate } from '@workspace/ui/utils'; import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { UserDetail } from './user-detail'; import { UserDetail } from './user-detail';
@@ -56,10 +58,10 @@ export default function Page() {
<Switch <Switch
defaultChecked={row.getValue('enable')} defaultChecked={row.getValue('enable')}
onCheckedChange={async (checked) => { onCheckedChange={async (checked) => {
await updateUser({ await updateUserBasicInfo({
...row.original, ...row.original,
enable: checked, enable: checked,
} as unknown as API.UpdateUserRequest); } as unknown as API.UpdateUserBasiceInfoRequest);
toast.success(t('updateSuccess')); toast.success(t('updateSuccess'));
ref.current?.refresh(); ref.current?.refresh();
}} }}
@@ -74,7 +76,17 @@ export default function Page() {
{ {
accessorKey: 'auth_methods', accessorKey: 'auth_methods',
header: t('userName'), header: t('userName'),
cell: ({ row }) => row.original.auth_methods?.[0]?.auth_identifier || '--', cell: ({ row }) => {
const method = row.original.auth_methods?.[0];
return (
<div>
<Badge className='mr-1 uppercase' title={method?.verified ? t('verified') : ''}>
{method?.auth_type}
</Badge>
{method?.auth_identifier}
</div>
);
},
}, },
{ {
accessorKey: 'balance', accessorKey: 'balance',
@@ -117,31 +129,9 @@ export default function Page() {
actions={{ actions={{
render: (row) => { render: (row) => {
return [ return [
<UserForm<API.UpdateUserRequest> <Button key='detail' asChild>
key='edit' <Link href={`/dashboard/user/${row.id}`}>{t('edit')}</Link>
trigger={t('edit')} </Button>,
title={t('editUser')}
loading={loading}
initialValues={row as unknown as API.UpdateUserRequest}
onSubmit={async (values) => {
setLoading(true);
try {
await updateUser({
...row,
...values,
});
toast.success(t('updateSuccess'));
ref.current?.refresh();
setLoading(false);
return true;
} catch (error) {
setLoading(false);
return false;
}
}}
/>,
<ConfirmButton <ConfirmButton
key='edit' key='edit'
trigger={<Button variant='destructive'>{t('delete')}</Button>} trigger={<Button variant='destructive'>{t('delete')}</Button>}
@@ -7,6 +7,7 @@ import { Button } from '@workspace/ui/components/button';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '@workspace/ui/components/hover-card';
import { formatDate } from '@workspace/ui/utils'; import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useState } from 'react'; import { useState } from 'react';
export function UserDetail({ id }: { id: number }) { export function UserDetail({ id }: { id: number }) {
@@ -27,8 +28,10 @@ export function UserDetail({ id }: { id: number }) {
return ( return (
<HoverCard> <HoverCard>
<HoverCardTrigger asChild onMouseEnter={() => setShouldFetch(true)}> <HoverCardTrigger asChild onMouseEnter={() => setShouldFetch(true)}>
<Button variant='link' className='p-0'> <Button variant='link' className='p-0' asChild>
@{data?.email?.split('@')[0] || 'Loading...'} <Link href={`/dashboard/user/${id}`}>
{data?.auth_methods[0]?.auth_identifier || t('loading')}
</Link>
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent> <HoverCardContent>
@@ -38,10 +41,6 @@ export function UserDetail({ id }: { id: number }) {
<span className='text-muted-foreground'>ID</span> <span className='text-muted-foreground'>ID</span>
<span>{data?.id}</span> <span>{data?.id}</span>
</li> </li>
<li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>{t('email')}</span>
<span>{data?.email}</span>
</li>
<li className='flex items-center justify-between font-semibold'> <li className='flex items-center justify-between font-semibold'>
<span className='text-muted-foreground'>{t('balance')}</span> <span className='text-muted-foreground'>{t('balance')}</span>
<span> <span>
+21 -22
View File
@@ -52,7 +52,7 @@ export default function UserForm<T extends Record<string, any>>({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const formSchema = z.object({ const formSchema = z.object({
email: z.string().email(t('form.invalidEmailFormat')), email: z.string().email(t('invalidEmailFormat')),
telephone_area_code: z.string().optional(), telephone_area_code: z.string().optional(),
telephone: z.string().optional(), telephone: z.string().optional(),
password: z.string().optional(), password: z.string().optional(),
@@ -104,10 +104,10 @@ export default function UserForm<T extends Record<string, any>>({
name='email' name='email'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.userEmail')}</FormLabel> <FormLabel>{t('userEmail')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
placeholder={t('form.userEmailPlaceholder')} placeholder={t('userEmailPlaceholder')}
{...field} {...field}
onValueChange={(value) => { onValueChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
@@ -124,7 +124,7 @@ export default function UserForm<T extends Record<string, any>>({
name='telephone' name='telephone'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.telephone')}</FormLabel> <FormLabel>{t('telephone')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
prefix={ prefix={
@@ -137,7 +137,7 @@ export default function UserForm<T extends Record<string, any>>({
<AreaCodeSelect <AreaCodeSelect
className='w-32 rounded-none border-y-0 border-l-0' className='w-32 rounded-none border-y-0 border-l-0'
simple simple
placeholder={t('form.areaCodePlaceholder')} placeholder={t('areaCodePlaceholder')}
value={field.value} value={field.value}
onChange={(value) => { onChange={(value) => {
form.setValue(field.name, value.phone); form.setValue(field.name, value.phone);
@@ -149,7 +149,7 @@ export default function UserForm<T extends Record<string, any>>({
)} )}
/> />
} }
placeholder={t('form.telephonePlaceholder')} placeholder={t('telephonePlaceholder')}
{...field} {...field}
onValueChange={(value) => { onValueChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
@@ -165,10 +165,10 @@ export default function UserForm<T extends Record<string, any>>({
name='password' name='password'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.password')}</FormLabel> <FormLabel>{t('password')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
placeholder={t('form.passwordPlaceholder')} placeholder={t('passwordPlaceholder')}
{...field} {...field}
onValueChange={(value) => { onValueChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
@@ -184,10 +184,10 @@ export default function UserForm<T extends Record<string, any>>({
name='referer_id' name='referer_id'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.refererId')}</FormLabel> <FormLabel>{t('refererId')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
placeholder={t('form.refererIdPlaceholder')} placeholder={t('refererIdPlaceholder')}
{...field} {...field}
type='number' type='number'
onValueChange={(value) => { onValueChange={(value) => {
@@ -204,10 +204,10 @@ export default function UserForm<T extends Record<string, any>>({
name='refer_code' name='refer_code'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.inviteCode')}</FormLabel> <FormLabel>{t('inviteCode')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
placeholder={t('form.inviteCodePlaceholder')} placeholder={t('inviteCodePlaceholder')}
{...field} {...field}
onValueChange={(value) => { onValueChange={(value) => {
form.setValue(field.name, value); form.setValue(field.name, value);
@@ -223,11 +223,11 @@ export default function UserForm<T extends Record<string, any>>({
name='balance' name='balance'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.balance')}</FormLabel> <FormLabel>{t('balance')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
prefix={currency?.currency_symbol ?? '$'} prefix={currency?.currency_symbol ?? '$'}
placeholder={t('form.balancePlaceholder')} placeholder={t('balancePlaceholder')}
type='number' type='number'
{...field} {...field}
formatInput={(value) => unitConversion('centsToDollars', value)} formatInput={(value) => unitConversion('centsToDollars', value)}
@@ -247,11 +247,11 @@ export default function UserForm<T extends Record<string, any>>({
name='gift_amount' name='gift_amount'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.giftAmount')}</FormLabel> <FormLabel>{t('giftAmount')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
prefix={currency?.currency_symbol ?? '$'} prefix={currency?.currency_symbol ?? '$'}
placeholder={t('form.giftAmountPlaceholder')} placeholder={t('giftAmountPlaceholder')}
type='number' type='number'
{...field} {...field}
formatInput={(value) => unitConversion('centsToDollars', value)} formatInput={(value) => unitConversion('centsToDollars', value)}
@@ -271,11 +271,11 @@ export default function UserForm<T extends Record<string, any>>({
name='commission' name='commission'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.commission')}</FormLabel> <FormLabel>{t('commission')}</FormLabel>
<FormControl> <FormControl>
<EnhancedInput <EnhancedInput
prefix={currency?.currency_symbol ?? '$'} prefix={currency?.currency_symbol ?? '$'}
placeholder={t('form.commissionPlaceholder')} placeholder={t('commissionPlaceholder')}
type='number' type='number'
{...field} {...field}
formatInput={(value) => unitConversion('centsToDollars', value)} formatInput={(value) => unitConversion('centsToDollars', value)}
@@ -295,7 +295,7 @@ export default function UserForm<T extends Record<string, any>>({
name='is_admin' name='is_admin'
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('form.manager')}</FormLabel> <FormLabel>{t('manager')}</FormLabel>
<FormControl> <FormControl>
<div className='pt-2'> <div className='pt-2'>
<Switch checked={!!field.value} onCheckedChange={field.onChange} /> <Switch checked={!!field.value} onCheckedChange={field.onChange} />
@@ -316,11 +316,10 @@ export default function UserForm<T extends Record<string, any>>({
setOpen(false); setOpen(false);
}} }}
> >
{t('form.cancel')} {t('cancel')}
</Button> </Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}> <Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />}{' '} {loading && <Icon icon='mdi:loading' className='mr-2 animate-spin' />} {t('confirm')}
{t('form.confirm')}
</Button> </Button>
</SheetFooter> </SheetFooter>
</SheetContent> </SheetContent>
@@ -59,7 +59,7 @@ export function RevenueStatisticsCard() {
return ( return (
<Tabs defaultValue='today'> <Tabs defaultValue='today'>
<Card> <Card className='h-full'>
<CardHeader className='flex !flex-row items-center justify-between'> <CardHeader className='flex !flex-row items-center justify-between'>
<CardTitle>{t('revenueTitle')}</CardTitle> <CardTitle>{t('revenueTitle')}</CardTitle>
<TabsList> <TabsList>
@@ -68,8 +68,8 @@ export function RevenueStatisticsCard() {
<TabsTrigger value='total'>{t('total')}</TabsTrigger> <TabsTrigger value='total'>{t('total')}</TabsTrigger>
</TabsList> </TabsList>
</CardHeader> </CardHeader>
<TabsContent value='today'> <TabsContent value='today' className='h-full'>
<CardContent> <CardContent className='h-80'>
{RevenueStatistics?.today.new_order_amount || {RevenueStatistics?.today.new_order_amount ||
RevenueStatistics?.today.renewal_order_amount ? ( RevenueStatistics?.today.renewal_order_amount ? (
<ChartContainer config={IncomeStatisticsConfig} className='mx-auto max-h-80'> <ChartContainer config={IncomeStatisticsConfig} className='mx-auto max-h-80'>
@@ -129,7 +129,9 @@ export function RevenueStatisticsCard() {
</PieChart> </PieChart>
</ChartContainer> </ChartContainer>
) : ( ) : (
<Empty /> <div className='flex h-full items-center justify-center'>
<Empty />
</div>
)} )}
</CardContent> </CardContent>
<CardFooter className='flex h-20 flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
@@ -162,8 +164,8 @@ export function RevenueStatisticsCard() {
</CardFooter> </CardFooter>
</TabsContent> </TabsContent>
<TabsContent value='month'> <TabsContent value='month' className='h-full'>
<CardContent> <CardContent className='h-80'>
{RevenueStatistics?.monthly.list && RevenueStatistics?.monthly.list.length > 0 ? ( {RevenueStatistics?.monthly.list && RevenueStatistics?.monthly.list.length > 0 ? (
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'> <ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
<BarChart <BarChart
@@ -196,7 +198,9 @@ export function RevenueStatisticsCard() {
</BarChart> </BarChart>
</ChartContainer> </ChartContainer>
) : ( ) : (
<Empty /> <div className='flex h-full items-center justify-center'>
<Empty />
</div>
)} )}
</CardContent> </CardContent>
<CardFooter className='flex h-20 flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
@@ -232,8 +236,8 @@ export function RevenueStatisticsCard() {
</CardFooter> </CardFooter>
</TabsContent> </TabsContent>
<TabsContent value='total'> <TabsContent value='total' className='h-full'>
<CardContent> <CardContent className='h-80'>
{RevenueStatistics?.all.list && RevenueStatistics?.all.list.length > 0 ? ( {RevenueStatistics?.all.list && RevenueStatistics?.all.list.length > 0 ? (
<ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'> <ChartContainer config={IncomeStatisticsConfig} className='max-h-80 w-full'>
<AreaChart <AreaChart
@@ -283,7 +287,9 @@ export function RevenueStatisticsCard() {
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>
) : ( ) : (
<Empty /> <div className='flex h-full items-center justify-center'>
<Empty />
</div>
)} )}
</CardContent> </CardContent>
<CardFooter className='flex h-20 flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
@@ -149,7 +149,7 @@ export default function Statistics() {
</TabsList> </TabsList>
</Tabs> </Tabs>
</CardHeader> </CardHeader>
<CardContent> <CardContent className='h-80'>
<div className='mb-6 flex items-center justify-between'> <div className='mb-6 flex items-center justify-between'>
<h4 className='font-semibold'> <h4 className='font-semibold'>
{dataType === 'nodes' ? t('nodeTraffic') : t('userTraffic')} {dataType === 'nodes' ? t('nodeTraffic') : t('userTraffic')}
@@ -230,7 +230,9 @@ export default function Statistics() {
</BarChart> </BarChart>
</ChartContainer> </ChartContainer>
) : ( ) : (
<Empty /> <div className='flex h-full items-center justify-center'>
<Empty />
</div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
@@ -61,7 +61,7 @@ export function UserStatisticsCard() {
return ( return (
<Tabs defaultValue='today'> <Tabs defaultValue='today'>
<Card> <Card className='h-full'>
<CardHeader className='flex !flex-row items-center justify-between'> <CardHeader className='flex !flex-row items-center justify-between'>
<CardTitle>{t('userTitle')}</CardTitle> <CardTitle>{t('userTitle')}</CardTitle>
<TabsList> <TabsList>
@@ -71,8 +71,8 @@ export function UserStatisticsCard() {
</TabsList> </TabsList>
</CardHeader> </CardHeader>
<TabsContent value='today'> <TabsContent value='today' className='h-full'>
<CardContent> <CardContent className='h-80'>
{UserStatistics?.today.register || {UserStatistics?.today.register ||
UserStatistics?.today.new_order_users || UserStatistics?.today.new_order_users ||
UserStatistics?.today.renewal_order_users ? ( UserStatistics?.today.renewal_order_users ? (
@@ -133,7 +133,9 @@ export function UserStatisticsCard() {
</PieChart> </PieChart>
</ChartContainer> </ChartContainer>
) : ( ) : (
<Empty /> <div className='flex h-full items-center justify-center'>
<Empty />
</div>
)} )}
</CardContent> </CardContent>
<CardFooter className='flex h-20 flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
@@ -168,8 +170,8 @@ export function UserStatisticsCard() {
</CardFooter> </CardFooter>
</TabsContent> </TabsContent>
<TabsContent value='month'> <TabsContent value='month' className='h-full'>
<CardContent> <CardContent className='h-80'>
{UserStatistics?.monthly.list && UserStatistics?.monthly.list.length > 0 ? ( {UserStatistics?.monthly.list && UserStatistics?.monthly.list.length > 0 ? (
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'> <ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
<BarChart <BarChart
@@ -204,7 +206,9 @@ export function UserStatisticsCard() {
</BarChart> </BarChart>
</ChartContainer> </ChartContainer>
) : ( ) : (
<Empty /> <div className='flex h-full items-center justify-center'>
<Empty />
</div>
)} )}
</CardContent> </CardContent>
<CardFooter className='flex h-20 flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
@@ -239,8 +243,8 @@ export function UserStatisticsCard() {
</CardFooter> </CardFooter>
</TabsContent> </TabsContent>
<TabsContent value='total'> <TabsContent value='total' className='h-full'>
<CardContent> <CardContent className='h-80'>
{UserStatistics?.all.list && UserStatistics?.all.list.length > 0 ? ( {UserStatistics?.all.list && UserStatistics?.all.list.length > 0 ? (
<ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'> <ChartContainer config={UserStatisticsConfig} className='max-h-80 w-full'>
<AreaChart <AreaChart
@@ -299,7 +303,9 @@ export function UserStatisticsCard() {
</AreaChart> </AreaChart>
</ChartContainer> </ChartContainer>
) : ( ) : (
<Empty /> <div className='flex h-full items-center justify-center'>
<Empty />
</div>
)} )}
</CardContent> </CardContent>
<CardFooter className='flex h-20 flex-row border-t p-4'> <CardFooter className='flex h-20 flex-row border-t p-4'>
+29 -4
View File
@@ -11,13 +11,38 @@ import {
} from '@workspace/ui/components/select'; } from '@workspace/ui/components/select';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { getCountry } from '@workspace/ui/utils'; import { getCountry } from '@workspace/ui/utils';
import { useLocale, useTranslations } from 'next-intl'; import { useLocale } from 'next-intl';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
const languages = {
'cs-CZ': 'Čeština',
'de-DE': 'Deutsch',
'en-US': 'English',
'es-ES': 'Español',
'es-MX': 'Español (México)',
'fa-IR': 'فارسی',
'fi-FI': 'Suomi',
'fr-FR': 'Français',
'hi-IN': 'हिन्दी',
'hu-HU': 'Magyar',
'ja-JP': '日本語',
'ko-KR': '한국어',
'no-NO': 'Norsk',
'pl-PL': 'Polski',
'pt-BR': 'Português (Brasil)',
'ro-RO': 'Română',
'ru-RU': 'Русский',
'th-TH': 'ไทย',
'tr-TR': 'Türkçe',
'uk-UA': 'Українська',
'vi-VN': 'Tiếng Việt',
'zh-CN': '简体中文',
'zh-HK': '繁體中文',
} as const;
export default function LanguageSwitch() { export default function LanguageSwitch() {
const locale = useLocale(); const locale = useLocale();
const country = getCountry(locale); const country = getCountry(locale);
const t = useTranslations('language');
const router = useRouter(); const router = useRouter();
const handleLanguageChange = (value: string) => { const handleLanguageChange = (value: string) => {
@@ -31,7 +56,7 @@ export default function LanguageSwitch() {
<SelectValue> <SelectValue>
<div className='flex items-center'> <div className='flex items-center'>
<Icon icon={`flagpack:${country?.alpha2.toLowerCase()}`} className='!size-5' /> <Icon icon={`flagpack:${country?.alpha2.toLowerCase()}`} className='!size-5' />
<span className='sr-only'>{t(`${locale}`)}</span> <span className='sr-only'>{languages[locale as keyof typeof languages]}</span>
</div> </div>
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
@@ -40,7 +65,7 @@ export default function LanguageSwitch() {
<SelectItem key={`${item?.lang}-${item?.alpha2}`} value={`${item?.lang}-${item?.alpha2}`}> <SelectItem key={`${item?.lang}-${item?.alpha2}`} value={`${item?.lang}-${item?.alpha2}`}>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<Icon icon={`flagpack:${item?.alpha2.toLowerCase()}`} className='!size-5' /> <Icon icon={`flagpack:${item?.alpha2.toLowerCase()}`} className='!size-5' />
{t(`${item?.lang}-${item?.alpha2}`)} {languages[`${item?.lang}-${item?.alpha2}` as keyof typeof languages]}
</div> </div>
</SelectItem> </SelectItem>
))} ))}
+2
View File
@@ -2,6 +2,7 @@
import { ProTable as _ProTable, ProTableProps } from '@workspace/ui/custom-components/pro-table'; import { ProTable as _ProTable, ProTableProps } from '@workspace/ui/custom-components/pro-table';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { Empty } from './empty';
export { type ProTableActions } from '@workspace/ui/custom-components/pro-table'; export { type ProTableActions } from '@workspace/ui/custom-components/pro-table';
export function ProTable< export function ProTable<
@@ -25,6 +26,7 @@ export function ProTable<
}, },
textRowsPerPage: t('rowsPerPage'), textRowsPerPage: t('rowsPerPage'),
}} }}
empty={<Empty />}
/> />
); );
} }
+23 -2
View File
@@ -35,6 +35,10 @@ export const AuthControl = [
title: 'GitHub', title: 'GitHub',
url: '/dashboard/auth-control/github', url: '/dashboard/auth-control/github',
}, },
{
title: 'Device',
url: '/dashboard/auth-control/device',
},
]; ];
export const navs = [ export const navs = [
@@ -57,6 +61,11 @@ export const navs = [
icon: 'flat-color-icons:lock-portrait', icon: 'flat-color-icons:lock-portrait',
items: AuthControl, items: AuthControl,
}, },
{
title: 'ADS Config',
url: '/dashboard/ads',
icon: 'flat-color-icons:electrical-sensor',
},
{ {
title: 'Payment Config', title: 'Payment Config',
url: '/dashboard/payment', url: '/dashboard/payment',
@@ -101,11 +110,17 @@ export const navs = [
title: 'User Management', title: 'User Management',
url: '/dashboard/user', url: '/dashboard/user',
icon: 'flat-color-icons:conference-call', icon: 'flat-color-icons:conference-call',
items: [
{
title: 'User Detail',
url: '/dashboard/user/:id',
},
],
}, },
{ {
title: 'Announcement Management', title: 'Announcement Management',
url: '/dashboard/announcement', url: '/dashboard/announcement',
icon: 'flat-color-icons:news', icon: 'flat-color-icons:advertising',
}, },
{ {
title: 'Ticket Management', title: 'Ticket Management',
@@ -127,9 +142,15 @@ export const navs = [
]; ];
export function findNavByUrl(url: string) { export function findNavByUrl(url: string) {
function matchDynamicRoute(pattern: string, path: string): boolean {
const regexPattern = pattern.replace(/:[^/]+/g, '[^/]+').replace(/\//g, '\\/');
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(path);
}
function findNav(items: any[], url: string, path: any[] = []): any[] { function findNav(items: any[], url: string, path: any[] = []): any[] {
for (const item of items) { for (const item of items) {
if (item.url === url) { if (item.url === url || (item.url && matchDynamicRoute(item.url, url))) {
return [...path, item]; return [...path, item];
} }
if (item.items) { if (item.items) {
+16 -28
View File
@@ -14,50 +14,34 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
site_name: '', site_name: '',
site_desc: '', site_desc: '',
site_logo: '', site_logo: '',
keywords: '',
custom_html: '',
}, },
verify: { verify: {
turnstile_site_key: '',
enable_login_verify: false, enable_login_verify: false,
enable_register_verify: false, enable_register_verify: false,
enable_reset_password_verify: false, enable_reset_password_verify: false,
turnstile_site_key: '',
}, },
auth: { auth: {
sms: { mobile: {
sms_enabled: false, enable: false,
sms_limit: 0, enable_whitelist: false,
sms_interval: 0, whitelist: [],
sms_expire_time: 0,
}, },
email: { email: {
email_enabled: false, enable: false,
email_enable_verify: false, enable_verify: false,
email_enable_domain_suffix: false, enable_domain_suffix: false,
email_domain_suffix_list: '', domain_suffix_list: '',
}, },
register: { register: {
stop_register: false, stop_register: false,
enable_trial: false,
enable_ip_register_limit: false, enable_ip_register_limit: false,
ip_register_limit: 0, ip_register_limit: 0,
ip_register_limit_duration: 0, ip_register_limit_duration: 0,
}, },
}, },
register: {
stop_register: false,
enable_email_verify: false,
enable_email_domain_suffix: false,
email_domain_suffix_list: '',
enable_trial: false,
enable_ip_register_limit: false,
ip_register_limit: 0,
ip_register_limit_duration: 0,
sms: {
sms_enabled: false,
sms_limit: 0,
sms_interval: 0,
sms_expire_time: 0,
},
},
invite: { invite: {
forced_invite: false, forced_invite: false,
referral_percentage: 0, referral_percentage: 0,
@@ -66,7 +50,6 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
currency: { currency: {
currency_unit: 'USD', currency_unit: 'USD',
currency_symbol: '$', currency_symbol: '$',
access_key: '',
}, },
subscribe: { subscribe: {
single_model: false, single_model: false,
@@ -74,6 +57,11 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
subscribe_domain: '', subscribe_domain: '',
pan_domain: false, pan_domain: false,
}, },
verify_code: {
verify_code_expire_time: 5,
verify_code_limit: 15,
verify_code_interval: 60,
},
oauth_methods: [], oauth_methods: [],
}, },
user: undefined, user: undefined,
+40
View File
@@ -0,0 +1,40 @@
{
"cancel": "Zrušit",
"confirm": "Potvrdit",
"confirmDelete": "Potvrdit smazání",
"create": "Vytvořit inzerát",
"createAds": "Vytvořit reklamu",
"createSuccess": "Úspěšně vytvořeno",
"delete": "Smazat",
"deleteSuccess": "Úspěšně smazáno",
"deleteWarning": "Opravdu chcete smazat tento inzerát?",
"disabled": "Deaktivováno",
"edit": "Upravit",
"editAds": "Upravit reklamu",
"enabled": "Aktivováno",
"form": {
"cancel": "Zrušit",
"confirm": "Potvrdit",
"content": "Obsah",
"description": "Popis",
"endTime": "Čas ukončení",
"enterContent": "Prosím zadejte obsah inzerátu",
"enterDescription": "Prosím zadejte popis inzerátu",
"enterEndTime": "Prosím vyberte čas ukončení",
"enterStartTime": "Prosím vyberte čas zahájení",
"enterTargetUrl": "Prosím zadejte cílovou URL",
"enterTitle": "Prosím zadejte název inzerátu",
"startTime": "Čas zahájení",
"targetUrl": "Cílová URL",
"title": "Název",
"type": "Typ inzerátu",
"typeImage": "Obrázek",
"typeVideo": "Video"
},
"status": "Stav",
"targetUrl": "Cílová URL",
"title": "Název",
"type": "Typ",
"updateSuccess": "Úspěšně aktualizováno",
"validityPeriod": "Doba platnosti"
}
+2 -2
View File
@@ -7,8 +7,8 @@
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého Apple ID", "enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého Apple ID",
"keyId": "ID klíče", "keyId": "ID klíče",
"keyIdDescription": "ID vašeho soukromého klíče z Apple Developer Portálu", "keyIdDescription": "ID vašeho soukromého klíče z Apple Developer Portálu",
"redirectUri": "Návratová URL", "redirectUri": "Přesměrovací URL",
"redirectUriDescription": "URL, na kterou Apple přesměruje po úspěšném ověření", "redirectUriDescription": "Prosím vyplňte adresu API přesměrované URL po úspěšném ověření pomocí Apple. Nepoužívejte / na konci.",
"saveFailed": "Uložení se nezdařilo", "saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné", "saveSuccess": "Uložení bylo úspěšné",
"teamId": "ID týmu", "teamId": "ID týmu",
+72 -11
View File
@@ -1,13 +1,74 @@
{ {
"ipRegistrationLimit": "Limit registrace IP", "invite": {
"ipRegistrationLimitDescription": "Když je povoleno, IP adresy, které splňují požadavky pravidla, budou omezeny v registraci; mějte na paměti, že určení IP může způsobit problémy kvůli CDN nebo frontendovým proxy serverům", "commissionFirstTimeOnly": "Provize pouze za první nákup",
"penaltyTime": "Čas trestu (minuty)", "commissionFirstTimeOnlyDescription": "Pokud je povoleno, provize se generuje pouze při první platbě pozývatele; jednotlivé uživatele můžete konfigurovat v uživatelské správě",
"penaltyTimeDescription": "Uživatelé musí počkat, až vyprší doba penalizace, než se mohou znovu zaregistrovat", "enableForcedInvite": "Povolit nucené pozvání",
"registrationLimitCount": "Počet registrací", "enableForcedInviteDescription": "Pokud je povoleno, mohou se registrovat pouze pozvaní uživatelé",
"registrationLimitCountDescription": "Povolit sankci po dosažení limitu registrací", "inputPlaceholder": "Zadejte",
"saveSuccess": "Uložení bylo úspěšné", "inviteCommissionPercentage": "Procento provize za pozvání",
"stopNewUserRegistration": "Zastavit registraci nových uživatelů", "inviteCommissionPercentageDescription": "Výchozí globální poměr rozdělení provize; jednotlivé poměry můžete konfigurovat v uživatelské správě",
"stopNewUserRegistrationDescription": "Když je povoleno, nikdo se nemůže registrovat", "inviteSettings": "Nastavení pozvání",
"trialRegistration": "Registrace do zkušební verze", "saveSuccess": "Uložení úspěšné"
"trialRegistrationDescription": "Povolit registraci na zkušební verzi; nejprve upravte zkušební balíček a dobu trvání" },
"log": {
"content": "Obsah",
"createdAt": "Vytvořeno",
"emailLog": "Emailový záznam",
"mobileLog": "Mobilní záznam",
"platform": "Platforma",
"sendFailed": "Selhalo",
"sendSuccess": "Úspěch",
"status": "Stav",
"subject": "Předmět",
"to": "Příjemce",
"updatedAt": "Aktualizováno"
},
"register": {
"day": "Den",
"hour": "Hodina",
"ipRegistrationLimit": "Omezení registrace podle IP",
"ipRegistrationLimitDescription": "Pokud je povoleno, IP adresy splňující pravidla budou omezeny v registraci; mějte na paměti, že určení IP může způsobit problémy kvůli CDN nebo proxy serverům",
"minute": "Minuta",
"month": "Měsíc",
"noLimit": "Bez omezení",
"penaltyTime": "Doba trestu (minuty)",
"penaltyTimeDescription": "Uživatelé musí počkat, až doba trestu vyprší, než se znovu zaregistrují",
"registerSettings": "Nastavení registrace",
"registrationLimitCount": "Počet omezení registrace",
"registrationLimitCountDescription": "Povolit trest po dosažení registračního limitu",
"saveSuccess": "Uložení úspěšné",
"stopNewUserRegistration": "Zastavit registraci nových uživatelů",
"stopNewUserRegistrationDescription": "Pokud je povoleno, nikdo se nemůže registrovat",
"trialDuration": "Doba zkušební verze",
"trialRegistration": "Zkušební registrace",
"trialRegistrationDescription": "Povolit zkušební registraci; nejprve upravte zkušební balíček a dobu trvání",
"trialSubscribePlan": "Plán zkušebního předplatného",
"trialSubscribePlanDescription": "Vyberte plán zkušebního předplatného",
"year": "Rok"
},
"verify": {
"inputPlaceholder": "Zadejte",
"loginVerificationCode": "Ověřovací kód pro přihlášení",
"loginVerificationCodeDescription": "Ověření člověka během přihlášení",
"registrationVerificationCode": "Ověřovací kód pro registraci",
"registrationVerificationCodeDescription": "Ověření člověka během registrace",
"resetPasswordVerificationCode": "Ověřovací kód pro resetování hesla",
"resetPasswordVerificationCodeDescription": "Ověření člověka během resetování hesla",
"saveSuccess": "Uložení úspěšné",
"turnstileSecretDescription": "Tajný klíč turniketu poskytovaný Cloudflare",
"turnstileSiteKeyDescription": "Klíč pro web turniketu poskytovaný Cloudflare",
"verifySettings": "Nastavení ověření"
},
"verify-code": {
"dailyLimit": "Denní limit",
"dailyLimitDescription": "Maximální počet ověřovacích kódů, které lze odeslat za den",
"expireTime": "Čas vypršení",
"expireTimeDescription": "Doba platnosti ověřovacího kódu (vteřiny)",
"interval": "Interval odesílání",
"intervalDescription": "Minimální interval mezi odesíláním ověřovacích kódů (sekundy)",
"saveSuccess": "Uložení úspěšné",
"second": "sekundy",
"times": "krát",
"verifyCodeSettings": "Nastavení ověřovacího kódu"
}
} }
+1 -1
View File
@@ -20,7 +20,7 @@
"logout": "Odhlásit se", "logout": "Odhlásit se",
"methods": { "methods": {
"email": "E-mail", "email": "E-mail",
"sms": "Telefon" "mobile": "Telefon"
}, },
"orWithEmail": "nebo pomocí e-mailu", "orWithEmail": "nebo pomocí e-mailu",
"register": { "register": {
+2
View File
@@ -45,9 +45,11 @@
"60002": "Toto předplatné nelze momentálně použít, zkuste to prosím později.", "60002": "Toto předplatné nelze momentálně použít, zkuste to prosím později.",
"60003": "Bylo zjištěno existující předplatné. Zrušte ho, než budete pokračovat.", "60003": "Bylo zjištěno existující předplatné. Zrušte ho, než budete pokračovat.",
"60004": "Nelze momentálně odstranit, protože předplatné má aktivní uživatele.", "60004": "Nelze momentálně odstranit, protože předplatné má aktivní uživatele.",
"60005": "Režim jednotného předplatného překročil limit uživatelů",
"70001": "Ověřovací kód je nesprávný, zadejte ho prosím znovu.", "70001": "Ověřovací kód je nesprávný, zadejte ho prosím znovu.",
"80001": "Úkol nebyl úspěšně přidán do fronty, zkuste to prosím později.", "80001": "Úkol nebyl úspěšně přidán do fronty, zkuste to prosím později.",
"90001": "Vypněte prosím režim DEBUG a zkuste to znovu.", "90001": "Vypněte prosím režim DEBUG a zkuste to znovu.",
"90015": "Tento účet dnes dosáhl limitu pro odesílání, zkuste to prosím znovu zítra",
"undefined": "Došlo k chybě systému, zkuste to prosím později." "undefined": "Došlo k chybě systému, zkuste to prosím později."
}, },
"table": { "table": {
+14
View File
@@ -0,0 +1,14 @@
{
"blockVirtualMachine": "Blokovat virtuální stroj",
"blockVirtualMachineDescription": "Pokud je povoleno, zařízení nebudou moci běžet na virtuálních strojích nebo emulátorech",
"communicationKey": "Komunikační klíč",
"communicationKeyDescription": "Komunikační klíč se používá pro bezpečnou komunikaci mezi zařízeními a servery",
"enable": "Povolit",
"enableDescription": "Po povolení jsou podporovány různé identifikátory zařízení, jako jsou IMEI/IDFA/IDFV/AndroidID/Mac adresa pro přihlášení a registraci",
"enableSecurity": "Povolit šifrování komunikace",
"enableSecurityDescription": "Pokud je povoleno, komunikace mezi zařízeními a servery bude šifrována",
"saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné",
"showAds": "Zobrazit reklamy",
"showAdsDescription": "Pokud je povoleno, na zařízeních se budou zobrazovat reklamy"
}
+4 -4
View File
@@ -1,19 +1,17 @@
{ {
"emailBasicConfig": "Základní konfigurace",
"emailBasicConfigDescription": "Nakonfigurujte nastavení SMTP serveru a možnosti ověření e-mailu", "emailBasicConfigDescription": "Nakonfigurujte nastavení SMTP serveru a možnosti ověření e-mailu",
"emailLogs": "Záznamy e-mailů",
"emailLogsDescription": "Zobrazit historii odeslaných e-mailů a jejich stav", "emailLogsDescription": "Zobrazit historii odeslaných e-mailů a jejich stav",
"emailSuffixWhitelist": "Seznam povolených přípon e-mailů", "emailSuffixWhitelist": "Seznam povolených přípon e-mailů",
"emailSuffixWhitelistDescription": "Když je povoleno, mohou se registrovat pouze e-maily s příponami v seznamu", "emailSuffixWhitelistDescription": "Když je povoleno, mohou se registrovat pouze e-maily s příponami v seznamu",
"emailTemplate": "Šablona e-mailu",
"emailVerification": "Ověření e-mailu", "emailVerification": "Ověření e-mailu",
"emailVerificationDescription": "Když je povoleno, uživatelé budou muset ověřit svůj e-mail", "emailVerificationDescription": "Když je povoleno, uživatelé budou muset ověřit svůj e-mail",
"enable": "Povolit", "enable": "Povolit",
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu", "enableDescription": "Po aktivaci budou povoleny funkce registrace e-mailem, přihlášení, propojení a odpojení.",
"expiration_email_template": "Šablona oznámení o vypršení platnosti", "expiration_email_template": "Šablona oznámení o vypršení platnosti",
"expiration_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.", "expiration_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"failed": "Neúspěch", "failed": "Neúspěch",
"inputPlaceholder": "Zadejte hodnotu...", "inputPlaceholder": "Zadejte hodnotu...",
"logs": "Protokoly",
"maintenance_email_template": "Šablona oznámení o údržbě", "maintenance_email_template": "Šablona oznámení o údržbě",
"maintenance_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.", "maintenance_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"recipient": "Příjemce", "recipient": "Příjemce",
@@ -27,6 +25,7 @@
"senderAddressDescription": "Výchozí e-mailová adresa používaná pro odesílání e-mailů.", "senderAddressDescription": "Výchozí e-mailová adresa používaná pro odesílání e-mailů.",
"sent": "Odesláno", "sent": "Odesláno",
"sentAt": "Odesláno", "sentAt": "Odesláno",
"settings": "Nastavení",
"smtpAccount": "SMTP účet", "smtpAccount": "SMTP účet",
"smtpAccountDescription": "E-mailový účet používaný pro ověřování.", "smtpAccountDescription": "E-mailový účet používaný pro ověřování.",
"smtpEncryptionMethod": "Metoda šifrování SMTP", "smtpEncryptionMethod": "Metoda šifrování SMTP",
@@ -39,6 +38,7 @@
"smtpServerPortDescription": "Zadejte port používaný k připojení k SMTP serveru.", "smtpServerPortDescription": "Zadejte port používaný k připojení k SMTP serveru.",
"status": "Stav", "status": "Stav",
"subject": "Předmět", "subject": "Předmět",
"template": "Šablona",
"verify_email_template": "Šablona ověřovacího e-mailu", "verify_email_template": "Šablona ověřovacího e-mailu",
"verify_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.", "verify_email_templateDescription": "Zástupné symboly {after}.variable{before} budou nahrazeny skutečnými daty. Ujistěte se, že tyto proměnné zachováte.",
"whitelistSuffixes": "Povolené přípony", "whitelistSuffixes": "Povolené přípony",
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Facebook App Secret z Facebook Developers Console", "clientSecretDescription": "Facebook App Secret z Facebook Developers Console",
"enable": "Povolit", "enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu na Facebooku", "enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu na Facebooku",
"redirectUri": "Autorizovaná přesměrovací URI",
"redirectUriDescription": "URL, na kterou bude uživatel přesměrován po ověření na Facebooku",
"saveFailed": "Uložení se nezdařilo", "saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné" "saveSuccess": "Uložení bylo úspěšné"
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Tajný klíč klienta z nastavení vaší GitHub OAuth aplikace", "clientSecretDescription": "Tajný klíč klienta z nastavení vaší GitHub OAuth aplikace",
"enable": "Povolit ověřování GitHub", "enable": "Povolit ověřování GitHub",
"enableDescription": "Povolit uživatelům přihlásit se pomocí jejich účtů na GitHubu", "enableDescription": "Povolit uživatelům přihlásit se pomocí jejich účtů na GitHubu",
"redirectUri": "URL pro zpětné volání autorizace",
"redirectUriDescription": "URL ve vaší aplikaci, kam budou uživatelé přesměrováni po autentizaci na GitHubu",
"saveFailed": "Nepodařilo se uložit nastavení GitHubu", "saveFailed": "Nepodařilo se uložit nastavení GitHubu",
"saveSuccess": "Nastavení GitHubu bylo úspěšně uloženo" "saveSuccess": "Nastavení GitHubu bylo úspěšně uloženo"
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Tajný klíč klienta Google OAuth 2.0 z Google Cloud Console", "clientSecretDescription": "Tajný klíč klienta Google OAuth 2.0 z Google Cloud Console",
"enable": "Povolit", "enable": "Povolit",
"enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu Google", "enableDescription": "Po povolení se uživatelé mohou přihlásit pomocí svého účtu Google",
"redirectUri": "Autorizovaná přesměrovací URI",
"redirectUriDescription": "URL, na kterou bude uživatel přesměrován po ověření Googlem",
"saveFailed": "Uložení se nezdařilo", "saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné" "saveSuccess": "Uložení bylo úspěšné"
} }
-25
View File
@@ -1,25 +0,0 @@
{
"cs-CZ": "čeština",
"de-DE": "němčina",
"en-US": "angličtina",
"es-ES": "španělština",
"es-MX": "španělština (Mexiko)",
"fa-IR": "Perština",
"fi-FI": "finština",
"fr-FR": "Francouzština",
"hi-IN": "hindština",
"hu-HU": "maďarština",
"ja-JP": "Japonština",
"ko-KR": "Korejština",
"no-NO": "norština",
"pl-PL": "polština",
"pt-BR": "portugalština (Brazílie)",
"ro-RO": "rumunština",
"ru-RU": "ruština",
"th-TH": "thajština",
"tr-TR": "turečtina",
"uk-UA": "Ukrajinština",
"vi-VN": "Vietnamština",
"zh-CN": "čeština (CS)",
"zh-HK": "čínština (tradiční)"
}
+3
View File
@@ -1,9 +1,11 @@
{ {
"ADS Config": "Konfigurace ADS",
"Announcement Management": "Správa oznámení", "Announcement Management": "Správa oznámení",
"Apple": "Apple ID", "Apple": "Apple ID",
"Auth Control": "Řízení ověřování", "Auth Control": "Řízení ověřování",
"Coupon Management": "Správa kupónů", "Coupon Management": "Správa kupónů",
"Dashboard": "Přístrojová deska", "Dashboard": "Přístrojová deska",
"Device": "Zařízení",
"Document Management": "Správa dokumentů", "Document Management": "Správa dokumentů",
"Email": "E-mail", "Email": "E-mail",
"Facebook": "Facebook", "Facebook": "Facebook",
@@ -24,5 +26,6 @@
"Ticket Management": "Správa lístků", "Ticket Management": "Správa lístků",
"Twitter": "Twitter", "Twitter": "Twitter",
"User": "Uživatel", "User": "Uživatel",
"User Detail": "Podrobnosti o uživateli",
"User Management": "Správa uživatelů" "User Management": "Správa uživatelů"
} }
+12 -21
View File
@@ -1,41 +1,32 @@
{ {
"SmsList": "Seznam SMS záznamů", "accessLabel": "Přístup",
"applyPlatform": "Použít platformu", "applyPlatform": "Použít platformu",
"areaCode": "Směrový kód",
"content": "Obsah",
"createdAt": "Čas odeslání",
"enable": "Povolit", "enable": "Povolit",
"enableTip": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu", "enableTip": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"expireTime": "Čas vypršení", "endpointLabel": "Koncový bod",
"expireTimeTip": "Doba platnosti ověřovacího kódu SMS (v sekundách)",
"interval": "Interval",
"intervalTip": "Interval odesílání SMS ověřovacího kódu pro stejné telefonní číslo (v sekundách), 0 znamená bez omezení",
"limit": "Denní limit",
"limitTip": "Denní limit pro odesílání ověřovacích SMS kódů na stejné telefonní číslo, 0 znamená bez omezení",
"logs": "Protokoly", "logs": "Protokoly",
"phoneNumberLabel": "Telefonní číslo",
"placeholders": { "placeholders": {
"expireTime": "Zadejte dobu vypršení, výchozí je 300",
"interval": "Zadejte interval, výchozí je 60",
"limit": "Zadejte denní limit, výchozí je 20",
"template": "Váš ověřovací kód je {code}, platný po dobu 5 minut", "template": "Váš ověřovací kód je {code}, platný po dobu 5 minut",
"templateCode": "Zadejte kód šablony" "templateCode": "Zadejte kód šablony"
}, },
"platform": "SMS platforma", "platform": "SMS platforma",
"platformConfigTip": "Vyplňte prosím poskytnutou konfiguraci {key}", "platformConfigTip": "Vyplňte prosím poskytnutou konfiguraci {key}",
"platformTip": "Vyberte prosím platformu pro SMS", "platformTip": "Vyberte prosím platformu pro SMS",
"search": "Vyhledat telefonní číslo", "secretLabel": "Tajný klíč",
"sendFailed": "Odeslání se nezdařilo", "sendFailed": "Odeslání se nezdařilo",
"sendSuccess": "Úspěšně odesláno", "sendSuccess": "Úspěšně odesláno",
"settings": "Nastavení", "settings": "Nastavení",
"status": "Stav", "signNameLabel": "Název podpisu",
"telephone": "Telefonní číslo",
"template": "Šablona SMS", "template": "Šablona SMS",
"templateCode": "Kód šablony", "templateCodeLabel": "Kód šablony",
"templateParam": "Parametr šablony", "templateTip": "Prosím vyplňte SMS šablonu, ponechte {code} uprostřed, jinak SMS funkce nebude fungovat",
"templateTip": "Vyplňte prosím šablonu SMS, ponechte {code} uprostřed, jinak funkce SMS nebude fungovat",
"testSms": "Odeslat testovací SMS", "testSms": "Odeslat testovací SMS",
"testSmsContent": "Toto je testovací zpráva",
"testSmsPhone": "Zadejte telefonní číslo", "testSmsPhone": "Zadejte telefonní číslo",
"testSmsTip": "Odeslat testovací SMS pro ověření vaší konfigurace", "testSmsTip": "Odeslat testovací SMS pro ověření vaší konfigurace",
"updateSuccess": "Aktualizace úspěšná" "updateSuccess": "Aktualizace úspěšná",
"whitelistAreaCode": "Kód oblasti v bílém seznamu",
"whitelistAreaCodeTip": "Zadejte kódy oblastí v bílém seznamu, např. 1, 852, 886, 888",
"whitelistValidation": "Ověření bílého seznamu",
"whitelistValidationTip": "Pokud je povoleno, mohou SMS odesílat pouze kódy oblastí v bílém seznamu"
} }
+1
View File
@@ -51,6 +51,7 @@
"subscriptionPath": "Cesta předplatného", "subscriptionPath": "Cesta předplatného",
"subscriptionPathDescription": "Používá se pro předplatné; po úpravě nezapomeňte restartovat systém pro optimální výkon", "subscriptionPathDescription": "Používá se pro předplatné; po úpravě nezapomeňte restartovat systém pro optimální výkon",
"subscriptionPathPlaceholder": "Zadejte", "subscriptionPathPlaceholder": "Zadejte",
"updateSuccess": "Úspěšně aktualizováno",
"wildcardResolution": "Řešení zástupných znaků", "wildcardResolution": "Řešení zástupných znaků",
"wildcardResolutionDescription": "Používá se pro předplatné" "wildcardResolutionDescription": "Používá se pro předplatné"
}, },
+8 -41
View File
@@ -8,17 +8,15 @@
"currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému", "currencyUnitDescription": "Pouze pro zobrazení, po změně se změní všechny měnové jednotky v systému",
"saveSuccess": "Úspěšně uloženo" "saveSuccess": "Úspěšně uloženo"
}, },
"invite": { "privacy-policy": {
"commissionFirstTimeOnly": "Provize pouze při prvním nákupu", "saveSuccess": "Uložení bylo úspěšné",
"commissionFirstTimeOnlyDescription": "Po aktivaci se provize generuje pouze při první platbě pozvaného uživatele. Můžete konfigurovat jednotlivé uživatele ve správě uživatelů.", "title": "Zásady ochrany osobních údajů"
"enableForcedInvite": "Povolit povinné pozvání",
"enableForcedInviteDescription": "Po aktivaci se mohou registrovat pouze pozvaní uživatelé.",
"inputPlaceholder": "Prosím zadejte",
"inviteCommissionPercentage": "Procento provize za pozvání",
"inviteCommissionPercentageDescription": "Výchozí globální procento rozdělení provize, můžete konfigurovat jednotlivé procento ve správě uživatelů.",
"saveSuccess": "Úspěšně uloženo"
}, },
"site": { "site": {
"customHtml": "Vlastní HTML",
"customHtmlDescription": "Vlastní HTML kód, který bude vložen na konec tagu body na stránce.",
"keywords": "Klíčová slova",
"keywordsDescription": "Používá se pro SEO účely",
"logo": "LOGO", "logo": "LOGO",
"logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO", "logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO",
"logoPlaceholder": "Zadejte URL adresu LOGA, nekončete '/'", "logoPlaceholder": "Zadejte URL adresu LOGA, nekončete '/'",
@@ -35,42 +33,11 @@
}, },
"tabs": { "tabs": {
"currency": "Měna", "currency": "Měna",
"invite": "Pozvat",
"site": "Stránka", "site": "Stránka",
"telegram": "Telegram", "tos": "Podmínky služby"
"tos": "Podmínky služby",
"verify": "Ověřit"
},
"telegram": {
"botToken": "Token robota",
"botTokenDescription": "Zadejte token poskytnutý Botfatherem",
"enableBotNotifications": "Povolit oznámení robota",
"enableBotNotificationsDescription": "Po povolení bude robot posílat základní oznámení správcům a uživatelům propojeným s Telegramem",
"groupURL": "URL skupiny",
"groupURLDescription": "Po vyplnění se zobrazí na uživatelské straně nebo tam, kde je potřeba",
"inputPlaceholderBotToken": "0000000000:xxxxxxxxx_xxxxxxxxxxxxxxx",
"inputPlaceholderGroupURL": "https://t.me/xxxxxx",
"inputPlaceholderWebhookDomain": "https://example.com",
"saveSuccess": "Úspěšně uloženo",
"webhookDomain": "Doména webhooku",
"webhookDomainDescription": "Zadejte název domény serveru webhooku"
}, },
"tos": { "tos": {
"saveSuccess": "Uložení bylo úspěšné", "saveSuccess": "Uložení bylo úspěšné",
"title": "Podmínky služby" "title": "Podmínky služby"
},
"verify": {
"inputPlaceholder": "Prosím zadejte",
"loginVerificationCode": "Ověřovací kód pro přihlášení",
"loginVerificationCodeDescription": "Ověření člověka při přihlášení",
"registrationVerificationCode": "Ověřovací kód pro registraci",
"registrationVerificationCodeDescription": "Ověření člověka při registraci",
"resetPasswordVerificationCode": "Ověřovací kód pro resetování hesla",
"resetPasswordVerificationCodeDescription": "Ověření člověka při resetování hesla",
"saveSuccess": "Úspěšně uloženo",
"turnstileSecret": "Turnstile tajný klíč",
"turnstileSecretDescription": "Turnstile tajný klíč poskytovaný Cloudflare",
"turnstileSiteKey": "Turnstile klíč webu",
"turnstileSiteKeyDescription": "Turnstile klíč webu poskytovaný Cloudflare"
} }
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Telegram Bot Token, který můžete získat od @BotFather", "clientSecretDescription": "Telegram Bot Token, který můžete získat od @BotFather",
"enable": "Povolit", "enable": "Povolit",
"enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu", "enableDescription": "Po povolení budou povoleny funkce registrace, přihlášení, připojení a odpojení mobilního telefonu",
"redirectUri": "Přesměrovací URL",
"redirectUriDescription": "URL, na kterou Telegram přesměruje po úspěšném ověření",
"saveFailed": "Uložení se nezdařilo", "saveFailed": "Uložení se nezdařilo",
"saveSuccess": "Uložení bylo úspěšné" "saveSuccess": "Uložení bylo úspěšné"
} }
+83 -25
View File
@@ -1,50 +1,108 @@
{ {
"accountEnable": "Povolení účtu",
"actions": "akce", "actions": "akce",
"add": "Přidat",
"administrator": "Administrátor",
"areaCode": "Směrový kód",
"areaCodePlaceholder": "Kód oblasti",
"auth": "Autentizace",
"authIdentifier": "Identifikátor ověření",
"authIdentifierPlaceholder": "Zadejte identifikátor ověření",
"authMethodsTitle": "Nastavení ověřování",
"authType": "Typ ověření",
"authUser": "Autentizovaný uživatel",
"avatar": "Avatar",
"balance": "Zůstatek", "balance": "Zůstatek",
"balanceNotifications": "Oznámení o změně zůstatku",
"balancePlaceholder": "Zůstatek",
"basicInfoTitle": "Základní informace",
"cancel": "zrušit", "cancel": "zrušit",
"commission": "Provize", "commission": "Provize",
"commissionPlaceholder": "Zadejte provizi",
"confirm": "Potvrdit", "confirm": "Potvrdit",
"confirmDelete": "Opravdu chcete smazat?", "confirmDelete": "Opravdu chcete smazat?",
"confirmOffline": "Potvrdit offline",
"create": "Vytvořit", "create": "Vytvořit",
"createSubscription": "Vytvořit předplatné",
"createSuccess": "Vytvoření úspěšné", "createSuccess": "Vytvoření úspěšné",
"createUser": "Vytvořit uživatele", "createUser": "Vytvořit uživatele",
"createdAt": "Datum registrace", "createdAt": "Datum registrace",
"delete": "smazat", "delete": "smazat",
"deleteDescription": "Po odstranění nelze data obnovit, prosím, postupujte opatrně.", "deleteDescription": "Po odstranění nelze data obnovit, prosím, postupujte opatrně.",
"deleteSubscriptionDescription": "Opravdu chcete smazat toto předplatné?",
"deleteSuccess": "Úspěšně odstraněno", "deleteSuccess": "Úspěšně odstraněno",
"detail": "Detail",
"deviceLimit": "Limit zařízení",
"download": "Stáhnout",
"downloadTraffic": "Stáhnout provoz",
"edit": "Upravit", "edit": "Upravit",
"editSubscription": "Upravit předplatné",
"editUser": "Upravit uživatele", "editUser": "Upravit uživatele",
"email": "e-mail", "email": "e-mail",
"enable": "Povolit", "enable": "Povolit",
"form": { "expireTime": "Čas vypršení",
"areaCode": "Směrové číslo", "expiredAt": "Platnost vypršela",
"areaCodePlaceholder": "Směrové číslo", "failed": "Neúspěch",
"balance": "Zůstatek",
"balancePlaceholder": "Zůstatek",
"cancel": "Zrušit",
"commission": "Provize",
"commissionPlaceholder": "Zadejte provizi",
"confirm": "Potvrdit",
"giftAmount": "Částka daru",
"giftAmountPlaceholder": "Zadejte částku daru",
"invalidEmailFormat": "Neplatný formát e-mailu",
"inviteCode": "Pozvánkový kód",
"inviteCodePlaceholder": "Zadejte pozvánkový kód (ponechte prázdné pro automatické generování)",
"manager": "Správce",
"password": "Heslo",
"passwordPlaceholder": "Zadejte nové heslo (může zůstat prázdné)",
"refererId": "ID doporučitele",
"refererIdPlaceholder": "Zadejte ID doporučitele",
"telephone": "Telefonní číslo",
"telephonePlaceholder": "Zadejte telefonní číslo",
"userEmail": "E-mail uživatele",
"userEmailPlaceholder": "Zadejte e-mail uživatele"
},
"giftAmount": "Částka dárku", "giftAmount": "Částka dárku",
"giftAmountPlaceholder": "Zadejte částku dárku",
"invalidEmailFormat": "Neplatný formát e-mailu",
"inviteCode": "Pozvánkový kód", "inviteCode": "Pozvánkový kód",
"inviteCodePlaceholder": "Zadejte kód pozvánky (ponechte prázdné pro vygenerování)",
"kickOfflineConfirm": "Opravdu chcete odpojit IP {ip}?",
"kickOfflineSuccess": "Úspěšně oznámeno odpojení",
"lastSeen": "Naposledy viděno",
"loading": "Načítání...",
"loginIp": "Přihlašovací IP",
"loginNotifications": "Oznámení o přihlášení",
"loginStatus": "Stav přihlášení",
"loginTime": "Čas přihlášení",
"manager": "Manažer",
"notifySettingsTitle": "Nastavení oznámení",
"onlineDevices": "Online zařízení",
"password": "Heslo",
"passwordPlaceholder": "Zadejte nové heslo (volitelné)",
"permanent": "Trvalý",
"pleaseEnterEmail": "Prosím, zadejte e-mail",
"referer": "Doporučitel", "referer": "Doporučitel",
"refererId": "ID doporučitele",
"refererIdPlaceholder": "Zadejte ID doporučitele",
"referralCode": "Doporučovací kód",
"referrerUserId": "Odesílatel (uživatelské ID)",
"remove": "Odstranit",
"resetTime": "Obnovit čas",
"save": "Uložit",
"searchIp": "Vyhledat IP adresu",
"selectAuthType": "Vyberte typ ověření",
"speedLimit": "Rychlostní limit",
"subscription": "Předplatné",
"subscriptionDetails": "Podrobnosti o předplatném",
"subscriptionList": "Seznam předplatných",
"subscriptionLogs": "Záznamy o předplatném",
"subscriptionName": "Název předplatného",
"subscriptionNotifications": "Oznámení o předplatném",
"success": "Úspěch",
"telephone": "Telefonní číslo", "telephone": "Telefonní číslo",
"telephonePlaceholder": "Zadejte telefonní číslo",
"time": "Čas",
"token": "Token",
"totalTraffic": "Celkový provoz",
"tradeNotifications": "Obchodní oznámení",
"trafficLimit": "Limit provozu",
"trafficLogs": "Protokoly provozu",
"unlimited": "Neomezený",
"unverified": "Neověřeno",
"update": "Aktualizovat",
"updateSuccess": "Aktualizace byla úspěšná", "updateSuccess": "Aktualizace byla úspěšná",
"upload": "Nahrát",
"uploadTraffic": "Nahrát provoz",
"userAgent": "Uživatelský agent",
"userEmail": "Uživatelský e-mail",
"userEmailPlaceholder": "Zadejte e-mail uživatele",
"userList": "Seznam uživatelů", "userList": "Seznam uživatelů",
"userName": "Identifikátor uživatele" "userLogs": "Historie přihlášení",
"userName": "Identifikátor uživatele",
"userOrders": "Objednávky",
"userProfile": "Profil",
"userSubscriptions": "Předplatné",
"verified": "Ověřeno"
} }
+40
View File
@@ -0,0 +1,40 @@
{
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"confirmDelete": "Löschen bestätigen",
"create": "Anzeige erstellen",
"createAds": "Anzeige erstellen",
"createSuccess": "Erfolgreich erstellt",
"delete": "Löschen",
"deleteSuccess": "Erfolgreich gelöscht",
"deleteWarning": "Sind Sie sicher, dass Sie diese Anzeige löschen möchten?",
"disabled": "Deaktiviert",
"edit": "Bearbeiten",
"editAds": "Anzeige bearbeiten",
"enabled": "Aktiviert",
"form": {
"cancel": "Abbrechen",
"confirm": "Bestätigen",
"content": "Inhalt",
"description": "Beschreibung",
"endTime": "Endzeit",
"enterContent": "Bitte geben Sie den Anzeigeninhalt ein",
"enterDescription": "Bitte geben Sie die Anzeigenbeschreibung ein",
"enterEndTime": "Bitte wählen Sie die Endzeit aus",
"enterStartTime": "Bitte wählen Sie die Startzeit aus",
"enterTargetUrl": "Bitte geben Sie die Ziel-URL ein",
"enterTitle": "Bitte geben Sie den Anzeigentitel ein",
"startTime": "Startzeit",
"targetUrl": "Ziel-URL",
"title": "Titel",
"type": "Anzeigentyp",
"typeImage": "Bild",
"typeVideo": "Video"
},
"status": "Status",
"targetUrl": "Ziel-URL",
"title": "Titel",
"type": "Typ",
"updateSuccess": "Erfolgreich aktualisiert",
"validityPeriod": "Gültigkeitsdauer"
}
+2 -2
View File
@@ -7,8 +7,8 @@
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrer Apple-ID anmelden", "enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrer Apple-ID anmelden",
"keyId": "Schlüssel-ID", "keyId": "Schlüssel-ID",
"keyIdDescription": "Die ID Ihres privaten Schlüssels aus dem Apple Developer Portal", "keyIdDescription": "Die ID Ihres privaten Schlüssels aus dem Apple Developer Portal",
"redirectUri": "Rückkehr-URL", "redirectUri": "Weiterleitungs-URL",
"redirectUriDescription": "Die URL, zu der Apple nach erfolgreicher Authentifizierung weiterleitet", "redirectUriDescription": "Bitte geben Sie die API-Adresse der umgeleiteten URL ein, nachdem die Apple-Authentifizierung erfolgreich abgeschlossen wurde. Verwenden Sie kein / am Ende.",
"saveFailed": "Speichern fehlgeschlagen", "saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert", "saveSuccess": "Erfolgreich gespeichert",
"teamId": "Team-ID", "teamId": "Team-ID",
+72 -11
View File
@@ -1,13 +1,74 @@
{ {
"ipRegistrationLimit": "IP-Registrierungsgrenze", "invite": {
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die die Regelanforderungen erfüllen, von der Registrierung ausgeschlossen; beachten Sie, dass die Bestimmung der IPs aufgrund von CDNs oder Frontend-Proxys Probleme verursachen kann", "commissionFirstTimeOnly": "Provision nur beim ersten Kauf",
"penaltyTime": "Strafzeit (Minuten)", "commissionFirstTimeOnlyDescription": "Wenn aktiviert, wird die Provision nur bei der ersten Zahlung des Einladers generiert; individuelle Benutzer können in der Benutzerverwaltung konfiguriert werden",
"penaltyTimeDescription": "Benutzer müssen warten, bis die Strafzeit abgelaufen ist, bevor sie sich erneut registrieren können", "enableForcedInvite": "Erzwinge Einladung",
"registrationLimitCount": "Anzahl der Anmeldungen", "enableForcedInviteDescription": "Wenn aktiviert, können sich nur eingeladene Benutzer registrieren",
"registrationLimitCountDescription": "Strafe aktivieren, nachdem das Registrierungslimit erreicht wurde", "inputPlaceholder": "Eingeben",
"saveSuccess": "Erfolgreich gespeichert", "inviteCommissionPercentage": "Einladungsprovisionssatz",
"stopNewUserRegistration": "Neue Benutzerregistrierung stoppen", "inviteCommissionPercentageDescription": "Standardmäßiges globales Provisionsverteilungsschema; individuelle Sätze können in der Benutzerverwaltung konfiguriert werden",
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren", "inviteSettings": "Einladungseinstellungen",
"trialRegistration": "Studienregistrierung", "saveSuccess": "Erfolgreich gespeichert"
"trialRegistrationDescription": "Testregistrierung aktivieren; zuerst Testpaket und -dauer anpassen" },
"log": {
"content": "Inhalt",
"createdAt": "Erstellt am",
"emailLog": "E-Mail-Protokoll",
"mobileLog": "Mobilprotokoll",
"platform": "Plattform",
"sendFailed": "Fehlgeschlagen",
"sendSuccess": "Erfolgreich",
"status": "Status",
"subject": "Betreff",
"to": "Empfänger",
"updatedAt": "Aktualisiert am"
},
"register": {
"day": "Tag",
"hour": "Stunde",
"ipRegistrationLimit": "IP-Registrierungsgrenze",
"ipRegistrationLimitDescription": "Wenn aktiviert, werden IPs, die die Regelanforderungen erfüllen, von der Registrierung ausgeschlossen; beachten Sie, dass die IP-Bestimmung aufgrund von CDNs oder Frontend-Proxys Probleme verursachen kann",
"minute": "Minute",
"month": "Monat",
"noLimit": "Keine Begrenzung",
"penaltyTime": "Strafzeit (Minuten)",
"penaltyTimeDescription": "Benutzer müssen warten, bis die Strafzeit abgelaufen ist, bevor sie sich erneut registrieren",
"registerSettings": "Registrierungseinstellungen",
"registrationLimitCount": "Registrierungsgrenzanzahl",
"registrationLimitCountDescription": "Strafe aktivieren, nachdem die Registrierungsgrenze erreicht wurde",
"saveSuccess": "Erfolgreich gespeichert",
"stopNewUserRegistration": "Neue Benutzerregistrierung stoppen",
"stopNewUserRegistrationDescription": "Wenn aktiviert, kann sich niemand registrieren",
"trialDuration": "Testdauer",
"trialRegistration": "Testregistrierung",
"trialRegistrationDescription": "Testregistrierung aktivieren; Testpaket und -dauer zuerst ändern",
"trialSubscribePlan": "Testabonnement-Plan",
"trialSubscribePlanDescription": "Wählen Sie einen Testabonnement-Plan aus",
"year": "Jahr"
},
"verify": {
"inputPlaceholder": "Eingeben",
"loginVerificationCode": "Anmeldebestätigungscode",
"loginVerificationCodeDescription": "Menschliche Überprüfung während der Anmeldung",
"registrationVerificationCode": "Registrierungsbestätigungscode",
"registrationVerificationCodeDescription": "Menschliche Überprüfung während der Registrierung",
"resetPasswordVerificationCode": "Bestätigungscode zum Zurücksetzen des Passworts",
"resetPasswordVerificationCodeDescription": "Menschliche Überprüfung während der Passwortzurücksetzung",
"saveSuccess": "Erfolgreich gespeichert",
"turnstileSecretDescription": "Turnstile-Geheimschlüssel, bereitgestellt von Cloudflare",
"turnstileSiteKeyDescription": "Turnstile-Seitenschlüssel, bereitgestellt von Cloudflare",
"verifySettings": "Überprüfungseinstellungen"
},
"verify-code": {
"dailyLimit": "Tägliches Limit",
"dailyLimitDescription": "Maximale Anzahl von Bestätigungscodes, die pro Tag gesendet werden können",
"expireTime": "Ablaufzeit",
"expireTimeDescription": "Ablaufzeit des Bestätigungscodes (Sekunden)",
"interval": "Sendeintervall",
"intervalDescription": "Minimales Intervall zwischen dem Versenden von Bestätigungscodes (Sekunden)",
"saveSuccess": "Erfolgreich gespeichert",
"second": "Sekunden",
"times": "Mal",
"verifyCodeSettings": "Einstellungen für den Bestätigungscode"
}
} }
+1 -1
View File
@@ -20,7 +20,7 @@
"logout": "Abmelden", "logout": "Abmelden",
"methods": { "methods": {
"email": "E-Mail", "email": "E-Mail",
"sms": "Telefon" "mobile": "Telefon"
}, },
"orWithEmail": "oder mit E-Mail", "orWithEmail": "oder mit E-Mail",
"register": { "register": {
+2
View File
@@ -45,9 +45,11 @@
"60002": "Das Abonnement kann derzeit nicht verwendet werden, bitte versuchen Sie es später erneut.", "60002": "Das Abonnement kann derzeit nicht verwendet werden, bitte versuchen Sie es später erneut.",
"60003": "Ein bestehendes Abonnement wurde erkannt. Bitte kündigen Sie es, bevor Sie fortfahren.", "60003": "Ein bestehendes Abonnement wurde erkannt. Bitte kündigen Sie es, bevor Sie fortfahren.",
"60004": "Kann derzeit nicht gelöscht werden, da das Abonnement aktive Benutzer hat.", "60004": "Kann derzeit nicht gelöscht werden, da das Abonnement aktive Benutzer hat.",
"60005": "Im Single-Abo-Modus wurde das Benutzerlimit überschritten",
"70001": "Der Bestätigungscode ist falsch, bitte erneut eingeben.", "70001": "Der Bestätigungscode ist falsch, bitte erneut eingeben.",
"80001": "Die Aufgabe wurde nicht erfolgreich in die Warteschlange aufgenommen, bitte versuchen Sie es später erneut.", "80001": "Die Aufgabe wurde nicht erfolgreich in die Warteschlange aufgenommen, bitte versuchen Sie es später erneut.",
"90001": "Bitte deaktivieren Sie den DEBUG-Modus und versuchen Sie es erneut.", "90001": "Bitte deaktivieren Sie den DEBUG-Modus und versuchen Sie es erneut.",
"90015": "Dieses Konto hat das tägliche Sendelimit erreicht. Bitte versuchen Sie es morgen erneut",
"undefined": "Es ist ein Systemfehler aufgetreten, bitte versuchen Sie es später erneut." "undefined": "Es ist ein Systemfehler aufgetreten, bitte versuchen Sie es später erneut."
}, },
"table": { "table": {
+14
View File
@@ -0,0 +1,14 @@
{
"blockVirtualMachine": "Virtuelle Maschine blockieren",
"blockVirtualMachineDescription": "Wenn aktiviert, wird verhindert, dass Geräte auf virtuellen Maschinen oder Emulatoren ausgeführt werden.",
"communicationKey": "Kommunikationsschlüssel",
"communicationKeyDescription": "Der Kommunikationsschlüssel wird für die sichere Kommunikation zwischen Geräten und Servern verwendet.",
"enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden mehrere Geräteidentifikatoren wie IMEI/IDFA/IDFV/AndroidID/Mac-Adresse für die Anmeldung und Registrierung unterstützt.",
"enableSecurity": "Kommunikationsverschlüsselung aktivieren",
"enableSecurityDescription": "Wenn aktiviert, wird die Kommunikation zwischen Geräten und Servern verschlüsselt.",
"saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Speichern erfolgreich",
"showAds": "Werbung anzeigen",
"showAdsDescription": "Wenn aktiviert, werden auf den Geräten Anzeigen angezeigt."
}
+4 -4
View File
@@ -1,19 +1,17 @@
{ {
"emailBasicConfig": "Grundkonfiguration",
"emailBasicConfigDescription": "Konfigurieren Sie die SMTP-Servereinstellungen und E-Mail-Verifizierungsoptionen", "emailBasicConfigDescription": "Konfigurieren Sie die SMTP-Servereinstellungen und E-Mail-Verifizierungsoptionen",
"emailLogs": "E-Mail-Protokolle",
"emailLogsDescription": "Sehen Sie sich den Verlauf gesendeter E-Mails und deren Status an", "emailLogsDescription": "Sehen Sie sich den Verlauf gesendeter E-Mails und deren Status an",
"emailSuffixWhitelist": "E-Mail-Suffix-Whitelist", "emailSuffixWhitelist": "E-Mail-Suffix-Whitelist",
"emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit Suffixen aus der Liste registrieren", "emailSuffixWhitelistDescription": "Wenn aktiviert, können sich nur E-Mails mit Suffixen aus der Liste registrieren",
"emailTemplate": "E-Mail-Vorlage",
"emailVerification": "E-Mail-Verifizierung", "emailVerification": "E-Mail-Verifizierung",
"emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail-Adresse verifizieren", "emailVerificationDescription": "Wenn aktiviert, müssen Benutzer ihre E-Mail-Adresse verifizieren",
"enable": "Aktivieren", "enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert", "enableDescription": "Nach der Aktivierung werden die Funktionen zur E-Mail-Registrierung, Anmeldung, Bindung und Entbindung aktiviert.",
"expiration_email_template": "Vorlage für Ablaufbenachrichtigung", "expiration_email_template": "Vorlage für Ablaufbenachrichtigung",
"expiration_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.", "expiration_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
"failed": "Fehlgeschlagen", "failed": "Fehlgeschlagen",
"inputPlaceholder": "Wert eingeben...", "inputPlaceholder": "Wert eingeben...",
"logs": "Protokolle",
"maintenance_email_template": "Wartungshinweis Vorlage", "maintenance_email_template": "Wartungshinweis Vorlage",
"maintenance_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.", "maintenance_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass Sie diese Variablen beibehalten.",
"recipient": "Empfänger", "recipient": "Empfänger",
@@ -27,6 +25,7 @@
"senderAddressDescription": "Die standardmäßige E-Mail-Adresse, die zum Versenden von E-Mails verwendet wird.", "senderAddressDescription": "Die standardmäßige E-Mail-Adresse, die zum Versenden von E-Mails verwendet wird.",
"sent": "Gesendet", "sent": "Gesendet",
"sentAt": "Gesendet am", "sentAt": "Gesendet am",
"settings": "Einstellungen",
"smtpAccount": "SMTP-Konto", "smtpAccount": "SMTP-Konto",
"smtpAccountDescription": "Das E-Mail-Konto, das für die Authentifizierung verwendet wird.", "smtpAccountDescription": "Das E-Mail-Konto, das für die Authentifizierung verwendet wird.",
"smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode", "smtpEncryptionMethod": "SMTP-Verschlüsselungsmethode",
@@ -39,6 +38,7 @@
"smtpServerPortDescription": "Geben Sie den Port an, der für die Verbindung zum SMTP-Server verwendet wird.", "smtpServerPortDescription": "Geben Sie den Port an, der für die Verbindung zum SMTP-Server verwendet wird.",
"status": "Status", "status": "Status",
"subject": "Betreff", "subject": "Betreff",
"template": "Vorlage",
"verify_email_template": "E-Mail-Bestätigungsvorlage", "verify_email_template": "E-Mail-Bestätigungsvorlage",
"verify_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass diese Variablen beibehalten werden.", "verify_email_templateDescription": "Die Platzhalter {after}.variable{before} werden durch tatsächliche Daten ersetzt. Stellen Sie sicher, dass diese Variablen beibehalten werden.",
"whitelistSuffixes": "Whitelist-Suffixe", "whitelistSuffixes": "Whitelist-Suffixe",
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Facebook-App-Geheimnis aus der Facebook-Entwicklerkonsole", "clientSecretDescription": "Facebook-App-Geheimnis aus der Facebook-Entwicklerkonsole",
"enable": "Aktivieren", "enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Facebook-Konto anmelden", "enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Facebook-Konto anmelden",
"redirectUri": "Autorisierte Umleitungs-URI",
"redirectUriDescription": "Die URL, zu der der Benutzer nach der Facebook-Authentifizierung weitergeleitet wird",
"saveFailed": "Speichern fehlgeschlagen", "saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert" "saveSuccess": "Erfolgreich gespeichert"
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Das Client-Geheimnis aus den Einstellungen Ihrer GitHub-OAuth-Anwendung", "clientSecretDescription": "Das Client-Geheimnis aus den Einstellungen Ihrer GitHub-OAuth-Anwendung",
"enable": "GitHub-Authentifizierung aktivieren", "enable": "GitHub-Authentifizierung aktivieren",
"enableDescription": "Ermöglichen Sie Benutzern, sich mit ihren GitHub-Konten anzumelden", "enableDescription": "Ermöglichen Sie Benutzern, sich mit ihren GitHub-Konten anzumelden",
"redirectUri": "Autorisierungs-Callback-URL",
"redirectUriDescription": "Die URL in Ihrer Anwendung, zu der Benutzer nach der GitHub-Authentifizierung weitergeleitet werden",
"saveFailed": "Speichern der GitHub-Einstellungen fehlgeschlagen", "saveFailed": "Speichern der GitHub-Einstellungen fehlgeschlagen",
"saveSuccess": "GitHub-Einstellungen erfolgreich gespeichert" "saveSuccess": "GitHub-Einstellungen erfolgreich gespeichert"
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Google OAuth 2.0-Client-Geheimnis aus der Google Cloud Console", "clientSecretDescription": "Google OAuth 2.0-Client-Geheimnis aus der Google Cloud Console",
"enable": "Aktivieren", "enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Google-Konto anmelden", "enableDescription": "Nach der Aktivierung können sich Benutzer mit ihrem Google-Konto anmelden",
"redirectUri": "Autorisierte Umleitungs-URI",
"redirectUriDescription": "Die URL, zu der der Benutzer nach der Google-Authentifizierung weitergeleitet wird",
"saveFailed": "Speichern fehlgeschlagen", "saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert" "saveSuccess": "Erfolgreich gespeichert"
} }
-25
View File
@@ -1,25 +0,0 @@
{
"cs-CZ": "Tschechisch",
"de-DE": "Deutsch",
"en-US": "Englisch",
"es-ES": "Spanisch",
"es-MX": "Spanisch (Mexiko)",
"fa-IR": "Persisch",
"fi-FI": "Finnisch",
"fr-FR": "Französisch",
"hi-IN": "Hindi",
"hu-HU": "Ungarisch",
"ja-JP": "Japanisch",
"ko-KR": "Koreanisch",
"no-NO": "Norwegisch",
"pl-PL": "Polnisch",
"pt-BR": "Portugiesisch (Brasilien)",
"ro-RO": "Rumänisch",
"ru-RU": "Russisch",
"th-TH": "Thailändisch",
"tr-TR": "Türkisch",
"uk-UA": "Ukrainisch",
"vi-VN": "Vietnamesisch",
"zh-CN": "Chinesisch (vereinfacht)",
"zh-HK": "Chinesisch (Traditionell)"
}
+3
View File
@@ -1,9 +1,11 @@
{ {
"ADS Config": "ADS-Konfiguration",
"Announcement Management": "Ankündigungsverwaltung", "Announcement Management": "Ankündigungsverwaltung",
"Apple": "Apple-ID", "Apple": "Apple-ID",
"Auth Control": "Authentifizierungskontrolle", "Auth Control": "Authentifizierungskontrolle",
"Coupon Management": "Gutscheinverwaltung", "Coupon Management": "Gutscheinverwaltung",
"Dashboard": "Armaturenbrett", "Dashboard": "Armaturenbrett",
"Device": "Gerät",
"Document Management": "Dokumentenverwaltung", "Document Management": "Dokumentenverwaltung",
"Email": "E-Mail", "Email": "E-Mail",
"Facebook": "Facebook", "Facebook": "Facebook",
@@ -24,5 +26,6 @@
"Ticket Management": "Ticketverwaltung", "Ticket Management": "Ticketverwaltung",
"Twitter": "Twitter", "Twitter": "Twitter",
"User": "Benutzer", "User": "Benutzer",
"User Detail": "Benutzerdetails",
"User Management": "Benutzerverwaltung" "User Management": "Benutzerverwaltung"
} }
+12 -21
View File
@@ -1,41 +1,32 @@
{ {
"SmsList": "SMS-Protokollliste", "accessLabel": "Zugriff",
"applyPlatform": "Plattform anwenden", "applyPlatform": "Plattform anwenden",
"areaCode": "Vorwahl",
"content": "Inhalt",
"createdAt": "Sendezeit",
"enable": "Aktivieren", "enable": "Aktivieren",
"enableTip": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert", "enableTip": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"expireTime": "Ablaufzeit", "endpointLabel": "Endpunkt",
"expireTimeTip": "Gültigkeitsdauer des SMS-Bestätigungscodes (Sekunden)",
"interval": "Intervall",
"intervalTip": "SMS-Bestätigungscode-Sendeintervall für dieselbe Telefonnummer (Sekunden), 0 bedeutet keine Begrenzung",
"limit": "Tageslimit",
"limitTip": "Tägliches Limit für das Senden von SMS-Verifizierungscodes an dieselbe Telefonnummer, 0 bedeutet kein Limit",
"logs": "Protokolle", "logs": "Protokolle",
"phoneNumberLabel": "Telefonnummer",
"placeholders": { "placeholders": {
"expireTime": "Geben Sie die Ablaufzeit ein, Standard ist 300",
"interval": "Geben Sie das Intervall ein, Standard ist 60",
"limit": "Geben Sie das tägliche Limit ein, Standard ist 20",
"template": "Ihr Bestätigungscode ist {code}, gültig für 5 Minuten", "template": "Ihr Bestätigungscode ist {code}, gültig für 5 Minuten",
"templateCode": "Geben Sie den Vorlagen-Code ein" "templateCode": "Geben Sie den Vorlagen-Code ein"
}, },
"platform": "SMS-Plattform", "platform": "SMS-Plattform",
"platformConfigTip": "Bitte füllen Sie die bereitgestellte {key}-Konfiguration aus", "platformConfigTip": "Bitte füllen Sie die bereitgestellte {key}-Konfiguration aus",
"platformTip": "Bitte wählen Sie die SMS-Plattform aus", "platformTip": "Bitte wählen Sie die SMS-Plattform aus",
"search": "Telefonnummer suchen", "secretLabel": "Geheimnis",
"sendFailed": "Senden fehlgeschlagen", "sendFailed": "Senden fehlgeschlagen",
"sendSuccess": "Erfolgreich gesendet", "sendSuccess": "Erfolgreich gesendet",
"settings": "Einstellungen", "settings": "Einstellungen",
"status": "Status", "signNameLabel": "Signaturname",
"telephone": "Telefonnummer",
"template": "SMS-Vorlage", "template": "SMS-Vorlage",
"templateCode": "Vorlagen-Code", "templateCodeLabel": "Vorlagen-Code",
"templateParam": "Vorlagenparameter", "templateTip": "Bitte füllen Sie die SMS-Vorlage aus, lassen Sie {code} in der Mitte, andernfalls funktioniert die SMS-Funktion nicht.",
"templateTip": "Bitte füllen Sie die SMS-Vorlage aus und lassen Sie {code} in der Mitte stehen, da sonst die SMS-Funktion nicht funktioniert.",
"testSms": "Test-SMS senden", "testSms": "Test-SMS senden",
"testSmsContent": "Dies ist eine Testnachricht",
"testSmsPhone": "Telefonnummer eingeben", "testSmsPhone": "Telefonnummer eingeben",
"testSmsTip": "Senden Sie eine Test-SMS, um Ihre Konfiguration zu überprüfen", "testSmsTip": "Senden Sie eine Test-SMS, um Ihre Konfiguration zu überprüfen",
"updateSuccess": "Aktualisierung erfolgreich" "updateSuccess": "Aktualisierung erfolgreich",
"whitelistAreaCode": "Whitelist Bereichscode",
"whitelistAreaCodeTip": "Bitte geben Sie die Whitelist Bereichscodes ein, z.B. 1, 852, 886, 888",
"whitelistValidation": "Whitelist Überprüfung",
"whitelistValidationTip": "Wenn aktiviert, können nur Bereichscodes in der Whitelist SMS senden"
} }
+1
View File
@@ -51,6 +51,7 @@
"subscriptionPath": "Abonnement-Pfad", "subscriptionPath": "Abonnement-Pfad",
"subscriptionPathDescription": "Wird für Abonnements verwendet; starten Sie das System nach der Änderung neu, um die optimale Leistung zu gewährleisten", "subscriptionPathDescription": "Wird für Abonnements verwendet; starten Sie das System nach der Änderung neu, um die optimale Leistung zu gewährleisten",
"subscriptionPathPlaceholder": "Eingeben", "subscriptionPathPlaceholder": "Eingeben",
"updateSuccess": "Erfolgreich aktualisiert",
"wildcardResolution": "Wildcard-Auflösung", "wildcardResolution": "Wildcard-Auflösung",
"wildcardResolutionDescription": "Wird für Abonnements verwendet" "wildcardResolutionDescription": "Wird für Abonnements verwendet"
}, },
+8 -41
View File
@@ -8,17 +8,15 @@
"currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert", "currencyUnitDescription": "Nur zur Anzeige verwendet, nach der Änderung werden alle Währungseinheiten im System geändert",
"saveSuccess": "Erfolgreich gespeichert" "saveSuccess": "Erfolgreich gespeichert"
}, },
"invite": { "privacy-policy": {
"commissionFirstTimeOnly": "Nur Provision beim ersten Kauf", "saveSuccess": "Erfolgreich gespeichert",
"commissionFirstTimeOnlyDescription": "Nach Aktivierung wird die Provision nur bei der ersten Zahlung des Eingeladenen generiert. Sie können in der Benutzerverwaltung einen individuellen Benutzer konfigurieren.", "title": "Datenschutzrichtlinie"
"enableForcedInvite": "Erzwungene Einladung aktivieren",
"enableForcedInviteDescription": "Nach Aktivierung können sich nur eingeladene Benutzer registrieren.",
"inputPlaceholder": "Bitte eingeben",
"inviteCommissionPercentage": "Einladungsprovisionsprozentsatz",
"inviteCommissionPercentageDescription": "Standardmäßiger globaler Provisionsverteilungssatz. Sie können in der Benutzerverwaltung einen individuellen Satz konfigurieren.",
"saveSuccess": "Erfolgreich gespeichert"
}, },
"site": { "site": {
"customHtml": "Benutzerdefiniertes HTML",
"customHtmlDescription": "Benutzerdefinierter HTML-Code, der am Ende des body-Tags der Seite eingefügt werden soll.",
"keywords": "Schlüsselwörter",
"keywordsDescription": "Für SEO-Zwecke verwendet",
"logo": "LOGO", "logo": "LOGO",
"logoDescription": "Position zur Anzeige des LOGOs", "logoDescription": "Position zur Anzeige des LOGOs",
"logoPlaceholder": "Bitte geben Sie die URL-Adresse des LOGOs ein, ohne '/' am Ende", "logoPlaceholder": "Bitte geben Sie die URL-Adresse des LOGOs ein, ohne '/' am Ende",
@@ -35,42 +33,11 @@
}, },
"tabs": { "tabs": {
"currency": "Währung", "currency": "Währung",
"invite": "Einladen",
"site": "Seite", "site": "Seite",
"telegram": "Telegram", "tos": "Nutzungsbedingungen"
"tos": "Nutzungsbedingungen",
"verify": "Verifizieren"
},
"telegram": {
"botToken": "Bot-Token",
"botTokenDescription": "Bitte geben Sie das von Botfather bereitgestellte Token ein",
"enableBotNotifications": "Bot-Benachrichtigungen aktivieren",
"enableBotNotificationsDescription": "Nach Aktivierung sendet der Bot grundlegende Benachrichtigungen an Administratoren und Benutzer, die mit Telegram verbunden sind",
"groupURL": "Gruppen-URL",
"groupURLDescription": "Nach Eingabe wird sie auf der Benutzerseite angezeigt oder dort verwendet, wo sie benötigt wird",
"inputPlaceholderBotToken": "0000000000:xxxxxxxxx_xxxxxxxxxxxxxxx",
"inputPlaceholderGroupURL": "https://t.me/xxxxxx",
"inputPlaceholderWebhookDomain": "https://example.com",
"saveSuccess": "Erfolgreich gespeichert",
"webhookDomain": "Webhook-Domain",
"webhookDomainDescription": "Geben Sie den Domainnamen des Webhook-Servers ein"
}, },
"tos": { "tos": {
"saveSuccess": "Erfolgreich gespeichert", "saveSuccess": "Erfolgreich gespeichert",
"title": "Nutzungsbedingungen" "title": "Nutzungsbedingungen"
},
"verify": {
"inputPlaceholder": "Bitte eingeben",
"loginVerificationCode": "Anmeldebestätigungscode",
"loginVerificationCodeDescription": "Mensch-Maschine-Verifizierung bei der Anmeldung",
"registrationVerificationCode": "Registrierungsbestätigungscode",
"registrationVerificationCodeDescription": "Mensch-Maschine-Verifizierung bei der Registrierung",
"resetPasswordVerificationCode": "Passwort-zurücksetzen-Bestätigungscode",
"resetPasswordVerificationCodeDescription": "Mensch-Maschine-Verifizierung beim Zurücksetzen des Passworts",
"saveSuccess": "Erfolgreich gespeichert",
"turnstileSecret": "Turnstile-Schlüssel",
"turnstileSecretDescription": "Von Cloudflare bereitgestellter Turnstile-Schlüssel",
"turnstileSiteKey": "Turnstile-Standortschlüssel",
"turnstileSiteKeyDescription": "Von Cloudflare bereitgestellter Turnstile-Standortschlüssel"
} }
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Telegram-Bot-Token, den Sie von @BotFather erhalten können", "clientSecretDescription": "Telegram-Bot-Token, den Sie von @BotFather erhalten können",
"enable": "Aktivieren", "enable": "Aktivieren",
"enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert", "enableDescription": "Nach der Aktivierung werden die Funktionen zur Registrierung, Anmeldung, Bindung und Entbindung von Mobiltelefonen aktiviert",
"redirectUri": "Weiterleitungs-URL",
"redirectUriDescription": "Die URL, zu der Telegram nach erfolgreicher Authentifizierung weiterleitet",
"saveFailed": "Speichern fehlgeschlagen", "saveFailed": "Speichern fehlgeschlagen",
"saveSuccess": "Erfolgreich gespeichert" "saveSuccess": "Erfolgreich gespeichert"
} }
+83 -25
View File
@@ -1,50 +1,108 @@
{ {
"accountEnable": "Konto aktivieren",
"actions": "Aktionen", "actions": "Aktionen",
"add": "Hinzufügen",
"administrator": "Administrator",
"areaCode": "Vorwahl",
"areaCodePlaceholder": "Vorwahl",
"auth": "Authentifizierung",
"authIdentifier": "Authentifizierungskennung",
"authIdentifierPlaceholder": "Geben Sie die Authentifizierungskennung ein",
"authMethodsTitle": "Authentifizierungseinstellungen",
"authType": "Authentifizierungstyp",
"authUser": "Authentifizierter Benutzer",
"avatar": "Avatar",
"balance": "Kontostand", "balance": "Kontostand",
"balanceNotifications": "Benachrichtigungen über Kontostandsänderungen",
"balancePlaceholder": "Kontostand",
"basicInfoTitle": "Grundinformationen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"commission": "Provision", "commission": "Provision",
"commissionPlaceholder": "Provision eingeben",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"confirmDelete": "Sind Sie sicher, dass Sie löschen möchten?", "confirmDelete": "Sind Sie sicher, dass Sie löschen möchten?",
"confirmOffline": "Offline bestätigen",
"create": "Erstellen", "create": "Erstellen",
"createSubscription": "Abonnement erstellen",
"createSuccess": "Erstellung erfolgreich", "createSuccess": "Erstellung erfolgreich",
"createUser": "Benutzer erstellen", "createUser": "Benutzer erstellen",
"createdAt": "Registrierungsdatum", "createdAt": "Registrierungsdatum",
"delete": "Löschen", "delete": "Löschen",
"deleteDescription": "Nach dem Löschen können die Daten nicht wiederhergestellt werden. Bitte gehen Sie vorsichtig vor.", "deleteDescription": "Nach dem Löschen können die Daten nicht wiederhergestellt werden. Bitte gehen Sie vorsichtig vor.",
"deleteSubscriptionDescription": "Sind Sie sicher, dass Sie dieses Abonnement löschen möchten?",
"deleteSuccess": "Erfolgreich gelöscht", "deleteSuccess": "Erfolgreich gelöscht",
"detail": "Detail",
"deviceLimit": "Gerätelimit",
"download": "Herunterladen",
"downloadTraffic": "Download-Verkehr",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"editSubscription": "Abonnement bearbeiten",
"editUser": "Benutzer bearbeiten", "editUser": "Benutzer bearbeiten",
"email": "E-Mail", "email": "E-Mail",
"enable": "Aktivieren", "enable": "Aktivieren",
"form": { "expireTime": "Ablaufzeit",
"areaCode": "Vorwahl", "expiredAt": "Abgelaufen am",
"areaCodePlaceholder": "Vorwahl eingeben", "failed": "Fehlgeschlagen",
"balance": "Kontostand",
"balancePlaceholder": "Kontostand",
"cancel": "Abbrechen",
"commission": "Provision",
"commissionPlaceholder": "Geben Sie die Provision ein",
"confirm": "Bestätigen",
"giftAmount": "Geschenkbetrag",
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
"invalidEmailFormat": "Ungültiges E-Mail-Format",
"inviteCode": "Einladungscode",
"inviteCodePlaceholder": "Bitte Einladungscode eingeben (leer lassen für automatische Generierung)",
"manager": "Administrator",
"password": "Passwort",
"passwordPlaceholder": "Bitte neues Passwort eingeben (kann leer bleiben)",
"refererId": "Empfehler-ID",
"refererIdPlaceholder": "Bitte Empfehler-ID eingeben",
"telephone": "Telefonnummer",
"telephonePlaceholder": "Telefonnummer eingeben",
"userEmail": "Benutzer-E-Mail",
"userEmailPlaceholder": "Bitte Benutzer-E-Mail eingeben"
},
"giftAmount": "Geschenkbetrag", "giftAmount": "Geschenkbetrag",
"giftAmountPlaceholder": "Geben Sie den Geschenkbetrag ein",
"invalidEmailFormat": "Ungültiges E-Mail-Format",
"inviteCode": "Einladungscode", "inviteCode": "Einladungscode",
"inviteCodePlaceholder": "Einladungscode eingeben (leer lassen, um zu generieren)",
"kickOfflineConfirm": "Sind Sie sicher, dass Sie die IP {ip} abmelden möchten?",
"kickOfflineSuccess": "Erfolgreich offline benachrichtigt",
"lastSeen": "Zuletzt gesehen",
"loading": "Laden...",
"loginIp": "Login-IP",
"loginNotifications": "Anmeldebenachrichtigungen",
"loginStatus": "Anmeldestatus",
"loginTime": "Anmeldezeit",
"manager": "Manager",
"notifySettingsTitle": "Benachrichtigungseinstellungen",
"onlineDevices": "Online-Geräte",
"password": "Passwort",
"passwordPlaceholder": "Neues Passwort eingeben (optional)",
"permanent": "Dauerhaft",
"pleaseEnterEmail": "Bitte E-Mail eingeben",
"referer": "Empfehler", "referer": "Empfehler",
"refererId": "Referenz-ID",
"refererIdPlaceholder": "Geben Sie die Referenz-ID ein",
"referralCode": "Empfehlungscode",
"referrerUserId": "Empfehler (Benutzer-ID)",
"remove": "Entfernen",
"resetTime": "Zeit zurücksetzen",
"save": "Speichern",
"searchIp": "IP-Adresse suchen",
"selectAuthType": "Authentifizierungstyp auswählen",
"speedLimit": "Geschwindigkeitsbegrenzung",
"subscription": "Abonnement",
"subscriptionDetails": "Abonnementdetails",
"subscriptionList": "Abonnementsliste",
"subscriptionLogs": "Abonnementprotokolle",
"subscriptionName": "Abonnementname",
"subscriptionNotifications": "Abonnement-Benachrichtigungen",
"success": "Erfolg",
"telephone": "Telefonnummer", "telephone": "Telefonnummer",
"telephonePlaceholder": "Telefonnummer eingeben",
"time": "Zeit",
"token": "Token",
"totalTraffic": "Gesamtverkehr",
"tradeNotifications": "Handelsbenachrichtigungen",
"trafficLimit": "Verkehrslimit",
"trafficLogs": "Verkehrsprotokolle",
"unlimited": "Unbegrenzt",
"unverified": "Unbestätigt",
"update": "Aktualisieren",
"updateSuccess": "Aktualisierung erfolgreich", "updateSuccess": "Aktualisierung erfolgreich",
"upload": "Hochladen",
"uploadTraffic": "Datenverkehr hochladen",
"userAgent": "Benutzeragent",
"userEmail": "Benutzer-E-Mail",
"userEmailPlaceholder": "Benutzer-E-Mail eingeben",
"userList": "Benutzerliste", "userList": "Benutzerliste",
"userName": "Benutzerkennung" "userLogs": "Anmeldeverlauf",
"userName": "Benutzerkennung",
"userOrders": "Bestellungen",
"userProfile": "Profil",
"userSubscriptions": "Abonnements",
"verified": "Verifiziert"
} }
+40
View File
@@ -0,0 +1,40 @@
{
"cancel": "Cancel",
"confirm": "Confirm",
"confirmDelete": "Confirm Delete",
"create": "Create Ad",
"createAds": "Create Advertisement",
"createSuccess": "Successfully created",
"delete": "Delete",
"deleteSuccess": "Successfully deleted",
"deleteWarning": "Are you sure you want to delete this ad?",
"disabled": "Disabled",
"edit": "Edit",
"editAds": "Edit Advertisement",
"enabled": "Enabled",
"form": {
"cancel": "Cancel",
"confirm": "Confirm",
"content": "Content",
"description": "Description",
"endTime": "End Time",
"enterContent": "Please enter ad content",
"enterDescription": "Please enter ad description",
"enterEndTime": "Please select end time",
"enterStartTime": "Please select start time",
"enterTargetUrl": "Please enter target URL",
"enterTitle": "Please enter ad title",
"startTime": "Start Time",
"targetUrl": "Target URL",
"title": "Title",
"type": "Ad Type",
"typeImage": "Image",
"typeVideo": "Video"
},
"status": "Status",
"targetUrl": "Target URL",
"title": "Title",
"type": "Type",
"updateSuccess": "Successfully updated",
"validityPeriod": "Validity Period"
}
+2 -2
View File
@@ -7,8 +7,8 @@
"enableDescription": "After enabling, users can sign in with their Apple ID", "enableDescription": "After enabling, users can sign in with their Apple ID",
"keyId": "Key ID", "keyId": "Key ID",
"keyIdDescription": "The ID of your private key from Apple Developer Portal", "keyIdDescription": "The ID of your private key from Apple Developer Portal",
"redirectUri": "Return URL", "redirectUri": "Redirect URL",
"redirectUriDescription": "The URL that Apple will redirect to after successful authentication", "redirectUriDescription": "Please fill in the API address of the redirected URL after successfully passing Apple authentication. Do not use / at the end.",
"saveFailed": "Save failed", "saveFailed": "Save failed",
"saveSuccess": "Save successful", "saveSuccess": "Save successful",
"teamId": "Team ID", "teamId": "Team ID",
+72 -11
View File
@@ -1,13 +1,74 @@
{ {
"ipRegistrationLimit": "IP Registration Limit", "invite": {
"ipRegistrationLimitDescription": "When enabled, IPs that meet the rule requirements will be restricted from registering; note that IP determination may cause issues due to CDNs or frontend proxies", "commissionFirstTimeOnly": "Commission for First Purchase Only",
"penaltyTime": "Penalty Time (minutes)", "commissionFirstTimeOnlyDescription": "When enabled, commission is generated only on the inviter's first payment; you can configure individual users in user management",
"penaltyTimeDescription": "Users must wait for the penalty time to expire before registering again", "enableForcedInvite": "Enable Forced Invite",
"registrationLimitCount": "Registration Limit Count", "enableForcedInviteDescription": "When enabled, only invited users can register",
"registrationLimitCountDescription": "Enable penalty after reaching registration limit", "inputPlaceholder": "Enter",
"saveSuccess": "Save Successful", "inviteCommissionPercentage": "Invite Commission Percentage",
"stopNewUserRegistration": "Stop New User Registration", "inviteCommissionPercentageDescription": "Default global commission distribution ratio; you can configure individual ratios in user management",
"stopNewUserRegistrationDescription": "When enabled, no one can register", "inviteSettings": "Invite Settings",
"trialRegistration": "Trial Registration", "saveSuccess": "Save Successful"
"trialRegistrationDescription": "Enable trial registration; modify trial package and duration first" },
"log": {
"content": "Content",
"createdAt": "Created At",
"emailLog": "Email Log",
"mobileLog": "Mobile Log",
"platform": "Platform",
"sendFailed": "Failed",
"sendSuccess": "Success",
"status": "Status",
"subject": "Subject",
"to": "Recipient",
"updatedAt": "Updated At"
},
"register": {
"day": "Day",
"hour": "Hour",
"ipRegistrationLimit": "IP Registration Limit",
"ipRegistrationLimitDescription": "When enabled, IPs that meet the rule requirements will be restricted from registering; note that IP determination may cause issues due to CDNs or frontend proxies",
"minute": "Minute",
"month": "Month",
"noLimit": "No Limit",
"penaltyTime": "Penalty Time (minutes)",
"penaltyTimeDescription": "Users must wait for the penalty time to expire before registering again",
"registerSettings": "Register Settings",
"registrationLimitCount": "Registration Limit Count",
"registrationLimitCountDescription": "Enable penalty after reaching registration limit",
"saveSuccess": "Save Successful",
"stopNewUserRegistration": "Stop New User Registration",
"stopNewUserRegistrationDescription": "When enabled, no one can register",
"trialDuration": "Trial Duration",
"trialRegistration": "Trial Registration",
"trialRegistrationDescription": "Enable trial registration; modify trial package and duration first",
"trialSubscribePlan": "Trial Subscription Plan",
"trialSubscribePlanDescription": "Select trial subscription plan",
"year": "Year"
},
"verify": {
"inputPlaceholder": "Enter",
"loginVerificationCode": "Login Verification Code",
"loginVerificationCodeDescription": "Human verification during login",
"registrationVerificationCode": "Registration Verification Code",
"registrationVerificationCodeDescription": "Human verification during registration",
"resetPasswordVerificationCode": "Reset Password Verification Code",
"resetPasswordVerificationCodeDescription": "Human verification during password reset",
"saveSuccess": "Save Successful",
"turnstileSecretDescription": "Turnstile secret key provided by Cloudflare",
"turnstileSiteKeyDescription": "Turnstile site key provided by Cloudflare",
"verifySettings": "Verify Settings"
},
"verify-code": {
"dailyLimit": "Daily Limit",
"dailyLimitDescription": "Maximum number of verification codes that can be sent per day",
"expireTime": "Expire Time",
"expireTimeDescription": "Verification code expiration time (seconds)",
"interval": "Send Interval",
"intervalDescription": "Minimum interval between sending verification codes (seconds)",
"saveSuccess": "Save Successful",
"second": "seconds",
"times": "times",
"verifyCodeSettings": "Verification Code Settings"
}
} }
+1 -1
View File
@@ -20,7 +20,7 @@
"logout": "Logout", "logout": "Logout",
"methods": { "methods": {
"email": "Email", "email": "Email",
"sms": "Telephone" "mobile": "Telephone"
}, },
"orWithEmail": "Or with Email", "orWithEmail": "Or with Email",
"register": { "register": {
+2
View File
@@ -45,9 +45,11 @@
"60002": "Unable to use the subscription at the moment, please try again later.", "60002": "Unable to use the subscription at the moment, please try again later.",
"60003": "An existing subscription is detected. Please cancel it before proceeding.", "60003": "An existing subscription is detected. Please cancel it before proceeding.",
"60004": "Unable to delete at the moment as the subscription has active users.", "60004": "Unable to delete at the moment as the subscription has active users.",
"60005": "Single subscription mode has exceeded user limit",
"70001": "Incorrect verification code, please re-enter.", "70001": "Incorrect verification code, please re-enter.",
"80001": "Task was not successfully queued, please try again later.", "80001": "Task was not successfully queued, please try again later.",
"90001": "Please disable DEBUG mode and try again.", "90001": "Please disable DEBUG mode and try again.",
"90015": "This account has reached the limit of sending times today, please try again tomorrow.",
"undefined": "An error occurred in the system, please try again later." "undefined": "An error occurred in the system, please try again later."
}, },
"table": { "table": {
+14
View File
@@ -0,0 +1,14 @@
{
"blockVirtualMachine": "Block Virtual Machine",
"blockVirtualMachineDescription": "When enabled, devices will be prevented from running on virtual machines or emulators",
"communicationKey": "Communication Key",
"communicationKeyDescription": "Communication key is used for secure communication between devices and servers",
"enable": "Enable",
"enableDescription": "After enabling, multiple device identifiers such as IMEI/IDFA/IDFV/AndroidID/Mac address are supported for login and registration",
"enableSecurity": "Enable Communication Encryption",
"enableSecurityDescription": "When enabled, the communication between devices and servers will be encrypted",
"saveFailed": "Save failed",
"saveSuccess": "Save successful",
"showAds": "Show Advertisements",
"showAdsDescription": "When enabled, advertisements will be displayed on devices"
}
+4 -4
View File
@@ -1,19 +1,17 @@
{ {
"emailBasicConfig": "Basic Configuration",
"emailBasicConfigDescription": "Configure SMTP server settings and email verification options", "emailBasicConfigDescription": "Configure SMTP server settings and email verification options",
"emailLogs": "Email Logs",
"emailLogsDescription": "View the history of sent emails and their status", "emailLogsDescription": "View the history of sent emails and their status",
"emailSuffixWhitelist": "Email Suffix Whitelist", "emailSuffixWhitelist": "Email Suffix Whitelist",
"emailSuffixWhitelistDescription": "When enabled, only emails with suffixes in the list can register", "emailSuffixWhitelistDescription": "When enabled, only emails with suffixes in the list can register",
"emailTemplate": "Email Template",
"emailVerification": "Email Verification", "emailVerification": "Email Verification",
"emailVerificationDescription": "When enabled, users will need to verify their email", "emailVerificationDescription": "When enabled, users will need to verify their email",
"enable": "Enable", "enable": "Enable",
"enableDescription": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled", "enableDescription": "After enabling, email registration, login, binding, and unbinding functions will be enabled",
"expiration_email_template": "Expiration Notice Template", "expiration_email_template": "Expiration Notice Template",
"expiration_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.", "expiration_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"failed": "Failed", "failed": "Failed",
"inputPlaceholder": "Enter value...", "inputPlaceholder": "Enter value...",
"logs": "Logs",
"maintenance_email_template": "Maintenance Notice Template", "maintenance_email_template": "Maintenance Notice Template",
"maintenance_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.", "maintenance_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"recipient": "Recipient", "recipient": "Recipient",
@@ -27,6 +25,7 @@
"senderAddressDescription": "The default email address used for sending emails.", "senderAddressDescription": "The default email address used for sending emails.",
"sent": "Sent", "sent": "Sent",
"sentAt": "Sent At", "sentAt": "Sent At",
"settings": "Settings",
"smtpAccount": "SMTP Account", "smtpAccount": "SMTP Account",
"smtpAccountDescription": "The email account used for authentication.", "smtpAccountDescription": "The email account used for authentication.",
"smtpEncryptionMethod": "SMTP Encryption Method", "smtpEncryptionMethod": "SMTP Encryption Method",
@@ -39,6 +38,7 @@
"smtpServerPortDescription": "Specify the port used to connect to the SMTP server.", "smtpServerPortDescription": "Specify the port used to connect to the SMTP server.",
"status": "Status", "status": "Status",
"subject": "Subject", "subject": "Subject",
"template": "Template",
"verify_email_template": "Verification Email Template", "verify_email_template": "Verification Email Template",
"verify_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.", "verify_email_templateDescription": "The {after}.variable{before} placeholders will be replaced with actual data. Ensure to keep these variables.",
"whitelistSuffixes": "Whitelist Suffixes", "whitelistSuffixes": "Whitelist Suffixes",
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Facebook App Secret from Facebook Developers Console", "clientSecretDescription": "Facebook App Secret from Facebook Developers Console",
"enable": "Enable", "enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Facebook account", "enableDescription": "After enabling, users can sign in with their Facebook account",
"redirectUri": "Authorized redirect URI",
"redirectUriDescription": "The URL to which the user will be redirected after Facebook authentication",
"saveFailed": "Save failed", "saveFailed": "Save failed",
"saveSuccess": "Save successful" "saveSuccess": "Save successful"
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "The client secret from your GitHub OAuth application settings", "clientSecretDescription": "The client secret from your GitHub OAuth application settings",
"enable": "Enable GitHub Authentication", "enable": "Enable GitHub Authentication",
"enableDescription": "Allow users to sign in with their GitHub accounts", "enableDescription": "Allow users to sign in with their GitHub accounts",
"redirectUri": "Authorization callback URL",
"redirectUriDescription": "The URL in your application where users will be redirected after GitHub authentication",
"saveFailed": "Failed to save GitHub settings", "saveFailed": "Failed to save GitHub settings",
"saveSuccess": "GitHub settings saved successfully" "saveSuccess": "GitHub settings saved successfully"
} }
-2
View File
@@ -5,8 +5,6 @@
"clientSecretDescription": "Google OAuth 2.0 Client Secret from Google Cloud Console", "clientSecretDescription": "Google OAuth 2.0 Client Secret from Google Cloud Console",
"enable": "Enable", "enable": "Enable",
"enableDescription": "After enabling, users can sign in with their Google account", "enableDescription": "After enabling, users can sign in with their Google account",
"redirectUri": "Authorized redirect URI",
"redirectUriDescription": "The URL to which the user will be redirected after Google authentication",
"saveFailed": "Save failed", "saveFailed": "Save failed",
"saveSuccess": "Save successful" "saveSuccess": "Save successful"
} }
-25
View File
@@ -1,25 +0,0 @@
{
"cs-CZ": "Czech",
"de-DE": "German",
"en-US": "English",
"es-ES": "Spanish",
"es-MX": "Spanish (Mexico)",
"fa-IR": "Persian",
"fi-FI": "Finnish",
"fr-FR": "French",
"hi-IN": "Hindi",
"hu-HU": "Hungarian",
"ja-JP": "Japanese",
"ko-KR": "Korean",
"no-NO": "Norwegian",
"pl-PL": "Polish",
"pt-BR": "Portuguese (Brazil)",
"ro-RO": "Romanian",
"ru-RU": "Russian",
"th-TH": "Thai",
"tr-TR": "Turkish",
"uk-UA": "Ukrainian",
"vi-VN": "Vietnamese",
"zh-CN": "Chinese (Simplified)",
"zh-HK": "Chinese (Traditional)"
}
+3
View File
@@ -1,9 +1,11 @@
{ {
"ADS Config": "ADS Config",
"Announcement Management": "Announcement Management", "Announcement Management": "Announcement Management",
"Apple": "Apple ID", "Apple": "Apple ID",
"Auth Control": "Auth Control", "Auth Control": "Auth Control",
"Coupon Management": "Coupon Management", "Coupon Management": "Coupon Management",
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Device": "Device",
"Document Management": "Document Management", "Document Management": "Document Management",
"Email": "Email", "Email": "Email",
"Facebook": "Facebook", "Facebook": "Facebook",
@@ -24,5 +26,6 @@
"Ticket Management": "Ticket Management", "Ticket Management": "Ticket Management",
"Twitter": "Twitter", "Twitter": "Twitter",
"User": "User", "User": "User",
"User Detail": "User Detail",
"User Management": "User Management" "User Management": "User Management"
} }
+8 -1
View File
@@ -13,7 +13,14 @@
"stripe_wechat_pay": "Stripe (WeChat)" "stripe_wechat_pay": "Stripe (WeChat)"
}, },
"orderNumber": "Order Number", "orderNumber": "Order Number",
"status": "Order Status", "status": {
"0": "Status",
"1": "Pending",
"2": "Paid",
"3": "Cancelled",
"4": "Closed",
"5": "Completed"
},
"subscribe": "Subscribe", "subscribe": "Subscribe",
"subscribePrice": "Subscription Price", "subscribePrice": "Subscription Price",
"total": "Total", "total": "Total",
+12 -21
View File
@@ -1,41 +1,32 @@
{ {
"SmsList": "SMS Log List", "accessLabel": "Access",
"applyPlatform": "Apply Platform", "applyPlatform": "Apply Platform",
"areaCode": "Area Code",
"content": "Content",
"createdAt": "Send Time",
"enable": "Enable", "enable": "Enable",
"enableTip": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled", "enableTip": "After enabling, mobile phone registration, login, binding, and unbinding functions will be enabled",
"expireTime": "Expire Time", "endpointLabel": "Endpoint",
"expireTimeTip": "SMS verification code validity period (seconds)",
"interval": "Interval",
"intervalTip": "SMS verification code sending interval for the same phone number (seconds), 0 means no limit",
"limit": "Daily Limit",
"limitTip": "Daily SMS verification code sending limit for the same phone number, 0 means no limit",
"logs": "Logs", "logs": "Logs",
"phoneNumberLabel": "Phone Number",
"placeholders": { "placeholders": {
"expireTime": "Enter expire time, default is 300",
"interval": "Enter interval time, default is 60",
"limit": "Enter daily limit, default is 20",
"template": "Your verification code is {code}, valid for 5 minutes", "template": "Your verification code is {code}, valid for 5 minutes",
"templateCode": "Enter template code" "templateCode": "Enter template code"
}, },
"platform": "SMS Platform", "platform": "SMS Platform",
"platformConfigTip": "Please fill in the provided {key} configuration", "platformConfigTip": "Please fill in the provided {key} configuration",
"platformTip": "Please select SMS platform", "platformTip": "Please select SMS platform",
"search": "Search phone number", "secretLabel": "Secret",
"sendFailed": "Send Failed", "sendFailed": "Send Failed",
"sendSuccess": "Send Success", "sendSuccess": "Send Success",
"settings": "Settings", "settings": "Settings",
"status": "Status", "signNameLabel": "Sign Name",
"telephone": "Phone Number",
"template": "SMS Template", "template": "SMS Template",
"templateCode": "Template Code", "templateCodeLabel": "Template Code",
"templateParam": "Template Parameter",
"templateTip": "Please fill in the SMS template, keep {code} in the middle, otherwise SMS function will not work", "templateTip": "Please fill in the SMS template, keep {code} in the middle, otherwise SMS function will not work",
"testSms": "Send Test SMS", "testSms": "Send Test SMS",
"testSmsContent": "This is a test message",
"testSmsPhone": "Enter phone number", "testSmsPhone": "Enter phone number",
"testSmsTip": "Send a test SMS to verify your configuration", "testSmsTip": "Send a test message to verify your configuration",
"updateSuccess": "Update Success" "updateSuccess": "Update Success",
"whitelistAreaCode": "Whitelist Area Code",
"whitelistAreaCodeTip": "Please enter whitelist area codes, e.g., 1, 852, 886, 888",
"whitelistValidation": "Whitelist Verification",
"whitelistValidationTip": "When enabled, only area codes in the whitelist can send SMS"
} }

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