🎉 feat: initialization
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
count.txt
|
||||
.env
|
||||
.nitro
|
||||
.tanstack
|
||||
.wrangler
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||

|
||||
|
||||
</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:
|
||||
|
||||
[][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
|
||||
|
||||
[](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 you’re 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
|
||||
@@ -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]
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary><kbd>目录</kbd></summary>
|
||||
|
||||
#### 目录
|
||||
|
||||
- [⌨️ 本地开发](#️-本地开发)
|
||||
- [🚀 在 Vercel 上部署](#-在-vercel-上部署)
|
||||
- [🤝 贡献](#-贡献)
|
||||
- [📝 许可证](#-许可证)
|
||||
|
||||
####
|
||||
|
||||
</details>
|
||||
|
||||
## ⌨️ 本地开发
|
||||
|
||||
您可以使用 Github Codespaces 进行在线开发:
|
||||
|
||||
[![][codespaces-shield]][codespaces-link]
|
||||
|
||||
您可以使用 Gitpod 进行在线开发:
|
||||
|
||||
[][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 上部署
|
||||
|
||||
[](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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "用户标识"
|
||||
}
|
||||
@@ -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": "邀请码"
|
||||
}
|
||||
@@ -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": "年"
|
||||
}
|
||||
@@ -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": "减少"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
@@ -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 />
|
||||
</>
|
||||
),
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -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?")}
|
||||
<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?")}
|
||||
<Button
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
setInitialValues(undefined);
|
||||
onSwitchForm("login");
|
||||
}}
|
||||
variant="link"
|
||||
>
|
||||
{t("reset.switchToLogin", "Login")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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?")}
|
||||
<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?")}
|
||||
<Button
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
onSwitchForm("login");
|
||||
}}
|
||||
variant="link"
|
||||
>
|
||||
{t("reset.switchToLogin", "Login")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||