🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"projectName": "apps/user",
"mode": "file-router",
"typescript": true,
"tailwind": true,
"packageManager": "bun",
"addOnOptions": {},
"git": true,
"version": 1,
"framework": "react-cra",
"chosenAddOns": ["biome", "tanstack-query", "table", "form", "shadcn"]
}
+10
View File
@@ -0,0 +1,10 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
count.txt
.env
.nitro
.tanstack
.wrangler
+35
View File
@@ -0,0 +1,35 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
},
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}
+141
View File
@@ -0,0 +1,141 @@
<a name="readme-top"></a>
<div align="center">
<img width="160" src="https://raw.githubusercontent.com/perfect-panel/ppanel-assets/refs/heads/main/logo.svg">
<h1>PPanel user web</h1>
This is a PPanel user web powered by PPanel
English
·
[Chinese](./README.zh-CN.md)
·
[Changelog](../../CHANGELOG.md)
·
[Report Bug][issues-link]
·
[Request Feature][issues-link]
<!-- SHIELD GROUP -->
[![][github-release-shield]][github-release-link]
[![][github-releasedate-shield]][github-releasedate-link]
[![][github-action-test-shield]][github-action-test-link]
[![][github-action-release-shield]][github-action-release-link]<br/>
[![][github-contributors-shield]][github-contributors-link]
[![][github-forks-shield]][github-forks-link]
[![][github-stars-shield]][github-stars-link]
[![][github-issues-shield]][github-issues-link]
[![][github-license-shield]][github-license-link]
![](https://urlscan.io/liveshot/?width=1920&height=1080&url=https://user.ppanel.dev)
</div>
<details>
<summary><kbd>Table of contents</kbd></summary>
#### TOC
- [⌨️ Local Development](#-local-development)
- [🚀 Deploy on Vercel](#-deploy-on-vercel)
- [🤝 Contributing](#-contributing)
- [📝 License](#-license)
####
</details>
## ⌨️ Local Development
You can use Github Codespaces for online development:
[![][codespaces-shield]][codespaces-link]
You can use Gitpod for online development:
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)][gitpod-link]
or clone it for local development:
```bash
git clone https://github.com/perfect-panel/ppanel-web.git
cd ppanel-web
# Install dependencies
bun install
# Run the development server
cd apps/user
bun dev
```
Open <http://localhost:3000> with your browser to see the result.
## 🚀 Deploy on Vercel
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%user.ppanel.dev&demo-title=PPanel%20User%20Web&demo-url=https%3A%2F%2Fuser.ppanel.dev%2F&from=.&project-name=ppanel-user-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%user&skippable-integrations=1)
The easiest way to deploy your Next.js app is to use the
[Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
from the creators of Next.js.
Check out our
[Next.js deployment documentation](https://nextjs.org/docs/deployment)
for more details.
## 🤝 Contributing
Contributions of all types are more than welcome,
if you're interested in contributing code, feel free to check out our GitHub
[Issues][github-issues-link] to get stuck in to show us what youre made of.
[![][pr-welcome-shield]][pr-welcome-link]
[![][contributors-contrib]][contributors-url]
<div align="right">
[![][back-to-top]](#readme-top)
</div>
---
## 📝 License
Copyright © 2024 [PPanel][profile-link]. <br />
This project is [GNU](../../LICENSE) licensed.
<!-- LINK GROUP -->
[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square
[codespaces-link]: https://codespaces.new/perfect-panel/ppanel-web
[codespaces-shield]: https://github.com/codespaces/badge.svg
[contributors-contrib]: https://contrib.rocks/image?repo=perfect-panel/ppanel-web
[contributors-url]: https://github.com/perfect-panel/ppanel-web/graphs/contributors
[github-action-release-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/release.yml
[github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square
[github-action-test-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/test.yml
[github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square
[github-contributors-link]: https://github.com/perfect-panel/ppanel-web/graphs/contributors
[github-contributors-shield]: https://img.shields.io/github/contributors/perfect-panel/ppanel-web?color=c4f042&labelColor=black&style=flat-square
[github-forks-link]: https://github.com/perfect-panel/ppanel-web/network/members
[github-forks-shield]: https://img.shields.io/github/forks/perfect-panel/ppanel-web?color=8ae8ff&labelColor=black&style=flat-square
[github-issues-link]: https://github.com/perfect-panel/ppanel-web/issues
[github-issues-shield]: https://img.shields.io/github/issues/perfect-panel/ppanel-web?color=ff80eb&labelColor=black&style=flat-square
[github-license-link]: https://github.com/perfect-panel/ppanel-web/blob/master/LICENSE
[github-license-shield]: https://img.shields.io/github/license/perfect-panel/ppanel-web?color=white&labelColor=black&style=flat-square
[github-release-link]: https://github.com/perfect-panel/ppanel-web/releases
[github-release-shield]: https://img.shields.io/github/v/release/perfect-panel/ppanel-web?style=flat-square&sort=semver&logo=github
[github-releasedate-link]: https://github.com/perfect-panel/ppanel-web/releases
[github-releasedate-shield]: https://img.shields.io/github/release-date/perfect-panel/ppanel-web?labelColor=black&style=flat-square
[github-stars-link]: https://github.com/perfect-panel/ppanel-web/network/stargazers
[github-stars-shield]: https://img.shields.io/github/stars/perfect-panel/ppanel-web?color=ffcb47&labelColor=black&style=flat-square
[gitpod-link]: https://gitpod.io/#https://github.com/perfect-panel/ppanel-web
[issues-link]: https://github.com/perfect-panel/ppanel-web/issues/new/choose
[pr-welcome-link]: https://github.com/perfect-panel/ppanel-web/pulls
[pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge
[profile-link]: https://github.com/perfect-panel
+141
View File
@@ -0,0 +1,141 @@
<a name="readme-top"></a>
<div align="center">
<img width="160" src="https://raw.githubusercontent.com/perfect-panel/ppanel-assets/refs/heads/main/logo.svg">
<h1>PPanel 用户端</h1>
这是由 PPanel 提供支持的 PPanel 用户端
[英文](./README.md)
·
中文
·
[更新日志](../../CHANGELOG.md)
·
[报告问题][issues-link]
·
[请求功能][issues-link]
<!-- SHIELD GROUP -->
[![][github-release-shield]][github-release-link]
[![][github-releasedate-shield]][github-releasedate-link]
[![][github-action-test-shield]][github-action-test-link]
[![][github-action-release-shield]][github-action-release-link]<br/>
[![][github-contributors-shield]][github-contributors-link]
[![][github-forks-shield]][github-forks-link]
[![][github-stars-shield]][github-stars-link]
[![][github-issues-shield]][github-issues-link]
[![][github-license-shield]][github-license-link]
![](https://urlscan.io/liveshot/?width=1920&height=1080&url=https://user.ppanel.dev)
</div>
<details>
<summary><kbd>目录</kbd></summary>
#### 目录
- [⌨️ 本地开发](#-本地开发)
- [🚀 在 Vercel 上部署](#-在-vercel-上部署)
- [🤝 贡献](#-贡献)
- [📝 许可证](#-许可证)
####
</details>
## ⌨️ 本地开发
您可以使用 Github Codespaces 进行在线开发:
[![][codespaces-shield]][codespaces-link]
您可以使用 Gitpod 进行在线开发:
[![在 Gitpod 中打开](https://gitpod.io/button/open-in-gitpod.svg)][gitpod-link]
或者克隆项目进行本地开发:
```bash
git clone https://github.com/perfect-panel/ppanel-web.git
cd ppanel-web
# 安装依赖
bun install
# 运行开发服务器
cd apps/user
bun dev
```
在浏览器中打开 <http://localhost:3000> 查看结果。
## 🚀 在 Vercel 上部署
[![使用 Vercel 部署](https://vercel.com/button)](https://vercel.com/new/clone?demo-description=PPanel%20is%20a%20pure%2C%20professional%2C%20and%20perfect%20open-source%20proxy%20panel%20tool%2C%20designed%20to%20be%20your%20ideal%20choice%20for%20learning%20and%20practical%20use&demo-image=https%3A%2F%2Furlscan.io%2Fliveshot%2F%3Fwidth%3D1920%26height%3D1080%26url%3Dhttps%3A%2F%2Fuser.ppanel.dev&demo-title=PPanel%20User%20Web&demo-url=https%3A%2F%2Fuser.ppanel.dev%2F&from=.&project-name=ppanel-user-web&repository-name=ppanel-web&repository-url=https%3A%2F%2Fgithub.com%2Fperfect-panel%2Fppanel-web&root-directory=apps%2Fuser&skippable-integrations=1)
部署 Next.js 应用的最简单方式是使用
[ Vercel 平台](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme)
由 Next.js 的创建者提供支持。
查看我们的
[Next.js 部署文档](https://nextjs.org/docs/deployment)
获取更多详情。
## 🤝 贡献
欢迎各种类型的贡献,
如果您有兴趣贡献代码,请随时查看我们的 GitHub
[问题][github-issues-link] 来展示您的能力。
[![][pr-welcome-shield]][pr-welcome-link]
[![][contributors-contrib]][contributors-url]
<div align="right">
[![][back-to-top]](#readme-top)
</div>
---
## 📝 许可证
版权所有 © 2024 [PPanel][profile-link]。<br />
本项目使用 [GNU](./LICENSE) 许可证。
<!-- LINK GROUP -->
[back-to-top]: https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square
[codespaces-link]: https://codespaces.new/perfect-panel/ppanel-web
[codespaces-shield]: https://github.com/codespaces/badge.svg
[contributors-contrib]: https://contrib.rocks/image?repo=perfect-panel/ppanel-web
[contributors-url]: https://github.com/perfect-panel/ppanel-web/graphs/contributors
[github-action-release-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/release.yml
[github-action-release-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/release.yml?label=release&labelColor=black&logo=githubactions&logoColor=white&style=flat-square
[github-action-test-link]: https://github.com/perfect-panel/ppanel-web/actions/workflows/test.yml
[github-action-test-shield]: https://img.shields.io/github/actions/workflow/status/perfect-panel/ppanel-web/test.yml?label=test&labelColor=black&logo=githubactions&logoColor=white&style=flat-square
[github-contributors-link]: https://github.com/perfect-panel/ppanel-web/graphs/contributors
[github-contributors-shield]: https://img.shields.io/github/contributors/perfect-panel/ppanel-web?color=c4f042&labelColor=black&style=flat-square
[github-forks-link]: https://github.com/perfect-panel/ppanel-web/network/members
[github-forks-shield]: https://img.shields.io/github/forks/perfect-panel/ppanel-web?color=8ae8ff&labelColor=black&style=flat-square
[github-issues-link]: https://github.com/perfect-panel/ppanel-web/issues
[github-issues-shield]: https://img.shields.io/github/issues/perfect-panel/ppanel-web?color=ff80eb&labelColor=black&style=flat-square
[github-license-link]: https://github.com/perfect-panel/ppanel-web/blob/master/LICENSE
[github-license-shield]: https://img.shields.io/github/license/perfect-panel/ppanel-web?color=white&labelColor=black&style=flat-square
[github-release-link]: https://github.com/perfect-panel/ppanel-web/releases
[github-release-shield]: https://img.shields.io/github/v/release/perfect-panel/ppanel-web?style=flat-square&sort=semver&logo=github
[github-releasedate-link]: https://github.com/perfect-panel/ppanel-web/releases
[github-releasedate-shield]: https://img.shields.io/github/release-date/perfect-panel/ppanel-web?labelColor=black&style=flat-square
[github-stars-link]: https://github.com/perfect-panel/ppanel-web/network/stargazers
[github-stars-shield]: https://img.shields.io/github/stars/perfect-panel/ppanel-web?color=ffcb47&labelColor=black&style=flat-square
[gitpod-link]: https://gitpod.io/#https://github.com/perfect-panel/ppanel-web
[issues-link]: https://github.com/perfect-panel/ppanel-web/issues/new/choose
[pr-welcome-link]: https://github.com/perfect-panel/ppanel-web/pulls
[pr-welcome-shield]: https://img.shields.io/badge/🤯_pr_welcome-%E2%86%92-ffcb47?labelColor=black&style=for-the-badge
[profile-link]: https://github.com/perfect-panel
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@workspace/ui/components",
"utils": "@workspace/ui/lib/utils",
"hooks": "@workspace/ui/hooks",
"lib": "@workspace/ui/lib",
"ui": "@workspace/ui/components"
},
"iconLibrary": "lucide"
}
+23
View File
@@ -0,0 +1,23 @@
import { defineConfig } from "i18next-cli";
/**
* i18next CLI configuration
*
* Used for extracting translation keys and managing translation files.
* Inherits language settings from the shared i18n configuration.
*
* @see https://github.com/i18next/i18next-cli
*/
export default defineConfig({
// Use supported languages from shared config
locales: ["en-US", "zh-CN"],
// Extraction configuration
extract: {
input: [
"src/**/*.{js,jsx,ts,tsx}",
"../../packages/ui/src/**/*.{js,jsx,ts,tsx}",
], // Source files to scan
output: "public/locales/{{language}}/{{namespace}}.json", // Output path template
},
});
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<!-- PWA -->
<link rel="manifest" href="/site.webmanifest">
<meta
name="theme-color"
content="#FFFFFF"
media="(prefers-color-scheme: light)"
>
<meta
name="theme-color"
content="#000000"
media="(prefers-color-scheme: dark)"
>
<!-- SEO -->
<meta name="description" content="Loading...">
<meta name="keywords" content="">
<title>Loading...</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
{
"name": "ppanel-user-web",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --port 3000",
"build": "vite build && tsc",
"serve": "vite preview",
"lint": "biome lint",
"check": "biome check",
"i18n:extract": "i18next-cli extract",
"i18n:sync": "i18next-cli sync",
"i18n:status": "i18next-cli status"
},
"dependencies": {
"@faker-js/faker": "^10.0.0",
"@lottiefiles/dotlottie-react": "^0.17.7",
"@stripe/react-stripe-js": "^5.4.0",
"@stripe/stripe-js": "^8.5.2",
"@tailwindcss/vite": "^4.0.6",
"@tanstack/match-sorter-utils": "^8.19.4",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-form": "^1.0.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/react-table": "^8.21.2",
"@tanstack/router-plugin": "^1.132.0",
"@workspace/typescript-config": "workspace:*",
"@workspace/ui": "workspace:*",
"ahooks": "^3.9.6",
"framer-motion": "^12.23.24",
"js-yaml": "^4.1.1",
"qrcode.react": "^4.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-helmet-async": "^2.0.5",
"react-turnstile": "^1.1.4",
"zustand": "^5.0.8"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@tanstack/devtools-vite": "^0.3.11",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^27.0.0",
"vite": "^7.1.7",
"vitest": "^3.0.5",
"web-vitals": "^5.1.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" fill="none"><path stroke="url(#a)" stroke-linecap="round" stroke-width="25" d="M162.779 118.828v105.346"/><path fill="url(#b)" fill-rule="evenodd" d="M175.279 38.443v97.01l39.384-16.222c6.384-2.629 13.69.414 16.319 6.797 2.629 6.383-.414 13.689-6.797 16.319l-29.869 12.303 39.758 21.079c6.099 3.234 8.422 10.8 5.188 16.899-3.234 6.099-10.8 8.422-16.899 5.188l-58.083-30.794-24.537 10.107c-.308.127-.617.24-.928.341L32.554 221.278l-17.265 7.118V86.468l-.007-.017 142.683-59.543 17.314-7.226v18.761Zm-39.745 113.338-95.245 39.267v-87.943l109.99-45.9v74.098l-35.34-18.737c-6.1-3.234-13.666-.911-16.9 5.188-3.233 6.1-.91 13.666 5.189 16.899l32.306 17.128Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="163.279" x2="163.279" y1="118.828" y2="224.174" gradientUnits="userSpaceOnUse"><stop stop-color="#2E58FF"/><stop offset="1" stop-color="#26D0FF"/></linearGradient><linearGradient id="b" x1="128.003" x2="105.241" y1="19.688" y2="214.91" gradientUnits="userSpaceOnUse"><stop stop-color="#02F"/><stop offset=".48" stop-color="#5EE7FF"/><stop offset=".975" stop-color="#FCD8FF"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,11 @@
{
"commissionInfo": "Commission Info",
"commissionRate": "Commission Rate",
"copyInviteLink": "Copy Invite Link",
"copySuccess": "Copy Success",
"inviteCode": "Invite Code",
"inviteRecords": "Invite Records",
"registrationTime": "Registration Time",
"totalCommission": "Total Commission",
"userIdentifier": "User Identifier"
}
+36
View File
@@ -0,0 +1,36 @@
{
"authenticating": "Authenticating...",
"binding": "Binding account...",
"get": "Get Code",
"login": {
"codeLogin": "Login with Code",
"email": "Please enter a valid email address",
"forgotPassword": "Forgot Password?",
"passwordLogin": "Login with Password",
"registerAccount": "Register Account",
"success": "Login successful!",
"title": "Login"
},
"privacyPolicy": "Privacy Policy",
"register": {
"email": "Please enter a valid email address",
"existingAccount": "Already have an account?",
"invite": "Invitation Code (Optional)",
"message": "Registration is currently disabled",
"passwordMismatch": "Passwords do not match",
"success": "Registration successful!",
"switchToLogin": "Login",
"title": "Register",
"whitelist": "This email domain is not in the whitelist"
},
"reset": {
"email": "Please enter a valid email address",
"existingAccount": "Remember your password?",
"success": "Password reset successful!",
"switchToLogin": "Login",
"title": "Reset Password"
},
"tos": "Terms of Service",
"verifyAccount": "Verify Your Account",
"verifyAccountDesc": "Please login or register to continue"
}
@@ -0,0 +1,87 @@
{
"empty": {
"tips": {
"0": "Imagine this space filled with exciting content! For now, you'll have to use your imagination...",
"1": "This area mysteriously disappeared, but we're summoning it back!",
"2": "Oh no, nothing happened... Feel free to fill in the blank!",
"3": "It's like discovering an empty stage at a concert... Why not go up and perform?",
"4": "You've found a blank canvas! How about building a house?",
"5": "This area is currently empty, but creativity starts here!",
"6": "Nothing here... but don't worry, it's just the beginning!",
"7": "This place was supposed to have a big surprise, but the surprise slipped away!",
"8": "There's nothing here for now, like an empty snack cabinet.",
"9": "This empty space is waiting for its protagonist to take the stage!"
}
},
"error": {
"400": "The request parameters are incorrect, please check and resubmit.",
"401": "Request is too frequent, please try again later.",
"500": "The server is having some issues, please try again later.",
"10001": "Query was not successful, please try again later or check your conditions.",
"10002": "Update operation was not successful, please try again later.",
"10003": "Insert operation cannot be completed at the moment, please try again later.",
"10004": "Deletion operation could not be completed, please try again later.",
"20001": "The user information already exists, please retry with different information.",
"20002": "User not found, please check the information and try again.",
"20003": "Incorrect password, please re-enter.",
"20004": "The user is disabled, please contact customer service if you have questions.",
"20005": "Insufficient balance, please recharge and try again.",
"20006": "The registration function is temporarily unavailable, please try again later.",
"20008": "User information is incorrect, please check and try again.",
"30001": "The node already exists, please do not add it again.",
"30002": "Related node not found, please check and try again.",
"30003": "Group already exists, please try using a different name.",
"30004": "Group not found, please verify the information and try again.",
"30005": "There is still content in the group, please clear it and try again.",
"40002": "Valid Token not found, please log in before retrying.",
"40003": "Current Token is invalid, please reacquire before trying again.",
"40004": "Token has expired, please log in again.",
"40005": "You do not have access permission, please contact the administrator if you have any questions.",
"50001": "Corresponding coupon information not found, please check and try again.",
"50002": "The coupon has been used, cannot be used again.",
"60001": "Subscription has expired, please renew before using.",
"60002": "Unable to use the subscription at the moment, please try again later.",
"60003": "An existing subscription is detected. Please cancel it before proceeding.",
"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.",
"80001": "Task was not successfully queued, please try again later.",
"90001": "Please disable DEBUG mode and try again.",
"90015": "This account has reached the limit of sending times today, please try again tomorrow.",
"unknown": "An error occurred in the system, please try again later."
},
"footer": {
"copyright": "All rights reserved",
"privacyPolicy": "Privacy Policy",
"tos": "Terms of Service"
},
"language": "Language",
"loginRegister": "Login / Register",
"menu": {
"affiliate": "Commission",
"announcement": "Announcement Management",
"dashboard": "Dashboard",
"document": "Document Management",
"finance": "Commerce",
"help": "Users & Support",
"logout": "Logout",
"order": "Order Management",
"personal": "Personal",
"profile": "User Detail",
"server": "Server Management",
"subscribe": "Subscribe",
"ticket": "Ticket Management",
"wallet": "Balance"
},
"pagination": {
"pageInfo": "Page {{page}} of {{total}}",
"rowsPerPage": "Rows per page"
},
"theme": {
"dark": "Dark",
"light": "Light",
"system": "System",
"toggle": "Toggle theme"
},
"unlimited": "unlimited"
}
@@ -0,0 +1,29 @@
{
"cancel": "Cancel",
"confirm": "Confirm",
"confirmResetSubscription": "Are you sure you want to reset your subscription?",
"copy": "Copy",
"copySuccess": "Copy Success",
"deducted": "Deducted",
"download": "Download",
"expirationDays": "Expiration Days",
"expired": "Expired",
"finished": "Finished",
"import": "Import",
"latestAnnouncement": "Latest Announcement",
"manualImportMessage": "Please import manually",
"mySubscriptions": "My Subscriptions",
"nextResetDays": "Next Reset Days",
"noLimit": "No Limit",
"noReset": "No Reset",
"prompt": "Prompt",
"purchaseSubscription": "Purchase Subscription",
"qrCode": "QR Code",
"resetSubscription": "Reset Subscription",
"resetSuccess": "Reset Success",
"scanToSubscribe": "Scan to Subscribe",
"subscriptionUrl": "Subscription URL",
"totalTraffic": "Total Traffic",
"unknown": "Unknown",
"used": "Used"
}
@@ -0,0 +1,6 @@
{
"all": "All",
"document": "Document",
"read": "Read",
"tutorial": "Tutorial"
}
@@ -0,0 +1,7 @@
{
"accountBalance": "Account Balance",
"commission": "Commission",
"copySuccess": "Copy Success",
"giftAmount": "Gift Amount",
"inviteCode": "Invite Code"
}
+21
View File
@@ -0,0 +1,21 @@
{
"Day": "Day",
"global_map_description": "Explore seamless global connectivity. Choose network services that suit your needs and stay connected anytime, anywhere.",
"global_map_itle": "Global Connection, Easy and Worry-free",
"Hour": "Hour",
"locations": "Locations",
"locations_description": "Available in multiple regions",
"Minute": "Minute",
"Month": "Month",
"NoLimit": "No Limit",
"product_showcase_description": "Let us help you select the package that best suits you and enjoy exploring it.",
"product_showcase_title": "Choose Your Package",
"servers": "Servers",
"servers_description": "High-performance servers globally",
"started": "Get Started",
"subscribe": "Subscribe",
"users": "Users",
"users_description": "Trusted by users worldwide",
"welcome": "Welcome to",
"Year": "Year"
}
+40
View File
@@ -0,0 +1,40 @@
{
"balanceRecharge": "Balance Recharge",
"cancel": "Cancel",
"createdAt": "Created At",
"detail": "Detail",
"goToPayment": "Go to Payment",
"name": "Product Name",
"orderClosed": "Order Closed",
"orderList": "Order List",
"orderNo": "Order No",
"orderNumber": "Order Number",
"payment": "Payment",
"paymentAmount": "Amount",
"paymentMethod": "Payment Method",
"paymentSuccess": "Payment Successful",
"productList": "Product List",
"rechargeAmount": "Recharge Amount",
"resetPrice": "Reset Price",
"resetTraffic": "Reset Traffic",
"scanToPay": "Scan to Pay",
"status": {
"0": "Status",
"1": "Pending",
"2": "Paid",
"3": "Cancelled",
"4": "Closed",
"5": "Completed"
},
"subscribeNow": "Subscribe Now",
"timeExpired": "Time Expired",
"type": {
"0": "Type",
"1": "New Purchase",
"2": "Renewal",
"3": "Reset Traffic",
"4": "Recharge"
},
"viewDocument": "View Document",
"waitingForPayment": "Waiting for Payment"
}
@@ -0,0 +1,27 @@
{
"stripe": {
"card": {
"loading": "Loading Stripe..."
},
"card_name": "Cardholder Name",
"card_number": "Card Number",
"element_error": "Please fill in all card details",
"error": "An error occurred",
"expiry_date": "Expiry Date",
"loading": "Loading Stripe...",
"name_placeholder": "Full Name on Card",
"name_required": "Cardholder name is required",
"pay_button": "Pay Now",
"payment_failed": "Payment failed",
"processing": "Processing payment...",
"processing_button": "Processing...",
"qrcode": {
"alipay": "Scan with Alipay to pay",
"wechat_pay": "Scan with WeChat to pay"
},
"secure_notice": "Payments are secure and encrypted",
"security_code": "CVC",
"success_message": "Thank you for your purchase. Your order has been processed.",
"success_title": "Payment Successful"
}
}
@@ -0,0 +1,31 @@
{
"accountSettings": {
"accountSettings": "Account Settings",
"newPassword": "New Password",
"passwordMismatch": "Passwords do not match",
"repeatNewPassword": "Repeat New Password",
"updatePassword": "Update Password",
"updateSuccess": "Password updated successfully"
},
"notify": {
"balanceChange": "Balance Change Notification",
"finance": "Finance Notification",
"login": "Login Notification",
"notificationSettings": "Notification Settings",
"save": "Save",
"subscribe": "Subscribe Notification",
"updateSuccess": "Notification settings updated successfully"
},
"thirdParty": {
"bind": "thirdParty.bind",
"bindFailed": "Bind failed",
"bindMobile": "Bind Mobile",
"bindSuccess": "Bind successfully",
"confirm": "Confirm",
"save": "thirdParty.save",
"title": "Third-party Accounts",
"unbind": "thirdParty.unbind",
"update": "thirdParty.update",
"updateSuccess": "Update successfully"
}
}
@@ -0,0 +1,63 @@
{
"balanceRecharge": "Balance Recharge",
"billing": {
"billingTitle": "Product Billing",
"couponDiscount": "Coupon Discount",
"duration": "Duration",
"fee": "Fee",
"gift": "gift Deduction",
"price": "Price",
"productDiscount": "Product Discount",
"total": "Total"
},
"buy": "Buy",
"buyNow": "Buy Now",
"buySubscription": "Buy Subscription",
"coupon": "Coupon",
"Day": "Day",
"detail": {
"availableTraffic": "Available Traffic",
"connectedDevices": "Connected Devices",
"connectionSpeed": "Connection Speed",
"productDetail": "Product Details"
},
"discount": "Discount",
"discountInfo": "Discount Info",
"emailDomainNotAllowed": "Email domain is not in the whitelist",
"emailInputTitle": "Enter the email address for your {{siteName}} account",
"emailRequired": "Please enter your email address.",
"enterAmount": "Enter recharge amount",
"enterCoupon": "Enter Coupon Code",
"Hour": "Hour",
"invalidEmail": "Please enter a valid email address",
"Minute": "Minute",
"Month": "Month",
"NoLimit": "No Limit",
"passwordHint": "If you do not enter a password, we will automatically generate one and send it to your email.",
"paymentMethod": "Payment Method",
"productDescription": "productDescription",
"purchaseDuration": "Purchase Duration",
"recharge": "Recharge",
"rechargeAmount": "Recharge Amount",
"rechargeDescription": "One-click recharge, easy to handle",
"rechargeNow": "Recharge Now",
"renew": "Renew",
"renewSubscription": "Renew Subscription",
"resetPrice": "Reset Price",
"resetTraffic": "Reset Traffic",
"resetTrafficDescription": "Reset traffic to zero, and start a new billing cycle",
"resetTrafficTitle": "Reset Traffic",
"subscriptionNotFound": "Subscription not found",
"unsubscribe": {
"cancel": "Cancel",
"confirm": "Confirm",
"confirmUnsubscribe": "Confirm Unsubscribe",
"confirmUnsubscribeDescription": "Are you sure you want to unsubscribe?",
"failed": "Failed to unsubscribe. Please try again.",
"residualValue": "Residual value:",
"success": "You have been unsubscribed successfully.",
"unsubscribe": "Unsubscribe",
"unsubscribeDescription": "Please note: If you unsubscribe now, the remaining value of the subscription will be refunded to your account balance, which can be used for your next subscription purchase or renewal."
},
"Year": "Year"
}
@@ -0,0 +1,26 @@
{
"cancel": "Cancel",
"check": "Check",
"close": "Close",
"closeSuccess": "Close Success",
"closeWarning": "Are you sure you want to close this ticket?",
"confirm": "Confirm",
"confirmClose": "Confirm Close",
"createSuccess": "Create Success",
"createTicket": "Create Ticket",
"createTicketDescription": "Create Ticket Description",
"description": "Description",
"inputPlaceholder": "Input Placeholder",
"reply": "Reply",
"status": {
"0": "Status",
"1": "Pending Reply",
"2": "Pending Follow-up",
"3": "Resolved",
"4": "Closed"
},
"submit": "Submit",
"ticketList": "Ticket List",
"title": "Title",
"updatedAt": "Updated At"
}
@@ -0,0 +1,34 @@
{
"amount": "Amount",
"assetOverview": "Asset Overview",
"balance": "Balance",
"commission": "Commission",
"createdAt": "Created At",
"giftAmount": "Gift Amount",
"totalAssets": "Total Assets",
"type": {
"0": "Type",
"1": "Recharge",
"2": "Withdrawal",
"3": "Purchase",
"4": "Refund",
"5": "Reward",
"6": "Commission",
"231": "Auto Reset",
"232": "Advance Reset",
"233": "Paid Reset",
"321": "Recharge",
"322": "Withdraw",
"323": "Payment",
"324": "Refund",
"325": "Reward",
"326": "Admin Adjust",
"331": "Purchase",
"332": "Renewal",
"333": "Refund",
"334": "Withdraw",
"335": "Admin Adjust",
"341": "Increase",
"342": "Reduce"
}
}
@@ -0,0 +1,11 @@
{
"commissionInfo": "佣金信息",
"commissionRate": "佣金比例",
"copyInviteLink": "复制邀请链接",
"copySuccess": "复制成功",
"inviteCode": "邀请码",
"inviteRecords": "邀请记录",
"registrationTime": "注册时间",
"totalCommission": "累计佣金",
"userIdentifier": "用户标识"
}
+36
View File
@@ -0,0 +1,36 @@
{
"authenticating": "正在认证...",
"binding": "正在绑定账号...",
"get": "获取验证码",
"login": {
"codeLogin": "验证码登录",
"email": "请输入有效的邮箱地址",
"forgotPassword": "忘记密码?",
"passwordLogin": "密码登录",
"registerAccount": "注册账号",
"success": "登录成功!",
"title": "登录"
},
"privacyPolicy": "隐私政策",
"register": {
"email": "请输入有效的邮箱地址",
"existingAccount": "已有账号?",
"invite": "邀请码(可选)",
"message": "注册功能暂时不可用",
"passwordMismatch": "两次密码输入不一致",
"success": "注册成功!",
"switchToLogin": "登录",
"title": "注册",
"whitelist": "该邮箱域名不在白名单中"
},
"reset": {
"email": "请输入有效的邮箱地址",
"existingAccount": "记得密码了?",
"success": "密码重置成功!",
"switchToLogin": "登录",
"title": "重置密码"
},
"tos": "服务条款",
"verifyAccount": "验证您的账户",
"verifyAccountDesc": "请登录或注册以继续"
}
@@ -0,0 +1,87 @@
{
"empty": {
"tips": {
"0": "想象一下这里充满了精彩的内容!现在,你只能发挥你的想象力了...",
"1": "这个区域神秘消失了,但我们正在召唤它回来!",
"2": "哦不,什么都没发生... 随意填补空白吧!",
"3": "就像在音乐会上发现一个空舞台... 为什么不上去表演呢?",
"4": "你发现了一块空白画布!建个房子怎么样?",
"5": "这个区域目前是空的,但创造力从这里开始!",
"6": "这里什么都没有... 但别担心,这只是开始!",
"7": "这个地方本该有个大惊喜,但惊喜溜走了!",
"8": "暂时这里什么都没有,就像一个空的零食柜。",
"9": "这个空荡荡的空间正在等待它的主角登场!"
}
},
"error": {
"400": "请求参数不正确,请检查后重新提交。",
"401": "请求过于频繁,请稍后再试。",
"500": "服务器出现了一些问题,请稍后再试。",
"10001": "查询不成功,请稍后再试或检查查询条件。",
"10002": "更新操作不成功,请稍后再试。",
"10003": "插入操作暂时无法完成,请稍后再试。",
"10004": "删除操作无法完成,请稍后再试。",
"20001": "用户信息已存在,请使用不同的信息重试。",
"20002": "找不到用户,请检查信息后重试。",
"20003": "密码不正确,请重新输入。",
"20004": "用户已被禁用,如有疑问请联系客服。",
"20005": "余额不足,请充值后再试。",
"20006": "注册功能暂时不可用,请稍后再试。",
"20008": "用户信息不正确,请检查后重试。",
"30001": "该节点已存在,请勿重复添加。",
"30002": "找不到相关节点,请检查后重试。",
"30003": "分组已存在,请尝试使用不同的名称。",
"30004": "找不到分组,请验证信息后重试。",
"30005": "分组中还有内容,请清空后再试。",
"40002": "未找到有效的 Token,请登录后再试。",
"40003": "当前 Token 无效,请重新获取后再试。",
"40004": "Token 已过期,请重新登录。",
"40005": "您没有访问权限,如有疑问请联系管理员。",
"50001": "找不到对应的优惠券信息,请检查后重试。",
"50002": "该优惠券已被使用,无法再次使用。",
"60001": "订阅已过期,请续费后使用。",
"60002": "暂时无法使用该订阅,请稍后再试。",
"60003": "检测到现有订阅,请先取消后再继续。",
"60004": "由于订阅有活跃用户,暂时无法删除。",
"60005": "单一订阅模式已超过用户限制",
"70001": "验证码不正确,请重新输入。",
"80001": "任务未成功加入队列,请稍后再试。",
"90001": "请禁用 DEBUG 模式后再试。",
"90015": "该账户今日已达到发送次数限制,请明天再试。",
"unknown": "系统发生错误,请稍后再试。"
},
"footer": {
"copyright": "版权所有",
"privacyPolicy": "隐私政策",
"tos": "服务条款"
},
"language": "语言",
"loginRegister": "登录 / 注册",
"menu": {
"affiliate": "推广佣金",
"announcement": "公告管理",
"dashboard": "仪表盘",
"document": "文档管理",
"finance": "财务管理",
"help": "用户支持",
"logout": "退出登录",
"order": "订单管理",
"personal": "个人中心",
"profile": "用户详情",
"server": "服务器管理",
"subscribe": "订阅管理",
"ticket": "工单管理",
"wallet": "余额管理"
},
"pagination": {
"pageInfo": "第 {{page}} 页,共 {{total}} 页",
"rowsPerPage": "每页行数"
},
"theme": {
"dark": "深色",
"light": "浅色",
"system": "系统",
"toggle": "切换主题"
},
"unlimited": "无限制"
}
@@ -0,0 +1,29 @@
{
"cancel": "取消",
"confirm": "确认",
"confirmResetSubscription": "您确定要重置订阅吗?",
"copy": "复制",
"copySuccess": "复制成功",
"deducted": "已扣除",
"download": "下载",
"expirationDays": "过期天数",
"expired": "已过期",
"finished": "已完成",
"import": "一键导入",
"latestAnnouncement": "最新公告",
"manualImportMessage": "请手动导入",
"mySubscriptions": "我的订阅",
"nextResetDays": "距离下次重置天数",
"noLimit": "无限制",
"noReset": "不重置",
"prompt": "提示",
"purchaseSubscription": "购买订阅",
"qrCode": "二维码",
"resetSubscription": "重置订阅",
"resetSuccess": "重置成功",
"scanToSubscribe": "扫码订阅",
"subscriptionUrl": "订阅链接",
"totalTraffic": "总流量",
"unknown": "未知",
"used": "已使用"
}
@@ -0,0 +1,6 @@
{
"all": "全部",
"document": "文档",
"read": "阅读",
"tutorial": "教程"
}
@@ -0,0 +1,7 @@
{
"accountBalance": "账户余额",
"commission": "佣金",
"copySuccess": "邀请链接复制成功",
"giftAmount": "赠送金额",
"inviteCode": "邀请码"
}
+21
View File
@@ -0,0 +1,21 @@
{
"Day": "天",
"global_map_description": "探索无缝的全球连接。选择适合您需求的网络服务,随时随地保持连接。",
"global_map_itle": "全球连接,轻松无忧",
"Hour": "小时",
"locations": "节点位置",
"locations_description": "覆盖多个地区",
"Minute": "分钟",
"Month": "月",
"NoLimit": "无限制",
"product_showcase_description": "让我们帮助您选择最适合您的套餐,享受探索的乐趣。",
"product_showcase_title": "选择您的套餐",
"servers": "服务器",
"servers_description": "全球高性能服务器",
"started": "开始使用",
"subscribe": "订阅",
"users": "用户",
"users_description": "受到全球用户的信赖",
"welcome": "欢迎来到",
"Year": "年"
}
+40
View File
@@ -0,0 +1,40 @@
{
"balanceRecharge": "余额充值",
"cancel": "取消",
"createdAt": "创建时间",
"detail": "详情",
"goToPayment": "去支付",
"name": "商品名称",
"orderClosed": "订单已关闭",
"orderList": "订单列表",
"orderNo": "订单号",
"orderNumber": "订单编号",
"payment": "支付",
"paymentAmount": "支付金额",
"paymentMethod": "支付方式",
"paymentSuccess": "支付成功",
"productList": "商品列表",
"rechargeAmount": "充值金额",
"resetPrice": "重置价格",
"resetTraffic": "重置流量",
"scanToPay": "扫码支付",
"status": {
"0": "状态",
"1": "待支付",
"2": "已支付",
"3": "已取消",
"4": "已关闭",
"5": "已完成"
},
"subscribeNow": "立即订阅",
"timeExpired": "时间已过期",
"type": {
"0": "类型",
"1": "新购",
"2": "续费",
"3": "重置流量",
"4": "充值"
},
"viewDocument": "查看文档",
"waitingForPayment": "等待支付"
}
@@ -0,0 +1,27 @@
{
"stripe": {
"card": {
"loading": "正在加载 Stripe..."
},
"card_name": "",
"card_number": "",
"element_error": "",
"error": "发生错误",
"expiry_date": "",
"loading": "",
"name_placeholder": "",
"name_required": "",
"pay_button": "",
"payment_failed": "",
"processing": "",
"processing_button": "",
"qrcode": {
"alipay": "使用支付宝扫码支付",
"wechat_pay": "使用微信扫码支付"
},
"secure_notice": "",
"security_code": "",
"success_message": "",
"success_title": ""
}
}
@@ -0,0 +1,31 @@
{
"accountSettings": {
"accountSettings": "账户设置",
"newPassword": "新密码",
"passwordMismatch": "密码不匹配",
"repeatNewPassword": "重复新密码",
"updatePassword": "更新密码",
"updateSuccess": "密码更新成功"
},
"notify": {
"balanceChange": "余额变动通知",
"finance": "财务通知",
"login": "登录通知",
"notificationSettings": "通知设置",
"save": "保存",
"subscribe": "订阅通知",
"updateSuccess": "通知设置更新成功"
},
"thirdParty": {
"bind": "绑定",
"bindFailed": "绑定失败",
"bindMobile": "绑定手机",
"bindSuccess": "绑定成功",
"confirm": "确认",
"save": "保存",
"title": "第三方账户",
"unbind": "解绑",
"update": "更新",
"updateSuccess": "更新成功"
}
}
@@ -0,0 +1,63 @@
{
"balanceRecharge": "余额充值",
"billing": {
"billingTitle": "商品账单",
"couponDiscount": "折扣码优惠",
"duration": "套餐时长",
"fee": "手续费",
"gift": "赠金抵扣",
"price": "价格",
"productDiscount": "商品折扣",
"total": "总价"
},
"buy": "购买",
"buyNow": "立即购买",
"buySubscription": "购买订阅",
"coupon": "优惠券",
"Day": "天",
"detail": {
"availableTraffic": "可用流量",
"connectedDevices": "同时连接 IP 数",
"connectionSpeed": "连接速度",
"productDetail": "商品详情"
},
"discount": "折扣",
"discountInfo": "折扣信息",
"emailDomainNotAllowed": "邮箱域名不在白名单中",
"emailInputTitle": "输入您的 {{siteName}} 账户邮箱",
"emailRequired": "请输入您的邮箱地址。",
"enterAmount": "输入充值金额",
"enterCoupon": "输入优惠券代码",
"Hour": "小时",
"invalidEmail": "请输入有效的邮箱地址",
"Minute": "分钟",
"Month": "月",
"NoLimit": "无限制",
"passwordHint": "如果您不输入密码,我们将自动生成一个并发送到您的邮箱。",
"paymentMethod": "支付方式",
"productDescription": "商品描述",
"purchaseDuration": "购买时长",
"recharge": "充值",
"rechargeAmount": "充值金额",
"rechargeDescription": "一键充值,轻松处理",
"rechargeNow": "立即充值",
"renew": "续订",
"renewSubscription": "续订",
"resetPrice": "重置价格",
"resetTraffic": "重置流量",
"resetTrafficDescription": "将流量重置为零,并开始新的计费周期",
"resetTrafficTitle": "重置流量",
"subscriptionNotFound": "未找到订阅",
"unsubscribe": {
"cancel": "取消",
"confirm": "确认",
"confirmUnsubscribe": "确认取消订阅",
"confirmUnsubscribeDescription": "您确定要取消订阅吗?",
"failed": "取消订阅失败。请重试。",
"residualValue": "剩余价值:",
"success": "您已成功取消订阅。",
"unsubscribe": "取消订阅",
"unsubscribeDescription": "请注意:如果您现在取消订阅,订阅的剩余价值将退还到您的账户余额中,可用于您下次的订阅购买或续订。"
},
"Year": "年"
}
@@ -0,0 +1,26 @@
{
"cancel": "取消",
"check": "查看",
"close": "关闭",
"closeSuccess": "关闭成功",
"closeWarning": "您确定要关闭此工单吗?",
"confirm": "确认",
"confirmClose": "确认关闭",
"createSuccess": "创建成功",
"createTicket": "创建工单",
"createTicketDescription": "创建工单描述",
"description": "描述",
"inputPlaceholder": "请输入内容",
"reply": "回复",
"status": {
"0": "状态",
"1": "待回复",
"2": "待跟进",
"3": "已解决",
"4": "已关闭"
},
"submit": "提交",
"ticketList": "工单列表",
"title": "标题",
"updatedAt": "更新时间"
}
@@ -0,0 +1,34 @@
{
"amount": "金额",
"assetOverview": "资产概览",
"balance": "余额",
"commission": "佣金",
"createdAt": "创建时间",
"giftAmount": "赠送金额",
"totalAssets": "总资产",
"type": {
"0": "类型",
"1": "充值",
"2": "提现",
"3": "购买",
"4": "退款",
"5": "奖励",
"6": "佣金",
"231": "自动重置",
"232": "提前重置",
"233": "付费重置",
"321": "充值",
"322": "提现",
"323": "支付",
"324": "退款",
"325": "奖励",
"326": "管理员调整",
"331": "购买",
"332": "续费",
"333": "退款",
"334": "提现",
"335": "管理员调整",
"341": "增加",
"342": "减少"
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#0071cd" d="M0.038 23.657c0.003 1.309 1.066 2.367 2.375 2.364 0 0 24.875-0.056 24.875-0.056 1.42-0.003 2.575 1.132 2.606 2.545 0-0.018 0.003-0.035 0.003-0.053 0 0-0.048-21.313-0.048-21.313-0.003-1.438-1.171-2.601-2.609-2.597 0 0-24.875 0.056-24.875 0.056-1.309 0.003-2.367 1.066-2.364 2.375 0 0 0.038 16.68 0.038 16.68z"></path>
<path fill="#1e9b5f" d="M29.298 16.714c0 0-21.592 9.368-21.592 9.368-0.456 0.198-0.987-0.063-1.187-0.582 0 0-5.232-13.621-5.232-13.621-0.2-0.519 0.008-1.101 0.464-1.299 0 0 21.592-9.368 21.592-9.368 0.456-0.198 0.987 0.063 1.187 0.582 0 0 5.232 13.621 5.232 13.621 0.2 0.519-0.008 1.101-0.464 1.299z"></path>
<path fill="#22b573" d="M29.768 20.094c0 0-24.591 5.455-24.591 5.455-0.519 0.115-1.028-0.237-1.138-0.787 0 0-2.859-14.411-2.859-14.411-0.109-0.55 0.223-1.089 0.743-1.204 0 0 24.591-5.455 24.591-5.455 0.519-0.115 1.028 0.237 1.138 0.787 0 0 2.859 14.411 2.859 14.411 0.109 0.55-0.223 1.089-0.743 1.204z"></path>
<path fill="#008cff" d="M31.432 11.74c-0.003-1.309-1.066-2.367-2.375-2.364 0 0-26.448 0.060-26.448 0.060-1.42 0.003-2.575-1.132-2.606-2.545-0 0.018-0.003 0.035-0.003 0.053 0 0 0.048 21.313 0.048 21.313 0.003 1.438 1.171 2.601 2.609 2.597 0 0 26.448-0.060 26.448-0.060 1.309-0.003 2.367-1.066 2.364-2.375 0 0-0.038-16.68-0.038-16.68z"></path>
<path fill="#0070cd" d="M26.236 22.693c0 0 5.237-0.012 5.237-0.012 0.292-0.001 0.528-0.238 0.527-0.53 0 0-0.009-3.803-0.009-3.803-0.001-0.292-0.238-0.528-0.53-0.528 0 0-5.237 0.012-5.237 0.012-1.342 0.003-2.428 1.094-2.425 2.436 0 0 0 0 0 0 0.003 1.342 1.093 2.428 2.436 2.425z"></path>
<path fill="#ddd" d="M27.349 20.26c0.001 0.618-0.498 1.12-1.116 1.121s-1.12-0.498-1.121-1.116c-0.001-0.618 0.498-1.12 1.116-1.121s1.12 0.498 1.121 1.116z"></path>
<path fill="#fff" d="M26.315 19.817c0 0.208-0.168 0.377-0.376 0.377s-0.377-0.168-0.377-0.376c-0-0.208 0.168-0.377 0.376-0.377s0.377 0.168 0.377 0.376z"></path>
<path fill="#fff" d="M11.717 22.215l-0.152-0.173c-0.747-0.851-2.045-2.149-2.893-2.894l-0.173-0.152c-0.431-0.379-1.007-0.588-1.62-0.588s-1.189 0.209-1.621 0.588l-0.173 0.152c-0.849 0.745-2.147 2.043-2.893 2.894l-0.152 0.173c-0.783 0.893-0.783 2.345 0 3.238l0.152 0.173c0.746 0.85 2.044 2.149 2.893 2.895l0.173 0.152c0.432 0.379 1.007 0.588 1.621 0.588s1.189-0.209 1.621-0.588l0.273-0.24c0.269-0.235 0.391-0.343 0.768-0.712l0.089-0.089-0.014-0.015c0.075-0.151 0.027-0.334-0.113-0.431s-0.331-0.079-0.449 0.043c-0.036 0.034-0.534 0.502-0.842 0.773l-0.173 0.152c-0.305 0.267-0.717 0.414-1.16 0.414s-0.855-0.147-1.16-0.415l-0.173-0.152c-0.831-0.729-2.101-1.999-2.83-2.83l-0.152-0.173c-0.266-0.304-0.413-0.717-0.413-1.163s0.147-0.859 0.413-1.163l0.152-0.173c0.729-0.831 1.998-2.101 2.83-2.831l0.173-0.152c0.305-0.267 0.717-0.415 1.16-0.415s0.855 0.147 1.16 0.415l0.173 0.152c0.831 0.73 2.101 2 2.83 2.83l0.152 0.173c0.554 0.631 0.555 1.738 0.003 2.322l-0.088 0.093c-0.249 0.265-0.4 0.425-0.559 0.473-0.151 0.045-0.342-0.003-0.705-0.176-0.607-0.289-1.466-0.774-1.764-0.943 0.121-0.237 0.398-0.817 0.528-1.398l0.035-0.156h-1.49v-0.182h1.896v-0.694h-1.896v-0.875h-0.697v0.875h-1.896v0.694h1.896v0.182h-1.459v0.693h2.627c-0.057 0.176-0.161 0.394-0.235 0.536-0.271-0.106-0.989-0.355-1.727-0.355-0.465 0-0.857 0.124-1.133 0.359-0.274 0.233-0.419 0.556-0.419 0.936s0.139 0.705 0.401 0.94c0.268 0.24 0.649 0.368 1.103 0.368 0.524 0 1.067-0.23 1.57-0.662 0.196-0.169 0.375-0.357 0.534-0.56 0.281 0.154 1.103 0.604 1.869 1.009 0.5 0.264 0.851 0.317 1.174 0.175 0.307-0.135 0.577-0.442 0.949-0.867l0.006-0.006c0.783-0.893 0.783-2.345-0.001-3.238zM5.602 25.648c-0.737 0-0.794-0.452-0.794-0.59 0.002-0.14 0.055-0.274 0.149-0.379 0.136-0.147 0.348-0.222 0.633-0.222 0.612 0 1.164 0.208 1.414 0.319-0.227 0.265-0.811 0.873-1.401 0.873z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+11
View File
@@ -0,0 +1,11 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#0071cd" d="M0.114 24.238c0.003 1.309 1.066 2.367 2.375 2.364 0 0 24.875-0.056 24.875-0.056 1.42-0.003 2.575 1.132 2.606 2.545 0-0.018 0.003-0.035 0.003-0.053 0 0-0.048-21.313-0.048-21.313-0.003-1.438-1.171-2.601-2.609-2.597 0 0-24.875 0.056-24.875 0.056-1.309 0.003-2.367 1.066-2.364 2.375 0 0 0.038 16.68 0.038 16.68z"></path>
<path fill="#1e9b5f" d="M29.374 17.295c0 0-21.592 9.368-21.592 9.368-0.456 0.198-0.987-0.063-1.187-0.582 0 0-5.232-13.621-5.232-13.621-0.2-0.519 0.008-1.101 0.464-1.299 0 0 21.592-9.368 21.592-9.368 0.456-0.198 0.987 0.063 1.187 0.582 0 0 5.232 13.621 5.232 13.621 0.2 0.519-0.008 1.101-0.464 1.299z"></path>
<path fill="#22b573" d="M29.844 20.675c0 0-24.591 5.455-24.591 5.455-0.519 0.115-1.028-0.237-1.138-0.787 0 0-2.859-14.411-2.859-14.411-0.109-0.55 0.223-1.089 0.743-1.204 0 0 24.591-5.455 24.591-5.455 0.519-0.115 1.028 0.237 1.138 0.787 0 0 2.859 14.411 2.859 14.411 0.109 0.55-0.223 1.089-0.743 1.204z"></path>
<path fill="#008cff" d="M31.509 12.321c-0.003-1.309-1.066-2.367-2.375-2.364 0 0-26.448 0.060-26.448 0.060-1.42 0.003-2.575-1.132-2.606-2.545-0 0.018-0.003 0.035-0.003 0.053 0 0 0.048 21.313 0.048 21.313 0.003 1.438 1.171 2.601 2.609 2.597 0 0 26.448-0.060 26.448-0.060 1.309-0.003 2.367-1.066 2.364-2.375 0 0-0.038-16.68-0.038-16.68z"></path>
<path fill="#0070cd" d="M26.312 23.274c0 0 5.237-0.012 5.237-0.012 0.292-0.001 0.528-0.238 0.527-0.53 0 0-0.009-3.803-0.009-3.803-0.001-0.292-0.238-0.528-0.53-0.528 0 0-5.237 0.012-5.237 0.012-1.342 0.003-2.428 1.094-2.425 2.436 0 0 0 0 0 0 0.003 1.342 1.093 2.428 2.436 2.425z"></path>
<path fill="#ddd" d="M27.425 20.841c0.001 0.618-0.498 1.12-1.116 1.121s-1.12-0.498-1.121-1.116c-0.001-0.618 0.498-1.12 1.116-1.121s1.12 0.498 1.121 1.116z"></path>
<path fill="#fff" d="M26.392 20.398c0 0.208-0.168 0.377-0.376 0.377s-0.377-0.168-0.377-0.376c-0-0.208 0.168-0.377 0.376-0.377s0.377 0.168 0.377 0.376z"></path>
<path fill="#fff" d="M11.794 22.796l-0.152-0.173c-0.747-0.851-2.045-2.149-2.893-2.894l-0.173-0.152c-0.431-0.379-1.007-0.588-1.62-0.588s-1.189 0.209-1.621 0.588l-0.173 0.152c-0.849 0.745-2.147 2.043-2.893 2.894l-0.152 0.173c-0.783 0.893-0.783 2.345 0 3.238l0.152 0.173c0.746 0.85 2.044 2.149 2.893 2.895l0.173 0.152c0.432 0.379 1.007 0.588 1.621 0.588s1.189-0.209 1.621-0.588l0.273-0.24c0.269-0.235 0.391-0.343 0.768-0.712l0.089-0.089-0.014-0.015c0.075-0.151 0.027-0.334-0.113-0.431s-0.331-0.079-0.449 0.043c-0.036 0.034-0.534 0.502-0.842 0.773l-0.173 0.152c-0.305 0.267-0.717 0.414-1.16 0.414s-0.855-0.147-1.16-0.415l-0.173-0.152c-0.831-0.729-2.101-1.999-2.83-2.83l-0.152-0.173c-0.266-0.304-0.413-0.717-0.413-1.163s0.147-0.859 0.413-1.163l0.152-0.173c0.729-0.831 1.998-2.101 2.83-2.831l0.173-0.152c0.305-0.267 0.717-0.415 1.16-0.415s0.855 0.147 1.16 0.415l0.173 0.152c0.831 0.73 2.101 2 2.83 2.83l0.152 0.173c0.554 0.631 0.555 1.738 0.003 2.322l-0.088 0.093c-0.249 0.265-0.4 0.425-0.559 0.473-0.151 0.045-0.342-0.003-0.705-0.176-0.607-0.289-1.466-0.774-1.764-0.943 0.121-0.237 0.398-0.817 0.528-1.398l0.035-0.156h-1.49v-0.182h1.896v-0.694h-1.896v-0.875h-0.697v0.875h-1.896v0.694h1.896v0.182h-1.459v0.693h2.627c-0.057 0.176-0.161 0.394-0.235 0.536-0.271-0.106-0.989-0.355-1.727-0.355-0.465 0-0.857 0.124-1.133 0.359-0.274 0.233-0.419 0.556-0.419 0.936s0.139 0.705 0.401 0.94c0.268 0.24 0.649 0.368 1.103 0.368 0.524 0 1.067-0.23 1.57-0.662 0.196-0.169 0.375-0.357 0.534-0.56 0.281 0.154 1.103 0.604 1.869 1.009 0.5 0.264 0.851 0.317 1.174 0.175 0.307-0.135 0.577-0.442 0.949-0.867l0.006-0.006c0.783-0.893 0.783-2.345-0.001-3.238zM5.679 26.229c-0.737 0-0.794-0.452-0.794-0.59 0.002-0.14 0.055-0.274 0.149-0.379 0.136-0.147 0.348-0.222 0.633-0.222 0.612 0 1.164 0.208 1.414 0.319-0.227 0.265-0.811 0.873-1.401 0.873z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+14
View File
@@ -0,0 +1,14 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#1a1a1a" d="M0.038 23.657c0.003 1.309 1.066 2.367 2.375 2.364 0 0 24.875-0.056 24.875-0.056 1.42-0.003 2.575 1.132 2.606 2.545 0-0.018 0.003-0.035 0.003-0.053 0 0-0.048-21.313-0.048-21.313-0.003-1.438-1.171-2.601-2.609-2.597 0 0-24.875 0.056-24.875 0.056-1.309 0.003-2.367 1.066-2.364 2.375 0 0 0.038 16.68 0.038 16.68z"></path>
<path fill="#1e9b5f" d="M29.298 16.714c0 0-21.592 9.368-21.592 9.368-0.456 0.198-0.987-0.063-1.187-0.582 0 0-5.232-13.621-5.232-13.621-0.2-0.519 0.008-1.101 0.464-1.299 0 0 21.592-9.368 21.592-9.368 0.456-0.198 0.987 0.063 1.187 0.582 0 0 5.232 13.621 5.232 13.621 0.2 0.519-0.008 1.101-0.464 1.299z"></path>
<path fill="#22b573" d="M29.768 20.094c0 0-24.591 5.455-24.591 5.455-0.519 0.115-1.028-0.237-1.138-0.787 0 0-2.859-14.411-2.859-14.411-0.109-0.55 0.223-1.089 0.743-1.204 0 0 24.591-5.455 24.591-5.455 0.519-0.115 1.028 0.237 1.138 0.787 0 0 2.859 14.411 2.859 14.411 0.109 0.55-0.223 1.089-0.743 1.204z"></path>
<path fill="#434343" d="M31.432 11.74c-0.003-1.309-1.066-2.367-2.375-2.364 0 0-26.448 0.060-26.448 0.060-1.42 0.003-2.575-1.132-2.606-2.545-0 0.018-0.003 0.035-0.003 0.053 0 0 0.048 21.313 0.048 21.313 0.003 1.438 1.171 2.601 2.609 2.597 0 0 26.448-0.060 26.448-0.060 1.309-0.003 2.367-1.066 2.364-2.375 0 0-0.038-16.68-0.038-16.68z"></path>
<path fill="#1a1a1a" d="M26.236 22.693c0 0 5.237-0.012 5.237-0.012 0.292-0.001 0.528-0.238 0.527-0.53 0 0-0.009-3.803-0.009-3.803-0.001-0.292-0.238-0.528-0.53-0.528 0 0-5.237 0.012-5.237 0.012-1.342 0.003-2.428 1.094-2.425 2.436 0 0 0 0 0 0 0.003 1.342 1.093 2.428 2.436 2.425z"></path>
<path fill="#ddd" d="M27.349 20.26c0.001 0.618-0.498 1.12-1.116 1.121s-1.12-0.498-1.121-1.116c-0.001-0.618 0.498-1.12 1.116-1.121s1.12 0.498 1.121 1.116z"></path>
<path fill="#fff" d="M26.315 19.817c0 0.208-0.168 0.377-0.376 0.377s-0.377-0.168-0.377-0.376c-0-0.208 0.168-0.377 0.376-0.377s0.377 0.168 0.377 0.376z"></path>
<path fill="#fff" d="M5.778 26.32c-1.082 0-1.96-0.493-1.966-1.101 0 0.233-0.001 0.436-0.001 0.436 0 0.611 0.88 1.107 1.966 1.107s1.966-0.495 1.966-1.107v-0.442c0.001 0.611-0.879 1.107-1.965 1.107zM5.778 27.089c-1.082 0-1.96-0.492-1.966-1.101 0 0.233-0.001 0.436-0.001 0.436 0 0.611 0.88 1.107 1.966 1.107s1.966-0.495 1.966-1.107v-0.441c0.001 0.611-0.879 1.106-1.965 1.106zM8.294 25.807c-0.063 0-0.125-0.002-0.187-0.005-0.002 0.019-0.002 0.039-0.005 0.058 0.013 0.039 0.020 0.081 0.020 0.124v0.26c0.057 0.003 0.115 0.005 0.173 0.005 1.086 0 1.966-0.495 1.966-1.107v-0.441c0 0.611-0.88 1.107-1.967 1.107z"></path>
<path fill="#fff" d="M8.294 25.217c-0.063 0-0.125-0.002-0.187-0.005-0.002 0.019-0.002 0.038-0.005 0.056 0.013 0.038 0.020 0.078 0.020 0.12v0.251c0.057 0.003 0.115 0.005 0.173 0.005 1.086 0 1.965-0.479 1.965-1.071v-0.427c0 0.592-0.88 1.071-1.966 1.071zM8.294 23.726c-1.082 0-1.96-0.477-1.965-1.066 0 0.225-0.001 0.422-0.001 0.422 0 0.253 0.162 0.485 0.43 0.668 0.272 0.073 0.514 0.176 0.715 0.305 0.25 0.062 0.527 0.098 0.82 0.098 1.086 0 1.965-0.479 1.965-1.071v-0.427c0.002 0.593-0.879 1.071-1.965 1.071z"></path>
<path fill="#fff" d="M8.294 24.288c-0.132 0-0.261-0.008-0.386-0.021 0.1 0.136 0.167 0.284 0.195 0.443 0.063 0.004 0.126 0.005 0.19 0.005 1.087 0 1.966-0.48 1.966-1.072v-0.427c0.002 0.592-0.879 1.072-1.966 1.072zM8.294 21.082c-1.086 0-1.966 0.48-1.966 1.072s0.88 1.072 1.966 1.072c1.087 0 1.966-0.48 1.966-1.072s-0.88-1.072-1.966-1.072zM5.778 23.815c-1.086 0-1.966 0.48-1.966 1.072s0.88 1.072 1.966 1.072c1.087 0 1.966-0.48 1.966-1.072s-0.88-1.072-1.966-1.072z"></path>
<path fill="#fff" d="M7.037 18.722c-2.999 0-5.428 2.43-5.428 5.427s2.429 5.427 5.428 5.427c2.996 0 5.425-2.43 5.425-5.427s-2.429-5.427-5.425-5.427v0zM7.037 28.897c-2.624 0-4.749-2.126-4.749-4.748s2.125-4.748 4.749-4.748c2.621 0 4.747 2.126 4.747 4.748s-2.126 4.748-4.747 4.748v0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+12
View File
@@ -0,0 +1,12 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#549f24" d="M0.038 23.657c0.003 1.309 1.066 2.367 2.375 2.364 0 0 24.875-0.056 24.875-0.056 1.42-0.003 2.575 1.132 2.606 2.545 0-0.018 0.003-0.035 0.003-0.053 0 0-0.048-21.313-0.048-21.313-0.003-1.438-1.171-2.601-2.609-2.597 0 0-24.875 0.056-24.875 0.056-1.309 0.003-2.367 1.066-2.364 2.375 0 0 0.038 16.68 0.038 16.68z"></path>
<path fill="#1e9b5f" d="M29.298 16.714c0 0-21.592 9.368-21.592 9.368-0.456 0.198-0.987-0.063-1.187-0.582 0 0-5.232-13.621-5.232-13.621-0.2-0.519 0.008-1.101 0.464-1.299 0 0 21.592-9.368 21.592-9.368 0.456-0.198 0.987 0.063 1.187 0.582 0 0 5.232 13.621 5.232 13.621 0.2 0.519-0.008 1.101-0.464 1.299z"></path>
<path fill="#22b573" d="M29.768 20.094c0 0-24.591 5.455-24.591 5.455-0.519 0.115-1.028-0.237-1.138-0.787 0 0-2.859-14.411-2.859-14.411-0.109-0.55 0.223-1.089 0.743-1.204 0 0 24.591-5.455 24.591-5.455 0.519-0.115 1.028 0.237 1.138 0.787 0 0 2.859 14.411 2.859 14.411 0.109 0.55-0.223 1.089-0.743 1.204z"></path>
<path fill="#85c65c" d="M31.432 11.74c-0.003-1.309-1.066-2.367-2.375-2.364 0 0-26.448 0.060-26.448 0.060-1.42 0.003-2.575-1.132-2.606-2.545-0 0.018-0.003 0.035-0.003 0.053 0 0 0.048 21.313 0.048 21.313 0.003 1.438 1.171 2.601 2.609 2.597 0 0 26.448-0.060 26.448-0.060 1.309-0.003 2.367-1.066 2.364-2.375 0 0-0.038-16.68-0.038-16.68z"></path>
<path fill="#539e24" d="M26.236 22.693c0 0 5.237-0.012 5.237-0.012 0.292-0.001 0.528-0.238 0.527-0.53 0 0-0.009-3.803-0.009-3.803-0.001-0.292-0.238-0.528-0.53-0.528 0 0-5.237 0.012-5.237 0.012-1.342 0.003-2.428 1.094-2.425 2.436 0 0 0 0 0 0 0.003 1.342 1.093 2.428 2.436 2.425z"></path>
<path fill="#ddd" d="M27.349 20.26c0.001 0.618-0.498 1.12-1.116 1.121s-1.12-0.498-1.121-1.116c-0.001-0.618 0.498-1.12 1.116-1.121s1.12 0.498 1.121 1.116z"></path>
<path fill="#fff" d="M26.315 19.817c0 0.208-0.168 0.377-0.376 0.377s-0.377-0.168-0.377-0.376c-0-0.208 0.168-0.377 0.376-0.377s0.377 0.168 0.377 0.376z"></path>
<path fill="#fff" d="M6.88 18.722c-2.999 0-5.428 2.43-5.428 5.427s2.429 5.427 5.428 5.427c2.996 0 5.425-2.43 5.425-5.427s-2.429-5.427-5.425-5.427v0zM6.88 28.897c-2.624 0-4.749-2.126-4.749-4.748s2.125-4.748 4.749-4.748c2.621 0 4.747 2.126 4.747 4.748s-2.126 4.748-4.747 4.748v0z"></path>
<path fill="#fff" d="M7.316 22.976c0.623 0 1.283-0.002 1.92 0.002 0.656 0.003 0.741-0.063 0.868-0.785-1.813-0.003-3.079-0.005-3.914-0.010-1.251-0.008-1.499 0.233-1.789 1.506-0.022 0.096-0.030 0.142-0.046 0.24h-0.857l0.671 0.819c-0.020 0.131-0.048 0.274-0.076 0.404-0.138 0.64 0.028 1.278 0.792 1.278 1.299 0 2.646-0.005 3.916 0 0.291 0 0.376-0.137 0.423-0.312 0.026-0.094 0.105-0.473 0.105-0.473h-2.254c-0.196 0-0.377 0.001-0.546 0-0.248-0.001-0.449-0.103-0.539-0.231-0.195-0.27 0.046-0.661 0.441-0.661l2.589-0.002c0 0 0.314 0.010 0.413-0.055 0.083-0.056 0.143-0.139 0.166-0.234l0.125-0.531h-2.796c-0.633-0.040-0.629-0.455-0.629-0.455 0.014-0.379 0.328-0.503 0.519-0.498 0.163 0.004 0.203 0 0.499 0h-0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,12 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#0071cd" d="M0.038 23.769c0.003 1.328 1.082 2.401 2.409 2.399 0 0 25.24-0.057 25.24-0.057 1.441-0.003 2.612 1.148 2.644 2.582 0-0.018 0.003-0.035 0.003-0.053 0 0-0.049-21.626-0.049-21.626-0.003-1.459-1.188-2.639-2.647-2.635 0 0-25.24 0.057-25.24 0.057-1.328 0.003-2.401 1.082-2.399 2.409 0 0 0.038 16.924 0.038 16.924z"></path>
<path fill="#1e9b5f" d="M29.727 16.724c0 0-21.908 9.505-21.908 9.505-0.462 0.201-1.002-0.064-1.204-0.591 0 0-5.309-13.82-5.309-13.82-0.203-0.527 0.008-1.117 0.471-1.318 0 0 21.908-9.505 21.908-9.505 0.462-0.201 1.002 0.064 1.204 0.591 0 0 5.309 13.82 5.309 13.82 0.203 0.527-0.008 1.117-0.471 1.318z"></path>
<path fill="#22b573" d="M30.204 20.154c0 0-24.952 5.535-24.952 5.535-0.527 0.117-1.043-0.24-1.154-0.798 0 0-2.901-14.622-2.901-14.622-0.111-0.558 0.227-1.105 0.753-1.221 0 0 24.952-5.535 24.952-5.535 0.527-0.117 1.043 0.241 1.154 0.798 0 0 2.901 14.622 2.901 14.622 0.111 0.558-0.227 1.105-0.753 1.221z"></path>
<path fill="#008cff" d="M31.892 11.678c-0.003-1.328-1.082-2.402-2.409-2.399 0 0-26.836 0.060-26.836 0.060-1.441 0.003-2.612-1.148-2.644-2.582-0 0.018-0.003 0.035-0.003 0.053 0 0 0.049 21.626 0.049 21.626 0.003 1.459 1.188 2.639 2.647 2.635 0 0 26.836-0.060 26.836-0.060 1.328-0.003 2.401-1.082 2.399-2.409 0 0-0.038-16.924-0.038-16.924z"></path>
<path fill="#0070cd" d="M26.62 22.791c0 0 5.314-0.012 5.314-0.012 0.296-0.001 0.536-0.241 0.535-0.538 0 0-0.009-3.859-0.009-3.859-0.001-0.296-0.241-0.536-0.538-0.535 0 0-5.314 0.012-5.314 0.012-1.362 0.003-2.463 1.11-2.46 2.472 0 0 0 0 0 0 0.003 1.362 1.11 2.463 2.471 2.46z"></path>
<path fill="#ddd" d="M27.749 20.323c0.001 0.627-0.506 1.136-1.132 1.137s-1.136-0.506-1.137-1.132c-0.001-0.627 0.506-1.136 1.132-1.137s1.136 0.506 1.137 1.132z"></path>
<path fill="#fff" d="M26.7 19.872c0 0.211-0.17 0.382-0.381 0.383s-0.382-0.17-0.383-0.381c-0-0.211 0.17-0.382 0.381-0.383s0.382 0.17 0.383 0.381z"></path>
<path fill="#fff" d="M29.944 19.956c-0.243-0.091-0.375-0.161-0.375-0.272 0-0.094 0.076-0.148 0.212-0.148 0.249 0 0.505 0.097 0.681 0.184l0.099-0.621c-0.14-0.068-0.425-0.178-0.82-0.178-0.279 0-0.512 0.074-0.678 0.212-0.173 0.144-0.262 0.353-0.262 0.604 0 0.457 0.276 0.651 0.724 0.816 0.289 0.104 0.385 0.178 0.385 0.292 0 0.111-0.094 0.175-0.263 0.175-0.21 0-0.555-0.104-0.781-0.238l-0.101 0.628c0.195 0.111 0.553 0.225 0.926 0.225 0.295 0 0.541-0.071 0.707-0.205 0.186-0.148 0.282-0.366 0.282-0.648 0-0.467-0.282-0.661-0.737-0.826h0z"></path>
<path fill="#fff" d="M11.889 22.306l-0.154-0.176c-0.758-0.864-2.075-2.181-2.936-2.936l-0.176-0.154c-0.438-0.385-1.022-0.597-1.644-0.597s-1.207 0.212-1.645 0.597l-0.176 0.154c-0.861 0.756-2.178 2.073-2.936 2.937l-0.154 0.176c-0.794 0.906-0.794 2.38 0 3.286l0.154 0.176c0.757 0.863 2.074 2.18 2.936 2.937l0.176 0.154c0.438 0.385 1.022 0.597 1.645 0.597s1.206-0.212 1.645-0.597l0.277-0.243c0.273-0.239 0.397-0.348 0.779-0.723l0.090-0.090-0.014-0.015c0.076-0.154 0.028-0.339-0.114-0.438s-0.335-0.080-0.456 0.043c-0.036 0.034-0.542 0.509-0.854 0.784l-0.175 0.154c-0.309 0.271-0.728 0.421-1.177 0.421s-0.868-0.149-1.177-0.421l-0.176-0.154c-0.843-0.74-2.131-2.029-2.871-2.872l-0.154-0.176c-0.27-0.308-0.419-0.727-0.419-1.18s0.149-0.872 0.419-1.18l0.154-0.176c0.74-0.843 2.028-2.132 2.871-2.872l0.175-0.154c0.309-0.271 0.727-0.421 1.177-0.421s0.868 0.149 1.177 0.421l0.176 0.154c0.843 0.741 2.132 2.029 2.871 2.872l0.154 0.176c0.562 0.64 0.564 1.763 0.003 2.356l-0.089 0.094c-0.253 0.269-0.406 0.432-0.567 0.48-0.153 0.046-0.347-0.003-0.715-0.178-0.616-0.294-1.487-0.785-1.79-0.957 0.123-0.241 0.403-0.829 0.536-1.418l0.036-0.158h-1.511v-0.185h1.924v-0.704h-1.924v-0.888h-0.707v0.888h-1.924v0.704h1.924v0.185h-1.48v0.704h2.666c-0.058 0.178-0.164 0.4-0.239 0.544-0.275-0.107-1.004-0.36-1.753-0.36-0.472 0-0.869 0.126-1.15 0.364-0.278 0.236-0.425 0.564-0.425 0.95s0.141 0.715 0.407 0.954c0.272 0.244 0.659 0.373 1.119 0.373 0.531 0 1.082-0.233 1.593-0.672 0.199-0.172 0.381-0.362 0.542-0.569 0.285 0.156 1.119 0.613 1.897 1.024 0.507 0.268 0.864 0.321 1.192 0.177 0.312-0.137 0.585-0.448 0.963-0.879l0.006-0.007c0.794-0.906 0.794-2.38-0.001-3.286zM5.684 25.789c-0.748 0-0.806-0.459-0.806-0.599 0.002-0.142 0.056-0.279 0.151-0.385 0.137-0.15 0.354-0.226 0.642-0.226 0.621 0 1.181 0.211 1.434 0.323-0.23 0.269-0.823 0.886-1.422 0.886z"></path>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

@@ -0,0 +1,14 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#006924" d="M0.038 23.657c0.003 1.309 1.066 2.367 2.375 2.364 0 0 24.875-0.056 24.875-0.056 1.42-0.003 2.575 1.132 2.606 2.545 0-0.018 0.003-0.035 0.003-0.053 0 0-0.048-21.313-0.048-21.313-0.003-1.438-1.171-2.601-2.609-2.597 0 0-24.875 0.056-24.875 0.056-1.309 0.003-2.367 1.066-2.364 2.375 0 0 0.038 16.68 0.038 16.68z"></path>
<path fill="#1e9b5f" d="M29.298 16.714c0 0-21.592 9.368-21.592 9.368-0.456 0.198-0.987-0.063-1.187-0.582 0 0-5.232-13.621-5.232-13.621-0.2-0.519 0.008-1.101 0.464-1.299 0 0 21.592-9.368 21.592-9.368 0.456-0.198 0.987 0.063 1.187 0.582 0 0 5.232 13.621 5.232 13.621 0.2 0.519-0.008 1.101-0.464 1.299z"></path>
<path fill="#22b573" d="M29.768 20.094c0 0-24.591 5.455-24.591 5.455-0.519 0.115-1.028-0.237-1.138-0.787 0 0-2.859-14.411-2.859-14.411-0.109-0.55 0.223-1.089 0.743-1.204 0 0 24.591-5.455 24.591-5.455 0.519-0.115 1.028 0.237 1.138 0.787 0 0 2.859 14.411 2.859 14.411 0.109 0.55-0.223 1.089-0.743 1.204z"></path>
<path fill="#01cd47" d="M31.432 11.74c-0.003-1.309-1.066-2.367-2.375-2.364 0 0-26.448 0.060-26.448 0.060-1.42 0.003-2.575-1.132-2.606-2.545-0 0.018-0.003 0.035-0.003 0.053 0 0 0.048 21.313 0.048 21.313 0.003 1.438 1.171 2.601 2.609 2.597 0 0 26.448-0.060 26.448-0.060 1.309-0.003 2.367-1.066 2.364-2.375 0 0-0.038-16.68-0.038-16.68z"></path>
<path fill="#006924" d="M26.236 22.693c0 0 5.237-0.012 5.237-0.012 0.292-0.001 0.528-0.238 0.527-0.53 0 0-0.009-3.803-0.009-3.803-0.001-0.292-0.238-0.528-0.53-0.528 0 0-5.237 0.012-5.237 0.012-1.342 0.003-2.428 1.094-2.425 2.436 0 0 0 0 0 0 0.003 1.342 1.093 2.428 2.436 2.425z"></path>
<path fill="#ddd" d="M27.349 20.26c0.001 0.618-0.498 1.12-1.116 1.121s-1.12-0.498-1.121-1.116c-0.001-0.618 0.498-1.12 1.116-1.121s1.12 0.498 1.121 1.116z"></path>
<path fill="#fff" d="M26.315 19.817c0 0.208-0.168 0.377-0.376 0.377s-0.377-0.168-0.377-0.376c-0-0.208 0.168-0.377 0.376-0.377s0.377 0.168 0.377 0.376z"></path>
<path fill="#fff" d="M10.874 24.652c0-0.935-0.977-1.694-2.184-1.694-1.206 0-2.184 0.758-2.184 1.694 0 0.93 0.967 1.684 2.164 1.693 0.521 0.871 0.998 0.704 0.998 0.704s-0.372-0.246-0.411-0.762c0.931-0.194 1.617-0.852 1.617-1.635v0zM7.964 24.388c-0.141 0-0.255-0.114-0.255-0.254s0.115-0.254 0.255-0.254c0.141 0 0.255 0.114 0.255 0.254s-0.115 0.254-0.255 0.254v0zM9.363 24.388c-0.142 0-0.255-0.114-0.255-0.254s0.113-0.254 0.255-0.254c0.141 0 0.254 0.114 0.254 0.254s-0.113 0.254-0.254 0.254v0z"></path>
<path fill="#fff" d="M8.36 22.6c-0.363-0.793-1.347-1.363-2.505-1.363-1.467 0-2.657 0.914-2.657 2.041 0 0.966 0.874 1.775 2.046 1.986-0.022 0.668-0.504 0.987-0.504 0.987s0.607 0.214 1.258-0.932c0 0-0.003-0.001-0.009-0.002 0.123-0.006 0.244-0.017 0.362-0.034-0.157-0.248-0.246-0.527-0.246-0.822-0-0.991 0.996-1.801 2.254-1.861v0zM5.062 22.937c-0.186 0-0.335-0.149-0.335-0.332 0-0.185 0.15-0.333 0.335-0.333 0.184 0 0.334 0.149 0.334 0.333 0 0.183-0.149 0.332-0.334 0.332v0zM6.413 22.605c0-0.185 0.15-0.333 0.335-0.333 0.184 0 0.334 0.149 0.334 0.333 0 0.184-0.15 0.332-0.334 0.332-0.186 0-0.335-0.149-0.335-0.332v0z"></path>
<path fill="#fff" d="M7.037 18.722c-2.999 0-5.428 2.43-5.428 5.427s2.429 5.427 5.428 5.427c2.996 0 5.425-2.43 5.425-5.427s-2.429-5.427-5.425-5.427v0zM7.037 28.897c-2.624 0-4.749-2.126-4.749-4.748s2.125-4.748 4.749-4.748c2.621 0 4.747 2.126 4.747 4.748s-2.126 4.748-4.747 4.748v0z"></path>
<path fill="#fff" d="M29.354 19.899c-0.239-0.090-0.37-0.159-0.37-0.268 0-0.093 0.075-0.145 0.209-0.145 0.245 0 0.497 0.096 0.671 0.182l0.098-0.612c-0.138-0.067-0.419-0.175-0.808-0.175-0.275 0-0.504 0.073-0.668 0.209-0.17 0.142-0.259 0.348-0.259 0.596 0 0.45 0.272 0.642 0.713 0.804 0.285 0.103 0.38 0.175 0.38 0.288 0 0.109-0.093 0.172-0.259 0.172-0.207 0-0.547-0.103-0.77-0.235l-0.099 0.619c0.192 0.109 0.545 0.222 0.912 0.222 0.291 0 0.534-0.070 0.697-0.202 0.183-0.145 0.278-0.361 0.278-0.639 0-0.46-0.278-0.652-0.726-0.814h0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+13
View File
@@ -0,0 +1,13 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#006924" d="M0.038 23.657c0.003 1.309 1.066 2.367 2.375 2.364 0 0 24.875-0.056 24.875-0.056 1.42-0.003 2.575 1.132 2.606 2.545 0-0.018 0.003-0.035 0.003-0.053 0 0-0.048-21.313-0.048-21.313-0.003-1.438-1.171-2.601-2.609-2.597 0 0-24.875 0.056-24.875 0.056-1.309 0.003-2.367 1.066-2.364 2.375 0 0 0.038 16.68 0.038 16.68z"></path>
<path fill="#1e9b5f" d="M29.298 16.714c0 0-21.592 9.368-21.592 9.368-0.456 0.198-0.987-0.063-1.187-0.582 0 0-5.232-13.621-5.232-13.621-0.2-0.519 0.008-1.101 0.464-1.299 0 0 21.592-9.368 21.592-9.368 0.456-0.198 0.987 0.063 1.187 0.582 0 0 5.232 13.621 5.232 13.621 0.2 0.519-0.008 1.101-0.464 1.299z"></path>
<path fill="#22b573" d="M29.768 20.094c0 0-24.591 5.455-24.591 5.455-0.519 0.115-1.028-0.237-1.138-0.787 0 0-2.859-14.411-2.859-14.411-0.109-0.55 0.223-1.089 0.743-1.204 0 0 24.591-5.455 24.591-5.455 0.519-0.115 1.028 0.237 1.138 0.787 0 0 2.859 14.411 2.859 14.411 0.109 0.55-0.223 1.089-0.743 1.204z"></path>
<path fill="#01cd47" d="M31.432 11.74c-0.003-1.309-1.066-2.367-2.375-2.364 0 0-26.448 0.060-26.448 0.060-1.42 0.003-2.575-1.132-2.606-2.545-0 0.018-0.003 0.035-0.003 0.053 0 0 0.048 21.313 0.048 21.313 0.003 1.438 1.171 2.601 2.609 2.597 0 0 26.448-0.060 26.448-0.060 1.309-0.003 2.367-1.066 2.364-2.375 0 0-0.038-16.68-0.038-16.68z"></path>
<path fill="#006924" d="M26.236 22.693c0 0 5.237-0.012 5.237-0.012 0.292-0.001 0.528-0.238 0.527-0.53 0 0-0.009-3.803-0.009-3.803-0.001-0.292-0.238-0.528-0.53-0.528 0 0-5.237 0.012-5.237 0.012-1.342 0.003-2.428 1.094-2.425 2.436 0 0 0 0 0 0 0.003 1.342 1.093 2.428 2.436 2.425z"></path>
<path fill="#ddd" d="M27.349 20.26c0.001 0.618-0.498 1.12-1.116 1.121s-1.12-0.498-1.121-1.116c-0.001-0.618 0.498-1.12 1.116-1.121s1.12 0.498 1.121 1.116z"></path>
<path fill="#fff" d="M26.315 19.817c0 0.208-0.168 0.377-0.376 0.377s-0.377-0.168-0.377-0.376c-0-0.208 0.168-0.377 0.376-0.377s0.377 0.168 0.377 0.376z"></path>
<path fill="#fff" d="M10.874 24.652c0-0.935-0.977-1.694-2.184-1.694-1.206 0-2.184 0.758-2.184 1.694 0 0.93 0.967 1.684 2.164 1.693 0.521 0.871 0.998 0.704 0.998 0.704s-0.372-0.246-0.411-0.762c0.931-0.194 1.617-0.852 1.617-1.635v0zM7.964 24.388c-0.141 0-0.255-0.114-0.255-0.254s0.115-0.254 0.255-0.254c0.141 0 0.255 0.114 0.255 0.254s-0.115 0.254-0.255 0.254v0zM9.363 24.388c-0.142 0-0.255-0.114-0.255-0.254s0.113-0.254 0.255-0.254c0.141 0 0.254 0.114 0.254 0.254s-0.113 0.254-0.254 0.254v0z"></path>
<path fill="#fff" d="M8.36 22.6c-0.363-0.793-1.347-1.363-2.505-1.363-1.467 0-2.657 0.914-2.657 2.041 0 0.966 0.874 1.775 2.046 1.986-0.022 0.668-0.504 0.987-0.504 0.987s0.607 0.214 1.258-0.932c0 0-0.003-0.001-0.009-0.002 0.123-0.006 0.244-0.017 0.362-0.034-0.157-0.248-0.246-0.527-0.246-0.822-0-0.991 0.996-1.801 2.254-1.861v0zM5.062 22.937c-0.186 0-0.335-0.149-0.335-0.332 0-0.185 0.15-0.333 0.335-0.333 0.184 0 0.334 0.149 0.334 0.333 0 0.183-0.149 0.332-0.334 0.332v0zM6.413 22.605c0-0.185 0.15-0.333 0.335-0.333 0.184 0 0.334 0.149 0.334 0.333 0 0.184-0.15 0.332-0.334 0.332-0.186 0-0.335-0.149-0.335-0.332v0z"></path>
<path fill="#fff" d="M7.037 18.722c-2.999 0-5.428 2.43-5.428 5.427s2.429 5.427 5.428 5.427c2.996 0 5.425-2.43 5.425-5.427s-2.429-5.427-5.425-5.427v0zM7.037 28.897c-2.624 0-4.749-2.126-4.749-4.748s2.125-4.748 4.749-4.748c2.621 0 4.747 2.126 4.747 4.748s-2.126 4.748-4.747 4.748v0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+34
View File
@@ -0,0 +1,34 @@
{
"name": "PPanel",
"short_name": "PPanel",
"icons": [
{
"src": "/pwa-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/pwa-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/pwa-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/pwa-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"background_color": "#FFFFFF",
"theme_color": "#FFFFFF"
}
+49
View File
@@ -0,0 +1,49 @@
import { formatBytes } from "@workspace/ui/utils/formatting";
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "@/stores/global";
type DisplayType = "currency" | "traffic" | "number" | "trafficSpeed";
interface DisplayProps<T> {
value?: T;
unlimited?: boolean;
type?: DisplayType;
}
export function Display<T extends number | undefined | null>({
value = 0,
unlimited = false,
type = "number",
}: DisplayProps<T>): string {
const { t } = useTranslation("components");
const { common } = useGlobalStore();
const { currency } = common;
if (type === "currency") {
const formattedValue = `${currency?.currency_symbol ?? ""}${unitConversion("centsToDollars", value as number)?.toFixed(2) ?? "0.00"}`;
return formattedValue;
}
if (
["traffic", "trafficSpeed", "number"].includes(type) &&
unlimited &&
!value
) {
return t("unlimited");
}
if (type === "traffic") {
return value ? formatBytes(value) : "0";
}
if (type === "trafficSpeed") {
return value ? `${formatBytes(value).replace("B", "b")}ps` : "0";
}
if (type === "number") {
return value ? value.toString() : "0";
}
return "0";
}
+9
View File
@@ -0,0 +1,9 @@
export const fallbackLng = "en-US";
export const supportedLngs = ["en-US", "zh-CN"];
export const CDN_URL =
import.meta.env.VITE_CDN_URL || "https://cdn.jsdmirror.com";
export const TUTORIAL_DOCUMENT =
import.meta.env.VITE_TUTORIAL_DOCUMENT || "true";
export const USER_EMAIL = import.meta.env.VITE_USER_EMAIL;
export const USER_PASSWORD = import.meta.env.VITE_USER_PASSWORD;
+126
View File
@@ -0,0 +1,126 @@
"use client";
import { Separator } from "@workspace/ui/components/separator";
import { Icon } from "@workspace/ui/composed/icon";
import { Fragment, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "@/stores/global";
interface CustomData {
community?: {
discord?: string;
facebook?: string;
github?: string;
instagram?: string;
linkedin?: string;
telegram?: string;
twitter?: string;
};
contacts?: {
address?: string;
email?: string;
telephone?: string;
};
website?: string;
}
export default function Footer() {
const { t } = useTranslation("components");
const { common } = useGlobalStore();
const { site } = common;
const customData = useMemo<CustomData>(() => {
try {
return JSON.parse(site.custom_data || "{}");
} catch {
return {};
}
}, [site.custom_data]);
const links = useMemo(
() => [
{
name: "email",
icon: "uil:envelope",
href: customData.contacts?.email
? `mailto:${customData.contacts.email}`
: undefined,
},
{
name: "telegram",
icon: "uil:telegram",
href: customData.community?.telegram,
},
{
name: "twitter",
icon: "uil:twitter",
href: customData.community?.twitter,
},
{
name: "discord",
icon: "uil:discord",
href: customData.community?.discord,
},
{
name: "instagram",
icon: "uil:instagram",
href: customData.community?.instagram,
},
{
name: "linkedin",
icon: "uil:linkedin",
href: customData.community?.linkedin,
},
{
name: "github",
icon: "uil:github",
href: customData.community?.github,
},
{
name: "facebook",
icon: "uil:facebook",
href: customData.community?.facebook,
},
],
[customData]
);
return (
<footer>
<Separator className="my-14" />
<div className="container mb-14 flex flex-wrap justify-between gap-4 text-muted-foreground text-sm">
<nav className="flex flex-wrap items-center gap-2">
{links
.filter((item) => item.href)
.map((item, index) => (
<Fragment key={index}>
{index !== 0 && <Separator orientation="vertical" />}
<a
aria-label={t(
`footer.social.${item.name}`,
`Visit our ${item.name}`
)}
href={item.href}
rel="noopener noreferrer"
target="_blank"
>
<Icon className="size-5 text-foreground" icon={item.icon} />
</a>
</Fragment>
))}
</nav>
<div>
<strong className="text-foreground">{site.site_name}</strong> ©{" "}
{t("footer.copyright", "All rights reserved")}.
<div>
<a className="underline" href="/tos">
{t("footer.tos", "Terms of Service")}
</a>
<a className="ml-2 underline" href="/privacy-policy">
{t("footer.privacyPolicy", "Privacy Policy")}
</a>
</div>
</div>
</div>
</footer>
);
}
+46
View File
@@ -0,0 +1,46 @@
import { Link } from "@tanstack/react-router";
import { buttonVariants } from "@workspace/ui/components/button";
import { LanguageSwitch } from "@workspace/ui/composed/language-switch";
import { ThemeSwitch } from "@workspace/ui/composed/theme-switch";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "@/stores/global";
import { UserNav } from "./user-nav";
export default function Header() {
const { t } = useTranslation("components");
const { common, user } = useGlobalStore();
const { site } = common;
const Logo = (
<Link className="flex items-center gap-2 font-bold text-lg" to="/">
{site.site_logo && (
<img alt="logo" height={36} src={site.site_logo} width={36} />
)}
<span className="">{site.site_name}</span>
</Link>
);
return (
<header className="sticky top-0 z-50 border-b backdrop-blur-md">
<div className="container flex h-16 items-center justify-between">
<nav className="flex-col gap-6 font-medium text-lg md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6">
{Logo}
</nav>
<div className="flex flex-1 items-center justify-end gap-2">
<LanguageSwitch />
<ThemeSwitch />
<UserNav />
{!user && (
<Link
className={buttonVariants({
size: "sm",
})}
to="/auth"
>
{t("loginRegister", "Login / Register")}
</Link>
)}
</div>
</div>
</header>
);
}
+132
View File
@@ -0,0 +1,132 @@
import { useTranslation } from "react-i18next";
export interface NavItem {
title: string;
url: string;
icon: string;
}
export interface NavGroup {
title: string;
url?: string;
icon?: string;
items?: NavItem[];
}
export function useNavs() {
const { t } = useTranslation("components");
const navs: NavGroup[] = [
{
title: t("menu.dashboard", "Dashboard"),
url: "/dashboard",
icon: "uil:dashboard",
},
{
title: t("menu.personal", "Personal"),
items: [
{
title: t("menu.profile", "User Detail"),
url: "/profile",
icon: "uil:user",
},
],
},
{
title: t("menu.server", "Server Management"),
items: [
{
url: "/subscribe",
icon: "uil:shop",
title: t("menu.subscribe", "Subscribe"),
},
],
},
{
title: t("menu.finance", "Commerce"),
items: [
{
url: "/order",
icon: "uil:notes",
title: t("menu.order", "Order Management"),
},
{
url: "/wallet",
icon: "uil:wallet",
title: t("menu.wallet", "Balance"),
},
{
url: "/affiliate",
icon: "uil:users-alt",
title: t("menu.affiliate", "Commission"),
},
],
},
{
title: t("menu.help", "Users & Support"),
items: [
{
url: "/document",
icon: "uil:book-alt",
title: t("menu.document", "Document Management"),
},
{
url: "/announcement",
icon: "uil:megaphone",
title: t("menu.announcement", "Announcement Management"),
},
{
url: "/ticket",
icon: "uil:message",
title: t("menu.ticket", "Ticket Management"),
},
],
},
];
return navs;
}
export function useFindNavByUrl(url: string) {
const navs = useNavs();
for (const nav of navs) {
if (nav.url && nav.url === url) {
return [nav];
}
if (nav.items) {
const current = nav.items.find((item) => item.url === url);
if (current) {
return [nav, current];
}
}
}
return [];
}
export function useNavItems() {
const { t } = useTranslation("components");
return [
{
url: "/profile",
icon: "uil:user",
title: t("menu.profile", "User Detail"),
},
{
url: "/subscribe",
icon: "uil:shop",
title: t("menu.subscribe", "Subscribe"),
},
{
url: "/order",
icon: "uil:notes",
title: t("menu.order", "Order Management"),
},
{
url: "/wallet",
icon: "uil:wallet",
title: t("menu.wallet", "Balance"),
},
];
}
+120
View File
@@ -0,0 +1,120 @@
"use client";
import { useNavigate } from "@tanstack/react-router";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@workspace/ui/components/avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@workspace/ui/components/dropdown-menu";
import { Icon } from "@workspace/ui/composed/icon";
import { removeCookie } from "@workspace/ui/lib/cookies";
import { useTranslation } from "react-i18next";
import { useNavs } from "@/layout/navs";
import { useGlobalStore } from "@/stores/global";
export function UserNav() {
const { t } = useTranslation("components");
const { user, setUser } = useGlobalStore();
const navigate = useNavigate();
const navs = useNavs();
const handleLogout = () => {
removeCookie("Authorization");
setUser(undefined);
navigate({ to: "/" });
};
if (user) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="flex cursor-pointer items-center gap-2 rounded-full border bg-background px-2 py-1.5 transition-colors duration-200 hover:bg-accent">
<Avatar className="h-6 w-6">
<AvatarImage
alt={user?.avatar ?? ""}
className="object-cover"
src={user?.auth_methods?.[0]?.auth_identifier ?? ""}
/>
<AvatarFallback className="bg-gradient-to-br from-primary/90 to-primary font-medium text-background">
{user?.auth_methods?.[0]?.auth_identifier
.toUpperCase()
.charAt(0)}
</AvatarFallback>
</Avatar>
<span className="max-w-[40px] truncate text-sm sm:max-w-[100px]">
{user?.auth_methods?.[0]?.auth_identifier.split("@")[0]}
</span>
<Icon
className="size-4 text-muted-foreground"
icon="mdi:chevron-down"
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64" forceMount>
<div className="flex items-center justify-start gap-2 p-2">
<Avatar className="h-10 w-10">
<AvatarImage
alt={user?.avatar ?? ""}
className="object-cover"
src={user?.avatar ?? ""}
/>
<AvatarFallback className="bg-gradient-to-br from-primary/90 to-primary text-background">
{user?.auth_methods?.[0]?.auth_identifier
.toUpperCase()
.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col space-y-0.5">
<p className="font-medium text-sm leading-none">
{user?.auth_methods?.[0]?.auth_identifier.split("@")[0]}
</p>
<p className="text-muted-foreground text-xs">
{user?.auth_methods?.[0]?.auth_identifier}
</p>
</div>
</div>
<DropdownMenuSeparator />
{navs.map((nav) => (
<DropdownMenuGroup key={nav.title}>
{(nav.items || [nav]).map((item) => (
<DropdownMenuItem
className="flex cursor-pointer items-center gap-2 py-2"
key={item.title}
onClick={() => {
navigate({ to: item.url });
}}
>
<Icon
className="size-4 flex-none text-muted-foreground"
icon={item.icon as string}
/>
<span className="flex-grow truncate">{item.title}</span>
<Icon
className="size-4 text-muted-foreground opacity-50"
icon="lucide:chevron-right"
/>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
className="flex cursor-pointer items-center gap-2 py-2 text-destructive focus:text-destructive"
onClick={handleLogout}
>
<Icon className="size-4 flex-none" icon="uil:exit" />
<span className="flex-grow">{t("menu.logout", "Logout")}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
}
+86
View File
@@ -0,0 +1,86 @@
import { createRouter, RouterProvider } from "@tanstack/react-router";
import {
TanStackQueryContext,
TanStackQueryProvider,
} from "@workspace/ui/integrations/tanstack-query";
import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
// Import the generated route tree
import { routeTree } from "./routeTree.gen";
// Styles
import "@workspace/ui/globals.css";
import { DirectionProvider } from "@workspace/ui/integrations/direction";
import { LanguageProvider } from "@workspace/ui/integrations/language";
import { ThemeProvider } from "@workspace/ui/integrations/theme";
import { initializeI18n } from "@workspace/ui/lib/i18n";
import { fallbackLng, supportedLngs } from "./config/index.ts";
// Report web vitals
import reportWebVitals from "./reportWebVitals.ts";
// Common utilities
import { Logout } from "./utils/common.ts";
initializeI18n({
supportedLngs,
fallbackLng,
ns: [
"affiliate",
"auth",
"components",
"dashboard",
"document",
"layout",
"main",
"order",
"payment",
"profile",
"subscribe",
"ticket",
"wallet",
],
});
window.logout = Logout;
// Create a new router instance
const TanStackQueryProviderContext = TanStackQueryContext();
const router = createRouter({
routeTree,
context: {
...TanStackQueryProviderContext,
},
defaultPreload: "intent",
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
});
// Register the router instance for type safety
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
// Render the app
const rootElement = document.getElementById("app");
if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<TanStackQueryProvider {...TanStackQueryProviderContext}>
<LanguageProvider supportedLanguages={supportedLngs}>
<ThemeProvider>
<DirectionProvider>
<RouterProvider router={router} />
</DirectionProvider>
</ThemeProvider>
</LanguageProvider>
</TanStackQueryProvider>
</StrictMode>
);
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
+13
View File
@@ -0,0 +1,13 @@
const reportWebVitals = (onPerfEntry?: () => void) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import("web-vitals").then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
onCLS(onPerfEntry);
onINP(onPerfEntry);
onFCP(onPerfEntry);
onLCP(onPerfEntry);
onTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+546
View File
@@ -0,0 +1,546 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { createFileRoute } from '@tanstack/react-router'
import { Route as rootRouteImport } from './routes/__root'
import { Route as mainPurchasingOrderIndexRouteImport } from './routes/(main)/purchasing/order/index'
const mainRouteLazyRouteImport = createFileRoute('/(main)')()
const AuthIndexLazyRouteImport = createFileRoute('/auth/')()
const mainIndexLazyRouteImport = createFileRoute('/(main)/')()
const OauthPlatformLazyRouteImport = createFileRoute('/oauth/$platform')()
const BindPlatformLazyRouteImport = createFileRoute('/bind/$platform')()
const mainTosLazyRouteImport = createFileRoute('/(main)/tos')()
const mainPrivacyPolicyLazyRouteImport = createFileRoute(
'/(main)/privacy-policy',
)()
const mainPaymentLazyRouteImport = createFileRoute('/(main)/payment')()
const mainuserRouteLazyRouteImport = createFileRoute('/(main)/(user)')()
const mainPurchasingIndexLazyRouteImport = createFileRoute(
'/(main)/purchasing/',
)()
const mainuserWalletLazyRouteImport = createFileRoute('/(main)/(user)/wallet')()
const mainuserTicketLazyRouteImport = createFileRoute('/(main)/(user)/ticket')()
const mainuserSubscribeLazyRouteImport = createFileRoute(
'/(main)/(user)/subscribe',
)()
const mainuserProfileLazyRouteImport = createFileRoute(
'/(main)/(user)/profile',
)()
const mainuserOrderLazyRouteImport = createFileRoute('/(main)/(user)/order')()
const mainuserDocumentLazyRouteImport = createFileRoute(
'/(main)/(user)/document',
)()
const mainuserDashboardLazyRouteImport = createFileRoute(
'/(main)/(user)/dashboard',
)()
const mainuserAnnouncementLazyRouteImport = createFileRoute(
'/(main)/(user)/announcement',
)()
const mainuserAffiliateLazyRouteImport = createFileRoute(
'/(main)/(user)/affiliate',
)()
const mainRouteLazyRoute = mainRouteLazyRouteImport
.update({
id: '/(main)',
getParentRoute: () => rootRouteImport,
} as any)
.lazy(() => import('./routes/(main)/route.lazy').then((d) => d.Route))
const AuthIndexLazyRoute = AuthIndexLazyRouteImport.update({
id: '/auth/',
path: '/auth/',
getParentRoute: () => rootRouteImport,
} as any).lazy(() => import('./routes/auth/index.lazy').then((d) => d.Route))
const mainIndexLazyRoute = mainIndexLazyRouteImport
.update({
id: '/',
path: '/',
getParentRoute: () => mainRouteLazyRoute,
} as any)
.lazy(() => import('./routes/(main)/index.lazy').then((d) => d.Route))
const OauthPlatformLazyRoute = OauthPlatformLazyRouteImport.update({
id: '/oauth/$platform',
path: '/oauth/$platform',
getParentRoute: () => rootRouteImport,
} as any).lazy(() =>
import('./routes/oauth/$platform.lazy').then((d) => d.Route),
)
const BindPlatformLazyRoute = BindPlatformLazyRouteImport.update({
id: '/bind/$platform',
path: '/bind/$platform',
getParentRoute: () => rootRouteImport,
} as any).lazy(() =>
import('./routes/bind/$platform.lazy').then((d) => d.Route),
)
const mainTosLazyRoute = mainTosLazyRouteImport
.update({
id: '/tos',
path: '/tos',
getParentRoute: () => mainRouteLazyRoute,
} as any)
.lazy(() => import('./routes/(main)/tos.lazy').then((d) => d.Route))
const mainPrivacyPolicyLazyRoute = mainPrivacyPolicyLazyRouteImport
.update({
id: '/privacy-policy',
path: '/privacy-policy',
getParentRoute: () => mainRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/privacy-policy.lazy').then((d) => d.Route),
)
const mainPaymentLazyRoute = mainPaymentLazyRouteImport
.update({
id: '/payment',
path: '/payment',
getParentRoute: () => mainRouteLazyRoute,
} as any)
.lazy(() => import('./routes/(main)/payment.lazy').then((d) => d.Route))
const mainuserRouteLazyRoute = mainuserRouteLazyRouteImport
.update({
id: '/(user)',
getParentRoute: () => mainRouteLazyRoute,
} as any)
.lazy(() => import('./routes/(main)/(user)/route.lazy').then((d) => d.Route))
const mainPurchasingIndexLazyRoute = mainPurchasingIndexLazyRouteImport
.update({
id: '/purchasing/',
path: '/purchasing/',
getParentRoute: () => mainRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/purchasing/index.lazy').then((d) => d.Route),
)
const mainuserWalletLazyRoute = mainuserWalletLazyRouteImport
.update({
id: '/wallet',
path: '/wallet',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() => import('./routes/(main)/(user)/wallet.lazy').then((d) => d.Route))
const mainuserTicketLazyRoute = mainuserTicketLazyRouteImport
.update({
id: '/ticket',
path: '/ticket',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() => import('./routes/(main)/(user)/ticket.lazy').then((d) => d.Route))
const mainuserSubscribeLazyRoute = mainuserSubscribeLazyRouteImport
.update({
id: '/subscribe',
path: '/subscribe',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/(user)/subscribe.lazy').then((d) => d.Route),
)
const mainuserProfileLazyRoute = mainuserProfileLazyRouteImport
.update({
id: '/profile',
path: '/profile',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/(user)/profile.lazy').then((d) => d.Route),
)
const mainuserOrderLazyRoute = mainuserOrderLazyRouteImport
.update({
id: '/order',
path: '/order',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() => import('./routes/(main)/(user)/order.lazy').then((d) => d.Route))
const mainuserDocumentLazyRoute = mainuserDocumentLazyRouteImport
.update({
id: '/document',
path: '/document',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/(user)/document.lazy').then((d) => d.Route),
)
const mainuserDashboardLazyRoute = mainuserDashboardLazyRouteImport
.update({
id: '/dashboard',
path: '/dashboard',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/(user)/dashboard.lazy').then((d) => d.Route),
)
const mainuserAnnouncementLazyRoute = mainuserAnnouncementLazyRouteImport
.update({
id: '/announcement',
path: '/announcement',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/(user)/announcement.lazy').then((d) => d.Route),
)
const mainuserAffiliateLazyRoute = mainuserAffiliateLazyRouteImport
.update({
id: '/affiliate',
path: '/affiliate',
getParentRoute: () => mainuserRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/(user)/affiliate.lazy').then((d) => d.Route),
)
const mainPurchasingOrderIndexRoute = mainPurchasingOrderIndexRouteImport
.update({
id: '/purchasing/order/',
path: '/purchasing/order/',
getParentRoute: () => mainRouteLazyRoute,
} as any)
.lazy(() =>
import('./routes/(main)/purchasing/order/index.lazy').then((d) => d.Route),
)
export interface FileRoutesByFullPath {
'/': typeof mainIndexLazyRoute
'/payment': typeof mainPaymentLazyRoute
'/privacy-policy': typeof mainPrivacyPolicyLazyRoute
'/tos': typeof mainTosLazyRoute
'/bind/$platform': typeof BindPlatformLazyRoute
'/oauth/$platform': typeof OauthPlatformLazyRoute
'/auth': typeof AuthIndexLazyRoute
'/affiliate': typeof mainuserAffiliateLazyRoute
'/announcement': typeof mainuserAnnouncementLazyRoute
'/dashboard': typeof mainuserDashboardLazyRoute
'/document': typeof mainuserDocumentLazyRoute
'/order': typeof mainuserOrderLazyRoute
'/profile': typeof mainuserProfileLazyRoute
'/subscribe': typeof mainuserSubscribeLazyRoute
'/ticket': typeof mainuserTicketLazyRoute
'/wallet': typeof mainuserWalletLazyRoute
'/purchasing': typeof mainPurchasingIndexLazyRoute
'/purchasing/order': typeof mainPurchasingOrderIndexRoute
}
export interface FileRoutesByTo {
'/': typeof mainIndexLazyRoute
'/payment': typeof mainPaymentLazyRoute
'/privacy-policy': typeof mainPrivacyPolicyLazyRoute
'/tos': typeof mainTosLazyRoute
'/bind/$platform': typeof BindPlatformLazyRoute
'/oauth/$platform': typeof OauthPlatformLazyRoute
'/auth': typeof AuthIndexLazyRoute
'/affiliate': typeof mainuserAffiliateLazyRoute
'/announcement': typeof mainuserAnnouncementLazyRoute
'/dashboard': typeof mainuserDashboardLazyRoute
'/document': typeof mainuserDocumentLazyRoute
'/order': typeof mainuserOrderLazyRoute
'/profile': typeof mainuserProfileLazyRoute
'/subscribe': typeof mainuserSubscribeLazyRoute
'/ticket': typeof mainuserTicketLazyRoute
'/wallet': typeof mainuserWalletLazyRoute
'/purchasing': typeof mainPurchasingIndexLazyRoute
'/purchasing/order': typeof mainPurchasingOrderIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/(main)': typeof mainRouteLazyRouteWithChildren
'/(main)/(user)': typeof mainuserRouteLazyRouteWithChildren
'/(main)/payment': typeof mainPaymentLazyRoute
'/(main)/privacy-policy': typeof mainPrivacyPolicyLazyRoute
'/(main)/tos': typeof mainTosLazyRoute
'/bind/$platform': typeof BindPlatformLazyRoute
'/oauth/$platform': typeof OauthPlatformLazyRoute
'/(main)/': typeof mainIndexLazyRoute
'/auth/': typeof AuthIndexLazyRoute
'/(main)/(user)/affiliate': typeof mainuserAffiliateLazyRoute
'/(main)/(user)/announcement': typeof mainuserAnnouncementLazyRoute
'/(main)/(user)/dashboard': typeof mainuserDashboardLazyRoute
'/(main)/(user)/document': typeof mainuserDocumentLazyRoute
'/(main)/(user)/order': typeof mainuserOrderLazyRoute
'/(main)/(user)/profile': typeof mainuserProfileLazyRoute
'/(main)/(user)/subscribe': typeof mainuserSubscribeLazyRoute
'/(main)/(user)/ticket': typeof mainuserTicketLazyRoute
'/(main)/(user)/wallet': typeof mainuserWalletLazyRoute
'/(main)/purchasing/': typeof mainPurchasingIndexLazyRoute
'/(main)/purchasing/order/': typeof mainPurchasingOrderIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/payment'
| '/privacy-policy'
| '/tos'
| '/bind/$platform'
| '/oauth/$platform'
| '/auth'
| '/affiliate'
| '/announcement'
| '/dashboard'
| '/document'
| '/order'
| '/profile'
| '/subscribe'
| '/ticket'
| '/wallet'
| '/purchasing'
| '/purchasing/order'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/payment'
| '/privacy-policy'
| '/tos'
| '/bind/$platform'
| '/oauth/$platform'
| '/auth'
| '/affiliate'
| '/announcement'
| '/dashboard'
| '/document'
| '/order'
| '/profile'
| '/subscribe'
| '/ticket'
| '/wallet'
| '/purchasing'
| '/purchasing/order'
id:
| '__root__'
| '/(main)'
| '/(main)/(user)'
| '/(main)/payment'
| '/(main)/privacy-policy'
| '/(main)/tos'
| '/bind/$platform'
| '/oauth/$platform'
| '/(main)/'
| '/auth/'
| '/(main)/(user)/affiliate'
| '/(main)/(user)/announcement'
| '/(main)/(user)/dashboard'
| '/(main)/(user)/document'
| '/(main)/(user)/order'
| '/(main)/(user)/profile'
| '/(main)/(user)/subscribe'
| '/(main)/(user)/ticket'
| '/(main)/(user)/wallet'
| '/(main)/purchasing/'
| '/(main)/purchasing/order/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
mainRouteLazyRoute: typeof mainRouteLazyRouteWithChildren
BindPlatformLazyRoute: typeof BindPlatformLazyRoute
OauthPlatformLazyRoute: typeof OauthPlatformLazyRoute
AuthIndexLazyRoute: typeof AuthIndexLazyRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/(main)': {
id: '/(main)'
path: '/'
fullPath: '/'
preLoaderRoute: typeof mainRouteLazyRouteImport
parentRoute: typeof rootRouteImport
}
'/auth/': {
id: '/auth/'
path: '/auth'
fullPath: '/auth'
preLoaderRoute: typeof AuthIndexLazyRouteImport
parentRoute: typeof rootRouteImport
}
'/(main)/': {
id: '/(main)/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof mainIndexLazyRouteImport
parentRoute: typeof mainRouteLazyRoute
}
'/oauth/$platform': {
id: '/oauth/$platform'
path: '/oauth/$platform'
fullPath: '/oauth/$platform'
preLoaderRoute: typeof OauthPlatformLazyRouteImport
parentRoute: typeof rootRouteImport
}
'/bind/$platform': {
id: '/bind/$platform'
path: '/bind/$platform'
fullPath: '/bind/$platform'
preLoaderRoute: typeof BindPlatformLazyRouteImport
parentRoute: typeof rootRouteImport
}
'/(main)/tos': {
id: '/(main)/tos'
path: '/tos'
fullPath: '/tos'
preLoaderRoute: typeof mainTosLazyRouteImport
parentRoute: typeof mainRouteLazyRoute
}
'/(main)/privacy-policy': {
id: '/(main)/privacy-policy'
path: '/privacy-policy'
fullPath: '/privacy-policy'
preLoaderRoute: typeof mainPrivacyPolicyLazyRouteImport
parentRoute: typeof mainRouteLazyRoute
}
'/(main)/payment': {
id: '/(main)/payment'
path: '/payment'
fullPath: '/payment'
preLoaderRoute: typeof mainPaymentLazyRouteImport
parentRoute: typeof mainRouteLazyRoute
}
'/(main)/(user)': {
id: '/(main)/(user)'
path: '/'
fullPath: '/'
preLoaderRoute: typeof mainuserRouteLazyRouteImport
parentRoute: typeof mainRouteLazyRoute
}
'/(main)/purchasing/': {
id: '/(main)/purchasing/'
path: '/purchasing'
fullPath: '/purchasing'
preLoaderRoute: typeof mainPurchasingIndexLazyRouteImport
parentRoute: typeof mainRouteLazyRoute
}
'/(main)/(user)/wallet': {
id: '/(main)/(user)/wallet'
path: '/wallet'
fullPath: '/wallet'
preLoaderRoute: typeof mainuserWalletLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/ticket': {
id: '/(main)/(user)/ticket'
path: '/ticket'
fullPath: '/ticket'
preLoaderRoute: typeof mainuserTicketLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/subscribe': {
id: '/(main)/(user)/subscribe'
path: '/subscribe'
fullPath: '/subscribe'
preLoaderRoute: typeof mainuserSubscribeLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/profile': {
id: '/(main)/(user)/profile'
path: '/profile'
fullPath: '/profile'
preLoaderRoute: typeof mainuserProfileLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/order': {
id: '/(main)/(user)/order'
path: '/order'
fullPath: '/order'
preLoaderRoute: typeof mainuserOrderLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/document': {
id: '/(main)/(user)/document'
path: '/document'
fullPath: '/document'
preLoaderRoute: typeof mainuserDocumentLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/dashboard': {
id: '/(main)/(user)/dashboard'
path: '/dashboard'
fullPath: '/dashboard'
preLoaderRoute: typeof mainuserDashboardLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/announcement': {
id: '/(main)/(user)/announcement'
path: '/announcement'
fullPath: '/announcement'
preLoaderRoute: typeof mainuserAnnouncementLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/(user)/affiliate': {
id: '/(main)/(user)/affiliate'
path: '/affiliate'
fullPath: '/affiliate'
preLoaderRoute: typeof mainuserAffiliateLazyRouteImport
parentRoute: typeof mainuserRouteLazyRoute
}
'/(main)/purchasing/order/': {
id: '/(main)/purchasing/order/'
path: '/purchasing/order'
fullPath: '/purchasing/order'
preLoaderRoute: typeof mainPurchasingOrderIndexRouteImport
parentRoute: typeof mainRouteLazyRoute
}
}
}
interface mainuserRouteLazyRouteChildren {
mainuserAffiliateLazyRoute: typeof mainuserAffiliateLazyRoute
mainuserAnnouncementLazyRoute: typeof mainuserAnnouncementLazyRoute
mainuserDashboardLazyRoute: typeof mainuserDashboardLazyRoute
mainuserDocumentLazyRoute: typeof mainuserDocumentLazyRoute
mainuserOrderLazyRoute: typeof mainuserOrderLazyRoute
mainuserProfileLazyRoute: typeof mainuserProfileLazyRoute
mainuserSubscribeLazyRoute: typeof mainuserSubscribeLazyRoute
mainuserTicketLazyRoute: typeof mainuserTicketLazyRoute
mainuserWalletLazyRoute: typeof mainuserWalletLazyRoute
}
const mainuserRouteLazyRouteChildren: mainuserRouteLazyRouteChildren = {
mainuserAffiliateLazyRoute: mainuserAffiliateLazyRoute,
mainuserAnnouncementLazyRoute: mainuserAnnouncementLazyRoute,
mainuserDashboardLazyRoute: mainuserDashboardLazyRoute,
mainuserDocumentLazyRoute: mainuserDocumentLazyRoute,
mainuserOrderLazyRoute: mainuserOrderLazyRoute,
mainuserProfileLazyRoute: mainuserProfileLazyRoute,
mainuserSubscribeLazyRoute: mainuserSubscribeLazyRoute,
mainuserTicketLazyRoute: mainuserTicketLazyRoute,
mainuserWalletLazyRoute: mainuserWalletLazyRoute,
}
const mainuserRouteLazyRouteWithChildren =
mainuserRouteLazyRoute._addFileChildren(mainuserRouteLazyRouteChildren)
interface mainRouteLazyRouteChildren {
mainuserRouteLazyRoute: typeof mainuserRouteLazyRouteWithChildren
mainPaymentLazyRoute: typeof mainPaymentLazyRoute
mainPrivacyPolicyLazyRoute: typeof mainPrivacyPolicyLazyRoute
mainTosLazyRoute: typeof mainTosLazyRoute
mainIndexLazyRoute: typeof mainIndexLazyRoute
mainPurchasingIndexLazyRoute: typeof mainPurchasingIndexLazyRoute
mainPurchasingOrderIndexRoute: typeof mainPurchasingOrderIndexRoute
}
const mainRouteLazyRouteChildren: mainRouteLazyRouteChildren = {
mainuserRouteLazyRoute: mainuserRouteLazyRouteWithChildren,
mainPaymentLazyRoute: mainPaymentLazyRoute,
mainPrivacyPolicyLazyRoute: mainPrivacyPolicyLazyRoute,
mainTosLazyRoute: mainTosLazyRoute,
mainIndexLazyRoute: mainIndexLazyRoute,
mainPurchasingIndexLazyRoute: mainPurchasingIndexLazyRoute,
mainPurchasingOrderIndexRoute: mainPurchasingOrderIndexRoute,
}
const mainRouteLazyRouteWithChildren = mainRouteLazyRoute._addFileChildren(
mainRouteLazyRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
mainRouteLazyRoute: mainRouteLazyRouteWithChildren,
BindPlatformLazyRoute: BindPlatformLazyRoute,
OauthPlatformLazyRoute: OauthPlatformLazyRoute,
AuthIndexLazyRoute: AuthIndexLazyRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Affiliate from "@/sections/user/affiliate";
export const Route = createLazyFileRoute("/(main)/(user)/affiliate")({
component: Affiliate,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Announcement from "@/sections/user/announcement/index";
export const Route = createLazyFileRoute("/(main)/(user)/announcement")({
component: Announcement,
});
@@ -0,0 +1,7 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Dashboard from "@/sections/user/dashboard";
export const Route = createLazyFileRoute("/(main)/(user)/dashboard")({
component: Dashboard,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Document from "@/sections/user/document";
export const Route = createLazyFileRoute("/(main)/(user)/document")({
component: Document,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Order from "@/sections/user/order";
export const Route = createLazyFileRoute("/(main)/(user)/order")({
component: Order,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Profile from "@/sections/user/profile";
export const Route = createLazyFileRoute("/(main)/(user)/profile")({
component: Profile,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import UserLayout from "@/sections/user/layout";
export const Route = createLazyFileRoute("/(main)/(user)")({
component: UserLayout,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Subscribe from "@/sections/subscribe";
export const Route = createLazyFileRoute("/(main)/(user)/subscribe")({
component: Subscribe,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Ticket from "@/sections/user/ticket";
export const Route = createLazyFileRoute("/(main)/(user)/ticket")({
component: Ticket,
});
@@ -0,0 +1,7 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Wallet from "@/sections/user/wallet";
export const Route = createLazyFileRoute("/(main)/(user)/wallet")({
component: Wallet,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Main from "@/sections/main";
export const Route = createLazyFileRoute("/(main)/")({
component: Main,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Payment from "@/sections/user/payment";
export const Route = createLazyFileRoute("/(main)/payment")({
component: Payment,
});
@@ -0,0 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { Markdown } from "@workspace/ui/composed/markdown";
import { getPrivacyPolicy } from "@workspace/ui/services/common/common";
export const Route = createLazyFileRoute("/(main)/privacy-policy")({
component: () => {
const { data = "" } = useQuery({
queryKey: ["tos"],
queryFn: async () => {
const { data } = await getPrivacyPolicy();
return data.data?.privacy_policy;
},
});
return (
<div className="container py-8">
<Markdown>{data}</Markdown>
</div>
);
},
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Purchasing from "@/sections/purchasing";
export const Route = createLazyFileRoute("/(main)/purchasing/")({
component: Purchasing,
});
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Order from "@/sections/purchasing/order";
export const Route = createLazyFileRoute("/(main)/purchasing/order/")({
component: Order,
});
@@ -0,0 +1,10 @@
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
const searchSchema = z.object({
order_no: z.string().optional(),
});
export const Route = createFileRoute("/(main)/purchasing/order/")({
validateSearch: searchSchema,
});
@@ -0,0 +1,13 @@
import { createLazyFileRoute, Outlet } from "@tanstack/react-router";
import Footer from "@/layout/footer";
import Header from "@/layout/header";
export const Route = createLazyFileRoute("/(main)")({
component: () => (
<>
<Header />
<Outlet />
<Footer />
</>
),
});
+21
View File
@@ -0,0 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { Markdown } from "@workspace/ui/composed/markdown";
import { getTos } from "@workspace/ui/services/common/common";
export const Route = createLazyFileRoute("/(main)/tos")({
component: () => {
const { data = "" } = useQuery({
queryKey: ["tos"],
queryFn: async () => {
const { data } = await getTos();
return data.data?.tos_content;
},
});
return (
<div className="container py-8">
<Markdown>{data}</Markdown>
</div>
);
},
});
+79
View File
@@ -0,0 +1,79 @@
import { TanStackDevtools } from "@tanstack/react-devtools";
import { createRootRouteWithContext, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { Toaster } from "@workspace/ui/components/sonner";
import { NavigationProgress } from "@workspace/ui/composed/navigation-progress";
import { TanStackQueryDevtools } from "@workspace/ui/integrations/tanstack-query-devtools";
import { getCookie } from "@workspace/ui/lib/cookies";
import { getGlobalConfig } from "@workspace/ui/services/common/common";
import { isBrowser } from "@workspace/ui/utils/index";
import { useEffect } from "react";
import { Helmet, HelmetProvider } from "react-helmet-async";
import { useGlobalStore } from "@/stores/global";
export const Route = createRootRouteWithContext()({
component: () => {
const { common, setCommon, getUserInfo } = useGlobalStore();
useEffect(() => {
const initializeApp = async () => {
try {
const configResponse = await getGlobalConfig();
if (configResponse.data?.data) {
setCommon(configResponse.data.data);
}
try {
if (getCookie("Authorization")) {
await getUserInfo();
}
} catch {
/* empty */
}
} catch (error) {
console.error("Failed to initialize app:", error);
}
};
initializeApp();
}, []);
const { site } = common;
const title = site.site_name || "Loading...";
const description = site.site_desc || "";
const keywords = site.keywords || "";
const logo = site.site_logo || "";
const url = isBrowser() ? window.location.href : "";
return (
<HelmetProvider>
<Helmet>
<title>{title}</title>
<meta content={description} name="description" />
<meta content={keywords} name="keywords" />
<link href={url} rel="canonical" />
<link href={logo} rel="icon" type="image/svg+xml" />
<link href={logo} rel="apple-touch-icon" sizes="180x180" />
<link href="/site.webmanifest" rel="manifest" />
</Helmet>
<NavigationProgress />
<Outlet />
<Toaster closeButton richColors />
<div
dangerouslySetInnerHTML={{ __html: common?.site.custom_html || "" }}
id="custom_html"
/>
<TanStackDevtools
config={{
position: "bottom-right",
}}
plugins={[
{
name: "Tanstack Router",
render: <TanStackRouterDevtoolsPanel />,
},
TanStackQueryDevtools,
]}
/>
</HelmetProvider>
);
},
});
+6
View File
@@ -0,0 +1,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import Auth from "@/sections/auth";
export const Route = createLazyFileRoute("/auth/")({
component: Auth,
});
@@ -0,0 +1,9 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import BindPage from "@/sections/bind";
export const Route = createLazyFileRoute("/bind/$platform")({
component: () => {
const { platform } = Route.useParams();
return <BindPage platform={platform} />;
},
});
@@ -0,0 +1,9 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import OAuthPage from "@/sections/oauth";
export const Route = createLazyFileRoute("/oauth/$platform")({
component: () => {
const { platform } = Route.useParams();
return <OAuthPage platform={platform} />;
},
});
@@ -0,0 +1,106 @@
"use client";
import { useNavigate } from "@tanstack/react-router";
import {
resetPassword,
userLogin,
userRegister,
} from "@workspace/ui/services/common/auth";
import type { ReactNode } from "react";
import { useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { USER_EMAIL, USER_PASSWORD } from "@/config";
import { useGlobalStore } from "@/stores/global";
import { getRedirectUrl, setAuthorization } from "@/utils/common";
import LoginForm from "./login-form";
import RegisterForm from "./register-form";
import ResetForm from "./reset-form";
export default function EmailAuthForm() {
const { t } = useTranslation("auth");
const navigate = useNavigate();
const { getUserInfo } = useGlobalStore();
const [type, setType] = useState<"login" | "register" | "reset">("login");
const [loading, startTransition] = useTransition();
const [initialValues, setInitialValues] = useState<{
email?: string;
password?: string;
}>({
email: USER_EMAIL,
password: USER_PASSWORD,
});
const handleFormSubmit = async (params: any) => {
const onLogin = async (token?: string) => {
if (!token) return;
setAuthorization(token);
await getUserInfo();
navigate({ to: getRedirectUrl() });
};
startTransition(async () => {
try {
switch (type) {
case "login": {
const login = await userLogin(params);
toast.success(t("login.success", "Login successful!"));
onLogin(login.data.data?.token);
break;
}
case "register": {
const create = await userRegister(params);
toast.success(t("register.success", "Registration successful!"));
onLogin(create.data.data?.token);
break;
}
case "reset":
await resetPassword(params);
toast.success(t("reset.success", "Password reset successful!"));
setType("login");
break;
}
} catch (_error) {
/* empty */
}
});
};
let UserForm: ReactNode = null;
switch (type) {
case "login":
UserForm = (
<LoginForm
initialValues={initialValues}
loading={loading}
onSubmit={handleFormSubmit}
onSwitchForm={setType}
setInitialValues={setInitialValues}
/>
);
break;
case "register":
UserForm = (
<RegisterForm
initialValues={initialValues}
loading={loading}
onSubmit={handleFormSubmit}
onSwitchForm={setType}
setInitialValues={setInitialValues}
/>
);
break;
case "reset":
UserForm = (
<ResetForm
initialValues={initialValues}
loading={loading}
onSubmit={handleFormSubmit}
onSwitchForm={setType}
setInitialValues={setInitialValues}
/>
);
break;
}
return UserForm;
}
@@ -0,0 +1,142 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@workspace/ui/components/form";
import { Input } from "@workspace/ui/components/input";
import { Icon } from "@workspace/ui/composed/icon";
import type { Dispatch, SetStateAction } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useGlobalStore } from "@/stores/global";
import type { TurnstileRef } from "../turnstile";
import CloudFlareTurnstile from "../turnstile";
export default function LoginForm({
loading,
onSubmit,
initialValues,
setInitialValues,
onSwitchForm,
}: {
loading?: boolean;
onSubmit: (data: any) => void;
initialValues: any;
setInitialValues: Dispatch<SetStateAction<any>>;
onSwitchForm: Dispatch<SetStateAction<"register" | "reset" | "login">>;
}) {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { verify } = common;
const formSchema = z.object({
email: z.email(t("login.email", "Please enter a valid email address")),
password: z.string(),
cf_token:
verify.enable_login_verify && verify.turnstile_site_key
? z.string()
: z.string().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: initialValues,
});
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (_error) {
turnstile.current?.reset();
}
});
return (
<>
<Form {...form}>
<form className="grid gap-6" onSubmit={handleSubmit}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your email..."
type="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your password..."
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{verify.enable_login_verify && (
<FormField
control={form.control}
name="cf_token"
render={({ field }) => (
<FormItem>
<FormControl>
<CloudFlareTurnstile
id="login"
{...field}
ref={turnstile}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button disabled={loading} type="submit">
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
{t("login.title", "Login")}
</Button>
</form>
</Form>
<div className="mt-4 flex w-full justify-between text-sm">
<Button
className="p-0"
onClick={() => onSwitchForm("reset")}
type="button"
variant="link"
>
{t("login.forgotPassword", "Forgot Password?")}
</Button>
<Button
className="p-0"
onClick={() => {
setInitialValues(undefined);
onSwitchForm("register");
}}
variant="link"
>
{t("login.registerAccount", "Register Account")}
</Button>
</div>
</>
);
}
@@ -0,0 +1,248 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@workspace/ui/components/form";
import { Input } from "@workspace/ui/components/input";
import { Icon } from "@workspace/ui/composed/icon";
import { Markdown } from "@workspace/ui/composed/markdown";
import type { Dispatch, SetStateAction } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useGlobalStore } from "@/stores/global";
import SendCode from "../send-code";
import type { TurnstileRef } from "../turnstile";
import CloudFlareTurnstile from "../turnstile";
export default function RegisterForm({
loading,
onSubmit,
initialValues,
setInitialValues,
onSwitchForm,
}: {
loading?: boolean;
onSubmit: (data: any) => void;
initialValues: any;
setInitialValues: Dispatch<SetStateAction<any>>;
onSwitchForm: Dispatch<SetStateAction<"register" | "reset" | "login">>;
}) {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { verify, auth, invite } = common;
const handleCheckUser = async (email: string) => {
try {
if (!auth.email.enable_domain_suffix) return true;
const domain = email.split("@")[1];
const isValid = auth.email?.domain_suffix_list
.split("\n")
.includes(domain || "");
return isValid;
} catch (error) {
console.log("Error checking user:", error);
return false;
}
};
const formSchema = z
.object({
email: z
.string()
.email(t("register.email", "Please enter a valid email address"))
.refine(handleCheckUser, {
message: t(
"register.whitelist",
"This email domain is not in the whitelist"
),
}),
password: z.string(),
repeat_password: z.string(),
code: auth.email.enable_verify ? z.string() : z.string().nullish(),
invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
cf_token:
verify.enable_register_verify && verify.turnstile_site_key
? z.string()
: z.string().nullish(),
})
.superRefine(({ password, repeat_password }, ctx) => {
if (password !== repeat_password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("register.passwordMismatch", "Passwords do not match"),
path: ["repeat_password"],
});
}
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
...initialValues,
invite: localStorage.getItem("invite") || "",
},
});
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (_error) {
turnstile.current?.reset();
}
});
return (
<>
{auth.register.stop_register ? (
<Markdown>
{t("register.message", "Registration is currently disabled")}
</Markdown>
) : (
<Form {...form}>
<form className="grid gap-6" onSubmit={handleSubmit}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your email..."
type="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your password..."
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repeat_password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
disabled={loading}
placeholder="Enter password again..."
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{auth.email.enable_verify && (
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-2">
<Input
disabled={loading}
placeholder="Enter code..."
type="text"
{...field}
value={field.value as string}
/>
<SendCode
params={{
...form.getValues(),
type: 1,
}}
type="email"
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="invite"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
disabled={loading || !!localStorage.getItem("invite")}
placeholder={t(
"register.invite",
"Invitation Code (Optional)"
)}
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{verify.enable_register_verify && (
<FormField
control={form.control}
name="cf_token"
render={({ field }) => (
<FormItem>
<FormControl>
<CloudFlareTurnstile
id="register"
{...field}
ref={turnstile}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button disabled={loading} type="submit">
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
{t("register.title", "Register")}
</Button>
</form>
</Form>
)}
<div className="mt-4 text-right text-sm">
{t("register.existingAccount", "Already have an account?")}&nbsp;
<Button
className="p-0"
onClick={() => {
setInitialValues(undefined);
onSwitchForm("login");
}}
variant="link"
>
{t("register.switchToLogin", "Login")}
</Button>
</div>
</>
);
}
@@ -0,0 +1,167 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@workspace/ui/components/form";
import { Input } from "@workspace/ui/components/input";
import { Icon } from "@workspace/ui/composed/icon";
import type { Dispatch, SetStateAction } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useGlobalStore } from "@/stores/global";
import SendCode from "../send-code";
import type { TurnstileRef } from "../turnstile";
import CloudFlareTurnstile from "../turnstile";
export default function ResetForm({
loading,
onSubmit,
initialValues,
setInitialValues,
onSwitchForm,
}: {
loading?: boolean;
onSubmit: (data: any) => void;
initialValues: any;
setInitialValues: Dispatch<SetStateAction<any>>;
onSwitchForm: Dispatch<SetStateAction<"register" | "reset" | "login">>;
}) {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { verify, auth } = common;
const formSchema = z.object({
email: z
.string()
.email(t("reset.email", "Please enter a valid email address")),
password: z.string(),
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
cf_token:
verify.enable_register_verify && verify.turnstile_site_key
? z.string()
: z.string().nullish(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: initialValues,
});
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (_error) {
turnstile.current?.reset();
}
});
return (
<>
<Form {...form}>
<form className="grid gap-6" onSubmit={handleSubmit}>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your email..."
type="email"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-2">
<Input
disabled={loading}
placeholder="Enter code..."
type="text"
{...field}
value={field.value as string}
/>
<SendCode
params={{
...form.getValues(),
type: 2,
}}
type="email"
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your new password..."
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{verify.enable_reset_password_verify && (
<FormField
control={form.control}
name="cf_token"
render={({ field }) => (
<FormItem>
<FormControl>
<CloudFlareTurnstile
id="reset"
{...field}
ref={turnstile}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button disabled={loading} type="submit">
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
{t("reset.title", "Reset Password")}
</Button>
</form>
</Form>
<div className="mt-4 text-right text-sm">
{t("reset.existingAccount", "Remember your password?")}&nbsp;
<Button
className="p-0"
onClick={() => {
setInitialValues(undefined);
onSwitchForm("login");
}}
variant="link"
>
{t("reset.switchToLogin", "Login")}
</Button>
</div>
</>
);
}
+113
View File
@@ -0,0 +1,113 @@
"use client";
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
import { Link } from "@tanstack/react-router";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@workspace/ui/components/tabs";
import { LanguageSwitch } from "@workspace/ui/composed/language-switch";
import { ThemeSwitch } from "@workspace/ui/composed/theme-switch";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "@/stores/global";
import EmailAuthForm from "./email/auth-form";
import { OAuthMethods } from "./oauth-methods";
import PhoneAuthForm from "./phone/auth-form";
export default function Main() {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { site, auth } = common;
const AUTH_METHODS = [
{
key: "email",
enabled: auth.email.enable,
children: <EmailAuthForm />,
},
{
key: "mobile",
enabled: auth.mobile.enable,
children: <PhoneAuthForm />,
},
].filter((method) => method.enabled);
return (
<main className="flex h-full min-h-screen items-center bg-muted/50">
<div className="flex size-full flex-auto flex-col lg:flex-row">
<div className="flex bg-center bg-cover lg:w-1/2 lg:flex-auto">
<div className="flex w-full flex-col items-center justify-center px-5 py-7 md:px-15 lg:py-15">
<Link className="mb-0 flex flex-col items-center lg:mb-12" to="/">
{site.site_logo && (
<img alt="logo" height={48} src={site.site_logo} width={48} />
)}
<span className="font-semibold text-2xl">{site.site_name}</span>
</Link>
<DotLottieReact
autoplay
className="mx-auto hidden w-[275px] lg:block xl:w-[500px]"
loop
src="/lotties/login.json"
/>
<p className="hidden w-[275px] text-center md:w-1/2 lg:block xl:w-[500px]">
{site.site_desc}
</p>
</div>
</div>
<div className="flex flex-initial justify-center p-12 lg:flex-auto lg:justify-end">
<div className="flex w-full flex-col items-center rounded-2xl md:w-[600px] md:p-10 lg:flex-auto lg:bg-background lg:shadow">
<div className="flex w-full flex-col items-stretch justify-center md:w-[400px] lg:h-full">
<div className="flex flex-col justify-center lg:flex-auto">
<h1 className="mb-3 text-center font-bold text-2xl">
{t("verifyAccount", "Verify Your Account")}
</h1>
<div className="mb-6 text-center font-medium text-muted-foreground">
{t(
"verifyAccountDesc",
"Please login or register to continue"
)}
</div>
{AUTH_METHODS.length === 1
? AUTH_METHODS[0]?.children
: AUTH_METHODS[0] && (
<Tabs defaultValue={AUTH_METHODS[0].key}>
<TabsList className="mb-6 flex w-full *:flex-1">
{AUTH_METHODS.map((item) => (
<TabsTrigger key={item.key} value={item.key}>
{t(`methods.${item.key}`)}
</TabsTrigger>
))}
</TabsList>
{AUTH_METHODS.map((item) => (
<TabsContent key={item.key} value={item.key}>
{item.children}
</TabsContent>
))}
</Tabs>
)}
</div>
<div className="py-8">
<OAuthMethods />
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-5">
<LanguageSwitch />
<ThemeSwitch />
</div>
<div className="flex gap-2 font-semibold text-primary text-sm">
<Link to="/tos">{t("tos", "Terms of Service")}</Link>
<span className="text-foreground/30">|</span>
<Link to="/privacy-policy">
{t("privacyPolicy", "Privacy Policy")}
</Link>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
);
}
@@ -0,0 +1,54 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import { Icon } from "@workspace/ui/composed/icon";
import { oAuthLogin } from "@workspace/ui/services/common/oauth";
import { useGlobalStore } from "@/stores/global";
const icons = {
apple: "uil:apple",
google: "logos:google-icon",
facebook: "logos:facebook",
github: "uil:github",
telegram: "logos:telegram",
};
export function OAuthMethods() {
const { common } = useGlobalStore();
const { oauth_methods } = common;
const OAUTH_METHODS = oauth_methods?.filter(
(method: string) => !["mobile", "email", "device"].includes(method)
);
return (
OAUTH_METHODS?.length > 0 && (
<>
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-border after:border-t">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
<div className="mt-6 flex justify-center gap-4 *:size-12 *:p-2">
{OAUTH_METHODS?.map((method: string) => (
<Button
asChild
key={method}
onClick={async () => {
const { data } = await oAuthLogin({
method,
redirect: `${window.location.origin}/oauth/${method}`,
});
if (data.data?.redirect) {
window.location.href = data.data?.redirect;
}
}}
size="icon"
variant="ghost"
>
<Icon icon={icons[method as keyof typeof icons]} />
</Button>
))}
</div>
</>
)
);
}
@@ -0,0 +1,105 @@
"use client";
import { useNavigate } from "@tanstack/react-router";
import {
telephoneLogin,
telephoneResetPassword,
telephoneUserRegister,
} from "@workspace/ui/services/common/auth";
import type { ReactNode } from "react";
import { useState, useTransition } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { useGlobalStore } from "@/stores/global";
import { getRedirectUrl, setAuthorization } from "@/utils/common";
import LoginForm from "./login-form";
import RegisterForm from "./register-form";
import ResetForm from "./reset-form";
export default function PhoneAuthForm() {
const { t } = useTranslation("auth");
const navigate = useNavigate();
const { getUserInfo } = useGlobalStore();
const [type, setType] = useState<"login" | "register" | "reset">("login");
const [loading, startTransition] = useTransition();
const [initialValues, setInitialValues] = useState<API.TelephoneLoginRequest>(
{
identifier: "",
telephone: "",
telephone_area_code: "1",
password: "",
telephone_code: "",
}
);
const handleFormSubmit = async (params: any) => {
const onLogin = async (token?: string) => {
if (!token) return;
setAuthorization(token);
await getUserInfo();
navigate({ to: getRedirectUrl() });
};
startTransition(async () => {
try {
switch (type) {
case "login": {
const login = await telephoneLogin(params);
toast.success(t("login.success", "Login successful!"));
onLogin(login.data.data?.token);
break;
}
case "register": {
const create = await telephoneUserRegister(params);
toast.success(t("register.success", "Registration successful!"));
onLogin(create.data.data?.token);
break;
}
case "reset":
await telephoneResetPassword(params);
toast.success(t("reset.success", "Password reset successful!"));
setType("login");
break;
}
} catch (_error) {
/* empty */
}
});
};
let UserForm: ReactNode = null;
switch (type) {
case "login":
UserForm = (
<LoginForm
initialValues={initialValues}
loading={loading}
onSubmit={handleFormSubmit}
onSwitchForm={setType}
/>
);
break;
case "register":
UserForm = (
<RegisterForm
initialValues={initialValues}
loading={loading}
onSubmit={handleFormSubmit}
onSwitchForm={setType}
/>
);
break;
case "reset":
UserForm = (
<ResetForm
initialValues={initialValues}
loading={loading}
onSubmit={handleFormSubmit}
onSwitchForm={setType}
setInitialValues={setInitialValues}
/>
);
break;
}
return UserForm;
}
@@ -0,0 +1,203 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@workspace/ui/components/form";
import { Input } from "@workspace/ui/components/input";
import { AreaCodeSelect } from "@workspace/ui/composed/area-code-select";
import { Icon } from "@workspace/ui/composed/icon";
import type { Dispatch, SetStateAction } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useGlobalStore } from "@/stores/global";
import SendCode from "../send-code";
import type { TurnstileRef } from "../turnstile";
import CloudFlareTurnstile from "../turnstile";
export default function LoginForm({
loading,
onSubmit,
initialValues,
onSwitchForm,
}: {
loading?: boolean;
onSubmit: (data: any) => void;
initialValues: any;
onSwitchForm: Dispatch<SetStateAction<"register" | "reset" | "login">>;
}) {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { verify } = common;
const formSchema = z.object({
telephone_area_code: z.string(),
telephone: z.string(),
telephone_code: z.string().optional(),
password: z.string().optional(),
cf_token:
verify.enable_login_verify && verify.turnstile_site_key
? z.string()
: z.string().optional(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: initialValues,
});
const [mode, setMode] = useState<"password" | "code">("password");
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (_error) {
turnstile.current?.reset();
}
});
return (
<>
<Form {...form}>
<form className="grid gap-6" onSubmit={handleSubmit}>
<FormField
control={form.control}
name="telephone"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex">
<FormField
control={form.control}
name="telephone_area_code"
render={({ field }) => (
<FormItem>
<FormControl>
<AreaCodeSelect
className="w-32 rounded-r-none border-r-0"
onChange={(value) => {
if (value.phone) {
form.setValue(
"telephone_area_code",
value.phone
);
}
}}
placeholder="Area code..."
simple
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Input
className="rounded-l-none"
placeholder="Enter your telephone..."
type="tel"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={mode === "code" ? "telephone_code" : "password"}
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex gap-2">
<Input
placeholder={
mode === "code" ? "Enter code..." : "Enter password..."
}
type={mode === "code" ? "text" : "password"}
{...field}
/>
{mode === "code" && (
<SendCode
params={{
...form.getValues(),
type: 2,
}}
type="phone"
/>
)}
</div>
</FormControl>
<div className="!mt-0 text-right">
<Button
className="px-0 text-primary text-sm"
onClick={(e) => {
e.preventDefault();
setMode(mode === "password" ? "code" : "password");
}}
variant="link"
>
{mode === "password"
? t("login.codeLogin", "Login with Code")
: t("login.passwordLogin", "Login with Password")}
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
{verify.enable_login_verify && (
<FormField
control={form.control}
name="cf_token"
render={({ field }) => (
<FormItem>
<FormControl>
<CloudFlareTurnstile
id="login"
{...field}
ref={turnstile}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button disabled={loading} type="submit">
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
{t("login.title", "Login")}
</Button>
</form>
</Form>
<div className="mt-4 flex w-full justify-between text-sm">
<Button
className="p-0"
onClick={() => onSwitchForm("reset")}
type="button"
variant="link"
>
{t("login.forgotPassword", "Forgot Password?")}
</Button>
<Button
className="p-0"
onClick={() => {
// setInitialValues(undefined);
onSwitchForm("register");
}}
variant="link"
>
{t("login.registerAccount", "Register Account")}
</Button>
</div>
</>
);
}
@@ -0,0 +1,256 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@workspace/ui/components/form";
import { Input } from "@workspace/ui/components/input";
import { AreaCodeSelect } from "@workspace/ui/composed/area-code-select";
import { Icon } from "@workspace/ui/composed/icon";
import { Markdown } from "@workspace/ui/composed/markdown";
import type { Dispatch, SetStateAction } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useGlobalStore } from "@/stores/global";
import SendCode from "../send-code";
import type { TurnstileRef } from "../turnstile";
import CloudFlareTurnstile from "../turnstile";
export default function RegisterForm({
loading,
onSubmit,
initialValues,
onSwitchForm,
}: {
loading?: boolean;
onSubmit: (data: any) => void;
initialValues: any;
onSwitchForm: Dispatch<SetStateAction<"register" | "reset" | "login">>;
}) {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { verify, auth, invite } = common;
const { enable_whitelist, whitelist } = auth.mobile;
const formSchema = z
.object({
telephone_area_code: z.string(),
telephone: z.string(),
password: z.string(),
repeat_password: z.string(),
code: z.string(),
invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
cf_token:
verify.enable_register_verify && verify.turnstile_site_key
? z.string()
: z.string().nullish(),
})
.superRefine(({ password, repeat_password }, ctx) => {
if (password !== repeat_password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t("register.passwordMismatch", "Passwords do not match"),
path: ["repeat_password"],
});
}
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
...initialValues,
telephone_area_code: initialValues?.telephone_area_code || "1",
invite: localStorage.getItem("invite") || "",
},
});
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (_error) {
turnstile.current?.reset();
}
});
return (
<>
{auth.register.stop_register ? (
<Markdown>
{t("register.message", "Registration is currently disabled")}
</Markdown>
) : (
<Form {...form}>
<form className="grid gap-6" onSubmit={handleSubmit}>
<FormField
control={form.control}
name="telephone"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex">
<FormField
control={form.control}
name="telephone_area_code"
render={({ field }) => (
<FormItem>
<FormControl>
<AreaCodeSelect
className="w-32 rounded-r-none border-r-0"
onChange={(value) => {
if (value.phone) {
form.setValue(
"telephone_area_code",
value.phone
);
}
}}
placeholder="Area code..."
simple
value={field.value}
whitelist={enable_whitelist ? whitelist : []}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Input
className="rounded-l-none"
placeholder="Enter your telephone..."
type="tel"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your password..."
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="repeat_password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
disabled={loading}
placeholder="Enter password again..."
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-2">
<Input
disabled={loading}
placeholder="Enter code..."
type="text"
{...field}
value={field.value as string}
/>
<SendCode
params={{
...form.getValues(),
type: 1,
}}
type="phone"
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="invite"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
disabled={loading || !!localStorage.getItem("invite")}
placeholder={t(
"register.invite",
"Invitation Code (Optional)"
)}
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{verify.enable_register_verify && (
<FormField
control={form.control}
name="cf_token"
render={({ field }) => (
<FormItem>
<FormControl>
<CloudFlareTurnstile
id="register"
{...field}
ref={turnstile}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button disabled={loading} type="submit">
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
{t("register.title", "Register")}
</Button>
</form>
</Form>
)}
<div className="mt-4 text-right text-sm">
{t("register.existingAccount", "Already have an account?")}&nbsp;
<Button
className="p-0"
onClick={() => {
// setInitialValues(undefined);
onSwitchForm("login");
}}
variant="link"
>
{t("register.switchToLogin", "Login")}
</Button>
</div>
</>
);
}
@@ -0,0 +1,192 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@workspace/ui/components/form";
import { Input } from "@workspace/ui/components/input";
import { AreaCodeSelect } from "@workspace/ui/composed/area-code-select";
import { Icon } from "@workspace/ui/composed/icon";
import type { Dispatch, SetStateAction } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useGlobalStore } from "@/stores/global";
import SendCode from "../send-code";
import type { TurnstileRef } from "../turnstile";
import CloudFlareTurnstile from "../turnstile";
export default function ResetForm({
loading,
onSubmit,
initialValues,
onSwitchForm,
}: {
loading?: boolean;
onSubmit: (data: any) => void;
initialValues: any;
setInitialValues: Dispatch<SetStateAction<any>>;
onSwitchForm: Dispatch<SetStateAction<"register" | "reset" | "login">>;
}) {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { verify, auth } = common;
const formSchema = z.object({
telephone_area_code: z.string(),
telephone: z.string(),
password: z.string(),
code: auth?.email?.enable_verify ? z.string() : z.string().nullish(),
cf_token:
verify.enable_register_verify && verify.turnstile_site_key
? z.string()
: z.string().nullish(),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: initialValues,
});
const turnstile = useRef<TurnstileRef>(null);
const handleSubmit = form.handleSubmit((data) => {
try {
onSubmit(data);
} catch (_error) {
turnstile.current?.reset();
}
});
return (
<>
<Form {...form}>
<form className="grid gap-6" onSubmit={handleSubmit}>
<FormField
control={form.control}
name="telephone"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex">
<FormField
control={form.control}
name="telephone_area_code"
render={({ field }) => (
<FormItem>
<FormControl>
<AreaCodeSelect
className="w-32 rounded-r-none border-r-0"
onChange={(value) => {
if (value.phone) {
form.setValue(
"telephone_area_code",
value.phone
);
}
}}
placeholder="Area code..."
simple
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Input
className="rounded-l-none"
placeholder="Enter your telephone..."
type="tel"
{...field}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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
params={{
...form.getValues(),
type: 2,
}}
type="phone"
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your new password..."
type="password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{verify.enable_reset_password_verify && (
<FormField
control={form.control}
name="cf_token"
render={({ field }) => (
<FormItem>
<FormControl>
<CloudFlareTurnstile
id="reset"
{...field}
ref={turnstile}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<Button disabled={loading} type="submit">
{loading && <Icon className="animate-spin" icon="mdi:loading" />}
{t("reset.title", "Reset Password")}
</Button>
</form>
</Form>
<div className="mt-4 text-right text-sm">
{t("reset.existingAccount", "Remember your password?")}&nbsp;
<Button
className="p-0"
onClick={() => {
onSwitchForm("login");
}}
variant="link"
>
{t("reset.switchToLogin", "Login")}
</Button>
</div>
</>
);
}
+108
View File
@@ -0,0 +1,108 @@
"use client";
import { Button } from "@workspace/ui/components/button";
import {
sendEmailCode,
sendSmsCode,
} from "@workspace/ui/services/common/common";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useGlobalStore } from "@/stores/global";
interface SendCodeProps {
type: "email" | "phone";
params: {
email?: string;
type?: 1 | 2;
telephone_area_code?: string;
telephone?: string;
};
}
export default function SendCode({ type, params }: SendCodeProps) {
const { t } = useTranslation("auth");
const { common } = useGlobalStore();
const { verify_code_interval } = common.verify_code;
const [targetDate, setTargetDate] = useState<number>();
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const storedEndTime = localStorage.getItem(`verify_code_${type}`);
if (storedEndTime) {
const endTime = Number.parseInt(storedEndTime, 10);
if (endTime > Date.now()) {
setTargetDate(endTime);
} else {
localStorage.removeItem(`verify_code_${type}`);
}
}
}, [type]);
useEffect(() => {
if (!targetDate) {
setSeconds(0);
return;
}
const updateTimer = () => {
const now = Date.now();
const remaining = Math.max(0, Math.ceil((targetDate - now) / 1000));
setSeconds(remaining);
if (remaining === 0) {
setTargetDate(undefined);
localStorage.removeItem(`verify_code_${type}`);
}
};
updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [targetDate, type]);
const setCodeTimer = () => {
const endTime = Date.now() + verify_code_interval * 1000;
setTargetDate(endTime);
localStorage.setItem(`verify_code_${type}`, endTime.toString());
};
const getEmailCode = async () => {
if (params.email && params.type) {
await sendEmailCode({
email: params.email,
type: params.type,
});
setCodeTimer();
}
};
const getPhoneCode = async () => {
if (params.telephone && params.telephone_area_code && params.type) {
await sendSmsCode({
telephone: params.telephone,
telephone_area_code: params.telephone_area_code,
type: params.type,
});
setCodeTimer();
}
};
const handleSendCode = async () => {
if (type === "email") {
getEmailCode();
} else {
getPhoneCode();
}
};
const disabled =
seconds > 0 ||
(type === "email"
? !params.email
: !(params.telephone && params.telephone_area_code));
return (
<Button disabled={disabled} onClick={handleSendCode} type="button">
{seconds > 0 ? `${seconds}s` : t("get", "Get Code")}
</Button>
);
}
+68
View File
@@ -0,0 +1,68 @@
"use client";
import { useTheme } from "next-themes";
import { forwardRef, useEffect, useImperativeHandle } from "react";
import { useTranslation } from "react-i18next";
import Turnstile, { useTurnstile } from "react-turnstile";
import { useGlobalStore } from "@/stores/global";
export type TurnstileRef = {
reset: () => void;
};
const CloudFlareTurnstile = forwardRef<
TurnstileRef,
{
id?: string;
value?: null | string;
onChange: (value?: string) => void;
}
>(function CloudFlareTurnstile({ id, value, onChange }, ref) {
const { common } = useGlobalStore();
const { verify } = common;
const { resolvedTheme } = useTheme();
const { i18n } = useTranslation();
const locale = i18n.language;
const turnstile = useTurnstile();
useImperativeHandle(
ref,
() => ({
reset: () => turnstile.reset(),
}),
[turnstile]
);
useEffect(() => {
if (value === "") {
turnstile.reset();
}
}, [turnstile, value]);
return (
verify.turnstile_site_key && (
<Turnstile
fixedSize
id={id}
language={locale.toLowerCase()}
onExpire={() => {
onChange();
turnstile.reset();
}}
onTimeout={() => {
onChange();
turnstile.reset();
}}
onVerify={(token) => onChange(token)}
// onError={() => {
// onChange();
// turnstile.reset();
// }}
sitekey={verify.turnstile_site_key}
theme={resolvedTheme as "light" | "dark"}
/>
)
);
});
export default CloudFlareTurnstile;

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